lastfm_scrobbler speed up track culling, better duplicate logic

rather than going through the whole file each time we can just walk
through the remainder speeds up duplicate removal by quite a bit

check timestamp of tracks to not remove multiple plays of the same song
at different times, unless back to back (then its probably a resume after
shutdown)

devices without RTC use current_tick but thats in centiseconds
convert to milliseconds so we can use the timestamp in the above calculations

my fix to the scrobbler_viewer makes the include menus disappear as
soon as you hide columns so explicitly mark if we are
dealing with a scrobbler log at load

Change-Id: I11bbebe9af45945a7e1326a5e419290086b05aaa
This commit is contained in:
William Wilgus 2025-01-13 10:40:41 -05:00
parent ab9b687607
commit 0493ee19c3
3 changed files with 59 additions and 49 deletions

View file

@ -1259,7 +1259,7 @@ void add_playbacklog(struct mp3entry *id3)
if (!global_settings.playback_log) if (!global_settings.playback_log)
return; return;
ssize_t used = 0; ssize_t used = 0;
unsigned long timestamp = current_tick; unsigned long timestamp = current_tick * (1000 / HZ); /* milliseconds */
#if (CONFIG_STORAGE & STORAGE_ATA) #if (CONFIG_STORAGE & STORAGE_ATA)
char *buf = NULL; char *buf = NULL;
ssize_t bufsz; ssize_t bufsz;

View file

