From c3fd32bdf29626c9a7464a84ca68f99ac58f7053 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Fri, 13 Dec 2024 12:25:02 -0500 Subject: [PATCH] [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 --- apps/lang/english.lang | 28 + apps/main.c | 1 + apps/menus/playback_menu.c | 3 + apps/playback.c | 108 +- apps/playback.h | 3 + apps/plugin.c | 1 + apps/plugin.h | 1 + apps/plugins/lastfm_scrobbler.c | 1022 +++++++---------- apps/plugins/lastfm_scrobbler_viewer.c | 112 +- apps/settings.h | 1 + apps/settings_list.c | 1 + manual/configure_rockbox/playback_options.tex | 12 + manual/plugins/lastfm_scrobbler.tex | 53 + manual/plugins/main.tex | 2 + 14 files changed, 705 insertions(+), 643 deletions(-) create mode 100644 manual/plugins/lastfm_scrobbler.tex diff --git a/apps/lang/english.lang b/apps/lang/english.lang index 4377ba8ee1..5abf704a76 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -16573,3 +16573,31 @@ *: "tracks saved" + + id: LANG_LOGGING + desc: playback logging + user: core + + *: "Logging" + + + *: "Logging" + + + *: "logging" + + + + id: LANG_VIEWLOG + desc: view Log + user: core + + *: "View log" + + + *: "View log" + + + *: "view log" + + diff --git a/apps/main.c b/apps/main.c index c570fbcbad..3d8bdc3432 100644 --- a/apps/main.c +++ b/apps/main.c @@ -200,6 +200,7 @@ int main(void) #endif #if !defined(BOOTLOADER) + allocate_playback_log(); if (!file_exists(ROCKBOX_DIR"/playername.txt")) { int fd = open(ROCKBOX_DIR"/playername.txt", O_CREAT|O_WRONLY|O_TRUNC, 0666); diff --git a/apps/menus/playback_menu.c b/apps/menus/playback_menu.c index f28fffa8f6..052eb68bc3 100644 --- a/apps/menus/playback_menu.c +++ b/apps/menus/playback_menu.c @@ -199,6 +199,8 @@ MENUITEM_SETTING(album_art, &global_settings.album_art, albumart_callback); #endif +MENUITEM_SETTING(playback_log, &global_settings.playback_log, NULL); + MAKE_MENU(playback_settings,ID2P(LANG_PLAYBACK),0, Icon_Playback_menu, &shuffle_item, &repeat_mode, &play_selected, @@ -232,6 +234,7 @@ MAKE_MENU(playback_settings,ID2P(LANG_PLAYBACK),0, #ifdef HAVE_ALBUMART ,&album_art #endif + ,&playback_log ); /* PLAYBACK MENU */ diff --git a/apps/playback.c b/apps/playback.c index 0c0de0db0b..422445f0ff 100644 --- a/apps/playback.c +++ b/apps/playback.c @@ -46,6 +46,8 @@ #include "settings.h" #include "audiohw.h" +#include + #ifdef HAVE_TAGCACHE #include "tagcache.h" #endif @@ -346,6 +348,11 @@ static int codec_skip_status; static bool codec_seeking = false; /* Codec seeking ack expected? */ static unsigned int position_key = 0; +#if (CONFIG_STORAGE & STORAGE_ATA) +#define PLAYBACK_LOG_BUFSZ (MAX_PATH * 10) +static int playback_log_handle = 0; /* core_alloc handle for playback log buffer */ +#endif + /* Forward declarations */ enum audio_start_playback_flags { @@ -1229,6 +1236,104 @@ static void audio_handle_track_load_status(int trackstat) } } +void allocate_playback_log(void) INIT_ATTR; +void allocate_playback_log(void) +{ +#if (CONFIG_STORAGE & STORAGE_ATA) + if (global_settings.playback_log && playback_log_handle == 0) + { + playback_log_handle = core_alloc(PLAYBACK_LOG_BUFSZ); + if (playback_log_handle > 0) + { + DEBUGF("%s Allocated %d bytes\n", __func__, PLAYBACK_LOG_BUFSZ); + char *buf = core_get_data(playback_log_handle); + buf[0] = '\0'; + return; + } + } +#endif +} + +void add_playbacklog(struct mp3entry *id3) +{ + if (!global_settings.playback_log) + return; + ssize_t used = 0; + unsigned long timestamp = current_tick; +#if (CONFIG_STORAGE & STORAGE_ATA) + char *buf = NULL; + ssize_t bufsz; + + /* if the user just enabled playback logging rather than stopping playback + * to allocate a buffer or if buffer too large just flush direct to disk + * buffer will attempt to be allocated next start-up */ + if (playback_log_handle > 0) + { + buf = core_get_data(playback_log_handle); + used = strlen(buf); + bufsz = PLAYBACK_LOG_BUFSZ - used; + buf += used; + DEBUGF("%s Used %lu Remain: %lu\n", __func__, used, bufsz); + } +#endif + if (id3 && id3->elapsed > 500) /* 500 ms*/ + { +#if CONFIG_RTC + timestamp = mktime(get_time()); +#endif +#if (CONFIG_STORAGE & STORAGE_ATA) + if (buf) /* we have a buffer allocd from core */ + { + /*10:10:10:MAX_PATH\n*/ + ssize_t entrylen = snprintf(buf, bufsz,"%lu:%ld:%ld:%s\n", + timestamp, (long)id3->elapsed, (long)id3->length, id3->path); + + if (entrylen < bufsz) + { + DEBUGF("BUFFERED: time: %lu elapsed %ld/%ld saving file: %s\n", + timestamp, id3->elapsed, id3->length, id3->path); + return; /* succeed or snprintf fail return */ + } + buf[0] = '\0'; + } + /* that didn't fit, flush buffer & write this entry to disk */ +#endif + } + else + id3 = NULL; + + if (id3 || used > 0) /* flush */ + { + DEBUGF("Opening %s \n", ROCKBOX_DIR "/playback.log"); + int fd = open(ROCKBOX_DIR "/playback.log", O_WRONLY|O_CREAT|O_APPEND, 0666); + if (fd < 0) + { + return; /* failure */ + } +#if (CONFIG_STORAGE & STORAGE_ATA) + if (buf) /* we have a buffer allocd from core */ + { + buf = core_get_data_pinned(playback_log_handle); /* we might yield - pin it*/ + write(fd, buf, used); + DEBUGF("%s Writing %lu bytes of buf:\n%s\n", __func__, used, buf); + buf[0] = '\0'; + core_put_data_pinned(buf); + } +#endif + if (id3) + { + /* we have the timestamp from when we tried to add to buffer */ + DEBUGF("LOGGED: time: %lu elapsed %ld/%ld saving file: %s\n", + timestamp, id3->elapsed, id3->length, id3->path); + fdprintf(fd, "%lu:%ld:%ld:%s\n", + timestamp, (long)id3->elapsed, (long)id3->length, id3->path); + } + + close(fd); + return; + } +} + /* Send track events that use a struct track_event for data */ static void send_track_event(unsigned int id, unsigned int flags, struct mp3entry *id3) @@ -1246,9 +1351,9 @@ static void audio_playlist_track_finish(void) struct mp3entry *id3 = valid_mp3entry(ply_id3); playlist_update_resume_info(filling == STATE_ENDED ? NULL : id3); - if (id3) { + add_playbacklog(id3); send_track_event(PLAYBACK_EVENT_TRACK_FINISH, track_event_flags, id3); } @@ -3010,6 +3115,7 @@ static void audio_stop_playback(void) /* Go idle */ filling = STATE_IDLE; cancel_cpu_boost(); + add_playbacklog(NULL); } /* Pause the playback of the current track diff --git a/apps/playback.h b/apps/playback.h index e388055f9b..6953927942 100644 --- a/apps/playback.h +++ b/apps/playback.h @@ -95,4 +95,7 @@ unsigned int playback_status(void); struct mp3entry* get_temp_mp3entry(struct mp3entry *free); +void allocate_playback_log(void); +void add_playbacklog(struct mp3entry *id3); + #endif /* _PLAYBACK_H */ diff --git a/apps/plugin.c b/apps/plugin.c index 2c4b7ea6fd..1a5507538b 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -841,6 +841,7 @@ static const struct plugin_api rockbox_api = { /* new stuff at the end, sort into place next time the API gets incompatible */ + add_playbacklog, }; static int plugin_buffer_handle; diff --git a/apps/plugin.h b/apps/plugin.h index d7b5cff69b..bae25883f6 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -990,6 +990,7 @@ struct plugin_api { #endif /* new stuff at the end, sort into place next time the API gets incompatible */ + void (*add_playbacklog)(struct mp3entry *id3); }; /* plugin header */ diff --git a/apps/plugins/lastfm_scrobbler.c b/apps/plugins/lastfm_scrobbler.c index cb6a1a9d97..7a7c499184 100644 --- a/apps/plugins/lastfm_scrobbler.c +++ b/apps/plugins/lastfm_scrobbler.c @@ -7,9 +7,7 @@ * \/ \/ \/ \/ \/ * $Id$ * - * Copyright (C) 2006-2008 Robert Keevil - * Converted to Plugin - * Copyright (C) 2022 William Wilgus + * Copyright (C) 2024 William Wilgus * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -83,12 +81,7 @@ Example #endif /****************** constants ******************/ -#define EV_EXIT MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFF) -#define EV_FLUSHCACHE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFE) -#define EV_USER_ERROR MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFD) -#define EV_STARTUP MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x01) -#define EV_TRACKCHANGE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x02) -#define EV_TRACKFINISH MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x03) + #define ERR_NONE (0) #define ERR_WRITING_FILE (-1) @@ -103,101 +96,65 @@ Example #define SCROBBLER_BAD_ENTRY "# FAILED - " /* longest entry I've had is 323, add a safety margin */ -#define SCROBBLER_CACHE_LEN (512) -#define SCROBBLER_MAX_CACHE (32 * SCROBBLER_CACHE_LEN) - -#define SCROBBLER_MAX_TRACK_MRU (32) /* list of hashes to detect repeats */ +#define SCROBBLER_MAXENTRY_LEN (512) #define ITEM_HDR "#ARTIST #ALBUM #TITLE #TRACKNUM #LENGTH #RATING #TIMESTAMP #MUSICBRAINZ_TRACKID\n" #define CFG_FILE "/lastfm_scrobbler.cfg" -#define CFG_VER 1 +#define CFG_VER 3 #if CONFIG_RTC -static time_t timestamp; #define BASE_FILENAME HOME_DIR "/.scrobbler.log" #define HDR_STR_TIMELESS -#define get_timestamp() ((long)timestamp) -#define record_timestamp() ((void)(timestamp = rb->mktime(rb->get_time()))) #else /* !CONFIG_RTC */ #define HDR_STR_TIMELESS " Timeless" #define BASE_FILENAME HOME_DIR "/.scrobbler-timeless.log" -#define get_timestamp() (0l) -#define record_timestamp() ({}) #endif /* CONFIG_RTC */ -#define THREAD_STACK_SIZE 4*DEFAULT_STACK_SIZE - /****************** prototypes ******************/ enum plugin_status plugin_start(const void* parameter); /* entry */ void play_tone(unsigned int frequency, unsigned int duration); -/****************** globals ******************/ -/* communication to the worker thread */ -static struct -{ - bool exiting; /* signal to the thread that we want to exit */ - bool hide_reentry; /* we may return on WPS fail, hide next invocation */ - unsigned int id; /* worker thread id */ - struct event_queue queue; /* thread event queue */ - struct queue_sender_list queue_send; - long stack[THREAD_STACK_SIZE / sizeof(long)]; -} gThread; +static int view_playback_log(void); +static int export_scrobbler_file(void); -struct cache_entry +struct scrobbler_entry { - size_t len; - uint32_t crc; - char buf[ ]; + unsigned long timestamp; + unsigned long elapsed; + unsigned long length; + char *path; }; -static struct scrobbler_cache -{ - int entries; - char *buf; - size_t pos; - size_t size; - bool pending; - bool force_flush; - struct mutex mtx; -} gCache; - static struct scrobbler_cfg { - int uniqct; int savepct; int minms; - int beeplvl; - bool playback; - bool verbose; + bool remove_dup; + bool delete_log; + } gConfig; static struct configdata config[] = { - #define MAX_MRU (SCROBBLER_MAX_TRACK_MRU) - {TYPE_INT, 0, MAX_MRU, { .int_p = &gConfig.uniqct }, "UniqCt", NULL}, - {TYPE_INT, 0, 100, { .int_p = &gConfig.savepct }, "SavePct", NULL}, - {TYPE_INT, 0, 10000, { .int_p = &gConfig.minms }, "MinMs", NULL}, - {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.playback }, "Playback", NULL}, - {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.verbose }, "Verbose", NULL}, - {TYPE_INT, 0, 10, { .int_p = &gConfig.beeplvl }, "BeepLvl", NULL}, - #undef MAX_MRU + {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.remove_dup }, "RemoveDupes", NULL}, + {TYPE_BOOL, 0, 1, { .bool_p = &gConfig.delete_log }, "DeleteLog", NULL}, + {TYPE_INT, 0, 100, { .int_p = &gConfig.savepct }, "SavePct", NULL}, + {TYPE_INT, 0, 10000, { .int_p = &gConfig.minms }, "MinMs", NULL}, }; const int gCfg_sz = sizeof(config)/sizeof(*config); /****************** config functions *****************/ static void config_set_defaults(void) { - gConfig.uniqct = SCROBBLER_MAX_TRACK_MRU; gConfig.savepct = 50; gConfig.minms = 500; - gConfig.playback = false; - gConfig.verbose = true; - gConfig.beeplvl = 10; + gConfig.remove_dup = true; + gConfig.delete_log = true; } -static int config_settings_menu(void) +static int scrobbler_menu(bool resume) { - int selection = 0; + int selection = resume ? 5 : 0; /* if resume we are returning from log view */ static uint32_t crc = 0; @@ -217,20 +174,21 @@ static int config_settings_menu(void) MENU_ITEM_COUNT(sizeof( name##_)/sizeof(*name##_)), \ { .strings = name##_},{.callback_and_desc = & name##__}}; - MENUITEM_STRINGLIST_CUSTOM(settings_menu, ID2P(LANG_SETTINGS), NULL, - ID2P(LANG_RESUME_PLAYBACK), - "Save Threshold", - "Minimum Elapsed", - "Verbose", - "Beep Level", - "Unique Track MRU", + MENUITEM_STRINGLIST_CUSTOM(settings_menu, ID2P(LANG_AUDIOSCROBBLER), NULL, + "Remove duplicates", + "Delete playback log", + "Save threshold", + "Minimum elapsed", + ID2P(VOICE_BLANK), + ID2P(LANG_VIEWLOG), ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS), ID2P(VOICE_BLANK), ID2P(LANG_CANCEL_0), - ID2P(LANG_SAVE_EXIT)); + ID2P(LANG_EXPORT)); #undef MENUITEM_STRINGLIST_CUSTOM + int res; const int items = MENU_GET_COUNT(settings_menu.flags); const unsigned int flags = settings_menu.flags & (~MENU_ITEM_COUNT(MENU_COUNT_MASK)); if (crc == 0) @@ -238,10 +196,12 @@ static int config_settings_menu(void) crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); } + bool has_log = rb->file_exists(ROCKBOX_DIR "/playback.log"); + do { - if (crc == rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF)) + if (!has_log) { - /* hide save item -- there are no changes to save */ + /* hide save item -- there is no log to export */ settings_menu.flags = flags|MENU_ITEM_COUNT((items - 1)); } else @@ -251,28 +211,38 @@ static int config_settings_menu(void) selection=rb->do_menu(&settings_menu,&selection, parentvp, true); switch(selection) { - case 0: /* resume playback on plugin start */ - rb->set_bool(rb->str(LANG_RESUME_PLAYBACK), &gConfig.playback); + case 0: /* remove duplicates */ + rb->set_bool("Remove log duplicates", &gConfig.remove_dup); break; - case 1: /* % of track played to indicate listened status */ + case 1: /* delete log */ + rb->set_bool("Delete playback log", &gConfig.delete_log); + break; + case 2: /* % of track played to indicate listened status */ rb->set_int("Save Threshold", "%", UNIT_PERCENT, &gConfig.savepct, NULL, 10, 0, 100, NULL ); break; - case 2: /* tracks played less than this will not be logged */ + case 3: /* tracks played less than this will not be logged */ rb->set_int("Minimum Elapsed", "ms", UNIT_MS, &gConfig.minms, NULL, 100, 0, 10000, NULL ); break; - case 3: /* suppress non-error messages */ - rb->set_bool("Verbose", &gConfig.verbose); + case 4: /* sep */ break; - case 4: /* set volume of start-up beep */ - rb->set_int("Beep Level", "", UNIT_INT, - &gConfig.beeplvl, NULL, 1, 0, 10, NULL); - play_tone(1500, 100); - break; - case 5: /* keep a list of tracks to prevent repeat [Skipped] entries */ - rb->set_int("Unique Track MRU Size", "", UNIT_INT, - &gConfig.uniqct, NULL, 1, 0, SCROBBLER_MAX_TRACK_MRU, NULL); + case 5: /* view playback log */ + if (crc != rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF)) + { + /* there are changes to save */ + if (!rb->yesno_pop(ID2P(LANG_SAVE_CHANGES))) + { + return view_playback_log(); + } + } + res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); + if (res >= 0) + { + crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); + logf("SCROBBLER: cfg saved %s %d bytes", CFG_FILE, gCfg_sz); + } + return view_playback_log(); break; case 6: /* set defaults */ { @@ -282,27 +252,45 @@ static int config_settings_menu(void) if(rb->gui_syncyesno_run(&prompt, NULL, NULL) == YESNO_YES) { config_set_defaults(); - if (gConfig.verbose) - rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); + rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); } break; } case 7: /*sep*/ continue; case 8: /* Cancel */ - return -1; - break; - case 9: /* Save & exit */ + has_log = false; + if (crc != rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF)) + { + /* there are changes to save */ + if (!rb->yesno_pop(ID2P(LANG_SAVE_CHANGES))) + { + return -1; + } + } + case 9: /* Export & exit */ { - int res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); + res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); if (res >= 0) { - crc = rb->crc_32(&gConfig, sizeof(struct scrobbler_cfg), 0xFFFFFFFF); logf("SCROBBLER: cfg saved %s %d bytes", CFG_FILE, gCfg_sz); - return PLUGIN_OK; } - logf("SCROBBLER: cfg FAILED (%d) %s", res, CFG_FILE); - return PLUGIN_ERROR; + else + { + logf("SCROBBLER: cfg FAILED (%d) %s", res, CFG_FILE); + } +#if defined(HAVE_ADJUSTABLE_CPU_FREQ) + if (has_log) + { + rb->cpu_boost(true); + return export_scrobbler_file(); + rb->cpu_boost(false); + } +#else + if (has_log) + return export_scrobbler_file(); +#endif + return PLUGIN_OK; } case MENU_ATTACHED_USB: return PLUGIN_USB_CONNECTED; @@ -314,104 +302,12 @@ static int config_settings_menu(void) } /****************** helper fuctions ******************/ -void play_tone(unsigned int frequency, unsigned int duration) -{ - if (gConfig.beeplvl > 0) - rb->beep_play(frequency, duration, 100 * gConfig.beeplvl); -} - -int scrobbler_init_cache(void) -{ - memset(&gCache, 0, sizeof(struct scrobbler_cache)); - gCache.buf = rb->plugin_get_buffer(&gCache.size); - - /* we need to reserve the space we want for our use in TSR plugins since - * someone else could call plugin_get_buffer() and corrupt our memory */ - size_t reqsz = SCROBBLER_MAX_CACHE; - gCache.size = PLUGIN_BUFFER_SIZE - rb->plugin_reserve_buffer(reqsz); - - if (gCache.size < reqsz) - { - logf("SCROBBLER: OOM , %zu < req:%zu", gCache.size, reqsz); - return -1; - } - gCache.force_flush = true; - rb->mutex_init(&gCache.mtx); - logf("SCROBBLER: Initialized"); - return 1; -} - -static inline size_t cache_get_entry_size(int str_len) -{ - /* entry_sz consists of the cache entry + str_len + \0NULL terminator */ - return ALIGN_UP(str_len + 1 + sizeof(struct cache_entry), alignof(struct cache_entry)); -} static inline const char* str_chk_valid(const char *s, const char *alt) { return (s != NULL ? s : alt); } -static bool track_is_unique(uint32_t hash1, uint32_t hash2) -{ - bool is_unique = false; - static uint8_t mru_len = 0; - - struct hash64 { uint32_t hash1; uint32_t hash2; }; - - static struct hash64 hash_mru[SCROBBLER_MAX_TRACK_MRU]; - struct hash64 i = {0}; - struct hash64 itmp; - uint8_t mru; - - if (mru_len > gConfig.uniqct) - mru_len = gConfig.uniqct; - - if (gConfig.uniqct < 1) - return true; - - /* Search in MRU */ - for (mru = 0; mru < mru_len; mru++) - { - /* Items shifted >> 1 */ - itmp = i; - i = hash_mru[mru]; - hash_mru[mru] = itmp; - - /* Found in MRU */ - if ((i.hash1 == hash1) && (i.hash2 == hash2)) - { - logf("SCROBBLER: hash [%jx, %jx] found in MRU @ %d", - (intmax_t)i.hash1, (intmax_t)i.hash2, mru); - goto Found; - } - } - - /* Add MRU entry */ - is_unique = true; - if (mru_len < SCROBBLER_MAX_TRACK_MRU && mru_len < gConfig.uniqct) - { - hash_mru[mru_len] = i; - mru_len++; - } - else - { - logf("SCROBBLER: hash [%jx, %jx] evicted from MRU", - (intmax_t) i.hash1, (intmax_t) i.hash2); - } - - i = (struct hash64){.hash1 = hash1, .hash2 = hash2}; - logf("SCROBBLER: hash [%jx, %jx] added to MRU[%d]", - (intmax_t)i.hash1, (intmax_t)i.hash2, mru_len); - -Found: - - /* Promote MRU item to top of MRU */ - hash_mru[0] = i; - - return is_unique; -} - static void get_scrobbler_filename(char *path, size_t size) { int used; @@ -425,22 +321,113 @@ static void get_scrobbler_filename(char *path, size_t size) } } -static void scrobbler_write_cache(void) +static unsigned long scrobbler_get_threshold(unsigned long length_ms) { - int i; - int fd; - logf("%s", __func__); - char scrobbler_file[MAX_PATH]; + /* length is assumed to be in miliseconds */ + return length_ms / 100 * gConfig.savepct; +} - rb->mutex_lock(&gCache.mtx); +static int create_log_entry(struct scrobbler_entry *entry, int output_fd) +{ + #define SEP "\t" + #define EOL "\n" + struct mp3entry id3, *id; + char *path = rb->strrchr(entry->path, '/'); + if (!path) + path = entry->path; + else + path++; /* remove slash */ + char rating = 'S'; /* Skipped */ + if (entry->elapsed >= scrobbler_get_threshold(entry->length)) + rating = 'L'; /* Listened */ + +#if (CONFIG_RTC) + unsigned long timestamp = entry->timestamp; +#else + unsigned long timestamp = 0U; +#endif + + if (!rb->get_metadata(&id3, -1, entry->path)) + { + /* failure to read metadata not fatal, write what we have */ + rb->fdprintf(output_fd, + "%s"SEP"%s"SEP"%s"SEP"%s"SEP"%d"SEP"%c"SEP"%lu"SEP"%s"EOL"", + "", + "", + path, + "-1", + (int)(entry->length / 1000), + rating, + timestamp, + ""); + return PLUGIN_OK; + } + if (!output_fd) + return PLUGIN_ERROR; + id = &id3; + + char* artist = id->artist ? id->artist : id->albumartist; + + char tracknum[11] = { "" }; + + if (id->tracknum > 0) + rb->snprintf(tracknum, sizeof (tracknum), "%d", id->tracknum); + + rb->fdprintf(output_fd, + "%s"SEP"%s"SEP"%s"SEP"%s"SEP"%d"SEP"%c"SEP"%lu"SEP"%s"EOL"", + str_chk_valid(artist, UNTAGGED), + str_chk_valid(id->album, ""), + str_chk_valid(id->title, path), + tracknum, + (int)(entry->length / 1000), + rating, + timestamp, + str_chk_valid(id->mb_track_id, "")); + #undef SEP + #undef EOL + return PLUGIN_OK; +} + +static void ask_enable_playbacklog(void) +{ + const char *lines[]={"LastFm", "Playback logging required", "Enable?"}; + const char *response[]= { + "Playback Settings:", "Logging: Enabled", + "Playback Settings:", "Logging: Disabled" + }; + const struct text_message message= {lines, 3}; + const struct text_message yes_msg= {&response[0], 2}; + const struct text_message no_msg= {&response[2], 2}; + if(rb->gui_syncyesno_run(&message, &yes_msg, &no_msg) == YESNO_YES) + { + rb->global_settings->playback_log = true; + rb->settings_save(); + rb->sleep(HZ * 2); + } +} + +static int view_playback_log(void) +{ + const char* plugin = VIEWERS_DIR "/lastfm_scrobbler_viewer.rock"; + rb->splashf(100, "Opening %s", plugin); + if (rb->file_exists(plugin)) + { + return rb->plugin_open(plugin, "-scrobbler_view_pbl"); + } + return PLUGIN_ERROR; +} + +static int open_create_scrobbler_log(void) +{ + int fd; + char scrobbler_file[MAX_PATH]; get_scrobbler_filename(scrobbler_file, sizeof(scrobbler_file)); - /* If the file doesn't exist, create it. - Check at each write since file may be deleted at any time */ + /* If the file doesn't exist, create it. */ if(!rb->file_exists(scrobbler_file)) { - fd = rb->open(scrobbler_file, O_RDWR | O_CREAT, 0666); + fd = rb->open(scrobbler_file, O_WRONLY | O_CREAT, 0666); if(fd >= 0) { /* write file header */ @@ -449,493 +436,270 @@ static void scrobbler_write_cache(void) TARGET_NAME SCROBBLER_REVISION HDR_STR_TIMELESS "\n"); rb->fdprintf(fd, ITEM_HDR); - - rb->close(fd); } else { logf("SCROBBLER: cannot create log file (%s)", scrobbler_file); } } - - int entries = gCache.entries; - size_t used = gCache.pos; - size_t pos = 0; - /* clear even if unsuccessful - we don't want to overflow the buffer */ - gCache.pos = 0; - gCache.entries = 0; - - /* write the cache entries */ - fd = rb->open(scrobbler_file, O_WRONLY | O_APPEND); - if(fd >= 0) - { - logf("SCROBBLER: writing %d entries", entries); - /* copy cached data to storage */ - uint32_t prev_crc = 0x0; - uint32_t crc; - size_t entry_sz, len; - bool err = false; - - for (i = 0; i < entries && pos < used; i++) - { - logf("SCROBBLER: write %d read pos [%zu]", i, pos); - - struct cache_entry *entry = (struct cache_entry*)&gCache.buf[pos]; - - entry_sz = cache_get_entry_size(entry->len); - crc = rb->crc_32(entry->buf, entry->len, 0xFFFFFFFF) ^ prev_crc; - prev_crc = crc; - - len = rb->strlen(entry->buf); - logf("SCROBBLER: write entry %d sz [%zu] len [%zu]", i, entry_sz, len); - - if (len != entry->len || crc != entry->crc) /* the entry is corrupted */ - { - rb->write(fd, SCROBBLER_BAD_ENTRY, sizeof(SCROBBLER_BAD_ENTRY)-1); - logf("SCROBBLER: Bad entry %d", i); - if(!err) - { - rb->queue_post(&gThread.queue, EV_USER_ERROR, ERR_WRITING_DATA); - err = true; - } - } - - logf("SCROBBLER: writing %s", entry->buf); - - if (rb->write(fd, entry->buf, len) != (ssize_t)len) - break; - - if (entry->buf[len - 1] != '\n') - rb->write(fd, "\n", 1); /* ensure newline termination */ - - pos += entry_sz; - } - rb->close(fd); - } else - { - logf("SCROBBLER: error writing file"); - rb->queue_post(&gThread.queue, EV_USER_ERROR, ERR_WRITING_FILE); - } - rb->mutex_unlock(&gCache.mtx); + fd = rb->open(scrobbler_file, O_WRONLY | O_APPEND); + + return fd; } -#if USING_STORAGE_CALLBACK -static void scrobbler_flush_callback(void) +static bool playbacklog_parse_entry(struct scrobbler_entry *entry, char *begin) { - if(gCache.pos == 0) - return; -#if (CONFIG_STORAGE & STORAGE_ATA) - else -#else - if ((gCache.pos >= SCROBBLER_MAX_CACHE / 2) || gCache.force_flush == true) -#endif - { - gCache.force_flush = false; - logf("%s", __func__); - scrobbler_write_cache(); - } -} -#endif + char *sep; + memset(entry, 0, sizeof(*entry)); -static unsigned long scrobbler_get_threshold(unsigned long length) -{ - /* length is assumed to be in miliseconds */ - return length / 100 * gConfig.savepct; + sep = rb->strchr(begin, ':'); + if (!sep) + return false; + + entry->timestamp = rb->atoi(begin); + + begin = sep + 1; + sep = rb->strchr(begin, ':'); + if (!sep) + return false; + + entry->elapsed = rb->atoi(begin); + + begin = sep + 1; + sep = rb->strchr(begin, ':'); + if (!sep) + return false; + + entry->length = rb->atoi(begin); + + begin = sep + 1; + if (*begin == '\0') + return false; + + entry->path = begin; + + if (entry->length == 0 || entry->elapsed > entry->length) + { + return false; + } + return true; /* success */ } -static int create_log_entry(const struct mp3entry *id, - struct cache_entry *entry, int *trk_info_len) +static bool cull_playback_duplicates(int fd, struct scrobbler_entry *curentry, + int cur_line, char*buf, size_t bufsz) { - #define SEP "\t" - #define EOL "\n" - char* artist = id->artist ? id->artist : id->albumartist; - char rating = 'S'; /* Skipped */ - if (id->elapsed >= scrobbler_get_threshold(id->length)) - rating = 'L'; /* Listened */ - - char tracknum[11] = { "" }; - - if (id->tracknum > 0) - rb->snprintf(tracknum, sizeof (tracknum), "%d", id->tracknum); - - int ret = rb->snprintf(entry->buf, - SCROBBLER_CACHE_LEN, - "%s"SEP"%s"SEP"%s"SEP"%s"SEP"%d%n"SEP"%c"SEP"%ld"SEP"%s"EOL"", - str_chk_valid(artist, UNTAGGED), - str_chk_valid(id->album, ""), - str_chk_valid(id->title, id->path), - tracknum, - (int)(id->length / 1000), - trk_info_len, /* receives len of the string written so far */ - rating, - get_timestamp(), - str_chk_valid(id->mb_track_id, "")); - - #undef SEP - #undef EOL - return ret; -} - -static void scrobbler_add_to_cache(const struct mp3entry *id) -{ - logf("%s", __func__); - int trk_info_len = 0; - - if (id->elapsed < (unsigned long) gConfig.minms) + int line_num = 0; + int rd, pos, pos_end; + struct scrobbler_entry compare; + rb->lseek(fd, 0, SEEK_SET); + while(1) { - logf("SCROBBLER: skipping entry < %d ms: %s", gConfig.minms, id->path); - return; - } + pos = rb->lseek(fd, 0, SEEK_CUR); + if ((rd = rb->read_line(fd, buf, bufsz)) <= 0) + break; + line_num++; + if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ + continue; + if (line_num == cur_line || !playbacklog_parse_entry(&compare, buf)) + continue; - rb->mutex_lock(&gCache.mtx); + rb->yield(); + if (rb->strcmp(curentry->path, compare.path) != 0) + continue; /* different track */ - /* not enough room left to guarantee next entry will fit so flush the cache */ - if ( gCache.pos > SCROBBLER_MAX_CACHE - SCROBBLER_CACHE_LEN ) - scrobbler_write_cache(); - - logf("SCROBBLER: add_to_cache[%d] write pos[%zu]", gCache.entries, gCache.pos); - /* use prev_crc to allow whole buffer to be checked for consistency */ - static uint32_t prev_crc = 0x0; - if (gCache.pos == 0) - prev_crc = 0x0; - - void *buf = &gCache.buf[gCache.pos]; - memset(buf, 0, SCROBBLER_CACHE_LEN); - - struct cache_entry *entry = buf; - - int ret = create_log_entry(id, entry, &trk_info_len); - - if (ret <= 0 || (size_t) ret >= SCROBBLER_CACHE_LEN) - { - logf("SCROBBLER: entry too long:"); - logf("SCROBBLER: %s", id->path); - rb->queue_post(&gThread.queue, EV_USER_ERROR, ERR_ENTRY_LENGTH); - } - else if (ret > 0) - { - /* first generate a crc over the static portion of the track info data - this and a crc of the filename will be used to detect repeat entries - */ - static uint32_t last_crc = 0; - uint32_t crc_entry = rb->crc_32(entry->buf, trk_info_len, 0xFFFFFFFF); - uint32_t crc_path = rb->crc_32(id->path, rb->strlen(id->path), 0xFFFFFFFF); - bool is_unique = track_is_unique(crc_entry, crc_path); - bool is_listened = (id->elapsed >= scrobbler_get_threshold(id->length)); - - if (is_unique || is_listened) + if (curentry->elapsed > compare.elapsed) { - /* finish calculating the CRC of the whole entry */ - const void *src = entry->buf + trk_info_len; - entry->crc = rb->crc_32(src, ret - trk_info_len, crc_entry) ^ prev_crc; - prev_crc = entry->crc; - entry->len = ret; - - /* since Listened entries are written regardless - make sure this isn't a direct repeat */ - if ((entry->crc ^ crc_path) != last_crc) - { - - if (is_listened) - last_crc = (entry->crc ^ crc_path); - else - last_crc = 0; - - size_t entry_sz = cache_get_entry_size(ret); - - logf("SCROBBLER: Added (#%d) sz[%zu] len[%d], %s", - gCache.entries, entry_sz, ret, entry->buf); - - gCache.entries++; - /* increase pos by string len + null terminator + sizeof entry */ - gCache.pos += entry_sz; - -#if USING_STORAGE_CALLBACK - rb->register_storage_idle_func(scrobbler_flush_callback); -#endif - } + /*logf("entry %s (%lu) @ %d culled\n", compare.path, compare.elapsed, line_num);*/ + pos_end = rb->lseek(fd, 0, SEEK_CUR); + rb->lseek(fd, pos, SEEK_SET); + rb->write(fd, "#", 1); /* make this entry a comment */ + rb->lseek(fd, pos_end, SEEK_SET); } - else - logf("SCROBBLER: skipping repeat entry: %s", id->path); - } - rb->mutex_unlock(&gCache.mtx); -} - -static void scrobbler_flush_cache(void) -{ - logf("%s", __func__); - /* Add any pending entries to the cache */ - if (gCache.pending) - { - logf("SCROBBLER: pending entry"); - gCache.pending = false; - if (rb->audio_status()) - scrobbler_add_to_cache(rb->audio_current_track()); - } - - /* Write the cache to disk if needed */ - if (gCache.pos > 0) - { - scrobbler_write_cache(); - } -} - -static void track_change_event(unsigned short id, void *ev_data) -{ - (void)id; - logf("%s", __func__); - struct mp3entry *id3 = ((struct track_event *)ev_data)->id3; - - /* check if track was resumed > %threshold played ( likely got saved ) */ - if ((id3->elapsed > scrobbler_get_threshold(id3->length))) - { - gCache.pending = false; - logf("SCROBBLER: skipping file %s", id3->path); - } - else - { - logf("SCROBBLER: add pending %s",id3->path); - record_timestamp(); - gCache.pending = true; - } -} - -#ifdef ROCKBOX_HAS_LOGF -static const char* track_event_info(struct track_event* te) -{ - - static const char *strflags[] = {"TEF_NONE", "TEF_CURRENT", - "TEF_AUTOSKIP", "TEF_CUR|ASKIP", - "TEF_REWIND", "TEF_CUR|REW", - "TEF_ASKIP|REW", "TEF_CUR|ASKIP|REW"}; -/*TEF_NONE = 0x0, no flags are set -* TEF_CURRENT = 0x1, event is for the current track -* TEF_AUTO_SKIP = 0x2, event is sent in context of auto skip -* TEF_REWIND = 0x4, interpret as rewind, id3->elapsed is the - position before the seek back to 0 -*/ - logf("SCROBBLER: flag %d", te->flags); - return strflags[te->flags&0x7]; -} -#endif - -static void track_finish_event(unsigned short id, void *ev_data) -{ - (void)id; - struct track_event *te = ((struct track_event *)ev_data); - logf("%s %s %s", __func__, gCache.pending?"True":"False", track_event_info(te)); - /* add entry using the currently ending track */ - if (gCache.pending && (te->flags & TEF_CURRENT) && !(te->flags & TEF_REWIND)) - { - gCache.pending = false; - - scrobbler_add_to_cache(te->id3); - } -} - -/****************** main thread + helpers ******************/ -static void events_unregister(void) -{ - /* we don't want any more events */ - rb->remove_event(PLAYBACK_EVENT_TRACK_CHANGE, track_change_event); - rb->remove_event(PLAYBACK_EVENT_TRACK_FINISH, track_finish_event); -} - -static void events_register(void) -{ - rb->add_event(PLAYBACK_EVENT_TRACK_CHANGE, track_change_event); - rb->add_event(PLAYBACK_EVENT_TRACK_FINISH, track_finish_event); -} - -void thread(void) -{ - bool in_usb = false; - - struct queue_event ev; - while (!gThread.exiting) - { - rb->queue_wait(&gThread.queue, &ev); - - switch (ev.id) + else if (curentry->elapsed < compare.elapsed) { - case SYS_USB_CONNECTED: - scrobbler_flush_cache(); - rb->usb_acknowledge(SYS_USB_CONNECTED_ACK); - in_usb = true; - break; - case SYS_USB_DISCONNECTED: - in_usb = false; - /*fall through*/ - case EV_STARTUP: - logf("SCROBBLER: Thread Started"); - events_register(); - play_tone(1500, 100); - break; - case SYS_POWEROFF: - logf("SYS_POWEROFF"); - /*fall through*/ - case SYS_REBOOT: - gCache.force_flush = true; - /*fall through*/ - case EV_EXIT: -#if USING_STORAGE_CALLBACK - rb->unregister_storage_idle_func(scrobbler_flush_callback, false); -#endif - if (!in_usb) - scrobbler_flush_cache(); - - events_unregister(); - return; - case EV_FLUSHCACHE: - scrobbler_flush_cache(); - rb->queue_reply(&gThread.queue, 0); - break; - case EV_USER_ERROR: - if (!in_usb) - { - if (ev.data == ERR_WRITING_FILE) - rb->splash(HZ, "SCROBBLER: error writing log"); - else if (ev.data == ERR_ENTRY_LENGTH) - rb->splash(HZ, "SCROBBLER: error entry too long"); - else if (ev.data == ERR_WRITING_DATA) - rb->splash(HZ, "SCROBBLER: error bad entry data"); - } - break; - default: - logf("default %ld", ev.id); - break; + /*entry is not the greatest elapsed*/ + return false; } } + return true; /* this item is unique or the greatest elapsed */ } -void thread_create(void) +static void remove_playback_duplicates(int fd, char *buf, size_t bufsz) { - /* put the thread's queue in the broadcast list */ - rb->queue_init(&gThread.queue, true); - gThread.id = rb->create_thread(thread, gThread.stack, sizeof(gThread.stack), - 0, "Last.Fm_TSR" - IF_PRIO(, PRIORITY_BACKGROUND) - IF_COP(, CPU)); - rb->queue_enable_queue_send(&gThread.queue, &gThread.queue_send, gThread.id); - rb->queue_post(&gThread.queue, EV_STARTUP, 0); - rb->yield(); -} + logf("%s()\n", __func__); + struct scrobbler_entry entry; + char tmp_buf[SCROBBLER_MAXENTRY_LEN]; + int pos, endpos; + int rd; + int line_num = 0; + rb->lseek(fd, 0, SEEK_SET); -void thread_quit(void) -{ - if (!gThread.exiting) { - gThread.exiting = true; - rb->queue_post(&gThread.queue, EV_EXIT, 0); - rb->thread_wait(gThread.id); - /* remove the thread's queue from the broadcast list */ - rb->queue_delete(&gThread.queue); - } -} - -/* callback to end the TSR plugin, called before a new plugin gets loaded */ -static int plugin_exit_tsr(bool reenter) -{ - MENUITEM_STRINGLIST(menu, ID2P(LANG_AUDIOSCROBBLER), NULL, ID2P(LANG_SETTINGS), - "Flush Cache", "Exit Plugin", ID2P(LANG_BACK)); - - const struct text_message quit_prompt = { - (const char*[]){ ID2P(LANG_AUDIOSCROBBLER), - "is currently running.", - "Quit scrobbler?" }, 3 - }; - - if (gThread.hide_reentry && - (rb->audio_status() & (AUDIO_STATUS_PLAY | AUDIO_STATUS_PAUSE)) == 0) + while(1) { - gThread.hide_reentry = false; - return PLUGIN_TSR_CONTINUE; - } + pos = rb->lseek(fd, 0, SEEK_CUR); + if ((rd = rb->read_line(fd, buf, bufsz)) <= 0) + break; - while(true) - { - int result = reenter ? rb->do_menu(&menu, NULL, NULL, false) : 2; - switch(result) + line_num++; + if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ + continue; + if (!playbacklog_parse_entry(&entry, buf)) { - case 0: /* settings */ - config_settings_menu(); - break; - case 1: /* flush cache */ - if (gCache.entries > 0) - { - rb->queue_send(&gThread.queue, EV_FLUSHCACHE, 0); - if (gConfig.verbose) - rb->splashf(2*HZ, "%s Cache Flushed", rb->str(LANG_AUDIOSCROBBLER)); - } - break; - - case 2: /* exit plugin - quit */ - if(rb->gui_syncyesno_run(&quit_prompt, NULL, NULL) == YESNO_YES) - { - scrobbler_flush_cache(); - thread_quit(); - return (reenter ? PLUGIN_TSR_TERMINATE : PLUGIN_TSR_SUSPEND); - } - /* Fall Through */ - case 3: /* back to menu */ - return PLUGIN_TSR_CONTINUE; + /*logf("%s failed parsing entry @ %d\n", __func__, line_num);*/ + continue; } + //logf("current entry %s (%lu) @ %d", entry.path, entry.elapsed, line_num); + + endpos = rb->lseek(fd, 0, SEEK_CUR); + if (!cull_playback_duplicates(fd, &entry, line_num, tmp_buf, sizeof(tmp_buf))) + { + rb->lseek(fd, pos, SEEK_SET); + /*logf("entry: %s @ %d is a duplicate", entry.path, line_num);*/ + rb->write(fd, "#", 1); /* make this entry a comment */ + endpos = 0; + line_num = 0; + } + rb->lseek(fd, endpos, SEEK_SET); } } -/****************** main ******************/ -static int plugin_main(const void* parameter) +static int export_scrobbler_file(void) { - struct scrobbler_cfg cfg; - rb->memcpy(&cfg, &gConfig, sizeof(struct scrobbler_cfg)); /* store settings */ + const char* filename = ROCKBOX_DIR "/playback.log"; + rb->splash(0, ID2P(LANG_WAIT)); + static char buf[SCROBBLER_MAXENTRY_LEN]; + struct scrobbler_entry entry; - /* Resume plugin ? -- silences startup */ - if (parameter == rb->plugin_tsr) + int tracks_saved = 0; + int line_num = 0; + int rd = 0; + + rb->remove(ROCKBOX_DIR "/playback.old"); + + int fd_copy = rb->open(ROCKBOX_DIR "/playback.old", O_RDWR | O_CREAT | O_TRUNC, 0666); + if (fd_copy < 0) { - gConfig.beeplvl = 0; - gConfig.playback = false; - gConfig.verbose = false; + logf("Scrobbler Error opening: %s\n", ROCKBOX_DIR "/playback.old"); + rb->splashf(HZ *2, "Scrobbler Error opening: %s", ROCKBOX_DIR "/playback.old"); + return PLUGIN_ERROR; + } + rb->add_playbacklog(NULL); /* ensure the log has been flushed */ + + /* We don't want any writes while copying and (possibly) deleting the log */ + bool log_enabled = rb->global_settings->playback_log; + rb->global_settings->playback_log = false; + int fd = rb->open_utf8(filename, O_RDONLY); + if (fd < 0) + { + rb->global_settings->playback_log = log_enabled; /* re-enable logging */ + logf("Scrobbler Error opening: %s\n", filename); + rb->splashf(HZ *2, "Scrobbler Error opening: %s", filename); + return PLUGIN_ERROR; + } + while(rb->read_line(fd, buf, sizeof(buf)) > 0) + { + line_num++; + if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ + continue; + if (!playbacklog_parse_entry(&entry, buf)) + { + logf("%s failed parsing entry @ line: %d\n", __func__, line_num); + continue; + } + + if ((int) entry.elapsed < gConfig.minms) + { + logf("Skipping path:'%s' @ line: %d\nelapsed: %ld length: %ld\nmin: %d\n", + entry.path, line_num, entry.elapsed, entry.length, gConfig.minms); + continue; + } + /* add a space to beginning of every line remove_playback_duplicates + * will use this to prepend '#' to entries that will be ignored */ + rb->fdprintf(fd_copy, " %s\n", buf); + tracks_saved++; + } + rb->close(fd); + logf("%s %d tracks copied\n", __func__, tracks_saved); + + if (gConfig.delete_log && tracks_saved > 0) + { + rb->remove(filename); + } + rb->global_settings->playback_log = log_enabled; /* re-enable logging */ + + if (gConfig.remove_dup && tracks_saved > 0) + remove_playback_duplicates(fd_copy, buf, sizeof(buf)); + + rb->lseek(fd_copy, 0, SEEK_SET); + + tracks_saved = 0; + int scrobbler_fd = open_create_scrobbler_log(); + line_num = 0; + while (1) + { + if ((rd = rb->read_line(fd_copy, buf, sizeof(buf))) <= 0) + break; + line_num++; + if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ + continue; + if (!playbacklog_parse_entry(&entry, buf)) + { + logf("%s failed parsing entry @ line: %d\n", __func__, line_num); + continue; + } + + logf("Read (%d) @ line: %d: timestamp: %lu\nelapsed: %ld\nlength: %ld\npath: '%s'\n", + rd, line_num, entry.timestamp, entry.elapsed, entry.length, entry.path); + int ret = create_log_entry(&entry, scrobbler_fd); + if (ret == PLUGIN_ERROR) + goto entry_error; + tracks_saved++; + /* process our valid entry */ } - rb->memset(&gThread, 0, sizeof(gThread)); - if (gConfig.verbose) - rb->splashf(HZ / 2, "%s Started",rb->str(LANG_AUDIOSCROBBLER)); - logf("%s: %s Started", __func__, rb->str(LANG_AUDIOSCROBBLER)); + logf("%s %d tracks saved", __func__, tracks_saved); + rb->close(scrobbler_fd); + rb->close(fd_copy); - rb->plugin_tsr(plugin_exit_tsr); /* stay resident */ + rb->splashf(HZ *2, "%d tracks saved", tracks_saved); - thread_create(); - rb->memcpy(&gConfig, &cfg, sizeof(struct scrobbler_cfg)); /*restore settings */ + //ROCKBOX_DIR "/playback.log" - if (gConfig.playback) - { - gThread.hide_reentry = true; - return PLUGIN_GOTO_WPS; - } return PLUGIN_OK; +entry_error: + if (scrobbler_fd > 0) + rb->close(scrobbler_fd); + rb->close(fd_copy); + return PLUGIN_ERROR; + (void)line_num; } /***************** Plugin Entry Point *****************/ enum plugin_status plugin_start(const void* parameter) { + bool resume; + const char * param_str = (const char*) parameter; + resume = (parameter && param_str[0] == '-' && rb->strcmp(param_str, "-resume") == 0); + + logf("Resume %s", resume ? "YES" : "NO"); + + if (!resume && !rb->global_settings->playback_log) + ask_enable_playbacklog(); + /* now go ahead and have fun! */ if (rb->usb_inserted() == true) return PLUGIN_USB_CONNECTED; - if (scrobbler_init_cache() < 0) - return PLUGIN_ERROR; - config_set_defaults(); if (configfile_load(CFG_FILE, config, gCfg_sz, CFG_VER) < 0) { /* If the loading failed, save a new config file */ configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER); - if (gConfig.verbose) - rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); + rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS)); } - int ret = plugin_main(parameter); - return ret; + return scrobbler_menu(resume); } diff --git a/apps/plugins/lastfm_scrobbler_viewer.c b/apps/plugins/lastfm_scrobbler_viewer.c index c35ba64918..0dfb8e9e2c 100644 --- a/apps/plugins/lastfm_scrobbler_viewer.c +++ b/apps/plugins/lastfm_scrobbler_viewer.c @@ -77,6 +77,7 @@ enum e_find_type { }; static void synclist_set(int selected_item, int items, int sel_size, struct printcell_data_t *pc_data); +static int scrobbler_read_line(int fd, char* buf, size_t buf_size); static void pc_data_set_header(struct printcell_data_t *pc_data); static void browse_file(char *buf, size_t bufsz) @@ -286,7 +287,7 @@ static const char* list_get_name_cb(int selected_item, void* data, rb->lseek(fd, file_last_seek, SEEK_SET); line_num = file_line_num; - while ((rb->read_line(fd, buf, buf_len)) > 0) + while ((scrobbler_read_line(fd, buf, buf_len)) > 0) { if(buf[0] == '#') continue; @@ -395,6 +396,45 @@ static enum themable_icons list_icon_cb(int selected_item, void *data) return Icon_NOICON; } +static int scrobbler_read_line(int fd, char* buf, size_t buf_size) +{ + int count = 0; + unsigned int pos = 0; + char sep = '\t'; + char ch, last_ch = sep; + bool comment = false; + + while (rb->read(fd, &ch, 1) > 0) + { + if (ch == sep && last_ch == sep && buf_size > pos) + buf[pos++] = ' '; + + if (count++ == 0 && ch == '#') /* skip comments */ + comment = true; + else if (!comment && ch != '\r' && buf_size > pos) + buf[pos++] = ch; + + last_ch = ch; + + if (pos > PRINTCELL_MAXLINELEN * 2) + break; + + if (ch == '\n') + { + if (!comment) + { + buf[pos] = '\0'; + if (buf_size > pos) + return count; + } + last_ch = sep; + comment = false; + count = 0; + } + } + return count; +} + /* load file entries into pc_data buffer, file should already be opened * and will be closed if the whole file was buffered */ static int file_load_entries(struct printcell_data_t *pc_data) @@ -404,18 +444,23 @@ static int file_load_entries(struct printcell_data_t *pc_data) int buffered = 0; unsigned int pos = 0; bool comment = false; - char ch; + char sep = '\t'; + char ch, last_ch = sep; int fd = pc_data->fd_cur; if (fd < 0) return 0; + size_t buf_size = pc_data->buf_size - 1; rb->lseek(fd, 0, SEEK_SET); while (rb->read(fd, &ch, 1) > 0) { + if (ch == sep && last_ch == sep && buf_size > pos) + pc_data->buf[pos++] = ' '; + if (count++ == 0 && ch == '#') /* skip comments */ comment = true; - else if (!comment && ch != '\r' && pc_data->buf_size > pos) + else if (!comment && ch != '\r' && buf_size > pos) pc_data->buf[pos++] = ch; if (items == 0 && pos > PRINTCELL_MAXLINELEN * 2) @@ -426,13 +471,14 @@ static int file_load_entries(struct printcell_data_t *pc_data) if (!comment) { pc_data->buf[pos] = '\0'; - if (pc_data->buf_size > pos) + if (buf_size > pos) { pos++; buffered++; } items++; } + last_ch = sep; comment = false; count = 0; rb->yield(); @@ -903,9 +949,32 @@ static void synclist_set(int selected_item, int items, int sel_size, struct prin SCROBBLER_MIN_COLUMNS, pc_data); if (max_cols < SCROBBLER_MIN_COLUMNS) /* not a scrobbler file? */ { - rb->gui_synclist_set_voice_callback(&lists, NULL); - pc_data->view_columns = printcell_set_columns(&lists, NULL, - "$*512$", Icon_Questionmark); + /*check for a playlist_control file or a playback log*/ + + max_cols = count_max_columns(items, ':', 3, pc_data); + + if (max_cols >= 3) + { + char headerbuf[32]; + int w = gConfig.col_width; + rb->snprintf(headerbuf, sizeof(headerbuf), + "$*%d$$*%d$$*%d$$*%d$", w, w, w, w); + + + struct printcell_settings pcs = {.cell_separator = gConfig.separator, + .title_delimeter = '$', + .text_delimeter = ':', + .hidecol_flags = gConfig.hidecol_flags}; + rb->gui_synclist_set_voice_callback(&lists, NULL); + pc_data->view_columns = printcell_set_columns(&lists, &pcs, + headerbuf, Icon_Rockbox); + } + else + { + rb->gui_synclist_set_voice_callback(&lists, NULL); + pc_data->view_columns = printcell_set_columns(&lists, NULL, + "$*512$", Icon_Questionmark); + } } int curcol = printcell_get_column_selected(); @@ -951,19 +1020,32 @@ enum plugin_status plugin_start(const void* parameter) if (parameter) { - rb->strlcpy(filename, (const char*)parameter, MAX_PATH); + rb->strlcpy(filename, (const char*)parameter, sizeof(filename)); filename[MAX_PATH - 1] = '\0'; + parameter = NULL; } if (!rb->file_exists(filename)) { - browse_file(filename, sizeof(filename)); - if (!rb->file_exists(filename)) + if (rb->strcmp(filename, "-scrobbler_view_pbl") == 0) { - rb->splash(HZ, "No Scrobbler file Goodbye."); - return PLUGIN_ERROR; + parameter = PLUGIN_APPS_DIR "/lastfm_scrobbler.rock"; + rb->strlcpy(filename, ROCKBOX_DIR "/playback.log", MAX_PATH); + if (!rb->file_exists(filename)) + { + rb->splashf(HZ * 2, "Viewer: NO log! %s", filename); + return rb->plugin_open(parameter, "-resume"); + } + } + else + { + browse_file(filename, sizeof(filename)); + if (!rb->file_exists(filename)) + { + rb->splash(HZ, "No file Goodbye."); + return PLUGIN_ERROR; + } } - } config_set_defaults(); @@ -1029,5 +1111,9 @@ enum plugin_status plugin_start(const void* parameter) } config_save(); cleanup(&printcell_data); + if (parameter) + { + return rb->plugin_open((const char*)parameter, "-resume"); + } return ret; } diff --git a/apps/settings.h b/apps/settings.h index 54c7b62236..c8afc1b8a2 100644 --- a/apps/settings.h +++ b/apps/settings.h @@ -910,6 +910,7 @@ struct user_settings #if defined(HAVE_EROS_QN_CODEC) int hp_lo_select; /* indicates automatic, headphone-only, or lineout-only operation */ #endif + bool playback_log; /* ROCKBOX_DIR/playback.log for tracks played */ }; /** global variables **/ diff --git a/apps/settings_list.c b/apps/settings_list.c index 66375c5f04..69f2f49c2b 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -2314,6 +2314,7 @@ const struct settings_list settings[] = { "auto,headphone,lineout", hp_lo_select_apply, 3, ID2P(LANG_AUTO), ID2P(LANG_HEADPHONE), ID2P(LANG_LINEOUT)), #endif + OFFON_SETTING(0, playback_log, LANG_LOGGING, false, "play log", NULL), }; const int nb_settings = sizeof(settings)/sizeof(*settings); diff --git a/manual/configure_rockbox/playback_options.tex b/manual/configure_rockbox/playback_options.tex index 0265491b94..c178dd41e5 100644 --- a/manual/configure_rockbox/playback_options.tex +++ b/manual/configure_rockbox/playback_options.tex @@ -325,3 +325,15 @@ you to configure settings related to audio playback. \setting{Prefer Image File}. The default behavior is to \setting{Prefer Embedded} album art. } + +\section{Logging}\index{Logging} + This option will record information about tracks played on the device + in the following format 'timestamp:elapsed(ms):length(ms):path' + Devices without a Real Time Clock will use current system tick. + Tracks played for a very short duration < 1 second will not be recorded + to minimize disk access while skipping quickly through songs. + You should periodically delete this log as excessive file sizes may cause + decreased device performace see \setting{LastFm Scrobbler} plugin. +\begin{verbatim} + the log can be found under '/.rockbox/playback.log' +\end{verbatim} diff --git a/manual/plugins/lastfm_scrobbler.tex b/manual/plugins/lastfm_scrobbler.tex new file mode 100644 index 0000000000..89df044944 --- /dev/null +++ b/manual/plugins/lastfm_scrobbler.tex @@ -0,0 +1,53 @@ +\subsection{LastFm Scrobbler} +The \setting{LastFm Scrobbler} plugin enables you to parse the rockbox +playback log for tracks you have played for your own logging or upload +to scrobbling services, such as Last.fm, Libre.fm or ListenBrainz. + +\setting{Playback Logging} must be enabled to record the tracks played +the plugin will ask you to enable logging if run with logging disabled. + +\subsubsection{Menu} +\begin{itemize} +\item Remove duplicates - Only keeps the same track with the most time elapsed. +\item Delete playback log - Remove the current playback log once it has been read. +\item Save threshold - Percentage of track played to be considered 'L'istened. +\item Minimum elapsed (ms) - Tracks played less than this will not be recorded in log. +\item View log - View the current playback log. +\item Revert to Default - Default settings restored. +\item Cancel - Exit, you will be asked to save any changes +\item Export - Append scrobbler log and save any changes, not visible if no playback log exists. +\end{itemize} + +After the plugin has exported the scrobbler log you can find it in the root +of the drive '.scrobbler.log' open it in the file browser to view the log. + +Subsequent exports will be appended to .scrobbler.log thus Delete playback log is advised. + +\begin{verbatim} +A copy of the log can be found in +'/rockbox/playback.old' +it will be overwritten with each export +\end{verbatim} + +\subsubsection{Format} +Data will be saved in Audioscrobbler spec at: (use wayback machine). +\url{http://www.audioscrobbler.net/wiki/Portable_Player_Logging}. + +\begin{verbatim} +The scrobbler format consists of the following data items +tab '\t' separated followed by newline '\n' +\end{verbatim} + +\begin{itemize} +\item ARTIST +\item ALBUM +\item TITLE +\item TRACKNUM +\item LENGTH +\item RATING +\item TIMESTAMP +\item MUSICBRAINZ-TRACKID +\end{itemize} + +If track info is not available (due to missing file or format limitations) +the track path will be used instead. diff --git a/manual/plugins/main.tex b/manual/plugins/main.tex index b2274f18af..9c65baa7f6 100644 --- a/manual/plugins/main.tex +++ b/manual/plugins/main.tex @@ -263,6 +263,8 @@ option from the \setting{Context Menu} (see \reference{ref:Contextmenu}).} \opt{HAVE_BACKLIGHT}{\input{plugins/lamp.tex}} +\input{plugins/lastfm_scrobbler.tex} + \input{plugins/lrcplayer.tex} \input{plugins/main_menu_config.tex}