jellyfin-kodi/jellyfin_kodi/monitor.py

363 lines
11 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function, unicode_literals
#################################################################################################
import binascii
import json
import threading
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