await db.execute(sql_query, (item_id,media_type, provider))
await db.commit()
- async def artists(self, filter_query=None, limit=100000, offset=0, orderby='name', fulldata=False, db=None) -> List[Artist]:
+ async def artists(self, filter_query=None, limit=100000, offset=0, orderby='name', fulldata=True, db=None) -> List[Artist]:
''' fetch artist records from table'''
artists = []
sql_query = 'SELECT * FROM artists'
artist.sort_name = db_row[2]
artist.provider_ids = await self.__get_prov_ids(artist.item_id, MediaType.Artist, db)
artist.in_library = await self.__get_library_providers(artist.item_id, MediaType.Artist, db)
- artist.external_ids = await self.__get_external_ids(artist.item_id, MediaType.Artist, db)
if fulldata:
+ artist.external_ids = await self.__get_external_ids(artist.item_id, MediaType.Artist, db)
artist.metadata = await self.__get_metadata(artist.item_id, MediaType.Artist, db)
artist.tags = await self.__get_tags(artist.item_id, MediaType.Artist, db)
- else:
artist.metadata = await self.__get_metadata(artist.item_id, MediaType.Artist, db, filter_key='image')
artists.append(artist)
if should_close_db:
LOGGER.debug('added artist %s (%s) to database: %s' %(artist.name, artist.provider_ids, artist_id))
return artist_id
- async def albums(self, filter_query=None, limit=100000, offset=0, orderby='name', fulldata=False, db=None) -> List[Album]:
+ async def albums(self, filter_query=None, limit=100000, offset=0, orderby='name', fulldata=True, db=None) -> List[Album]:
''' fetch all album records from table'''
albums = []
sql_query = 'SELECT * FROM albums'
for db_row in db_rows:
album = Album()
album.item_id = db_row[0]
- album.artist = await self.artist(db_row[1], fulldata=fulldata)
album.name = db_row[2]
album.albumtype = db_row[3]
album.year = db_row[4]
album.version = db_row[5]
album.provider_ids = await self.__get_prov_ids(album.item_id, MediaType.Album, db)
album.in_library = await self.__get_library_providers(album.item_id, MediaType.Album, db)
- album.external_ids = await self.__get_external_ids(album.item_id, MediaType.Album, db)
if fulldata:
+ album.artist = await self.artist(db_row[1], fulldata=False)
+ album.external_ids = await self.__get_external_ids(album.item_id, MediaType.Album, db)
album.metadata = await self.__get_metadata(album.item_id, MediaType.Album, db)
album.tags = await self.__get_tags(album.item_id, MediaType.Album, db)
album.labels = await self.__get_album_labels(album.item_id, db)
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=False, db=None) -> List[Track]:
+ async def tracks(self, filter_query=None, limit=100000, offset=0, orderby='name', fulldata=True, db=None) -> List[Track]:
''' fetch all track records from table'''
tracks = []
sql_query = 'SELECT * FROM tracks'
track = Track()
track.item_id = db_row[0]
track.name = db_row[1]
- track.album = await self.album(db_row[2], fulldata=fulldata, db=db)
+ 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.metadata = await self.__get_metadata(track.item_id, MediaType.Track, db)
- track.tags = await self.__get_tags(track.item_id, MediaType.Track, db)
- track.provider_ids = await self.__get_prov_ids(track.item_id, MediaType.Track, db)
track.in_library = await self.__get_library_providers(track.item_id, MediaType.Track, db)
- track.artists = await self.__get_track_artists(track.item_id, db, fulldata=fulldata)
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()
''' 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
- return await self.tracks(sql_query, limit=limit, offset=offset, orderby=orderby)
+ 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]:
''' get all library albums for the given artist '''
sql_query = ' WHERE artist_id = %d' % artist_id
- return await self.albums(sql_query, limit=limit, offset=offset, orderby=orderby)
+ return await self.albums(sql_query, limit=limit, offset=offset, orderby=orderby, fulldata=False)
- async def playlist_tracks(self, playlist_id:int, limit=100000, offset=0, orderby='position', fulldata=False) -> List[Track]:
+ 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 = []
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)
+ 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)
async def add_playlist_track(self, playlist_id:int, track_id, position):
''' add playlist track to playlist '''
async with aiosqlite.connect(self.dbfile, timeout=20) as db:
- sql_query = 'INSERT or IGNORE INTO playlist_tracks (playlist_id, track_id, position) VALUES(?,?,?);'
+ sql_query = 'INSERT or REPLACE INTO playlist_tracks (playlist_id, track_id, position) VALUES(?,?,?);'
await db.execute(sql_query, (playlist_id, track_id, position))
await db.commit()
async def __get_track_artists(self, track_id, db, fulldata=False) -> List[Artist]:
''' get artists for track '''
sql_query = 'WHERE artist_id in (SELECT artist_id FROM track_artists WHERE track_id = %s)' % track_id
- return await self.artists(sql_query, db=db)
+ return await self.artists(sql_query, db=db, fulldata=fulldata)
async def __add_external_ids(self, item_id, media_type, external_ids, db):
''' add or update external_ids'''
''' perform action on item (such as library add/remove) '''
result = None
item = await self.item(item_id, media_type, provider, lazy=False)
- if item and action in ['library_add', 'library_remove']:
+ if not item:
+ return False
+ if action in ['library_add', 'library_remove']:
# remove or add item to the library
for prov_mapping in item.provider_ids:
prov_id = prov_mapping['provider']
elif action == 'library_remove':
result = await prov.remove_library(prov_item_id, media_type)
await self.mass.db.remove_from_library(item.item_id, item.media_type, prov_id)
- elif action == 'add_to_playlist':
- result = await self.add_playlist_tracks(action_details, [item])
+ elif action == 'playlist_add':
+ result = await self.add_playlist_tracks(action_details, [item])
+ elif action == 'playlist_remove':
+ result = await self.remove_playlist_tracks(action_details, [item])
return result
async def add_playlist_tracks(self, playlist_id, tracks:List[Track]):
# grab all (database) track ids in the playlist so we can check for duplicates
cur_playlist_track_ids = [item.item_id for item in cur_playlist_tracks]
track_ids_to_add = []
- for track in tracks:
+ for index, track in enumerate(tracks):
if not track.provider == 'database':
# make sure we have a database track
track = await self.track(track.item_id, track.provider, lazy=False)
else:
LOGGER.warning("Track %s not available on provider %s - skip addition to playlist %s" %(track.name, playlist_prov['provider'], playlist.name))
continue
+ # add track to db playlist
+ 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
- await self.providers[playlist_prov['provider']].add_playlist_tracks(playlist_prov['item_id'], track_ids_to_add)
- # schedule sync
- self.mass.event_loop.create_task(self.sync_playlist_tracks(playlist.item_id, playlist_prov['provider'], playlist_prov['item_id']))
+ 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 '''
+ # we can only edit playlists that are in the database (marked as editable)
+ playlist = await self.playlist(playlist_id, 'database')
+ if not playlist or not playlist.is_editable:
+ LOGGER.warning("Playlist %s is not editable - skip removal of tracks" %(playlist.name))
+ return False
+ prov_playlist = playlist.provider_ids[0] # playlist can only have one provider (for now)
+ prov_playlist_playlist_id = prov_playlist['item_id']
+ prov_playlist_provider_id = prov_playlist['provider']
+ track_ids_to_remove = []
+ for track in tracks:
+ if not track.provider == 'database':
+ # make sure we have a database track
+ track = await self.track(track.item_id, track.provider, lazy=False)
+ # a track can contain multiple versions on the same provider, remove all
+ for track_provider in track.provider_ids:
+ if track_provider['provider'] == prov_playlist_provider_id:
+ track_ids_to_remove.append(track_provider['item_id'])
+ # 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)
@run_periodic(3600)
async def sync_music_providers(self):
app.add_routes([web.get('/api/artists/{artist_id}/toptracks', self.artist_toptracks)])
app.add_routes([web.get('/api/artists/{artist_id}/albums', self.artist_albums)])
app.add_routes([web.get('/api/albums/{album_id}/tracks', self.album_tracks)])
- app.add_routes([web.get('/api/{media_type}/{media_id}/{action}', self.get_item)])
app.add_routes([web.get('/api/{media_type}/{media_id}', self.get_item)])
app.add_routes([web.get('/api/{media_type}', self.get_items)])
app.add_routes([web.get('/', self.index)])
media_type_str = request.match_info.get('media_type')
media_type = media_type_from_string(media_type_str)
media_id = request.match_info.get('media_id')
- action = request.match_info.get('action','')
+ # optional params
+ action = request.rel_url.query.get('action','')
action_details = request.rel_url.query.get('action_details')
lazy = request.rel_url.query.get('lazy', '') != 'false'
provider = request.rel_url.query.get('provider')
},
]
-let router = new VueRouter({
- //mode: 'history',
- routes // short for `routes: routes`
-})
-
-router.beforeEach((to, from, next) => {
- next()
-})
-
const globalStore = new Vue({
data: {
windowtitle: 'Home',
loading: false,
- showplaymenu: false,
- showsearchbox: false,
- playmenuitem: null,
+ showcontextmenu: false,
+ contextmenuitem: null,
+ contextmenucontext: null,
server: null,
apiAddress: null,
wsAddress: null
}
})
+
Vue.prototype.$globals = globalStore;
Vue.prototype.isMobile = isMobile;
Vue.prototype.isInStandaloneMode = isInStandaloneMode;
Vue.prototype.showPlayMenu = showPlayMenu;
Vue.prototype.clickItem= clickItem;
+let router = new VueRouter({
+ //mode: 'history',
+ routes // short for `routes: routes`
+})
+
+router.beforeEach((to, from, next) => {
+ next()
+})
+
const i18n = new VueI18n({
locale: navigator.language.split('-')[0],
fallbackLocale: 'en',
},
data: { },
methods: {},
- router
+ router,
+ template: `
+ <v-app light>
+ <v-content>
+ <headermenu></headermenu>
+ <player></player>
+ <router-view app :key="$route.path"></router-view>
+ </v-content>
+ <loading :active.sync="$globals.loading" :can-cancel="true" color="#2196f3" loader="dots"></loading>
+ </v-app>
+ `
})
\ No newline at end of file
--- /dev/null
+Vue.component("addtoplaylistdialog", {\r
+ template: `\r
+ <v-dialog :value="value" @input="$emit('input', $event)" max-width="500px">\r
+ <v-card>\r
+ <v-list>\r
+ <v-subheader class="title">{{ header }}</v-subheader>\r
+ <v-subheader>{{ subheader }}</v-subheader>\r
+ <div v-for="(item, index) in menuItems">\r
+ <v-list-tile avatar @click="itemCommand(item.action)">\r
+ <v-list-tile-avatar>\r
+ <v-icon>{{item.icon}}</v-icon>\r
+ </v-list-tile-avatar>\r
+ <v-list-tile-content>\r
+ <v-list-tile-title>{{ $t(item.label) }}</v-list-tile-title>\r
+ </v-list-tile-content>\r
+ </v-list-tile>\r
+ <v-divider></v-divider>\r
+ </div>\r
+ </v-list>\r
+ </v-card>\r
+ </v-dialog>\r
+`,\r
+ props: ['value', 'active_player'],\r
+ data () { \r
+ return {\r
+ mediaPlayItems: [\r
+ {\r
+ label: "play_now",\r
+ action: "play",\r
+ icon: "play_circle_outline"\r
+ },\r
+ {\r
+ label: "play_next",\r
+ action: "next",\r
+ icon: "queue_play_next"\r
+ },\r
+ {\r
+ label: "add_queue",\r
+ action: "add",\r
+ icon: "playlist_add"\r
+ }\r
+ ],\r
+ showTrackInfoItem: {\r
+ label: "show_info",\r
+ action: "info",\r
+ icon: "info"\r
+ },\r
+ addToPlaylistItem: {\r
+ label: "add_playlist",\r
+ action: "add_playlist",\r
+ icon: "add_circle_outline"\r
+ },\r
+ removeFromPlaylistItem: {\r
+ label: "remove_playlist",\r
+ action: "remove_playlist",\r
+ icon: "remove_circle_outline"\r
+ },\r
+ playerQueueItems: [\r
+ ],\r
+ playlists: [],\r
+ show_playlists: false\r
+ }\r
+ },\r
+ mounted() { },\r
+ created() { },\r
+ computed: {\r
+ menuItems() {\r
+ if (!this.$globals.contextmenuitem)\r
+ return [];\r
+ else if (this.show_playlists)\r
+ return this.playlists;\r
+ else if (this.$globals.contextmenucontext == 'playerqueue')\r
+ return this.playerQueueItems; // TODO: return queue contextmenu\r
+ else if (this.$globals.contextmenucontext == 'trackdetails') {\r
+ // track details\r
+ var items = [];\r
+ items.push(...this.mediaPlayItems);\r
+ items.push(this.addToPlaylistItem);\r
+ return items;\r
+ }\r
+ else if (this.$globals.contextmenuitem.media_type == 3) {\r
+ // track item in list\r
+ var items = [];\r
+ items.push(...this.mediaPlayItems);\r
+ items.push(this.showTrackInfoItem);\r
+ items.push(this.addToPlaylistItem);\r
+ if (this.$globals.contextmenucontext.is_editable)\r
+ items.push(this.removeFromPlaylistItem);\r
+ return items;\r
+ }\r
+ else {\r
+ // all other playable media\r
+ return this.mediaPlayItems;\r
+ }\r
+ },\r
+ header() {\r
+ return !!this.$globals.contextmenuitem ? this.$globals.contextmenuitem.name : '';\r
+ },\r
+ subheader() {\r
+ if (!!this.active_player)\r
+ return this.$t('play_on') + this.active_player.name;\r
+ else\r
+ return "";\r
+ }\r
+ },\r
+ methods: { \r
+ itemCommand(cmd) {\r
+ console.log('itemCommand: ' + cmd);\r
+ if (cmd == 'info') {\r
+ // show track info\r
+ this.$router.push({ path: '/tracks/' + this.$globals.contextmenuitem.item_id, query: {provider: this.$globals.contextmenuitem.provider}})\r
+ this.$globals.showcontextmenu = false;\r
+ } \r
+ else if (cmd == 'add_playlist') {\r
+ // add to playlist\r
+ console.log(`add ${this.$globals.contextmenuitem.name} to playlist?`);\r
+ this.getPlaylists();\r
+ this.show_playlists = true;\r
+ }\r
+ else if (cmd == 'remove_playlist') {\r
+ // remove track from playlist\r
+ this.playlistAddRemove(this.$globals.contextmenuitem, this.$globals.contextmenucontext.item_id, 'playlist_remove');\r
+ this.$globals.showcontextmenu = false;\r
+ }\r
+ else {\r
+ // assume play command\r
+ this.$emit('playItem', this.$globals.contextmenuitem, cmd)\r
+ this.$globals.showcontextmenu = false;\r
+ }\r
+ \r
+ },\r
+ playlistAddRemove(track, playlist_id, action='playlist_add') {\r
+ /// add or remove track on playlist\r
+ var url = `${this.$globals.server}api/track/${track.item_id}`;\r
+ console.log('loading ' + url);\r
+ axios\r
+ .get(url, { params: { \r
+ provider: track.provider, \r
+ action: action, \r
+ action_details: playlist_id\r
+ }})\r
+ .then(result => {\r
+ console.log(result);\r
+ // reload playlist\r
+ if (action == 'playlist_remove')\r
+ this.$router.go()\r
+ })\r
+ .catch(error => {\r
+ console.log("error", error);\r
+ });\r
+ },\r
+ getPlaylists() {\r
+ // get all editable playlists\r
+ const api_url = this.$globals.apiAddress + 'playlists';\r
+ axios\r
+ .get(api_url, { })\r
+ .then(result => {\r
+ let items = []\r
+ for (var item of result.data) {\r
+ if (item.item_id != this.$globals.contextmenucontext.item_id)\r
+ if (item.is_editable)\r
+ items.push(item);\r
+ }\r
+ console.log(items);\r
+ this.playlists = items;\r
+ })\r
+ .catch(error => {\r
+ console.log("error", error);\r
+ this.playlists = [];\r
+ });\r
+ }\r
+ }\r
+ })\r
--- /dev/null
+Vue.component("contextmenu", {\r
+ template: `\r
+ <listdialog v-model="$globals.showcontextmenu" \r
+ v-on:onClick="itemCommand" \r
+ :items=menuItems \r
+ :header="header" \r
+ :subheader="subheader" \r
+ </listdialog>\r
+`,\r
+ props: ['active_player'],\r
+ data () { \r
+ return {\r
+ mediaPlayItems: [\r
+ {\r
+ label: "play_now",\r
+ action: "play",\r
+ icon: "play_circle_outline"\r
+ },\r
+ {\r
+ label: "play_next",\r
+ action: "next",\r
+ icon: "queue_play_next"\r
+ },\r
+ {\r
+ label: "add_queue",\r
+ action: "add",\r
+ icon: "playlist_add"\r
+ }\r
+ ],\r
+ showTrackInfoItem: {\r
+ label: "show_info",\r
+ action: "info",\r
+ icon: "info"\r
+ },\r
+ addToPlaylistItem: {\r
+ label: "add_playlist",\r
+ action: "add_playlist",\r
+ icon: "add_circle_outline"\r
+ },\r
+ removeFromPlaylistItem: {\r
+ label: "remove_playlist",\r
+ action: "remove_playlist",\r
+ icon: "remove_circle_outline"\r
+ },\r
+ playerQueueItems: [\r
+ ]\r
+ }\r
+ },\r
+ mounted() { },\r
+ created() { },\r
+ computed: {\r
+ menuItems() {\r
+ if (!this.$globals.contextmenuitem)\r
+ return [];\r
+ else if (this.$globals.contextmenucontext == 'playerqueue')\r
+ return this.playerQueueItems; // TODO: return queue contextmenu\r
+ else if (this.$globals.contextmenucontext == 'trackdetails') {\r
+ // track details\r
+ var items = [];\r
+ items.push(...this.mediaPlayItems);\r
+ items.push(this.addToPlaylistItem);\r
+ return items;\r
+ }\r
+ else if (this.$globals.contextmenuitem.media_type == 3) {\r
+ // track item in list\r
+ var items = [];\r
+ items.push(...this.mediaPlayItems);\r
+ items.push(this.showTrackInfoItem);\r
+ items.push(this.addToPlaylistItem);\r
+ if (this.$globals.contextmenucontext.is_editable)\r
+ items.push(this.removeFromPlaylistItem);\r
+ return items;\r
+ }\r
+ else {\r
+ // all other playable media\r
+ return this.mediaPlayItems;\r
+ }\r
+ },\r
+ header() {\r
+ return !!this.$globals.contextmenuitem ? this.$globals.contextmenuitem.name : '';\r
+ },\r
+ subheader() {\r
+ if (!!this.active_player)\r
+ return this.$t('play_on') + this.active_player.name;\r
+ else\r
+ return "";\r
+ }\r
+ },\r
+ methods: { \r
+ itemCommand(cmd) {\r
+ console.log('itemCommand: ' + cmd);\r
+ if (cmd == 'info') {\r
+ // show track info\r
+ this.$router.push({ path: '/tracks/' + this.$globals.contextmenuitem.item_id, query: {provider: this.$globals.contextmenuitem.provider}})\r
+ this.$globals.showcontextmenu = false;\r
+ } \r
+ else if (cmd == 'add_playlist') {\r
+ // add to playlist\r
+ console.log(`add ${this.$globals.contextmenuitem.name} to playlist?`);\r
+ this.getPlaylists();\r
+ this.show_playlists = true;\r
+ }\r
+ else if (cmd == 'remove_playlist') {\r
+ // remove track from playlist\r
+ this.playlistAddRemove(this.$globals.contextmenuitem, this.$globals.contextmenucontext.item_id, 'playlist_remove');\r
+ this.$globals.showcontextmenu = false;\r
+ }\r
+ else {\r
+ // assume play command\r
+ this.$emit('playItem', this.$globals.contextmenuitem, cmd)\r
+ this.$globals.showcontextmenu = false;\r
+ }\r
+ \r
+ },\r
+ playlistAddRemove(track, playlist_id, action='playlist_add') {\r
+ /// add or remove track on playlist\r
+ var url = `${this.$globals.server}api/track/${track.item_id}`;\r
+ console.log('loading ' + url);\r
+ axios\r
+ .get(url, { params: { \r
+ provider: track.provider, \r
+ action: action, \r
+ action_details: playlist_id\r
+ }})\r
+ .then(result => {\r
+ console.log(result);\r
+ // reload playlist\r
+ if (action == 'playlist_remove')\r
+ this.$router.go()\r
+ })\r
+ .catch(error => {\r
+ console.log("error", error);\r
+ });\r
+ },\r
+ getPlaylists() {\r
+ // get all editable playlists\r
+ const api_url = this.$globals.apiAddress + 'playlists';\r
+ axios\r
+ .get(api_url, { })\r
+ .then(result => {\r
+ let items = []\r
+ for (var item of result.data) {\r
+ if (item.item_id != this.$globals.contextmenucontext.item_id)\r
+ if (item.is_editable)\r
+ items.push(item);\r
+ }\r
+ console.log(items);\r
+ this.playlists = items;\r
+ })\r
+ .catch(error => {\r
+ console.log("error", error);\r
+ this.playlists = [];\r
+ });\r
+ }\r
+ }\r
+ })\r
\r
<!-- left side: cover image -->\r
<v-flex xs5 pa-4 v-if="!isMobile()">\r
- <v-img :src="getThumb()" lazy-src="./images/default_artist.png" width="250px" height="250px" style="border: 4px solid grey;border-radius: 15px;"></v-img>\r
- \r
- <!-- tech specs and provider icons -->\r
- <div style="margin-top:10px;">\r
- <providericons v-bind:item="info" :height="30" :compact="false"/>\r
- </div>\r
+ <v-img :src="getThumb()" lazy-src="./images/default_artist.png" width="250px" height="250px" style="border: 4px solid grey;border-radius: 15px;"></v-img>\r
+ \r
+ <!-- tech specs and provider icons -->\r
+ <div style="margin-top:10px;">\r
+ <providericons v-bind:item="info" :height="30" :compact="false"/>\r
+ </div>\r
</v-flex>\r
\r
<v-flex>\r
\r
<!-- play/info buttons -->\r
<div style="margin-left:8px;">\r
- <v-btn color="blue-grey" @click="showPlayMenu(info)" class="white--text"><v-icon v-if="!isMobile()" left dark>play_circle_outline</v-icon>{{ $t('play') }}</v-btn>\r
+ <v-btn color="blue-grey" @click="showPlayMenu(info, context)" class="white--text"><v-icon v-if="!isMobile()" left dark>play_circle_outline</v-icon>{{ $t('play') }}</v-btn>\r
<v-btn v-if="!!info.in_library && info.in_library.length == 0" color="blue-grey" @click="toggleLibrary(info)" class="white--text"><v-icon v-if="!isMobile()" left dark>favorite_border</v-icon>{{ $t('add_library') }}</v-btn>\r
<v-btn v-if="!!info.in_library && info.in_library.length > 0" color="blue-grey" @click="toggleLibrary(info)" class="white--text"><v-icon v-if="!isMobile()" left dark>favorite</v-icon>{{ $t('remove_library') }}</v-btn>\r
</div>\r
</v-card>\r
</v-flex>\r
`,\r
- props: ['info'],\r
+ props: ['info', 'context'],\r
data (){\r
return{}\r
},\r
--- /dev/null
+Vue.component("listdialog", {\r
+ template: `\r
+ <v-dialog :value="value" @input="$emit('input', $event)" max-width="500px">\r
+ <v-card>\r
+ <v-list>\r
+ <v-subheader class="title">{{ header }}</v-subheader>\r
+ <v-subheader>{{ subheader }}</v-subheader>\r
+ <div v-for="(item, index) in items">\r
+ <v-list-tile avatar @click="$emit('onClick', item)">\r
+ <v-list-tile-avatar>\r
+ <v-icon>{{item.icon}}</v-icon>\r
+ </v-list-tile-avatar>\r
+ <v-list-tile-content>\r
+ <v-list-tile-title>{{ $t(item.label) }}</v-list-tile-title>\r
+ </v-list-tile-content>\r
+ </v-list-tile>\r
+ <v-divider></v-divider>\r
+ </div>\r
+ </v-list>\r
+ </v-card>\r
+ </v-dialog>\r
+`,\r
+ props: ['value', 'items', 'header', 'subheader'],\r
+ data () { \r
+ return {}\r
+ },\r
+ mounted() { },\r
+ created() { },\r
+ computed: { },\r
+ methods: { }\r
+ })\r
<v-list-tile
avatar
ripple
- @click="clickItem(item)">
+ @click="clickItem(item, context)">
<v-list-tile-avatar color="grey" v-if="!hideavatar">
<img v-if="(item.media_type != 3) && item.metadata && item.metadata.image" :src="item.metadata.image"/>
</v-list-tile-action>
<!-- menu button/icon -->
- <v-icon v-if="!hidemenu" @click="showPlayMenu(item)" @click.stop="" color="grey lighten-1" style="margin-right:-10px;padding-left:10px">more_vert</v-icon>
+ <v-icon v-if="!hidemenu" @click="showPlayMenu(item, context)" @click.stop="" color="grey lighten-1" style="margin-right:-10px;padding-left:10px">more_vert</v-icon>
</v-list-tile>
<v-divider v-if="index + 1 < totalitems" :key="index"></v-divider>
</div>
`,
-props: ['item', 'index', 'totalitems', 'hideavatar', 'hidetracknum', 'hideproviders', 'hidemenu', 'hidelibrary', 'hideduration'],
+props: ['item', 'context', 'index', 'totalitems', 'hideavatar', 'hidetracknum', 'hideproviders', 'hidemenu', 'hidelibrary', 'hideduration'],
data() {
return {}
},
</div>
</v-list>
</v-navigation-drawer>
- <playmenu v-model="$globals.showplaymenu" v-on:playItem="playItem" :active_player="active_player" />
+ <contextmenu v-on:playItem="playItem" :active_player="active_player" />
</div>
`,
+++ /dev/null
-Vue.component("playmenu", {\r
- template: `\r
- <v-dialog :value="value" @input="$emit('input', $event)" max-width="500px" v-if="$globals.playmenuitem">\r
- <v-card>\r
- <v-list>\r
- <v-subheader class="title">{{ !!$globals.playmenuitem ? $globals.playmenuitem.name : '' }}</v-subheader>\r
- <v-subheader>{{ $t('play_on') }} {{ active_player.name }}</v-subheader>\r
- \r
- <v-list-tile avatar @click="itemClick('play')">\r
- <v-list-tile-avatar>\r
- <v-icon>play_circle_outline</v-icon>\r
- </v-list-tile-avatar>\r
- <v-list-tile-content>\r
- <v-list-tile-title>{{ $t('play_now') }}</v-list-tile-title>\r
- </v-list-tile-content>\r
- </v-list-tile>\r
- <v-divider></v-divider>\r
-\r
- <v-list-tile avatar @click="itemClick('next')">\r
- <v-list-tile-avatar>\r
- <v-icon>queue_play_next</v-icon>\r
- </v-list-tile-avatar>\r
- <v-list-tile-content>\r
- <v-list-tile-title>{{ $t('play_next') }}</v-list-tile-title>\r
- </v-list-tile-content>\r
- </v-list-tile>\r
- <v-divider></v-divider>\r
-\r
- <v-list-tile avatar @click="itemClick('add')">\r
- <v-list-tile-avatar>\r
- <v-icon>playlist_add</v-icon>\r
- </v-list-tile-avatar>\r
- <v-list-tile-content>\r
- <v-list-tile-title>{{ $t('add_queue') }}</v-list-tile-title>\r
- </v-list-tile-content>\r
- </v-list-tile>\r
- <v-divider></v-divider>\r
-\r
- <v-list-tile avatar @click="itemClick('info')" v-if="$globals.playmenuitem.media_type == 3">\r
- <v-list-tile-avatar>\r
- <v-icon>info</v-icon>\r
- </v-list-tile-avatar>\r
- <v-list-tile-content>\r
- <v-list-tile-title>{{ $t('show_info') }}</v-list-tile-title>\r
- </v-list-tile-content>\r
- </v-list-tile>\r
- <v-divider v-if="$globals.playmenuitem.media_type == 3"/>\r
-\r
- <v-list-tile avatar @click="itemClick('add_playlist')" v-if="$globals.playmenuitem.media_type == 3">\r
- <v-list-tile-avatar>\r
- <v-icon>add_circle_outline</v-icon>\r
- </v-list-tile-avatar>\r
- <v-list-tile-content>\r
- <v-list-tile-title>{{ $t('add_playlist') }}</v-list-tile-title>\r
- </v-list-tile-content>\r
- </v-list-tile>\r
- <v-divider v-if="$globals.playmenuitem.media_type == 3"/>\r
-\r
- <v-list-tile avatar @click="itemClick('remove_playlist')" v-if="$globals.playmenuitem.media_type == 3 && this.$route.path.startsWith('/playlists/')">\r
- <v-list-tile-avatar>\r
- <v-icon>remove_circle_outline</v-icon>\r
- </v-list-tile-avatar>\r
- <v-list-tile-content>\r
- <v-list-tile-title>{{ $t('remove_playlist') }}</v-list-tile-title>\r
- </v-list-tile-content>\r
- </v-list-tile>\r
- <v-divider v-if="$globals.playmenuitem.media_type == 3 && this.$route.path.startsWith('/playlists/')"/>\r
- \r
- </v-list>\r
- </v-card>\r
- </v-dialog>\r
-`,\r
- props: ['value', 'active_player'],\r
- data (){\r
- return{\r
- fav: true,\r
- message: false,\r
- hints: true,\r
- }\r
- },\r
- mounted() { },\r
- created() { },\r
- methods: { \r
- itemClick(cmd) {\r
- if (cmd == 'info')\r
- this.$router.push({ path: '/tracks/' + this.$globals.playmenuitem.item_id, query: {provider: this.$globals.playmenuitem.provider}})\r
- else\r
- this.$emit('playItem', this.$globals.playmenuitem, cmd)\r
- // close dialog\r
- this.$globals.showplaymenu = false;\r
- },\r
- }\r
- })\r
+++ /dev/null
-Vue.component("searchbox", {
- template: `
- <v-dialog :value="$globals.showsearchbox" @input="$emit('input', $event)" max-width="500px">
- <v-text-field
- solo
- clearable
- :label="$t('type_to_search')"
- prepend-inner-icon="search"
- v-model="searchQuery">
- </v-text-field>
- </v-dialog>
- `,
- data () {
- return {
- searchQuery: "",
- }
- },
- props: ['value'],
- mounted () {
- this.searchQuery = "" // TODO: set to last searchquery ?
- },
- watch: {
- searchQuery: {
- handler: _.debounce(function (val) {
- this.onSearch();
- // if (this.searchQuery)
- // this.$globals.showsearchbox = false;
- }, 1000)
- },
- newSearchQuery (val) {
- this.searchQuery = val
- }
- },
- computed: {},
- methods: {
- onSearch () {
- //this.$emit('clickSearch', this.searchQuery)
- console.log(this.searchQuery);
- router.push({ path: '/search', query: {searchQuery: this.searchQuery}});
- },
- }
-})
-/* <style>
-.searchbar {
- padding: 1rem 1.5rem!important;
- width: 100%;
- box-shadow: 0 0 70px 0 rgba(0, 0, 0, 0.3);
- background: #fff;
-}
-</style> */
\ No newline at end of file
<body>
<div id="app">
- <v-app light>
- <v-content>
- <headermenu></headermenu>
- <player></player>
- <router-view app :key="$route.path"></router-view>
- <searchbox/>
- </v-content>
- <loading :active.sync="$globals.loading" :can-cancel="true" color="#2196f3" loader="dots"></loading>
- </v-app>
+
</div>
<script src='./components/player.vue.js'></script>
<script src='./components/listviewItem.vue.js'></script>
<script src='./components/readmore.vue.js'></script>
- <script src='./components/playmenu.vue.js'></script>
+ <script src='./components/listdialog.vue.js'></script>
+ <script src='./components/contextmenu.vue.js'></script>
+ <script src='./components/addtoplaylistdialog.vue.js'></script>
<script src='./components/volumecontrol.vue.js'></script>
<script src='./components/infoheader.vue.js'></script>
<script src='./components/providericons.vue.js'></script>
- <script src='./components/searchbox.vue.js'></script>
<script src='./strings.js'></script>
<script src='./app.js'></script>
const isMobile = () => (document.body.clientWidth < 800);
const isInStandaloneMode = () => ('standalone' in window.navigator) && (window.navigator.standalone);
-function showPlayMenu (item) {
- this.$globals.playmenuitem = item;
- this.$globals.showplaymenu = !this.$globals.showplaymenu;
+function showPlayMenu (item, context=null) {
+ /// make the contextmenu visible
+ console.log(context);
+ this.$globals.contextmenuitem = item;
+ this.$globals.contextmenucontext = context;
+ this.$globals.showcontextmenu = !this.$globals.showcontextmenu;
}
-function clickItem (item) {
+function clickItem (item, context=null) {
+ /// triggered when user clicks on mediaitem
var endpoint = "";
if (item.media_type == 1)
endpoint = "/artists/"
else if (item.media_type == 2)
endpoint = "/albums/"
else if (item.media_type == 3 || item.media_type == 5)
- {
- this.showPlayMenu(item);
+ {
+ this.showPlayMenu(item, context);
return;
- }
+ }
else if (item.media_type == 4)
endpoint = "/playlists/"
item_id = item.item_id.toString();
return hours+':'+minutes+':'+seconds;
}
function toggleLibrary (item) {
+ /// triggered when user clicks the library (heart) button
var endpoint = this.$globals.server + "api/" + item.media_type + "/";
item_id = item.item_id.toString();
- var action = "/library_remove"
+ var action = "library_remove"
if (item.in_library.length == 0)
- action = "/library_add"
- var url = endpoint + item_id + action;
+ action = "library_add"
+ var url = endpoint + item_id;
console.log('loading ' + url);
axios
- .get(url, { params: { provider: item.provider }})
+ .get(url, { params: { provider: item.provider, action: action }})
.then(result => {
data = result.data;
console.log(data);
.catch(error => {
console.log("error", error);
});
-
};
+
+
var AlbumDetails = Vue.component('AlbumDetails', {
template: `
<section>
- <infoheader v-bind:info="info"/>
+ <infoheader v-bind:info="info" :context="'albumdetails'"/>
<v-tabs
v-model="active"
color="transparent"
v-bind:index="index"
:hideavatar="true"
:hideproviders="isMobile()"
+ :context="'albumtracks'"
>
</listviewItem>
</v-list>
:key="item.db_id"
v-bind:totalitems="albumversions.length"
v-bind:index="index"
+ :context="'albumtracks'"
>
</listviewItem>
</v-list>
},
methods: {
getInfo () {
- this.$globals.loading = true;
const api_url = this.$globals.server + 'api/albums/' + this.media_id
axios
.get(api_url, { params: { provider: this.provider }})
data = result.data;
this.info = data;
this.getAlbumVersions()
- this.$globals.loading = false;
+ this.$globals.curContext = data;
})
.catch(error => {
console.log("error", error);
});
},
getAlbumTracks () {
+ this.$globals.loading = true;
const api_url = this.$globals.server + 'api/albums/' + this.media_id + '/tracks'
axios
.get(api_url, { params: { offset: this.offset, limit: 50, provider: this.provider}})
data = result.data;
this.albumtracks.push(...data);
this.offset += 50;
+ this.$globals.loading = false;
})
.catch(error => {
console.log("error", error);
var ArtistDetails = Vue.component('ArtistDetails', {
template: `
<section>
- <infoheader v-bind:info="info"/>
+ <infoheader v-bind:info="info" :context="'artistdetails'"/>
<v-tabs
v-model="active"
color="transparent"
:hideavatar="isMobile()"
:hidetracknum="true"
:hideproviders="isMobile()"
- :hidelibrary="isMobile()">
+ :hidelibrary="isMobile()"
+ :context="'artisttracks'"
+ >
</listviewItem>
</v-list>
</v-card>
v-bind:totalitems="artistalbums.length"
v-bind:index="index"
:hideproviders="isMobile()"
+ :context="'artistalbums'"
>
</listviewItem>
</v-list>
artistalbums: [],
bg_image: "../images/info_gradient.jpg",
active: null,
- playmenu: false,
- playmenuitem: null
+ contextmenu: false,
+ contextmenuitem: null
}
},
created() {
.then(result => {
data = result.data;
this.info = data;
+ this.$globals.curContext = data;
this.$globals.loading = false;
if (data.is_lazy == true)
// refresh the info if we got a lazy object
<v-list two-line>
<listviewItem
v-for="(item, index) in items"
- :key="item.db_id"
+ :key="item.item_id"
v-bind:item="item"
v-bind:totalitems="items.length"
v-bind:index="index"
:hideavatar="item.media_type == 3 ? isMobile() : false"
:hidetracknum="true"
:hideproviders="isMobile()"
- :hidelibrary="isMobile() ? true : item.media_type != 3">
+ :hidelibrary="isMobile() ? true : item.media_type != 3"
+ :context="mediatype"
+ >
</listviewItem>
</v-list>
</section>
var PlaylistDetails = Vue.component('PlaylistDetails', {
template: `
<section>
- <infoheader v-bind:info="info"/>
+ <infoheader v-bind:info="info" :context="'playlistdetails'"/>
<v-tabs
v-model="active"
color="transparent"
:hideavatar="isMobile()"
:hidetracknum="true"
:hideproviders="isMobile()"
- :hidelibrary="isMobile()">
+ :hidelibrary="isMobile()"
+ v-bind:context="info"
+ >
</listviewItem>
</v-list>
</v-card>
.then(result => {
data = result.data;
this.info = data;
+ this.$globals.curContext = data;
})
.catch(error => {
console.log("error", error);
:hideavatar="isMobile()"
:hidetracknum="true"
:hideproviders="isMobile()"
- :hidelibrary="isMobile()">
+ :hidelibrary="isMobile()"
+ :context="'playerqueue'"
+ >
</listviewItem>
</v-list>
</section>`,
:hidetracknum="true"
:hideproviders="isMobile()"
:hideduration="isMobile()"
- :showlibrary="true">
+ :showlibrary="true"
+ :context="'searchtracks'"
+ >
</listviewItem>
</v-list>
</v-card>
v-bind:totalitems="artists.length"
v-bind:index="index"
:hideproviders="isMobile()"
+ :context="'searchartists'"
>
</listviewItem>
</v-list>
v-bind:totalitems="albums.length"
v-bind:index="index"
:hideproviders="isMobile()"
+ :context="'searchalbums'"
>
</listviewItem>
</v-list>
:key="item.db_id"
v-bind:totalitems="playlists.length"
v-bind:index="index"
- :hidelibrary="true">
+ :hidelibrary="true"
+ :context="'searchplaylists'"
+ >
</listviewItem>
</v-list>
</v-card>
var TrackDetails = Vue.component('TrackDetails', {
template: `
<section>
- <infoheader v-bind:info="info"/>
+ <infoheader v-bind:info="info" :context="'trackdetails'"/>
<v-tabs
v-model="active"
color="transparent"
:hideavatar="isMobile()"
:hidetracknum="true"
:hideproviders="isMobile()"
- :hidelibrary="isMobile()">
+ :hidelibrary="isMobile()"
+ :context="'trackversions'"
+ >
</listviewItem>
</v-list>
</v-card>
.then(result => {
data = result.data;
this.info = data;
+ this.$globals.curContext = data;
this.getTrackVersions()
this.$globals.loading = false;
})