From 50ac5f6aebbae9ad9bdf5c02a2ad8667224ed032 Mon Sep 17 00:00:00 2001 From: marcelveldt Date: Sat, 19 Oct 2019 10:18:37 +0200 Subject: [PATCH] implement own player protocol --- music_assistant/__init__.py | 19 ++ music_assistant/http_streamer.py | 30 ++- music_assistant/models/playerstate.py | 10 + music_assistant/playerproviders/web.py | 111 ++++++++++- music_assistant/web.py | 51 +++--- music_assistant/web/app.js | 109 +++++++++++ music_assistant/web/components/player.vue.js | 156 ++++++++++++++-- music_assistant/web/index.html | 183 +------------------ music_assistant/web/lib/utils.js | 63 +++++++ 9 files changed, 494 insertions(+), 238 deletions(-) create mode 100644 music_assistant/web/app.js create mode 100644 music_assistant/web/lib/utils.js diff --git a/music_assistant/__init__.py b/music_assistant/__init__.py index ba048266..a9f2b920 100644 --- a/music_assistant/__init__.py +++ b/music_assistant/__init__.py @@ -57,6 +57,10 @@ class MusicAssistant(): await self.players.setup() await self.web.setup() await self.http_streamer.setup() + # temp code to chase memory leak + import subprocess + subprocess.call("pip install pympler", shell=True) + self.event_loop.create_task(self.print_memory()) def handle_exception(self, loop, context): ''' global exception handler ''' @@ -80,3 +84,18 @@ class MusicAssistant(): async def remove_event_listener(self, cb_id): ''' remove callback from our event listeners ''' self.event_listeners.pop(cb_id, None) + + @run_periodic(30) + async def print_memory(self): + + from pympler import muppy, summary + + all_objects = muppy.get_objects() + sum1 = summary.summarize(all_objects) + # Prints out a summary of the large objects + summary.print_(sum1) + # Get references to certain types of objects such as dataframe + # dataframes = [ao for ao in all_objects if isinstance(ao, pd.DataFrame)] + # for d in dataframes: + # print(d.columns.values) + # print(len(d)) diff --git a/music_assistant/http_streamer.py b/music_assistant/http_streamer.py index 1c9638d3..464aded7 100755 --- a/music_assistant/http_streamer.py +++ b/music_assistant/http_streamer.py @@ -33,21 +33,7 @@ class HTTPStreamer(): pass # self.mass.event_loop.create_task( # asyncio.start_server(self.sockets_streamer, '0.0.0.0', 8093)) - - async def webplayer(self, http_request): - ''' - start stream for a player - ''' - from .models import Player - player_id = http_request.match_info.get('player_id') - player = Player(self.mass, player_id, 'web') - player.name = player_id - await self.mass.players.add_player(player) - # wait for queue - while not player.queue.items: - await asyncio.sleep(0.2) - return await self.stream(http_request) - + async def stream(self, http_request): ''' start stream for a player @@ -58,7 +44,10 @@ class HTTPStreamer(): assert(player) # prepare headers as audio/flac content resp = web.StreamResponse(status=200, reason='OK', - headers={'Content-Type': 'audio/flac'}) + headers={ + 'Content-Type': 'audio/mp3' if player.player_provider else 'audio/flac', + 'Accept-Ranges': 'None' + }) await resp.prepare(http_request) # send content only on GET request if http_request.method.upper() != 'GET': @@ -120,12 +109,17 @@ class HTTPStreamer(): fade_length = try_parse_int(player.settings["crossfade_duration"]) if not sample_rate or sample_rate < 44100 or sample_rate > 384000: sample_rate = 96000 + elif player.player_provider == 'web': + sample_rate = 41100 if fade_length: fade_bytes = int(sample_rate * 4 * 2 * fade_length) else: fade_bytes = int(sample_rate * 4 * 2) pcm_args = 'raw -b 32 -c 2 -e signed-integer -r %s' % sample_rate - args = 'sox -t %s - -t flac -C 0 -' % pcm_args + if player.player_provider == 'web': + args = 'sox -t %s - -t flac -C 0 -' % pcm_args + else: + args = 'sox -t %s - -t mp3 -' % pcm_args # start sox process # we use normal subprocess instead of asyncio because of bug with executor # this should be fixed with python 3.8 @@ -133,7 +127,7 @@ class HTTPStreamer(): stdout=subprocess.PIPE, stdin=subprocess.PIPE) def fill_buffer(): - sample_size = int(sample_rate * 4 * 2 * 2) + sample_size = int(sample_rate * 4 * 2) while sox_proc.returncode == None: chunk = sox_proc.stdout.read(sample_size) if not chunk: diff --git a/music_assistant/models/playerstate.py b/music_assistant/models/playerstate.py index 34336119..6ca6e2e0 100755 --- a/music_assistant/models/playerstate.py +++ b/music_assistant/models/playerstate.py @@ -8,3 +8,13 @@ class PlayerState(str, Enum): Stopped = "stopped" Paused = "paused" Playing = "playing" + + # def from_string(self, string): + # if string == "off": + # return self.Off + # elif string == "stopped": + # return self.Stopped + # elif string == "paused": + # return self.Paused + # elif string == "playing": + # return self.Playing diff --git a/music_assistant/playerproviders/web.py b/music_assistant/playerproviders/web.py index 09555889..6b6af1b4 100644 --- a/music_assistant/playerproviders/web.py +++ b/music_assistant/playerproviders/web.py @@ -26,9 +26,17 @@ CONFIG_ENTRIES = [ PLAYER_CONFIG_ENTRIES = [] +EVENT_WEBPLAYER_CMD = 'webplayer command' +EVENT_WEBPLAYER_STATE = 'webplayer state' +EVENT_WEBPLAYER_REGISTER = 'webplayer register' class WebPlayerProvider(PlayerProvider): - ''' Python implementation of SlimProto server ''' + ''' + Implementation of a player using pure HTML/javascript + used in the front-end. + Communication is handled through the websocket connection + and our internal event bus + ''' def __init__(self, mass, conf): super().__init__(mass, conf) @@ -40,6 +48,103 @@ class WebPlayerProvider(PlayerProvider): async def setup(self): ''' async initialize of module ''' - pass + await self.mass.add_event_listener(self.handle_mass_event, EVENT_WEBPLAYER_STATE) + await self.mass.add_event_listener(self.handle_mass_event, EVENT_WEBPLAYER_REGISTER) + self.mass.event_loop.create_task(self.check_players()) + + async def handle_mass_event(self, msg, msg_details): + ''' received event for the webplayer component ''' + #print("%s ---> %s" %(msg, msg_details)) + if msg == EVENT_WEBPLAYER_REGISTER: + # register new player + player_id = msg_details['player_id'] + player = WebPlayer(self.mass, player_id, self.prov_id) + player.supports_crossfade = False + player.supports_gapless = False + player.supports_queue = False + player.name = msg_details['name'] + await self.add_player(player) + elif msg == EVENT_WEBPLAYER_STATE: + player_id = msg_details['player_id'] + player = await self.get_player(player_id) + if player: + await player.handle_state(msg_details) + + @run_periodic(30) + async def check_players(self): + ''' invalidate players that did not send a heartbeat message in a while ''' + cur_time = time.time() + offline_players = [] + for player in self.players: + if cur_time - player._last_message > 30: + offline_players.append(player.player_id) + for player_id in offline_players: + await self.remove_player(player_id) + + +class WebPlayer(Player): + ''' Web player object ''' + + def __init__(self, mass, player_id, prov_id): + self._last_message = time.time() + super().__init__(mass, player_id, prov_id) + + async def cmd_stop(self): + ''' send stop command to player ''' + data = { 'player_id': self.player_id, 'cmd': 'stop'} + await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def cmd_play(self): + ''' send play command to player ''' + data = { 'player_id': self.player_id, 'cmd': 'play'} + await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def cmd_pause(self): + ''' send pause command to player ''' + data = { 'player_id': self.player_id, 'cmd': 'pause'} + await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def cmd_power_on(self): + ''' send power ON command to player ''' + self.powered = True # not supported on webplayer + data = { 'player_id': self.player_id, 'cmd': 'stop'} + await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def cmd_power_off(self): + ''' send power OFF command to player ''' + self.powered = False + + async def cmd_volume_set(self, volume_level): + ''' send new volume level command to player ''' + data = { 'player_id': self.player_id, 'cmd': 'volume_set', 'volume_level': volume_level} + await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def cmd_volume_mute(self, is_muted=False): + ''' send mute command to player ''' + data = { 'player_id': self.player_id, 'cmd': 'volume_mute', 'is_muted': is_muted} + await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def cmd_play_uri(self, uri:str): + ''' play single uri on player ''' + data = { 'player_id': self.player_id, 'cmd': 'play_uri', 'uri': uri} + await self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def handle_state(self, data): + ''' handle state event from player ''' + if 'volume_level' in data: + self.volume_level = data['volume_level'] + if 'muted' in data: + self.muted = data['muted'] + if 'state' in data: + self.state = PlayerState(data['state']) + if 'cur_time' in data: + self.cur_time = data['cur_time'] + if 'cur_uri' in data: + self.cur_uri = data['cur_uri'] + if 'powered' in data: + self.powered = data['powered'] + if 'name' in data: + self.name = data['name'] + self._last_message = time.time() + - \ No newline at end of file diff --git a/music_assistant/web.py b/music_assistant/web.py index e855e9d8..10bed343 100755 --- a/music_assistant/web.py +++ b/music_assistant/web.py @@ -10,7 +10,6 @@ import ssl import concurrent import threading from .models.media_types import MediaItem, MediaType, media_type_from_string -from .models.player import Player from .utils import run_periodic, LOGGER, run_async_background_task, get_ip, json_serializer CONF_KEY = 'web' @@ -50,7 +49,6 @@ class Web(): 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)]) - app.add_routes([web.get('/stream/web/{player_id}', self.mass.http_streamer.webplayer)]) app.add_routes([web.get('/stream/{player_id}/{queue_item_id}', self.mass.http_streamer.stream)]) app.add_routes([web.get('/api/search', self.search)]) app.add_routes([web.get('/api/config', self.get_config)]) @@ -235,27 +233,34 @@ class Web(): cb_id = await self.mass.add_event_listener(send_event) # process incoming messages async for msg in ws: - if msg.type != aiohttp.WSMsgType.TEXT: - continue - # for now we only use WS for (simple) player commands - if msg.data == '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 msg.data.startswith('players') and '/cmd/' in msg.data: - # players/{player_id}/cmd/{cmd} or players/{player_id}/cmd/{cmd}/{cmd_args} - msg_data_parts = msg.data.split('/') - player_id = msg_data_parts[1] - cmd = msg_data_parts[3] - cmd_args = msg_data_parts[4] if len(msg_data_parts) == 5 else None - 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() - except (Exception, AssertionError) as exc: + if msg.type == aiohttp.WSMsgType.ERROR: + LOGGER.debug('ws connection closed with exception %s' % + ws.exception()) + elif msg.type != aiohttp.WSMsgType.TEXT: + 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']) + except (Exception, AssertionError, asyncio.CancelledError) as exc: LOGGER.warning("Websocket disconnected - %s" % str(exc)) finally: await self.mass.remove_event_listener(cb_id) diff --git a/music_assistant/web/app.js b/music_assistant/web/app.js new file mode 100644 index 00000000..c270e5a6 --- /dev/null +++ b/music_assistant/web/app.js @@ -0,0 +1,109 @@ +Vue.use(VueRouter); +Vue.use(VeeValidate); +Vue.use(Vuetify); +Vue.use(VueI18n); +Vue.use(VueLoading); +Vue.use(Toasted, {duration: 5000, fullWidth: true}); + + +const routes = [ + { + path: '/', + component: home + }, + { + path: '/config', + component: Config, + }, + { + path: '/queue/:player_id', + component: Queue, + props: route => ({ ...route.params, ...route.query }) + }, + { + path: '/artists/:media_id', + component: ArtistDetails, + props: route => ({ ...route.params, ...route.query }) + }, + { + path: '/albums/:media_id', + component: AlbumDetails, + props: route => ({ ...route.params, ...route.query }) + }, + { + path: '/tracks/:media_id', + component: TrackDetails, + props: route => ({ ...route.params, ...route.query }) + }, + { + path: '/playlists/:media_id', + component: PlaylistDetails, + props: route => ({ ...route.params, ...route.query }) + }, + { + path: '/search', + component: Search, + props: route => ({ ...route.params, ...route.query }) + }, + { + path: '/:mediatype', + component: Browse, + props: route => ({ ...route.params, ...route.query }) + }, +] + +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 + } +}) +Vue.prototype.$globals = globalStore; +Vue.prototype.isMobile = isMobile; +Vue.prototype.isInStandaloneMode = isInStandaloneMode; +Vue.prototype.toggleLibrary = toggleLibrary; +Vue.prototype.showPlayMenu = showPlayMenu; +Vue.prototype.clickItem= clickItem; + +const i18n = new VueI18n({ + locale: navigator.language.split('-')[0], + fallbackLocale: 'en', + enableInSFC: true, + messages + }) + +var app = new Vue({ + i18n, + el: '#app', + watch: {}, + mounted() { + }, + components: { + Loading: VueLoading + }, + created() { + // little hack to force refresh PWA on iOS by simple reloading it every hour + var d = new Date(); + var cur_update = d.getDay() + d.getHours(); + if (localStorage.getItem('last_update') != cur_update) + { + localStorage.setItem('last_update', cur_update); + window.location.reload(true); + } + }, + data: { }, + methods: {}, + router +}) \ No newline at end of file diff --git a/music_assistant/web/components/player.vue.js b/music_assistant/web/components/player.vue.js index b0a80b6a..c526f845 100755 --- a/music_assistant/web/components/player.vue.js +++ b/music_assistant/web/components/player.vue.js @@ -81,7 +81,7 @@ Vue.component("player", { - + speaker {{ active_player_id ? players[active_player_id].name : '' }} @@ -93,8 +93,6 @@ Vue.component("player", { - - @@ -151,11 +149,18 @@ Vue.component("player", { menu: false, players: {}, active_player_id: "", - ws: null + ws: null, + file: "", + audioPlayer: null, + audioPlayerId: '', + audioPlayerName: '' } }, - mounted() { }, + mounted() { + + }, created() { + // connect the websocket this.connectWS(); }, computed: { @@ -196,10 +201,12 @@ Vue.component("player", { }, methods: { playerCommand (cmd, cmd_opt=null, player_id=this.active_player_id) { - if (cmd_opt) - cmd = cmd + '/' + cmd_opt - cmd = 'players/' + player_id + '/cmd/' + cmd; - this.ws.send(cmd); + let msg_details = { + player_id: player_id, + cmd: cmd, + cmd_args: cmd_opt + } + this.ws.send(JSON.stringify({message:'player command', message_details: msg_details})); }, playItem(item, queueopt) { console.log('playItem: ' + item); @@ -244,6 +251,110 @@ Vue.component("player", { else this.playerCommand('power_on', null, player_id); }, + handleAudioPlayerCommand(data) { + /// we received a command for our built-in audio player + if (data.cmd == 'play') + this.audioPlayer.play(); + else if (data.cmd == 'pause') + this.audioPlayer.pause(); + else if (data.cmd == 'stop') + { + console.log('stop called'); + this.audioPlayer.pause(); + this.audioPlayer = new Audio(); + let msg_details = { + player_id: this.audioPlayerId, + state: 'stopped' + } + this.ws.send(JSON.stringify({message:'webplayer state', message_details: msg_details})); + } + else if (data.cmd == 'volume_set') + this.audioPlayer.volume = data.volume_level/100; + else if (data.cmd == 'volume_mute') + this.audioPlayer.mute = data.is_muted; + else if (data.cmd == 'play_uri') + { + this.audioPlayer.src = data.uri; + this.audioPlayer.load(); + } + }, + createAudioPlayer(data) { + if (localStorage.getItem('audio_player_id')) + // get player id from local storage + this.audioPlayerId = localStorage.getItem('audio_player_id'); + else + { + // generate a new (randomized) player id + this.audioPlayerId = (Date.now().toString(36) + Math.random().toString(36).substr(2, 5)).toUpperCase(); + localStorage.setItem('audio_player_id', this.audioPlayerId); + } + this.audioPlayerName = 'Webplayer ' + this.audioPlayerId.substring(1, 4); + this.audioPlayer = new Audio(); + this.audioPlayer.autoplay = false; + this.audioPlayer.preload = 'none'; + let msg_details = { + player_id: this.audioPlayerId, + name: this.audioPlayerName, + state: 'stopped', + powered: true, + volume_level: this.audioPlayer.volume * 100, + muted: this.audioPlayer.muted, + cur_uri: this.audioPlayer.src + } + // register the player on the server + this.ws.send(JSON.stringify({message:'webplayer register', message_details: msg_details})); + // add event handlers + this.audioPlayer.addEventListener("canplaythrough", event => { + /* the audio is now playable; play it if permissions allow */ + console.log("canplaythrough") + this.audioPlayer.play(); + }); + this.audioPlayer.addEventListener("canplay", event => { + /* the audio is now playable; play it if permissions allow */ + console.log("canplay"); + //this.audioPlayer.play(); + //msg_details['cur_uri'] = this.audioPlayer.src; + //this.ws.send(JSON.stringify({message:'webplayer state', message_details: msg_details})); + }); + this.audioPlayer.addEventListener("emptied", event => { + /* the audio is now playable; play it if permissions allow */ + console.log("emptied"); + //this.audioPlayer.play(); + }); + const timeupdateHandler = (event) => { + // currenTime of player updated, sent state (throttled at 1 sec) + msg_details['cur_time'] = Math.round(this.audioPlayer.currentTime); + this.ws.send(JSON.stringify({message:'webplayer state', message_details: msg_details})); + } + const throttledTimeUpdateHandler = this.throttle(timeupdateHandler, 1000); + this.audioPlayer.addEventListener("timeupdate",throttledTimeUpdateHandler); + + this.audioPlayer.addEventListener("volumechange", event => { + /* the audio is now playable; play it if permissions allow */ + console.log('volume: ' + this.audioPlayer.volume); + msg_details['volume_level'] = this.audioPlayer.volume*100; + msg_details['muted'] = this.audioPlayer.muted; + this.ws.send(JSON.stringify({message:'webplayer state', message_details: msg_details})); + }); + this.audioPlayer.addEventListener("playing", event => { + msg_details['state'] = 'playing'; + this.ws.send(JSON.stringify({message:'webplayer state', message_details: msg_details})); + }); + this.audioPlayer.addEventListener("pause", event => { + msg_details['state'] = 'paused'; + this.ws.send(JSON.stringify({message:'webplayer state', message_details: msg_details})); + }); + this.audioPlayer.addEventListener("ended", event => { + msg_details['state'] = 'stopped'; + this.ws.send(JSON.stringify({message:'webplayer state', message_details: msg_details})); + }); + const heartbeatMessage = (event) => { + // heartbeat message + this.ws.send(JSON.stringify({message:'webplayer state', message_details: msg_details})); + } + setInterval(heartbeatMessage, 5000); + + }, connectWS() { var loc = window.location, new_uri; if (loc.protocol === "https:") { @@ -257,12 +368,14 @@ Vue.component("player", { this.ws.onopen = function() { console.log('websocket connected!'); - this.ws.send('players'); + this.createAudioPlayer(); + data = JSON.stringify({message:'players', message_details: null}); + this.ws.send(data); }.bind(this); this.ws.onmessage = function(e) { var msg = JSON.parse(e.data); - if (msg.message == 'player changed') + if (msg.message == 'player changed' || msg.message == 'player added') { Vue.set(this.players, msg.message_details.player_id, msg.message_details); } @@ -271,12 +384,13 @@ Vue.component("player", { } else if (msg.message == 'players') { for (var item of msg.message_details) { - console.log("new player: " + item.player_id); Vue.set(this.players, item.player_id, item); } } - else - console.log(msg); + else if (msg.message == 'webplayer command' && msg.message_details.player_id == this.audioPlayerId) { + // message for our audio player + this.handleAudioPlayerCommand(msg.message_details); + } // select new active player // TODO: store previous player in local storage @@ -309,6 +423,18 @@ Vue.component("player", { console.error('Socket encountered error: ', err.message, 'Closing socket'); this.ws.close(); }.bind(this); - } + }, + throttle (callback, limit) { + var wait = false; + return function () { + if (!wait) { + callback.apply(null, arguments); + wait = true; + setTimeout(function () { + wait = false; + }, limit); + } + } + } } }) diff --git a/music_assistant/web/index.html b/music_assistant/web/index.html index dcef414f..14b39713 100755 --- a/music_assistant/web/index.html +++ b/music_assistant/web/index.html @@ -40,75 +40,11 @@ - - - + + + @@ -132,118 +68,7 @@ - - + \ No newline at end of file diff --git a/music_assistant/web/lib/utils.js b/music_assistant/web/lib/utils.js new file mode 100644 index 00000000..e078d26d --- /dev/null +++ b/music_assistant/web/lib/utils.js @@ -0,0 +1,63 @@ +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 clickItem (item) { + 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); + return; + } + else if (item.media_type == 4) + endpoint = "/playlists/" + item_id = item.item_id.toString(); + var url = endpoint + item_id; + router.push({ path: url, query: {provider: item.provider}}); +} + +String.prototype.formatDuration = function () { + var sec_num = parseInt(this, 10); // don't forget the second param + var hours = Math.floor(sec_num / 3600); + var minutes = Math.floor((sec_num - (hours * 3600)) / 60); + var seconds = sec_num - (hours * 3600) - (minutes * 60); + + if (hours < 10) {hours = "0"+hours;} + if (minutes < 10) {minutes = "0"+minutes;} + if (seconds < 10) {seconds = "0"+seconds;} + if (hours == '00') + return minutes+':'+seconds; + else + return hours+':'+minutes+':'+seconds; +} +function toggleLibrary (item) { + var endpoint = "/api/" + item.media_type + "/"; + item_id = item.item_id.toString(); + var action = "/library_remove" + if (item.in_library.length == 0) + action = "/library_add" + var url = endpoint + item_id + action; + console.log('loading ' + url); + axios + .get(url, { params: { provider: item.provider }}) + .then(result => { + data = result.data; + console.log(data); + if (action == "/library_remove") + item.in_library = [] + else + item.in_library = [provider] + }) + .catch(error => { + console.log("error", error); + }); + +}; -- 2.34.1