@ -82,12 +82,6 @@ Example
/****************** constants ******************/ /****************** constants ******************/
#define ERR_NONE (0)
#define ERR_WRITING_FILE (-1)
#define ERR_ENTRY_LENGTH (-2)
#define ERR_WRITING_DATA (-3)
/* increment this on any code change that effects output */ /* increment this on any code change that effects output */
#define SCROBBLER_VERSION "1.1" #define SCROBBLER_VERSION "1.1"
@ -96,7 +90,7 @@ Example
#define SCROBBLER_BAD_ENTRY "# FAILED - " #define SCROBBLER_BAD_ENTRY "# FAILED - "
/* longest entry I've had is 323, add a safety margin */ /* longest entry I've had is 323, add a safety margin */
#define SCROBBLER_MAXENTRY_LEN (512) #define SCROBBLER_MAXENTRY_LEN (MAX_PATH + 60 + 10)
#define ITEM_HDR "#ARTIST #ALBUM #TITLE #TRACKNUM #LENGTH #RATING #TIMESTAMP #MUSICBRAINZ_TRACKID\n" #define ITEM_HDR "#ARTIST #ALBUM #TITLE #TRACKNUM #LENGTH #RATING #TIMESTAMP #MUSICBRAINZ_TRACKID\n"
@ -113,7 +107,6 @@ Example
/****************** prototypes ******************/ /****************** prototypes ******************/
enum plugin_status plugin_start(const void* parameter); /* entry */ enum plugin_status plugin_start(const void* parameter); /* entry */
void play_tone(unsigned int frequency, unsigned int duration);
static int view_playback_log(void); static int view_playback_log(void);
static int export_scrobbler_file(void); static int export_scrobbler_file(void);
@ -308,7 +301,7 @@ static inline const char* str_chk_valid(const char *s, const char *alt)
return (s != NULL ? s : alt); return (s != NULL ? s : alt);
} }
static void get_scrobbler_filename(char *path, size_t size) static void scrobbler_get_filename(char *path, size_t size)
{ {
int used; int used;
@ -422,7 +415,7 @@ static int open_create_scrobbler_log(void)
int fd; int fd;
char scrobbler_file[MAX_PATH]; char scrobbler_file[MAX_PATH];
get_scrobbler_filename(scrobbler_file, sizeof(scrobbler_file)); scrobbler_get_filename(scrobbler_file, sizeof(scrobbler_file));
/* If the file doesn't exist, create it. */ /* If the file doesn't exist, create it. */
if(!rb->file_exists(scrobbler_file)) if(!rb->file_exists(scrobbler_file))
@ -486,61 +479,77 @@ static bool playbacklog_parse_entry(struct scrobbler_entry *entry, char *begin)
return true; /* success */ return true; /* success */
} }
static bool cull_playback_duplicates(int fd, struct scrobbler_entry *curentry, static inline bool pbl_cull_duplicates(int fd, struct scrobbler_entry *curentry,
int cur_line, char*buf, size_t bufsz) int cur_line, char*buf, size_t bufsz)
{ {
int line_num = 0; /* child function of remove_duplicates */
int rd, pos, pos_end; int line_num = cur_line;
int rd, start_pos, pos;
struct scrobbler_entry compare; struct scrobbler_entry compare;
rb->lseek(fd, 0, SEEK_SET); pos = rb->lseek(fd, 0, SEEK_CUR);
while(1) while(1)
{ {
pos = rb->lseek(fd, 0, SEEK_CUR);
if ((rd = rb->read_line(fd, buf, bufsz)) <= 0) if ((rd = rb->read_line(fd, buf, bufsz)) <= 0)
break; break; /* EOF */
/* save start of entry in case we need to remove it */
start_pos = pos;
pos += rd;
line_num++; line_num++;
if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */
continue; continue;
if (line_num == cur_line || !playbacklog_parse_entry(&compare, buf)) if (!playbacklog_parse_entry(&compare, buf))
continue; continue;
rb->yield(); rb->yield();
if (rb->strcmp(curentry->path, compare.path) != 0)
unsigned long length = compare.length;
if (curentry->length != length
|| rb->strcmp(curentry->path, compare.path) != 0)
continue; /* different track */ continue; /* different track */
if (curentry->elapsed > compare.elapsed) /* if this is two distinct plays keep both */
if ((cur_line + 1 == line_num) /* unless back to back then its probably a resume */
|| (curentry->timestamp <= compare.timestamp + length
&& compare.timestamp <= curentry->timestamp + length))
{ {
/*logf("entry %s (%lu) @ %d culled\n", compare.path, compare.elapsed, line_num);*/
pos_end = rb->lseek(fd, 0, SEEK_CUR); if (curentry->elapsed >= compare.elapsed)
rb->lseek(fd, pos, SEEK_SET); {
rb->write(fd, "#", 1); /* make this entry a comment */ /* compare entry is not the greatest elapsed */
rb->lseek(fd, pos_end, SEEK_SET); /*logf("entry %s (%lu) @ %d culled\n", compare.path, compare.elapsed, line_num);*/
} rb->lseek(fd, start_pos, SEEK_SET);
else if (curentry->elapsed < compare.elapsed) rb->write(fd, "#", 1); /* make this entry a comment */
{ rb->lseek(fd, pos, SEEK_SET);
/*entry is not the greatest elapsed*/ }
return false; else
{
/*current entry is not the greatest elapsed*/
return false;
}
} }
} }
return true; /* this item is unique or the greatest elapsed */ return true; /* this item is unique or the greatest elapsed */
} }
static void remove_playback_duplicates(int fd, char *buf, size_t bufsz) static void playbacklog_remove_duplicates(int fd, char *buf, size_t bufsz)
{ {
logf("%s()\n", __func__); logf("%s()\n", __func__);
struct scrobbler_entry entry; struct scrobbler_entry entry;
char tmp_buf[SCROBBLER_MAXENTRY_LEN]; char tmp_buf[SCROBBLER_MAXENTRY_LEN];
int pos, endpos; int start_pos, pos = 0;
int rd; int rd;
int line_num = 0; int line_num = 0;
rb->lseek(fd, 0, SEEK_SET); rb->lseek(fd, 0, SEEK_SET);
while(1) while(1)
{ {
pos = rb->lseek(fd, 0, SEEK_CUR);
if ((rd = rb->read_line(fd, buf, bufsz)) <= 0) if ((rd = rb->read_line(fd, buf, bufsz)) <= 0)
break; break; /* EOF */
/* save start of entry in case we need to remove it */
start_pos = pos;
pos += rd;
line_num++; line_num++;
if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */
continue; continue;
@ -549,18 +558,15 @@ static void remove_playback_duplicates(int fd, char *buf, size_t bufsz)
/*logf("%s failed parsing entry @ %d\n", __func__, line_num);*/ /*logf("%s failed parsing entry @ %d\n", __func__, line_num);*/
continue; continue;
} }
//logf("current entry %s (%lu) @ %d", entry.path, entry.elapsed, line_num); /*logf("current entry %s (%lu) @ %d", entry.path, entry.elapsed, line_num);*/
endpos = rb->lseek(fd, 0, SEEK_CUR); if (!pbl_cull_duplicates(fd, &entry, line_num, tmp_buf, sizeof(tmp_buf)))
if (!cull_playback_duplicates(fd, &entry, line_num, tmp_buf, sizeof(tmp_buf)))
{ {
rb->lseek(fd, pos, SEEK_SET); rb->lseek(fd, start_pos, SEEK_SET);
/*logf("entry: %s @ %d is a duplicate", entry.path, line_num);*/ /*logf("entry: %s @ %d is a duplicate", entry.path, line_num);*/
rb->write(fd, "#", 1); /* make this entry a comment */ rb->write(fd, "#", 1); /* make this entry a comment */
endpos = 0;
line_num = 0;
} }
rb->lseek(fd, endpos, SEEK_SET); rb->lseek(fd, pos, SEEK_SET);
} }
} }
@ -589,32 +595,35 @@ static int export_scrobbler_file(void)
/* We don't want any writes while copying and (possibly) deleting the log */ /* We don't want any writes while copying and (possibly) deleting the log */
bool log_enabled = rb->global_settings->playback_log; bool log_enabled = rb->global_settings->playback_log;
rb->global_settings->playback_log = false; rb->global_settings->playback_log = false;
int fd = rb->open_utf8(filename, O_RDONLY); int fd = rb->open_utf8(filename, O_RDONLY);
if (fd < 0) if (fd < 0)
{ {
rb->global_settings->playback_log = log_enabled; /* re-enable logging */ rb->global_settings->playback_log = log_enabled; /* restore logging */
logf("Scrobbler Error opening: %s\n", filename); logf("Scrobbler Error opening: %s\n", filename);
rb->splashf(HZ *2, "Scrobbler Error opening: %s", filename); rb->splashf(HZ *2, "Scrobbler Error opening: %s", filename);
return PLUGIN_ERROR; return PLUGIN_ERROR;
} }
/* copy loop playback.log => playback.old */
while(rb->read_line(fd, buf, sizeof(buf)) > 0) while(rb->read_line(fd, buf, sizeof(buf)) > 0)
{ {
line_num++; line_num++;
if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */ if (buf[0] == '#' || buf[0] == '\0') /* skip comments and empty lines */
continue; continue;
/* parse entry will fail if elapsed > length or other invalid entry */
if (!playbacklog_parse_entry(&entry, buf)) if (!playbacklog_parse_entry(&entry, buf))
{ {
logf("%s failed parsing entry @ line: %d\n", __func__, line_num); logf("%s failed parsing entry @ line: %d\n", __func__, line_num);
continue; continue;
} }
/* don't copy entries that do not meet users minimum play length */
if ((int) entry.elapsed < gConfig.minms) if ((int) entry.elapsed < gConfig.minms)
{ {
logf("Skipping path:'%s' @ line: %d\nelapsed: %ld length: %ld\nmin: %d\n", logf("Skipping path:'%s' @ line: %d\nelapsed: %ld length: %ld\nmin: %d\n",
entry.path, line_num, entry.elapsed, entry.length, gConfig.minms); entry.path, line_num, entry.elapsed, entry.length, gConfig.minms);
continue; continue;
} }
/* add a space to beginning of every line remove_playback_duplicates /* add a space to beginning of every line playbacklog_remove_duplicates
* will use this to prepend '#' to entries that will be ignored */ * will use this to prepend '#' to entries that will be ignored */
rb->fdprintf(fd_copy, " %s\n", buf); rb->fdprintf(fd_copy, " %s\n", buf);
tracks_saved++; tracks_saved++;
@ -626,10 +635,10 @@ static int export_scrobbler_file(void)
{ {
rb->remove(filename); rb->remove(filename);
} }
rb->global_settings->playback_log = log_enabled; /* re-enable logging */ rb->global_settings->playback_log = log_enabled; /* restore logging */
if (gConfig.remove_dup && tracks_saved > 0) if (gConfig.remove_dup && tracks_saved > 0)
remove_playback_duplicates(fd_copy, buf, sizeof(buf)); playbacklog_remove_duplicates(fd_copy, buf, sizeof(buf));
rb->lseek(fd_copy, 0, SEEK_SET); rb->lseek(fd_copy, 0, SEEK_SET);

View file

@ -62,7 +62,7 @@ struct printcell_data_t {
size_t buf_size; size_t buf_size;
off_t buf_used; off_t buf_used;
char header[PRINTCELL_MAXLINELEN]; char header[PRINTCELL_MAXLINELEN];
bool is_scrobbler;
}; };
enum e_find_type { enum e_find_type {
@ -730,7 +730,7 @@ static int scrobbler_context_menu(struct printcell_data_t *pc_data)
menu_item[2]= "Include"; menu_item[2]= "Include";
menu_item[3]= "Custom Filter"; menu_item[3]= "Custom Filter";
if (pc_data->view_columns < SCROBBLER_MIN_COLUMNS) if (!pc_data->is_scrobbler)
col = -1; col = -1;
if (col == -1) if (col == -1)
@ -947,11 +947,11 @@ static void synclist_set(int selected_item, int items, int sel_size, struct prin
pc_data->header, Icon_Rockbox); pc_data->header, Icon_Rockbox);
printcell_enable(true); printcell_enable(true);
int max_cols = count_max_columns(items, pcs.text_delimeter, int max_cols = count_max_columns(items, pcs.text_delimeter,
SCROBBLER_MIN_COLUMNS, pc_data); SCROBBLER_MIN_COLUMNS, pc_data);
if (max_cols < SCROBBLER_MIN_COLUMNS) /* not a scrobbler file? */ if (max_cols < SCROBBLER_MIN_COLUMNS) /* not a scrobbler file? */
{ {
pc_data->is_scrobbler = false;
/*check for a playlist_control file or a playback log*/ /*check for a playlist_control file or a playback log*/
max_cols = count_max_columns(items, ':', 3, pc_data); max_cols = count_max_columns(items, ':', 3, pc_data);
@ -1064,6 +1064,7 @@ enum plugin_status plugin_start(const void* parameter)
rb->memset(&printcell_data, 0, sizeof (struct printcell_data_t)); rb->memset(&printcell_data, 0, sizeof (struct printcell_data_t));
printcell_data.fd_cur = -1; printcell_data.fd_cur = -1;
printcell_data.view_lastcol = -1; printcell_data.view_lastcol = -1;
printcell_data.is_scrobbler = true;
if (rb->file_exists(filename)) if (rb->file_exists(filename))
{ {