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 '''
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))
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
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':
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
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:
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
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)
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
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'
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)])
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)
--- /dev/null
+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
<!-- active player btn -->
<v-list-tile-action style="padding:30px;margin-right:-13px;">
- <v-btn x-small flat icon @click="menu = !menu">
+ <v-btn x-small flat icon @click="menu = !menu;createAudioPlayer();">
<v-flex xs12 class="vertical-btn">
<v-icon>speaker</v-icon>
<span class="caption">{{ active_player_id ? players[active_player_id].name : '' }}</span>
<!-- add some additional whitespace in standalone mode only -->
<v-list-tile avatar ripple style="height:14px" v-if="isInStandaloneMode()"/>
-
-
</v-card>
</v-footer>
menu: false,
players: {},
active_player_id: "",
- ws: null
+ ws: null,
+ file: "",
+ audioPlayer: null,
+ audioPlayerId: '',
+ audioPlayerName: ''
}
},
- mounted() { },
+ mounted() {
+
+ },
created() {
+ // connect the websocket
this.connectWS();
},
computed: {
},
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);
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:") {
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);
}
}
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
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);
+ }
+ }
+ }
}
})
<script src="https://unpkg.com/vee-validate@2.0.0-rc.25/dist/vee-validate.js"></script>
<script src="./lib/vue-loading-overlay.js"></script>
<script src="https://unpkg.com/vue-toasted"></script>
-
-
- <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 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);
- });
-
- };
- </script>
+ <script src="https://requirejs.org/docs/release/2.3.5/minified/require.js"></script>
+ <script src="./lib/utils.js"></script>
<!-- Vue Pages and Components here -->
+
<script src='./pages/home.vue.js'></script>
<script src='./pages/browse.vue.js'></script>
<script src='./components/searchbox.vue.js'></script>
<script src='./strings.js'></script>
-
- <script>
- 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
- })
- </script>
+ <script src='./app.js'></script>
</body>
</html>
\ No newline at end of file
--- /dev/null
+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);
+ });
+
+};