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.

[BugFix] Last Fm Scrobbler corrupted entries

I couldn't seem to reproduce the issue here:
https://forums.rockbox.org/index.php/topic,54165.msg252081.html#msg252081

but I figure its probably a threading issue

so we now have a mutex on the cache

and to top it all off each cached entry has a crc and length that are
checked before writing the entry to the file otherwise it is prepended
with # FAILED - so hopefully scrobbler 'parsers?' don't barf on the log

Other changes:
there is now a MRU table for tracks this should help prevent duplicates
it is configurable..

the cache buffer now no longer uses fixed chunks allowing more tracks
to be written between flushes

Change-Id: Iaab7e3f6a76abfc61130f3233379a51c9a6d12e5

+420 -124
+420 -124
apps/plugins/lastfm_scrobbler.c
··· 21 21 * 22 22 ****************************************************************************/ 23 23 /* Scrobbler Plugin 24 - Audioscrobbler spec at: 24 + Audioscrobbler spec at: (use wayback machine) 25 25 http://www.audioscrobbler.net/wiki/Portable_Player_Logging 26 + * EXCERPT: 27 + * The first lines of .scrobbler.log should be header lines, indicated by the leading '#' character: 28 + 29 + #AUDIOSCROBBLER/1.1 30 + #TZ/[UNKNOWN|UTC] 31 + #CLIENT/<IDENTIFICATION STRING> 32 + 33 + Where 1.1 is the version for this file format 34 + 35 + If the device knows what timezone it is in, 36 + it must convert all logged times to UTC (aka GMT+0) 37 + eg: #TZ/UTC 38 + If the device knows the time, but not the timezone 39 + eg: #TZ/UNKNOWN 40 + 41 + <IDENTIFICATION STRING> should be replaced by the name/model of the hardware device 42 + and the revision of the software producing the log file. 43 + 44 + After the header lines, simply append one line of text for every song 45 + that is played or skipped. 46 + 47 + The following fields comprise each line, and are tab (\t) 48 + separated (strip any tab characters from the data): 49 + 50 + - artist name 51 + - album name (optional) 52 + - track name 53 + - track position on album (optional) 54 + - song duration in seconds 55 + - rating (L if listened at least 50% or S if skipped) 56 + - unix timestamp when song started playing 57 + - MusicBrainz Track ID (optional) 58 + lines should be terminated with \n 59 + Example 60 + (listened to enter sandman, skipped cowboys, listened to the pusher) : 61 + #AUDIOSCROBBLER/1.0 62 + #TZ/UTC 63 + #CLIENT/Rockbox h3xx 1.1 64 + Metallica Metallica Enter Sandman 1 365 L 1143374412 62c2e20a?-559e-422f-a44c-9afa7882f0c4? 65 + Portishead Roseland NYC Live Cowboys 2 312 S 1143374777 db45ed76-f5bf-430f-a19f-fbe3cd1c77d3 66 + Steppenwolf Live The Pusher 12 350 L 1143374779 58ddd581-0fcc-45ed-9352-25255bf80bfb? 67 + If the data for optional fields is not available to you, leave the field blank (\t\t). 68 + All strings should be written as UTF-8, although the file does not use a BOM. 69 + All fields except those marked (optional) above are required. 26 70 */ 27 71 28 72 #include "plugin.h" ··· 41 85 /****************** constants ******************/ 42 86 #define EV_EXIT MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFF) 43 87 #define EV_FLUSHCACHE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFE) 88 + #define EV_USER_ERROR MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFD) 44 89 #define EV_STARTUP MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x01) 45 90 #define EV_TRACKCHANGE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x02) 46 91 #define EV_TRACKFINISH MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x03) 47 92 48 - #define SCROBBLER_VERSION "1.1" 93 + #define ERR_NONE (0) 94 + #define ERR_WRITING_FILE (-1) 95 + #define ERR_ENTRY_LENGTH (-2) 96 + #define ERR_WRITING_DATA (-3) 49 97 50 98 /* increment this on any code change that effects output */ 99 + #define SCROBBLER_VERSION "1.1" 100 + 51 101 #define SCROBBLER_REVISION " $Revision$" 52 102 53 - #define SCROBBLER_MAX_CACHE 32 103 + #define SCROBBLER_BAD_ENTRY "# FAILED - " 104 + 54 105 /* longest entry I've had is 323, add a safety margin */ 55 - #define SCROBBLER_CACHE_LEN 512 106 + #define SCROBBLER_CACHE_LEN (512) 107 + #define SCROBBLER_MAX_CACHE (32 * SCROBBLER_CACHE_LEN) 108 + 109 + #define SCROBBLER_MAX_TRACK_MRU (32) /* list of hashes to detect repeats */ 56 110 57 111 #define ITEM_HDR "#ARTIST #ALBUM #TITLE #TRACKNUM #LENGTH #RATING #TIMESTAMP #MUSICBRAINZ_TRACKID\n" 58 112 ··· 67 121 #define record_timestamp() ((void)(timestamp = rb->mktime(rb->get_time()))) 68 122 #else /* !CONFIG_RTC */ 69 123 #define HDR_STR_TIMELESS " Timeless" 70 - #define BASE_FILENAME ".scrobbler-timeless.log" 124 + #define BASE_FILENAME HOME_DIR "/.scrobbler-timeless.log" 71 125 #define get_timestamp() (0l) 72 126 #define record_timestamp() ({}) 73 127 #endif /* CONFIG_RTC */ ··· 76 130 77 131 /****************** prototypes ******************/ 78 132 enum plugin_status plugin_start(const void* parameter); /* entry */ 79 - 133 + void play_tone(unsigned int frequency, unsigned int duration); 80 134 /****************** globals ******************/ 81 - unsigned char **language_strings; /* for use with str() macro; must be init */ 82 135 /* communication to the worker thread */ 83 136 static struct 84 137 { ··· 89 142 long stack[THREAD_STACK_SIZE / sizeof(long)]; 90 143 } gThread; 91 144 92 - static struct 145 + struct cache_entry 93 146 { 147 + size_t len; 148 + uint32_t crc; 149 + char buf[ ]; 150 + }; 151 + 152 + static struct scrobbler_cache 153 + { 154 + int entries; 94 155 char *buf; 95 - int pos; 156 + size_t pos; 96 157 size_t size; 97 158 bool pending; 98 159 bool force_flush; 160 + struct mutex mtx; 99 161 } gCache; 100 162 101 - static struct lastfm_config 163 + static struct scrobbler_cfg 102 164 { 165 + int uniqct; 103 166 int savepct; 167 + int minms; 104 168 int beeplvl; 105 169 bool playback; 106 170 bool verbose; ··· 108 172 109 173 static struct configdata config[] = 110 174 { 111 - {TYPE_INT, 0, 100, { .int_p = &gConfig.savepct }, "SavePct", NULL}, 112 - {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.playback }, "Playback", NULL}, 113 - {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.verbose }, "Verbose", NULL}, 114 - {TYPE_INT, 0, 10, { .int_p = &gConfig.beeplvl }, "BeepLvl", NULL}, 175 + #define MAX_MRU (SCROBBLER_MAX_TRACK_MRU) 176 + {TYPE_INT, 0, MAX_MRU, { .int_p = &gConfig.uniqct }, "UniqCt", NULL}, 177 + {TYPE_INT, 0, 100, { .int_p = &gConfig.savepct }, "SavePct", NULL}, 178 + {TYPE_INT, 0, 10000, { .int_p = &gConfig.minms }, "MinMs", NULL}, 179 + {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.playback }, "Playback", NULL}, 180 + {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.verbose }, "Verbose", NULL}, 181 + {TYPE_INT, 0, 10, { .int_p = &gConfig.beeplvl }, "BeepLvl", NULL}, 182 + #undef MAX_MRU 115 183 }; 116 184 const int gCfg_sz = sizeof(config)/sizeof(*config); 185 + 117 186 /****************** config functions *****************/ 118 187 static void config_set_defaults(void) 119 188 { 189 + gConfig.uniqct = SCROBBLER_MAX_TRACK_MRU; 120 190 gConfig.savepct = 50; 191 + gConfig.minms = 500; 121 192 gConfig.playback = false; 122 193 gConfig.verbose = true; 123 194 gConfig.beeplvl = 10; ··· 127 198 { 128 199 int selection = 0; 129 200 201 + static uint32_t crc = 0; 202 + 130 203 struct viewport parentvp[NB_SCREENS]; 131 204 FOR_NB_SCREENS(l) 132 205 { ··· 134 207 rb->viewport_set_fullscreen(&parentvp[l], l); 135 208 } 136 209 137 - MENUITEM_STRINGLIST(settings_menu, ID2P(LANG_SETTINGS), NULL, 210 + #define MENUITEM_STRINGLIST_CUSTOM(name, str, callback, ... ) \ 211 + static const char *name##_[] = {__VA_ARGS__}; \ 212 + static const struct menu_callback_with_desc name##__ = \ 213 + {callback,str, Icon_NOICON}; \ 214 + struct menu_item_ex name = \ 215 + {MT_RETURN_ID|MENU_HAS_DESC| \ 216 + MENU_ITEM_COUNT(sizeof( name##_)/sizeof(*name##_)), \ 217 + { .strings = name##_},{.callback_and_desc = & name##__}}; 218 + 219 + MENUITEM_STRINGLIST_CUSTOM(settings_menu, ID2P(LANG_SETTINGS), NULL, 138 220 ID2P(LANG_RESUME_PLAYBACK), 139 221 "Save Threshold", 222 + "Minimum Elapsed", 140 223 "Verbose", 141 224 "Beep Level", 225 + "Unique Track MRU", 226 + ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS), 142 227 ID2P(VOICE_BLANK), 143 228 ID2P(LANG_CANCEL_0), 144 229 ID2P(LANG_SAVE_EXIT)); 145 230 231 + #undef MENUITEM_STRINGLIST_CUSTOM 232 + 233 + const int items = MENU_GET_COUNT(settings_menu.flags); 234 + const unsigned int flags = settings_menu.flags & (~MENU_ITEM_COUNT(MENU_COUNT_MASK)); 235 + if (crc == 0) 236 + { 237 + crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); 238 + } 239 + 146 240 do { 241 + if (crc == rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF)) 242 + { 243 + /* hide save item -- there are no changes to save */ 244 + settings_menu.flags = flags|MENU_ITEM_COUNT((items - 1)); 245 + } 246 + else 247 + { 248 + settings_menu.flags = flags|MENU_ITEM_COUNT(items); 249 + } 147 250 selection=rb->do_menu(&settings_menu,&selection, parentvp, true); 148 251 switch(selection) { 149 252 150 - case 0: 151 - rb->set_bool(str(LANG_RESUME_PLAYBACK), &gConfig.playback); 253 + case 0: /* resume playback on plugin start */ 254 + rb->set_bool(rb->str(LANG_RESUME_PLAYBACK), &gConfig.playback); 152 255 break; 153 - case 1: 256 + case 1: /* % of track played to indicate listened status */ 154 257 rb->set_int("Save Threshold", "%", UNIT_PERCENT, 155 258 &gConfig.savepct, NULL, 10, 0, 100, NULL ); 156 259 break; 157 - case 2: 260 + case 2: /* tracks played less than this will not be logged */ 261 + rb->set_int("Minimum Elapsed", "ms", UNIT_MS, 262 + &gConfig.minms, NULL, 100, 0, 10000, NULL ); 263 + break; 264 + case 3: /* suppress non-error messages */ 158 265 rb->set_bool("Verbose", &gConfig.verbose); 159 266 break; 160 - case 3: 267 + case 4: /* set volume of start-up beep */ 161 268 rb->set_int("Beep Level", "", UNIT_INT, 162 269 &gConfig.beeplvl, NULL, 1, 0, 10, NULL); 163 - if (gConfig.beeplvl > 0) 164 - rb->beep_play(1500, 100, 100 * gConfig.beeplvl); 165 - case 4: /*sep*/ 270 + play_tone(1500, 100); 271 + break; 272 + case 5: /* keep a list of tracks to prevent repeat [Skipped] entries */ 273 + rb->set_int("Unique Track MRU Size", "", UNIT_INT, 274 + &gConfig.uniqct, NULL, 1, 0, SCROBBLER_MAX_TRACK_MRU, NULL); 275 + break; 276 + case 6: /* set defaults */ 277 + { 278 + const struct text_message prompt = { 279 + (const char*[]){ ID2P(LANG_AUDIOSCROBBLER), 280 + ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)}, 2}; 281 + if(rb->gui_syncyesno_run(&prompt, NULL, NULL) == YESNO_YES) 282 + { 283 + config_set_defaults(); 284 + if (gConfig.verbose) 285 + rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); 286 + } 287 + break; 288 + } 289 + case 7: /*sep*/ 166 290 continue; 167 - case 5: 291 + case 8: /* Cancel */ 168 292 return -1; 169 293 break; 170 - case 6: 294 + case 9: /* Save & exit */ 171 295 { 172 296 int res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 173 297 if (res >= 0) 174 298 { 175 - logf("Scrobbler cfg saved %s %d bytes", CFG_FILE, gCfg_sz); 299 + crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); 300 + logf("SCROBBLER: cfg saved %s %d bytes", CFG_FILE, gCfg_sz); 176 301 return PLUGIN_OK; 177 302 } 178 - logf("Scrobbler cfg FAILED (%d) %s", res, CFG_FILE); 303 + logf("SCROBBLER: cfg FAILED (%d) %s", res, CFG_FILE); 179 304 return PLUGIN_ERROR; 180 305 } 181 306 case MENU_ATTACHED_USB: ··· 188 313 } 189 314 190 315 /****************** helper fuctions ******************/ 316 + void play_tone(unsigned int frequency, unsigned int duration) 317 + { 318 + if (gConfig.beeplvl > 0) 319 + rb->beep_play(frequency, duration, 100 * gConfig.beeplvl); 320 + } 191 321 192 - int scrobbler_init(void) 322 + int scrobbler_init_cache(void) 193 323 { 324 + memset(&gCache, 0, sizeof(struct scrobbler_cache)); 194 325 gCache.buf = rb->plugin_get_buffer(&gCache.size); 195 326 196 - size_t reqsz = SCROBBLER_MAX_CACHE*SCROBBLER_CACHE_LEN; 327 + /* we need to reserve the space we want for our use in TSR plugins since 328 + * someone else could call plugin_get_buffer() and corrupt our memory */ 329 + size_t reqsz = SCROBBLER_MAX_CACHE; 197 330 gCache.size = PLUGIN_BUFFER_SIZE - rb->plugin_reserve_buffer(reqsz); 198 331 199 332 if (gCache.size < reqsz) ··· 201 334 logf("SCROBBLER: OOM , %ld < req:%ld", gCache.size, reqsz); 202 335 return -1; 203 336 } 204 - 205 - gCache.pos = 0; 206 - gCache.pending = false; 207 337 gCache.force_flush = true; 208 - logf("Scrobbler Initialized"); 338 + rb->mutex_init(&gCache.mtx); 339 + logf("SCROBBLER: Initialized"); 209 340 return 1; 210 341 } 211 342 343 + static inline size_t cache_get_entry_size(int str_len) 344 + { 345 + /* entry_sz consists of the cache entry + str_len + \0NULL terminator */ 346 + return str_len + 1 + sizeof(struct cache_entry); 347 + } 348 + 349 + static inline const char* str_chk_valid(const char *s, const char *alt) 350 + { 351 + return (s != NULL ? s : alt); 352 + } 353 + 354 + static bool track_is_unique(uint32_t hash1, uint32_t hash2) 355 + { 356 + bool is_unique = false; 357 + static uint8_t mru_len = 0; 358 + 359 + struct hash64 { uint32_t hash1; uint32_t hash2; }; 360 + 361 + static struct hash64 hash_mru[SCROBBLER_MAX_TRACK_MRU]; 362 + struct hash64 i = {0}; 363 + struct hash64 itmp; 364 + uint8_t mru; 365 + 366 + if (mru_len > gConfig.uniqct) 367 + mru_len = gConfig.uniqct; 368 + 369 + if (gConfig.uniqct < 1) 370 + return true; 371 + 372 + /* Search in MRU */ 373 + for (mru = 0; mru < mru_len; mru++) 374 + { 375 + /* Items shifted >> 1 */ 376 + itmp = i; 377 + i = hash_mru[mru]; 378 + hash_mru[mru] = itmp; 379 + 380 + /* Found in MRU */ 381 + if ((i.hash1 == hash1) && (i.hash2 == hash2)) 382 + { 383 + logf("SCROBBLER: hash [%x, %x] found in MRU @ %d", i.hash1, i.hash2, mru); 384 + goto Found; 385 + } 386 + } 387 + 388 + /* Add MRU entry */ 389 + is_unique = true; 390 + if (mru_len < SCROBBLER_MAX_TRACK_MRU && mru_len < gConfig.uniqct) 391 + { 392 + hash_mru[mru_len] = i; 393 + mru_len++; 394 + } 395 + else 396 + { 397 + logf("SCROBBLER: hash [%x, %x] evicted from MRU", i.hash1, i.hash2); 398 + } 399 + 400 + i = (struct hash64){.hash1 = hash1, .hash2 = hash2}; 401 + logf("SCROBBLER: hash [%x, %x] added to MRU[%d]", i.hash1, i.hash2, mru_len); 402 + 403 + Found: 404 + 405 + /* Promote MRU item to top of MRU */ 406 + hash_mru[0] = i; 407 + 408 + return is_unique; 409 + } 410 + 212 411 static void get_scrobbler_filename(char *path, size_t size) 213 412 { 214 413 int used; ··· 217 416 218 417 if (used >= (int)size) 219 418 { 220 - logf("%s: not enough buffer space for log file", __func__); 419 + logf("%s: not enough buffer space for log filename", __func__); 221 420 rb->memset(path, 0, size); 222 421 } 223 422 } ··· 228 427 int fd; 229 428 logf("%s", __func__); 230 429 char scrobbler_file[MAX_PATH]; 430 + 431 + rb->mutex_lock(&gCache.mtx); 432 + 231 433 get_scrobbler_filename(scrobbler_file, sizeof(scrobbler_file)); 232 434 233 435 /* If the file doesn't exist, create it. ··· 237 439 fd = rb->open(scrobbler_file, O_RDWR | O_CREAT, 0666); 238 440 if(fd >= 0) 239 441 { 442 + /* write file header */ 240 443 rb->fdprintf(fd, "#AUDIOSCROBBLER/" SCROBBLER_VERSION "\n" 241 444 "#TZ/UNKNOWN\n" "#CLIENT/Rockbox " 242 445 TARGET_NAME SCROBBLER_REVISION ··· 251 454 } 252 455 } 253 456 457 + int entries = gCache.entries; 458 + size_t used = gCache.pos; 459 + size_t pos = 0; 460 + /* clear even if unsuccessful - we don't want to overflow the buffer */ 461 + gCache.pos = 0; 462 + gCache.entries = 0; 463 + 254 464 /* write the cache entries */ 255 465 fd = rb->open(scrobbler_file, O_WRONLY | O_APPEND); 256 466 if(fd >= 0) 257 467 { 258 - logf("SCROBBLER: writing %d entries", gCache.pos); 259 - /* copy data to temporary storage in case data moves during I/O */ 260 - char temp_buf[SCROBBLER_CACHE_LEN]; 261 - for ( i=0; i < gCache.pos; i++ ) 468 + logf("SCROBBLER: writing %d entries", entries); 469 + /* copy cached data to storage */ 470 + uint32_t prev_crc = 0x0; 471 + uint32_t crc; 472 + size_t entry_sz, len; 473 + bool err = false; 474 + 475 + for (i = 0; i < entries && pos < used; i++) 262 476 { 263 - logf("SCROBBLER: write %d", i); 264 - char* scrobbler_buf = gCache.buf; 265 - ssize_t len = rb->strlcpy(temp_buf, scrobbler_buf+(SCROBBLER_CACHE_LEN*i), 266 - sizeof(temp_buf)); 267 - if (rb->write(fd, temp_buf, len) != len) 477 + logf("SCROBBLER: write %d read pos [%ld]", i, pos); 478 + 479 + struct cache_entry *entry = (struct cache_entry*)&gCache.buf[pos]; 480 + 481 + entry_sz = cache_get_entry_size(entry->len); 482 + crc = rb->crc_32(entry->buf, entry->len, 0xFFFFFFFF) ^ prev_crc; 483 + prev_crc = crc; 484 + 485 + len = rb->strlen(entry->buf); 486 + logf("SCROBBLER: write entry %d sz [%ld] len [%ld]", i, entry_sz, len); 487 + 488 + if (len != entry->len || crc != entry->crc) /* the entry is corrupted */ 489 + { 490 + rb->write(fd, SCROBBLER_BAD_ENTRY, sizeof(SCROBBLER_BAD_ENTRY)-1); 491 + logf("SCROBBLER: Bad entry %d", i); 492 + if(!err) 493 + { 494 + rb->queue_post(&gThread.queue, EV_USER_ERROR, ERR_WRITING_DATA); 495 + err = true; 496 + } 497 + } 498 + 499 + logf("SCROBBLER: writing %s", entry->buf); 500 + 501 + if (rb->write(fd, entry->buf, len) != (ssize_t)len) 268 502 break; 503 + 504 + if (entry->buf[len - 1] != '\n') 505 + rb->write(fd, "\n", 1); /* ensure newline termination */ 506 + 507 + pos += entry_sz; 269 508 } 270 509 rb->close(fd); 271 510 } 272 511 else 273 512 { 274 513 logf("SCROBBLER: error writing file"); 514 + rb->queue_post(&gThread.queue, EV_USER_ERROR, ERR_WRITING_FILE); 275 515 } 276 - 277 - /* clear even if unsuccessful - don't want to overflow the buffer */ 278 - gCache.pos = 0; 516 + rb->mutex_unlock(&gCache.mtx); 279 517 } 280 518 281 519 #if USING_STORAGE_CALLBACK 282 520 static void scrobbler_flush_callback(void) 283 521 { 284 - (void) gCache.force_flush; 285 - if(gCache.pos <= 0) 522 + if(gCache.pos == 0) 286 523 return; 287 524 #if (CONFIG_STORAGE & STORAGE_ATA) 288 525 else ··· 297 534 } 298 535 #endif 299 536 300 - static inline char* str_chk_valid(char *s, char *alt) 301 - { 302 - return (s != NULL ? s : alt); 303 - } 304 - 305 537 static unsigned long scrobbler_get_threshold(unsigned long length) 306 538 { 307 539 /* length is assumed to be in miliseconds */ 308 540 return length / 100 * gConfig.savepct; 309 - 310 541 } 311 542 312 - static void scrobbler_add_to_cache(const struct mp3entry *id) 543 + static int create_log_entry(const struct mp3entry *id, 544 + struct cache_entry *entry, int *trk_info_len) 313 545 { 314 - static uint32_t last_crc = 0; 315 - int trk_info_len = 0; 316 - 317 - if ( gCache.pos >= SCROBBLER_MAX_CACHE ) 318 - scrobbler_write_cache(); 319 - 546 + #define SEP "\t" 547 + #define EOL "\n" 548 + char* artist = id->artist ? id->artist : id->albumartist; 320 549 char rating = 'S'; /* Skipped */ 321 - char* scrobbler_buf = gCache.buf; 322 - 323 - logf("SCROBBLER: add_to_cache[%d]", gCache.pos); 324 - 325 550 if (id->elapsed >= scrobbler_get_threshold(id->length)) 326 551 rating = 'L'; /* Listened */ 327 552 ··· 330 555 if (id->tracknum > 0) 331 556 rb->snprintf(tracknum, sizeof (tracknum), "%d", id->tracknum); 332 557 333 - char* artist = id->artist ? id->artist : id->albumartist; 558 + int ret = rb->snprintf(entry->buf, 559 + SCROBBLER_CACHE_LEN, 560 + "%s"SEP"%s"SEP"%s"SEP"%s"SEP"%d%n"SEP"%c"SEP"%ld"SEP"%s"EOL"", 561 + str_chk_valid(artist, UNTAGGED), 562 + str_chk_valid(id->album, ""), 563 + str_chk_valid(id->title, id->path), 564 + tracknum, 565 + (int)(id->length / 1000), 566 + trk_info_len, /* receives len of the string written so far */ 567 + rating, 568 + get_timestamp(), 569 + str_chk_valid(id->mb_track_id, "")); 334 570 335 - int ret = rb->snprintf(&scrobbler_buf[(SCROBBLER_CACHE_LEN*gCache.pos)], 336 - SCROBBLER_CACHE_LEN, 337 - "%s\t%s\t%s\t%s\t%d\t%c%n\t%ld\t%s\n", 338 - str_chk_valid(artist, UNTAGGED), 339 - str_chk_valid(id->album, ""), 340 - str_chk_valid(id->title, ""), 341 - tracknum, 342 - (int)(id->length / 1000), 343 - rating, 344 - &trk_info_len, 345 - get_timestamp(), 346 - str_chk_valid(id->mb_track_id, "")); 571 + #undef SEP 572 + #undef EOL 573 + return ret; 574 + } 575 + 576 + static void scrobbler_add_to_cache(const struct mp3entry *id) 577 + { 578 + int trk_info_len = 0; 347 579 348 - if ( ret >= SCROBBLER_CACHE_LEN ) 580 + if (id->elapsed < (unsigned long) gConfig.minms) 581 + { 582 + logf("SCROBBLER: skipping entry < %d ms: %s", gConfig.minms, id->path); 583 + return; 584 + } 585 + 586 + rb->mutex_lock(&gCache.mtx); 587 + 588 + /* not enough room left to guarantee next entry will fit so flush the cache */ 589 + if ( gCache.pos > SCROBBLER_MAX_CACHE - SCROBBLER_CACHE_LEN ) 590 + scrobbler_write_cache(); 591 + 592 + logf("SCROBBLER: add_to_cache[%d] write pos[%ld]", gCache.entries, gCache.pos); 593 + /* use prev_crc to allow whole buffer to be checked for consistency */ 594 + static uint32_t prev_crc = 0x0; 595 + if (gCache.pos == 0) 596 + prev_crc = 0x0; 597 + 598 + void *buf = &gCache.buf[gCache.pos]; 599 + memset(buf, 0, SCROBBLER_CACHE_LEN); 600 + 601 + struct cache_entry *entry = buf; 602 + 603 + int ret = create_log_entry(id, entry, &trk_info_len); 604 + 605 + if (ret <= 0 || (size_t) ret >= SCROBBLER_CACHE_LEN) 349 606 { 350 607 logf("SCROBBLER: entry too long:"); 351 608 logf("SCROBBLER: %s", id->path); 609 + rb->queue_post(&gThread.queue, EV_USER_ERROR, ERR_ENTRY_LENGTH); 352 610 } 353 - else 611 + else if (ret > 0) 354 612 { 355 - uint32_t crc = rb->crc_32(&scrobbler_buf[(SCROBBLER_CACHE_LEN*gCache.pos)], 356 - trk_info_len, 0xFFFFFFFF); 357 - if (crc != last_crc) 613 + /* first generate a crc over the static portion of the track info data 614 + this and a crc of the filename will be used to detect repeat entries 615 + */ 616 + static uint32_t last_crc = 0; 617 + uint32_t crc_entry = rb->crc_32(entry->buf, trk_info_len, 0xFFFFFFFF); 618 + uint32_t crc_path = rb->crc_32(id->path, rb->strlen(id->path), 0xFFFFFFFF); 619 + bool is_unique = track_is_unique(crc_entry, crc_path); 620 + bool is_listened = (id->elapsed >= scrobbler_get_threshold(id->length)); 621 + 622 + if (is_unique || is_listened) 358 623 { 359 - last_crc = crc; 360 - logf("Added %s", scrobbler_buf); 361 - gCache.pos++; 624 + /* finish calculating the CRC of the whole entry */ 625 + const void *src = entry->buf + trk_info_len; 626 + entry->crc = rb->crc_32(src, ret - trk_info_len, crc_entry) ^ prev_crc; 627 + prev_crc = entry->crc; 628 + entry->len = ret; 629 + 630 + /* since Listened entries are written regardless 631 + make sure this isn't a direct repeat */ 632 + if ((entry->crc ^ crc_path) != last_crc) 633 + { 634 + 635 + if (is_listened) 636 + last_crc = (entry->crc ^ crc_path); 637 + else 638 + last_crc = 0; 639 + 640 + size_t entry_sz = cache_get_entry_size(ret); 641 + 642 + logf("SCROBBLER: Added (#%d) sz[%ld] len[%d], %s", 643 + gCache.entries, entry_sz, ret, entry->buf); 644 + 645 + gCache.entries++; 646 + /* increase pos by string len + null terminator + sizeof entry */ 647 + gCache.pos += entry_sz; 648 + 362 649 #if USING_STORAGE_CALLBACK 363 - rb->register_storage_idle_func(scrobbler_flush_callback); 650 + rb->register_storage_idle_func(scrobbler_flush_callback); 364 651 #endif 652 + } 365 653 } 366 654 else 367 655 logf("SCROBBLER: skipping repeat entry: %s", id->path); 368 656 } 369 - 657 + rb->mutex_unlock(&gCache.mtx); 370 658 } 371 659 372 660 static void scrobbler_flush_cache(void) ··· 387 675 } 388 676 } 389 677 390 - static void scrobbler_change_event(unsigned short id, void *ev_data) 678 + static void track_change_event(unsigned short id, void *ev_data) 391 679 { 392 680 (void)id; 393 681 logf("%s", __func__); 394 682 struct mp3entry *id3 = ((struct track_event *)ev_data)->id3; 395 683 396 - /* check if track was resumed > %threshold played ( likely got saved ) 397 - check for blank artist or track name */ 398 - if ((id3->elapsed > scrobbler_get_threshold(id3->length)) 399 - || (!id3->artist && !id3->albumartist) || !id3->title) 684 + /* check if track was resumed > %threshold played ( likely got saved ) */ 685 + if ((id3->elapsed > scrobbler_get_threshold(id3->length))) 400 686 { 401 687 gCache.pending = false; 402 688 logf("SCROBBLER: skipping file %s", id3->path); ··· 408 694 gCache.pending = true; 409 695 } 410 696 } 697 + 411 698 #ifdef ROCKBOX_HAS_LOGF 412 699 static const char* track_event_info(struct track_event* te) 413 700 { ··· 422 709 * TEF_REWIND = 0x4, interpret as rewind, id3->elapsed is the 423 710 position before the seek back to 0 424 711 */ 425 - logf("flag %d", te->flags); 712 + logf("SCROBBLER: flag %d", te->flags); 426 713 return strflags[te->flags&0x7]; 427 714 } 428 - 429 715 #endif 430 - static void scrobbler_finish_event(unsigned short id, void *ev_data) 716 + 717 + static void track_finish_event(unsigned short id, void *ev_data) 431 718 { 432 719 (void)id; 433 720 struct track_event *te = ((struct track_event *)ev_data); ··· 439 726 440 727 scrobbler_add_to_cache(te->id3); 441 728 } 442 - 443 - 444 729 } 445 730 446 731 /****************** main thread + helpers ******************/ 447 732 static void events_unregister(void) 448 733 { 449 734 /* we don't want any more events */ 450 - rb->remove_event(PLAYBACK_EVENT_TRACK_CHANGE, scrobbler_change_event); 451 - rb->remove_event(PLAYBACK_EVENT_TRACK_FINISH, scrobbler_finish_event); 735 + rb->remove_event(PLAYBACK_EVENT_TRACK_CHANGE, track_change_event); 736 + rb->remove_event(PLAYBACK_EVENT_TRACK_FINISH, track_finish_event); 452 737 } 453 738 454 739 static void events_register(void) 455 740 { 456 - rb->add_event(PLAYBACK_EVENT_TRACK_CHANGE, scrobbler_change_event); 457 - rb->add_event(PLAYBACK_EVENT_TRACK_FINISH, scrobbler_finish_event); 741 + rb->add_event(PLAYBACK_EVENT_TRACK_CHANGE, track_change_event); 742 + rb->add_event(PLAYBACK_EVENT_TRACK_FINISH, track_finish_event); 458 743 } 459 744 460 745 void thread(void) ··· 478 763 /*fall through*/ 479 764 case EV_STARTUP: 480 765 events_register(); 481 - if (gConfig.beeplvl > 0) 482 - rb->beep_play(1500, 100, 100 * gConfig.beeplvl); 766 + play_tone(1500, 100); 483 767 break; 484 768 case SYS_POWEROFF: 485 769 case SYS_REBOOT: ··· 487 771 /*fall through*/ 488 772 case EV_EXIT: 489 773 #if USING_STORAGE_CALLBACK 490 - rb->unregister_storage_idle_func(scrobbler_flush_callback, !in_usb); 774 + rb->unregister_storage_idle_func(scrobbler_flush_callback, false); 491 775 #else 492 776 if (!in_usb) 493 777 scrobbler_flush_cache(); ··· 498 782 scrobbler_flush_cache(); 499 783 rb->queue_reply(&gThread.queue, 0); 500 784 break; 785 + case EV_USER_ERROR: 786 + if (!in_usb) 787 + { 788 + if (ev.data == ERR_WRITING_FILE) 789 + rb->splash(HZ, "SCROBBLER: error writing log"); 790 + else if (ev.data == ERR_ENTRY_LENGTH) 791 + rb->splash(HZ, "SCROBBLER: error entry too long"); 792 + else if (ev.data == ERR_WRITING_DATA) 793 + rb->splash(HZ, "SCROBBLER: error bad entry data"); 794 + } 795 + break; 501 796 default: 502 797 logf("default %ld", ev.id); 503 798 break; ··· 507 802 508 803 void thread_create(void) 509 804 { 510 - /* put the thread's queue in the bcast list */ 805 + /* put the thread's queue in the broadcast list */ 511 806 rb->queue_init(&gThread.queue, true); 512 807 gThread.id = rb->create_thread(thread, gThread.stack, sizeof(gThread.stack), 513 808 0, "Last.Fm_TSR" ··· 529 824 } 530 825 } 531 826 532 - /* callback to end the TSR plugin, called before a new one gets loaded */ 533 - static int exit_tsr(bool reenter) 827 + /* callback to end the TSR plugin, called before a new plugin gets loaded */ 828 + static int plugin_exit_tsr(bool reenter) 534 829 { 535 830 MENUITEM_STRINGLIST(menu, ID2P(LANG_AUDIOSCROBBLER), NULL, ID2P(LANG_SETTINGS), 536 831 "Flush Cache", "Exit Plugin", ID2P(LANG_BACK)); ··· 550 845 config_settings_menu(); 551 846 break; 552 847 case 1: /* flush cache */ 553 - rb->queue_send(&gThread.queue, EV_FLUSHCACHE, 0); 554 - if (gConfig.verbose) 555 - rb->splashf(2*HZ, "%s Cache Flushed", str(LANG_AUDIOSCROBBLER)); 848 + if (gCache.entries > 0) 849 + { 850 + rb->queue_send(&gThread.queue, EV_FLUSHCACHE, 0); 851 + if (gConfig.verbose) 852 + rb->splashf(2*HZ, "%s Cache Flushed", rb->str(LANG_AUDIOSCROBBLER)); 853 + } 556 854 break; 557 855 558 856 case 2: /* exit plugin - quit */ ··· 572 870 /****************** main ******************/ 573 871 static int plugin_main(const void* parameter) 574 872 { 575 - struct lastfm_config cfg; 576 - rb->memcpy(&cfg, & gConfig, sizeof(struct lastfm_config)); 873 + struct scrobbler_cfg cfg; 874 + rb->memcpy(&cfg, &gConfig, sizeof(struct scrobbler_cfg)); /* store settings */ 577 875 578 - /* Resume plugin ? */ 876 + /* Resume plugin ? -- silences startup */ 579 877 if (parameter == rb->plugin_tsr) 580 878 { 581 - 582 879 gConfig.beeplvl = 0; 583 880 gConfig.playback = false; 584 881 gConfig.verbose = false; ··· 586 883 587 884 rb->memset(&gThread, 0, sizeof(gThread)); 588 885 if (gConfig.verbose) 589 - rb->splashf(HZ / 2, "%s Started",str(LANG_AUDIOSCROBBLER)); 590 - logf("%s: %s Started", __func__, str(LANG_AUDIOSCROBBLER)); 886 + rb->splashf(HZ / 2, "%s Started",rb->str(LANG_AUDIOSCROBBLER)); 887 + logf("%s: %s Started", __func__, rb->str(LANG_AUDIOSCROBBLER)); 591 888 592 - rb->plugin_tsr(exit_tsr); /* stay resident */ 889 + rb->plugin_tsr(plugin_exit_tsr); /* stay resident */ 593 890 594 891 thread_create(); 595 - rb->memcpy(&gConfig, &cfg, sizeof(struct lastfm_config)); 892 + rb->memcpy(&gConfig, &cfg, sizeof(struct scrobbler_cfg)); /*restore settings */ 596 893 597 894 if (gConfig.playback) 598 895 return PLUGIN_GOTO_WPS; ··· 607 904 /* now go ahead and have fun! */ 608 905 if (rb->usb_inserted() == true) 609 906 return PLUGIN_USB_CONNECTED; 610 - language_strings = rb->language_strings; 611 - if (scrobbler_init() < 0) 907 + 908 + if (scrobbler_init_cache() < 0) 612 909 return PLUGIN_ERROR; 613 910 614 911 config_set_defaults(); ··· 616 913 if (configfile_load(CFG_FILE, config, gCfg_sz, CFG_VER) < 0) 617 914 { 618 915 /* If the loading failed, save a new config file */ 619 - config_set_defaults(); 620 916 configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 621 - 622 - rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); 917 + if (gConfig.verbose) 918 + rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); 623 919 } 624 920 625 921 int ret = plugin_main(parameter);