mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-29 00:23:23 -05:00
commit
4365869fb1
@ -251,6 +251,8 @@ OWNTONE_MODULES_CHECK([OWNTONE], [LIBAV],
|
|||||||
[libavutil/avutil.h])
|
[libavutil/avutil.h])
|
||||||
OWNTONE_CHECK_DECLS([avformat_network_init],
|
OWNTONE_CHECK_DECLS([avformat_network_init],
|
||||||
[libavformat/avformat.h])
|
[libavformat/avformat.h])
|
||||||
|
OWNTONE_CHECK_DECLS([av_dict_iterate],
|
||||||
|
[libavutil/dict.h])
|
||||||
])
|
])
|
||||||
|
|
||||||
AC_CHECK_SIZEOF([void *])
|
AC_CHECK_SIZEOF([void *])
|
||||||
|
@ -1544,7 +1544,8 @@ curl -X GET "http://localhost:3689/api/library/tracks/1"
|
|||||||
"data_kind": "file",
|
"data_kind": "file",
|
||||||
"path": "/music/srv/Incubus/Make Yourself/12 Pardon Me.mp3",
|
"path": "/music/srv/Incubus/Make Yourself/12 Pardon Me.mp3",
|
||||||
"uri": "library:track:1",
|
"uri": "library:track:1",
|
||||||
"artwork_url": "/artwork/item/1"
|
"artwork_url": "/artwork/item/1",
|
||||||
|
"lyrics": "[00:00:10] Let's start the music [...]"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -2622,6 +2623,7 @@ curl --include \
|
|||||||
| uri | string | Resource identifier |
|
| uri | string | Resource identifier |
|
||||||
| artwork_url | string | *(optional)* [Artwork url](#artwork-urls) |
|
| artwork_url | string | *(optional)* [Artwork url](#artwork-urls) |
|
||||||
| usermark | integer | User review marking of track (ranges from 0) |
|
| usermark | integer | User review marking of track (ranges from 0) |
|
||||||
|
| lyrics | string | The lyrics if found either as LRC or plain text |
|
||||||
|
|
||||||
|
|
||||||
### `paging` object
|
### `paging` object
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
src/db.c
3
src/db.c
@ -230,6 +230,7 @@ static const struct col_type_map mfi_cols_map[] =
|
|||||||
{ "channels", mfi_offsetof(channels), DB_TYPE_INT },
|
{ "channels", mfi_offsetof(channels), DB_TYPE_INT },
|
||||||
{ "usermark", mfi_offsetof(usermark), DB_TYPE_INT },
|
{ "usermark", mfi_offsetof(usermark), DB_TYPE_INT },
|
||||||
{ "scan_kind", mfi_offsetof(scan_kind), DB_TYPE_INT },
|
{ "scan_kind", mfi_offsetof(scan_kind), DB_TYPE_INT },
|
||||||
|
{ "lyrics", mfi_offsetof(lyrics), DB_TYPE_STRING },
|
||||||
};
|
};
|
||||||
|
|
||||||
/* This list must be kept in sync with
|
/* This list must be kept in sync with
|
||||||
@ -371,6 +372,7 @@ static const ssize_t dbmfi_cols_map[] =
|
|||||||
dbmfi_offsetof(channels),
|
dbmfi_offsetof(channels),
|
||||||
dbmfi_offsetof(usermark),
|
dbmfi_offsetof(usermark),
|
||||||
dbmfi_offsetof(scan_kind),
|
dbmfi_offsetof(scan_kind),
|
||||||
|
dbmfi_offsetof(lyrics),
|
||||||
};
|
};
|
||||||
|
|
||||||
/* This list must be kept in sync with
|
/* This list must be kept in sync with
|
||||||
@ -777,6 +779,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);
|
||||||
|
2
src/db.h
2
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)
|
||||||
@ -422,6 +423,7 @@ struct db_media_file_info {
|
|||||||
char *channels;
|
char *channels;
|
||||||
char *usermark;
|
char *usermark;
|
||||||
char *scan_kind;
|
char *scan_kind;
|
||||||
|
char *lyrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define dbmfi_offsetof(field) offsetof(struct db_media_file_info, field)
|
#define dbmfi_offsetof(field) offsetof(struct db_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;
|
||||||
|
@ -322,7 +322,7 @@ track_to_json(struct db_media_file_info *dbmfi)
|
|||||||
|
|
||||||
safe_json_add_string(item, "path", dbmfi->path);
|
safe_json_add_string(item, "path", dbmfi->path);
|
||||||
|
|
||||||
ret = snprintf(uri, sizeof(uri), "%s:%s:%s", "library", "track", dbmfi->id);
|
ret = snprintf(uri, sizeof(uri), "library:track:%s", dbmfi->id);
|
||||||
if (ret < sizeof(uri))
|
if (ret < sizeof(uri))
|
||||||
json_object_object_add(item, "uri", json_object_new_string(uri));
|
json_object_object_add(item, "uri", json_object_new_string(uri));
|
||||||
|
|
||||||
@ -330,6 +330,7 @@ track_to_json(struct db_media_file_info *dbmfi)
|
|||||||
if (ret < sizeof(artwork_url))
|
if (ret < sizeof(artwork_url))
|
||||||
json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url));
|
json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url));
|
||||||
|
|
||||||
|
safe_json_add_string(item, "lyrics", dbmfi->lyrics);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,13 +37,18 @@
|
|||||||
#include "http.h"
|
#include "http.h"
|
||||||
#include "conffile.h"
|
#include "conffile.h"
|
||||||
|
|
||||||
|
// From libavutil 57.37.100
|
||||||
|
#if !defined(HAVE_DECL_AV_DICT_ITERATE) || !(HAVE_DECL_AV_DICT_ITERATE)
|
||||||
|
# define av_dict_iterate(dict, entry) av_dict_get((dict), "", (entry), AV_DICT_IGNORE_SUFFIX)
|
||||||
|
#endif
|
||||||
|
|
||||||
/* Mapping between the metadata name(s) and the offset
|
/* Mapping between the metadata name(s) and the offset
|
||||||
* of the equivalent metadata field in struct media_file_info */
|
* of the equivalent metadata field in struct media_file_info */
|
||||||
struct metadata_map {
|
struct metadata_map {
|
||||||
char *key;
|
char *key;
|
||||||
int as_int;
|
int as_int;
|
||||||
size_t offset;
|
size_t offset;
|
||||||
int (*handler_function)(struct media_file_info *, char *);
|
int (*handler_function)(struct media_file_info *, const char *);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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)
|
||||||
@ -57,7 +62,7 @@ err2str(int errnum)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
parse_genre(struct media_file_info *mfi, char *genre_string)
|
parse_genre(struct media_file_info *mfi, const char *genre_string)
|
||||||
{
|
{
|
||||||
char **genre = (char**)((char *) mfi + mfi_offsetof(genre));
|
char **genre = (char**)((char *) mfi + mfi_offsetof(genre));
|
||||||
char *ptr;
|
char *ptr;
|
||||||
@ -78,12 +83,17 @@ parse_genre(struct media_file_info *mfi, char *genre_string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
parse_slash_separated_ints(char *string, uint32_t *firstval, uint32_t *secondval)
|
parse_slash_separated_ints(const char *string, uint32_t *firstval, uint32_t *secondval)
|
||||||
{
|
{
|
||||||
int numvals = 0;
|
int numvals = 0;
|
||||||
|
char buf[64];
|
||||||
char *ptr;
|
char *ptr;
|
||||||
|
|
||||||
ptr = strchr(string, '/');
|
// dict.h: "The returned entry key or value must not be changed, or it will
|
||||||
|
// cause undefined behavior" -> so we must make a copy
|
||||||
|
snprintf(buf, sizeof(buf), "%s", string);
|
||||||
|
|
||||||
|
ptr = strchr(buf, '/');
|
||||||
if (ptr)
|
if (ptr)
|
||||||
{
|
{
|
||||||
*ptr = '\0';
|
*ptr = '\0';
|
||||||
@ -91,14 +101,14 @@ parse_slash_separated_ints(char *string, uint32_t *firstval, uint32_t *secondval
|
|||||||
numvals++;
|
numvals++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (safe_atou32(string, firstval) == 0)
|
if (safe_atou32(buf, firstval) == 0)
|
||||||
numvals++;
|
numvals++;
|
||||||
|
|
||||||
return numvals;
|
return numvals;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
parse_track(struct media_file_info *mfi, char *track_string)
|
parse_track(struct media_file_info *mfi, const char *track_string)
|
||||||
{
|
{
|
||||||
uint32_t *track = (uint32_t *) ((char *) mfi + mfi_offsetof(track));
|
uint32_t *track = (uint32_t *) ((char *) mfi + mfi_offsetof(track));
|
||||||
uint32_t *total_tracks = (uint32_t *) ((char *) mfi + mfi_offsetof(total_tracks));
|
uint32_t *total_tracks = (uint32_t *) ((char *) mfi + mfi_offsetof(total_tracks));
|
||||||
@ -107,7 +117,7 @@ parse_track(struct media_file_info *mfi, char *track_string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
parse_disc(struct media_file_info *mfi, char *disc_string)
|
parse_disc(struct media_file_info *mfi, const char *disc_string)
|
||||||
{
|
{
|
||||||
uint32_t *disc = (uint32_t *) ((char *) mfi + mfi_offsetof(disc));
|
uint32_t *disc = (uint32_t *) ((char *) mfi + mfi_offsetof(disc));
|
||||||
uint32_t *total_discs = (uint32_t *) ((char *) mfi + mfi_offsetof(total_discs));
|
uint32_t *total_discs = (uint32_t *) ((char *) mfi + mfi_offsetof(total_discs));
|
||||||
@ -116,7 +126,7 @@ parse_disc(struct media_file_info *mfi, char *disc_string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
parse_date(struct media_file_info *mfi, char *date_string)
|
parse_date(struct media_file_info *mfi, const char *date_string)
|
||||||
{
|
{
|
||||||
char year_string[32];
|
char year_string[32];
|
||||||
uint32_t *year = (uint32_t *) ((char *) mfi + mfi_offsetof(year));
|
uint32_t *year = (uint32_t *) ((char *) mfi + mfi_offsetof(year));
|
||||||
@ -152,7 +162,7 @@ parse_date(struct media_file_info *mfi, char *date_string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
parse_albumid(struct media_file_info *mfi, char *id_string)
|
parse_albumid(struct media_file_info *mfi, const char *id_string)
|
||||||
{
|
{
|
||||||
// Already set by a previous tag that we give higher priority
|
// Already set by a previous tag that we give higher priority
|
||||||
if (mfi->songalbumid)
|
if (mfi->songalbumid)
|
||||||
@ -186,6 +196,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 },
|
||||||
@ -279,60 +290,63 @@ static const struct metadata_map md_map_id3[] =
|
|||||||
|
|
||||||
|
|
||||||
static int
|
static int
|
||||||
extract_metadata_core(struct media_file_info *mfi, AVDictionary *md, const struct metadata_map *md_map)
|
extract_metadata_from_kv(struct media_file_info *mfi, const char *key, const char *value, const struct metadata_map *md_map)
|
||||||
{
|
{
|
||||||
AVDictionaryEntry *mdt;
|
|
||||||
char **strval;
|
char **strval;
|
||||||
uint32_t *intval;
|
uint32_t *intval;
|
||||||
int mdcount;
|
|
||||||
int i;
|
int i;
|
||||||
int ret;
|
|
||||||
|
|
||||||
#if 0
|
if ((value == NULL) || (strlen(value) == 0))
|
||||||
/* Dump all the metadata reported by ffmpeg */
|
return 0;
|
||||||
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;
|
if (strncmp(key, "lyrics-", sizeof("lyrics-") - 1) == 0)
|
||||||
|
key = "lyrics";
|
||||||
|
|
||||||
/* 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)
|
break;
|
||||||
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].key == NULL)
|
||||||
|
return 0; // Not found in map
|
||||||
|
|
||||||
|
if (md_map[i].handler_function)
|
||||||
|
return md_map[i].handler_function(mfi, value);
|
||||||
|
|
||||||
if (!md_map[i].as_int)
|
if (!md_map[i].as_int)
|
||||||
{
|
{
|
||||||
strval = (char **) ((char *) mfi + md_map[i].offset);
|
strval = (char **) ((char *) mfi + md_map[i].offset);
|
||||||
|
|
||||||
if (*strval == NULL)
|
if (*strval != NULL)
|
||||||
*strval = strdup(mdt->value);
|
return 0;
|
||||||
|
|
||||||
|
*strval = strdup(value);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
intval = (uint32_t *) ((char *) mfi + md_map[i].offset);
|
intval = (uint32_t *) ((char *) mfi + md_map[i].offset);
|
||||||
|
|
||||||
if (*intval == 0)
|
if (*intval != 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (safe_atou32(value, intval) < 0)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
extract_metadata_from_dict(struct media_file_info *mfi, AVDictionary *md, const struct metadata_map *md_map)
|
||||||
{
|
{
|
||||||
ret = safe_atou32(mdt->value, intval);
|
const AVDictionaryEntry *mdt = NULL;
|
||||||
if (ret < 0)
|
int mdcount = 0;
|
||||||
continue;
|
|
||||||
}
|
while ((mdt = av_dict_iterate(md, mdt)))
|
||||||
}
|
{
|
||||||
|
// DPRINTF(E_DBG, L_SCAN, " -> %s = %s\n", mdt->key, mdt->value);
|
||||||
|
mdcount += extract_metadata_from_kv(mfi, mdt->key, mdt->value, md_map);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mdcount;
|
return mdcount;
|
||||||
@ -348,7 +362,7 @@ extract_metadata(struct media_file_info *mfi, AVFormatContext *ctx, AVStream *au
|
|||||||
|
|
||||||
if (ctx->metadata)
|
if (ctx->metadata)
|
||||||
{
|
{
|
||||||
ret = extract_metadata_core(mfi, ctx->metadata, md_map);
|
ret = extract_metadata_from_dict(mfi, ctx->metadata, md_map);
|
||||||
mdcount += ret;
|
mdcount += ret;
|
||||||
|
|
||||||
DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from file metadata\n", ret);
|
DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from file metadata\n", ret);
|
||||||
@ -356,7 +370,7 @@ extract_metadata(struct media_file_info *mfi, AVFormatContext *ctx, AVStream *au
|
|||||||
|
|
||||||
if (audio_stream->metadata)
|
if (audio_stream->metadata)
|
||||||
{
|
{
|
||||||
ret = extract_metadata_core(mfi, audio_stream->metadata, md_map);
|
ret = extract_metadata_from_dict(mfi, audio_stream->metadata, md_map);
|
||||||
mdcount += ret;
|
mdcount += ret;
|
||||||
|
|
||||||
DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from audio stream metadata\n", ret);
|
DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from audio stream metadata\n", ret);
|
||||||
@ -364,7 +378,7 @@ extract_metadata(struct media_file_info *mfi, AVFormatContext *ctx, AVStream *au
|
|||||||
|
|
||||||
if (video_stream && video_stream->metadata)
|
if (video_stream && video_stream->metadata)
|
||||||
{
|
{
|
||||||
ret = extract_metadata_core(mfi, video_stream->metadata, md_map);
|
ret = extract_metadata_from_dict(mfi, video_stream->metadata, md_map);
|
||||||
mdcount += ret;
|
mdcount += ret;
|
||||||
|
|
||||||
DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from video stream metadata\n", ret);
|
DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from video stream metadata\n", ret);
|
||||||
|
@ -297,12 +297,22 @@ export default {
|
|||||||
update_player_status() {
|
update_player_status() {
|
||||||
webapi.player_status().then(({ data }) => {
|
webapi.player_status().then(({ data }) => {
|
||||||
this.$store.commit(types.UPDATE_PLAYER_STATUS, data)
|
this.$store.commit(types.UPDATE_PLAYER_STATUS, data)
|
||||||
|
this.update_lyrics()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
update_queue() {
|
update_queue() {
|
||||||
webapi.queue().then(({ data }) => {
|
webapi.queue().then(({ data }) => {
|
||||||
this.$store.commit(types.UPDATE_QUEUE, data)
|
this.$store.commit(types.UPDATE_QUEUE, data)
|
||||||
|
this.update_lyrics()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
update_lyrics() {
|
||||||
|
let track = this.$store.state.queue.items.filter(e => e.id == this.$store.state.player.item_id)
|
||||||
|
if (track.length >= 1)
|
||||||
|
webapi.library_track(track[0].track_id).then(({ data }) => {
|
||||||
|
this.$store.commit(types.UPDATE_LYRICS, data)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
188
web-src/src/components/Lyrics.vue
Normal file
188
web-src/src/components/Lyrics.vue
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="lyrics-wrapper"
|
||||||
|
ref="lyricsWrapper"
|
||||||
|
@touchstart="autoScroll = false"
|
||||||
|
@touchend="autoScroll = true"
|
||||||
|
v-on:scroll.passive="startedScroll"
|
||||||
|
v-on:wheel.passive="startedScroll"
|
||||||
|
>
|
||||||
|
<div class="lyrics">
|
||||||
|
<p
|
||||||
|
v-for="(item, key) in lyricsArr"
|
||||||
|
:class="key == lyricIndex && is_sync && 'gradient'"
|
||||||
|
>
|
||||||
|
{{ item[0] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "lyrics",
|
||||||
|
data() {
|
||||||
|
// Non reactive
|
||||||
|
// Used as a cache to speed up finding the lyric index in the array for the current time
|
||||||
|
this.lastIndex = 0;
|
||||||
|
// Fired upon scrolling, that's disabling the auto scrolling for 5s
|
||||||
|
this.scrollTimer = null;
|
||||||
|
this.lastItemId = -1;
|
||||||
|
// Reactive
|
||||||
|
return {
|
||||||
|
scroll: {},
|
||||||
|
// lineHeight: 42,
|
||||||
|
autoScroll: true, // stop scroll to element when touch
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
player() {
|
||||||
|
return this.$store.state.player
|
||||||
|
},
|
||||||
|
|
||||||
|
is_sync() {
|
||||||
|
return this.lyricsArr.length && this.lyricsArr[0].length > 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
lyricIndex() {
|
||||||
|
// We have to perform a dichotomic search in the time array to find the index that's matching
|
||||||
|
const curTime = this.player.item_progress_ms / 1000;
|
||||||
|
const la = this.lyricsArr;
|
||||||
|
if (la.length && la[0].length === 1) return 0; // Bail out for non synchronized lyrics
|
||||||
|
if (this.player.item_id != this.lastItemId
|
||||||
|
|| this.lastIndex < la.length && la[this.lastIndex][1] > curTime) {
|
||||||
|
// Song changed or time scrolled back, let's reset the cache
|
||||||
|
this.lastItemId = this.player.item_id;
|
||||||
|
this.lastIndex = 0;
|
||||||
|
}
|
||||||
|
// Check the cached value to avoid searching the times
|
||||||
|
if (this.lastIndex < la.length - 1 && la[this.lastIndex + 1][1] > curTime)
|
||||||
|
return this.lastIndex;
|
||||||
|
if (this.lastIndex < la.length - 2 && la[this.lastIndex + 2][1] > curTime)
|
||||||
|
return this.lastIndex + 1;
|
||||||
|
|
||||||
|
// Not found in the next 2 items, so start dichotomic search for the best time
|
||||||
|
let i;
|
||||||
|
let start = 0,
|
||||||
|
end = la.length - 1;
|
||||||
|
while (start <= end) {
|
||||||
|
i = ((end + start) / 2) | 0;
|
||||||
|
if (la[i][1] <= curTime && ((la.length > i+1) && la[i + 1][1] > curTime)) break;
|
||||||
|
if (la[i][1] < curTime) start = i + 1;
|
||||||
|
else end = i - 1;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
},
|
||||||
|
lyricDuration() {
|
||||||
|
// Ignore unsynchronized lyrics.
|
||||||
|
if (!this.lyricsArr.length || this.lyricsArr[0].length < 2) return 3600;
|
||||||
|
// The index is 0 before the first lyric until the end of the first lyric
|
||||||
|
if (!this.lyricIndex && this.player.item_progress_ms / 1000 < this.lyricsArr[0][1])
|
||||||
|
return this.lyricsArr[0][1];
|
||||||
|
return this.lyricIndex < this.lyricsArr.length - 1
|
||||||
|
? this.lyricsArr[this.lyricIndex + 1][1] -
|
||||||
|
this.lyricsArr[this.lyricIndex][1]
|
||||||
|
: 3600;
|
||||||
|
},
|
||||||
|
lyricsArr() {
|
||||||
|
return this.$store.getters.lyrics;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
lyricIndex() {
|
||||||
|
// Scroll current lyric in the center of the view unless user manipulated
|
||||||
|
this.autoScroll && this._scrollToElement();
|
||||||
|
this.lastIndex = this.lyricIndex;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
startedScroll(e) {
|
||||||
|
// Ugly trick to check if a scroll event comes from the user or from JS
|
||||||
|
if (!e.screenX || e.screenX == 0 || !e.screenY || e.screenY == 0) return; // Programmatically triggered event are ignored here
|
||||||
|
this.autoScroll = false;
|
||||||
|
if (this.scrollTimer) clearTimeout(this.scrollTimer);
|
||||||
|
let t = this;
|
||||||
|
// Re-enable automatic scrolling after 5s
|
||||||
|
this.scrollTimer = setTimeout(function () {
|
||||||
|
t.autoScroll = true;
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
|
||||||
|
_scrollToElement() {
|
||||||
|
let scrollTouch = this.$refs.lyricsWrapper,
|
||||||
|
currentLyric = scrollTouch.children[0].children[this.lyricIndex],
|
||||||
|
offsetToCenter = scrollTouch.offsetHeight >> 1;
|
||||||
|
if (!this.lyricsArr || !currentLyric) return;
|
||||||
|
|
||||||
|
let currOff = scrollTouch.scrollTop,
|
||||||
|
destOff = currentLyric.offsetTop - offsetToCenter;
|
||||||
|
// Using scrollBy ensure that scrolling will happen
|
||||||
|
// even if the element is visible before scrolling
|
||||||
|
scrollTouch.scrollBy({
|
||||||
|
top: destOff - currOff,
|
||||||
|
left: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then prepare the animated gradient too
|
||||||
|
currentLyric.style.animationDuration = this.lyricDuration + "s";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.lyrics-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: -1rem;
|
||||||
|
left: calc(50% - 40vw);
|
||||||
|
right: calc(50% - 40vw);
|
||||||
|
bottom: 0;
|
||||||
|
max-height: calc(100% - 9rem);
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
/* Glass effect */
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
-webkit-backdrop-filter: blur(3px);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.lyrics-wrapper .lyrics {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.lyrics-wrapper .lyrics .gradient {
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 120%;
|
||||||
|
animation: slide-right 1 linear;
|
||||||
|
background-size: 200% 100%;
|
||||||
|
|
||||||
|
background-image: -webkit-linear-gradient(
|
||||||
|
left,
|
||||||
|
#080 50%,
|
||||||
|
#000 50%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-right {
|
||||||
|
0% {
|
||||||
|
background-position: 100% 0%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics-wrapper .lyrics p {
|
||||||
|
line-height: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
</style>
|
@ -107,7 +107,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-brand is-flex-grow-1">
|
<div class="navbar-brand is-flex-grow-1">
|
||||||
<navbar-item-link :to="{ name: 'queue' }" exact class="mr-auto">
|
<div class="navbar-item is-expanded is-justify-content-left is-no-basis">
|
||||||
|
<navbar-item-link :to="{ name: 'queue' }" exact>
|
||||||
<mdicon class="icon" name="playlist-play" size="24" />
|
<mdicon class="icon" name="playlist-play" size="24" />
|
||||||
</navbar-item-link>
|
</navbar-item-link>
|
||||||
<navbar-item-link
|
<navbar-item-link
|
||||||
@ -126,6 +127,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</navbar-item-link>
|
</navbar-item-link>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-item is-expanded is-justify-content-center is-no-basis">
|
||||||
<player-button-previous
|
<player-button-previous
|
||||||
v-if="is_now_playing_page"
|
v-if="is_now_playing_page"
|
||||||
class="navbar-item px-2"
|
class="navbar-item px-2"
|
||||||
@ -153,8 +156,15 @@
|
|||||||
class="navbar-item px-2"
|
class="navbar-item px-2"
|
||||||
:icon_size="24"
|
:icon_size="24"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-item is-expanded is-justify-content-right is-no-basis">
|
||||||
|
<player-button-lyrics
|
||||||
|
v-if="is_now_playing_page"
|
||||||
|
class="navbar-item"
|
||||||
|
:icon_size="24"
|
||||||
|
/>
|
||||||
<a
|
<a
|
||||||
class="navbar-item ml-auto"
|
class="navbar-item"
|
||||||
@click="show_player_menu = !show_player_menu"
|
@click="show_player_menu = !show_player_menu"
|
||||||
>
|
>
|
||||||
<mdicon
|
<mdicon
|
||||||
@ -163,6 +173,7 @@
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Player menu for mobile and tablet -->
|
<!-- Player menu for mobile and tablet -->
|
||||||
<div
|
<div
|
||||||
class="navbar-menu is-hidden-desktop"
|
class="navbar-menu is-hidden-desktop"
|
||||||
@ -268,6 +279,7 @@ import { mdiCancel } from '@mdi/js'
|
|||||||
import NavbarItemLink from './NavbarItemLink.vue'
|
import NavbarItemLink from './NavbarItemLink.vue'
|
||||||
import NavbarItemOutput from './NavbarItemOutput.vue'
|
import NavbarItemOutput from './NavbarItemOutput.vue'
|
||||||
import PlayerButtonConsume from '@/components/PlayerButtonConsume.vue'
|
import PlayerButtonConsume from '@/components/PlayerButtonConsume.vue'
|
||||||
|
import PlayerButtonLyrics from '@/components/PlayerButtonLyrics.vue'
|
||||||
import PlayerButtonNext from '@/components/PlayerButtonNext.vue'
|
import PlayerButtonNext from '@/components/PlayerButtonNext.vue'
|
||||||
import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause.vue'
|
import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause.vue'
|
||||||
import PlayerButtonPrevious from '@/components/PlayerButtonPrevious.vue'
|
import PlayerButtonPrevious from '@/components/PlayerButtonPrevious.vue'
|
||||||
@ -284,6 +296,7 @@ export default {
|
|||||||
NavbarItemLink,
|
NavbarItemLink,
|
||||||
NavbarItemOutput,
|
NavbarItemOutput,
|
||||||
PlayerButtonConsume,
|
PlayerButtonConsume,
|
||||||
|
PlayerButtonLyrics,
|
||||||
PlayerButtonNext,
|
PlayerButtonNext,
|
||||||
PlayerButtonPlayPause,
|
PlayerButtonPlayPause,
|
||||||
PlayerButtonPrevious,
|
PlayerButtonPrevious,
|
||||||
|
45
web-src/src/components/PlayerButtonLyrics.vue
Normal file
45
web-src/src/components/PlayerButtonLyrics.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<a :class="{ 'is-active': is_active }" @click="toggle_lyrics">
|
||||||
|
<mdicon
|
||||||
|
v-if="!is_active"
|
||||||
|
name="script-text-outline"
|
||||||
|
:size="icon_size"
|
||||||
|
:title="$t('player.button.toggle-lyrics')"
|
||||||
|
/>
|
||||||
|
<mdicon
|
||||||
|
v-if="is_active"
|
||||||
|
name="script-text-play"
|
||||||
|
:size="icon_size"
|
||||||
|
:title="$t('player.button.toggle-lyrics')"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PlayerButtonLyrics',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
icon_size: {
|
||||||
|
type: Number,
|
||||||
|
default: 16
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
is_active() {
|
||||||
|
return this.$store.getters.lyrics_pane;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggle_lyrics() {
|
||||||
|
this.$store.state.lyrics.lyrics_pane = !this.$store.state.lyrics.lyrics_pane;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
@ -45,6 +45,8 @@ import {
|
|||||||
mdiRepeatOnce,
|
mdiRepeatOnce,
|
||||||
mdiRewind10,
|
mdiRewind10,
|
||||||
mdiRss,
|
mdiRss,
|
||||||
|
mdiScriptTextOutline,
|
||||||
|
mdiScriptTextPlay,
|
||||||
mdiServer,
|
mdiServer,
|
||||||
mdiShuffle,
|
mdiShuffle,
|
||||||
mdiShuffleDisabled,
|
mdiShuffleDisabled,
|
||||||
@ -107,6 +109,8 @@ export const icons = {
|
|||||||
mdiRewind10,
|
mdiRewind10,
|
||||||
mdiRss,
|
mdiRss,
|
||||||
mdiServer,
|
mdiServer,
|
||||||
|
mdiScriptTextOutline,
|
||||||
|
mdiScriptTextPlay,
|
||||||
mdiShuffle,
|
mdiShuffle,
|
||||||
mdiShuffleDisabled,
|
mdiShuffleDisabled,
|
||||||
mdiSkipBackward,
|
mdiSkipBackward,
|
||||||
|
@ -578,7 +578,8 @@
|
|||||||
"shuffle-disabled": "Tracks in Reihenfolge wiedergeben",
|
"shuffle-disabled": "Tracks in Reihenfolge wiedergeben",
|
||||||
"skip-backward": "Zum vorherigen Track springen",
|
"skip-backward": "Zum vorherigen Track springen",
|
||||||
"skip-forward": "Zum nächsten Track springen",
|
"skip-forward": "Zum nächsten Track springen",
|
||||||
"stop": "Wiedergabe stoppen"
|
"stop": "Wiedergabe stoppen",
|
||||||
|
"toggle-lyrics": "Liedtexte anzeigen/verbergen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
|
@ -578,7 +578,8 @@
|
|||||||
"shuffle-disabled": "Play tracks in order",
|
"shuffle-disabled": "Play tracks in order",
|
||||||
"skip-backward": "Skip to previous track",
|
"skip-backward": "Skip to previous track",
|
||||||
"skip-forward": "Skip to next track",
|
"skip-forward": "Skip to next track",
|
||||||
"stop": "Stop"
|
"stop": "Stop",
|
||||||
|
"toggle-lyrics": "Toggle lyrics"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
|
@ -578,7 +578,8 @@
|
|||||||
"shuffle-disabled": "Lire les pistes dans l’ordre",
|
"shuffle-disabled": "Lire les pistes dans l’ordre",
|
||||||
"skip-backward": "Reculer à la piste précédente",
|
"skip-backward": "Reculer à la piste précédente",
|
||||||
"skip-forward": "Avancer à la piste suivante",
|
"skip-forward": "Avancer à la piste suivante",
|
||||||
"stop": "Arrêter la lecture"
|
"stop": "Arrêter la lecture",
|
||||||
|
"toggle-lyrics": "Voir/Cacher les paroles"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
|
@ -578,7 +578,8 @@
|
|||||||
"shuffle-disabled": "按顺序播放曲目",
|
"shuffle-disabled": "按顺序播放曲目",
|
||||||
"skip-backward": "播放上一首",
|
"skip-backward": "播放上一首",
|
||||||
"skip-forward": "播放下一首",
|
"skip-forward": "播放下一首",
|
||||||
"stop": "停止"
|
"stop": "停止",
|
||||||
|
"toggle-lyrics": "显示/隐藏歌词"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
|
@ -3,6 +3,27 @@
|
|||||||
@import 'bulma/bulma.sass';
|
@import 'bulma/bulma.sass';
|
||||||
@import 'bulma-switch';
|
@import 'bulma-switch';
|
||||||
|
|
||||||
|
.is-no-basis {
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix bulma issue for a group under a navbar-brand container (contrary to documentation) */
|
||||||
|
.navbar.is-white .navbar-brand > .navbar-item *:focus, .navbar.is-white .navbar-brand > .navbar-item *:hover, .navbar.is-white .navbar-brand > .navbar-item *.is-active {
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar.is-dark .navbar-brand > .navbar-item:focus, .navbar.is-dark .navbar-brand > .navbar-item:hover, .navbar.is-dark .navbar-brand > .navbar-item.is-active
|
||||||
|
.navbar.is-dark .navbar-brand > .navbar-item *:focus, .navbar.is-dark .navbar-brand > .navbar-item *:hover, .navbar.is-dark .navbar-brand > .navbar-item *.is-active {
|
||||||
|
background-color: $black-ter;;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar.is-dark .navbar-brand .navbar-item,
|
||||||
|
.navbar.is-white.is-dark .navbar-brand .navbar-item,
|
||||||
|
{
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
background-color: $info;
|
background-color: $info;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
class="is-clickable fd-has-shadow fd-cover-big-image"
|
class="is-clickable fd-has-shadow fd-cover-big-image"
|
||||||
@click="open_dialog(track)"
|
@click="open_dialog(track)"
|
||||||
/>
|
/>
|
||||||
|
<lyrics v-if="lyrics_visible" />
|
||||||
<control-slider
|
<control-slider
|
||||||
v-model:value="track_progress"
|
v-model:value="track_progress"
|
||||||
class="mt-5"
|
class="mt-5"
|
||||||
@ -56,6 +57,7 @@
|
|||||||
import * as types from '@/store/mutation_types'
|
import * as types from '@/store/mutation_types'
|
||||||
import ControlSlider from '@/components/ControlSlider.vue'
|
import ControlSlider from '@/components/ControlSlider.vue'
|
||||||
import CoverArtwork from '@/components/CoverArtwork.vue'
|
import CoverArtwork from '@/components/CoverArtwork.vue'
|
||||||
|
import Lyrics from '@/components/Lyrics.vue'
|
||||||
import { mdiCancel } from '@mdi/js'
|
import { mdiCancel } from '@mdi/js'
|
||||||
import ModalDialogQueueItem from '@/components/ModalDialogQueueItem.vue'
|
import ModalDialogQueueItem from '@/components/ModalDialogQueueItem.vue'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
@ -67,6 +69,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
ControlSlider,
|
ControlSlider,
|
||||||
CoverArtwork,
|
CoverArtwork,
|
||||||
|
Lyrics,
|
||||||
ModalDialogQueueItem
|
ModalDialogQueueItem
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -82,6 +85,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
lyrics_visible() {
|
||||||
|
return this.$store.getters.lyrics_pane;
|
||||||
|
},
|
||||||
|
|
||||||
is_live() {
|
is_live() {
|
||||||
return this.track.length_ms === 0
|
return this.track.length_ms === 0
|
||||||
},
|
},
|
||||||
|
@ -31,7 +31,12 @@ export default createStore({
|
|||||||
volume: 0,
|
volume: 0,
|
||||||
item_id: 0,
|
item_id: 0,
|
||||||
item_length_ms: 0,
|
item_length_ms: 0,
|
||||||
item_progress_ms: 0
|
item_progress_ms: 0,
|
||||||
|
},
|
||||||
|
lyrics: {
|
||||||
|
lyrics_pane: false,
|
||||||
|
lyrics_id: -1,
|
||||||
|
lyrics: []
|
||||||
},
|
},
|
||||||
queue: {
|
queue: {
|
||||||
version: 0,
|
version: 0,
|
||||||
@ -70,6 +75,14 @@ export default createStore({
|
|||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
|
lyrics_pane: (state) => {
|
||||||
|
return state.lyrics.lyrics_pane
|
||||||
|
},
|
||||||
|
|
||||||
|
lyrics: (state) => {
|
||||||
|
return state.lyrics.lyrics
|
||||||
|
},
|
||||||
|
|
||||||
now_playing: (state) => {
|
now_playing: (state) => {
|
||||||
const item = state.queue.items.find(function (item) {
|
const item = state.queue.items.find(function (item) {
|
||||||
return item.id === state.player.item_id
|
return item.id === state.player.item_id
|
||||||
@ -188,6 +201,26 @@ export default createStore({
|
|||||||
[types.UPDATE_QUEUE](state, queue) {
|
[types.UPDATE_QUEUE](state, queue) {
|
||||||
state.queue = queue
|
state.queue = queue
|
||||||
},
|
},
|
||||||
|
[types.UPDATE_LYRICS](state, lyrics) {
|
||||||
|
// Parse from .LRC or text format to synchronized lyrics
|
||||||
|
function parse(lyrics) {
|
||||||
|
let lyricsObj = [];
|
||||||
|
let tempArr = lyrics.split("\n");
|
||||||
|
const regex = /(\[(\d+):(\d+)(?:\.\d+)?\] ?)?(.*)/;
|
||||||
|
|
||||||
|
tempArr.forEach((item) => {
|
||||||
|
let matches = regex.exec(item);
|
||||||
|
if (matches !== null && matches[4].length) {
|
||||||
|
let obj = [matches[4]];
|
||||||
|
if (matches[2] != null && matches[3] != null)
|
||||||
|
obj.push(parseInt(matches[2], 10) * 60 + parseInt(matches[3], 10));
|
||||||
|
lyricsObj.push(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return lyricsObj;
|
||||||
|
}
|
||||||
|
state.lyrics.lyrics = "lyrics" in lyrics ? parse(lyrics.lyrics) : ""
|
||||||
|
},
|
||||||
[types.UPDATE_LASTFM](state, lastfm) {
|
[types.UPDATE_LASTFM](state, lastfm) {
|
||||||
state.lastfm = lastfm
|
state.lastfm = lastfm
|
||||||
},
|
},
|
||||||
|
@ -8,6 +8,7 @@ export const UPDATE_LIBRARY_RSS_COUNT = 'UPDATE_LIBRARY_RSS_COUNT'
|
|||||||
export const UPDATE_OUTPUTS = 'UPDATE_OUTPUTS'
|
export const UPDATE_OUTPUTS = 'UPDATE_OUTPUTS'
|
||||||
export const UPDATE_PLAYER_STATUS = 'UPDATE_PLAYER_STATUS'
|
export const UPDATE_PLAYER_STATUS = 'UPDATE_PLAYER_STATUS'
|
||||||
export const UPDATE_QUEUE = 'UPDATE_QUEUE'
|
export const UPDATE_QUEUE = 'UPDATE_QUEUE'
|
||||||
|
export const UPDATE_LYRICS = 'UPDATE_LYRICS'
|
||||||
export const UPDATE_LASTFM = 'UPDATE_LASTFM'
|
export const UPDATE_LASTFM = 'UPDATE_LASTFM'
|
||||||
export const UPDATE_SPOTIFY = 'UPDATE_SPOTIFY'
|
export const UPDATE_SPOTIFY = 'UPDATE_SPOTIFY'
|
||||||
export const UPDATE_PAIRING = 'UPDATE_PAIRING'
|
export const UPDATE_PAIRING = 'UPDATE_PAIRING'
|
||||||
|
Loading…
Reference in New Issue
Block a user