# -*- coding: utf-8 -*- from __future__ import division, absolute_import, print_function, unicode_literals ################################################################################################# import binascii import json import threading from kodi_six import xbmc from . import connect from . import player from .client import get_device_id from .objects import PlaylistWorker, on_play, on_update, special_listener from .helper import translate, settings, window, dialog, api, JSONRPC from .helper.utils import JsonDebugPrinter from .jellyfin import Jellyfin from .helper import LazyLogger ################################################################################################# LOG = LazyLogger(__name__) ################################################################################################# class Monitor(xbmc.Monitor): servers = [] sleep = False def __init__(self): self.player = player.Player() self.device_id = get_device_id() self.listener = Listener(self) self.listener.start() xbmc.Monitor.__init__(self) def onScanStarted(self, library): LOG.info("-->[ kodi scan/%s ]", library) def onScanFinished(self, library): LOG.info("--<[ kodi scan/%s ]", library) def onNotification(self, sender, method, data): if sender.lower() not in ('plugin.video.jellyfin', 'xbmc', 'upnextprovider.signal'): return if sender == 'plugin.video.jellyfin': method = method.split('.')[1] if method not in ('ReportProgressRequested', 'LoadServer', 'AddUser', 'PlayPlaylist', 'Play', 'Playstate', 'GeneralCommand'): return data = json.loads(data)[0] elif sender.startswith('upnextprovider'): LOG.info('Attempting to play the next episode via upnext') method = method.split('.', 1)[1] if method not in ('plugin.video.jellyfin_play_action',): LOG.info('Received invalid upnext method: %s', method) return data = json.loads(data) method = "Play" if data: data = json.loads(binascii.unhexlify(data[0])) else: if method not in ('Player.OnPlay', 'VideoLibrary.OnUpdate', 'Player.OnAVChange'): ''' We have to clear the playlist if it was stopped before it has been played completely. Otherwise the next played item will be added the previous queue. ''' if method == "Player.OnStop": xbmc.sleep(3000) # let's wait for the player, so we don't clear the canceled playlist by mistake. if xbmc.getCondVisibility("!Player.HasMedia + !Window.IsVisible(busydialog)"): xbmc.executebuiltin("Playlist.Clear") LOG.info("[ playlist ] cleared") return data = json.loads(data) LOG.debug("[ %s: %s ] %s", sender, method, JsonDebugPrinter(data)) if self.sleep: LOG.info("System.OnSleep detected, ignore monitor request.") return try: if not data.get('ServerId'): server = Jellyfin() else: if method != 'LoadServer' and data['ServerId'] not in self.servers: try: connect.Connect().register(data['ServerId']) self.server_instance(data['ServerId']) except Exception as error: LOG.exception(error) dialog("ok", "{jellyfin}", translate(33142)) return server = Jellyfin(data['ServerId']) except Exception as error: LOG.exception(error) server = Jellyfin() server = server.get_client() if method == 'Play': items = server.jellyfin.get_items(data['ItemIds']) PlaylistWorker(data.get('ServerId'), items, data['PlayCommand'] == 'PlayNow', data.get('StartPositionTicks', 0), data.get('AudioStreamIndex'), data.get('SubtitleStreamIndex')).start() # TODO no clue if this is called by anything elif method == 'PlayPlaylist': server.jellyfin.post_session(server.config.data['app.session'], "Playing", { 'PlayCommand': "PlayNow", 'ItemIds': data['Id'], 'StartPositionTicks': 0 }) elif method in ('ReportProgressRequested', 'Player.OnAVChange'): self.player.report_playback(data.get('Report', True)) elif method == 'Playstate': self.playstate(data) elif method == 'GeneralCommand': self.general_commands(data) elif method == 'LoadServer': self.server_instance(data['ServerId']) elif method == 'AddUser': server.jellyfin.session_add_user(server.config.data['app.session'], data['Id'], data['Add']) self.additional_users(server) elif method == 'Player.OnPlay': on_play(data, server) elif method == 'VideoLibrary.OnUpdate': on_update(data, server) def server_instance(self, server_id=None): server = Jellyfin(server_id).get_client() session = server.jellyfin.get_device(self.device_id) server.config.data['app.session'] = session[0]['Id'] if server_id is not None: self.servers.append(server_id) elif settings('additionalUsers'): users = settings('additionalUsers').split(',') all_users = server.jellyfin.get_users() for additional in users: for user in all_users: if user['Name'].lower() in additional.lower(): server.jellyfin.session_add_user(server.config.data['app.session'], user['Id'], True) self.additional_users(server) def additional_users(self, server): ''' Setup additional users images. ''' for i in range(10): window('JellyfinAdditionalUserImage.%s' % i, clear=True) try: session = server.jellyfin.get_device(self.device_id) except Exception as error: LOG.exception(error) return for index, user in enumerate(session[0]['AdditionalUsers']): info = server.jellyfin.get_user(user['UserId']) image = api.API(info, server.config.data['auth.server']).get_user_artwork(user['UserId']) window('JellyfinAdditionalUserImage.%s' % index, image) window('JellyfinAdditionalUserPosition.%s' % user['UserId'], str(index)) def playstate(self, data): ''' Jellyfin playstate updates. ''' command = data['Command'] actions = { 'Stop': self.player.stop, 'Unpause': self.player.pause, 'Pause': self.player.pause, 'PlayPause': self.player.pause, 'NextTrack': self.player.playnext, 'PreviousTrack': self.player.playprevious } if command == 'Seek': if self.player.isPlaying(): seektime = data['SeekPositionTicks'] / 10000000.0 self.player.seekTime(seektime) LOG.info("[ seek/%s ]", seektime) elif command in actions: actions[command]() LOG.info("[ command/%s ]", command) def general_commands(self, data): ''' General commands from Jellyfin to control the Kodi interface. ''' command = data['Name'] args = data['Arguments'] if command in ('Mute', 'Unmute', 'SetVolume', 'SetSubtitleStreamIndex', 'SetAudioStreamIndex', 'SetRepeatMode'): if command in ['Mute', 'Unmute']: xbmc.executebuiltin('Mute') elif command == 'SetAudioStreamIndex': self.player.set_audio_subs(args['Index']) elif command == 'SetRepeatMode': xbmc.executebuiltin('xbmc.PlayerControl(%s)' % args['RepeatMode']) elif command == 'SetSubtitleStreamIndex': self.player.set_audio_subs(None, args['Index']) elif command == 'SetVolume': xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % args['Volume']) # Kodi needs a bit of time to update its current status xbmc.sleep(500) self.player.report_playback() elif command == 'DisplayMessage': dialog("notification", heading=args['Header'], message=args['Text'], icon="{jellyfin}", time=int(settings('displayMessage')) * 1000) elif command == 'SendString': JSONRPC('Input.SendText').execute({'text': args['String'], 'done': False}) elif command == 'GoHome': JSONRPC('GUI.ActivateWindow').execute({'window': "home"}) elif command == 'Guide': JSONRPC('GUI.ActivateWindow').execute({'window': "tvguide"}) elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'): actions = { 'MoveUp': "Input.Up", 'MoveDown': "Input.Down", 'MoveRight': "Input.Right", 'MoveLeft': "Input.Left" } JSONRPC(actions[command]).execute() else: builtin = { 'ToggleFullscreen': 'Action(FullScreen)', 'ToggleOsdMenu': 'Action(OSD)', 'ToggleContextMenu': 'Action(ContextMenu)', 'Select': 'Action(Select)', 'Back': 'Action(back)', 'PageUp': 'Action(PageUp)', 'NextLetter': 'Action(NextLetter)', 'GoToSearch': 'VideoLibrary.Search', 'GoToSettings': 'ActivateWindow(Settings)', 'PageDown': 'Action(PageDown)', 'PreviousLetter': 'Action(PrevLetter)', 'TakeScreenshot': 'TakeScreenshot', 'ToggleMute': 'Mute', 'VolumeUp': 'Action(VolumeUp)', 'VolumeDown': 'Action(VolumeDown)', } if command in builtin: xbmc.executebuiltin(builtin[command]) class Listener(threading.Thread): stop_thread = False def __init__(self, monitor): self.monitor = monitor threading.Thread.__init__(self) def run(self): ''' Detect the resume dialog for widgets. Detect external players. ''' LOG.info("--->[ listener ]") while not self.stop_thread: special_listener() if self.monitor.waitForAbort(0.5): # Abort was requested while waiting. We should exit break LOG.info("---<[ listener ]") def stop(self): self.stop_thread = True