From cf8b3ecd3ae76898ee7718242da3ea357e6fa57f Mon Sep 17 00:00:00 2001 From: X-Ryl669 Date: Wed, 13 Sep 2023 18:15:38 +0200 Subject: [PATCH] [db,library] Add support for parsing lyrics and storing them in DB --- src/db.c | 1 + src/db.h | 1 + src/db_init.c | 3 +- src/db_init.h | 2 +- src/db_upgrade.c | 26 +++++++ src/library/filescanner_ffmpeg.c | 115 ++++++++++++++++++++----------- 6 files changed, 105 insertions(+), 43 deletions(-) diff --git a/src/db.c b/src/db.c index ae37ec92..a520c482 100644 --- a/src/db.c +++ b/src/db.c @@ -777,6 +777,7 @@ free_mfi(struct media_file_info *mfi, int content_only) free(mfi->composer_sort); free(mfi->album_artist_sort); free(mfi->virtual_path); + free(mfi->lyrics); if (!content_only) free(mfi); diff --git a/src/db.h b/src/db.h index 70b0ca87..c06964b2 100644 --- a/src/db.h +++ b/src/db.h @@ -248,6 +248,7 @@ struct media_file_info { char *composer_sort; uint32_t scan_kind; /* Identifies the library_source that created/updates this item */ + char *lyrics; }; #define mfi_offsetof(field) offsetof(struct media_file_info, field) diff --git a/src/db_init.c b/src/db_init.c index 49a07c01..1a406e1d 100644 --- a/src/db_init.c +++ b/src/db_init.c @@ -98,7 +98,8 @@ " composer_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " channels INTEGER DEFAULT 0," \ " usermark INTEGER DEFAULT 0," \ - " scan_kind INTEGER DEFAULT 0" \ + " scan_kind INTEGER DEFAULT 0," \ + " lyrics TEXT DEFAULT NULL COLLATE DAAP" \ ");" #define T_PL \ diff --git a/src/db_init.h b/src/db_init.h index 10eac42b..aaf6ba1b 100644 --- a/src/db_init.h +++ b/src/db_init.h @@ -26,7 +26,7 @@ * is a major upgrade. In other words minor version upgrades permit downgrading * the server after the database was upgraded. */ #define SCHEMA_VERSION_MAJOR 22 -#define SCHEMA_VERSION_MINOR 0 +#define SCHEMA_VERSION_MINOR 1 int db_init_indices(sqlite3 *hdl); diff --git a/src/db_upgrade.c b/src/db_upgrade.c index b53abf23..90fff3b8 100644 --- a/src/db_upgrade.c +++ b/src/db_upgrade.c @@ -1227,6 +1227,26 @@ static const struct db_upgrade_query db_upgrade_v2200_queries[] = { U_v2200_SCVER_MINOR, "set schema_version_minor to 00" }, }; +/* ---------------------------- 22.00 -> 22.01 ------------------------------ */ + +#define U_v2201_ALTER_FILES_ADD_LYRICS \ + "ALTER TABLE files ADD COLUMN lyrics TEXT DEFAULT NULL COLLATE DAAP;" + +#define U_v2201_SCVER_MAJOR \ + "UPDATE admin SET value = '22' WHERE key = 'schema_version_major';" +#define U_v2201_SCVER_MINOR \ + "UPDATE admin SET value = '01' WHERE key = 'schema_version_minor';" + +static const struct db_upgrade_query db_upgrade_v2201_queries[] = + { + { U_v2201_ALTER_FILES_ADD_LYRICS, "alter table files add column lyrics" }, + + { U_v2201_SCVER_MAJOR, "set schema_version_major to 22" }, + { U_v2201_SCVER_MINOR, "set schema_version_minor to 01" }, + }; + + + /* -------------------------- Main upgrade handler -------------------------- */ int @@ -1437,6 +1457,12 @@ db_upgrade(sqlite3 *hdl, int db_ver) if (ret < 0) return -1; + /* FALLTHROUGH */ + + case 2200: + ret = db_generic_upgrade(hdl, db_upgrade_v2201_queries, ARRAY_SIZE(db_upgrade_v2201_queries)); + if (ret < 0) + return -1; /* Last case statement is the only one that ends with a break statement! */ break; diff --git a/src/library/filescanner_ffmpeg.c b/src/library/filescanner_ffmpeg.c index 62a507ab..d07a5939 100644 --- a/src/library/filescanner_ffmpeg.c +++ b/src/library/filescanner_ffmpeg.c @@ -49,6 +49,8 @@ struct metadata_map { // Used for passing errors to DPRINTF (can't count on av_err2str being present) static char errbuf[64]; +static int lyricsindex = -1; + static inline char * err2str(int errnum) { @@ -186,6 +188,7 @@ static const struct metadata_map md_map_generic[] = { "artist-sort", 0, mfi_offsetof(artist_sort), NULL }, { "album-sort", 0, mfi_offsetof(album_sort), NULL }, { "compilation", 1, mfi_offsetof(compilation), NULL }, + { "lyrics", 0, mfi_offsetof(lyrics), NULL }, // ALAC sort tags { "sort_name", 0, mfi_offsetof(title_sort), NULL }, @@ -277,16 +280,67 @@ static const struct metadata_map md_map_id3[] = { NULL, 0, 0, NULL } }; +/* TODO: If the md_map was sorted, the search would be O(log N) instead of O(N) */ +static int +match_metadata(const char *key, const struct metadata_map *md_map) +{ + int i; + + for (i = 0; md_map[i].key != NULL; i++) + { + if (strcmp(key, md_map[i].key) == 0) + return i; + } + return -1; +} + + +static int +iterate_metadata(struct media_file_info *mfi, const AVDictionaryEntry *mdt, const struct metadata_map *md_map) +{ + char **strval; + uint32_t *intval; + int i; + + if ((mdt->value == NULL) || (strlen(mdt->value) == 0)) + return 0; + + if (strncmp(mdt->key, "lyrics-", sizeof("lyrics-") - 1) == 0) + i = lyricsindex; + else + i = match_metadata(mdt->key, md_map); + + if (i == -1) + return 0; + + if (md_map[i].handler_function) + return md_map[i].handler_function(mfi, mdt->value); + + if (!md_map[i].as_int) + { + strval = (char **) ((char *) mfi + md_map[i].offset); + + if (*strval == NULL) + *strval = strdup(mdt->value); + } + else + { + intval = (uint32_t *) ((char *) mfi + md_map[i].offset); + + if (*intval == 0) + { + if (safe_atou32(mdt->value, intval) < 0) + return 1; /* Should probably be 0 */ + } + } + return 1; +} static int extract_metadata_core(struct media_file_info *mfi, AVDictionary *md, const struct metadata_map *md_map) { - AVDictionaryEntry *mdt; - char **strval; - uint32_t *intval; + const AVDictionaryEntry *mdt; int mdcount; - int i; - int ret; #if 0 /* Dump all the metadata reported by ffmpeg */ @@ -297,42 +351,21 @@ extract_metadata_core(struct media_file_info *mfi, AVDictionary *md, const struc mdcount = 0; - /* Extract actual metadata */ - for (i = 0; md_map[i].key != NULL; i++) + /* Cache this search once to avoid doing it per metadata key */ + if (lyricsindex == -1) + lyricsindex = match_metadata("lyrics", md_map); + + /* Extract lyrics if any found. + FFMPEG creates a metadata key that's embedding the language code in it, like 'lyrics-eng' or 'lyrics-XXX' + So it's not possible to query this key directly except by brute-forcing all languages. + Instead, we are reversing the metadata searching algorithm to query all metadata key that FFMPEG fetched + and matching them against our own map. This is the most efficient method to search it without having 2 pass + on the KV store */ + mdt = av_dict_iterate(md, NULL); + while (mdt != NULL) { - mdt = av_dict_get(md, md_map[i].key, NULL, 0); - if (mdt == NULL) - continue; - - if ((mdt->value == NULL) || (strlen(mdt->value) == 0)) - continue; - - if (md_map[i].handler_function) - { - mdcount += md_map[i].handler_function(mfi, mdt->value); - continue; - } - - mdcount++; - - if (!md_map[i].as_int) - { - strval = (char **) ((char *) mfi + md_map[i].offset); - - if (*strval == NULL) - *strval = strdup(mdt->value); - } - else - { - intval = (uint32_t *) ((char *) mfi + md_map[i].offset); - - if (*intval == 0) - { - ret = safe_atou32(mdt->value, intval); - if (ret < 0) - continue; - } - } + mdcount += iterate_metadata(mfi, mdt, md_map); + mdt = av_dict_iterate(md, mdt); } return mdcount; @@ -518,7 +551,7 @@ scan_metadata_ffmpeg(struct media_file_info *mfi, const char *file) if (mfi->bits_per_sample == 0) mfi->bits_per_sample = av_get_bits_per_sample(codec_id); mfi->channels = channels; - } + } break; default: