"is-symbol": "^1.0.2"
}
},
+ "es6-object-assign": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
+ "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw="
+ },
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
}
}
},
+ "vue-directive-long-press": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/vue-directive-long-press/-/vue-directive-long-press-1.1.0.tgz",
+ "integrity": "sha512-OHhR4N1kuuKaK87SPmqkcm5Y81HjXgv5DfWr96flIkTYveWps1AozKiNrabsIirq+ibSZWFr3IUr0en+Ce3fAA=="
+ },
"vue-eslint-parser": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-5.0.0.tgz",
"resolved": "https://registry.npmjs.org/vue-loading-overlay/-/vue-loading-overlay-3.2.0.tgz",
"integrity": "sha512-QBHa+vwcQ3k3oKp4pucP7RHWHSQvgVWFlDFqSaXLu+kCuEv1PZCoerAo1T04enF5y9yMFCqh7L9ChrWHy7HYvA=="
},
+ "vue-long-click": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/vue-long-click/-/vue-long-click-0.0.4.tgz",
+ "integrity": "sha512-1/8KMsON6k8ebGqOhBZKU69EWlTLv4+LUluwUxTMNYno9t7ztk8j6rNVwewbp9hULktEoe+jBnxMFBngsPQCaQ==",
+ "requires": {
+ "vue": "^2.5.22"
+ }
+ },
"vue-observe-visibility": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz",
"vue-resize": "^0.4.5"
}
},
+ "vue2-touch-events": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/vue2-touch-events/-/vue2-touch-events-2.0.0.tgz",
+ "integrity": "sha512-t4LyihIieUif96RDMgRllrt/1qRE8Zu0dZoyqOAqAttx/TWynIshBQ8TumCj1xhog5EDaIgsihuVA6hPnssCLQ=="
+ },
+ "vuejs-logger": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/vuejs-logger/-/vuejs-logger-1.5.3.tgz",
+ "integrity": "sha512-jw+AQ+IMJBz18fA4opHsqaU7P7yQNugoGywT6i3DCd1BWqg9eUx03Fr21kayqGcP4dxUwhVkkjuOyeirxLJC8g==",
+ "requires": {
+ "es6-object-assign": "1.1.0"
+ }
+ },
"vuetify": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.1.7.tgz",
"register-service-worker": "^1.6.2",
"roboto-fontface": "*",
"vue": "^2.6.10",
+ "vue-directive-long-press": "^1.1.0",
"vue-i18n": "^8.0.0",
"vue-loading-overlay": "^3.2.0",
+ "vue-long-click": "0.0.4",
"vue-observe-visibility": "^0.4.6",
"vue-router": "^3.1.3",
"vue-virtual-scroller": "^1.0.0-rc.2",
+ "vue2-touch-events": "^2.0.0",
+ "vuejs-logger": "1.5.3",
"vuetify": "^2.1.0",
"vuex": "^3.0.1"
},
:hideproviders="false"\r
:hidelibrary="true"\r
:hidemenu="true"\r
- :onclickHandler="playlistSelected"\r
+ :onclickHandler="addToPlaylist"\r
></listviewItem>\r
</v-list>\r
</v-card>\r
if (mediaItem.in_library.length === 0) {\r
menuItems.push({\r
label: 'add_library',\r
- action: 'add_library',\r
+ action: 'toggle_library',\r
icon: 'favorite_border'\r
})\r
}\r
if (mediaItem.in_library.length > 0) {\r
menuItems.push({\r
label: 'remove_library',\r
- action: 'remove_library',\r
+ action: 'toggle_library',\r
icon: 'favorite'\r
})\r
}\r
for (let item of this.curItem.provider_ids) {\r
trackProviders.push(item.provider)\r
}\r
- let playlists = await this.$server.getData('playlists')\r
+ let playlists = await this.$server.getData('library/playlists')\r
let items = []\r
- for (var playlist of playlists.items) {\r
+ for (var playlist of playlists['items']) {\r
if (\r
playlist.is_editable &&\r
(!this.curPlaylist || playlist.item_id !== this.curPlaylist.item_id)\r
itemCommand (cmd) {\r
if (cmd === 'info') {\r
// show media info\r
+ let endpoint = ''\r
+ if (this.curItem.media_type === 1) endpoint = 'artists'\r
+ if (this.curItem.media_type === 2) endpoint = 'albums'\r
+ if (this.curItem.media_type === 3) endpoint = 'tracks'\r
+ if (this.curItem.media_type === 4) endpoint = 'playlists'\r
+ if (this.curItem.media_type === 5) endpoint = 'radios'\r
this.$router.push({\r
- path: '/' + this.curItem.media_type + '/' + this.curItem.item_id,\r
+ path: '/' + endpoint + '/' + this.curItem.item_id,\r
query: { provider: this.curItem.provider }\r
})\r
this.visible = false\r
return this.showPlaylistsMenu()\r
} else if (cmd === 'remove_playlist') {\r
// remove track from playlist\r
- this.playlistAddRemove(\r
+ this.removeFromPlaylist(\r
this.curItem,\r
this.curPlaylist.item_id,\r
'playlist_remove'\r
)\r
this.visible = false\r
+ } else if (cmd === 'toggle_library') {\r
+ // add/remove to/from library\r
+ this.$server.toggleLibrary(this.curItem)\r
+ this.visible = false\r
} else {\r
// assume play command\r
this.$server.playItem(this.curItem, cmd)\r
this.visible = false\r
}\r
},\r
- playlistSelected (playlistobj) {\r
- this.playlistAddRemove(\r
- this.curItem,\r
- playlistobj.item_id,\r
- 'playlist_add'\r
- )\r
- this.visible = false\r
+ addToPlaylist (playlistObj) {\r
+ /// add track to playlist\r
+ let endpoint = 'playlists/' + playlistObj.item_id + '/tracks'\r
+ this.$server.putData(endpoint, this.curItem)\r
+ .then(result => {\r
+ this.visible = false\r
+ })\r
},\r
- playlistAddRemove (track, playlistId, action = 'playlist_add') {\r
- /// add or remove track on playlist\r
- let endpoint = 'track/' + track.item_id\r
- let params = {\r
- provider: track.provider,\r
- action: action,\r
- action_details: playlistId\r
- }\r
- this.$server.getData(endpoint, params)\r
+ removeFromPlaylist (track, playlistId) {\r
+ /// remove track from playlist\r
+ let endpoint = 'playlists/' + playlistId + '/tracks'\r
+ this.$server.deleteData(endpoint, track)\r
.then(result => {\r
// reload listing\r
- if (action === 'playlist_remove') {\r
- this.$server.$emit('refresh_listing')\r
- }\r
+ this.$server.$emit('refresh_listing')\r
})\r
}\r
}\r
<template>
<div>
- <v-list-item ripple @click="itemClicked(item)">
+ <v-list-item ripple @click="itemClicked(item)" v-long-press="500" @click.stop
+ @long-press-start="menuClick(item)" @long-press-start.prevent>
<v-list-item-avatar tile color="grey" v-if="!hideavatar">
<img
:src="$server.getImageUrl(item, 'image', 80)"
onclickHandler: null
},
data () {
- return {}
+ return {
+ touchMoving: false
+ }
},
computed: {
isHiRes () {
return false
}
},
+ created () { },
mounted () { },
methods: {
itemClicked (mediaItem) {
import store from './plugins/store'
import server from './plugins/server'
import '@babel/polyfill'
+import VueLogger from 'vuejs-logger'
+import LongPress from 'vue-directive-long-press'
+
+const isProduction = process.env.NODE_ENV === 'production'
+const loggerOptions = {
+ isEnabled: true,
+ logLevel: isProduction ? 'error' : 'debug',
+ stringifyArguments: false,
+ showLogLevel: true,
+ showMethodName: false,
+ separator: '|',
+ showConsoleColors: true
+}
Vue.config.productionTip = false
+Vue.use(VueLogger, loggerOptions)
Vue.use(VueVirtualScroller)
Vue.use(store)
Vue.use(server)
+Vue.directive('long-press', LongPress)
// eslint-disable-next-line no-extend-native
String.prototype.formatDuration = function () {
},
methods: {
- connect (serverAddress) {
+ async connect (serverAddress) {
// Connect to the server
if (!serverAddress.endsWith('/')) {
serverAddress = serverAddress + '/'
}
this._address = serverAddress
let wsAddress = serverAddress.replace('http', 'ws') + 'ws'
+ // retrieve all players
+ let players = await this.getData('players')
+ for (let player of players) {
+ Vue.set(this.players, player.player_id, player)
+ }
+ this._selectActivePlayer()
+ this.$emit('players changed')
this._ws = new WebSocket(wsAddress)
this._ws.onopen = this._onWsConnect
this._ws.onmessage = this._onWsMessage
async toggleLibrary (item) {
/// triggered when user clicks the library (heart) button
- let endpoint = item.media_type + '/' + item.item_id
- let action = 'library_remove'
if (item.in_library.length === 0) {
- action = 'library_add'
- }
- await this.getData(endpoint, { provider: item.provider, action: action })
- if (action === '/library_remove') {
- item.in_library = []
- } else {
+ // add to library
+ await this.putData('library', item)
item.in_library = [item.provider]
+ } else {
+ // remove from library
+ await this.deleteData('library', item)
+ item.in_library = []
}
},
getImageUrl (mediaItem, imageType = 'image', size = 0) {
// format the image url
if (!mediaItem || !mediaItem.media_type) return ''
+ if (mediaItem.media_type in ['playlists', 'radios'] && imageType !== 'image') {
+ return ''
+ }
if (mediaItem.provider === 'database') {
return `${this._address}api/${mediaItem.media_type}/${mediaItem.item_id}/image?type=${imageType}&provider=${mediaItem.provider}&size=${size}`
} else if (mediaItem.metadata && mediaItem.metadata['image']) {
// get data from the server
let url = this._address + 'api/' + endpoint
let result = await _axios.get(url, { params: params })
+ Vue.$log.debug('getData', endpoint, result)
return result.data
},
async postData (endpoint, data) {
// post data to the server
let url = this._address + 'api/' + endpoint
+ data = JSON.stringify(data)
let result = await _axios.post(url, data)
+ Vue.$log.debug('postData', endpoint, result)
+ return result.data
+ },
+
+ async putData (endpoint, data) {
+ // put data to the server
+ let url = this._address + 'api/' + endpoint
+ data = JSON.stringify(data)
+ let result = await _axios.put(url, data)
+ Vue.$log.debug('putData', endpoint, result)
+ return result.data
+ },
+
+ async deleteData (endpoint, dataObj) {
+ // delete data on the server
+ let url = this._address + 'api/' + endpoint
+ dataObj = JSON.stringify(dataObj)
+ let result = await _axios.delete(url, { data: dataObj })
+ Vue.$log.debug('deleteData', endpoint, result)
return result.data
},
})
.done(function (fullList) {
// truncate list if needed
- if (list.length === 0) {
- list = []
- } else if (list.length > index) {
- list = list.slice(0, index)
+ if (list.length > fullList.items.length) {
+ list.splice(fullList.items.length)
}
})
},
- playerCommand (cmd, cmd_opt = null, playerId = this.activePlayerId) {
- let msgDetails = {
- player_id: playerId,
- cmd: cmd,
- cmd_args: cmd_opt
- }
- this._ws.send(JSON.stringify({ message: 'player command', message_details: msgDetails }))
+ playerCommand (cmd, cmd_opt = '', playerId = this.activePlayerId) {
+ let endpoint = 'players/' + playerId + '/cmd/' + cmd
+ this.postData(endpoint, cmd_opt)
},
async playItem (item, queueOpt) {
this.$store.loading = true
- let endpoint = 'players/' + this.activePlayerId + '/play_media/' + item.media_type + '/' + item.item_id + '/' + queueOpt
- await this.getData(endpoint)
+ let endpoint = 'players/' + this.activePlayerId + '/play_media/' + queueOpt
+ await this.postData(endpoint, item)
this.$store.loading = false
},
_onWsConnect () {
// Websockets connection established
- // console.log('Connected to server ' + this._address)
+ Vue.$log.info('Connected to server ' + this._address)
this.connected = true
// request all players
let data = JSON.stringify({ message: 'players', message_details: null })
Vue.delete(this.players, msg.message_details.player_id)
this._selectActivePlayer()
this.$emit('players changed')
- } else if (msg.message === 'players') {
- for (var item of msg.message_details) {
- Vue.set(this.players, item.player_id, item)
- }
- this._selectActivePlayer()
- this.$emit('players changed')
} else if (msg.message === 'music sync status') {
this.syncStatus = msg.message_details
} else {
_onWsClose (e) {
this.connected = false
- // console.log('Socket is closed. Reconnect will be attempted in 5 seconds.', e.reason)
+ Vue.$log.error('Socket is closed. Reconnect will be attempted in 5 seconds.', e.reason)
setTimeout(function () {
this.connect(this._address)
}.bind(this), 5000)
created () {
this.$store.windowtitle = this.$t(this.mediatype)
this.getItems()
+ this.$server.$on('refresh_listing', this.getItems)
+ },
+ beforeDestroy () {
+ this.$server.$off('refresh_listing')
},
methods: {
async getItems () {
// retrieve the full list of items
- return this.$server.getAllItems(this.mediatype, this.items)
+ let endpoint = 'library/' + this.mediatype
+ return this.$server.getAllItems(endpoint, this.items)
}
}
}
},
async confChanged (key, subkey, newvalue) {
let endpoint = 'config/' + key + '/' + subkey
- let result = await this.$server.postData(endpoint, newvalue)
+ let result = await this.$server.putData(endpoint, newvalue)
if (result.restart_required) {
this.restart_message = true
}
async def artist_tracks(self, artist_id, orderby='name') -> List[Track]:
''' get all library tracks for the given artist '''
artist_id = try_parse_int(artist_id)
- sql_query = 'SELECT * FROM tracks 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 = %s)' % artist_id
async for item in self.tracks(sql_query, orderby=orderby, fulldata=False):
yield item
async def artist_albums(self, artist_id, orderby='name') -> List[Album]:
''' get all library albums for the given artist '''
- sql_query = ' WHERE artist_id = %d' % artist_id
+ sql_query = ' WHERE artist_id = %s' % artist_id
async for item in self.albums(sql_query, orderby=orderby, fulldata=False):
yield item
search_results = await self.search(searchstr, [MediaType.Album],
limit=5)
for item in search_results["albums"]:
- if (item and
- (item.name in searchalbum.name
+ if (item and (item.name in searchalbum.name
or searchalbum.name in item.name) and compare_strings(
item.artist.name, searchalbum.artist.name, strict=False)):
# some providers mess up versions in the title, try to fix that situation
import uuid
import os
import pickle
+from enum import Enum
from ..utils import LOGGER, json, filename_from_string
from ..constants import CONF_ENABLED, EVENT_PLAYBACK_STARTED, EVENT_PLAYBACK_STOPPED, EVENT_QUEUE_UPDATED
from .playerstate import PlayerState
+class QueueOption(str, Enum):
+ Play = "play"
+ Replace = "replace"
+ Next = "next"
+ Add = "add"
+
+
class QueueItem(Track):
''' representation of a queue item, simplified version of track '''
def __init__(self, media_item=None):
import aiohttp
from .utils import run_periodic, LOGGER, load_provider_modules
-from .models.media_types import MediaType, Track, Artist, Album, Playlist, Radio
+from .models.media_types import MediaItem, MediaType, Track, Artist, Album, Playlist, Radio
from .constants import CONF_KEY_MUSICPROVIDERS, EVENT_MUSIC_SYNC_STATUS
prov_id)
return
sync_job = (prov_id, desc)
- if not method_class.running_sync_jobs:
- LOGGER.info("Music provider sync started")
method_class.running_sync_jobs.append(sync_job)
await method_class.mass.signal_event(
EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs)
method_class.running_sync_jobs.remove(sync_job)
await method_class.mass.signal_event(
EVENT_MUSIC_SYNC_STATUS, method_class.running_sync_jobs)
- if not method_class.running_sync_jobs:
- LOGGER.info("Music provider sync completed")
-
return wrapped
-
return wrapper
else:
return None
- async def library_artists(self, orderby='name', provider_filter=None) -> List[Artist]:
+ async def library_artists(self, orderby='name',
+ provider_filter=None) -> List[Artist]:
''' return all library artists, optionally filtered by provider '''
- async for item in self.mass.db.library_artists(provider=provider_filter, orderby=orderby):
+ async for item in self.mass.db.library_artists(
+ provider=provider_filter, orderby=orderby):
yield item
- async def library_albums(self,
- orderby='name',
+ async def library_albums(self, orderby='name',
provider_filter=None) -> List[Album]:
''' return all library albums, optionally filtered by provider '''
- async for item in self.mass.db.library_albums(provider=provider_filter, orderby=orderby):
+ async for item in self.mass.db.library_albums(provider=provider_filter,
+ orderby=orderby):
yield item
- async def library_tracks(self,
- orderby='name',
+ async def library_tracks(self, orderby='name',
provider_filter=None) -> List[Track]:
''' return all library tracks, optionally filtered by provider '''
- async for item in self.mass.db.library_tracks(provider=provider_filter, orderby=orderby):
+ async for item in self.mass.db.library_tracks(provider=provider_filter,
+ orderby=orderby):
yield item
- async def library_playlists(self,
- orderby='name',
- provider_filter=None) -> List[Playlist]:
+ async def library_playlists(self, orderby='name',
+ provider_filter=None) -> List[Playlist]:
''' return all library playlists, optionally filtered by provider '''
- async for item in self.mass.db.library_playlists(provider=provider_filter,
- orderby=orderby):
+ async for item in self.mass.db.library_playlists(
+ provider=provider_filter, orderby=orderby):
yield item
- async def library_radios(self,
- orderby='name',
- provider_filter=None) -> List[Playlist]:
+ async def library_radios(self, orderby='name',
+ provider_filter=None) -> List[Playlist]:
''' return all library radios, optionally filtered by provider '''
async for item in self.mass.db.library_radios(provider=provider_filter,
- orderby=orderby):
+ orderby=orderby):
yield item
async def artist(self, item_id, provider='database', lazy=True) -> Artist:
async for item in prov_obj.album_tracks(prov['item_id']):
yield item
- async def playlist_tracks(self, playlist_id, provider='database') -> List[Track]:
+ async def playlist_tracks(self, playlist_id,
+ provider='database') -> List[Track]:
''' get the tracks for given playlist '''
playlist = await self.playlist(playlist_id, provider)
# return playlist tracks from provider
prov = playlist.provider_ids[0]
- async for item in self.providers[prov['provider']].playlist_tracks(prov['item_id']):
+ async for item in self.providers[prov['provider']].playlist_tracks(
+ prov['item_id']):
yield item
async def search(self,
toolz.unique(items, key=operator.attrgetter('item_id')))
return result
- async def item_action(self,
- item_id,
- media_type,
- provider,
- action,
- action_details=None):
- ''' perform action on item (such as library add/remove) '''
- result = None
- item = await self.item(item_id, media_type, provider, lazy=False)
- if not item:
- return False
- if 'library_' in action:
- # remove or add item to the library
- for prov_mapping in item.provider_ids:
- prov_id = prov_mapping['provider']
- prov_item_id = prov_mapping['item_id']
- for prov in self.providers.values():
- if prov.prov_id == prov_id:
- if action == 'library_add':
- result = await prov.add_library(
- prov_item_id, media_type)
- await self.mass.db.add_to_library(
- item.item_id, item.media_type, prov_id)
- 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 == 'playlist_add':
- result = await self.add_playlist_tracks(action_details, [item])
- elif action == 'playlist_remove':
- result = await self.remove_playlist_tracks(action_details, [item])
+ async def library_add(self, media_items: List[MediaItem]):
+ '''Add media item(s) to the library'''
+ result = False
+ for item in media_items:
+ # make sure we have a database item
+ media_item = await self.item(item.item_id, item.media_type, item.provider, lazy=False)
+ if not media_item:
+ continue
+ # add to provider's libraries
+ for prov in item.provider_ids:
+ prov_id = prov['provider']
+ prov_item_id = prov['item_id']
+ if prov_id in self.providers:
+ result = await self.providers[prov_id].add_library(
+ prov_item_id, media_item.media_type)
+ # mark as library item in internal db
+ await self.mass.db.add_to_library(
+ media_item.item_id, media_item.media_type, prov_id)
return result
- async def add_playlist_tracks(self, playlist_id, tracks: List[Track]):
+ async def library_remove(self, media_items: List[MediaItem]):
+ '''Remove media item(s) from the library'''
+ result = False
+ for item in media_items:
+ # make sure we have a database item
+ media_item = await self.item(item.item_id, item.media_type, item.provider, lazy=False)
+ if not media_item:
+ continue
+ # remove from provider's libraries
+ for prov in item.provider_ids:
+ prov_id = prov['provider']
+ prov_item_id = prov['item_id']
+ if prov_id in self.providers:
+ result = await self.providers[prov_id].remove_library(
+ prov_item_id, media_item.media_type)
+ # mark as library item in internal db
+ await self.mass.db.remove_from_library(
+ media_item.item_id, media_item.media_type, prov_id)
+ return result
+
+ async def add_playlist_tracks(self, db_playlist_id, tracks: List[Track]):
''' add tracks to playlist - make sure we dont add dupes '''
# we can only edit playlists that are in the database (marked as editable)
- playlist = await self.playlist(playlist_id, 'database')
+ playlist = await self.playlist(db_playlist_id, 'database')
if not playlist or not playlist.is_editable:
- LOGGER.warning(
- "Playlist %s is not editable - skip addition of tracks" %
- (playlist.name))
return False
# playlist can only have one provider (for now)
playlist_prov = playlist.provider_ids[0]
- cur_playlist_tracks = await self.mass.db.playlist_tracks(playlist_id,
- limit=0)
- # 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]
+ # grab all existing track ids in the playlist so we can check for duplicates
+ cur_playlist_track_ids = []
+ async for item in self.providers[playlist_prov['provider']].playlist_tracks(
+ playlist_prov['item_id']):
+ cur_playlist_track_ids.append(item.item_id)
+ cur_playlist_track_ids += [i['item_id'] for i in item.provider_ids]
track_ids_to_add = []
- 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)
- if track.item_id in cur_playlist_track_ids:
- LOGGER.warning(
- "Track %s already in playlist %s - skip addition" %
- (track.name, playlist.name))
+ for track in tracks:
+ # check for duplicates
+ already_exists = track.item_id in cur_playlist_track_ids
+ for track_prov in track.provider_ids:
+ if track_prov['item_id'] in cur_playlist_track_ids:
+ already_exists = True
+ if already_exists:
continue
# we can only add a track to a provider playlist if the track is available on that provider
- # exception is the file provider which does accept tracks from all providers in the m3u playlist
# this should all be handled in the frontend but these checks are here just to be safe
- track_playlist_provs = [
- item['provider'] for item in track.provider_ids
- ]
- if playlist_prov['provider'] in track_playlist_provs:
- # a track can contain multiple versions on the same provider
- # simply sort by quality and just add the first one (assuming the track is still available)
- track_versions = sorted(track.provider_ids,
- key=operator.itemgetter('quality'),
- reverse=True)
- for track_version in track_versions:
- if track_version['provider'] == playlist_prov['provider']:
- track_ids_to_add.append(track_version['item_id'])
- break
- elif playlist_prov['provider'] == 'file':
- # the file provider can handle uri's from all providers in the file so simply add the db id
- track_ids_to_add.append(track.item_id)
- else:
- LOGGER.warning(
- "Track %s not available on provider %s - skip addition to playlist %s"
- % (track.name, playlist_prov['provider'], playlist.name))
- continue
+ # a track can contain multiple versions on the same provider
+ # simply sort by quality and just add the first one (assuming the track is still available)
+ for track_version in sorted(track.provider_ids,
+ key=operator.itemgetter('quality'),
+ reverse=True):
+ if track_version['provider'] == playlist_prov['provider']:
+ track_ids_to_add.append(track_version['item_id'])
+ break
+ elif playlist_prov['provider'] == 'file':
+ # the file provider can handle uri's from all providers in the file so simply add the uri
+ uri = f'{track_version["provider"]}://{track_version["item_id"]}'
+ track_ids_to_add.append(uri)
+ break
# actually add the tracks to the playlist on the provider
if track_ids_to_add:
# invalidate cache
- await self.mass.db.update_playlist(playlist.item_id, 'checksum', str(time.time()))
+ await self.mass.db.update_playlist(playlist.item_id, 'checksum',
+ str(time.time()))
+ # return result of the action on the provioer
return await self.providers[playlist_prov['provider']
].add_playlist_tracks(
playlist_prov['item_id'],
track_ids_to_add)
-
+ return False
- async def remove_playlist_tracks(self, playlist_id, tracks: List[Track]):
+ async def remove_playlist_tracks(self, db_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')
+ playlist = await self.playlist(db_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
# playlist can only have one provider (for now)
prov_playlist = playlist.provider_ids[0]
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:
# actually remove the tracks from the playlist on the provider
if track_ids_to_remove:
# invalidate cache
- await self.mass.db.update_playlist(playlist.item_id, 'checksum', str(time.time()))
+ await self.mass.db.update_playlist(playlist.item_id, 'checksum',
+ str(time.time()))
return await self.providers[prov_playlist_provider_id
].remove_playlist_tracks(
prov_playlist_playlist_id,
async def sync_library_artists(self, prov_id):
''' sync library artists for given provider'''
music_provider = self.providers[prov_id]
- prev_items = [item async for item in self.library_artists(provider_filter=prov_id)]
- prev_db_ids = [item.item_id for item in prev_items]
+ prev_db_ids = [
+ item.item_id
+ async for item in self.library_artists(provider_filter=prov_id)
+ ]
cur_db_ids = []
async for item in music_provider.get_library_artists():
db_item = await music_provider.artist(item.item_id, lazy=False)
async def sync_library_albums(self, prov_id):
''' sync library albums for given provider'''
music_provider = self.providers[prov_id]
- prev_items = [item async for item in self.library_albums(provider_filter=prov_id)]
- prev_db_ids = [item.item_id for item in prev_items]
+ prev_db_ids = [
+ item.item_id
+ async for item in self.library_albums(provider_filter=prov_id)
+ ]
cur_db_ids = []
async for item in music_provider.get_library_albums():
db_album = await music_provider.album(item.item_id, lazy=False)
async def sync_library_tracks(self, prov_id):
''' sync library tracks for given provider'''
music_provider = self.providers[prov_id]
- prev_items = [item async for item in self.library_tracks(provider_filter=prov_id)]
- prev_db_ids = [item.item_id for item in prev_items]
+ prev_db_ids = [
+ item.item_id
+ async for item in self.library_tracks(provider_filter=prov_id)
+ ]
cur_db_ids = []
async for item in music_provider.get_library_tracks():
db_item = await music_provider.track(item.item_id, lazy=False)
async def sync_library_playlists(self, prov_id):
''' sync library playlists for given provider'''
music_provider = self.providers[prov_id]
- prev_items = [item async for item in self.library_playlists(provider_filter=prov_id)]
- prev_db_ids = [item.item_id for item in prev_items]
+ prev_db_ids = [
+ item.item_id
+ async for item in self.library_playlists(provider_filter=prov_id)
+ ]
cur_db_ids = []
async for item in music_provider.get_library_playlists():
# always add to db because playlist attributes could have changed
if not db_id in prev_db_ids:
await self.mass.db.add_to_library(db_id, MediaType.Playlist,
prov_id)
+ # precache playlist tracks
+ [item async for item in music_provider.playlist_tracks(item.item_id)]
# process playlist deletions
for db_id in prev_db_ids:
if db_id not in cur_db_ids:
async def sync_library_radios(self, prov_id):
''' sync library radios for given provider'''
music_provider = self.providers[prov_id]
- prev_items = [item async for item in self.library_radios(provider_filter=prov_id)]
- prev_db_ids = [item.item_id for item in prev_items]
+ prev_db_ids = [
+ item.item_id
+ async for item in self.library_radios(provider_filter=prov_id)
+ ]
cur_db_ids = []
async for item in music_provider.get_radios():
db_id = await self.mass.db.get_database_id(prov_id, item.item_id,
async def get_library_playlists(self) -> List[Playlist]:
''' retrieve all library playlists from the provider '''
- endpoint = 'favorite/getUserPlaylists'
+ endpoint = 'playlist/getUserPlaylists'
async for item in self.__get_all_items(endpoint, key='playlists'):
playlist = await self.__parse_playlist(item)
if playlist:
headers=headers,
params=params,
verify_ssl=False) as response:
- try:
- result = await response.json()
- if "error" in result:
- return None
- return result
- except Exception as exc:
- LOGGER.error(exc)
- LOGGER.debug(url)
- LOGGER.debug(params)
- result = await response.text()
- LOGGER.error(result)
+ result = await response.json()
+ if 'error' in result or ('status' in result and 'error' in result['status']):
+ LOGGER.error('%s - %s', endpoint, result)
+ return None
+ return result
async def __post_data(self, endpoint, params=None, data=None):
''' post data to api'''
params=params,
json=data,
verify_ssl=False) as response:
- try:
- result = await response.json()
- if "error" in result:
- return None
- return result
- except Exception as exc:
- LOGGER.error(exc)
- LOGGER.debug(url)
- LOGGER.debug(params)
- result = await response.text()
- LOGGER.error(result)
+ result = await response.json()
+ if 'error' in result or ('status' in result and 'error' in result['status']):
+ LOGGER.error('%s - %s', endpoint, result)
+ return None
+ return result
verify_ssl=False) as response:
result = await response.json()
if not result or 'error' in result:
- LOGGER.error(url)
- LOGGER.error(params)
+ LOGGER.error('%s - %s', endpoint, result)
result = None
return result
import asyncio
import os
from enum import Enum
+from typing import List
import operator
import random
import functools
from .constants import CONF_KEY_PLAYERPROVIDERS, EVENT_PLAYER_ADDED, EVENT_PLAYER_REMOVED, EVENT_HASS_ENTITY_CHANGED
from .utils import run_periodic, LOGGER, try_parse_int, try_parse_float, \
- get_ip, run_async_background_task, load_provider_modules
-from .models.media_types import MediaType, TrackQuality
-from .models.player_queue import QueueItem
+ get_ip, run_async_background_task, load_provider_modules, iter_items
+from .models.media_types import MediaItem, MediaType, TrackQuality
+from .models.player_queue import QueueItem, QueueOption
from .models.playerstate import PlayerState
+from .models.player import Player
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODULES_PATH = os.path.join(BASE_DIR, "playerproviders" )
''' return list of all players '''
return self._players.values()
- async def get_player(self, player_id):
+ async def get_player(self, player_id:str):
''' return player by id '''
return self._players.get(player_id, None)
- def get_player_sync(self, player_id):
+ def get_player_sync(self, player_id:str):
''' return player by id (non async) '''
return self._players.get(player_id, None)
- async def add_player(self, player):
+ async def add_player(self, player:Player):
''' register a new player '''
player._initialized = True
self._players[player.player_id] = player
LOGGER.info(f"New player added: {player.player_provider}/{player.player_id}")
return player
- async def remove_player(self, player_id):
+ async def remove_player(self, player_id:str):
''' handle a player remove '''
self._players.pop(player_id, None)
await self.mass.signal_event(EVENT_PLAYER_REMOVED, {"player_id": player_id})
LOGGER.info(f"Player removed: {player_id}")
- async def trigger_update(self, player_id):
+ async def trigger_update(self, player_id:str):
''' manually trigger update for a player '''
if player_id in self._players:
await self._players[player_id].update(force=True)
- async def play_media(self, player_id, media_item, queue_opt='play'):
+ async def play_media(self,
+ player_id:str,
+ media_items:List[MediaItem],
+ queue_opt:QueueOption='play'):
'''
- play media item(s) on the given player
+ play media item(s) on the given player
:param media_item: media item(s) that should be played (Track, Album, Artist, Playlist, Radio)
single item or list of items
:param queue_opt:
- play -> insert new items in queue and start playing at the inserted position
- replace -> replace queue contents with these items
- next -> play item(s) after current playing item
- add -> append new items at end of the queue
+ QueueOption.Play -> insert new items in queue and start playing at the inserted position
+ QueueOption.Replace -> replace queue contents with these items
+ QueueOption.Next -> play item(s) after current playing item
+ QueueOption.Add -> append new items at end of the queue
'''
player = await self.get_player(player_id)
if not player:
return
# a single item or list of items may be provided
- media_items = media_item if isinstance(media_item, list) else [media_item]
queue_items = []
for media_item in media_items:
# collect tracks to play
if media_item.media_type == MediaType.Artist:
- tracks = await self.mass.music.artist_toptracks(media_item.item_id,
+ tracks = self.mass.music.artist_toptracks(media_item.item_id,
provider=media_item.provider)
elif media_item.media_type == MediaType.Album:
- tracks = await self.mass.music.album_tracks(media_item.item_id,
+ tracks = self.mass.music.album_tracks(media_item.item_id,
provider=media_item.provider)
elif media_item.media_type == MediaType.Playlist:
- tracks = await self.mass.music.playlist_tracks(media_item.item_id,
+ tracks = self.mass.music.playlist_tracks(media_item.item_id,
provider=media_item.provider)
else:
- tracks = [media_item] # single track
- for track in tracks:
+ tracks = iter_items(media_item) # single track
+ async for track in tracks:
queue_item = QueueItem(track)
# generate uri for this queue item
queue_item.uri = 'http://%s:%s/stream/%s/%s'% (
queue_items.append(queue_item)
# load items into the queue
- if queue_opt == 'replace' or (queue_opt in ['next', 'play'] and len(queue_items) > 10):
+ if (queue_opt == QueueOption.Replace or
+ (len(queue_items) > 10 and
+ queue_opt == QueueOption.Play or
+ queue_opt == QueueOption.Next)):
return await player.queue.load(queue_items)
- elif queue_opt == 'next':
+ elif queue_opt == QueueOption.Next:
return await player.queue.insert(queue_items, 1)
- elif queue_opt == 'play':
+ elif queue_opt == QueueOption.Play:
return await player.queue.insert(queue_items, 0)
- elif queue_opt == 'add':
+ elif queue_opt == QueueOption.Add:
return await player.queue.append(queue_items)
async def handle_mass_events(self, msg, msg_details=None):
except:
return 0
+async def iter_items(items):
+ '''fake async iterator for compatability reasons.'''
+ if not isinstance(items, list):
+ yield items
+ else:
+ for item in items:
+ yield items
+
def try_parse_float(possible_float):
try:
return float(possible_float)
import asyncio
import os
import aiohttp
+import inspect
import aiohttp_cors
from aiohttp import web
from functools import partial
('cert_fqdn_host', '', 'cert_fqdn_host')
]
+class ClassRouteTableDef(web.RouteTableDef):
+ def __repr__(self) -> str:
+ return "<ClassRouteTableDef count={}>".format(len(self._items))
+
+ def route(self,
+ method: str,
+ path: str,
+ **kwargs):
+ def inner(handler):
+ handler.route_info = (method, path, kwargs)
+ return handler
+ return inner
+
+ def add_class_routes(self, instance) -> None:
+ def predicate(member) -> bool:
+ return all((inspect.iscoroutinefunction(member),
+ hasattr(member, "route_info")))
+ for _, handler in inspect.getmembers(instance, predicate):
+ method, path, kwargs = handler.route_info
+ super().route(method, path, **kwargs)(handler)
+routes = ClassRouteTableDef()
+
class Web():
""" webserver and json/websocket api """
+ runner = None
def __init__(self, mass):
self.mass = mass
if config['ssl_certificate'] and not os.path.isfile(
config['ssl_certificate']):
enable_ssl = False
- LOGGER.warning("SSL certificate file not found: %s" % config['ssl_certificate'])
+ LOGGER.warning("SSL certificate file not found: %s", config['ssl_certificate'])
if config['ssl_key'] and not os.path.isfile(config['ssl_key']):
enable_ssl = False
- LOGGER.warning( "SSL certificate key file not found: %s" % config['ssl_key'])
+ LOGGER.warning( "SSL certificate key file not found: %s", config['ssl_key'])
self.https_port = config['https_port']
self._enable_ssl = enable_ssl
async def setup(self):
""" perform async setup """
+ routes.add_class_routes(self)
app = web.Application()
- app.add_routes([web.get('/jsonrpc.js', self.json_rpc)])
- app.add_routes([web.post('/jsonrpc.js', self.json_rpc)])
- app.add_routes([web.get('/ws', self.websocket_handler)])
- app.add_routes([web.get('/stream/{player_id}', self.mass.http_streamer.stream, allow_head=False)])
- app.add_routes([web.get('/stream/{player_id}/{queue_item_id}', self.mass.http_streamer.stream, allow_head=False)])
- app.add_routes([web.get('/api/search', self.search)])
- app.add_routes([web.post('/api/config/{key}/{subkey}', self.save_config)])
- app.add_routes([web.get('/api/config', self.get_config)])
- app.add_routes([web.get('/api/players', self.players)])
- app.add_routes([web.get('/api/players/{player_id}', self.player)])
- app.add_routes([web.get('/api/players/{player_id}/queue', self.player_queue)])
- app.add_routes([web.get('/api/players/{player_id}/queue/{item_id}', self.player_queue_item)])
- app.add_routes([web.get('/api/players/{player_id}/cmd/{cmd}', self.player_command)])
- app.add_routes([web.get('/api/players/{player_id}/cmd/{cmd}/{cmd_args}', self.player_command)])
- app.add_routes([web.get('/api/players/{player_id}/play_media/{media_type}/{media_id}', self.play_media)])
- app.add_routes([web.get('/api/players/{player_id}/play_media/{media_type}/{media_id}/{queue_opt}', self.play_media)])
- app.add_routes([web.get('/api/playlists/{playlist_id}/tracks', self.playlist_tracks)])
- 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}/image', self.get_image)])
- app.add_routes([web.get('/api/{media_type}/{media_id}', self.get_item)])
- app.add_routes([web.get('/api/artists', self.library_artists)])
- app.add_routes([web.get('/api/albums', self.library_albums)])
- app.add_routes([web.get('/api/tracks', self.library_tracks)])
- app.add_routes([web.get('/api/radios', self.library_radios)])
- app.add_routes([web.get('/api/playlists', self.library_playlists)])
- app.add_routes([web.get('/', self.index)])
+ app.add_routes(routes)
+ app.add_routes([
+ web.get('/stream/{player_id}', self.mass.http_streamer.stream, allow_head=False),
+ web.get('/stream/{player_id}/{queue_item_id}', self.mass.http_streamer.stream, allow_head=False),
+ web.get('/', self.index),
+ web.get('/jsonrpc.js', self.json_rpc),
+ web.post('/jsonrpc.js', self.json_rpc),
+ web.get('/ws', self.websocket_handler)
+ ])
webdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web/')
app.router.add_static("/", webdir)
"*": aiohttp_cors.ResourceOptions(
allow_credentials=True,
expose_headers="*",
- allow_headers="*")
+ allow_headers="*",
+ allow_methods=["POST", "PUT", "DELETE"])
})
for route in list(app.router.routes()):
cors.add(route)
await self.runner.setup()
http_site = web.TCPSite(self.runner, '0.0.0.0', self.http_port)
await http_site.start()
- LOGGER.info("Started HTTP webserver on port %s" % self.http_port)
+ LOGGER.info("Started HTTP webserver on port %s", self.http_port)
if self._enable_ssl:
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(self.config['ssl_certificate'], self.config['ssl_key'])
https_site = web.TCPSite(self.runner, '0.0.0.0', self.config['https_port'], ssl_context=ssl_context)
await https_site.start()
- LOGGER.info("Started HTTPS webserver on port %s" % self.config['https_port'])
+ LOGGER.info("Started HTTPS webserver on port %s", self.config['https_port'])
+
+ async def index(self, request):
+ index_file = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), 'web/index.html')
+ return web.FileResponse(index_file)
+ @routes.get('/api/library/artists')
async def library_artists(self, request):
"""Get all library artists."""
orderby = request.query.get('orderby', 'name')
iterator = self.mass.music.library_artists(orderby=orderby, provider_filter=provider_filter)
return await self.__stream_json(request, iterator)
+ @routes.get('/api/library/albums')
async def library_albums(self, request):
"""Get all library albums."""
orderby = request.query.get('orderby', 'name')
iterator = self.mass.music.library_albums(orderby=orderby, provider_filter=provider_filter)
return await self.__stream_json(request, iterator)
+ @routes.get('/api/library/tracks')
async def library_tracks(self, request):
"""Get all library tracks."""
orderby = request.query.get('orderby', 'name')
iterator = self.mass.music.library_tracks(orderby=orderby, provider_filter=provider_filter)
return await self.__stream_json(request, iterator)
+ @routes.get('/api/library/radios')
async def library_radios(self, request):
"""Get all library radios."""
orderby = request.query.get('orderby', 'name')
iterator = self.mass.music.library_radios(orderby=orderby, provider_filter=provider_filter)
return await self.__stream_json(request, iterator)
+ @routes.get('/api/library/playlists')
async def library_playlists(self, request):
"""Get all library playlists."""
orderby = request.query.get('orderby', 'name')
iterator = self.mass.music.library_playlists(orderby=orderby, provider_filter=provider_filter)
return await self.__stream_json(request, iterator)
- async def get_item(self, request):
- """ get item full details"""
- 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')
- # 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'
+ @routes.put('/api/library')
+ async def library_add(self, request):
+ """Add item(s) to the library"""
+ body = await request.json()
+ media_items = await self.__media_items_from_body(body)
+ result = await self.mass.music.library_add(media_items)
+ return web.json_response(result, dumps=json_serializer)
+
+ @routes.delete('/api/library')
+ async def library_remove(self, request):
+ """R remove item(s) from the library"""
+ body = await request.json()
+ media_items = await self.__media_items_from_body(body)
+ result = await self.mass.music.library_remove(media_items)
+ return web.json_response(result, dumps=json_serializer)
+
+ @routes.get('/api/artists/{item_id}')
+ async def artist(self, request):
+ """ get full artist details"""
+ item_id = request.match_info.get('item_id')
provider = request.rel_url.query.get('provider')
- if action:
- result = await self.mass.music.item_action(media_id, media_type, provider, action, action_details)
- else:
- result = await self.mass.music.item(media_id, media_type, provider, lazy=lazy)
+ if (item_id is None or provider is None):
+ return web.Response(text='invalid item or provider', status=501)
+ result = await self.mass.music.artist(item_id, provider, lazy=False)
+ return web.json_response(result, dumps=json_serializer)
+
+ @routes.get('/api/albums/{item_id}')
+ async def album(self, request):
+ """ get full album details"""
+ item_id = request.match_info.get('item_id')
+ provider = request.rel_url.query.get('provider')
+ if (item_id is None or provider is None):
+ return web.Response(text='invalid item or provider', status=501)
+ result = await self.mass.music.album(item_id, provider, lazy=False)
return web.json_response(result, dumps=json_serializer)
+ @routes.get('/api/tracks/{item_id}')
+ async def track(self, request):
+ """ get full track details"""
+ item_id = request.match_info.get('item_id')
+ provider = request.rel_url.query.get('provider')
+ if (item_id is None or provider is None):
+ return web.Response(text='invalid item or provider', status=501)
+ result = await self.mass.music.track(item_id, provider, lazy=False)
+ return web.json_response(result, dumps=json_serializer)
+
+ @routes.get('/api/playlists/{item_id}')
+ async def playlist(self, request):
+ """ get full playlist details"""
+ item_id = request.match_info.get('item_id')
+ provider = request.rel_url.query.get('provider')
+ if (item_id is None or provider is None):
+ return web.Response(text='invalid item or provider', status=501)
+ result = await self.mass.music.playlist(item_id, provider)
+ return web.json_response(result, dumps=json_serializer)
+
+ @routes.get('/api/radios/{item_id}')
+ async def radio(self, request):
+ """ get full radio details"""
+ item_id = request.match_info.get('item_id')
+ provider = request.rel_url.query.get('provider')
+ if (item_id is None or provider is None):
+ return web.Response(text='invalid item or provider', status=501)
+ result = await self.mass.music.radio(item_id, provider)
+ return web.json_response(result, dumps=json_serializer)
+
+ @routes.get('/api/{media_type}/{media_id}/image')
async def get_image(self, request):
""" get item image """
media_type_str = request.match_info.get('media_type')
provider = request.rel_url.query.get('provider', 'database')
size = int(request.rel_url.query.get('size', 0))
type_key = request.rel_url.query.get('type', 'image')
- img_file = await self.mass.music.get_image_path(media_id, media_type, provider, size, type_key)
+ img_file = await self.mass.music.get_image_path(
+ media_id, media_type, provider, size, type_key)
if not img_file or not os.path.isfile(img_file):
return web.Response(status=404)
headers = {'Cache-Control': 'max-age=86400, public', 'Pragma': 'public'}
return web.FileResponse(img_file, headers=headers)
+ @routes.get('/api/artists/{artist_id}/toptracks')
async def artist_toptracks(self, request):
""" get top tracks for given artist """
artist_id = request.match_info.get('artist_id')
iterator = self.mass.music.artist_toptracks(artist_id, provider)
return await self.__stream_json(request, iterator)
+ @routes.get('/api/artists/{artist_id}/albums')
async def artist_albums(self, request):
""" get (all) albums for given artist """
artist_id = request.match_info.get('artist_id')
iterator = self.mass.music.artist_albums(artist_id, provider)
return await self.__stream_json(request, iterator)
+ @routes.get('/api/playlists/{playlist_id}/tracks')
async def playlist_tracks(self, request):
""" get playlist tracks from provider"""
playlist_id = request.match_info.get('playlist_id')
iterator = self.mass.music.playlist_tracks(playlist_id, provider)
return await self.__stream_json(request, iterator)
+ @routes.put('/api/playlists/{playlist_id}/tracks')
+ async def add_playlist_tracks(self, request):
+ """Add tracks to (editable) playlist."""
+ playlist_id = request.match_info.get('playlist_id')
+ body = await request.json()
+ tracks = await self.__media_items_from_body(body)
+ result = await self.mass.music.add_playlist_tracks(playlist_id, tracks)
+ return web.json_response(result, dumps=json_serializer)
+
+ @routes.delete('/api/playlists/{playlist_id}/tracks')
+ async def remove_playlist_tracks(self, request):
+ """Remove tracks from (editable) playlist."""
+ playlist_id = request.match_info.get('playlist_id')
+ body = await request.json()
+ tracks = await self.__media_items_from_body(body)
+ result = await self.mass.music.remove_playlist_tracks(playlist_id, tracks)
+ return web.json_response(result, dumps=json_serializer)
+
+ @routes.get('/api/albums/{album_id}/tracks')
async def album_tracks(self, request):
""" get album tracks from provider"""
album_id = request.match_info.get('album_id')
- provider = request.rel_url.query.get('provider','database')
+ provider = request.rel_url.query.get('provider', 'database')
iterator = self.mass.music.album_tracks(album_id, provider)
return await self.__stream_json(request, iterator)
+ @routes.get('/api/search')
async def search(self, request):
""" search database or providers """
searchquery = request.rel_url.query.get('query')
media_types_query = request.rel_url.query.get('media_types')
- limit = request.rel_url.query.get('media_id', 5)
+ limit = request.rel_url.query.get('limit', 5)
online = request.rel_url.query.get('online', False)
media_types = []
if not media_types_query or "artists" in media_types_query:
result = await self.mass.music.search(searchquery, media_types, limit=limit, online=online)
return web.json_response(result, dumps=json_serializer)
+ @routes.get('/api/players')
async def players(self, request):
""" get all players """
players = list(self.mass.players.players)
players.sort(key=lambda x: x.name, reverse=False)
return web.json_response(players, dumps=json_serializer)
- async def player(self, request):
- """ get single player """
- player_id = request.match_info.get('player_id')
- player = await self.mass.players.get_player(player_id)
- return web.json_response(player, dumps=json_serializer)
-
+ @routes.post('/api/players/{player_id}/cmd/{cmd}')
async def player_command(self, request):
""" issue player command"""
result = False
player_id = request.match_info.get('player_id')
player = await self.mass.players.get_player(player_id)
- if player:
- cmd = request.match_info.get('cmd')
- cmd_args = request.match_info.get('cmd_args')
- player_cmd = getattr(player, cmd, None)
- if player_cmd and cmd_args != None:
- result = await player_cmd(cmd_args)
- elif player_cmd:
- result = await player_cmd()
- else:
- LOGGER.error("Received non-existing command %s for player %s" %(cmd, player.name))
+ if not player:
+ return web.Response(text='invalid player', status=404)
+ cmd = request.match_info.get('cmd')
+ cmd_args = await request.json()
+ player_cmd = getattr(player, cmd, None)
+ if player_cmd and cmd_args is not None:
+ result = await player_cmd(cmd_args)
+ elif player_cmd:
+ result = await player_cmd()
else:
- LOGGER.error("Received command for non-existing player %s" %(player_id))
+ return web.Response(text='invalid command', status=501)
return web.json_response(result, dumps=json_serializer)
- async def play_media(self, request):
+ @routes.post('/api/players/{player_id}/play_media/{queue_opt}')
+ async def player_play_media(self, request):
""" issue player play_media command"""
player_id = request.match_info.get('player_id')
- 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')
- queue_opt = request.match_info.get('queue_opt','')
- provider = request.rel_url.query.get('provider')
- media_item = await self.mass.music.item(media_id, media_type, provider, lazy=True)
- result = await self.mass.players.play_media(player_id, media_item, queue_opt)
- return web.json_response(result, dumps=json_serializer)
+ player = await self.mass.players.get_player(player_id)
+ if not player:
+ return web.Response(status=404)
+ queue_opt = request.match_info.get('queue_opt', 'play')
+ body = await request.json()
+ media_items = await self.__media_items_from_body(body)
+ result = await self.mass.players.play_media(player_id, media_items, queue_opt)
+ return web.json_response(result, dumps=json_serializer)
+
+ @routes.get('/api/players/{player_id}/queue/{queue_item}')
+ async def player_queue_item(self, request):
+ """ return item (by index or queue item id) from the player's queue """
+ player_id = request.match_info.get('player_id')
+ item_id = request.match_info.get('queue_item')
+ player = await self.mass.players.get_player(player_id)
+ try:
+ item_id = int(item_id)
+ queue_item = await player.queue.get_item(item_id)
+ except ValueError:
+ queue_item = await player.queue.by_item_id(item_id)
+ return web.json_response(queue_item, dumps=json_serializer)
+ @routes.get('/api/players/{player_id}/queue')
async def player_queue(self, request):
""" return the items in the player's queue """
player_id = request.match_info.get('player_id')
yield item
return await self.__stream_json(request, queue_tracks_iter())
- async def player_queue_item(self, request):
- """ return item (by index or queue item id) from the player's queue """
+ @routes.get('/api/players/{player_id}')
+ async def player(self, request):
+ """ get single player """
player_id = request.match_info.get('player_id')
- item_id = request.match_info.get('item_id')
player = await self.mass.players.get_player(player_id)
- try:
- item_id = int(item_id)
- queue_item = await player.queue.get_item(item_id)
- except:
- queue_item = await player.queue.by_item_id(item_id)
- return web.json_response(queue_item, dumps=json_serializer)
-
- async def index(self, request):
- index_file = os.path.join(
- os.path.dirname(os.path.abspath(__file__)), 'web/index.html')
- return web.FileResponse(index_file)
+ if not player:
+ return web.Response(text='invalid player', status=404)
+ return web.json_response(player, dumps=json_serializer)
+
+ @routes.get('/api/config')
+ async def get_config(self, request):
+ """ get the config """
+ return web.json_response(self.mass.config)
+
+ @routes.put('/api/config/{key}/{subkey}')
+ async def put_config(self, request):
+ """ save (partial) config """
+ conf_key = request.match_info.get('key')
+ conf_subkey = request.match_info.get('subkey')
+ new_values = await request.json()
+ LOGGER.debug(f'save config called for {conf_key}/{conf_subkey} - new value: {new_values}')
+ cur_values = self.mass.config[conf_key][conf_subkey]
+ result = {"success": True, "restart_required": False, "settings_changed": False}
+ if cur_values != new_values:
+ # config changed
+ result["settings_changed"] = True
+ self.mass.config[conf_key][conf_subkey] = new_values
+ if conf_key == "player_settings":
+ # player settings don't require restart, force update of player
+ self.mass.event_loop.create_task(
+ self.mass.players.trigger_update(conf_subkey))
+ else:
+ # TODO: allow some settings without restart ?
+ result["restart_required"] = True
+ self.mass.config.save()
+ return web.json_response(result)
async def websocket_handler(self, request):
""" websockets handler """
LOGGER.warning(msg.data)
else:
data = msg.json()
- # for now we only use WS for (simple) player commands
- if data['message'] == 'players':
- players = list(self.mass.players.players)
- players.sort(key=lambda x: x.name, reverse=False)
- ws_msg = {'message': 'players', 'message_details': players}
- await ws.send_json(ws_msg, dumps=json_serializer)
- elif data['message'] == 'player command':
- player_id = data['message_details']['player_id']
- cmd = data['message_details']['cmd']
- cmd_args = data['message_details']['cmd_args']
- player = await self.mass.players.get_player(player_id)
- player_cmd = getattr(player, cmd, None)
- if player_cmd and cmd_args:
- result = await player_cmd(cmd_args)
- elif player_cmd:
- result = await player_cmd()
- else:
- # echo the websocket message on event bus
- # can be picked up by other modules, e.g. the webplayer
- await self.mass.signal_event(data['message'], data['message_details'])
+ # echo the websocket message on event bus
+ # can be picked up by other modules, e.g. the webplayer
+ await self.mass.signal_event(data['message'], data['message_details'])
except (Exception, AssertionError, asyncio.CancelledError) as exc:
LOGGER.warning("Websocket disconnected - %s" % str(exc))
finally:
LOGGER.debug('websocket connection closed')
return ws
- async def get_config(self, request):
- """ get the config """
- return web.json_response(self.mass.config)
-
- async def save_config(self, request):
- """ save (partial) config """
- conf_key = request.match_info.get('key')
- conf_subkey = request.match_info.get('subkey')
- new_values = await request.json()
- LOGGER.debug(f'save config called for {conf_key}/{conf_subkey} - new value: {new_values}')
- cur_values = self.mass.config[conf_key][conf_subkey]
- result = {"success": True, "restart_required": False, "settings_changed": False}
- if cur_values != new_values:
- # config changed
- result["settings_changed"] = True
- self.mass.config[conf_key][conf_subkey] = new_values
- if conf_key == "player_settings":
- # player settings don't require restart, force update of player
- self.mass.event_loop.create_task(
- self.mass.players.trigger_update(conf_subkey))
- else:
- # TODO: allow some settings without restart ?
- result["restart_required"] = True
- self.mass.config.save()
- return web.json_response(result)
-
- async def headers_only(self, request):
- return web.Response(status=200)
-
- async def __stream_json(self, request, iterator):
- """ stream items from async iterator as json object """
- resp = web.StreamResponse(status=200,
- reason='OK',
- headers={'Content-Type': 'application/json'})
- await resp.prepare(request)
- # write json open tag
- json_response = '{ "items": ['
- await resp.write(json_response.encode('utf-8'))
- count = 0
- async for item in iterator:
- # write each item into the items object of the json
- json_response = json_serializer(item) + ','
- await resp.write(json_response.encode('utf-8'))
- count += 1
- # write json close tag
- json_response = '], "count": %s }' % count
- await resp.write((json_response).encode('utf-8'))
- await resp.write_eof()
- return resp
-
async def json_rpc(self, request):
"""
implement LMS jsonrpc interface
else:
return web.Response(text='command not supported')
return web.Response(text='success')
-
\ No newline at end of file
+
+ async def __media_items_from_body(self, data):
+ """Helper to turn posted body data into media items."""
+ if not isinstance(data, list):
+ data = [data]
+ media_items = []
+ for item in data:
+ media_item = await self.mass.music.item(
+ item['item_id'], item['media_type'], item['provider'], lazy=True)
+ media_items.append(media_item)
+ return media_items
+
+ async def __stream_json(self, request, iterator):
+ """ stream items from async iterator as json object """
+ resp = web.StreamResponse(status=200,
+ reason='OK',
+ headers={'Content-Type': 'application/json'})
+ await resp.prepare(request)
+ # write json open tag
+ json_response = '{ "items": ['
+ await resp.write(json_response.encode('utf-8'))
+ count = 0
+ async for item in iterator:
+ # write each item into the items object of the json
+ if count:
+ json_response = ',' + json_serializer(item)
+ else:
+ json_response = json_serializer(item)
+ await resp.write(json_response.encode('utf-8'))
+ count += 1
+ # write json close tag
+ json_response = '], "count": %s }' % count
+ await resp.write((json_response).encode('utf-8'))
+ await resp.write_eof()
+ return resp