mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-28 16:15:57 -05:00
[db,library] Add support for parsing lyrics and storing them in DB
This commit is contained in:
parent
d7d3a0767d
commit
cf8b3ecd3a
1
src/db.c
1
src/db.c
@ -777,6 +777,7 @@ free_mfi(struct media_file_info *mfi, int content_only)
|
|||||||
free(mfi->composer_sort);
|
free(mfi->composer_sort);
|
||||||
free(mfi->album_artist_sort);
|
free(mfi->album_artist_sort);
|
||||||
free(mfi->virtual_path);
|
free(mfi->virtual_path);
|
||||||
|
free(mfi->lyrics);
|
||||||
|
|
||||||
if (!content_only)
|
if (!content_only)
|
||||||
free(mfi);
|
free(mfi);
|
||||||
|
1
src/db.h
1
src/db.h
@ -248,6 +248,7 @@ struct media_file_info {
|
|||||||
char *composer_sort;
|
char *composer_sort;
|
||||||
|
|
||||||
uint32_t scan_kind; /* Identifies the library_source that created/updates this item */
|
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)
|
#define mfi_offsetof(field) offsetof(struct media_file_info, field)
|
||||||
|
@ -98,7 +98,8 @@
|
|||||||
" composer_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \
|
" composer_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \
|
||||||
" channels INTEGER DEFAULT 0," \
|
" channels INTEGER DEFAULT 0," \
|
||||||
" usermark 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 \
|
#define T_PL \
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
* is a major upgrade. In other words minor version upgrades permit downgrading
|
* is a major upgrade. In other words minor version upgrades permit downgrading
|
||||||
* the server after the database was upgraded. */
|
* the server after the database was upgraded. */
|
||||||
#define SCHEMA_VERSION_MAJOR 22
|
#define SCHEMA_VERSION_MAJOR 22
|
||||||
#define SCHEMA_VERSION_MINOR 0
|
#define SCHEMA_VERSION_MINOR 1
|
||||||
|
|
||||||
int
|
int
|
||||||
db_init_indices(sqlite3 *hdl);
|
db_init_indices(sqlite3 *hdl);
|
||||||
|
@ -1227,6 +1227,26 @@ static const struct db_upgrade_query db_upgrade_v2200_queries[] =
|
|||||||
{ U_v2200_SCVER_MINOR, "set schema_version_minor to 00" },
|
{ 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 -------------------------- */
|
/* -------------------------- Main upgrade handler -------------------------- */
|
||||||
|
|
||||||
int
|
int
|
||||||
@ -1437,6 +1457,12 @@ db_upgrade(sqlite3 *hdl, int db_ver)
|
|||||||
if (ret < 0)
|
if (ret < 0)
|
||||||
return -1;
|
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! */
|
/* Last case statement is the only one that ends with a break statement! */
|
||||||
break;
|
break;
|
||||||
|
@ -49,6 +49,8 @@ struct metadata_map {
|
|||||||
// Used for passing errors to DPRINTF (can't count on av_err2str being present)
|
// Used for passing errors to DPRINTF (can't count on av_err2str being present)
|
||||||
static char errbuf[64];
|
static char errbuf[64];
|
||||||
|
|
||||||
|
static int lyricsindex = -1;
|
||||||
|
|
||||||
static inline char *
|
static inline char *
|
||||||
err2str(int errnum)
|
err2str(int errnum)
|
||||||
{
|
{
|
||||||
@ -186,6 +188,7 @@ static const struct metadata_map md_map_generic[] =
|
|||||||
{ "artist-sort", 0, mfi_offsetof(artist_sort), NULL },
|
{ "artist-sort", 0, mfi_offsetof(artist_sort), NULL },
|
||||||
{ "album-sort", 0, mfi_offsetof(album_sort), NULL },
|
{ "album-sort", 0, mfi_offsetof(album_sort), NULL },
|
||||||
{ "compilation", 1, mfi_offsetof(compilation), NULL },
|
{ "compilation", 1, mfi_offsetof(compilation), NULL },
|
||||||
|
{ "lyrics", 0, mfi_offsetof(lyrics), NULL },
|
||||||
|
|
||||||
// ALAC sort tags
|
// ALAC sort tags
|
||||||
{ "sort_name", 0, mfi_offsetof(title_sort), NULL },
|
{ "sort_name", 0, mfi_offsetof(title_sort), NULL },
|
||||||
@ -277,43 +280,41 @@ static const struct metadata_map md_map_id3[] =
|
|||||||
{ NULL, 0, 0, NULL }
|
{ NULL, 0, 0, NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* TODO: If the md_map was sorted, the search would be O(log N) instead of O(N) */
|
||||||
static int
|
static int
|
||||||
extract_metadata_core(struct media_file_info *mfi, AVDictionary *md, const struct metadata_map *md_map)
|
match_metadata(const char *key, const struct metadata_map *md_map)
|
||||||
{
|
{
|
||||||
AVDictionaryEntry *mdt;
|
|
||||||
char **strval;
|
|
||||||
uint32_t *intval;
|
|
||||||
int mdcount;
|
|
||||||
int i;
|
int i;
|
||||||
int ret;
|
|
||||||
|
|
||||||
#if 0
|
|
||||||
/* Dump all the metadata reported by ffmpeg */
|
|
||||||
mdt = NULL;
|
|
||||||
while ((mdt = av_dict_get(md, "", mdt, AV_DICT_IGNORE_SUFFIX)) != NULL)
|
|
||||||
DPRINTF(E_DBG, L_SCAN, " -> %s = %s\n", mdt->key, mdt->value);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
mdcount = 0;
|
|
||||||
|
|
||||||
/* Extract actual metadata */
|
|
||||||
for (i = 0; md_map[i].key != NULL; i++)
|
for (i = 0; md_map[i].key != NULL; i++)
|
||||||
{
|
{
|
||||||
mdt = av_dict_get(md, md_map[i].key, NULL, 0);
|
if (strcmp(key, md_map[i].key) == 0)
|
||||||
if (mdt == NULL)
|
return i;
|
||||||
continue;
|
}
|
||||||
|
return -1;
|
||||||
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++;
|
|
||||||
|
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)
|
if (!md_map[i].as_int)
|
||||||
{
|
{
|
||||||
@ -328,11 +329,43 @@ extract_metadata_core(struct media_file_info *mfi, AVDictionary *md, const struc
|
|||||||
|
|
||||||
if (*intval == 0)
|
if (*intval == 0)
|
||||||
{
|
{
|
||||||
ret = safe_atou32(mdt->value, intval);
|
if (safe_atou32(mdt->value, intval) < 0)
|
||||||
if (ret < 0)
|
return 1; /* Should probably be 0 */
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
extract_metadata_core(struct media_file_info *mfi, AVDictionary *md, const struct metadata_map *md_map)
|
||||||
|
{
|
||||||
|
const AVDictionaryEntry *mdt;
|
||||||
|
int mdcount;
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
/* Dump all the metadata reported by ffmpeg */
|
||||||
|
mdt = NULL;
|
||||||
|
while ((mdt = av_dict_get(md, "", mdt, AV_DICT_IGNORE_SUFFIX)) != NULL)
|
||||||
|
DPRINTF(E_DBG, L_SCAN, " -> %s = %s\n", mdt->key, mdt->value);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
mdcount = 0;
|
||||||
|
|
||||||
|
/* 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)
|
||||||
|
{
|
||||||
|
mdcount += iterate_metadata(mfi, mdt, md_map);
|
||||||
|
mdt = av_dict_iterate(md, mdt);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mdcount;
|
return mdcount;
|
||||||
|
Loading…
Reference in New Issue
Block a user