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.

[Feature] playback logging from core

people don't like having to remember to run the TSR plugin so
lets meet them halfway

all tracks are added with timestamp, elapsed, length, trackname

added buffering for ATA devices

still needed:
-Done -- a plugin that parses for duplicates and reads the track info
to create the actual scrobbler log
(log can be truncated once dumped)
this should allow you to run the plugin at leisure

later I'd like to expand this logging to allow
track culling based on skipped songs..

remove the TSR plugin as hard disk no longer need to use it

Change-Id: Ib0b74b4c868fecb3e4941a8f4b9de7bd8728fe3e

authored by

William Wilgus and committed by
William Wilgus
c3fd32bd e5c9846c

+669 -607
+28
apps/lang/english.lang
··· 16573 16573 *: "tracks saved" 16574 16574 </voice> 16575 16575 </phrase> 16576 + <phrase> 16577 + id: LANG_LOGGING 16578 + desc: playback logging 16579 + user: core 16580 + <source> 16581 + *: "Logging" 16582 + </source> 16583 + <dest> 16584 + *: "Logging" 16585 + </dest> 16586 + <voice> 16587 + *: "logging" 16588 + </voice> 16589 + </phrase> 16590 + <phrase> 16591 + id: LANG_VIEWLOG 16592 + desc: view Log 16593 + user: core 16594 + <source> 16595 + *: "View log" 16596 + </source> 16597 + <dest> 16598 + *: "View log" 16599 + </dest> 16600 + <voice> 16601 + *: "view log" 16602 + </voice> 16603 + </phrase>
+1
apps/main.c
··· 200 200 #endif 201 201 202 202 #if !defined(BOOTLOADER) 203 + allocate_playback_log(); 203 204 if (!file_exists(ROCKBOX_DIR"/playername.txt")) 204 205 { 205 206 int fd = open(ROCKBOX_DIR"/playername.txt", O_CREAT|O_WRONLY|O_TRUNC, 0666);
+3
apps/menus/playback_menu.c
··· 199 199 albumart_callback); 200 200 #endif 201 201 202 + MENUITEM_SETTING(playback_log, &global_settings.playback_log, NULL); 203 + 202 204 MAKE_MENU(playback_settings,ID2P(LANG_PLAYBACK),0, 203 205 Icon_Playback_menu, 204 206 &shuffle_item, &repeat_mode, &play_selected, ··· 232 234 #ifdef HAVE_ALBUMART 233 235 ,&album_art 234 236 #endif 237 + ,&playback_log 235 238 ); 236 239 237 240 /* PLAYBACK MENU */
+107 -1
apps/playback.c
··· 46 46 #include "settings.h" 47 47 #include "audiohw.h" 48 48 49 + #include <stdio.h> 50 + 49 51 #ifdef HAVE_TAGCACHE 50 52 #include "tagcache.h" 51 53 #endif ··· 345 347 static int codec_skip_status; 346 348 static bool codec_seeking = false; /* Codec seeking ack expected? */ 347 349 static unsigned int position_key = 0; 350 + 351 + #if (CONFIG_STORAGE & STORAGE_ATA) 352 + #define PLAYBACK_LOG_BUFSZ (MAX_PATH * 10) 353 + static int playback_log_handle = 0; /* core_alloc handle for playback log buffer */ 354 + #endif 348 355 349 356 /* Forward declarations */ 350 357 enum audio_start_playback_flags ··· 1229 1236 } 1230 1237 } 1231 1238 1239 + void allocate_playback_log(void) INIT_ATTR; 1240 + void allocate_playback_log(void) 1241 + { 1242 + #if (CONFIG_STORAGE & STORAGE_ATA) 1243 + if (global_settings.playback_log && playback_log_handle == 0) 1244 + { 1245 + playback_log_handle = core_alloc(PLAYBACK_LOG_BUFSZ); 1246 + if (playback_log_handle > 0) 1247 + { 1248 + DEBUGF("%s Allocated %d bytes\n", __func__, PLAYBACK_LOG_BUFSZ); 1249 + char *buf = core_get_data(playback_log_handle); 1250 + buf[0] = '\0'; 1251 + return; 1252 + } 1253 + } 1254 + #endif 1255 + } 1256 + 1257 + void add_playbacklog(struct mp3entry *id3) 1258 + { 1259 + if (!global_settings.playback_log) 1260 + return; 1261 + ssize_t used = 0; 1262 + unsigned long timestamp = current_tick; 1263 + #if (CONFIG_STORAGE & STORAGE_ATA) 1264 + char *buf = NULL; 1265 + ssize_t bufsz; 1266 + 1267 + /* if the user just enabled playback logging rather than stopping playback 1268 + * to allocate a buffer or if buffer too large just flush direct to disk 1269 + * buffer will attempt to be allocated next start-up */ 1270 + if (playback_log_handle > 0) 1271 + { 1272 + buf = core_get_data(playback_log_handle); 1273 + used = strlen(buf); 1274 + bufsz = PLAYBACK_LOG_BUFSZ - used; 1275 + buf += used; 1276 + DEBUGF("%s Used %lu Remain: %lu\n", __func__, used, bufsz); 1277 + } 1278 + #endif 1279 + if (id3 && id3->elapsed > 500) /* 500 ms*/ 1280 + { 1281 + #if CONFIG_RTC 1282 + timestamp = mktime(get_time()); 1283 + #endif 1284 + #if (CONFIG_STORAGE & STORAGE_ATA) 1285 + if (buf) /* we have a buffer allocd from core */ 1286 + { 1287 + /*10:10:10:MAX_PATH\n*/ 1288 + ssize_t entrylen = snprintf(buf, bufsz,"%lu:%ld:%ld:%s\n", 1289 + timestamp, (long)id3->elapsed, (long)id3->length, id3->path); 1290 + 1291 + if (entrylen < bufsz) 1292 + { 1293 + DEBUGF("BUFFERED: time: %lu elapsed %ld/%ld saving file: %s\n", 1294 + timestamp, id3->elapsed, id3->length, id3->path); 1295 + return; /* succeed or snprintf fail return */ 1296 + } 1297 + buf[0] = '\0'; 1298 + } 1299 + /* that didn't fit, flush buffer & write this entry to disk */ 1300 + #endif 1301 + } 1302 + else 1303 + id3 = NULL; 1304 + 1305 + if (id3 || used > 0) /* flush */ 1306 + { 1307 + DEBUGF("Opening %s \n", ROCKBOX_DIR "/playback.log"); 1308 + int fd = open(ROCKBOX_DIR "/playback.log", O_WRONLY|O_CREAT|O_APPEND, 0666); 1309 + if (fd < 0) 1310 + { 1311 + return; /* failure */ 1312 + } 1313 + #if (CONFIG_STORAGE & STORAGE_ATA) 1314 + if (buf) /* we have a buffer allocd from core */ 1315 + { 1316 + buf = core_get_data_pinned(playback_log_handle); /* we might yield - pin it*/ 1317 + write(fd, buf, used); 1318 + DEBUGF("%s Writing %lu bytes of buf:\n%s\n", __func__, used, buf); 1319 + buf[0] = '\0'; 1320 + core_put_data_pinned(buf); 1321 + } 1322 + #endif 1323 + if (id3) 1324 + { 1325 + /* we have the timestamp from when we tried to add to buffer */ 1326 + DEBUGF("LOGGED: time: %lu elapsed %ld/%ld saving file: %s\n", 1327 + timestamp, id3->elapsed, id3->length, id3->path); 1328 + fdprintf(fd, "%lu:%ld:%ld:%s\n", 1329 + timestamp, (long)id3->elapsed, (long)id3->length, id3->path); 1330 + } 1331 + 1332 + close(fd); 1333 + return; 1334 + } 1335 + } 1336 + 1232 1337 /* Send track events that use a struct track_event for data */ 1233 1338 static void send_track_event(unsigned int id, unsigned int flags, 1234 1339 struct mp3entry *id3) ··· 1246 1351 struct mp3entry *id3 = valid_mp3entry(ply_id3); 1247 1352 1248 1353 playlist_update_resume_info(filling == STATE_ENDED ? NULL : id3); 1249 - 1250 1354 if (id3) 1251 1355 { 1356 + add_playbacklog(id3); 1252 1357 send_track_event(PLAYBACK_EVENT_TRACK_FINISH, 1253 1358 track_event_flags, id3); 1254 1359 } ··· 3010 3115 /* Go idle */ 3011 3116 filling = STATE_IDLE; 3012 3117 cancel_cpu_boost(); 3118 + add_playbacklog(NULL); 3013 3119 } 3014 3120 3015 3121 /* Pause the playback of the current track
+3
apps/playback.h
··· 95 95 96 96 struct mp3entry* get_temp_mp3entry(struct mp3entry *free); 97 97 98 + void allocate_playback_log(void); 99 + void add_playbacklog(struct mp3entry *id3); 100 + 98 101 #endif /* _PLAYBACK_H */
+1
apps/plugin.c
··· 841 841 842 842 /* new stuff at the end, sort into place next time 843 843 the API gets incompatible */ 844 + add_playbacklog, 844 845 }; 845 846 846 847 static int plugin_buffer_handle;
+1
apps/plugin.h
··· 990 990 #endif 991 991 /* new stuff at the end, sort into place next time 992 992 the API gets incompatible */ 993 + void (*add_playbacklog)(struct mp3entry *id3); 993 994 }; 994 995 995 996 /* plugin header */
+357 -593
apps/plugins/lastfm_scrobbler.c
··· 7 7 * \/ \/ \/ \/ \/ 8 8 * $Id$ 9 9 * 10 - * Copyright (C) 2006-2008 Robert Keevil 11 - * Converted to Plugin 12 - * Copyright (C) 2022 William Wilgus 10 + * Copyright (C) 2024 William Wilgus 13 11 * 14 12 * This program is free software; you can redistribute it and/or 15 13 * modify it under the terms of the GNU General Public License ··· 83 81 #endif 84 82 85 83 /****************** constants ******************/ 86 - #define EV_EXIT MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFF) 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) 89 - #define EV_STARTUP MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x01) 90 - #define EV_TRACKCHANGE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x02) 91 - #define EV_TRACKFINISH MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x03) 84 + 92 85 93 86 #define ERR_NONE (0) 94 87 #define ERR_WRITING_FILE (-1) ··· 103 96 #define SCROBBLER_BAD_ENTRY "# FAILED - " 104 97 105 98 /* longest entry I've had is 323, add a safety margin */ 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 */ 99 + #define SCROBBLER_MAXENTRY_LEN (512) 110 100 111 101 #define ITEM_HDR "#ARTIST #ALBUM #TITLE #TRACKNUM #LENGTH #RATING #TIMESTAMP #MUSICBRAINZ_TRACKID\n" 112 102 113 103 #define CFG_FILE "/lastfm_scrobbler.cfg" 114 - #define CFG_VER 1 104 + #define CFG_VER 3 115 105 116 106 #if CONFIG_RTC 117 - static time_t timestamp; 118 107 #define BASE_FILENAME HOME_DIR "/.scrobbler.log" 119 108 #define HDR_STR_TIMELESS 120 - #define get_timestamp() ((long)timestamp) 121 - #define record_timestamp() ((void)(timestamp = rb->mktime(rb->get_time()))) 122 109 #else /* !CONFIG_RTC */ 123 110 #define HDR_STR_TIMELESS " Timeless" 124 111 #define BASE_FILENAME HOME_DIR "/.scrobbler-timeless.log" 125 - #define get_timestamp() (0l) 126 - #define record_timestamp() ({}) 127 112 #endif /* CONFIG_RTC */ 128 113 129 - #define THREAD_STACK_SIZE 4*DEFAULT_STACK_SIZE 130 - 131 114 /****************** prototypes ******************/ 132 115 enum plugin_status plugin_start(const void* parameter); /* entry */ 133 116 void play_tone(unsigned int frequency, unsigned int duration); 134 - /****************** globals ******************/ 135 - /* communication to the worker thread */ 136 - static struct 137 - { 138 - bool exiting; /* signal to the thread that we want to exit */ 139 - bool hide_reentry; /* we may return on WPS fail, hide next invocation */ 140 - unsigned int id; /* worker thread id */ 141 - struct event_queue queue; /* thread event queue */ 142 - struct queue_sender_list queue_send; 143 - long stack[THREAD_STACK_SIZE / sizeof(long)]; 144 - } gThread; 117 + static int view_playback_log(void); 118 + static int export_scrobbler_file(void); 145 119 146 - struct cache_entry 120 + struct scrobbler_entry 147 121 { 148 - size_t len; 149 - uint32_t crc; 150 - char buf[ ]; 122 + unsigned long timestamp; 123 + unsigned long elapsed; 124 + unsigned long length; 125 + char *path; 151 126 }; 152 127 153 - static struct scrobbler_cache 154 - { 155 - int entries; 156 - char *buf; 157 - size_t pos; 158 - size_t size; 159 - bool pending; 160 - bool force_flush; 161 - struct mutex mtx; 162 - } gCache; 163 - 164 128 static struct scrobbler_cfg 165 129 { 166 - int uniqct; 167 130 int savepct; 168 131 int minms; 169 - int beeplvl; 170 - bool playback; 171 - bool verbose; 132 + bool remove_dup; 133 + bool delete_log; 134 + 172 135 } gConfig; 173 136 174 137 static struct configdata config[] = 175 138 { 176 - #define MAX_MRU (SCROBBLER_MAX_TRACK_MRU) 177 - {TYPE_INT, 0, MAX_MRU, { .int_p = &gConfig.uniqct }, "UniqCt", NULL}, 178 - {TYPE_INT, 0, 100, { .int_p = &gConfig.savepct }, "SavePct", NULL}, 179 - {TYPE_INT, 0, 10000, { .int_p = &gConfig.minms }, "MinMs", NULL}, 180 - {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.playback }, "Playback", NULL}, 181 - {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.verbose }, "Verbose", NULL}, 182 - {TYPE_INT, 0, 10, { .int_p = &gConfig.beeplvl }, "BeepLvl", NULL}, 183 - #undef MAX_MRU 139 + {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.remove_dup }, "RemoveDupes", NULL}, 140 + {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.delete_log }, "DeleteLog", NULL}, 141 + {TYPE_INT, 0, 100, { .int_p = &gConfig.savepct }, "SavePct", NULL}, 142 + {TYPE_INT, 0, 10000, { .int_p = &gConfig.minms }, "MinMs", NULL}, 184 143 }; 185 144 const int gCfg_sz = sizeof(config)/sizeof(*config); 186 145 187 146 /****************** config functions *****************/ 188 147 static void config_set_defaults(void) 189 148 { 190 - gConfig.uniqct = SCROBBLER_MAX_TRACK_MRU; 191 149 gConfig.savepct = 50; 192 150 gConfig.minms = 500; 193 - gConfig.playback = false; 194 - gConfig.verbose = true; 195 - gConfig.beeplvl = 10; 151 + gConfig.remove_dup = true; 152 + gConfig.delete_log = true; 196 153 } 197 154 198 - static int config_settings_menu(void) 155 + static int scrobbler_menu(bool resume) 199 156 { 200 - int selection = 0; 157 + int selection = resume ? 5 : 0; /* if resume we are returning from log view */ 201 158 202 159 static uint32_t crc = 0; 203 160 ··· 217 174 MENU_ITEM_COUNT(sizeof( name##_)/sizeof(*name##_)), \ 218 175 { .strings = name##_},{.callback_and_desc = & name##__}}; 219 176 220 - MENUITEM_STRINGLIST_CUSTOM(settings_menu, ID2P(LANG_SETTINGS), NULL, 221 - ID2P(LANG_RESUME_PLAYBACK), 222 - "Save Threshold", 223 - "Minimum Elapsed", 224 - "Verbose", 225 - "Beep Level", 226 - "Unique Track MRU", 177 + MENUITEM_STRINGLIST_CUSTOM(settings_menu, ID2P(LANG_AUDIOSCROBBLER), NULL, 178 + "Remove duplicates", 179 + "Delete playback log", 180 + "Save threshold", 181 + "Minimum elapsed", 182 + ID2P(VOICE_BLANK), 183 + ID2P(LANG_VIEWLOG), 227 184 ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS), 228 185 ID2P(VOICE_BLANK), 229 186 ID2P(LANG_CANCEL_0), 230 - ID2P(LANG_SAVE_EXIT)); 187 + ID2P(LANG_EXPORT)); 231 188 232 189 #undef MENUITEM_STRINGLIST_CUSTOM 233 190 191 + int res; 234 192 const int items = MENU_GET_COUNT(settings_menu.flags); 235 193 const unsigned int flags = settings_menu.flags & (~MENU_ITEM_COUNT(MENU_COUNT_MASK)); 236 194 if (crc == 0) ··· 238 196 crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); 239 197 } 240 198 199 + bool has_log = rb->file_exists(ROCKBOX_DIR "/playback.log"); 200 + 241 201 do { 242 - if (crc == rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF)) 202 + if (!has_log) 243 203 { 244 - /* hide save item -- there are no changes to save */ 204 + /* hide save item -- there is no log to export */ 245 205 settings_menu.flags = flags|MENU_ITEM_COUNT((items - 1)); 246 206 } 247 207 else ··· 251 211 selection=rb->do_menu(&settings_menu,&selection, parentvp, true); 252 212 switch(selection) { 253 213 254 - case 0: /* resume playback on plugin start */ 255 - rb->set_bool(rb->str(LANG_RESUME_PLAYBACK), &gConfig.playback); 214 + case 0: /* remove duplicates */ 215 + rb->set_bool("Remove log duplicates", &gConfig.remove_dup); 256 216 break; 257 - case 1: /* % of track played to indicate listened status */ 217 + case 1: /* delete log */ 218 + rb->set_bool("Delete playback log", &gConfig.delete_log); 219 + break; 220 + case 2: /* % of track played to indicate listened status */ 258 221 rb->set_int("Save Threshold", "%", UNIT_PERCENT, 259 222 &gConfig.savepct, NULL, 10, 0, 100, NULL ); 260 223 break; 261 - case 2: /* tracks played less than this will not be logged */ 224 + case 3: /* tracks played less than this will not be logged */ 262 225 rb->set_int("Minimum Elapsed", "ms", UNIT_MS, 263 226 &gConfig.minms, NULL, 100, 0, 10000, NULL ); 264 227 break; 265 - case 3: /* suppress non-error messages */ 266 - rb->set_bool("Verbose", &gConfig.verbose); 228 + case 4: /* sep */ 267 229 break; 268 - case 4: /* set volume of start-up beep */ 269 - rb->set_int("Beep Level", "", UNIT_INT, 270 - &gConfig.beeplvl, NULL, 1, 0, 10, NULL); 271 - play_tone(1500, 100); 272 - break; 273 - case 5: /* keep a list of tracks to prevent repeat [Skipped] entries */ 274 - rb->set_int("Unique Track MRU Size", "", UNIT_INT, 275 - &gConfig.uniqct, NULL, 1, 0, SCROBBLER_MAX_TRACK_MRU, NULL); 230 + case 5: /* view playback log */ 231 + if (crc != rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF)) 232 + { 233 + /* there are changes to save */ 234 + if (!rb->yesno_pop(ID2P(LANG_SAVE_CHANGES))) 235 + { 236 + return view_playback_log(); 237 + } 238 + } 239 + res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 240 + if (res >= 0) 241 + { 242 + crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); 243 + logf("SCROBBLER: cfg saved %s %d bytes", CFG_FILE, gCfg_sz); 244 + } 245 + return view_playback_log(); 276 246 break; 277 247 case 6: /* set defaults */ 278 248 { ··· 282 252 if(rb->gui_syncyesno_run(&prompt, NULL, NULL) == YESNO_YES) 283 253 { 284 254 config_set_defaults(); 285 - if (gConfig.verbose) 286 - rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); 255 + rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); 287 256 } 288 257 break; 289 258 } 290 259 case 7: /*sep*/ 291 260 continue; 292 261 case 8: /* Cancel */ 293 - return -1; 294 - break; 295 - case 9: /* Save & exit */ 262 + has_log = false; 263 + if (crc != rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF)) 264 + { 265 + /* there are changes to save */ 266 + if (!rb->yesno_pop(ID2P(LANG_SAVE_CHANGES))) 267 + { 268 + return -1; 269 + } 270 + } 271 + case 9: /* Export & exit */ 296 272 { 297 - int res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 273 + res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 298 274 if (res >= 0) 299 275 { 300 - crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); 301 276 logf("SCROBBLER: cfg saved %s %d bytes", CFG_FILE, gCfg_sz); 302 - return PLUGIN_OK; 277 + } 278 + else 279 + { 280 + logf("SCROBBLER: cfg FAILED (%d) %s", res, CFG_FILE); 281 + } 282 + #if defined(HAVE_ADJUSTABLE_CPU_FREQ) 283 + if (has_log) 284 + { 285 + rb->cpu_boost(true); 286 + return export_scrobbler_file(); 287 + rb->cpu_boost(false); 303 288 } 304 - logf("SCROBBLER: cfg FAILED (%d) %s", res, CFG_FILE); 305 - return PLUGIN_ERROR; 289 + #else 290 + if (has_log) 291 + return export_scrobbler_file(); 292 + #endif 293 + return PLUGIN_OK; 306 294 } 307 295 case MENU_ATTACHED_USB: 308 296 return PLUGIN_USB_CONNECTED; ··· 314 302 } 315 303 316 304 /****************** helper fuctions ******************/ 317 - void play_tone(unsigned int frequency, unsigned int duration) 305 + 306 + static inline const char* str_chk_valid(const char *s, const char *alt) 318 307 { 319 - if (gConfig.beeplvl > 0) 320 - rb->beep_play(frequency, duration, 100 * gConfig.beeplvl); 308 + return (s != NULL ? s : alt); 321 309 } 322 310 323 - int scrobbler_init_cache(void) 311 + static void get_scrobbler_filename(char *path, size_t size) 324 312 { 325 - memset(&gCache, 0, sizeof(struct scrobbler_cache)); 326 - gCache.buf = rb->plugin_get_buffer(&gCache.size); 313 + int used; 327 314 328 - /* we need to reserve the space we want for our use in TSR plugins since 329 - * someone else could call plugin_get_buffer() and corrupt our memory */ 330 - size_t reqsz = SCROBBLER_MAX_CACHE; 331 - gCache.size = PLUGIN_BUFFER_SIZE - rb->plugin_reserve_buffer(reqsz); 315 + used = rb->snprintf(path, size, "/%s", BASE_FILENAME); 332 316 333 - if (gCache.size < reqsz) 317 + if (used >= (int)size) 334 318 { 335 - logf("SCROBBLER: OOM , %zu < req:%zu", gCache.size, reqsz); 336 - return -1; 319 + logf("%s: not enough buffer space for log filename", __func__); 320 + rb->memset(path, 0, size); 337 321 } 338 - gCache.force_flush = true; 339 - rb->mutex_init(&gCache.mtx); 340 - logf("SCROBBLER: Initialized"); 341 - return 1; 342 322 } 343 323 344 - static inline size_t cache_get_entry_size(int str_len) 324 + static unsigned long scrobbler_get_threshold(unsigned long length_ms) 345 325 { 346 - /* entry_sz consists of the cache entry + str_len + \0NULL terminator */ 347 - return ALIGN_UP(str_len + 1 + sizeof(struct cache_entry), alignof(struct cache_entry)); 326 + /* length is assumed to be in miliseconds */ 327 + return length_ms / 100 * gConfig.savepct; 348 328 } 349 329 350 - static inline const char* str_chk_valid(const char *s, const char *alt) 330 + static int create_log_entry(struct scrobbler_entry *entry, int output_fd) 351 331 { 352 - return (s != NULL ? s : alt); 353 - } 332 + #define SEP "\t" 333 + #define EOL "\n" 334 + struct mp3entry id3, *id; 335 + char *path = rb->strrchr(entry->path, '/'); 336 + if (!path) 337 + path = entry->path; 338 + else 339 + path++; /* remove slash */ 340 + char rating = 'S'; /* Skipped */ 341 + if (entry->elapsed >= scrobbler_get_threshold(entry->length)) 342 + rating = 'L'; /* Listened */ 354 343 355 - static bool track_is_unique(uint32_t hash1, uint32_t hash2) 356 - { 357 - bool is_unique = false; 358 - static uint8_t mru_len = 0; 344 + #if (CONFIG_RTC) 345 + unsigned long timestamp = entry->timestamp; 346 + #else 347 + unsigned long timestamp = 0U; 348 + #endif 359 349 360 - struct hash64 { uint32_t hash1; uint32_t hash2; }; 350 + if (!rb->get_metadata(&id3, -1, entry->path)) 351 + { 352 + /* failure to read metadata not fatal, write what we have */ 353 + rb->fdprintf(output_fd, 354 + "%s"SEP"%s"SEP"%s"SEP"%s"SEP"%d"SEP"%c"SEP"%lu"SEP"%s"EOL"", 355 + "", 356 + "", 357 + path, 358 + "-1", 359 + (int)(entry->length / 1000), 360 + rating, 361 + timestamp, 362 + ""); 363 + return PLUGIN_OK; 364 + } 365 + if (!output_fd) 366 + return PLUGIN_ERROR; 367 + id = &id3; 361 368 362 - static struct hash64 hash_mru[SCROBBLER_MAX_TRACK_MRU]; 363 - struct hash64 i = {0}; 364 - struct hash64 itmp; 365 - uint8_t mru; 369 + char* artist = id->artist ? id->artist : id->albumartist; 366 370 367 - if (mru_len > gConfig.uniqct) 368 - mru_len = gConfig.uniqct; 371 + char tracknum[11] = { "" }; 369 372 370 - if (gConfig.uniqct < 1) 371 - return true; 373 + if (id->tracknum > 0) 374 + rb->snprintf(tracknum, sizeof (tracknum), "%d", id->tracknum); 372 375 373 - /* Search in MRU */ 374 - for (mru = 0; mru < mru_len; mru++) 375 - { 376 - /* Items shifted >> 1 */ 377 - itmp = i; 378 - i = hash_mru[mru]; 379 - hash_mru[mru] = itmp; 376 + rb->fdprintf(output_fd, 377 + "%s"SEP"%s"SEP"%s"SEP"%s"SEP"%d"SEP"%c"SEP"%lu"SEP"%s"EOL"", 378 + str_chk_valid(artist, UNTAGGED), 379 + str_chk_valid(id->album, ""), 380 + str_chk_valid(id->title, path), 381 + tracknum, 382 + (int)(entry->length / 1000), 383 + rating, 384 + timestamp, 385 + str_chk_valid(id->mb_track_id, "")); 386 + #undef SEP 387 + #undef EOL 388 + return PLUGIN_OK; 389 + } 380 390 381 - /* Found in MRU */ 382 - if ((i.hash1 == hash1) && (i.hash2 == hash2)) 383 - { 384 - logf("SCROBBLER: hash [%jx, %jx] found in MRU @ %d", 385 - (intmax_t)i.hash1, (intmax_t)i.hash2, mru); 386 - goto Found; 387 - } 388 - } 389 - 390 - /* Add MRU entry */ 391 - is_unique = true; 392 - if (mru_len < SCROBBLER_MAX_TRACK_MRU && mru_len < gConfig.uniqct) 393 - { 394 - hash_mru[mru_len] = i; 395 - mru_len++; 396 - } 397 - else 391 + static void ask_enable_playbacklog(void) 392 + { 393 + const char *lines[]={"LastFm", "Playback logging required", "Enable?"}; 394 + const char *response[]= { 395 + "Playback Settings:", "Logging: Enabled", 396 + "Playback Settings:", "Logging: Disabled" 397 + }; 398 + const struct text_message message= {lines, 3}; 399 + const struct text_message yes_msg= {&response[0], 2}; 400 + const struct text_message no_msg= {&response[2], 2}; 401 + if(rb->gui_syncyesno_run(&message, &yes_msg, &no_msg) == YESNO_YES) 398 402 { 399 - logf("SCROBBLER: hash [%jx, %jx] evicted from MRU", 400 - (intmax_t) i.hash1, (intmax_t) i.hash2); 403 + rb->global_settings->playback_log = true; 404 + rb->settings_save(); 405 + rb->sleep(HZ * 2); 401 406 } 402 - 403 - i = (struct hash64){.hash1 = hash1, .hash2 = hash2}; 404 - logf("SCROBBLER: hash [%jx, %jx] added to MRU[%d]", 405 - (intmax_t)i.hash1, (intmax_t)i.hash2, mru_len); 406 - 407 - Found: 408 - 409 - /* Promote MRU item to top of MRU */ 410 - hash_mru[0] = i; 411 - 412 - return is_unique; 413 407 } 414 408 415 - static void get_scrobbler_filename(char *path, size_t size) 409 + static int view_playback_log(void) 416 410 { 417 - int used; 418 - 419 - used = rb->snprintf(path, size, "/%s", BASE_FILENAME); 420 - 421 - if (used >= (int)size) 411 + const char* plugin = VIEWERS_DIR "/lastfm_scrobbler_viewer.rock"; 412 + rb->splashf(100, "Opening %s", plugin); 413 + if (rb->file_exists(plugin)) 422 414 { 423 - logf("%s: not enough buffer space for log filename", __func__); 424 - rb->memset(path, 0, size); 415 + return rb->plugin_open(plugin, "-scrobbler_view_pbl"); 425 416 } 417 + return PLUGIN_ERROR; 426 418 } 427 419 428 - static void scrobbler_write_cache(void) 420 + static int open_create_scrobbler_log(void) 429 421 { 430 - int i; 431 422 int fd; 432 - logf("%s", __func__); 433 423 char scrobbler_file[MAX_PATH]; 434 424 435 - rb->mutex_lock(&gCache.mtx); 436 - 437 425 get_scrobbler_filename(scrobbler_file, sizeof(scrobbler_file)); 438 426 439 - /* If the file doesn't exist, create it. 440 - Check at each write since file may be deleted at any time */ 427 + /* If the file doesn't exist, create it. */ 441 428 if(!rb->file_exists(scrobbler_file)) 442 429 { 443 - fd = rb->open(scrobbler_file, O_RDWR | O_CREAT, 0666); 430 + fd = rb->open(scrobbler_file, O_WRONLY | O_CREAT, 0666); 444 431 if(fd >= 0) 445 432 { 446 433 /* write file header */ ··· 449 436 TARGET_NAME SCROBBLER_REVISION 450 437 HDR_STR_TIMELESS "\n"); 451 438 rb->fdprintf(fd, ITEM_HDR); 452 - 453 - rb->close(fd); 454 439 } 455 440 else 456 441 { 457 442 logf("SCROBBLER: cannot create log file (%s)", scrobbler_file); 458 443 } 459 444 } 445 + else 446 + fd = rb->open(scrobbler_file, O_WRONLY | O_APPEND); 460 447 461 - int entries = gCache.entries; 462 - size_t used = gCache.pos; 463 - size_t pos = 0; 464 - /* clear even if unsuccessful - we don't want to overflow the buffer */ 465 - gCache.pos = 0; 466 - gCache.entries = 0; 448 + return fd; 449 + } 467 450 468 - /* write the cache entries */ 469 - fd = rb->open(scrobbler_file, O_WRONLY | O_APPEND); 470 - if(fd >= 0) 471 - { 472 - logf("SCROBBLER: writing %d entries", entries); 473 - /* copy cached data to storage */ 474 - uint32_t prev_crc = 0x0; 475 - uint32_t crc; 476 - size_t entry_sz, len; 477 - bool err = false; 451 + static bool playbacklog_parse_entry(struct scrobbler_entry *entry, char *begin) 452 + { 453 + char *sep; 454 + memset(entry, 0, sizeof(*entry)); 478 455 479 - for (i = 0; i < entries && pos < used; i++) 480 - { 481 - logf("SCROBBLER: write %d read pos [%zu]", i, pos); 456 + sep = rb->strchr(begin, ':'); 457 + if (!sep) 458 + return false; 482 459 483 - struct cache_entry *entry = (struct cache_entry*)&gCache.buf[pos]; 460 + entry->timestamp = rb->atoi(begin); 484 461 485 - entry_sz = cache_get_entry_size(entry->len); 486 - crc = rb->crc_32(entry->buf, entry->len, 0xFFFFFFFF) ^ prev_crc; 487 - prev_crc = crc; 462 + begin = sep + 1; 463 + sep = rb->strchr(begin, ':'); 464 + if (!sep) 465 + return false; 488 466 489 - len = rb->strlen(entry->buf); 490 - logf("SCROBBLER: write entry %d sz [%zu] len [%zu]", i, entry_sz, len); 467 + entry->elapsed = rb->atoi(begin); 491 468 492 - if (len != entry->len || crc != entry->crc) /* the entry is corrupted */ 493 - { 494 - rb->write(fd, SCROBBLER_BAD_ENTRY, sizeof(SCROBBLER_BAD_ENTRY)-1); 495 - logf("SCROBBLER: Bad entry %d", i); 496 - if(!err) 497 - { 498 - rb->queue_post(&gThread.queue, EV_USER_ERROR, ERR_WRITING_DATA); 499 - err = true; 500 - } 501 - } 469 + begin = sep + 1; 470 + sep = rb->strchr(begin, ':'); 471 + if (!sep) 472 + return false; 502 473 503 - logf("SCROBBLER: writing %s", entry->buf); 474 + entry->length = rb->atoi(begin); 504 475 505 - if (rb->write(fd, entry->buf, len) != (ssize_t)len) 506 - break; 476 + begin = sep + 1; 477 + if (*begin == '\0') 478 + return false; 507 479 508 - if (entry->buf[len - 1] != '\n') 509 - rb->write(fd, "\n", 1); /* ensure newline termination */ 480 + entry->path = begin; 510 481 511 - pos += entry_sz; 512 - } 513 - rb->close(fd); 514 - } 515 - else 482 + if (entry->length == 0 || entry->elapsed > entry->length) 516 483 { 517 - logf("SCROBBLER: error writing file"); 518 - rb->queue_post(&gThread.queue, EV_USER_ERROR, ERR_WRITING_FILE); 484 + return false; 519 485 } 520 - rb->mutex_unlock(&gCache.mtx); 486 + return true; /* success */ 521 487 } 522 488 523 - #if USING_STORAGE_CALLBACK 524 - static void scrobbler_flush_callback(void) 489 + static bool cull_playback_duplicates(int fd, struct scrobbler_entry *curentry, 490 + int cur_line, char*buf, size_t bufsz) 525 491 { 526 - if(gCache.pos == 0) 527 - return; 528 - #if (CONFIG_STORAGE & STORAGE_ATA) 529 - else 530 - #else 531 - if ((gCache.pos >= SCROBBLER_MAX_CACHE / 2) || gCache.force_flush == true) 532 - #endif 492 + int line_num = 0; 493 + int rd, pos, pos_end; 494 + struct scrobbler_entry compare; 495 + rb->lseek(fd, 0, SEEK_SET); 496 + while(1) 533 497 { 534 - gCache.force_flush = false; 535 - logf("%s", __func__); 536 - scrobbler_write_cache(); 537 - } 538 - } 539 - #endif 540 - 541 - static unsigned long scrobbler_get_threshold(unsigned long length) 542 - { 543 - /* length is assumed to be in miliseconds */ 544 - return length / 100 * gConfig.savepct; 545 - } 546 - 547 - static int create_log_entry(const struct mp3entry *id, 548 - struct cache_entry *entry, int *trk_info_len) 549 - { 550 - #define SEP "\t" 551 - #define EOL "\n" 552 - char* artist = id->artist ? id->artist : id->albumartist; 553 - char rating = 'S'; /* Skipped */ 554 - if (id->elapsed >= scrobbler_get_threshold(id->length)) 555 - rating = 'L'; /* Listened */ 556 - 557 - char tracknum[11] = { "" }; 558 - 559 - if (id->tracknum > 0) 560 - rb->snprintf(tracknum, sizeof (tracknum), "%d", id->tracknum); 498 + pos = rb->lseek(fd, 0, SEEK_CUR); 499 + if ((rd = rb->read_line(fd, buf, bufsz)) <= 0) 500 + break; 501 + line_num++; 502 + if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ 503 + continue; 504 + if (line_num == cur_line || !playbacklog_parse_entry(&compare, buf)) 505 + continue; 561 506 562 - int ret = rb->snprintf(entry->buf, 563 - SCROBBLER_CACHE_LEN, 564 - "%s"SEP"%s"SEP"%s"SEP"%s"SEP"%d%n"SEP"%c"SEP"%ld"SEP"%s"EOL"", 565 - str_chk_valid(artist, UNTAGGED), 566 - str_chk_valid(id->album, ""), 567 - str_chk_valid(id->title, id->path), 568 - tracknum, 569 - (int)(id->length / 1000), 570 - trk_info_len, /* receives len of the string written so far */ 571 - rating, 572 - get_timestamp(), 573 - str_chk_valid(id->mb_track_id, "")); 507 + rb->yield(); 508 + if (rb->strcmp(curentry->path, compare.path) != 0) 509 + continue; /* different track */ 574 510 575 - #undef SEP 576 - #undef EOL 577 - return ret; 511 + if (curentry->elapsed > compare.elapsed) 512 + { 513 + /*logf("entry %s (%lu) @ %d culled\n", compare.path, compare.elapsed, line_num);*/ 514 + pos_end = rb->lseek(fd, 0, SEEK_CUR); 515 + rb->lseek(fd, pos, SEEK_SET); 516 + rb->write(fd, "#", 1); /* make this entry a comment */ 517 + rb->lseek(fd, pos_end, SEEK_SET); 518 + } 519 + else if (curentry->elapsed < compare.elapsed) 520 + { 521 + /*entry is not the greatest elapsed*/ 522 + return false; 523 + } 524 + } 525 + return true; /* this item is unique or the greatest elapsed */ 578 526 } 579 527 580 - static void scrobbler_add_to_cache(const struct mp3entry *id) 528 + static void remove_playback_duplicates(int fd, char *buf, size_t bufsz) 581 529 { 582 - logf("%s", __func__); 583 - int trk_info_len = 0; 530 + logf("%s()\n", __func__); 531 + struct scrobbler_entry entry; 532 + char tmp_buf[SCROBBLER_MAXENTRY_LEN]; 533 + int pos, endpos; 534 + int rd; 535 + int line_num = 0; 536 + rb->lseek(fd, 0, SEEK_SET); 584 537 585 - if (id->elapsed < (unsigned long) gConfig.minms) 538 + while(1) 586 539 { 587 - logf("SCROBBLER: skipping entry < %d ms: %s", gConfig.minms, id->path); 588 - return; 589 - } 590 - 591 - rb->mutex_lock(&gCache.mtx); 592 - 593 - /* not enough room left to guarantee next entry will fit so flush the cache */ 594 - if ( gCache.pos > SCROBBLER_MAX_CACHE - SCROBBLER_CACHE_LEN ) 595 - scrobbler_write_cache(); 596 - 597 - logf("SCROBBLER: add_to_cache[%d] write pos[%zu]", gCache.entries, gCache.pos); 598 - /* use prev_crc to allow whole buffer to be checked for consistency */ 599 - static uint32_t prev_crc = 0x0; 600 - if (gCache.pos == 0) 601 - prev_crc = 0x0; 540 + pos = rb->lseek(fd, 0, SEEK_CUR); 541 + if ((rd = rb->read_line(fd, buf, bufsz)) <= 0) 542 + break; 602 543 603 - void *buf = &gCache.buf[gCache.pos]; 604 - memset(buf, 0, SCROBBLER_CACHE_LEN); 605 - 606 - struct cache_entry *entry = buf; 607 - 608 - int ret = create_log_entry(id, entry, &trk_info_len); 609 - 610 - if (ret <= 0 || (size_t) ret >= SCROBBLER_CACHE_LEN) 611 - { 612 - logf("SCROBBLER: entry too long:"); 613 - logf("SCROBBLER: %s", id->path); 614 - rb->queue_post(&gThread.queue, EV_USER_ERROR, ERR_ENTRY_LENGTH); 615 - } 616 - else if (ret > 0) 617 - { 618 - /* first generate a crc over the static portion of the track info data 619 - this and a crc of the filename will be used to detect repeat entries 620 - */ 621 - static uint32_t last_crc = 0; 622 - uint32_t crc_entry = rb->crc_32(entry->buf, trk_info_len, 0xFFFFFFFF); 623 - uint32_t crc_path = rb->crc_32(id->path, rb->strlen(id->path), 0xFFFFFFFF); 624 - bool is_unique = track_is_unique(crc_entry, crc_path); 625 - bool is_listened = (id->elapsed >= scrobbler_get_threshold(id->length)); 626 - 627 - if (is_unique || is_listened) 544 + line_num++; 545 + if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ 546 + continue; 547 + if (!playbacklog_parse_entry(&entry, buf)) 628 548 { 629 - /* finish calculating the CRC of the whole entry */ 630 - const void *src = entry->buf + trk_info_len; 631 - entry->crc = rb->crc_32(src, ret - trk_info_len, crc_entry) ^ prev_crc; 632 - prev_crc = entry->crc; 633 - entry->len = ret; 634 - 635 - /* since Listened entries are written regardless 636 - make sure this isn't a direct repeat */ 637 - if ((entry->crc ^ crc_path) != last_crc) 638 - { 639 - 640 - if (is_listened) 641 - last_crc = (entry->crc ^ crc_path); 642 - else 643 - last_crc = 0; 644 - 645 - size_t entry_sz = cache_get_entry_size(ret); 646 - 647 - logf("SCROBBLER: Added (#%d) sz[%zu] len[%d], %s", 648 - gCache.entries, entry_sz, ret, entry->buf); 649 - 650 - gCache.entries++; 651 - /* increase pos by string len + null terminator + sizeof entry */ 652 - gCache.pos += entry_sz; 549 + /*logf("%s failed parsing entry @ %d\n", __func__, line_num);*/ 550 + continue; 551 + } 552 + //logf("current entry %s (%lu) @ %d", entry.path, entry.elapsed, line_num); 653 553 654 - #if USING_STORAGE_CALLBACK 655 - rb->register_storage_idle_func(scrobbler_flush_callback); 656 - #endif 657 - } 554 + endpos = rb->lseek(fd, 0, SEEK_CUR); 555 + if (!cull_playback_duplicates(fd, &entry, line_num, tmp_buf, sizeof(tmp_buf))) 556 + { 557 + rb->lseek(fd, pos, SEEK_SET); 558 + /*logf("entry: %s @ %d is a duplicate", entry.path, line_num);*/ 559 + rb->write(fd, "#", 1); /* make this entry a comment */ 560 + endpos = 0; 561 + line_num = 0; 658 562 } 659 - else 660 - logf("SCROBBLER: skipping repeat entry: %s", id->path); 563 + rb->lseek(fd, endpos, SEEK_SET); 661 564 } 662 - rb->mutex_unlock(&gCache.mtx); 663 565 } 664 566 665 - static void scrobbler_flush_cache(void) 567 + static int export_scrobbler_file(void) 666 568 { 667 - logf("%s", __func__); 668 - /* Add any pending entries to the cache */ 669 - if (gCache.pending) 670 - { 671 - logf("SCROBBLER: pending entry"); 672 - gCache.pending = false; 673 - if (rb->audio_status()) 674 - scrobbler_add_to_cache(rb->audio_current_track()); 675 - } 569 + const char* filename = ROCKBOX_DIR "/playback.log"; 570 + rb->splash(0, ID2P(LANG_WAIT)); 571 + static char buf[SCROBBLER_MAXENTRY_LEN]; 572 + struct scrobbler_entry entry; 676 573 677 - /* Write the cache to disk if needed */ 678 - if (gCache.pos > 0) 679 - { 680 - scrobbler_write_cache(); 681 - } 682 - } 574 + int tracks_saved = 0; 575 + int line_num = 0; 576 + int rd = 0; 683 577 684 - static void track_change_event(unsigned short id, void *ev_data) 685 - { 686 - (void)id; 687 - logf("%s", __func__); 688 - struct mp3entry *id3 = ((struct track_event *)ev_data)->id3; 578 + rb->remove(ROCKBOX_DIR "/playback.old"); 689 579 690 - /* check if track was resumed > %threshold played ( likely got saved ) */ 691 - if ((id3->elapsed > scrobbler_get_threshold(id3->length))) 692 - { 693 - gCache.pending = false; 694 - logf("SCROBBLER: skipping file %s", id3->path); 695 - } 696 - else 580 + int fd_copy = rb->open(ROCKBOX_DIR "/playback.old", O_RDWR | O_CREAT | O_TRUNC, 0666); 581 + if (fd_copy < 0) 697 582 { 698 - logf("SCROBBLER: add pending %s",id3->path); 699 - record_timestamp(); 700 - gCache.pending = true; 583 + logf("Scrobbler Error opening: %s\n", ROCKBOX_DIR "/playback.old"); 584 + rb->splashf(HZ *2, "Scrobbler Error opening: %s", ROCKBOX_DIR "/playback.old"); 585 + return PLUGIN_ERROR; 701 586 } 702 - } 587 + rb->add_playbacklog(NULL); /* ensure the log has been flushed */ 703 588 704 - #ifdef ROCKBOX_HAS_LOGF 705 - static const char* track_event_info(struct track_event* te) 706 - { 707 - 708 - static const char *strflags[] = {"TEF_NONE", "TEF_CURRENT", 709 - "TEF_AUTOSKIP", "TEF_CUR|ASKIP", 710 - "TEF_REWIND", "TEF_CUR|REW", 711 - "TEF_ASKIP|REW", "TEF_CUR|ASKIP|REW"}; 712 - /*TEF_NONE = 0x0, no flags are set 713 - * TEF_CURRENT = 0x1, event is for the current track 714 - * TEF_AUTO_SKIP = 0x2, event is sent in context of auto skip 715 - * TEF_REWIND = 0x4, interpret as rewind, id3->elapsed is the 716 - position before the seek back to 0 717 - */ 718 - logf("SCROBBLER: flag %d", te->flags); 719 - return strflags[te->flags&0x7]; 720 - } 721 - #endif 722 - 723 - static void track_finish_event(unsigned short id, void *ev_data) 724 - { 725 - (void)id; 726 - struct track_event *te = ((struct track_event *)ev_data); 727 - logf("%s %s %s", __func__, gCache.pending?"True":"False", track_event_info(te)); 728 - /* add entry using the currently ending track */ 729 - if (gCache.pending && (te->flags & TEF_CURRENT) && !(te->flags & TEF_REWIND)) 589 + /* We don't want any writes while copying and (possibly) deleting the log */ 590 + bool log_enabled = rb->global_settings->playback_log; 591 + rb->global_settings->playback_log = false; 592 + int fd = rb->open_utf8(filename, O_RDONLY); 593 + if (fd < 0) 730 594 { 731 - gCache.pending = false; 732 - 733 - scrobbler_add_to_cache(te->id3); 595 + rb->global_settings->playback_log = log_enabled; /* re-enable logging */ 596 + logf("Scrobbler Error opening: %s\n", filename); 597 + rb->splashf(HZ *2, "Scrobbler Error opening: %s", filename); 598 + return PLUGIN_ERROR; 734 599 } 735 - } 736 - 737 - /****************** main thread + helpers ******************/ 738 - static void events_unregister(void) 739 - { 740 - /* we don't want any more events */ 741 - rb->remove_event(PLAYBACK_EVENT_TRACK_CHANGE, track_change_event); 742 - rb->remove_event(PLAYBACK_EVENT_TRACK_FINISH, track_finish_event); 743 - } 744 - 745 - static void events_register(void) 746 - { 747 - rb->add_event(PLAYBACK_EVENT_TRACK_CHANGE, track_change_event); 748 - rb->add_event(PLAYBACK_EVENT_TRACK_FINISH, track_finish_event); 749 - } 750 - 751 - void thread(void) 752 - { 753 - bool in_usb = false; 754 - 755 - struct queue_event ev; 756 - while (!gThread.exiting) 600 + while(rb->read_line(fd, buf, sizeof(buf)) > 0) 757 601 { 758 - rb->queue_wait(&gThread.queue, &ev); 602 + line_num++; 603 + if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ 604 + continue; 605 + if (!playbacklog_parse_entry(&entry, buf)) 606 + { 607 + logf("%s failed parsing entry @ line: %d\n", __func__, line_num); 608 + continue; 609 + } 759 610 760 - switch (ev.id) 611 + if ((int) entry.elapsed < gConfig.minms) 761 612 { 762 - case SYS_USB_CONNECTED: 763 - scrobbler_flush_cache(); 764 - rb->usb_acknowledge(SYS_USB_CONNECTED_ACK); 765 - in_usb = true; 766 - break; 767 - case SYS_USB_DISCONNECTED: 768 - in_usb = false; 769 - /*fall through*/ 770 - case EV_STARTUP: 771 - logf("SCROBBLER: Thread Started"); 772 - events_register(); 773 - play_tone(1500, 100); 774 - break; 775 - case SYS_POWEROFF: 776 - logf("SYS_POWEROFF"); 777 - /*fall through*/ 778 - case SYS_REBOOT: 779 - gCache.force_flush = true; 780 - /*fall through*/ 781 - case EV_EXIT: 782 - #if USING_STORAGE_CALLBACK 783 - rb->unregister_storage_idle_func(scrobbler_flush_callback, false); 784 - #endif 785 - if (!in_usb) 786 - scrobbler_flush_cache(); 787 - 788 - events_unregister(); 789 - return; 790 - case EV_FLUSHCACHE: 791 - scrobbler_flush_cache(); 792 - rb->queue_reply(&gThread.queue, 0); 793 - break; 794 - case EV_USER_ERROR: 795 - if (!in_usb) 796 - { 797 - if (ev.data == ERR_WRITING_FILE) 798 - rb->splash(HZ, "SCROBBLER: error writing log"); 799 - else if (ev.data == ERR_ENTRY_LENGTH) 800 - rb->splash(HZ, "SCROBBLER: error entry too long"); 801 - else if (ev.data == ERR_WRITING_DATA) 802 - rb->splash(HZ, "SCROBBLER: error bad entry data"); 803 - } 804 - break; 805 - default: 806 - logf("default %ld", ev.id); 807 - break; 613 + logf("Skipping path:'%s' @ line: %d\nelapsed: %ld length: %ld\nmin: %d\n", 614 + entry.path, line_num, entry.elapsed, entry.length, gConfig.minms); 615 + continue; 808 616 } 617 + /* add a space to beginning of every line remove_playback_duplicates 618 + * will use this to prepend '#' to entries that will be ignored */ 619 + rb->fdprintf(fd_copy, " %s\n", buf); 620 + tracks_saved++; 809 621 } 810 - } 622 + rb->close(fd); 623 + logf("%s %d tracks copied\n", __func__, tracks_saved); 811 624 812 - void thread_create(void) 813 - { 814 - /* put the thread's queue in the broadcast list */ 815 - rb->queue_init(&gThread.queue, true); 816 - gThread.id = rb->create_thread(thread, gThread.stack, sizeof(gThread.stack), 817 - 0, "Last.Fm_TSR" 818 - IF_PRIO(, PRIORITY_BACKGROUND) 819 - IF_COP(, CPU)); 820 - rb->queue_enable_queue_send(&gThread.queue, &gThread.queue_send, gThread.id); 821 - rb->queue_post(&gThread.queue, EV_STARTUP, 0); 822 - rb->yield(); 823 - } 824 - 825 - void thread_quit(void) 826 - { 827 - if (!gThread.exiting) { 828 - gThread.exiting = true; 829 - rb->queue_post(&gThread.queue, EV_EXIT, 0); 830 - rb->thread_wait(gThread.id); 831 - /* remove the thread's queue from the broadcast list */ 832 - rb->queue_delete(&gThread.queue); 625 + if (gConfig.delete_log && tracks_saved > 0) 626 + { 627 + rb->remove(filename); 833 628 } 834 - } 629 + rb->global_settings->playback_log = log_enabled; /* re-enable logging */ 835 630 836 - /* callback to end the TSR plugin, called before a new plugin gets loaded */ 837 - static int plugin_exit_tsr(bool reenter) 838 - { 839 - MENUITEM_STRINGLIST(menu, ID2P(LANG_AUDIOSCROBBLER), NULL, ID2P(LANG_SETTINGS), 840 - "Flush Cache", "Exit Plugin", ID2P(LANG_BACK)); 631 + if (gConfig.remove_dup && tracks_saved > 0) 632 + remove_playback_duplicates(fd_copy, buf, sizeof(buf)); 841 633 842 - const struct text_message quit_prompt = { 843 - (const char*[]){ ID2P(LANG_AUDIOSCROBBLER), 844 - "is currently running.", 845 - "Quit scrobbler?" }, 3 846 - }; 847 - 848 - if (gThread.hide_reentry && 849 - (rb->audio_status() & (AUDIO_STATUS_PLAY | AUDIO_STATUS_PAUSE)) == 0) 850 - { 851 - gThread.hide_reentry = false; 852 - return PLUGIN_TSR_CONTINUE; 853 - } 634 + rb->lseek(fd_copy, 0, SEEK_SET); 854 635 855 - while(true) 636 + tracks_saved = 0; 637 + int scrobbler_fd = open_create_scrobbler_log(); 638 + line_num = 0; 639 + while (1) 856 640 { 857 - int result = reenter ? rb->do_menu(&menu, NULL, NULL, false) : 2; 858 - switch(result) 641 + if ((rd = rb->read_line(fd_copy, buf, sizeof(buf))) <= 0) 642 + break; 643 + line_num++; 644 + if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ 645 + continue; 646 + if (!playbacklog_parse_entry(&entry, buf)) 859 647 { 860 - case 0: /* settings */ 861 - config_settings_menu(); 862 - break; 863 - case 1: /* flush cache */ 864 - if (gCache.entries > 0) 865 - { 866 - rb->queue_send(&gThread.queue, EV_FLUSHCACHE, 0); 867 - if (gConfig.verbose) 868 - rb->splashf(2*HZ, "%s Cache Flushed", rb->str(LANG_AUDIOSCROBBLER)); 869 - } 870 - break; 871 - 872 - case 2: /* exit plugin - quit */ 873 - if(rb->gui_syncyesno_run(&quit_prompt, NULL, NULL) == YESNO_YES) 874 - { 875 - scrobbler_flush_cache(); 876 - thread_quit(); 877 - return (reenter ? PLUGIN_TSR_TERMINATE : PLUGIN_TSR_SUSPEND); 878 - } 879 - /* Fall Through */ 880 - case 3: /* back to menu */ 881 - return PLUGIN_TSR_CONTINUE; 648 + logf("%s failed parsing entry @ line: %d\n", __func__, line_num); 649 + continue; 882 650 } 883 - } 884 - } 885 651 886 - /****************** main ******************/ 887 - static int plugin_main(const void* parameter) 888 - { 889 - struct scrobbler_cfg cfg; 890 - rb->memcpy(&cfg, &gConfig, sizeof(struct scrobbler_cfg)); /* store settings */ 891 - 892 - /* Resume plugin ? -- silences startup */ 893 - if (parameter == rb->plugin_tsr) 894 - { 895 - gConfig.beeplvl = 0; 896 - gConfig.playback = false; 897 - gConfig.verbose = false; 652 + logf("Read (%d) @ line: %d: timestamp: %lu\nelapsed: %ld\nlength: %ld\npath: '%s'\n", 653 + rd, line_num, entry.timestamp, entry.elapsed, entry.length, entry.path); 654 + int ret = create_log_entry(&entry, scrobbler_fd); 655 + if (ret == PLUGIN_ERROR) 656 + goto entry_error; 657 + tracks_saved++; 658 + /* process our valid entry */ 898 659 } 899 660 900 - rb->memset(&gThread, 0, sizeof(gThread)); 901 - if (gConfig.verbose) 902 - rb->splashf(HZ / 2, "%s Started",rb->str(LANG_AUDIOSCROBBLER)); 903 - logf("%s: %s Started", __func__, rb->str(LANG_AUDIOSCROBBLER)); 661 + logf("%s %d tracks saved", __func__, tracks_saved); 662 + rb->close(scrobbler_fd); 663 + rb->close(fd_copy); 904 664 905 - rb->plugin_tsr(plugin_exit_tsr); /* stay resident */ 665 + rb->splashf(HZ *2, "%d tracks saved", tracks_saved); 906 666 907 - thread_create(); 908 - rb->memcpy(&gConfig, &cfg, sizeof(struct scrobbler_cfg)); /*restore settings */ 667 + //ROCKBOX_DIR "/playback.log" 909 668 910 - if (gConfig.playback) 911 - { 912 - gThread.hide_reentry = true; 913 - return PLUGIN_GOTO_WPS; 914 - } 915 669 return PLUGIN_OK; 670 + entry_error: 671 + if (scrobbler_fd > 0) 672 + rb->close(scrobbler_fd); 673 + rb->close(fd_copy); 674 + return PLUGIN_ERROR; 675 + (void)line_num; 916 676 } 917 677 918 678 /***************** Plugin Entry Point *****************/ 919 679 920 680 enum plugin_status plugin_start(const void* parameter) 921 681 { 682 + bool resume; 683 + const char * param_str = (const char*) parameter; 684 + resume = (parameter && param_str[0] == '-' && rb->strcmp(param_str, "-resume") == 0); 685 + 686 + logf("Resume %s", resume ? "YES" : "NO"); 687 + 688 + if (!resume && !rb->global_settings->playback_log) 689 + ask_enable_playbacklog(); 690 + 922 691 /* now go ahead and have fun! */ 923 692 if (rb->usb_inserted() == true) 924 693 return PLUGIN_USB_CONNECTED; 925 694 926 - if (scrobbler_init_cache() < 0) 927 - return PLUGIN_ERROR; 928 - 929 695 config_set_defaults(); 930 696 931 697 if (configfile_load(CFG_FILE, config, gCfg_sz, CFG_VER) < 0) 932 698 { 933 699 /* If the loading failed, save a new config file */ 934 700 configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); 935 - if (gConfig.verbose) 936 - rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); 701 + rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); 937 702 } 938 703 939 - int ret = plugin_main(parameter); 940 - return ret; 704 + return scrobbler_menu(resume); 941 705 }
+99 -13
apps/plugins/lastfm_scrobbler_viewer.c
··· 77 77 }; 78 78 79 79 static void synclist_set(int selected_item, int items, int sel_size, struct printcell_data_t *pc_data); 80 + static int scrobbler_read_line(int fd, char* buf, size_t buf_size); 80 81 static void pc_data_set_header(struct printcell_data_t *pc_data); 81 82 82 83 static void browse_file(char *buf, size_t bufsz) ··· 286 287 rb->lseek(fd, file_last_seek, SEEK_SET); 287 288 line_num = file_line_num; 288 289 289 - while ((rb->read_line(fd, buf, buf_len)) > 0) 290 + while ((scrobbler_read_line(fd, buf, buf_len)) > 0) 290 291 { 291 292 if(buf[0] == '#') 292 293 continue; ··· 395 396 return Icon_NOICON; 396 397 } 397 398 399 + static int scrobbler_read_line(int fd, char* buf, size_t buf_size) 400 + { 401 + int count = 0; 402 + unsigned int pos = 0; 403 + char sep = '\t'; 404 + char ch, last_ch = sep; 405 + bool comment = false; 406 + 407 + while (rb->read(fd, &ch, 1) > 0) 408 + { 409 + if (ch == sep && last_ch == sep && buf_size > pos) 410 + buf[pos++] = ' '; 411 + 412 + if (count++ == 0 && ch == '#') /* skip comments */ 413 + comment = true; 414 + else if (!comment && ch != '\r' && buf_size > pos) 415 + buf[pos++] = ch; 416 + 417 + last_ch = ch; 418 + 419 + if (pos > PRINTCELL_MAXLINELEN * 2) 420 + break; 421 + 422 + if (ch == '\n') 423 + { 424 + if (!comment) 425 + { 426 + buf[pos] = '\0'; 427 + if (buf_size > pos) 428 + return count; 429 + } 430 + last_ch = sep; 431 + comment = false; 432 + count = 0; 433 + } 434 + } 435 + return count; 436 + } 437 + 398 438 /* load file entries into pc_data buffer, file should already be opened 399 439 * and will be closed if the whole file was buffered */ 400 440 static int file_load_entries(struct printcell_data_t *pc_data) ··· 404 444 int buffered = 0; 405 445 unsigned int pos = 0; 406 446 bool comment = false; 407 - char ch; 447 + char sep = '\t'; 448 + char ch, last_ch = sep; 408 449 int fd = pc_data->fd_cur; 409 450 if (fd < 0) 410 451 return 0; 452 + size_t buf_size = pc_data->buf_size - 1; 411 453 412 454 rb->lseek(fd, 0, SEEK_SET); 413 455 414 456 while (rb->read(fd, &ch, 1) > 0) 415 457 { 458 + if (ch == sep && last_ch == sep && buf_size > pos) 459 + pc_data->buf[pos++] = ' '; 460 + 416 461 if (count++ == 0 && ch == '#') /* skip comments */ 417 462 comment = true; 418 - else if (!comment && ch != '\r' && pc_data->buf_size > pos) 463 + else if (!comment && ch != '\r' && buf_size > pos) 419 464 pc_data->buf[pos++] = ch; 420 465 421 466 if (items == 0 && pos > PRINTCELL_MAXLINELEN * 2) ··· 426 471 if (!comment) 427 472 { 428 473 pc_data->buf[pos] = '\0'; 429 - if (pc_data->buf_size > pos) 474 + if (buf_size > pos) 430 475 { 431 476 pos++; 432 477 buffered++; 433 478 } 434 479 items++; 435 480 } 481 + last_ch = sep; 436 482 comment = false; 437 483 count = 0; 438 484 rb->yield(); ··· 903 949 SCROBBLER_MIN_COLUMNS, pc_data); 904 950 if (max_cols < SCROBBLER_MIN_COLUMNS) /* not a scrobbler file? */ 905 951 { 906 - rb->gui_synclist_set_voice_callback(&lists, NULL); 907 - pc_data->view_columns = printcell_set_columns(&lists, NULL, 908 - "$*512$", Icon_Questionmark); 952 + /*check for a playlist_control file or a playback log*/ 953 + 954 + max_cols = count_max_columns(items, ':', 3, pc_data); 955 + 956 + if (max_cols >= 3) 957 + { 958 + char headerbuf[32]; 959 + int w = gConfig.col_width; 960 + rb->snprintf(headerbuf, sizeof(headerbuf), 961 + "$*%d$$*%d$$*%d$$*%d$", w, w, w, w); 962 + 963 + 964 + struct printcell_settings pcs = {.cell_separator = gConfig.separator, 965 + .title_delimeter = '$', 966 + .text_delimeter = ':', 967 + .hidecol_flags = gConfig.hidecol_flags}; 968 + rb->gui_synclist_set_voice_callback(&lists, NULL); 969 + pc_data->view_columns = printcell_set_columns(&lists, &pcs, 970 + headerbuf, Icon_Rockbox); 971 + } 972 + else 973 + { 974 + rb->gui_synclist_set_voice_callback(&lists, NULL); 975 + pc_data->view_columns = printcell_set_columns(&lists, NULL, 976 + "$*512$", Icon_Questionmark); 977 + } 909 978 } 910 979 911 980 int curcol = printcell_get_column_selected(); ··· 951 1020 952 1021 if (parameter) 953 1022 { 954 - rb->strlcpy(filename, (const char*)parameter, MAX_PATH); 1023 + rb->strlcpy(filename, (const char*)parameter, sizeof(filename)); 955 1024 filename[MAX_PATH - 1] = '\0'; 1025 + parameter = NULL; 956 1026 } 957 1027 958 1028 if (!rb->file_exists(filename)) 959 1029 { 960 - browse_file(filename, sizeof(filename)); 961 - if (!rb->file_exists(filename)) 1030 + if (rb->strcmp(filename, "-scrobbler_view_pbl") == 0) 962 1031 { 963 - rb->splash(HZ, "No Scrobbler file Goodbye."); 964 - return PLUGIN_ERROR; 1032 + parameter = PLUGIN_APPS_DIR "/lastfm_scrobbler.rock"; 1033 + rb->strlcpy(filename, ROCKBOX_DIR "/playback.log", MAX_PATH); 1034 + if (!rb->file_exists(filename)) 1035 + { 1036 + rb->splashf(HZ * 2, "Viewer: NO log! %s", filename); 1037 + return rb->plugin_open(parameter, "-resume"); 1038 + } 965 1039 } 966 - 1040 + else 1041 + { 1042 + browse_file(filename, sizeof(filename)); 1043 + if (!rb->file_exists(filename)) 1044 + { 1045 + rb->splash(HZ, "No file Goodbye."); 1046 + return PLUGIN_ERROR; 1047 + } 1048 + } 967 1049 } 968 1050 969 1051 config_set_defaults(); ··· 1029 1111 } 1030 1112 config_save(); 1031 1113 cleanup(&printcell_data); 1114 + if (parameter) 1115 + { 1116 + return rb->plugin_open((const char*)parameter, "-resume"); 1117 + } 1032 1118 return ret; 1033 1119 }
+1
apps/settings.h
··· 910 910 #if defined(HAVE_EROS_QN_CODEC) 911 911 int hp_lo_select; /* indicates automatic, headphone-only, or lineout-only operation */ 912 912 #endif 913 + bool playback_log; /* ROCKBOX_DIR/playback.log for tracks played */ 913 914 }; 914 915 915 916 /** global variables **/
+1
apps/settings_list.c
··· 2314 2314 "auto,headphone,lineout", hp_lo_select_apply, 3, 2315 2315 ID2P(LANG_AUTO), ID2P(LANG_HEADPHONE), ID2P(LANG_LINEOUT)), 2316 2316 #endif 2317 + OFFON_SETTING(0, playback_log, LANG_LOGGING, false, "play log", NULL), 2317 2318 }; 2318 2319 2319 2320 const int nb_settings = sizeof(settings)/sizeof(*settings);
+12
manual/configure_rockbox/playback_options.tex
··· 325 325 \setting{Prefer Image File}. The default behavior is to 326 326 \setting{Prefer Embedded} album art. 327 327 } 328 + 329 + \section{Logging}\index{Logging} 330 + This option will record information about tracks played on the device 331 + in the following format 'timestamp:elapsed(ms):length(ms):path' 332 + Devices without a Real Time Clock will use current system tick. 333 + Tracks played for a very short duration < 1 second will not be recorded 334 + to minimize disk access while skipping quickly through songs. 335 + You should periodically delete this log as excessive file sizes may cause 336 + decreased device performace see \setting{LastFm Scrobbler} plugin. 337 + \begin{verbatim} 338 + the log can be found under '/.rockbox/playback.log' 339 + \end{verbatim}
+53
manual/plugins/lastfm_scrobbler.tex
··· 1 + \subsection{LastFm Scrobbler} 2 + The \setting{LastFm Scrobbler} plugin enables you to parse the rockbox 3 + playback log for tracks you have played for your own logging or upload 4 + to scrobbling services, such as Last.fm, Libre.fm or ListenBrainz. 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. 8 + 9 + \subsubsection{Menu} 10 + \begin{itemize} 11 + \item Remove duplicates - Only keeps the same track with the most time elapsed. 12 + \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. 14 + \item Minimum elapsed (ms) - Tracks played less than this will not be recorded in log. 15 + \item View log - View the current playback log. 16 + \item Revert to Default - Default settings restored. 17 + \item Cancel - Exit, you will be asked to save any changes 18 + \item Export - Append scrobbler log and save any changes, not visible if no playback log exists. 19 + \end{itemize} 20 + 21 + After the plugin has exported the scrobbler log you can find it in the root 22 + of the drive '.scrobbler.log' open it in the file browser to view the log. 23 + 24 + Subsequent exports will be appended to .scrobbler.log thus Delete playback log is advised. 25 + 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 + 32 + \subsubsection{Format} 33 + Data will be saved in Audioscrobbler spec at: (use wayback machine). 34 + \url{http://www.audioscrobbler.net/wiki/Portable_Player_Logging}. 35 + 36 + \begin{verbatim} 37 + The scrobbler format consists of the following data items 38 + tab '\t' separated followed by newline '\n' 39 + \end{verbatim} 40 + 41 + \begin{itemize} 42 + \item ARTIST 43 + \item ALBUM 44 + \item TITLE 45 + \item TRACKNUM 46 + \item LENGTH 47 + \item RATING 48 + \item TIMESTAMP 49 + \item MUSICBRAINZ-TRACKID 50 + \end{itemize} 51 + 52 + If track info is not available (due to missing file or format limitations) 53 + the track path will be used instead.
+2
manual/plugins/main.tex
··· 263 263 264 264 \opt{HAVE_BACKLIGHT}{\input{plugins/lamp.tex}} 265 265 266 + \input{plugins/lastfm_scrobbler.tex} 267 + 266 268 \input{plugins/lrcplayer.tex} 267 269 268 270 \input{plugins/main_menu_config.tex}