Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

lastfm_scrobbler fix formatting, progress messages, export mode

make the plugin context aware to allow user to one
click export from a shortcut, basically it checks if its running in the
file browser if elsewhere does auto export

use a buffer for the fixed portion of the scrobble data

add timed_yield to long running operations
add progress bars and messages as scanning for duplicates takes a while
speed up duplicate scanning 4x

1000 tracks processing took ~2 minutes with duplicate scanning down from ~5

previous patch makes playback logging create a new file after 512k
old files renamed playback_0001.log
make scrobbler able to parse these log files

add ability to skip tracks without metadata and count them

Remove duplicates only for resume or remove all duplicates

allow skipping tracks without metadata or save only filename

update manual

Change-Id: I115bcfd3381f5a978252aac1cdfcc080f0797dda

authored by

William Wilgus and committed by
William Wilgus
dad8f163 731f3fd8

+649 -321
+612 -306
apps/plugins/lastfm_scrobbler.c
··· 70 70 #include "plugin.h" 71 71 #include "lib/configfile.h" 72 72 73 + #define MIN_ELAPSED_MS (500) /*should not be lower than value found in playback.c*/ 74 + 73 75 #ifndef UNTAGGED 74 76 #define UNTAGGED "<UNTAGGED>" 75 77 #endif ··· 92 94 /* longest entry I've had is 323, add a safety margin */ 93 95 #define SCROBBLER_MAXENTRY_LEN (MAX_PATH + 60 + 10) 94 96 95 - #define ITEM_HDR "#ARTIST #ALBUM #TITLE #TRACKNUM #LENGTH #RATING #TIMESTAMP #MUSICBRAINZ_TRACKID\n" 97 + #define ITEM_HDR "#ARTIST\t#ALBUM\t#TITLE\t#TRACKNUM\t#LENGTH\t#RATING\t#TIMESTAMP\t#MUSICBRAINZ_TRACKID\n" 96 98 97 99 #define CFG_FILE "/lastfm_scrobbler.cfg" 98 100 #define CFG_VER 3 99 101 102 + #define SCROBBLER_MENU (PLUGIN_OK + 1) 103 + #define SCROBBLER_LOG_OK (PLUGIN_OK + 2) 104 + #define SCROBBLER_LOG_NOMETADATA (PLUGIN_OK + 3) 105 + #define SCROBBLER_LOG_SKIPTRACK (PLUGIN_OK + 4) 106 + #define SCROBBLER_LOG_ERROR (PLUGIN_ERROR) 107 + 100 108 #if CONFIG_RTC 101 109 #define BASE_FILENAME HOME_DIR "/.scrobbler.log" 102 110 #define HDR_STR_TIMELESS ··· 105 113 #define BASE_FILENAME HOME_DIR "/.scrobbler-timeless.log" 106 114 #endif /* CONFIG_RTC */ 107 115 108 - /****************** prototypes ******************/ 116 + #define PLAYBACK_LOG "playback" 117 + #define PLAYBACK_LOG_DIR ROCKBOX_DIR 118 + 119 + /****************** prototypes / globals ******************/ 109 120 enum plugin_status plugin_start(const void* parameter); /* entry */ 110 121 static int view_playback_log(void); 111 - static int export_scrobbler_file(void); 122 + static int sbl_export(void); 112 123 113 124 struct scrobbler_entry 114 125 { 115 126 unsigned long timestamp; 116 127 unsigned long elapsed; 117 128 unsigned long length; 118 - char *path; 129 + const char *path; 119 130 }; 120 131 121 132 static struct scrobbler_cfg 122 133 { 123 134 int savepct; 124 135 int minms; 125 - bool remove_dup; 136 + int tracknfo; 137 + int remove_dup; 126 138 bool delete_log; 127 - 128 139 } gConfig; 129 140 141 + static struct opt_items tracknfo_option[3] = { 142 + {ID2P(LANG_ALL), -1 }, 143 + { "Skip if Missing", -1}, 144 + {ID2P(LANG_DISPLAY_TRACK_NAME_ONLY), -1}, 145 + }; 146 + static char *tracknfo_strs[] = {"Save All", "Skip if Missing", "Filename Only"}; 147 + 148 + static struct opt_items dup_option[3] = { 149 + {ID2P(LANG_OFF), -1 }, 150 + {ID2P(LANG_RESUME_PLAYBACK), -1}, 151 + {ID2P(LANG_ALL), -1}, 152 + }; 153 + static char *dup_strs[] = {"Save All", "No Resume Duplicates", "No Duplicates"}; 154 + 130 155 static struct configdata config[] = 131 156 { 132 - {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.remove_dup }, "RemoveDupes", NULL}, 133 - {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.delete_log }, "DeleteLog", NULL}, 134 - {TYPE_INT, 0, 100, { .int_p = &gConfig.savepct }, "SavePct", NULL}, 135 - {TYPE_INT, 0, 10000, { .int_p = &gConfig.minms }, "MinMs", NULL}, 157 + {TYPE_ENUM, 0, 3, { .int_p = &gConfig.tracknfo }, "TrackInfo", tracknfo_strs}, 158 + {TYPE_ENUM, 0, 3, { .int_p = &gConfig.remove_dup }, "RemoveDupes", dup_strs}, 159 + {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.delete_log }, "DeleteLog", NULL}, 160 + {TYPE_INT, 0, 100, { .int_p = &gConfig.savepct }, "SavePct", NULL}, 161 + {TYPE_INT, MIN_ELAPSED_MS, 10000, { .int_p = &gConfig.minms }, "MinMs", NULL}, 136 162 }; 137 163 const int gCfg_sz = sizeof(config)/sizeof(*config); 164 + static long yield_tick = 0; 138 165 139 166 /****************** config functions *****************/ 140 167 static void config_set_defaults(void) 141 168 { 142 169 gConfig.savepct = 50; 143 - gConfig.minms = 500; 144 - gConfig.remove_dup = true; 170 + gConfig.minms = MIN_ELAPSED_MS; 171 + gConfig.remove_dup = 0; /* save all*/ 145 172 gConfig.delete_log = true; 173 + gConfig.tracknfo = 0; /* save all */ 146 174 } 147 175 148 - static int scrobbler_menu(bool resume) 176 + static int scrobbler_menu_action(int selection, bool has_log) 149 177 { 150 - int selection = resume ? 5 : 0; /* if resume we are returning from log view */ 178 + logf("%s sel: %d log: %d", __func__, selection, has_log); 151 179 180 + int res; 152 181 static uint32_t crc = 0; 182 + if (crc == 0) 183 + { 184 + crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); 185 + } 186 + 187 + switch(selection) 188 + { 189 + case 0: /* 0: all saved 1: no resume duplicates 2: no duplicates */ 190 + rb->set_option("Remove log duplicates", &gConfig.remove_dup, RB_INT, 191 + dup_option, 3, NULL); 192 + break; 193 + case 1: /* delete log */ 194 + rb->set_bool("Delete playback log", &gConfig.delete_log); 195 + break; 196 + case 2: /* % of track played to indicate listened status */ 197 + rb->set_int(ID2P(LANG_COMPRESSOR_THRESHOLD), "%", UNIT_PERCENT, 198 + &gConfig.savepct, NULL, 10, 0, 100, NULL ); 199 + break; 200 + case 3: /* tracks played less than this will not be logged */ 201 + rb->set_int("Minimum Elapsed", "ms", UNIT_MS, 202 + &gConfig.minms, NULL, 100, MIN_ELAPSED_MS, 10000, NULL ); 203 + break; 204 + case 4: /* 0: all saved 1: without metadata skipped 2: only filename saved*/ 205 + rb->set_option(ID2P(LANG_TRACK_INFO), &gConfig.tracknfo, RB_INT, 206 + tracknfo_option, 3, NULL); 207 + break; 208 + case 5: /* sep */ 209 + break; 210 + case 6: /* view playback log */ 211 + if (crc != rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF)) 212 + { 213 + /* there are changes to save */ 214 + if (!rb->yesno_pop(ID2P(LANG_SAVE_CHANGES))) 215 + { 216 + return view_playback_log(); 217 + } 218 + } 219 + res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 220 + if (res >= 0) 221 + { 222 + crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); 223 + logf("SCROBBLER: cfg saved %s %d bytes", CFG_FILE, gCfg_sz); 224 + } 225 + return view_playback_log(); 226 + break; 227 + case 7: /* set defaults */ 228 + { 229 + const struct text_message prompt = { 230 + (const char*[]){ ID2P(LANG_AUDIOSCROBBLER), 231 + ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)}, 2}; 232 + if(rb->gui_syncyesno_run(&prompt, NULL, NULL) == YESNO_YES) 233 + { 234 + config_set_defaults(); 235 + rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); 236 + } 237 + break; 238 + } 239 + case 8: /*sep*/ 240 + break; 241 + case 9: /* Cancel */ 242 + has_log = false; 243 + if (crc != rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF)) 244 + { 245 + /* there are changes to save */ 246 + if (!rb->yesno_pop(ID2P(LANG_SAVE_CHANGES))) 247 + { 248 + return -1; 249 + } 250 + } 251 + case 10: /* Export & exit */ 252 + { 253 + res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 254 + if (res >= 0) 255 + { 256 + logf("SCROBBLER: cfg saved %s %d bytes", CFG_FILE, gCfg_sz); 257 + } 258 + else 259 + { 260 + rb->splash(HZ*2, ID2P(LANG_ERROR_WRITING_CONFIG)); 261 + logf("SCROBBLER: %s (%d) %s", "Error writing config", res, CFG_FILE); 262 + } 263 + #if defined(HAVE_ADJUSTABLE_CPU_FREQ) 264 + if (has_log) 265 + { 266 + rb->cpu_boost(true); 267 + return sbl_export(); 268 + rb->cpu_boost(false); 269 + } 270 + #else 271 + if (has_log) 272 + return sbl_export(); 273 + #endif 274 + return PLUGIN_OK; 275 + } 276 + case MENU_ATTACHED_USB: 277 + return PLUGIN_USB_CONNECTED; 278 + default: 279 + return PLUGIN_OK; 280 + } 281 + return SCROBBLER_MENU; 282 + } 283 + 284 + static int scrobbler_menu(bool resume) 285 + { 286 + int selection = resume ? 5 : 0; /* if resume we are returning from log view */ 153 287 154 288 struct viewport parentvp[NB_SCREENS]; 155 289 FOR_NB_SCREENS(l) ··· 158 292 rb->viewport_set_fullscreen(&parentvp[l], l); 159 293 } 160 294 161 - #define MENUITEM_STRINGLIST_CUSTOM(name, str, callback, ... ) \ 295 + #define MENUITEM_STRINGLIST_CUSTOM(name, str, callback, ... ) \ 162 296 static const char *name##_[] = {__VA_ARGS__}; \ 163 297 static const struct menu_callback_with_desc name##__ = \ 164 298 {callback,str, Icon_NOICON}; \ 165 - struct menu_item_ex name = \ 299 + struct menu_item_ex name = \ 166 300 {MT_RETURN_ID|MENU_HAS_DESC| \ 167 301 MENU_ITEM_COUNT(sizeof( name##_)/sizeof(*name##_)), \ 168 302 { .strings = name##_},{.callback_and_desc = & name##__}}; ··· 170 304 MENUITEM_STRINGLIST_CUSTOM(settings_menu, ID2P(LANG_AUDIOSCROBBLER), NULL, 171 305 "Remove duplicates", 172 306 "Delete playback log", 173 - "Save threshold", 307 + ID2P(LANG_COMPRESSOR_THRESHOLD), 174 308 "Minimum elapsed", 309 + ID2P(LANG_TRACK_INFO), //Skip tracks without metadata 175 310 ID2P(VOICE_BLANK), 176 311 ID2P(LANG_VIEWLOG), 177 312 ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS), ··· 184 319 int res; 185 320 const int items = MENU_GET_COUNT(settings_menu.flags); 186 321 const unsigned int flags = settings_menu.flags & (~MENU_ITEM_COUNT(MENU_COUNT_MASK)); 187 - if (crc == 0) 188 - { 189 - crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); 190 - } 322 + 191 323 192 - bool has_log = rb->file_exists(ROCKBOX_DIR "/playback.log"); 324 + bool has_log = (rb->file_exists(ROCKBOX_DIR "/playback.log") 325 + || rb->file_exists(ROCKBOX_DIR "/playback_0001.log")); 193 326 194 327 do { 195 328 if (!has_log) ··· 202 335 settings_menu.flags = flags|MENU_ITEM_COUNT(items); 203 336 } 204 337 selection=rb->do_menu(&settings_menu,&selection, parentvp, true); 205 - switch(selection) { 206 338 207 - case 0: /* remove duplicates */ 208 - rb->set_bool("Remove log duplicates", &gConfig.remove_dup); 209 - break; 210 - case 1: /* delete log */ 211 - rb->set_bool("Delete playback log", &gConfig.delete_log); 212 - break; 213 - case 2: /* % of track played to indicate listened status */ 214 - rb->set_int("Save Threshold", "%", UNIT_PERCENT, 215 - &gConfig.savepct, NULL, 10, 0, 100, NULL ); 216 - break; 217 - case 3: /* tracks played less than this will not be logged */ 218 - rb->set_int("Minimum Elapsed", "ms", UNIT_MS, 219 - &gConfig.minms, NULL, 100, 0, 10000, NULL ); 220 - break; 221 - case 4: /* sep */ 222 - break; 223 - case 5: /* view playback log */ 224 - if (crc != rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF)) 225 - { 226 - /* there are changes to save */ 227 - if (!rb->yesno_pop(ID2P(LANG_SAVE_CHANGES))) 228 - { 229 - return view_playback_log(); 230 - } 231 - } 232 - res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 233 - if (res >= 0) 234 - { 235 - crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); 236 - logf("SCROBBLER: cfg saved %s %d bytes", CFG_FILE, gCfg_sz); 237 - } 238 - return view_playback_log(); 239 - break; 240 - case 6: /* set defaults */ 241 - { 242 - const struct text_message prompt = { 243 - (const char*[]){ ID2P(LANG_AUDIOSCROBBLER), 244 - ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)}, 2}; 245 - if(rb->gui_syncyesno_run(&prompt, NULL, NULL) == YESNO_YES) 246 - { 247 - config_set_defaults(); 248 - rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); 249 - } 250 - break; 251 - } 252 - case 7: /*sep*/ 253 - continue; 254 - case 8: /* Cancel */ 255 - has_log = false; 256 - if (crc != rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF)) 257 - { 258 - /* there are changes to save */ 259 - if (!rb->yesno_pop(ID2P(LANG_SAVE_CHANGES))) 260 - { 261 - return -1; 262 - } 263 - } 264 - case 9: /* Export & exit */ 265 - { 266 - res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 267 - if (res >= 0) 268 - { 269 - logf("SCROBBLER: cfg saved %s %d bytes", CFG_FILE, gCfg_sz); 270 - } 271 - else 272 - { 273 - logf("SCROBBLER: cfg FAILED (%d) %s", res, CFG_FILE); 274 - } 275 - #if defined(HAVE_ADJUSTABLE_CPU_FREQ) 276 - if (has_log) 277 - { 278 - rb->cpu_boost(true); 279 - return export_scrobbler_file(); 280 - rb->cpu_boost(false); 281 - } 282 - #else 283 - if (has_log) 284 - return export_scrobbler_file(); 285 - #endif 286 - return PLUGIN_OK; 287 - } 288 - case MENU_ATTACHED_USB: 289 - return PLUGIN_USB_CONNECTED; 290 - default: 291 - return PLUGIN_OK; 292 - } 339 + res = scrobbler_menu_action(selection, has_log); 340 + if (res != SCROBBLER_MENU) 341 + return res; 342 + 293 343 } while ( selection >= 0 ); 294 344 return 0; 295 345 } 296 346 297 347 /****************** helper fuctions ******************/ 348 + static void ask_enable_playbacklog(void) 349 + { 350 + const char *lines[]={"LastFm", "Playback logging required", "Enable?"}; 351 + const char *response[]= { 352 + "Playback Settings:", "Logging: Enabled", 353 + "Playback Settings:", "Logging: Disabled" 354 + }; 355 + const struct text_message message= {lines, 3}; 356 + const struct text_message yes_msg= {&response[0], 2}; 357 + const struct text_message no_msg= {&response[2], 2}; 358 + if(rb->gui_syncyesno_run(&message, &yes_msg, &no_msg) == YESNO_YES) 359 + { 360 + rb->global_settings->playback_log = true; 361 + rb->settings_save(); 362 + rb->sleep(HZ * 2); 363 + } 364 + } 298 365 299 - static inline const char* str_chk_valid(const char *s, const char *alt) 366 + static int view_playback_log(void) 300 367 { 301 - return (s != NULL ? s : alt); 368 + const char* plugin = VIEWERS_DIR "/lastfm_scrobbler_viewer.rock"; 369 + /*rb->splashf(100, "Opening %s", plugin);*/ 370 + if (rb->file_exists(plugin)) 371 + { 372 + return rb->plugin_open(plugin, "-scrobbler_view_pbl"); 373 + } 374 + return PLUGIN_ERROR; 302 375 } 303 376 304 - static void scrobbler_get_filename(char *path, size_t size) 377 + static inline bool do_timed_yield(void) 305 378 { 306 - int used; 379 + /* Exporting can lock up for quite a while, so yield occasionally */ 380 + if (TIME_AFTER(*rb->current_tick, yield_tick)) 381 + { 382 + rb->yield(); 383 + 384 + yield_tick = *rb->current_tick + (HZ/5); 385 + return true; 386 + } 387 + return false; 388 + } 307 389 308 - used = rb->snprintf(path, size, "/%s", BASE_FILENAME); 390 + static void clear_display(void) 391 + { 392 + static struct gui_synclist lists = {0}; 393 + rb->lcd_clear_display(); 394 + #ifdef HAVE_REMOTE_LCD 395 + rb->lcd_remote_clear_display(); 396 + #endif 309 397 310 - if (used >= (int)size) 398 + if (!lists.title) /* initialize the list, only used to display the title..*/ 311 399 { 312 - logf("%s: not enough buffer space for log filename", __func__); 313 - rb->memset(path, 0, size); 400 + rb->gui_synclist_init(&lists, NULL, NULL, false,1, NULL); 401 + rb->gui_synclist_set_title(&lists, rb->str(LANG_AUDIOSCROBBLER), Icon_Moving); 314 402 } 403 + 404 + rb->gui_synclist_draw(&lists); 315 405 } 316 406 317 - static unsigned long scrobbler_get_threshold(unsigned long length_ms) 407 + static inline const char* str_chk_valid(char *s, const char *alt) 408 + { 409 + if (s == NULL || *s == '\0') 410 + return alt; 411 + char *sep = s; 412 + /* strip tabs from the incoming string */ 413 + while ((sep = rb->strchr(sep, '\t')) != NULL) 414 + *sep = ' '; 415 + 416 + return s; 417 + } 418 + 419 + static unsigned long sbl_get_threshold(unsigned long length_ms) 318 420 { 319 421 /* length is assumed to be in miliseconds */ 320 422 return length_ms / 100 * gConfig.savepct; 321 423 } 322 424 323 - static int create_log_entry(struct scrobbler_entry *entry, int output_fd) 425 + static int sbl_create_entry(struct scrobbler_entry *entry, int output_fd) 324 426 { 427 + /* creates a scrobbler log entry and writes it to the (opened) output_fd */ 325 428 #define SEP "\t" 326 429 #define EOL "\n" 327 430 struct mp3entry id3, *id; 328 - char *path = rb->strrchr(entry->path, '/'); 431 + const char *path = rb->strrchr(entry->path, '/'); 329 432 if (!path) 330 433 path = entry->path; 331 434 else 332 435 path++; /* remove slash */ 333 436 char rating = 'S'; /* Skipped */ 334 - if (entry->elapsed >= scrobbler_get_threshold(entry->length)) 437 + if (entry->elapsed >= sbl_get_threshold(entry->length)) 335 438 rating = 'L'; /* Listened */ 336 439 337 440 #if (CONFIG_RTC) ··· 339 442 #else 340 443 unsigned long timestamp = 0U; 341 444 #endif 445 + /*lllllllllllllllllllllSlllllllllllllllllllllSrSlllllllllllllllllllllSgggggggg-uuuu-iiii-dddd-eeeeeeeeeeeeN0*/ 446 + static char track_len_rate_timestamp_mbid[128]; 447 + /*106 chars enough for 3 64 bit numbers (l), 4 (S)eparators, 448 + * 1 (r)ating char and 36 char GUIDe :-), (N)ewline and NULL (0) */ 342 449 343 - if (!rb->get_metadata(&id3, -1, entry->path)) 450 + if (output_fd < 0) 451 + return SCROBBLER_LOG_ERROR; 452 + 453 + /* if we are saving filename only then we don't need metadata */ 454 + if (gConfig.tracknfo == 2 || !rb->get_metadata(&id3, -1, entry->path)) 344 455 { 456 + if (gConfig.tracknfo == 1) /* user doesn't want tracks missing metadata*/ 457 + return SCROBBLER_LOG_SKIPTRACK; 345 458 /* failure to read metadata not fatal, write what we have */ 346 - rb->fdprintf(output_fd, 347 - "%s"SEP"%s"SEP"%s"SEP"%s"SEP"%d"SEP"%c"SEP"%lu"SEP"%s"EOL"", 348 - "", 349 - "", 350 - path, 351 - "-1", 352 - (int)(entry->length / 1000), 353 - rating, 354 - timestamp, 355 - ""); 356 - return PLUGIN_OK; 459 + rb->snprintf(track_len_rate_timestamp_mbid, sizeof(track_len_rate_timestamp_mbid), 460 + "%d"SEP"%ld"SEP"%c"SEP"%lu"SEP"%s"EOL"", 461 + -1, (long)(entry->length / 1000), rating, timestamp, ""); 462 + 463 + rb->fdprintf(output_fd,"%s"SEP"%s"SEP"%s"SEP"%s", 464 + UNTAGGED, 465 + "", 466 + path, 467 + track_len_rate_timestamp_mbid); 468 + return SCROBBLER_LOG_NOMETADATA; 357 469 } 358 - if (!output_fd) 359 - return PLUGIN_ERROR; 470 + 360 471 id = &id3; 361 472 362 473 char* artist = id->artist ? id->artist : id->albumartist; 363 474 364 - char tracknum[11] = { "" }; 475 + int track = id->tracknum; 365 476 366 - if (id->tracknum > 0) 367 - rb->snprintf(tracknum, sizeof (tracknum), "%d", id->tracknum); 477 + if (track < -1) 478 + track = -1; 368 479 369 - rb->fdprintf(output_fd, 370 - "%s"SEP"%s"SEP"%s"SEP"%s"SEP"%d"SEP"%c"SEP"%lu"SEP"%s"EOL"", 371 - str_chk_valid(artist, UNTAGGED), 372 - str_chk_valid(id->album, ""), 373 - str_chk_valid(id->title, path), 374 - tracknum, 375 - (int)(entry->length / 1000), 376 - rating, 377 - timestamp, 378 - str_chk_valid(id->mb_track_id, "")); 480 + rb->snprintf(track_len_rate_timestamp_mbid, sizeof(track_len_rate_timestamp_mbid), 481 + "%d"SEP"%ld"SEP"%c"SEP"%lu"SEP"%s"EOL"", track, (long)(entry->length / 1000), 482 + rating, timestamp, str_chk_valid(id->mb_track_id, "")); 483 + 484 + rb->fdprintf(output_fd,"%s"SEP"%s"SEP"%s"SEP"%s", 485 + str_chk_valid(artist, UNTAGGED), 486 + str_chk_valid(id->album, ""), 487 + str_chk_valid(id->title, path), 488 + track_len_rate_timestamp_mbid); 379 489 #undef SEP 380 490 #undef EOL 381 - return PLUGIN_OK; 491 + return SCROBBLER_LOG_OK; 382 492 } 383 493 384 - static void ask_enable_playbacklog(void) 494 + static int sbl_check_or_open(bool check_only) 385 495 { 386 - const char *lines[]={"LastFm", "Playback logging required", "Enable?"}; 387 - const char *response[]= { 388 - "Playback Settings:", "Logging: Enabled", 389 - "Playback Settings:", "Logging: Disabled" 390 - }; 391 - const struct text_message message= {lines, 3}; 392 - const struct text_message yes_msg= {&response[0], 2}; 393 - const struct text_message no_msg= {&response[2], 2}; 394 - if(rb->gui_syncyesno_run(&message, &yes_msg, &no_msg) == YESNO_YES) 496 + /* checks if scrobbler log exists and if !check_only creates it (if needed) 497 + * and returns handle */ 498 + char scrobbler_file[MAX_PATH]; 499 + int fd; 500 + int used; 501 + used = rb->snprintf(scrobbler_file, sizeof(scrobbler_file), "/%s", BASE_FILENAME); 502 + 503 + if (used <= 0 || used >= (int)sizeof(scrobbler_file)) 395 504 { 396 - rb->global_settings->playback_log = true; 397 - rb->settings_save(); 398 - rb->sleep(HZ * 2); 505 + logf("%s: not enough buffer space for log filename", __func__); 506 + rb->memset(scrobbler_file, 0, sizeof(scrobbler_file)); 399 507 } 400 - } 401 - 402 - static int view_playback_log(void) 403 - { 404 - const char* plugin = VIEWERS_DIR "/lastfm_scrobbler_viewer.rock"; 405 - rb->splashf(100, "Opening %s", plugin); 406 - if (rb->file_exists(plugin)) 508 + else 407 509 { 408 - return rb->plugin_open(plugin, "-scrobbler_view_pbl"); 510 + char *p = scrobbler_file; 511 + while(p[0] == '/' && p[1] == '/') 512 + p++; 513 + if (p != scrobbler_file) 514 + rb->memmove(scrobbler_file, p, rb->strlen(p) + 1); 409 515 } 410 - return PLUGIN_ERROR; 411 - } 412 516 413 - static int open_create_scrobbler_log(void) 414 - { 415 - int fd; 416 - char scrobbler_file[MAX_PATH]; 517 + logf("%s: log filename '%s'", __func__, scrobbler_file); 417 518 418 - scrobbler_get_filename(scrobbler_file, sizeof(scrobbler_file)); 519 + bool exists = rb->file_exists(scrobbler_file); 520 + 521 + if (check_only) 522 + return exists ? 1: 0; 419 523 420 524 /* If the file doesn't exist, create it. */ 421 - if(!rb->file_exists(scrobbler_file)) 525 + if(!exists) 422 526 { 423 527 fd = rb->open(scrobbler_file, O_WRONLY | O_CREAT, 0666); 424 528 if(fd >= 0) ··· 436 540 } 437 541 } 438 542 else 439 - fd = rb->open(scrobbler_file, O_WRONLY | O_APPEND); 543 + fd = rb->open_utf8(scrobbler_file, O_WRONLY | O_APPEND); 440 544 441 545 return fd; 442 546 } 443 547 444 - static bool playbacklog_parse_entry(struct scrobbler_entry *entry, char *begin) 548 + static int sbl_open_create(void) 445 549 { 446 - char *sep; 447 - memset(entry, 0, sizeof(*entry)); 550 + return sbl_check_or_open(false); 551 + } 448 552 449 - sep = rb->strchr(begin, ':'); 450 - if (!sep) 451 - return false; 553 + unsigned long pbl_parse_atoul_wlen(const char *str, size_t *len) 554 + { 452 555 453 - entry->timestamp = rb->atoi(begin); 556 + /* we don't need much for atoi no negatives & minimal leading zeros 557 + there are no checks for NULL pointer either */ 558 + unsigned long d, value = 0; 559 + *len = 0; 560 + while (str[*len]) 561 + { 562 + d = str[*len] - '0'; 563 + if (d < 10) 564 + { 565 + value = (value * 10) + d; 566 + } 567 + else if (value != 0) 568 + break; 569 + *len = *len + 1; 570 + } 571 + /*logf("atoul\n in '%s'\nout: %lu len: %lu", str, value, *len);*/ 572 + return value; 573 + } 454 574 455 - begin = sep + 1; 456 - sep = rb->strchr(begin, ':'); 457 - if (!sep) 458 - return false; 575 + unsigned long pbl_parse_atoul(const char *str) 576 + { 577 + /* we don't need much for atoi no negatives & minimal leading zeros 578 + there are no checks for NULL pointer either */ 579 + size_t len; 580 + return pbl_parse_atoul_wlen(str, &len); 581 + } 459 582 460 - entry->elapsed = rb->atoi(begin); 583 + static bool pbl_parse_entry(struct scrobbler_entry *entry, const char *begin) 584 + { 585 + /* get a playbacklog entry returns true if valid, false otherwise */ 586 + const char *p_time = begin; 587 + if (!p_time) 588 + goto failure; 589 + const char *p_elapsed = rb->strchr(p_time, ':'); 590 + if (!p_elapsed) 591 + goto failure; 592 + const char *p_length = rb->strchr(++p_elapsed, ':'); 593 + if (!p_length) 594 + goto failure; 595 + const char *p_path = rb->strchr(++p_length, ':'); 596 + if (!p_path || *(++p_path) == '\0') 597 + goto failure; 461 598 462 - begin = sep + 1; 463 - sep = rb->strchr(begin, ':'); 464 - if (!sep) 465 - return false; 466 - 467 - entry->length = rb->atoi(begin); 468 - 469 - begin = sep + 1; 470 - if (*begin == '\0') 471 - return false; 472 - 473 - entry->path = begin; 599 + entry->timestamp = pbl_parse_atoul(p_time); 600 + entry->elapsed = pbl_parse_atoul(p_elapsed); 601 + entry->length = pbl_parse_atoul(p_length); 602 + entry->path = p_path; 474 603 475 604 if (entry->length == 0 || entry->elapsed > entry->length) 476 605 { 477 - return false; 606 + goto failure; 478 607 } 479 608 return true; /* success */ 609 + 610 + failure: 611 + memset(entry, 0, sizeof(*entry)); 612 + return false; 613 + } 614 + 615 + static void pbl_parse_valid_entry(struct scrobbler_entry *entry, const char *begin) 616 + { 617 + /* Same as above but -- No error checking - only for pre-parsed entries */ 618 + size_t len; 619 + entry->timestamp = pbl_parse_atoul_wlen(begin, &len); 620 + begin += len + 1; /* skip ':' */ 621 + entry->elapsed = pbl_parse_atoul_wlen(begin, &len); 622 + begin += len + 1; /* skip ':' */ 623 + entry->length = pbl_parse_atoul_wlen(begin, &len); 624 + begin += len + 1; /* skip ':' */ 625 + entry->path = begin; 626 + /* success */ 480 627 } 481 628 482 629 static inline bool pbl_cull_duplicates(int fd, struct scrobbler_entry *curentry, 483 - int cur_line, char*buf, size_t bufsz) 630 + int pos, int cur_line, char*buf, size_t bufsz, int *dup) 484 631 { 485 - /* child function of remove_duplicates */ 486 - int line_num = cur_line; 487 - int rd, start_pos, pos; 632 + /* worker for remove_duplicates */ 633 + int line_num = cur_line; /* Note count skips empty lines\comments */ 634 + int rd, start_pos; 488 635 struct scrobbler_entry compare; 489 - pos = rb->lseek(fd, 0, SEEK_CUR); 636 + bool b2b = gConfig.remove_dup == 2; /* No duplicates */ 637 + 490 638 while(1) 491 639 { 492 640 if ((rd = rb->read_line(fd, buf, bufsz)) <= 0) 493 641 break; /* EOF */ 494 642 643 + if (buf[0] != ' ') /* skip comments and empty lines */ 644 + { 645 + rb->yield(); 646 + pos += rd; 647 + continue; 648 + } 649 + 495 650 /* save start of entry in case we need to remove it */ 496 651 start_pos = pos; 497 652 pos += rd; 498 653 line_num++; 499 - if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ 500 - continue; 501 - if (!playbacklog_parse_entry(&compare, buf)) 502 - continue; 503 654 504 - rb->yield(); 655 + pbl_parse_valid_entry(&compare, buf); 505 656 506 657 unsigned long length = compare.length; 507 658 if (curentry->length != length 508 659 || rb->strcmp(curentry->path, compare.path) != 0) 660 + { 661 + rb->yield(); 509 662 continue; /* different track */ 510 - 663 + } 664 + b2b |= (cur_line + 1 == line_num); 511 665 /* if this is two distinct plays keep both */ 512 - if ((cur_line + 1 == line_num) /* unless back to back then its probably a resume */ 666 + if (b2b /* unless back to back then its probably a resume */ 513 667 || (curentry->timestamp <= compare.timestamp + length 514 668 && compare.timestamp <= curentry->timestamp + length)) 515 669 { 516 - 517 670 if (curentry->elapsed >= compare.elapsed) 518 671 { 519 672 /* compare entry is not the greatest elapsed */ 520 673 /*logf("entry %s (%lu) @ %d culled\n", compare.path, compare.elapsed, line_num);*/ 521 674 rb->lseek(fd, start_pos, SEEK_SET); 522 - rb->write(fd, "#", 1); /* make this entry a comment */ 675 + rb->write(fd, "@", 1); /* skip this entry */ 523 676 rb->lseek(fd, pos, SEEK_SET); 677 + (*dup)++; 678 + rb->yield(); 524 679 } 525 680 else 526 681 { ··· 532 687 return true; /* this item is unique or the greatest elapsed */ 533 688 } 534 689 535 - static void playbacklog_remove_duplicates(int fd, char *buf, size_t bufsz) 690 + static void pbl_remove_duplicates(int fd, char *buf, size_t bufsz, int lines) 536 691 { 537 - logf("%s()\n", __func__); 692 + /* walks the log and removes duplicate tracks */ 693 + logf("%s() lines: %d\n", __func__, lines); 538 694 struct scrobbler_entry entry; 539 - char tmp_buf[SCROBBLER_MAXENTRY_LEN]; 695 + static char tmp_buf[SCROBBLER_MAXENTRY_LEN]; 540 696 int start_pos, pos = 0; 541 697 int rd; 542 - int line_num = 0; 698 + int line_num = 0; /* count includes empty lines\comments */ 699 + int dup = 0; 700 + 701 + rb->button_clear_queue(); 543 702 rb->lseek(fd, 0, SEEK_SET); 544 703 704 + clear_display(); 705 + 706 + rb->splash_progress(line_num, lines, 707 + ID2P(LANG_PLAYLIST_SEARCH_MSG), dup, rb->str(LANG_REPEAT)); 545 708 while(1) 546 709 { 547 710 if ((rd = rb->read_line(fd, buf, bufsz)) <= 0) 548 711 break; /* EOF */ 712 + if (do_timed_yield()) 713 + { 714 + rb->splash_progress(line_num, lines, 715 + ID2P(LANG_PLAYLIST_SEARCH_MSG), dup, rb->str(LANG_REPEAT)); 549 716 550 - /* save start of entry in case we need to remove it */ 551 - start_pos = pos; 552 - pos += rd; 717 + if (rb->action_userabort(TIMEOUT_NOBLOCK)) 718 + { 719 + if (rb->yesno_pop(ID2P(LANG_CANCEL_0))) 720 + { 721 + logf("User canceled"); 722 + break; 723 + } 724 + clear_display(); 725 + } 726 + } 727 + 553 728 line_num++; 554 - if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ 555 - continue; 556 - if (!playbacklog_parse_entry(&entry, buf)) 729 + if (buf[0] != ' ') /* skip comments and empty lines */ 557 730 { 558 - /*logf("%s failed parsing entry @ %d\n", __func__, line_num);*/ 731 + rb->splash_progress(line_num, lines, 732 + ID2P(LANG_PLAYLIST_SEARCH_MSG), dup, rb->str(LANG_REPEAT)); 733 + pos += rd; 559 734 continue; 560 735 } 561 - /*logf("current entry %s (%lu) @ %d", entry.path, entry.elapsed, line_num);*/ 736 + pbl_parse_valid_entry(&entry, buf); 562 737 563 - if (!pbl_cull_duplicates(fd, &entry, line_num, tmp_buf, sizeof(tmp_buf))) 738 + /* save start of entry in case we need to remove it */ 739 + start_pos = pos; 740 + pos += rd; 741 + /*logf("current entry %s (%lu) @ %d", entry.path, entry.elapsed, line_num);*/ 742 + if (!pbl_cull_duplicates(fd, &entry, pos, line_num, tmp_buf, sizeof(tmp_buf), &dup)) 564 743 { 565 744 rb->lseek(fd, start_pos, SEEK_SET); 566 745 /*logf("entry: %s @ %d is a duplicate", entry.path, line_num);*/ 567 - rb->write(fd, "#", 1); /* make this entry a comment */ 746 + rb->write(fd, "@", 1); /* skip this entry */ 747 + dup++; 568 748 } 569 - rb->lseek(fd, pos, SEEK_SET); 749 + rb->lseek(fd, pos, SEEK_SET); /* cull moves the file pos.. set it back */ 570 750 } 751 + 752 + logf("%s() lines: %d / %d\n", __func__, line_num, lines); 571 753 } 572 754 573 - static int export_scrobbler_file(void) 755 + static int pbl_copyloop(int fd_copy, const char *src_filename, 756 + char *buf, size_t buf_sz, int *total, int *lines) 574 757 { 575 - const char* filename = ROCKBOX_DIR "/playback.log"; 576 - rb->splash(0, ID2P(LANG_WAIT)); 577 - static char buf[SCROBBLER_MAXENTRY_LEN]; 758 + /* worker for copymerge_logs - copies valid entries from playback logs */ 578 759 struct scrobbler_entry entry; 579 - 580 - int tracks_saved = 0; 760 + long next_tick = *rb->current_tick; 761 + int count = 0; 581 762 int line_num = 0; 582 - int rd = 0; 583 - 584 - rb->remove(ROCKBOX_DIR "/playback.old"); 585 - 586 - int fd_copy = rb->open(ROCKBOX_DIR "/playback.old", O_RDWR | O_CREAT | O_TRUNC, 0666); 587 - if (fd_copy < 0) 763 + int lines_copied = 0; 764 + int fd_src = rb->open_utf8(src_filename, O_RDONLY); 765 + if (fd_src < 0) 588 766 { 589 - logf("Scrobbler Error opening: %s\n", ROCKBOX_DIR "/playback.old"); 590 - rb->splashf(HZ *2, "Scrobbler Error opening: %s", ROCKBOX_DIR "/playback.old"); 591 - return PLUGIN_ERROR; 767 + logf("Scrobbler Error opening: %s\n", src_filename); 768 + rb->splashf(0, "Scrobbler Error opening: %s", src_filename); 769 + return -1; 592 770 } 593 - rb->add_playbacklog(NULL); /* ensure the log has been flushed */ 594 771 595 - /* We don't want any writes while copying and (possibly) deleting the log */ 596 - bool log_enabled = rb->global_settings->playback_log; 597 - rb->global_settings->playback_log = false; 598 - 599 - int fd = rb->open_utf8(filename, O_RDONLY); 600 - if (fd < 0) 772 + while(rb->read_line(fd_src, buf, buf_sz) > 0) 601 773 { 602 - rb->global_settings->playback_log = log_enabled; /* restore logging */ 603 - logf("Scrobbler Error opening: %s\n", filename); 604 - rb->splashf(HZ *2, "Scrobbler Error opening: %s", filename); 605 - return PLUGIN_ERROR; 606 - } 607 - /* copy loop playback.log => playback.old */ 608 - while(rb->read_line(fd, buf, sizeof(buf)) > 0) 609 - { 774 + char skipch = ' '; 610 775 line_num++; 611 - if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ 776 + do_timed_yield(); 777 + if (buf[0] == '\0') /* skip empty lines */ 778 + continue; 779 + if (buf[0] == '#') /*copy comments*/ 780 + { 781 + lines_copied++; 782 + rb->fdprintf(fd_copy, "%s\n", buf); 612 783 continue; 784 + } 613 785 /* parse entry will fail if elapsed > length or other invalid entry */ 614 - if (!playbacklog_parse_entry(&entry, buf)) 786 + if (!pbl_parse_entry(&entry, buf)) 615 787 { 616 788 logf("%s failed parsing entry @ line: %d\n", __func__, line_num); 617 789 continue; 618 790 } 619 - /* don't copy entries that do not meet users minimum play length */ 791 + 792 + /* comment out entries that do not meet users minimum play length */ 620 793 if ((int) entry.elapsed < gConfig.minms) 621 794 { 622 795 logf("Skipping path:'%s' @ line: %d\nelapsed: %ld length: %ld\nmin: %d\n", 623 796 entry.path, line_num, entry.elapsed, entry.length, gConfig.minms); 624 - continue; 797 + skipch = '!'; /* ignore this entry */ 625 798 } 626 - /* add a space to beginning of every line playbacklog_remove_duplicates 627 - * will use this to prepend '#' to entries that will be ignored */ 628 - rb->fdprintf(fd_copy, " %s\n", buf); 629 - tracks_saved++; 799 + 800 + /* add a space (or !) to beginning of every line pbl_remove_duplicates 801 + * will use this to prepend '@' to entries that will be ignored 802 + * rewrite entry to ensure valid formatting of the parsed fields*/ 803 + rb->fdprintf(fd_copy, "%c%lu:%lu:%lu:%s\n", skipch, 804 + entry.timestamp, entry.elapsed, entry.length, entry.path); 805 + count++; 806 + lines_copied++; 807 + if (TIME_AFTER(*rb->current_tick, next_tick)) 808 + { 809 + rb->splashf(0, ID2P(LANG_PLAYLIST_SAVE_COUNT), *total + count, PLAYBACK_LOG "_old.log"); 810 + next_tick = *rb->current_tick + HZ; 811 + } 630 812 } 631 - rb->close(fd); 632 - logf("%s %d tracks copied\n", __func__, tracks_saved); 813 + 814 + logf("Scrobbler: copied %d entries from %s", count, src_filename); 815 + 816 + *total += count; 817 + *lines += lines_copied; 818 + rb->close(fd_src); 819 + return count; 820 + } 821 + 822 + static int pbl_copymerge_logs(int fd_copy, char *buf, size_t buf_sz, int *lines) 823 + { 824 + /* copies valid entries from playback.log and playback_####.log */ 825 + int copied; 826 + int total_tracks = 0; 827 + int total_lines = 0; 828 + char filename[MAX_PATH]; 829 + 830 + rb->add_playbacklog(NULL); /* ensure the log has been flushed */ 831 + 832 + /* We don't want any writes while copying and (possibly) deleting the log */ 833 + bool log_enabled = rb->global_settings->playback_log; 834 + rb->global_settings->playback_log = false; 835 + 836 + copied = pbl_copyloop(fd_copy, PLAYBACK_LOG_DIR "/"PLAYBACK_LOG".log", 837 + buf, buf_sz, &total_tracks, &total_lines); 633 838 634 - if (gConfig.delete_log && tracks_saved > 0) 839 + if (gConfig.delete_log && copied > 0) 635 840 { 636 - rb->remove(filename); 841 + rb->remove(PLAYBACK_LOG_DIR "/"PLAYBACK_LOG".log"); 637 842 } 638 - rb->global_settings->playback_log = log_enabled; /* restore logging */ 843 + rb->global_settings->playback_log = log_enabled; 639 844 640 - if (gConfig.remove_dup && tracks_saved > 0) 641 - playbacklog_remove_duplicates(fd_copy, buf, sizeof(buf)); 845 + if (copied < 0) 846 + { 847 + rb->sleep(HZ * 2); /* to let user see fail message */ 848 + } 642 849 643 - rb->lseek(fd_copy, 0, SEEK_SET); 850 + /* now check any sequential numbered log files */ 851 + for (int i = 1; i < 9999; i++) 852 + { 853 + rb->snprintf(filename, sizeof(filename), "%s/%s_%0*d%s", 854 + PLAYBACK_LOG_DIR, PLAYBACK_LOG, 4, i, ".log"); 644 855 645 - tracks_saved = 0; 646 - int scrobbler_fd = open_create_scrobbler_log(); 647 - line_num = 0; 648 - while (1) 649 - { 650 - if ((rd = rb->read_line(fd_copy, buf, sizeof(buf))) <= 0) 856 + if (!rb->file_exists(filename)) 857 + { 858 + logf("Scrobbler: %s doesn't exist", filename); 651 859 break; 652 - line_num++; 653 - if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ 654 - continue; 655 - if (!playbacklog_parse_entry(&entry, buf)) 860 + } 861 + copied = pbl_copyloop(fd_copy, filename, buf, buf_sz, &total_tracks, &total_lines); 862 + if (copied > 0) 656 863 { 657 - logf("%s failed parsing entry @ line: %d\n", __func__, line_num); 658 - continue; 864 + if (gConfig.delete_log) 865 + { 866 + rb->remove(filename); 867 + } 659 868 } 869 + else 870 + { 871 + rb->sleep(HZ*2); /* to let user see fail message */ 872 + } 873 + } 874 + *lines = total_lines; 875 + return total_tracks; 876 + } 877 + 878 + static int sbl_export(void) 879 + { 880 + /* Export playbacklog to scrobbler.log */ 881 + rb->lcd_clear_display(); 660 882 661 - logf("Read (%d) @ line: %d: timestamp: %lu\nelapsed: %ld\nlength: %ld\npath: '%s'\n", 662 - rd, line_num, entry.timestamp, entry.elapsed, entry.length, entry.path); 663 - int ret = create_log_entry(&entry, scrobbler_fd); 664 - if (ret == PLUGIN_ERROR) 665 - goto entry_error; 666 - tracks_saved++; 667 - /* process our valid entry */ 883 + rb->splash(0, ID2P(LANG_WAIT)); 884 + 885 + static char buf[SCROBBLER_MAXENTRY_LEN]; 886 + int tracks_total; 887 + int lines_total = 0; 888 + int tracks_saved = 0; 889 + int missing_meta = 0; 890 + int scrobbler_fd; 891 + int fd_copy = rb->open(PLAYBACK_LOG_DIR "/"PLAYBACK_LOG"_old.log", O_RDWR | O_CREAT | O_TRUNC, 0666); 892 + if (fd_copy < 0) 893 + { 894 + logf("Scrobbler Error opening: %s\n", PLAYBACK_LOG_DIR "/"PLAYBACK_LOG"_old.log"); 895 + rb->splashf(HZ *2, "Scrobbler Error opening: %s", PLAYBACK_LOG_DIR "/"PLAYBACK_LOG"_old.log"); 896 + return PLUGIN_ERROR; 668 897 } 669 898 670 - logf("%s %d tracks saved", __func__, tracks_saved); 671 - rb->close(scrobbler_fd); 672 - rb->close(fd_copy); 899 + rb->fdprintf(fd_copy, "#Parsed Playback log tags: '#' comment, '!' too short, '@' duplicate\n"); 900 + 901 + tracks_total = pbl_copymerge_logs(fd_copy, buf, sizeof(buf), &lines_total); 902 + logf("%s %d tracks copied %d lines copied\n", __func__, tracks_total, lines_total); 903 + 904 + if (tracks_total > 0) 905 + { 906 + if (gConfig.remove_dup > 0) 907 + pbl_remove_duplicates(fd_copy, buf, sizeof(buf), lines_total); 673 908 674 - rb->splashf(HZ *2, "%d tracks saved", tracks_saved); 909 + rb->lseek(fd_copy, 0, SEEK_SET); 675 910 676 - //ROCKBOX_DIR "/playback.log" 911 + struct scrobbler_entry entry; 912 + int rd = 0; 913 + int line_num = 0; 677 914 915 + scrobbler_fd = sbl_open_create(); 916 + if (scrobbler_fd >= 0) 917 + { 918 + clear_display(); 919 + while (1) 920 + { 921 + if ((rd = rb->read_line(fd_copy, buf, sizeof(buf))) <= 0) 922 + break; 923 + line_num++; 924 + if (buf[0] != ' ') /* skip culled entries comments and empty lines */ 925 + continue; 926 + pbl_parse_valid_entry(&entry, buf); 927 + 928 + logf("Read (%d) @ line: %d: timestamp: %lu\nelapsed: %ld\nlength: %ld\npath: '%s'\n", 929 + rd, line_num, entry.timestamp, entry.elapsed, entry.length, entry.path); 930 + 931 + int ret = sbl_create_entry(&entry, scrobbler_fd); 932 + if (ret == SCROBBLER_LOG_ERROR) 933 + goto entry_error; 934 + if (ret == SCROBBLER_LOG_SKIPTRACK) 935 + { 936 + missing_meta++; 937 + continue; 938 + } 939 + if (ret == SCROBBLER_LOG_NOMETADATA) 940 + missing_meta++; 941 + 942 + if (do_timed_yield()) 943 + { 944 + rb->splash_progress(tracks_saved, tracks_total, 945 + "%s %s", rb->str(LANG_EXPORT), rb->str(LANG_TRACKS)); 946 + } 947 + tracks_saved++; 948 + /* process our valid entry */ 949 + } 950 + rb->close(scrobbler_fd); 951 + } 952 + logf("%s %d tracks saved", __func__, tracks_saved); 953 + } 954 + rb->close(fd_copy); 955 + 956 + if (gConfig.tracknfo == 1) 957 + rb->snprintf(buf, sizeof(buf), "%d %s", missing_meta, rb->str(LANG_ID3_NO_INFO)); 958 + else 959 + rb->snprintf(buf, sizeof(buf), "%d %s", missing_meta, rb->str(LANG_DISPLAY_TRACK_NAME_ONLY)); 960 + 961 + clear_display(); 962 + rb->splashf(HZ *5, ID2P(LANG_PLAYLIST_SAVE_COUNT),tracks_saved, buf); 678 963 return PLUGIN_OK; 679 964 entry_error: 680 - if (scrobbler_fd > 0) 965 + if (scrobbler_fd >= 0) 681 966 rb->close(scrobbler_fd); 682 967 rb->close(fd_copy); 683 968 return PLUGIN_ERROR; 684 - (void)line_num; 685 969 } 686 970 687 971 /***************** Plugin Entry Point *****************/ ··· 690 974 { 691 975 bool resume; 692 976 const char * param_str = (const char*) parameter; 693 - resume = (parameter && param_str[0] == '-' && rb->strcmp(param_str, "-resume") == 0); 977 + resume = (parameter && param_str[0] == '-' 978 + && rb->strcasecmp(param_str, "-resume") == 0); 694 979 695 980 logf("Resume %s", resume ? "YES" : "NO"); 696 981 982 + if (!parameter) 983 + clear_display(); 984 + 697 985 if (!resume && !rb->global_settings->playback_log) 698 986 ask_enable_playbacklog(); 699 987 ··· 708 996 /* If the loading failed, save a new config file */ 709 997 configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 710 998 rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); 999 + } 1000 + else if (!parameter && rb->global_settings->playback_log 1001 + && rb->global_status->last_screen != GO_TO_PLUGIN) 1002 + { 1003 + if (rb->strcasestr(rb->tree_get_context()->currdir, PLUGIN_APPS_DIR) == NULL) 1004 + { 1005 + logf("Auto Export - Last screen: %d", rb->global_status->last_screen); 1006 + if (rb->file_exists(ROCKBOX_DIR "/playback.log") 1007 + || rb->file_exists(ROCKBOX_DIR "/playback_0001.log")) 1008 + { 1009 + return scrobbler_menu_action(10, true); /* export scrobbler file */ 1010 + } 1011 + else 1012 + { 1013 + rb->splashf(HZ, "0 %s", rb->str(LANG_TRACKS)); 1014 + return PLUGIN_OK; 1015 + } 1016 + } 711 1017 } 712 1018 713 1019 return scrobbler_menu(resume);
+2 -2
manual/configure_rockbox/playback_options.tex
··· 326 326 \setting{Prefer Embedded} album art. 327 327 } 328 328 329 - \section{Logging}\index{Logging} 329 + \section{Logging}\index{Logging}\label{sec:playbacklogging} 330 330 This option will record information about tracks played on the device 331 331 in the following format 'timestamp:elapsed(ms):length(ms):path' 332 332 Devices without a Real Time Clock will use current system tick. ··· 337 337 devices with Real Time Clock will record the date and time as well. 338 338 When the log gets too large (\textasciitilde 1500 tracks) it will be split into playback\_nnnn.log 339 339 where nnnn is 0001-9999} 340 - see \setting{LastFm Scrobbler} plugin. 340 + see \setting{LastFm Scrobbler} plugin [\ref{sec:scrobbler}]. 341 341 \begin{verbatim} 342 342 the log can be found under '/.rockbox/playback.log' 343 343 \end{verbatim}
+35 -13
manual/plugins/lastfm_scrobbler.tex
··· 1 - \subsection{LastFm Scrobbler} 1 + \subsection{LastFm Scrobbler}\label{sec:scrobbler} 2 2 The \setting{LastFm Scrobbler} plugin enables you to parse the rockbox 3 3 playback log for tracks you have played for your own logging or upload 4 4 to scrobbling services, such as Last.fm, Libre.fm or ListenBrainz. 5 5 6 - \setting{Playback Logging} must be enabled to record the tracks played 7 - the plugin will ask you to enable logging if run with logging disabled. 6 + \setting{Playback Logging} [\ref{sec:playbacklogging}] must be enabled to record the tracks played. 7 + The plugin will ask you to enable logging if run with logging disabled. 8 8 9 9 \subsubsection{Menu} 10 10 \begin{itemize} 11 11 \item Remove duplicates - Only keeps the same track with the most time elapsed. 12 + \begin{description} 13 + \item[Off.] Disables duplicate scanning, all tracks saved. 14 + % 15 + \item[Resume Playback.] No duplicates across track resumes, back to back plays 16 + were probably just a track resume, only the longest elapsed play will be saved. 17 + % 18 + \item[All.] No duplicates, only the longest elapsed play will be saved regardless of when it was played. 19 + \end{description} 12 20 \item Delete playback log - Remove the current playback log once it has been read. 13 - \item Save threshold - Percentage of track played to be considered 'L'istened. 21 + \item Threshold - Percentage of track played to be considered 'L'istened. 14 22 \item Minimum elapsed (ms) - Tracks played less than this will not be recorded in log. 23 + \item Track Info - Only keeps tracks with metadata. 24 + \begin{description} 25 + \item[All.] Tracks saved regardless of metadata availibility. 26 + % 27 + \item[Skip if Missing.] Tracks without metadata will not be saved. 28 + % 29 + \item[Track Name Only.] Tracks not scanned for metadata, only filename saved. 30 + \end{description} 15 31 \item View log - View the current playback log. 16 32 \item Revert to Default - Default settings restored. 17 33 \item Cancel - Exit, you will be asked to save any changes 18 34 \item Export - Append scrobbler log and save any changes, not visible if no playback log exists. 19 35 \end{itemize} 20 36 21 - After the plugin has exported the scrobbler log you can find it in the root 37 + \note{\begin{itemize} 38 + \item After the plugin has exported the scrobbler log you can find it in the root 22 39 of the drive '.scrobbler.log' open it in the file browser to view the log. 40 + % 41 + \item Subsequent exports will be appended to .scrobbler.log thus Delete playback log is advised. 42 + % 43 + \item Once setup you may run the scrobbler plugin from a shortcut and it will 44 + auto export the playback log and exit. 45 + If you would like to change settings run \setting{LastFm Scrobbler} from 46 + the \setting{Plugins Menu}. 47 + % 48 + \item A copy of the playback log can be found in '/rockbox/playback\_old.log' it will be overwritten with each export. 23 49 24 - Subsequent exports will be appended to .scrobbler.log thus Delete playback log is advised. 50 + \end{itemize} 51 + } 25 52 26 - \begin{verbatim} 27 - A copy of the log can be found in 28 - '/rockbox/playback.old' 29 - it will be overwritten with each export 30 - \end{verbatim} 31 53 32 54 \subsubsection{Format} 33 55 Data will be saved in Audioscrobbler spec at: (use wayback machine). ··· 49 71 \item MUSICBRAINZ-TRACKID 50 72 \end{itemize} 51 73 52 - If track info is not available (due to missing file or format limitations) 53 - the track path will be used instead. 74 + \note{If track info is not available (due to missing file or format limitations) 75 + the track path will be used instead (except when \setting{Skip if Missing} is selected).}