From: marcelveldt Date: Sun, 27 Oct 2019 22:29:08 +0000 (+0100) Subject: allow playlist add/remove from frontend X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=b055c0da85f35720b26e911517420be23fcf6965;p=music-assistant-server.git allow playlist add/remove from frontend --- diff --git a/music_assistant/database.py b/music_assistant/database.py index 7f087f1f..ebff6bed 100755 --- a/music_assistant/database.py +++ b/music_assistant/database.py @@ -77,6 +77,7 @@ class Database(): if MediaType.Album in media_types: result["albums"] = await self.albums(sql_query, limit=limit) if MediaType.Track in media_types: + sql_query = 'SELECT * FROM tracks WHERE name LIKE "%s"' % searchquery result["tracks"] = await self.tracks(sql_query, limit=limit) if MediaType.Playlist in media_types: result["playlists"] = await self.playlists(sql_query, limit=limit) @@ -101,9 +102,9 @@ class Database(): async def library_tracks(self, provider=None, limit=100000, offset=0, orderby='name') -> List[Track]: ''' get all library tracks, optionally filtered by provider''' if provider != None: - sql_query = ' WHERE track_id in (SELECT item_id FROM library_items WHERE provider = "%s" AND media_type = %d)' % (provider,MediaType.Track) + sql_query = 'SELECT * FROM tracks WHERE track_id in (SELECT item_id FROM library_items WHERE provider = "%s" AND media_type = %d)' % (provider,MediaType.Track) else: - sql_query = ' WHERE track_id in (SELECT item_id FROM library_items WHERE media_type = %d)' % MediaType.Track + sql_query = 'SELECT * FROM tracks WHERE track_id in (SELECT item_id FROM library_items WHERE media_type = %d)' % MediaType.Track return await self.tracks(sql_query, limit=limit, offset=offset, orderby=orderby) async def playlists(self, filter_query=None, provider=None, limit=100000, offset=0, orderby='name') -> List[Playlist]: @@ -406,47 +407,46 @@ class Database(): LOGGER.debug('added album %s (%s) to database: %s' %(album.name, album.provider_ids, album_id)) return album_id - async def tracks(self, filter_query=None, limit=100000, offset=0, orderby='name', fulldata=True, db=None) -> List[Track]: + async def tracks(self, custom_query=None, limit=100000, offset=0, orderby='name', fulldata=True) -> List[Track]: ''' fetch all track records from table''' tracks = [] sql_query = 'SELECT * FROM tracks' - if filter_query: - sql_query += ' ' + filter_query + if custom_query: + sql_query = custom_query sql_query += ' ORDER BY %s' % orderby if limit: sql_query += ' LIMIT %d OFFSET %d' %(limit, offset) - if not db: - db = await aiosqlite.connect(self.dbfile) - should_close_db = True - else: - should_close_db = False - async with db.execute(sql_query) as cursor: - db_rows = await cursor.fetchall() - for db_row in db_rows: - track = Track() - track.item_id = db_row[0] - track.name = db_row[1] - track.album = await self.album(db_row[2], fulldata=False, db=db) - track.artists = await self.__get_track_artists(track.item_id, db, fulldata=False) - track.duration = db_row[3] - track.version = db_row[4] - track.disc_number = db_row[5] - track.track_number = db_row[6] - track.in_library = await self.__get_library_providers(track.item_id, MediaType.Track, db) - track.external_ids = await self.__get_external_ids(track.item_id, MediaType.Track, db) - track.provider_ids = await self.__get_prov_ids(track.item_id, MediaType.Track, db) - if fulldata: - track.metadata = await self.__get_metadata(track.item_id, MediaType.Track, db) - track.tags = await self.__get_tags(track.item_id, MediaType.Track, db) - tracks.append(track) - if should_close_db: - await db.close() + async with aiosqlite.connect(self.dbfile) as db: + db.row_factory = aiosqlite.Row + async with db.execute(sql_query) as cursor: + for db_row in await cursor.fetchall(): + track = Track() + track.item_id = db_row["track_id"] + track.name = db_row["name"] + track.album = await self.album(db_row["album_id"], fulldata=False, db=db) + track.artists = await self.__get_track_artists(track.item_id, db, fulldata=False) + track.duration = db_row["duration"] + track.version = db_row["version"] + track.disc_number = db_row["disc_number"] + track.track_number = db_row["track_number"] + try: + track.position = db_row["position"] + except IndexError: + pass + track.in_library = await self.__get_library_providers(track.item_id, MediaType.Track, db) + track.external_ids = await self.__get_external_ids(track.item_id, MediaType.Track, db) + track.provider_ids = await self.__get_prov_ids(track.item_id, MediaType.Track, db) + if fulldata: + track.metadata = await self.__get_metadata(track.item_id, MediaType.Track, db) + track.tags = await self.__get_tags(track.item_id, MediaType.Track, db) + tracks.append(track) return tracks async def track(self, track_id:int, fulldata=True) -> Track: ''' get track record by id ''' track_id = try_parse_int(track_id) - tracks = await self.tracks('WHERE track_id = %s' % track_id, fulldata=fulldata) + sql_query = "SELECT * FROM tracks WHERE track_id = %s" % track_id + tracks = await self.tracks(sql_query, fulldata=fulldata) if not tracks: return None return tracks[0] @@ -504,7 +504,7 @@ class Database(): async def artist_tracks(self, artist_id, limit=1000000, offset=0, orderby='name') -> List[Track]: ''' get all library tracks for the given artist ''' artist_id = try_parse_int(artist_id) - sql_query = ' WHERE track_id in (SELECT track_id FROM track_artists WHERE artist_id = %d)' % artist_id + sql_query = 'SELECT * FROM tracks WHERE track_id in (SELECT track_id FROM track_artists WHERE artist_id = %d)' % artist_id return await self.tracks(sql_query, limit=limit, offset=offset, orderby=orderby, fulldata=False) async def artist_albums(self, artist_id, limit=1000000, offset=0, orderby='name') -> List[Album]: @@ -514,22 +514,10 @@ class Database(): async def playlist_tracks(self, playlist_id:int, limit=100000, offset=0, orderby='position') -> List[Track]: ''' get playlist tracks for the given playlist_id ''' - playlist_id = try_parse_int(playlist_id) - playlist_tracks = [] - sql_query = 'SELECT track_id, position FROM playlist_tracks WHERE playlist_id = ? ORDER BY track_id' - if limit: - sql_query += ' LIMIT %d OFFSET %d' %(limit, offset) - async with aiosqlite.connect(self.dbfile) as db: - async with db.execute(sql_query, (playlist_id,)) as cursor: - db_rows = await cursor.fetchall() - playlist_track_ids = [str(item[0]) for item in db_rows] - sql_query = 'WHERE track_id in (%s)' % ','.join(playlist_track_ids) - tracks = await self.tracks(sql_query, orderby='track_id', db=db, fulldata=False) - for index, track in enumerate(tracks): - track.position = db_rows[index][1] - playlist_tracks.append(track) - playlist_tracks = sorted(playlist_tracks, key=operator.attrgetter(orderby), reverse=False) - return playlist_tracks + sql_query = """SELECT *, playlist_tracks.position FROM tracks + INNER JOIN playlist_tracks USING(track_id) + WHERE playlist_tracks.playlist_id=%s""" % playlist_id + return await self.tracks(sql_query, orderby=orderby, limit=limit, offset=offset, fulldata=False) async def add_playlist_track(self, playlist_id:int, track_id, position): ''' add playlist track to playlist ''' diff --git a/music_assistant/music_manager.py b/music_assistant/music_manager.py index c7533542..0dab61b6 100755 --- a/music_assistant/music_manager.py +++ b/music_assistant/music_manager.py @@ -263,7 +263,8 @@ class MusicManager(): new_pos = len(cur_playlist_tracks) + index await self.mass.db.add_playlist_track(playlist.item_id, track.item_id, new_pos) # actually add the tracks to the playlist on the provider - return await self.providers[playlist_prov['provider']].add_playlist_tracks(playlist_prov['item_id'], track_ids_to_add) + if track_ids_to_add: + return await self.providers[playlist_prov['provider']].add_playlist_tracks(playlist_prov['item_id'], track_ids_to_add) async def remove_playlist_tracks(self, playlist_id, tracks:List[Track]): ''' remove tracks from playlist ''' @@ -287,7 +288,8 @@ class MusicManager(): # remove track from db playlist await self.mass.db.remove_playlist_track(playlist.item_id, track.item_id) # actually remove the tracks from the playlist on the provider - return await self.providers[prov_playlist_provider_id].add_playlist_tracks(prov_playlist_playlist_id, track_ids_to_remove) + if track_ids_to_remove: + return await self.providers[prov_playlist_provider_id].add_playlist_tracks(prov_playlist_playlist_id, track_ids_to_remove) @run_periodic(3600) async def sync_music_providers(self): @@ -400,8 +402,10 @@ class MusicManager(): item_provider = prov_mapping['provider'] prov_item_id = prov_mapping['item_id'] db_item = await self.providers[item_provider].track(prov_item_id, lazy=False) - cur_db_ids.append(db_item.item_id) - if not db_item.item_id in prev_db_ids: + if not db_item.item_id in cur_db_ids: + cur_db_ids.append(db_item.item_id) + # always add/update because position could be changed + # note: we ignore duplicate tracks in the same playlist await self.mass.db.add_playlist_track(db_playlist_id, db_item.item_id, pos) pos += 1 # process playlist track deletions diff --git a/music_assistant/web/components/addtoplaylistdialog.vue.js b/music_assistant/web/components/addtoplaylistdialog.vue.js deleted file mode 100644 index 97eba4a7..00000000 --- a/music_assistant/web/components/addtoplaylistdialog.vue.js +++ /dev/null @@ -1,173 +0,0 @@ -Vue.component("addtoplaylistdialog", { - template: ` - - - - {{ header }} - {{ subheader }} -
- - - {{item.icon}} - - - {{ $t(item.label) }} - - - -
-
-
-
-`, - props: ['value', 'active_player'], - data () { - return { - mediaPlayItems: [ - { - label: "play_now", - action: "play", - icon: "play_circle_outline" - }, - { - label: "play_next", - action: "next", - icon: "queue_play_next" - }, - { - label: "add_queue", - action: "add", - icon: "playlist_add" - } - ], - showTrackInfoItem: { - label: "show_info", - action: "info", - icon: "info" - }, - addToPlaylistItem: { - label: "add_playlist", - action: "add_playlist", - icon: "add_circle_outline" - }, - removeFromPlaylistItem: { - label: "remove_playlist", - action: "remove_playlist", - icon: "remove_circle_outline" - }, - playerQueueItems: [ - ], - playlists: [], - show_playlists: false - } - }, - mounted() { }, - created() { }, - computed: { - menuItems() { - if (!this.$globals.contextmenuitem) - return []; - else if (this.show_playlists) - return this.playlists; - else if (this.$globals.contextmenucontext == 'playerqueue') - return this.playerQueueItems; // TODO: return queue contextmenu - else if (this.$globals.contextmenucontext == 'trackdetails') { - // track details - var items = []; - items.push(...this.mediaPlayItems); - items.push(this.addToPlaylistItem); - return items; - } - else if (this.$globals.contextmenuitem.media_type == 3) { - // track item in list - var items = []; - items.push(...this.mediaPlayItems); - items.push(this.showTrackInfoItem); - items.push(this.addToPlaylistItem); - if (this.$globals.contextmenucontext.is_editable) - items.push(this.removeFromPlaylistItem); - return items; - } - else { - // all other playable media - return this.mediaPlayItems; - } - }, - header() { - return !!this.$globals.contextmenuitem ? this.$globals.contextmenuitem.name : ''; - }, - subheader() { - if (!!this.active_player) - return this.$t('play_on') + this.active_player.name; - else - return ""; - } - }, - methods: { - itemCommand(cmd) { - console.log('itemCommand: ' + cmd); - if (cmd == 'info') { - // show track info - this.$router.push({ path: '/tracks/' + this.$globals.contextmenuitem.item_id, query: {provider: this.$globals.contextmenuitem.provider}}) - this.$globals.showcontextmenu = false; - } - else if (cmd == 'add_playlist') { - // add to playlist - console.log(`add ${this.$globals.contextmenuitem.name} to playlist?`); - this.getPlaylists(); - this.show_playlists = true; - } - else if (cmd == 'remove_playlist') { - // remove track from playlist - this.playlistAddRemove(this.$globals.contextmenuitem, this.$globals.contextmenucontext.item_id, 'playlist_remove'); - this.$globals.showcontextmenu = false; - } - else { - // assume play command - this.$emit('playItem', this.$globals.contextmenuitem, cmd) - this.$globals.showcontextmenu = false; - } - - }, - playlistAddRemove(track, playlist_id, action='playlist_add') { - /// add or remove track on playlist - var url = `${this.$globals.server}api/track/${track.item_id}`; - console.log('loading ' + url); - axios - .get(url, { params: { - provider: track.provider, - action: action, - action_details: playlist_id - }}) - .then(result => { - console.log(result); - // reload playlist - if (action == 'playlist_remove') - this.$router.go() - }) - .catch(error => { - console.log("error", error); - }); - }, - getPlaylists() { - // get all editable playlists - const api_url = this.$globals.apiAddress + 'playlists'; - axios - .get(api_url, { }) - .then(result => { - let items = [] - for (var item of result.data) { - if (item.item_id != this.$globals.contextmenucontext.item_id) - if (item.is_editable) - items.push(item); - } - console.log(items); - this.playlists = items; - }) - .catch(error => { - console.log("error", error); - this.playlists = []; - }); - } - } - }) diff --git a/music_assistant/web/components/contextmenu.vue.js b/music_assistant/web/components/contextmenu.vue.js index 554987b2..0cad9c70 100644 --- a/music_assistant/web/components/contextmenu.vue.js +++ b/music_assistant/web/components/contextmenu.vue.js @@ -1,13 +1,48 @@ Vue.component("contextmenu", { template: ` - + + + + {{ header }} + {{ subheader }} + +
+ + + {{item.icon}} + + + {{ $t(item.label) }} + + + +
+ + +
+
+
`, - props: ['active_player'], + props: ['value', 'active_player'], + watch: { + value: function (val) { + if (!val) + this.show_playlists = false; + } + }, data () { return { mediaPlayItems: [ @@ -43,7 +78,9 @@ Vue.component("contextmenu", { icon: "remove_circle_outline" }, playerQueueItems: [ - ] + ], + playlists: [], + show_playlists: false } }, mounted() { }, @@ -52,6 +89,8 @@ Vue.component("contextmenu", { menuItems() { if (!this.$globals.contextmenuitem) return []; + else if (this.show_playlists) + return this.playlists; else if (this.$globals.contextmenucontext == 'playerqueue') return this.playerQueueItems; // TODO: return queue contextmenu else if (this.$globals.contextmenucontext == 'trackdetails') { @@ -77,18 +116,24 @@ Vue.component("contextmenu", { } }, header() { - return !!this.$globals.contextmenuitem ? this.$globals.contextmenuitem.name : ''; + if (this.show_playlists) + return this.$t('add_playlist'); + else if (!this.$globals.contextmenuitem) + return ""; + else + return this.$globals.contextmenuitem.name; }, subheader() { - if (!!this.active_player) - return this.$t('play_on') + this.active_player.name; - else + if (this.show_playlists && !!this.$globals.contextmenuitem) + return this.$globals.contextmenuitem.name; + else if (!this.active_player) return ""; + else + return this.$t('play_on') + this.active_player.name; } }, methods: { itemCommand(cmd) { - console.log('itemCommand: ' + cmd); if (cmd == 'info') { // show track info this.$router.push({ path: '/tracks/' + this.$globals.contextmenuitem.item_id, query: {provider: this.$globals.contextmenuitem.provider}}) @@ -96,7 +141,6 @@ Vue.component("contextmenu", { } else if (cmd == 'add_playlist') { // add to playlist - console.log(`add ${this.$globals.contextmenuitem.name} to playlist?`); this.getPlaylists(); this.show_playlists = true; } @@ -112,10 +156,13 @@ Vue.component("contextmenu", { } }, + playlistSelected(playlistobj) { + this.playlistAddRemove(this.$globals.contextmenuitem, playlistobj.item_id, 'playlist_add'); + this.$globals.showcontextmenu = false; + }, playlistAddRemove(track, playlist_id, action='playlist_add') { /// add or remove track on playlist var url = `${this.$globals.server}api/track/${track.item_id}`; - console.log('loading ' + url); axios .get(url, { params: { provider: track.provider, @@ -123,8 +170,7 @@ Vue.component("contextmenu", { action_details: playlist_id }}) .then(result => { - console.log(result); - // reload playlist + // reload listing if (action == 'playlist_remove') this.$router.go() }) @@ -135,16 +181,22 @@ Vue.component("contextmenu", { getPlaylists() { // get all editable playlists const api_url = this.$globals.apiAddress + 'playlists'; + let track_provs = []; + for (var prov of this.$globals.contextmenuitem.provider_ids) + track_provs.push(prov.provider); axios .get(api_url, { }) .then(result => { let items = [] - for (var item of result.data) { - if (item.item_id != this.$globals.contextmenucontext.item_id) - if (item.is_editable) - items.push(item); + for (var playlist of result.data) { + if (playlist.is_editable && playlist.item_id != this.$globals.contextmenucontext.item_id) + for (var prov of playlist.provider_ids) + if (track_provs.includes(prov.provider)) + { + items.push(playlist); + break + } } - console.log(items); this.playlists = items; }) .catch(error => { diff --git a/music_assistant/web/components/listdialog.vue.js b/music_assistant/web/components/listdialog.vue.js deleted file mode 100644 index 6df30bbd..00000000 --- a/music_assistant/web/components/listdialog.vue.js +++ /dev/null @@ -1,31 +0,0 @@ -Vue.component("listdialog", { - template: ` - - - - {{ header }} - {{ subheader }} -
- - - {{item.icon}} - - - {{ $t(item.label) }} - - - -
-
-
-
-`, - props: ['value', 'items', 'header', 'subheader'], - data () { - return {} - }, - mounted() { }, - created() { }, - computed: { }, - methods: { } - }) diff --git a/music_assistant/web/components/listviewItem.vue.js b/music_assistant/web/components/listviewItem.vue.js index b29a3260..79a847b7 100755 --- a/music_assistant/web/components/listviewItem.vue.js +++ b/music_assistant/web/components/listviewItem.vue.js @@ -4,8 +4,8 @@ Vue.component("listviewItem", { - + @click="onClick ? onClick(item, context) : clickItem(item, context)" + > @@ -22,7 +22,7 @@ Vue.component("listviewItem", { - + {{ artist.name }} @@ -61,12 +61,11 @@ Vue.component("listviewItem", { more_vert - `, -props: ['item', 'context', 'index', 'totalitems', 'hideavatar', 'hidetracknum', 'hideproviders', 'hidemenu', 'hidelibrary', 'hideduration'], +props: ['item', 'context', 'index', 'totalitems', 'hideavatar', 'hidetracknum', 'hideproviders', 'hidemenu', 'hidelibrary', 'hideduration', 'onClick'], data() { return {} }, diff --git a/music_assistant/web/components/player.vue.js b/music_assistant/web/components/player.vue.js index 92f03099..278a544f 100755 --- a/music_assistant/web/components/player.vue.js +++ b/music_assistant/web/components/player.vue.js @@ -135,7 +135,7 @@ Vue.component("player", { - + `, @@ -233,7 +233,6 @@ Vue.component("player", { this.ws.send(JSON.stringify({message:'player command', message_details: msg_details})); }, playItem(item, queueopt) { - console.log('playItem: ' + item); this.$globals.loading = true; var api_url = 'api/players/' + this.active_player_id + '/play_media/' + item.media_type + '/' + item.item_id + '/' + queueopt; axios @@ -243,11 +242,9 @@ Vue.component("player", { } }) .then(result => { - console.log(result.data); this.$globals.loading = false; }) .catch(error => { - console.log("error", error); this.$globals.loading = false; }); }, diff --git a/music_assistant/web/index.html b/music_assistant/web/index.html index ac1fae03..ad04ae46 100755 --- a/music_assistant/web/index.html +++ b/music_assistant/web/index.html @@ -53,9 +53,7 @@ - - diff --git a/music_assistant/web/lib/utils.js b/music_assistant/web/lib/utils.js index 9f08215d..de849155 100644 --- a/music_assistant/web/lib/utils.js +++ b/music_assistant/web/lib/utils.js @@ -3,7 +3,7 @@ const isInStandaloneMode = () => ('standalone' in window.navigator) && (window.n function showPlayMenu (item, context=null) { /// make the contextmenu visible - console.log(context); + console.log("showPlayMenu"); this.$globals.contextmenuitem = item; this.$globals.contextmenucontext = context; this.$globals.showcontextmenu = !this.$globals.showcontextmenu; diff --git a/music_assistant/web/pages/browse.vue.js b/music_assistant/web/pages/browse.vue.js index c9ba1141..058e13fd 100755 --- a/music_assistant/web/pages/browse.vue.js +++ b/music_assistant/web/pages/browse.vue.js @@ -4,14 +4,15 @@ var Browse = Vue.component('Browse', { @@ -23,26 +24,36 @@ var Browse = Vue.component('Browse', { return { selected: [2], items: [], - offset: 0 + offset: 0, + full_list_loaded: false } }, created() { - this.showavatar = true; - mediatitle = this.$globals.windowtitle = this.$t(this.mediatype) this.scroll(this.Browse); - this.getItems(); + if (!this.full_list_loaded) + this.getItems(); }, methods: { getItems () { + if (this.full_list_loaded) + return; this.$globals.loading = true const api_url = this.$globals.apiAddress + this.mediatype; + const limit = 20; axios - .get(api_url, { params: { offset: this.offset, limit: 50, provider: this.provider }}) + .get(api_url, { params: { offset: this.offset, limit: limit, provider: this.provider }}) .then(result => { data = result.data; + if (data.length < limit) + { + this.full_list_loaded = true; + this.$globals.loading = false; + if (data.length == 0) + return + } this.items.push(...data); - this.offset += 50; + this.offset += limit; this.$globals.loading = false; }) .catch(error => { @@ -53,8 +64,7 @@ var Browse = Vue.component('Browse', { scroll (Browse) { window.onscroll = () => { let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight === document.documentElement.offsetHeight; - - if (bottomOfWindow) { + if (bottomOfWindow && !this.full_list_loaded) { this.getItems(); } }; diff --git a/music_assistant/web/pages/playlistdetails.vue.js b/music_assistant/web/pages/playlistdetails.vue.js index 0414065b..0cb02bec 100755 --- a/music_assistant/web/pages/playlistdetails.vue.js +++ b/music_assistant/web/pages/playlistdetails.vue.js @@ -15,11 +15,12 @@ var PlaylistDetails = Vue.component('PlaylistDetails', { @@ -35,7 +36,8 @@ var PlaylistDetails = Vue.component('PlaylistDetails', { info: {}, items: [], offset: 0, - active: 0 + active: 0, + full_list_loaded: false } }, created() { @@ -45,7 +47,7 @@ var PlaylistDetails = Vue.component('PlaylistDetails', { this.scroll(this.Browse); }, methods: { - getInfo () { + async getInfo () { const api_url = this.$globals.apiAddress + 'playlists/' + this.media_id axios .get(api_url, { params: { provider: this.provider }}) @@ -58,26 +60,36 @@ var PlaylistDetails = Vue.component('PlaylistDetails', { console.log("error", error); }); }, - getPlaylistTracks () { - this.$globals.loading = true + async getPlaylistTracks () { + if (this.full_list_loaded) + return; + this.$globals.loading = true; const api_url = this.$globals.apiAddress + 'playlists/' + this.media_id + '/tracks' + let limit = 20; axios - .get(api_url, { params: { offset: this.offset, limit: 25, provider: this.provider}}) + .get(api_url, { params: { offset: this.offset, limit: limit, provider: this.provider}}) .then(result => { + this.$globals.loading = false; data = result.data; + if (data.length < limit) + { + this.full_list_loaded = true; + this.$globals.loading = false; + if (data.length == 0) + return + } this.items.push(...data); - this.offset += 25; - this.$globals.loading = false; + this.offset += limit; + }) .catch(error => { console.log("error", error); }); - }, scroll (Browse) { window.onscroll = () => { let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight === document.documentElement.offsetHeight; - if (bottomOfWindow) { + if (bottomOfWindow && !this.full_list_loaded) { this.getPlaylistTracks(); } };