summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWilliam Wilgus <wilgus.william@gmail.com>2023-04-18 00:32:43 -0400
committerWilliam Wilgus <wilgus.william@gmail.com>2023-05-20 22:41:34 -0400
commit8fbd44a3d37fddf323dea36e8f52193fd007bd75 (patch)
tree2162c62af36bd7f0e0fab09b3756f78fc5aa2b60
parent4554b908590e0f033ab76383516f921f92758435 (diff)
downloadrockbox-8fbd44a3d37fddf323dea36e8f52193fd007bd75.tar.gz
rockbox-8fbd44a3d37fddf323dea36e8f52193fd007bd75.zip
[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
-rw-r--r--apps/plugins/lastfm_scrobbler.c544
1 files changed, 420 insertions, 124 deletions
diff --git a/apps/plugins/lastfm_scrobbler.c b/apps/plugins/lastfm_scrobbler.c
index c835533b1f..1530ff7ae7 100644
--- a/apps/plugins/lastfm_scrobbler.c
+++ b/apps/plugins/lastfm_scrobbler.c
@@ -21,8 +21,52 @@
21 * 21 *
22 ****************************************************************************/ 22 ****************************************************************************/
23/* Scrobbler Plugin 23/* Scrobbler Plugin
24Audioscrobbler spec at: 24Audioscrobbler spec at: (use wayback machine)
25http://www.audioscrobbler.net/wiki/Portable_Player_Logging 25http://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
33Where 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
44After the header lines, simply append one line of text for every song
45 that is played or skipped.
46
47The 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)
58lines should be terminated with \n
59Example
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#include "plugin.h" 72#include "plugin.h"
@@ -41,18 +85,28 @@ http://www.audioscrobbler.net/wiki/Portable_Player_Logging
41/****************** constants ******************/ 85/****************** constants ******************/
42#define EV_EXIT MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFF) 86#define EV_EXIT MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFF)
43#define EV_FLUSHCACHE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFE) 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#define EV_STARTUP MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x01) 89#define EV_STARTUP MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x01)
45#define EV_TRACKCHANGE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x02) 90#define EV_TRACKCHANGE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x02)
46#define EV_TRACKFINISH MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x03) 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/* increment this on any code change that effects output */ 98/* increment this on any code change that effects output */
99#define SCROBBLER_VERSION "1.1"
100
51#define SCROBBLER_REVISION " $Revision$" 101#define SCROBBLER_REVISION " $Revision$"
52 102
53#define SCROBBLER_MAX_CACHE 32 103#define SCROBBLER_BAD_ENTRY "# FAILED - "
104
54/* longest entry I've had is 323, add a safety margin */ 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#define ITEM_HDR "#ARTIST #ALBUM #TITLE #TRACKNUM #LENGTH #RATING #TIMESTAMP #MUSICBRAINZ_TRACKID\n" 111#define ITEM_HDR "#ARTIST #ALBUM #TITLE #TRACKNUM #LENGTH #RATING #TIMESTAMP #MUSICBRAINZ_TRACKID\n"
58 112
@@ -67,7 +121,7 @@ static time_t timestamp;
67#define record_timestamp() ((void)(timestamp = rb->mktime(rb->get_time()))) 121#define record_timestamp() ((void)(timestamp = rb->mktime(rb->get_time())))
68#else /* !CONFIG_RTC */ 122#else /* !CONFIG_RTC */
69#define HDR_STR_TIMELESS " Timeless" 123#define HDR_STR_TIMELESS " Timeless"
70#define BASE_FILENAME ".scrobbler-timeless.log" 124#define BASE_FILENAME HOME_DIR "/.scrobbler-timeless.log"
71#define get_timestamp() (0l) 125#define get_timestamp() (0l)
72#define record_timestamp() ({}) 126#define record_timestamp() ({})
73#endif /* CONFIG_RTC */ 127#endif /* CONFIG_RTC */
@@ -76,9 +130,8 @@ static time_t timestamp;
76 130
77/****************** prototypes ******************/ 131/****************** prototypes ******************/
78enum plugin_status plugin_start(const void* parameter); /* entry */ 132enum plugin_status plugin_start(const void* parameter); /* entry */
79 133void play_tone(unsigned int frequency, unsigned int duration);
80/****************** globals ******************/ 134/****************** globals ******************/
81unsigned char **language_strings; /* for use with str() macro; must be init */
82/* communication to the worker thread */ 135/* communication to the worker thread */
83static struct 136static struct
84{ 137{
@@ -89,18 +142,29 @@ static struct
89 long stack[THREAD_STACK_SIZE / sizeof(long)]; 142 long stack[THREAD_STACK_SIZE / sizeof(long)];
90} gThread; 143} gThread;
91 144
92static struct 145struct cache_entry
93{ 146{
147 size_t len;
148 uint32_t crc;
149 char buf[ ];
150};
151
152static struct scrobbler_cache
153{
154 int entries;
94 char *buf; 155 char *buf;
95 int pos; 156 size_t pos;
96 size_t size; 157 size_t size;
97 bool pending; 158 bool pending;
98 bool force_flush; 159 bool force_flush;
160 struct mutex mtx;
99} gCache; 161} gCache;
100 162
101static struct lastfm_config 163static struct scrobbler_cfg
102{ 164{
165 int uniqct;
103 int savepct; 166 int savepct;
167 int minms;
104 int beeplvl; 168 int beeplvl;
105 bool playback; 169 bool playback;
106 bool verbose; 170 bool verbose;
@@ -108,16 +172,23 @@ static struct lastfm_config
108 172
109static struct configdata config[] = 173static struct configdata config[] =
110{ 174{
111 {TYPE_INT, 0, 100, { .int_p = &gConfig.savepct }, "SavePct", NULL}, 175 #define MAX_MRU (SCROBBLER_MAX_TRACK_MRU)
112 {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.playback }, "Playback", NULL}, 176 {TYPE_INT, 0, MAX_MRU, { .int_p = &gConfig.uniqct }, "UniqCt", NULL},
113 {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.verbose }, "Verbose", NULL}, 177 {TYPE_INT, 0, 100, { .int_p = &gConfig.savepct }, "SavePct", NULL},
114 {TYPE_INT, 0, 10, { .int_p = &gConfig.beeplvl }, "BeepLvl", 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};
116const int gCfg_sz = sizeof(config)/sizeof(*config); 184const int gCfg_sz = sizeof(config)/sizeof(*config);
185
117/****************** config functions *****************/ 186/****************** config functions *****************/
118static void config_set_defaults(void) 187static void config_set_defaults(void)
119{ 188{
189 gConfig.uniqct = SCROBBLER_MAX_TRACK_MRU;
120 gConfig.savepct = 50; 190 gConfig.savepct = 50;
191 gConfig.minms = 500;
121 gConfig.playback = false; 192 gConfig.playback = false;
122 gConfig.verbose = true; 193 gConfig.verbose = true;
123 gConfig.beeplvl = 10; 194 gConfig.beeplvl = 10;
@@ -127,6 +198,8 @@ static int config_settings_menu(void)
127{ 198{
128 int selection = 0; 199 int selection = 0;
129 200
201 static uint32_t crc = 0;
202
130 struct viewport parentvp[NB_SCREENS]; 203 struct viewport parentvp[NB_SCREENS];
131 FOR_NB_SCREENS(l) 204 FOR_NB_SCREENS(l)
132 { 205 {
@@ -134,48 +207,100 @@ static int config_settings_menu(void)
134 rb->viewport_set_fullscreen(&parentvp[l], l); 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 ID2P(LANG_RESUME_PLAYBACK), 220 ID2P(LANG_RESUME_PLAYBACK),
139 "Save Threshold", 221 "Save Threshold",
222 "Minimum Elapsed",
140 "Verbose", 223 "Verbose",
141 "Beep Level", 224 "Beep Level",
225 "Unique Track MRU",
226 ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS),
142 ID2P(VOICE_BLANK), 227 ID2P(VOICE_BLANK),
143 ID2P(LANG_CANCEL_0), 228 ID2P(LANG_CANCEL_0),
144 ID2P(LANG_SAVE_EXIT)); 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 do { 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 selection=rb->do_menu(&settings_menu,&selection, parentvp, true); 250 selection=rb->do_menu(&settings_menu,&selection, parentvp, true);
148 switch(selection) { 251 switch(selection) {
149 252
150 case 0: 253 case 0: /* resume playback on plugin start */
151 rb->set_bool(str(LANG_RESUME_PLAYBACK), &gConfig.playback); 254 rb->set_bool(rb->str(LANG_RESUME_PLAYBACK), &gConfig.playback);
152 break; 255 break;
153 case 1: 256 case 1: /* % of track played to indicate listened status */
154 rb->set_int("Save Threshold", "%", UNIT_PERCENT, 257 rb->set_int("Save Threshold", "%", UNIT_PERCENT,
155 &gConfig.savepct, NULL, 10, 0, 100, NULL ); 258 &gConfig.savepct, NULL, 10, 0, 100, NULL );
156 break; 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 rb->set_bool("Verbose", &gConfig.verbose); 265 rb->set_bool("Verbose", &gConfig.verbose);
159 break; 266 break;
160 case 3: 267 case 4: /* set volume of start-up beep */
161 rb->set_int("Beep Level", "", UNIT_INT, 268 rb->set_int("Beep Level", "", UNIT_INT,
162 &gConfig.beeplvl, NULL, 1, 0, 10, NULL); 269 &gConfig.beeplvl, NULL, 1, 0, 10, NULL);
163 if (gConfig.beeplvl > 0) 270 play_tone(1500, 100);
164 rb->beep_play(1500, 100, 100 * gConfig.beeplvl); 271 break;
165 case 4: /*sep*/ 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 continue; 290 continue;
167 case 5: 291 case 8: /* Cancel */
168 return -1; 292 return -1;
169 break; 293 break;
170 case 6: 294 case 9: /* Save & exit */
171 { 295 {
172 int res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 296 int res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER);
173 if (res >= 0) 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 return PLUGIN_OK; 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 return PLUGIN_ERROR; 304 return PLUGIN_ERROR;
180 } 305 }
181 case MENU_ATTACHED_USB: 306 case MENU_ATTACHED_USB:
@@ -188,12 +313,20 @@ static int config_settings_menu(void)
188} 313}
189 314
190/****************** helper fuctions ******************/ 315/****************** helper fuctions ******************/
316void 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
192int scrobbler_init(void) 322int scrobbler_init_cache(void)
193{ 323{
324 memset(&gCache, 0, sizeof(struct scrobbler_cache));
194 gCache.buf = rb->plugin_get_buffer(&gCache.size); 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 gCache.size = PLUGIN_BUFFER_SIZE - rb->plugin_reserve_buffer(reqsz); 330 gCache.size = PLUGIN_BUFFER_SIZE - rb->plugin_reserve_buffer(reqsz);
198 331
199 if (gCache.size < reqsz) 332 if (gCache.size < reqsz)
@@ -201,14 +334,80 @@ int scrobbler_init(void)
201 logf("SCROBBLER: OOM , %ld < req:%ld", gCache.size, reqsz); 334 logf("SCROBBLER: OOM , %ld < req:%ld", gCache.size, reqsz);
202 return -1; 335 return -1;
203 } 336 }
204
205 gCache.pos = 0;
206 gCache.pending = false;
207 gCache.force_flush = true; 337 gCache.force_flush = true;
208 logf("Scrobbler Initialized"); 338 rb->mutex_init(&gCache.mtx);
339 logf("SCROBBLER: Initialized");
209 return 1; 340 return 1;
210} 341}
211 342
343static 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
349static inline const char* str_chk_valid(const char *s, const char *alt)
350{
351 return (s != NULL ? s : alt);
352}
353
354static 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
403Found:
404
405 /* Promote MRU item to top of MRU */
406 hash_mru[0] = i;
407
408 return is_unique;
409}
410
212static void get_scrobbler_filename(char *path, size_t size) 411static void get_scrobbler_filename(char *path, size_t size)
213{ 412{
214 int used; 413 int used;
@@ -217,7 +416,7 @@ static void get_scrobbler_filename(char *path, size_t size)
217 416
218 if (used >= (int)size) 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 rb->memset(path, 0, size); 420 rb->memset(path, 0, size);
222 } 421 }
223} 422}
@@ -228,6 +427,9 @@ static void scrobbler_write_cache(void)
228 int fd; 427 int fd;
229 logf("%s", __func__); 428 logf("%s", __func__);
230 char scrobbler_file[MAX_PATH]; 429 char scrobbler_file[MAX_PATH];
430
431 rb->mutex_lock(&gCache.mtx);
432
231 get_scrobbler_filename(scrobbler_file, sizeof(scrobbler_file)); 433 get_scrobbler_filename(scrobbler_file, sizeof(scrobbler_file));
232 434
233 /* If the file doesn't exist, create it. 435 /* If the file doesn't exist, create it.
@@ -237,6 +439,7 @@ static void scrobbler_write_cache(void)
237 fd = rb->open(scrobbler_file, O_RDWR | O_CREAT, 0666); 439 fd = rb->open(scrobbler_file, O_RDWR | O_CREAT, 0666);
238 if(fd >= 0) 440 if(fd >= 0)
239 { 441 {
442 /* write file header */
240 rb->fdprintf(fd, "#AUDIOSCROBBLER/" SCROBBLER_VERSION "\n" 443 rb->fdprintf(fd, "#AUDIOSCROBBLER/" SCROBBLER_VERSION "\n"
241 "#TZ/UNKNOWN\n" "#CLIENT/Rockbox " 444 "#TZ/UNKNOWN\n" "#CLIENT/Rockbox "
242 TARGET_NAME SCROBBLER_REVISION 445 TARGET_NAME SCROBBLER_REVISION
@@ -251,38 +454,72 @@ static void scrobbler_write_cache(void)
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 /* write the cache entries */ 464 /* write the cache entries */
255 fd = rb->open(scrobbler_file, O_WRONLY | O_APPEND); 465 fd = rb->open(scrobbler_file, O_WRONLY | O_APPEND);
256 if(fd >= 0) 466 if(fd >= 0)
257 { 467 {
258 logf("SCROBBLER: writing %d entries", gCache.pos); 468 logf("SCROBBLER: writing %d entries", entries);
259 /* copy data to temporary storage in case data moves during I/O */ 469 /* copy cached data to storage */
260 char temp_buf[SCROBBLER_CACHE_LEN]; 470 uint32_t prev_crc = 0x0;
261 for ( i=0; i < gCache.pos; i++ ) 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); 477 logf("SCROBBLER: write %d read pos [%ld]", i, pos);
264 char* scrobbler_buf = gCache.buf; 478
265 ssize_t len = rb->strlcpy(temp_buf, scrobbler_buf+(SCROBBLER_CACHE_LEN*i), 479 struct cache_entry *entry = (struct cache_entry*)&gCache.buf[pos];
266 sizeof(temp_buf)); 480
267 if (rb->write(fd, temp_buf, len) != len) 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 break; 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 rb->close(fd); 509 rb->close(fd);
271 } 510 }
272 else 511 else
273 { 512 {
274 logf("SCROBBLER: error writing file"); 513 logf("SCROBBLER: error writing file");
514 rb->queue_post(&gThread.queue, EV_USER_ERROR, ERR_WRITING_FILE);
275 } 515 }
276 516 rb->mutex_unlock(&gCache.mtx);
277 /* clear even if unsuccessful - don't want to overflow the buffer */
278 gCache.pos = 0;
279} 517}
280 518
281#if USING_STORAGE_CALLBACK 519#if USING_STORAGE_CALLBACK
282static void scrobbler_flush_callback(void) 520static void scrobbler_flush_callback(void)
283{ 521{
284 (void) gCache.force_flush; 522 if(gCache.pos == 0)
285 if(gCache.pos <= 0)
286 return; 523 return;
287#if (CONFIG_STORAGE & STORAGE_ATA) 524#if (CONFIG_STORAGE & STORAGE_ATA)
288 else 525 else
@@ -297,76 +534,127 @@ static void scrobbler_flush_callback(void)
297} 534}
298#endif 535#endif
299 536
300static inline char* str_chk_valid(char *s, char *alt)
301{
302 return (s != NULL ? s : alt);
303}
304
305static unsigned long scrobbler_get_threshold(unsigned long length) 537static unsigned long scrobbler_get_threshold(unsigned long length)
306{ 538{
307 /* length is assumed to be in miliseconds */ 539 /* length is assumed to be in miliseconds */
308 return length / 100 * gConfig.savepct; 540 return length / 100 * gConfig.savepct;
541}
542
543static int create_log_entry(const struct mp3entry *id,
544 struct cache_entry *entry, int *trk_info_len)
545{
546 #define SEP "\t"
547 #define EOL "\n"
548 char* artist = id->artist ? id->artist : id->albumartist;
549 char rating = 'S'; /* Skipped */
550 if (id->elapsed >= scrobbler_get_threshold(id->length))
551 rating = 'L'; /* Listened */
309 552
553 char tracknum[11] = { "" };
554
555 if (id->tracknum > 0)
556 rb->snprintf(tracknum, sizeof (tracknum), "%d", id->tracknum);
557
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, ""));
570
571 #undef SEP
572 #undef EOL
573 return ret;
310} 574}
311 575
312static void scrobbler_add_to_cache(const struct mp3entry *id) 576static void scrobbler_add_to_cache(const struct mp3entry *id)
313{ 577{
314 static uint32_t last_crc = 0;
315 int trk_info_len = 0; 578 int trk_info_len = 0;
316 579
317 if ( gCache.pos >= SCROBBLER_MAX_CACHE ) 580 if (id->elapsed < (unsigned long) gConfig.minms)
318 scrobbler_write_cache(); 581 {
582 logf("SCROBBLER: skipping entry < %d ms: %s", gConfig.minms, id->path);
583 return;
584 }
319 585
320 char rating = 'S'; /* Skipped */ 586 rb->mutex_lock(&gCache.mtx);
321 char* scrobbler_buf = gCache.buf;
322 587
323 logf("SCROBBLER: add_to_cache[%d]", gCache.pos); 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();
324 591
325 if (id->elapsed >= scrobbler_get_threshold(id->length)) 592 logf("SCROBBLER: add_to_cache[%d] write pos[%ld]", gCache.entries, gCache.pos);
326 rating = 'L'; /* Listened */ 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;
327 597
328 char tracknum[11] = { "" }; 598 void *buf = &gCache.buf[gCache.pos];
599 memset(buf, 0, SCROBBLER_CACHE_LEN);
329 600
330 if (id->tracknum > 0) 601 struct cache_entry *entry = buf;
331 rb->snprintf(tracknum, sizeof (tracknum), "%d", id->tracknum);
332 602
333 char* artist = id->artist ? id->artist : id->albumartist; 603 int ret = create_log_entry(id, entry, &trk_info_len);
334 604
335 int ret = rb->snprintf(&scrobbler_buf[(SCROBBLER_CACHE_LEN*gCache.pos)], 605 if (ret <= 0 || (size_t) ret >= SCROBBLER_CACHE_LEN)
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, ""));
347
348 if ( ret >= SCROBBLER_CACHE_LEN )
349 { 606 {
350 logf("SCROBBLER: entry too long:"); 607 logf("SCROBBLER: entry too long:");
351 logf("SCROBBLER: %s", id->path); 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)], 613 /* first generate a crc over the static portion of the track info data
356 trk_info_len, 0xFFFFFFFF); 614 this and a crc of the filename will be used to detect repeat entries
357 if (crc != last_crc) 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; 624 /* finish calculating the CRC of the whole entry */
360 logf("Added %s", scrobbler_buf); 625 const void *src = entry->buf + trk_info_len;
361 gCache.pos++; 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#if USING_STORAGE_CALLBACK 649#if USING_STORAGE_CALLBACK
363 rb->register_storage_idle_func(scrobbler_flush_callback); 650 rb->register_storage_idle_func(scrobbler_flush_callback);
364#endif 651#endif
652 }
365 } 653 }
366 else 654 else
367 logf("SCROBBLER: skipping repeat entry: %s", id->path); 655 logf("SCROBBLER: skipping repeat entry: %s", id->path);
368 } 656 }
369 657 rb->mutex_unlock(&gCache.mtx);
370} 658}
371 659
372static void scrobbler_flush_cache(void) 660static void scrobbler_flush_cache(void)
@@ -387,16 +675,14 @@ static void scrobbler_flush_cache(void)
387 } 675 }
388} 676}
389 677
390static void scrobbler_change_event(unsigned short id, void *ev_data) 678static void track_change_event(unsigned short id, void *ev_data)
391{ 679{
392 (void)id; 680 (void)id;
393 logf("%s", __func__); 681 logf("%s", __func__);
394 struct mp3entry *id3 = ((struct track_event *)ev_data)->id3; 682 struct mp3entry *id3 = ((struct track_event *)ev_data)->id3;
395 683
396 /* check if track was resumed > %threshold played ( likely got saved ) 684 /* check if track was resumed > %threshold played ( likely got saved ) */
397 check for blank artist or track name */ 685 if ((id3->elapsed > scrobbler_get_threshold(id3->length)))
398 if ((id3->elapsed > scrobbler_get_threshold(id3->length))
399 || (!id3->artist && !id3->albumartist) || !id3->title)
400 { 686 {
401 gCache.pending = false; 687 gCache.pending = false;
402 logf("SCROBBLER: skipping file %s", id3->path); 688 logf("SCROBBLER: skipping file %s", id3->path);
@@ -408,6 +694,7 @@ static void scrobbler_change_event(unsigned short id, void *ev_data)
408 gCache.pending = true; 694 gCache.pending = true;
409 } 695 }
410} 696}
697
411#ifdef ROCKBOX_HAS_LOGF 698#ifdef ROCKBOX_HAS_LOGF
412static const char* track_event_info(struct track_event* te) 699static const char* track_event_info(struct track_event* te)
413{ 700{
@@ -422,12 +709,12 @@ static const char* track_event_info(struct track_event* te)
422* TEF_REWIND = 0x4, interpret as rewind, id3->elapsed is the 709* TEF_REWIND = 0x4, interpret as rewind, id3->elapsed is the
423 position before the seek back to 0 710 position before the seek back to 0
424*/ 711*/
425 logf("flag %d", te->flags); 712 logf("SCROBBLER: flag %d", te->flags);
426 return strflags[te->flags&0x7]; 713 return strflags[te->flags&0x7];
427} 714}
428
429#endif 715#endif
430static void scrobbler_finish_event(unsigned short id, void *ev_data) 716
717static void track_finish_event(unsigned short id, void *ev_data)
431{ 718{
432 (void)id; 719 (void)id;
433 struct track_event *te = ((struct track_event *)ev_data); 720 struct track_event *te = ((struct track_event *)ev_data);
@@ -439,22 +726,20 @@ static void scrobbler_finish_event(unsigned short id, void *ev_data)
439 726
440 scrobbler_add_to_cache(te->id3); 727 scrobbler_add_to_cache(te->id3);
441 } 728 }
442
443
444} 729}
445 730
446/****************** main thread + helpers ******************/ 731/****************** main thread + helpers ******************/
447static void events_unregister(void) 732static void events_unregister(void)
448{ 733{
449 /* we don't want any more events */ 734 /* we don't want any more events */
450 rb->remove_event(PLAYBACK_EVENT_TRACK_CHANGE, scrobbler_change_event); 735 rb->remove_event(PLAYBACK_EVENT_TRACK_CHANGE, track_change_event);
451 rb->remove_event(PLAYBACK_EVENT_TRACK_FINISH, scrobbler_finish_event); 736 rb->remove_event(PLAYBACK_EVENT_TRACK_FINISH, track_finish_event);
452} 737}
453 738
454static void events_register(void) 739static void events_register(void)
455{ 740{
456 rb->add_event(PLAYBACK_EVENT_TRACK_CHANGE, scrobbler_change_event); 741 rb->add_event(PLAYBACK_EVENT_TRACK_CHANGE, track_change_event);
457 rb->add_event(PLAYBACK_EVENT_TRACK_FINISH, scrobbler_finish_event); 742 rb->add_event(PLAYBACK_EVENT_TRACK_FINISH, track_finish_event);
458} 743}
459 744
460void thread(void) 745void thread(void)
@@ -478,8 +763,7 @@ void thread(void)
478 /*fall through*/ 763 /*fall through*/
479 case EV_STARTUP: 764 case EV_STARTUP:
480 events_register(); 765 events_register();
481 if (gConfig.beeplvl > 0) 766 play_tone(1500, 100);
482 rb->beep_play(1500, 100, 100 * gConfig.beeplvl);
483 break; 767 break;
484 case SYS_POWEROFF: 768 case SYS_POWEROFF:
485 case SYS_REBOOT: 769 case SYS_REBOOT:
@@ -487,7 +771,7 @@ void thread(void)
487 /*fall through*/ 771 /*fall through*/
488 case EV_EXIT: 772 case EV_EXIT:
489#if USING_STORAGE_CALLBACK 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#else 775#else
492 if (!in_usb) 776 if (!in_usb)
493 scrobbler_flush_cache(); 777 scrobbler_flush_cache();
@@ -498,6 +782,17 @@ void thread(void)
498 scrobbler_flush_cache(); 782 scrobbler_flush_cache();
499 rb->queue_reply(&gThread.queue, 0); 783 rb->queue_reply(&gThread.queue, 0);
500 break; 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 default: 796 default:
502 logf("default %ld", ev.id); 797 logf("default %ld", ev.id);
503 break; 798 break;
@@ -507,7 +802,7 @@ void thread(void)
507 802
508void thread_create(void) 803void 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 rb->queue_init(&gThread.queue, true); 806 rb->queue_init(&gThread.queue, true);
512 gThread.id = rb->create_thread(thread, gThread.stack, sizeof(gThread.stack), 807 gThread.id = rb->create_thread(thread, gThread.stack, sizeof(gThread.stack),
513 0, "Last.Fm_TSR" 808 0, "Last.Fm_TSR"
@@ -529,8 +824,8 @@ void thread_quit(void)
529 } 824 }
530} 825}
531 826
532/* callback to end the TSR plugin, called before a new one gets loaded */ 827/* callback to end the TSR plugin, called before a new plugin gets loaded */
533static int exit_tsr(bool reenter) 828static int plugin_exit_tsr(bool reenter)
534{ 829{
535 MENUITEM_STRINGLIST(menu, ID2P(LANG_AUDIOSCROBBLER), NULL, ID2P(LANG_SETTINGS), 830 MENUITEM_STRINGLIST(menu, ID2P(LANG_AUDIOSCROBBLER), NULL, ID2P(LANG_SETTINGS),
536 "Flush Cache", "Exit Plugin", ID2P(LANG_BACK)); 831 "Flush Cache", "Exit Plugin", ID2P(LANG_BACK));
@@ -550,9 +845,12 @@ static int exit_tsr(bool reenter)
550 config_settings_menu(); 845 config_settings_menu();
551 break; 846 break;
552 case 1: /* flush cache */ 847 case 1: /* flush cache */
553 rb->queue_send(&gThread.queue, EV_FLUSHCACHE, 0); 848 if (gCache.entries > 0)
554 if (gConfig.verbose) 849 {
555 rb->splashf(2*HZ, "%s Cache Flushed", str(LANG_AUDIOSCROBBLER)); 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 break; 854 break;
557 855
558 case 2: /* exit plugin - quit */ 856 case 2: /* exit plugin - quit */
@@ -572,13 +870,12 @@ static int exit_tsr(bool reenter)
572/****************** main ******************/ 870/****************** main ******************/
573static int plugin_main(const void* parameter) 871static int plugin_main(const void* parameter)
574{ 872{
575 struct lastfm_config cfg; 873 struct scrobbler_cfg cfg;
576 rb->memcpy(&cfg, & gConfig, sizeof(struct lastfm_config)); 874 rb->memcpy(&cfg, &gConfig, sizeof(struct scrobbler_cfg)); /* store settings */
577 875
578 /* Resume plugin ? */ 876 /* Resume plugin ? -- silences startup */
579 if (parameter == rb->plugin_tsr) 877 if (parameter == rb->plugin_tsr)
580 { 878 {
581
582 gConfig.beeplvl = 0; 879 gConfig.beeplvl = 0;
583 gConfig.playback = false; 880 gConfig.playback = false;
584 gConfig.verbose = false; 881 gConfig.verbose = false;
@@ -586,13 +883,13 @@ static int plugin_main(const void* parameter)
586 883
587 rb->memset(&gThread, 0, sizeof(gThread)); 884 rb->memset(&gThread, 0, sizeof(gThread));
588 if (gConfig.verbose) 885 if (gConfig.verbose)
589 rb->splashf(HZ / 2, "%s Started",str(LANG_AUDIOSCROBBLER)); 886 rb->splashf(HZ / 2, "%s Started",rb->str(LANG_AUDIOSCROBBLER));
590 logf("%s: %s Started", __func__, 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 thread_create(); 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 if (gConfig.playback) 894 if (gConfig.playback)
598 return PLUGIN_GOTO_WPS; 895 return PLUGIN_GOTO_WPS;
@@ -607,8 +904,8 @@ enum plugin_status plugin_start(const void* parameter)
607 /* now go ahead and have fun! */ 904 /* now go ahead and have fun! */
608 if (rb->usb_inserted() == true) 905 if (rb->usb_inserted() == true)
609 return PLUGIN_USB_CONNECTED; 906 return PLUGIN_USB_CONNECTED;
610 language_strings = rb->language_strings; 907
611 if (scrobbler_init() < 0) 908 if (scrobbler_init_cache() < 0)
612 return PLUGIN_ERROR; 909 return PLUGIN_ERROR;
613 910
614 config_set_defaults(); 911 config_set_defaults();
@@ -616,10 +913,9 @@ enum plugin_status plugin_start(const void* parameter)
616 if (configfile_load(CFG_FILE, config, gCfg_sz, CFG_VER) < 0) 913 if (configfile_load(CFG_FILE, config, gCfg_sz, CFG_VER) < 0)
617 { 914 {
618 /* If the loading failed, save a new config file */ 915 /* If the loading failed, save a new config file */
619 config_set_defaults();
620 configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 916 configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER);
621 917 if (gConfig.verbose)
622 rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); 918 rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS));
623 } 919 }
624 920
625 int ret = plugin_main(parameter); 921 int ret = plugin_main(parameter);