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}