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}