mirror of
https://github.com/owntone/owntone-server.git
synced 2025-04-30 15:11:25 -04:00
Merge pull request #747 from chme/pr718-save-queue
Rebased version of pr718 "save queue playlist"
This commit is contained in:
commit
1aae870449
@ -191,6 +191,16 @@ library {
|
|||||||
# (played or skipped). Both results are combined with a mix-factor of 0.75:
|
# (played or skipped). Both results are combined with a mix-factor of 0.75:
|
||||||
# new rating = 0.75 * stable rating + 0.25 * rolling rating)
|
# new rating = 0.75 * stable rating + 0.25 * rolling rating)
|
||||||
# rating_updates = false
|
# rating_updates = false
|
||||||
|
|
||||||
|
# Allows creating, deleting and modifying m3u playlists in the library directories.
|
||||||
|
# Defaults to being disabled.
|
||||||
|
# allow_modifying_stored_playlists = false
|
||||||
|
|
||||||
|
# A directory in one of the library directories that will be used as the default
|
||||||
|
# playlist directory. forked-dapd creates new playlists in this directory if only
|
||||||
|
# a playlist name is provided by the mpd client (requires "allow_modify_stored_playlists"
|
||||||
|
# set to true).
|
||||||
|
# default_playlist_directory = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Local audio output
|
# Local audio output
|
||||||
@ -320,16 +330,6 @@ mpd {
|
|||||||
# the playlist like MPD does. Note that some dacp clients do not show
|
# the playlist like MPD does. Note that some dacp clients do not show
|
||||||
# the playqueue if playback is stopped.
|
# the playqueue if playback is stopped.
|
||||||
# clear_queue_on_stop_disable = false
|
# clear_queue_on_stop_disable = false
|
||||||
|
|
||||||
# Allows creating, deleting and modifying m3u playlists in the library directories.
|
|
||||||
# Defaults to being disabled.
|
|
||||||
# allow_modifying_stored_playlists = false
|
|
||||||
|
|
||||||
# A directory in one of the library directories that will be used as the default
|
|
||||||
# playlist directory. forked-dapd creates new playlists in this directory if only
|
|
||||||
# a playlist name is provided by the mpd client (requires "allow_modify_stored_playlists"
|
|
||||||
# set to true).
|
|
||||||
# default_playlist_directory = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# SQLite configuration (allows to modify the operation of the SQLite databases)
|
# SQLite configuration (allows to modify the operation of the SQLite databases)
|
||||||
|
@ -103,6 +103,8 @@ static cfg_opt_t sec_library[] =
|
|||||||
CFG_INT("pipe_sample_rate", 44100, CFGF_NONE),
|
CFG_INT("pipe_sample_rate", 44100, CFGF_NONE),
|
||||||
CFG_INT("pipe_bits_per_sample", 16, CFGF_NONE),
|
CFG_INT("pipe_bits_per_sample", 16, CFGF_NONE),
|
||||||
CFG_BOOL("rating_updates", cfg_false, CFGF_NONE),
|
CFG_BOOL("rating_updates", cfg_false, CFGF_NONE),
|
||||||
|
CFG_BOOL("allow_modifying_stored_playlists", cfg_false, CFGF_NONE),
|
||||||
|
CFG_STR("default_playlist_directory", NULL, CFGF_NONE),
|
||||||
CFG_END()
|
CFG_END()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -178,8 +180,6 @@ static cfg_opt_t sec_mpd[] =
|
|||||||
CFG_INT("port", 6600, CFGF_NONE),
|
CFG_INT("port", 6600, CFGF_NONE),
|
||||||
CFG_INT("http_port", 0, CFGF_NONE),
|
CFG_INT("http_port", 0, CFGF_NONE),
|
||||||
CFG_BOOL("clear_queue_on_stop_disable", cfg_false, CFGF_NONE),
|
CFG_BOOL("clear_queue_on_stop_disable", cfg_false, CFGF_NONE),
|
||||||
CFG_BOOL("allow_modifying_stored_playlists", cfg_false, CFGF_NONE),
|
|
||||||
CFG_STR("default_playlist_directory", NULL, CFGF_NONE),
|
|
||||||
CFG_END()
|
CFG_END()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,6 +30,8 @@
|
|||||||
# include <config.h>
|
# include <config.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <regex.h>
|
#include <regex.h>
|
||||||
@ -57,6 +59,10 @@
|
|||||||
# include "spotify.h"
|
# include "spotify.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static bool allow_modifying_stored_playlists;
|
||||||
|
static char *default_pl_dir;
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------- HELPERS --------------------------------- */
|
/* -------------------------------- HELPERS --------------------------------- */
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
@ -706,6 +712,10 @@ jsonapi_reply_config(struct httpd_request *hreq)
|
|||||||
}
|
}
|
||||||
json_object_object_add(jreply, "directories", directories);
|
json_object_object_add(jreply, "directories", directories);
|
||||||
|
|
||||||
|
// Config for creating/modifying stored playlists
|
||||||
|
json_object_object_add(jreply, "allow_modifying_stored_playlists", json_object_new_boolean(cfg_getbool(lib, "allow_modifying_stored_playlists")));
|
||||||
|
json_object_object_add(jreply, "default_playlist_directory", json_object_new_string(cfg_getstr(lib, "default_playlist_directory")));
|
||||||
|
|
||||||
CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply)));
|
CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply)));
|
||||||
|
|
||||||
jparse_free(jreply);
|
jparse_free(jreply);
|
||||||
@ -2903,6 +2913,52 @@ jsonapi_reply_library_playlist_tracks(struct httpd_request *hreq)
|
|||||||
return HTTP_OK;
|
return HTTP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
jsonapi_reply_queue_save(struct httpd_request *hreq)
|
||||||
|
{
|
||||||
|
const char *param;
|
||||||
|
char buf[PATH_MAX+7];
|
||||||
|
char *playlist_name = NULL;
|
||||||
|
int ret = 0;
|
||||||
|
|
||||||
|
if ((param = evhttp_find_header(hreq->query, "name")) == NULL)
|
||||||
|
{
|
||||||
|
DPRINTF(E_LOG, L_WEB, "Invalid argument, missing 'name'\n");
|
||||||
|
return HTTP_BADREQUEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allow_modifying_stored_playlists)
|
||||||
|
{
|
||||||
|
DPRINTF(E_LOG, L_WEB, "Saving playlists disabled in cfg, ignoring request\n");
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access(default_pl_dir, W_OK) < 0)
|
||||||
|
{
|
||||||
|
DPRINTF(E_LOG, L_WEB, "Invalid playlist save directory=%s\n", default_pl_dir);
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
playlist_name = atrim(param);
|
||||||
|
|
||||||
|
if (strlen(playlist_name) < 1) {
|
||||||
|
free(playlist_name);
|
||||||
|
|
||||||
|
DPRINTF(E_LOG, L_WEB, "Empty playlist name parameter is not allowed\n");
|
||||||
|
return HTTP_BADREQUEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "/file:%s/%s", default_pl_dir, playlist_name);
|
||||||
|
free(playlist_name);
|
||||||
|
|
||||||
|
ret = library_queue_save(buf);
|
||||||
|
|
||||||
|
if (ret < 0)
|
||||||
|
return HTTP_INTERNAL;
|
||||||
|
|
||||||
|
return HTTP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
jsonapi_reply_library_genres(struct httpd_request *hreq)
|
jsonapi_reply_library_genres(struct httpd_request *hreq)
|
||||||
{
|
{
|
||||||
@ -3486,6 +3542,7 @@ static struct httpd_uri_map adm_handlers[] =
|
|||||||
{ EVHTTP_REQ_POST, "^/api/queue/items/add$", jsonapi_reply_queue_tracks_add },
|
{ EVHTTP_REQ_POST, "^/api/queue/items/add$", jsonapi_reply_queue_tracks_add },
|
||||||
{ EVHTTP_REQ_PUT, "^/api/queue/items/[[:digit:]]+$", jsonapi_reply_queue_tracks_move },
|
{ EVHTTP_REQ_PUT, "^/api/queue/items/[[:digit:]]+$", jsonapi_reply_queue_tracks_move },
|
||||||
{ EVHTTP_REQ_DELETE, "^/api/queue/items/[[:digit:]]+$", jsonapi_reply_queue_tracks_delete },
|
{ EVHTTP_REQ_DELETE, "^/api/queue/items/[[:digit:]]+$", jsonapi_reply_queue_tracks_delete },
|
||||||
|
{ EVHTTP_REQ_POST, "^/api/queue/save$", jsonapi_reply_queue_save},
|
||||||
|
|
||||||
{ EVHTTP_REQ_GET, "^/api/library/playlists$", jsonapi_reply_library_playlists },
|
{ EVHTTP_REQ_GET, "^/api/library/playlists$", jsonapi_reply_library_playlists },
|
||||||
{ EVHTTP_REQ_GET, "^/api/library/playlists/[[:digit:]]+$", jsonapi_reply_library_playlist },
|
{ EVHTTP_REQ_GET, "^/api/library/playlists/[[:digit:]]+$", jsonapi_reply_library_playlist },
|
||||||
@ -3557,6 +3614,9 @@ jsonapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed)
|
|||||||
case HTTP_BADREQUEST: /* 400 Bad Request */
|
case HTTP_BADREQUEST: /* 400 Bad Request */
|
||||||
httpd_send_error(req, status_code, "Bad Request");
|
httpd_send_error(req, status_code, "Bad Request");
|
||||||
break;
|
break;
|
||||||
|
case 403:
|
||||||
|
httpd_send_error(req, status_code, "Forbidden");
|
||||||
|
break;
|
||||||
case HTTP_NOTFOUND: /* 404 Not Found */
|
case HTTP_NOTFOUND: /* 404 Not Found */
|
||||||
httpd_send_error(req, status_code, "Not Found");
|
httpd_send_error(req, status_code, "Not Found");
|
||||||
break;
|
break;
|
||||||
@ -3600,6 +3660,22 @@ jsonapi_init(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default_pl_dir = NULL;
|
||||||
|
allow_modifying_stored_playlists = cfg_getbool(cfg_getsec(cfg, "library"), "allow_modifying_stored_playlists");
|
||||||
|
if (allow_modifying_stored_playlists)
|
||||||
|
{
|
||||||
|
default_pl_dir = cfg_getstr(cfg_getsec(cfg, "library"), "default_playlist_directory");
|
||||||
|
if (default_pl_dir == NULL)
|
||||||
|
{
|
||||||
|
allow_modifying_stored_playlists = false;
|
||||||
|
DPRINTF(E_LOG, L_WEB, "Invalid playlist save directory, disabling\n");
|
||||||
|
}
|
||||||
|
else if (access(default_pl_dir, W_OK) < 0)
|
||||||
|
{
|
||||||
|
DPRINTF(E_WARN, L_WEB, "Non-writable playlist save directory=%s\n", default_pl_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4843,8 +4843,8 @@ int mpd_init(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allow_modifying_stored_playlists = cfg_getbool(cfg_getsec(cfg, "mpd"), "allow_modifying_stored_playlists");
|
allow_modifying_stored_playlists = cfg_getbool(cfg_getsec(cfg, "library"), "allow_modifying_stored_playlists");
|
||||||
pl_dir = cfg_getstr(cfg_getsec(cfg, "mpd"), "default_playlist_directory");
|
pl_dir = cfg_getstr(cfg_getsec(cfg, "library"), "default_playlist_directory");
|
||||||
if (pl_dir)
|
if (pl_dir)
|
||||||
default_pl_dir = safe_asprintf("/file:%s", pl_dir);
|
default_pl_dir = safe_asprintf("/file:%s", pl_dir);
|
||||||
|
|
||||||
|
90
web-src/src/components/ModalDialogPlaylistSave.vue
Normal file
90
web-src/src/components/ModalDialogPlaylistSave.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<transition name="fade">
|
||||||
|
<div class="modal is-active" v-if="show">
|
||||||
|
<div class="modal-background" @click="$emit('close')"></div>
|
||||||
|
<div class="modal-content fd-modal-card">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="title is-4">
|
||||||
|
Save queue to playlist
|
||||||
|
</p>
|
||||||
|
<form v-on:submit.prevent="save" class="fd-has-margin-bottom">
|
||||||
|
<div class="field">
|
||||||
|
<p class="control is-expanded has-icons-left">
|
||||||
|
<input class="input is-shadowless" type="text" placeholder="Playlist name" v-model="playlist_name" :disabled="loading" ref="playlist_name_field">
|
||||||
|
<span class="icon is-left">
|
||||||
|
<i class="mdi mdi-file-music"></i>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<footer class="card-footer" v-if="loading">
|
||||||
|
<a class="card-footer-item has-text-dark">
|
||||||
|
<span class="icon"><i class="mdi mdi-web"></i></span> <span class="is-size-7">Saving ...</span>
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
<footer class="card-footer" v-else>
|
||||||
|
<a class="card-footer-item has-text-danger" @click="$emit('close')">
|
||||||
|
<span class="icon"><i class="mdi mdi-cancel"></i></span> <span class="is-size-7">Cancel</span>
|
||||||
|
</a>
|
||||||
|
<a class="card-footer-item has-background-info has-text-white has-text-weight-bold" @click="save">
|
||||||
|
<span class="icon"><i class="mdi mdi-content-save"></i></span> <span class="is-size-7">Save</span>
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ModalDialogPlaylistSave',
|
||||||
|
props: [ 'show' ],
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
playlist_name: '',
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
save: function () {
|
||||||
|
if (this.playlist_name.length < 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
webapi.queue_save_playlist(this.playlist_name).then(() => {
|
||||||
|
this.$emit('close')
|
||||||
|
this.playlist_name = ''
|
||||||
|
}).catch(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'show' () {
|
||||||
|
if (this.show) {
|
||||||
|
this.loading = false
|
||||||
|
|
||||||
|
// We need to delay setting the focus to the input field until the field is part of the dom and visible
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$refs.playlist_name_field.focus()
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
@ -38,6 +38,12 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>Clear</span>
|
<span>Clear</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="button is-small" v-if="is_queue_save_allowed" :disabled="queue_items.length === 0" @click="save_dialog">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="mdi mdi-content-save"></i>
|
||||||
|
</span>
|
||||||
|
<span>Save</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
@ -59,6 +65,7 @@
|
|||||||
</draggable>
|
</draggable>
|
||||||
<modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" />
|
<modal-dialog-queue-item :show="show_details_modal" :item="selected_item" @close="show_details_modal = false" />
|
||||||
<modal-dialog-add-url-stream :show="show_url_modal" @close="show_url_modal = false" />
|
<modal-dialog-add-url-stream :show="show_url_modal" @close="show_url_modal = false" />
|
||||||
|
<modal-dialog-playlist-save v-if="is_queue_save_allowed" :show="show_pls_save_modal" @close="show_pls_save_modal = false" />
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</template>
|
</template>
|
||||||
@ -68,13 +75,14 @@ import ContentWithHeading from '@/templates/ContentWithHeading'
|
|||||||
import ListItemQueueItem from '@/components/ListItemQueueItem'
|
import ListItemQueueItem from '@/components/ListItemQueueItem'
|
||||||
import ModalDialogQueueItem from '@/components/ModalDialogQueueItem'
|
import ModalDialogQueueItem from '@/components/ModalDialogQueueItem'
|
||||||
import ModalDialogAddUrlStream from '@/components/ModalDialogAddUrlStream'
|
import ModalDialogAddUrlStream from '@/components/ModalDialogAddUrlStream'
|
||||||
|
import ModalDialogPlaylistSave from '@/components/ModalDialogPlaylistSave'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
import * as types from '@/store/mutation_types'
|
import * as types from '@/store/mutation_types'
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PageQueue',
|
name: 'PageQueue',
|
||||||
components: { ContentWithHeading, ListItemQueueItem, draggable, ModalDialogQueueItem, ModalDialogAddUrlStream },
|
components: { ContentWithHeading, ListItemQueueItem, draggable, ModalDialogQueueItem, ModalDialogAddUrlStream, ModalDialogPlaylistSave },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -82,6 +90,7 @@ export default {
|
|||||||
|
|
||||||
show_details_modal: false,
|
show_details_modal: false,
|
||||||
show_url_modal: false,
|
show_url_modal: false,
|
||||||
|
show_pls_save_modal: false,
|
||||||
selected_item: {}
|
selected_item: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -90,6 +99,9 @@ export default {
|
|||||||
state () {
|
state () {
|
||||||
return this.$store.state.player
|
return this.$store.state.player
|
||||||
},
|
},
|
||||||
|
is_queue_save_allowed () {
|
||||||
|
return this.$store.state.config.allow_modifying_stored_playlists && this.$store.state.config.default_playlist_directory
|
||||||
|
},
|
||||||
queue () {
|
queue () {
|
||||||
return this.$store.state.queue
|
return this.$store.state.queue
|
||||||
},
|
},
|
||||||
@ -135,6 +147,10 @@ export default {
|
|||||||
|
|
||||||
open_add_stream_dialog: function (item) {
|
open_add_stream_dialog: function (item) {
|
||||||
this.show_url_modal = true
|
this.show_url_modal = true
|
||||||
|
},
|
||||||
|
|
||||||
|
save_dialog: function (item) {
|
||||||
|
this.show_pls_save_modal = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,13 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
queue_save_playlist (name) {
|
||||||
|
return axios.post('/api/queue/save', undefined, { params: { 'name': name } }).then((response) => {
|
||||||
|
store.dispatch('add_notification', { text: 'Queue saved to playlist "' + name + '"', type: 'info', timeout: 2000 })
|
||||||
|
return Promise.resolve(response)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
player_status () {
|
player_status () {
|
||||||
return axios.get('/api/player')
|
return axios.get('/api/player')
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user