From 79e18189477e181903a5d44006f6e642c509befa Mon Sep 17 00:00:00 2001 From: jcorporation Date: Sun, 1 Sep 2024 21:12:41 +0200 Subject: [PATCH] Feat: Add resume for albums #1338 --- CHANGELOG.md | 5 +- htdocs/js/album.js | 54 ++++++++++++++++++++++ htdocs/js/apidoc.js | 33 +++++++++++++- htdocs/js/clickActions.js | 17 +++++++ htdocs/js/playlists.js | 1 + htdocs/js/viewBrowseDatabase.js | 22 +++++++++ src/lib/api.h | 3 ++ src/mpd_client/search.c | 36 +++++++++++++++ src/mpd_client/search.h | 3 ++ src/mympd_api/browse.c | 7 +++ src/mympd_api/mympd_api_handler.c | 31 +++++++++++++ src/mympd_api/queue.c | 76 +++++++++++++++++++++++++++++++ src/mympd_api/queue.h | 7 +++ 13 files changed, 292 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e22a786c8..2de993965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,11 +28,14 @@ An another notable feature is the new list view that supplements the table and g - MYMPD_API_QUEUE_APPEND_PLAYLIST_RANGE: new - MYMPD_API_QUEUE_INSERT_PLAYLIST_RANGE: new - MYMPD_API_QUEUE_REPLACE_PLAYLIST_RANGE: new +- MYMPD_API_QUEUE_APPEND_ALBUM_RANGE: new +- MYMPD_API_QUEUE_INSERT_ALBUM_RANGE: new +- MYMPD_API_QUEUE_REPLACE_ALBUM_RANGE: new - MYMPD_API_SETTINGS_GET: returns now available sticker types ### Changelog -- Feat: Resume for songs +- Feat: Resume for songs, playlists and albums #1338 - Feat: Rating for albums and playlists #1134 - Feat: User defined stickers #1091 - Feat: Add list view diff --git a/htdocs/js/album.js b/htdocs/js/album.js index 080ddc12d..d4279a49a 100644 --- a/htdocs/js/album.js +++ b/htdocs/js/album.js @@ -37,3 +37,57 @@ function addAlbumDisc(action, albumId, disc) { logError('Invalid action: ' + action); } } + +/** + * Resume album API implementation. + * Load the album from last played song and start playing. + * @param {string} albumId Album ID + * @param {number} pos Position of first song to resume + * @param {string} action Action + * @returns {void} + */ +function resumeAlbum(albumId, pos, action) { + pos++; + switch(action) { + case 'append': + case 'appendPlay': + sendAPI("MYMPD_API_QUEUE_APPEND_ALBUM_RANGE", { + 'albumid': albumId, + 'start': pos, + 'end': -1, + 'play': true + }, null, false); + break; + case 'insert': + sendAPI('MYMPD_API_QUEUE_INSERT_ALBUM_RANGE', { + 'albumid': albumId, + 'start': pos, + 'end': -1, + 'play': true, + 'to': 0, + 'whence': 0 + }, null, false); + break; + case 'insertAfterCurrent': + case 'insertPlayAfterCurrent': + sendAPI('MYMPD_API_QUEUE_INSERT_ALBUM_RANGE', { + 'albumid': albumId, + 'start': pos, + 'end': -1, + 'play': true, + 'to': 0, + 'whence': 1 + }, null, false); + break; + case 'replace': + case 'replacePlay': + sendAPI("MYMPD_API_QUEUE_REPLACE_ALBUM_RANGE", { + 'albumid': albumId, + 'start': pos, + 'end': -1, + 'play': true + }, null, false); + break; + // No default + } +} diff --git a/htdocs/js/apidoc.js b/htdocs/js/apidoc.js index adf2b83e2..37c4f242e 100644 --- a/htdocs/js/apidoc.js +++ b/htdocs/js/apidoc.js @@ -502,7 +502,7 @@ const APImethods = { } }, "MYMPD_API_QUEUE_INSERT_ALBUMS": { - "desc": "Adds the albums to distinct position in the queue.", + "desc": "Inserts the albums to distinct position in the queue.", "params": { "albumids": APIparams.albumids, "to": APIparams.to, @@ -511,7 +511,7 @@ const APImethods = { } }, "MYMPD_API_QUEUE_INSERT_ALBUM_DISC": { - "desc": "Adds one discs from an album to distinct position in the queue.", + "desc": "Inserts one discs from an album to distinct position in the queue.", "params": { "albumid": APIparams.albumid, "disc": APIparams.disc, @@ -520,6 +520,17 @@ const APImethods = { "play": APIparams.play } }, + "MYMPD_API_QUEUE_INSERT_ALBUM_RANGE": { + "desc": "Inserts a range of song from an album into the queue", + "params": { + "albumid": APIparams.albumid, + "start": APIparams.start, + "end": APIparams.end, + "to": APIparams.to, + "whence": APIparams.whence, + "play": APIparams.play + } + }, "MYMPD_API_QUEUE_APPEND_PLAYLISTS": { "desc": "Appends the playlists to the queue.", "params": { @@ -581,6 +592,15 @@ const APImethods = { "play": APIparams.play } }, + "MYMPD_API_QUEUE_APPEND_ALBUM_RANGE": { + "desc": "Appends one disc of an album to the queue", + "params": { + "albumid": APIparams.albumid, + "start": APIparams.start, + "end": APIparams.end, + "play": APIparams.play + } + }, "MYMPD_API_QUEUE_REPLACE_PLAYLISTS": { "desc": "Replaces the queue with the playlists.", "params": { @@ -642,6 +662,15 @@ const APImethods = { "play": APIparams.play } }, + "MYMPD_API_QUEUE_REPLACE_ALBUM_RANGE": { + "desc": "Replaces the queue with a range of song from an album", + "params": { + "albumid": APIparams.albumid, + "start": APIparams.start, + "end": APIparams.end, + "play": APIparams.play + } + }, "MYMPD_API_QUEUE_SHUFFLE": { "desc": "Shuffles the queue.", "params": {} diff --git a/htdocs/js/clickActions.js b/htdocs/js/clickActions.js index 0b4edbc5a..32ff79e78 100644 --- a/htdocs/js/clickActions.js +++ b/htdocs/js/clickActions.js @@ -459,3 +459,20 @@ function clickResumePlist(event) { const action = event.target.getAttribute('data-action'); resumePlist(uri, pos, action); } + +/** + * Handler for resume album dropdown actions + * @param {Event} event Click event + * @returns {void} + */ +function clickResumeAlbum(event) { + event.preventDefault(); + if (event.target.nodeName !== 'BUTTON') { + return; + } + const dataNode = event.target.closest('.btn-group'); + const pos = getData(dataNode, 'pos'); + const albumId = getDataId('viewDatabaseAlbumDetailCover', 'AlbumId'); + const action = event.target.getAttribute('data-action'); + resumeAlbum(albumId, pos, action); +} diff --git a/htdocs/js/playlists.js b/htdocs/js/playlists.js index 18d56274d..40a21c177 100644 --- a/htdocs/js/playlists.js +++ b/htdocs/js/playlists.js @@ -377,6 +377,7 @@ function isMPDplaylist(uri) { * @returns {void} */ function resumePlist(plist, pos, action) { + pos++; switch(action) { case 'append': case 'appendPlay': diff --git a/htdocs/js/viewBrowseDatabase.js b/htdocs/js/viewBrowseDatabase.js index ccef1b60b..da4dde62c 100644 --- a/htdocs/js/viewBrowseDatabase.js +++ b/htdocs/js/viewBrowseDatabase.js @@ -308,6 +308,28 @@ function parseAlbumDetails(obj) { infoEl.appendChild(mbField); } + if (obj.result.lastPlayedSong.uri !== '' && + obj.result.lastPlayedSong.pos < obj.result.totalEntities - 1) + { + const resumeBtn = pEl.resumeBtn.cloneNode(true); + resumeBtn.classList.add('ms-3', 'dropdown'); + resumeBtn.classList.remove('dropup'); + setData(resumeBtn, 'pos', obj.result.lastPlayedSong.pos); + new BSN.Dropdown(resumeBtn.firstElementChild); + resumeBtn.lastElementChild.firstElementChild.addEventListener('click', function(event) { + clickResumeAlbum(event); + }, false); + infoEl.appendChild( + elCreateNodes('div', {"class": ["col-xl-6"]}, [ + elCreateTextTn('small', {}, 'Last played'), + elCreateNodes('p', {}, [ + document.createTextNode(obj.result.lastPlayedSong.title), + resumeBtn + ]) + ]) + ); + } + const rowTitle = tn(settingsWebuiFields.clickSong.validValues[settings.webuiSettings.clickSong]); updateTable(obj, 'BrowseDatabaseAlbumDetail', function(row, data) { setData(row, 'type', 'song'); diff --git a/src/lib/api.h b/src/lib/api.h index 308972b27..341bbde80 100644 --- a/src/lib/api.h +++ b/src/lib/api.h @@ -156,6 +156,7 @@ X(MYMPD_API_QUEUE_APPEND_URI_RESUME) \ X(MYMPD_API_QUEUE_APPEND_ALBUMS) \ X(MYMPD_API_QUEUE_APPEND_ALBUM_DISC) \ + X(MYMPD_API_QUEUE_APPEND_ALBUM_RANGE) \ X(MYMPD_API_QUEUE_CLEAR) \ X(MYMPD_API_QUEUE_CROP) \ X(MYMPD_API_QUEUE_CROP_OR_CLEAR) \ @@ -167,6 +168,7 @@ X(MYMPD_API_QUEUE_INSERT_URI_RESUME) \ X(MYMPD_API_QUEUE_INSERT_ALBUMS) \ X(MYMPD_API_QUEUE_INSERT_ALBUM_DISC) \ + X(MYMPD_API_QUEUE_INSERT_ALBUM_RANGE) \ X(MYMPD_API_QUEUE_MOVE_POSITION) \ X(MYMPD_API_QUEUE_MOVE_RELATIVE) \ X(MYMPD_API_QUEUE_PRIO_SET) \ @@ -179,6 +181,7 @@ X(MYMPD_API_QUEUE_REPLACE_URI_RESUME) \ X(MYMPD_API_QUEUE_REPLACE_ALBUMS) \ X(MYMPD_API_QUEUE_REPLACE_ALBUM_DISC) \ + X(MYMPD_API_QUEUE_REPLACE_ALBUM_RANGE) \ X(MYMPD_API_QUEUE_RM_RANGE) \ X(MYMPD_API_QUEUE_RM_IDS) \ X(MYMPD_API_QUEUE_SAVE) \ diff --git a/src/mpd_client/search.c b/src/mpd_client/search.c index dd13b7f92..ae6abac5b 100644 --- a/src/mpd_client/search.c +++ b/src/mpd_client/search.c @@ -108,6 +108,42 @@ bool mpd_client_search_add_to_queue(struct t_partition_state *partition_state, c return mympd_check_error_and_recover(partition_state, error, "mpd_search_add_db_songs"); } +/** + * Searches the mpd database for songs by expression and adds the result window to the queue + * @param partition_state pointer to partition specific states + * @param expression mpd search expression + * @param to position to insert the songs, UINT_MAX to append + * @param whence enum mpd_position_whence: + * 0 = MPD_POSITION_ABSOLUTE + * 1 = MPD_POSITION_AFTER_CURRENT + * 2 = MPD_POSITION_BEFORE_CURRENT + * @param sort tag to sort + * @param sortdesc false = ascending, true = descending + * @param start Start of the range (including) + * @param end End of the range (excluding), use UINT_MAX for open end + * @param error pointer to already allocated sds string for the error message + * or NULL to return no response + * @return true on success else false + */ +bool mpd_client_search_add_to_queue_window(struct t_partition_state *partition_state, const char *expression, + unsigned to, enum mpd_position_whence whence, const char *sort, bool sortdesc, + unsigned start, unsigned end, sds *error) +{ + if (mpd_search_add_db_songs(partition_state->conn, false) == false || + mpd_search_add_expression(partition_state->conn, expression) == false || + mpd_client_add_search_sort_param(partition_state, sort, sortdesc, true) == false || + mpd_search_add_window(partition_state->conn, start, end) == false || + add_search_whence_param(partition_state, to, whence) == false) + { + mpd_search_cancel(partition_state->conn); + *error = sdscat(*error, "Error creating MPD search command"); + return false; + } + mpd_search_commit(partition_state->conn); + mpd_response_finish(partition_state->conn); + return mympd_check_error_and_recover(partition_state, error, "mpd_search_add_db_songs"); +} + /** * Creates a mpd search expression to find all songs in an album * @param tag_albumartist albumartist tag diff --git a/src/mpd_client/search.h b/src/mpd_client/search.h index 829b460aa..50ca9b9c2 100644 --- a/src/mpd_client/search.h +++ b/src/mpd_client/search.h @@ -19,6 +19,9 @@ bool mpd_client_search_add_to_plist_window(struct t_partition_state *partition_s const char *plist, unsigned to, const char *sort, bool sortdesc, unsigned start, unsigned end, sds *error); bool mpd_client_search_add_to_queue(struct t_partition_state *partition_state, const char *expression, unsigned to, enum mpd_position_whence whence, const char *sort, bool sortdesc, sds *error); +bool mpd_client_search_add_to_queue_window(struct t_partition_state *partition_state, const char *expression, + unsigned to, enum mpd_position_whence whence, const char *sort, bool sortdesc, + unsigned start, unsigned end, sds *error); bool mpd_client_add_search_sort_param(struct t_partition_state *partition_state, const char *sort, bool sortdesc, bool check_version); bool mpd_client_add_search_group_param(struct mpd_connection *conn, enum mpd_tag_type tag); diff --git a/src/mympd_api/browse.c b/src/mympd_api/browse.c index 182ad784b..3fe35e642 100644 --- a/src/mympd_api/browse.c +++ b/src/mympd_api/browse.c @@ -84,6 +84,8 @@ sds mympd_api_browse_album_detail(struct t_mympd_state *mympd_state, struct t_pa time_t last_played_max = 0; sds first_song_uri = sdsempty(); sds last_played_song_uri = sdsempty(); + sds last_played_song_title = sdsempty(); + unsigned last_played_song_pos = 0; if (partition_state->config->albums.mode == ALBUM_MODE_SIMPLE) { // reset album values for simple album mode album_cache_set_total_time(mpd_album, 0); @@ -119,6 +121,8 @@ sds mympd_api_browse_album_detail(struct t_mympd_state *mympd_state, struct t_pa if (sticker.mympd[STICKER_LAST_PLAYED] > last_played_max) { last_played_max = (time_t)sticker.mympd[STICKER_LAST_PLAYED]; last_played_song_uri = sds_replace(last_played_song_uri, mpd_song_get_uri(song)); + last_played_song_title = sds_replace(last_played_song_title, mpd_song_get_tag(song, MPD_TAG_TITLE, 0)); + last_played_song_pos = entities_returned - 1; } sticker_struct_clear(&sticker); } @@ -153,6 +157,8 @@ sds mympd_api_browse_album_detail(struct t_mympd_state *mympd_state, struct t_pa buffer = print_album_tags(buffer, partition_state->mpd_state, &partition_state->mpd_state->tags_album, mpd_album); buffer = sdscat(buffer, ",\"lastPlayedSong\":{"); buffer = tojson_time(buffer, "time", last_played_max, true); + buffer = tojson_uint(buffer, "pos", last_played_song_pos, true); + buffer = tojson_sds(buffer, "title", last_played_song_uri, true); buffer = tojson_sds(buffer, "uri", last_played_song_uri, false); buffer = sdscatlen(buffer, "}", 1); if (partition_state->mpd_state->feat.stickers == true) { @@ -166,6 +172,7 @@ sds mympd_api_browse_album_detail(struct t_mympd_state *mympd_state, struct t_pa FREE_SDS(expression); FREE_SDS(first_song_uri); FREE_SDS(last_played_song_uri); + FREE_SDS(last_played_song_title); return buffer; } diff --git a/src/mympd_api/mympd_api_handler.c b/src/mympd_api/mympd_api_handler.c index 53a963419..51506196d 100644 --- a/src/mympd_api/mympd_api_handler.c +++ b/src/mympd_api/mympd_api_handler.c @@ -1219,6 +1219,37 @@ void mympd_api_handler(struct t_mympd_state *mympd_state, struct t_partition_sta } break; } + case MYMPD_API_QUEUE_APPEND_ALBUM_RANGE: + case MYMPD_API_QUEUE_REPLACE_ALBUM_RANGE: { + if (json_get_string(request->data, "$.params.albumid", 1, NAME_LEN_MAX, &sds_buf1, vcb_isalnum, &parse_error) == true && + json_get_uint(request->data, "$.params.start", 0, MPD_PLAYLIST_LENGTH_MAX, &uint_buf1, &parse_error) == true && + json_get_int(request->data, "$.params.end", -1, MPD_PLAYLIST_LENGTH_MAX, &int_buf1, &parse_error) == true && + json_get_bool(request->data, "$.params.play", &bool_buf1, &parse_error) == true) + { + rc = (request->cmd_id == MYMPD_API_QUEUE_APPEND_ALBUM_DISC + ? mympd_api_queue_append_album_range(partition_state, &mympd_state->album_cache, sds_buf1, uint_buf1, int_buf1, &error) + : mympd_api_queue_replace_album_range(partition_state, &mympd_state->album_cache, sds_buf1, uint_buf1, int_buf1, &error)) && + mpd_client_queue_check_start_play(partition_state, bool_buf1, &error); + response->data = jsonrpc_respond_with_message_or_error(response->data, request->cmd_id, request->id, rc, + JSONRPC_FACILITY_QUEUE, "Queue updated", error); + } + break; + } + case MYMPD_API_QUEUE_INSERT_ALBUM_RANGE: { + if (json_get_string(request->data, "$.params.albumid", 1, NAME_LEN_MAX, &sds_buf1, vcb_isalnum, &parse_error) == true && + json_get_uint(request->data, "$.params.to", 0, MPD_PLAYLIST_LENGTH_MAX, &uint_buf1, &parse_error) == true && + json_get_uint(request->data, "$.params.whence", 0, 2, &uint_buf2, &parse_error) == true && + json_get_uint(request->data, "$.params.start", 0, MPD_PLAYLIST_LENGTH_MAX, &uint_buf3, &parse_error) == true && + json_get_int(request->data, "$.params.end", -1, MPD_PLAYLIST_LENGTH_MAX, &int_buf1, &parse_error) == true && + json_get_bool(request->data, "$.params.play", &bool_buf1, &parse_error) == true) + { + rc = mympd_api_queue_insert_album_range(partition_state, &mympd_state->album_cache, sds_buf1, uint_buf3, int_buf1, uint_buf1, uint_buf2, &error) && + mpd_client_queue_check_start_play(partition_state, bool_buf1, &error); + response->data = jsonrpc_respond_with_message_or_error(response->data, request->cmd_id, request->id, rc, + JSONRPC_FACILITY_QUEUE, "Queue updated", error); + } + break; + } case MYMPD_API_QUEUE_SAVE: if (json_get_string(request->data, "$.params.plist", 1, FILENAME_LEN_MAX, &sds_buf1, vcb_isfilename, &parse_error) == true && json_get_string(request->data, "$.params.mode", 1, NAME_LEN_MAX, &sds_buf2, vcb_isalnum, &parse_error) == true) diff --git a/src/mympd_api/queue.c b/src/mympd_api/queue.c index a2dbf3c5a..fdc2367c9 100644 --- a/src/mympd_api/queue.c +++ b/src/mympd_api/queue.c @@ -535,6 +535,82 @@ bool mympd_api_queue_replace_album_disc(struct t_partition_state *partition_stat mympd_api_queue_append_album_disc(partition_state, album_cache, albumid, disc, error); } +/** + * Inserts a range of song from an album into the queue + * @param partition_state pointer to partition state + * @param album_cache pointer to album cache + * @param albumid album id to insert + * @param start start of the range (including) + * @param end end of the range (excluded) + * @param to position to insert + * @param whence how to interpret the to parameter + * @param error pointer to an already allocated sds string for the error message + * @return true on success, else false + */ +bool mympd_api_queue_insert_album_range(struct t_partition_state *partition_state, struct t_cache *album_cache, + sds albumid, unsigned start, int end, unsigned to, unsigned whence, sds *error) +{ + if (whence != MPD_POSITION_ABSOLUTE && + partition_state->mpd_state->feat.whence == false) + { + *error = sdscat(*error, "Method not supported"); + return false; + } + if (partition_state->mpd_state->feat.search_add_sort_window == false) { + *error = sdscat(*error, "Method not supported"); + return false; + } + struct mpd_song *mpd_album = album_cache_get_album(album_cache, albumid); + if (mpd_album == NULL) { + *error = sdscat(*error, "Album not found"); + return false; + } + unsigned end_uint = end == -1 + ? UINT_MAX + : (unsigned)end; + sds expression = get_search_expression_album(partition_state->mpd_state->tag_albumartist, + mpd_album, &partition_state->config->albums); + const char *sort = NULL; + bool sortdesc = false; + bool rc = mpd_client_search_add_to_queue_window(partition_state, expression, to, whence, + sort, sortdesc, start, end_uint, error); + FREE_SDS(expression); + return rc; +} + +/** + * Appends a range of song from an album to the queue + * @param partition_state pointer to partition state + * @param album_cache pointer to album cache + * @param albumid album id to append + * @param start start of the range (including) + * @param end end of the range (excluded) + * @param error pointer to an already allocated sds string for the error message + * @return true on success, else false + */ +bool mympd_api_queue_append_album_range(struct t_partition_state *partition_state, struct t_cache *album_cache, + sds albumid, unsigned start, int end, sds *error) +{ + return mympd_api_queue_insert_album_range(partition_state, album_cache, albumid, start, end, UINT_MAX, MPD_POSITION_ABSOLUTE, error); +} + +/** + * Replaces the queue with a range of song from an album + * @param partition_state pointer to partition state + * @param album_cache pointer to album cache + * @param albumid album id to insert + * @param start start of the range (including) + * @param end end of the range (excluded) + * @param error pointer to an already allocated sds string for the error message + * @return true on success, else false + */ +bool mympd_api_queue_replace_album_range(struct t_partition_state *partition_state,struct t_cache *album_cache, + sds albumid, unsigned start, int end, sds *error) +{ + return mpd_client_queue_clear(partition_state, error) && + mympd_api_queue_append_album_range(partition_state, album_cache, albumid, start, end, error); +} + /** * Inserts a playlist range into the queue * @param partition_state pointer to partition state diff --git a/src/mympd_api/queue.h b/src/mympd_api/queue.h index 30b2b5c21..144676408 100644 --- a/src/mympd_api/queue.h +++ b/src/mympd_api/queue.h @@ -66,4 +66,11 @@ bool mympd_api_queue_insert_album_disc(struct t_partition_state *partition_state sds albumid, sds disc, unsigned to, unsigned whence, sds *error); bool mympd_api_queue_replace_album_disc(struct t_partition_state *partition_state, struct t_cache *album_cache, sds albumid, sds disc, sds *error); +bool mympd_api_queue_insert_album_range(struct t_partition_state *partition_state, struct t_cache *album_cache, + sds albumid, unsigned start, int end, unsigned to, unsigned whence, sds *error); +bool mympd_api_queue_append_album_range(struct t_partition_state *partition_state, struct t_cache *album_cache, + sds albumid, unsigned start, int end, sds *error); +bool mympd_api_queue_replace_album_range(struct t_partition_state *partition_state, struct t_cache *album_cache, + sds albumid, unsigned start, int end, sds *error); + #endif