jellyfin-kodi/jellyfin_kodi/entrypoint/default.py

1335 lines
40 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function, unicode_literals
#################################################################################################
import json
import sys
import os
from urllib.parse import parse_qsl, urlencode
import xbmc
import xbmcvfs
import xbmcgui
import xbmcplugin
import xbmcaddon
from .. import client
from ..database import reset, get_sync, Database, jellyfin_db, get_credentials
from ..objects import Objects, Actions
from ..helper import (
translate,
event,
settings,
window,
dialog,
api,
JSONRPC,
LazyLogger,
)
from ..helper.utils import JsonDebugPrinter, translate_path, kodi_version, path_replacements
from ..jellyfin import Jellyfin
#################################################################################################
LOG = LazyLogger(__name__)
ADDON_BASE_URL = sys.argv[0]
try:
PROCESS_HANDLE = int(sys.argv[1])
QUERY_STRING = sys.argv[2]
except IndexError:
pass
#################################################################################################
class Events(object):
def __init__(self):
"""Parse the parameters. Reroute to our service.py
where user is fully identified already.
"""
base_url = ADDON_BASE_URL
path = QUERY_STRING
try:
params = dict(parse_qsl(path[1:]))
except Exception:
params = {}
mode = params.get("mode")
server = params.get("server")
if server == "None":
server = None
jellyfin_client = Jellyfin(server).get_client()
api_client = jellyfin_client.jellyfin
addon_data = translate_path(
"special://profile/addon_data/plugin.video.jellyfin/data.json"
)
try:
with open(addon_data, "rb") as infile:
data = json.load(infile)
server_data = data["Servers"][0]
api_client.config.data["auth.server"] = server_data.get("address")
api_client.config.data["auth.server-name"] = server_data.get("Name")
api_client.config.data["auth.user_id"] = server_data.get("UserId")
api_client.config.data["auth.token"] = server_data.get("AccessToken")
except Exception as e:
LOG.warning("Addon appears to not be configured yet: {}".format(e))
LOG.info("path: %s params: %s", path, JsonDebugPrinter(params))
if "/extrafanart" in base_url:
jellyfin_path = path[1:]
jellyfin_id = params.get("id")
get_fanart(jellyfin_id, jellyfin_path, server, api_client)
elif "/Extras" in base_url or "/VideoFiles" in base_url:
jellyfin_path = path[1:]
jellyfin_id = params.get("id")
get_video_extras(jellyfin_id, jellyfin_path, server, api_client)
elif mode == "play":
item = api_client.get_item(params["id"])
item["resumePlayback"] = sys.argv[3].split(":")[1] == "true"
Actions(server, api_client).play(
item,
params.get("dbid"),
params.get("transcode") == "true",
playlist=params.get("playlist") == "true",
)
elif mode == "playlist":
api_client.post_session(
api_client.config.data["app.session"],
"Playing",
{
"PlayCommand": "PlayNow",
"ItemIds": params["id"],
"StartPositionTicks": 0,
},
)
elif mode == "deviceid":
client.reset_device_id()
elif mode == "reset":
reset()
elif mode == "delete":
delete_item()
elif mode == "refreshboxsets":
event("SyncLibrary", {"Id": "Boxsets:Refresh"})
elif mode == "nextepisodes":
get_next_episodes(params["id"], params["limit"])
elif mode == "browse":
browse(
params.get("type"),
params.get("id"),
params.get("folder"),
server,
api_client,
)
elif mode == "synclib":
event("SyncLibrary", {"Id": params.get("id")})
elif mode == "updatelib":
event("SyncLibrary", {"Id": params.get("id"), "Update": True})
elif mode == "repairlib":
event("RepairLibrary", {"Id": params.get("id")})
elif mode == "removelib":
event("RemoveLibrary", {"Id": params.get("id")})
elif mode == "repairlibs":
event("RepairLibrarySelection")
elif mode == "updatelibs":
event("SyncLibrarySelection")
elif mode == "removelibs":
event("RemoveLibrarySelection")
elif mode == "addlibs":
event("AddLibrarySelection")
elif mode == "addserver":
event("AddServer")
elif mode == "login":
event("ServerConnect", {"Id": server})
elif mode == "removeserver":
event("RemoveServer", {"Id": server})
elif mode == "settings":
xbmc.executebuiltin("Addon.OpenSettings(plugin.video.jellyfin)")
elif mode == "adduser":
add_user(api_client)
elif mode == "updatepassword":
event("UpdatePassword")
elif mode == "thememedia":
get_themes(api_client)
elif mode == "managelibs":
manage_libraries()
elif mode == "managepaths":
path_replacements()
elif mode == "backup":
backup()
elif mode == "restartservice":
window("jellyfin.restart.bool", True)
elif (
mode is None
and not params
and base_url != "plugin://plugin.video.jellyfin/"
):
# Used when selecting "Browse" from a context menu, see #548
item_id = base_url.strip("/").split("/")[-1]
browse("", item_id, None, server, api_client)
else:
listing()
def listing():
"""Display all jellyfin nodes and dynamic entries when appropriate."""
total = int(window("Jellyfin.nodes.total") or 0)
sync = get_sync()
whitelist = [x.replace("Mixed:", "") for x in sync["Whitelist"]]
servers = get_credentials()["Servers"][1:]
for i in range(total):
window_prop = "Jellyfin.nodes.%s" % i
path = window("%s.index" % window_prop)
if not path:
path = window("%s.content" % window_prop) or window("%s.path" % window_prop)
label = window("%s.title" % window_prop)
node = window("%s.type" % window_prop)
artwork = window("%s.artwork" % window_prop)
view_id = window("%s.id" % window_prop)
context = []
if (
view_id
and node in ("movies", "tvshows", "musicvideos", "music", "mixed")
and view_id not in whitelist
):
label = "%s %s" % (label, translate(33166))
context.append(
(
translate(33123),
"RunPlugin(plugin://plugin.video.jellyfin/?mode=synclib&id=%s)"
% view_id,
)
)
if (
view_id
and node in ("movies", "tvshows", "musicvideos", "music")
and view_id in whitelist
):
context.append(
(
translate(33136),
"RunPlugin(plugin://plugin.video.jellyfin/?mode=updatelib&id=%s)"
% view_id,
)
)
context.append(
(
translate(33132),
"RunPlugin(plugin://plugin.video.jellyfin/?mode=repairlib&id=%s)"
% view_id,
)
)
context.append(
(
translate(33133),
"RunPlugin(plugin://plugin.video.jellyfin/?mode=removelib&id=%s)"
% view_id,
)
)
LOG.debug("--[ listing/%s/%s ] %s", node, label, path)
if path:
directory(label, path, artwork=artwork, context=context)
for server in servers:
context = []
if server.get("ManualAddress"):
context.append(
(
translate(33141),
"RunPlugin(plugin://plugin.video.jellyfin/?mode=removeserver&server=%s)"
% server["Id"],
)
)
if "AccessToken" not in server:
directory(
"%s (%s)" % (server["Name"], translate(30539)),
"plugin://plugin.video.jellyfin/?mode=login&server=%s" % server["Id"],
False,
context=context,
)
else:
directory(
server["Name"],
"plugin://plugin.video.jellyfin/?mode=browse&server=%s" % server["Id"],
context=context,
)
directory(translate(33194), "plugin://plugin.video.jellyfin/?mode=managelibs", True)
directory(translate(33203), "plugin://plugin.video.jellyfin/?mode=managepaths", True)
directory(translate(33134), "plugin://plugin.video.jellyfin/?mode=addserver", False)
directory(translate(33054), "plugin://plugin.video.jellyfin/?mode=adduser", False)
directory(translate(5), "plugin://plugin.video.jellyfin/?mode=settings", False)
directory(
translate(33161), "plugin://plugin.video.jellyfin/?mode=updatepassword", False
)
directory(translate(33058), "plugin://plugin.video.jellyfin/?mode=reset", False)
directory(
translate(33180), "plugin://plugin.video.jellyfin/?mode=restartservice", False
)
if settings("backupPath"):
directory(
translate(33092), "plugin://plugin.video.jellyfin/?mode=backup", False
)
xbmcplugin.setContent(PROCESS_HANDLE, "files")
xbmcplugin.endOfDirectory(PROCESS_HANDLE)
def directory(label, path, folder=True, artwork=None, fanart=None, context=None):
"""Add directory listitem. context should be a list of tuples [(label, action)*]"""
li = dir_listitem(label, path, artwork, fanart)
if context:
li.addContextMenuItems(context)
xbmcplugin.addDirectoryItem(PROCESS_HANDLE, path, li, folder)
return li
def dir_listitem(label, path, artwork=None, fanart=None):
"""Gets the icon paths for default node listings"""
li = xbmcgui.ListItem(label, path=path)
li.setArt(
{
"thumb": artwork
or "special://home/addons/plugin.video.jellyfin/resources/icon.png",
"fanart": fanart
or "special://home/addons/plugin.video.jellyfin/resources/fanart.png",
"landscape": artwork
or fanart
or "special://home/addons/plugin.video.jellyfin/resources/fanart.png",
}
)
return li
def manage_libraries():
directory(
translate(33098), "plugin://plugin.video.jellyfin/?mode=refreshboxsets", False
)
directory(translate(33154), "plugin://plugin.video.jellyfin/?mode=addlibs", False)
directory(
translate(33139), "plugin://plugin.video.jellyfin/?mode=updatelibs", False
)
directory(
translate(33140), "plugin://plugin.video.jellyfin/?mode=repairlibs", False
)
directory(
translate(33184), "plugin://plugin.video.jellyfin/?mode=removelibs", False
)
directory(
translate(33060), "plugin://plugin.video.jellyfin/?mode=thememedia", False
)
xbmcplugin.setContent(PROCESS_HANDLE, "files")
xbmcplugin.endOfDirectory(PROCESS_HANDLE)
def browse(media, view_id=None, folder=None, server_id=None, api_client=None):
"""Browse content dynamically."""
LOG.info("--[ v:%s/%s ] %s", view_id, media, folder)
if not window("jellyfin_online.bool") and server_id is None:
monitor = xbmc.Monitor()
for _i in range(300):
if window("jellyfin_online.bool"):
break
elif monitor.waitForAbort(0.1):
return
else:
LOG.error("Default server is not online.")
return
folder = folder.lower() if folder else None
if folder is None and media in ("homevideos", "movies", "books", "audiobooks"):
return browse_subfolders(media, view_id, server_id)
if folder and folder == "firstletter":
return browse_letters(media, view_id, server_id)
if view_id:
view = api_client.get_item(view_id)
xbmcplugin.setPluginCategory(PROCESS_HANDLE, view["Name"])
content_type = "files"
if media in (
"tvshows",
"seasons",
"episodes",
"movies",
"musicvideos",
"songs",
"albums",
):
content_type = media
elif media in ("homevideos", "photos"):
content_type = "images"
elif media in ("books", "audiobooks"):
content_type = "videos"
elif media == "music":
content_type = "artists"
if folder == "recentlyadded":
listing = api_client.get_recently_added(None, view_id, None)
elif folder == "genres":
listing = api_client.get_genres(view_id)
elif media == "livetv":
listing = api_client.get_channels()
elif folder == "unwatched":
listing = get_filtered_section(
view_id,
None,
None,
None,
None,
None,
["IsUnplayed"],
None,
server_id,
api_client,
)
elif folder == "favorite":
listing = get_filtered_section(
view_id,
None,
None,
None,
None,
None,
["IsFavorite"],
None,
server_id,
api_client,
)
elif folder == "inprogress":
listing = get_filtered_section(
view_id,
None,
None,
None,
None,
None,
["IsResumable"],
None,
server_id,
api_client,
)
elif folder == "boxsets":
listing = get_filtered_section(
view_id,
get_media_type("boxsets"),
None,
True,
None,
None,
None,
None,
server_id,
api_client,
)
elif folder == "random":
listing = get_filtered_section(
view_id,
get_media_type(content_type),
25,
True,
"Random",
None,
None,
None,
server_id,
api_client,
)
elif (folder or "").startswith("firstletter-"):
listing = get_filtered_section(
view_id,
get_media_type(content_type),
None,
None,
None,
None,
None,
{"NameStartsWith": folder.split("-")[1]},
server_id,
api_client,
)
elif (folder or "").startswith("genres-"):
listing = get_filtered_section(
view_id,
get_media_type(content_type),
None,
None,
None,
None,
None,
{"GenreIds": folder.split("-")[1]},
server_id,
api_client,
)
elif folder == "favepisodes":
listing = get_filtered_section(
None,
get_media_type(content_type),
25,
None,
None,
None,
["IsFavorite"],
None,
server_id,
api_client,
)
elif folder and media == "playlists":
listing = get_filtered_section(
folder,
get_media_type(content_type),
None,
False,
"None",
None,
None,
None,
server_id,
api_client,
)
elif media == "homevideos":
listing = get_filtered_section(
folder or view_id,
get_media_type(content_type),
None,
False,
None,
None,
None,
None,
server_id,
api_client,
)
elif media in ["movies", "episodes"]:
listing = get_filtered_section(
folder or view_id,
get_media_type(content_type),
None,
True,
None,
None,
None,
None,
server_id,
api_client,
)
elif media in ("boxset", "library"):
listing = get_filtered_section(
folder or view_id,
None,
None,
True,
None,
None,
None,
None,
server_id,
api_client,
)
elif media == "boxsets":
listing = get_filtered_section(
folder or view_id,
None,
None,
False,
None,
None,
["Boxsets"],
None,
server_id,
api_client,
)
elif media == "tvshows":
listing = get_filtered_section(
folder or view_id,
get_media_type(content_type),
None,
True,
None,
None,
None,
None,
server_id,
api_client,
)
elif media == "seasons":
listing = api_client.get_seasons(folder)
elif media != "files":
listing = get_filtered_section(
folder or view_id,
get_media_type(content_type),
None,
False,
None,
None,
None,
None,
server_id,
api_client,
)
else:
listing = get_filtered_section(
folder or view_id,
None,
None,
False,
None,
None,
None,
None,
server_id,
api_client,
)
if listing:
actions = Actions(server_id, api_client)
list_li = []
listing = listing if isinstance(listing, list) else listing.get("Items", [])
for item in listing:
li = xbmcgui.ListItem()
li.setProperty("jellyfinid", item["Id"])
li.setProperty("jellyfinserver", server_id)
actions.set_listitem(item, li)
if item.get("IsFolder"):
params = {
"id": view_id or item["Id"],
"mode": "browse",
"type": get_folder_type(item, media) or media,
"folder": item["Id"],
"server": server_id,
}
path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params))
context = []
if item["Type"] in ("Series", "Season", "Playlist"):
context.append(
(
"Play",
"RunPlugin(plugin://plugin.video.jellyfin/?mode=playlist&id=%s&server=%s)"
% (item["Id"], server_id),
)
)
if item["UserData"]["Played"]:
context.append(
(
translate(16104),
"RunPlugin(plugin://plugin.video.jellyfin/?mode=unwatched&id=%s&server=%s)"
% (item["Id"], server_id),
)
)
else:
context.append(
(
translate(16103),
"RunPlugin(plugin://plugin.video.jellyfin/?mode=watched&id=%s&server=%s)"
% (item["Id"], server_id),
)
)
li.addContextMenuItems(context)
list_li.append((path, li, True))
elif item["Type"] == "Genre":
params = {
"id": view_id or item["Id"],
"mode": "browse",
"type": get_folder_type(item, media) or media,
"folder": "genres-%s" % item["Id"],
"server": server_id,
}
path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params))
list_li.append((path, li, True))
else:
if item["Type"] not in ("Photo", "PhotoAlbum"):
params = {"id": item["Id"], "mode": "play", "server": server_id}
path = "%s?%s" % (
"plugin://plugin.video.jellyfin/",
urlencode(params),
)
li.setProperty("path", path)
context = [
(
translate(13412),
"RunPlugin(plugin://plugin.video.jellyfin/?mode=playlist&id=%s&server=%s)"
% (item["Id"], server_id),
)
]
if item["UserData"]["Played"]:
context.append(
(
translate(16104),
"RunPlugin(plugin://plugin.video.jellyfin/?mode=unwatched&id=%s&server=%s)"
% (item["Id"], server_id),
)
)
else:
context.append(
(
translate(16103),
"RunPlugin(plugin://plugin.video.jellyfin/?mode=watched&id=%s&server=%s)"
% (item["Id"], server_id),
)
)
li.addContextMenuItems(context)
list_li.append((li.getProperty("path"), li, False))
xbmcplugin.addDirectoryItems(PROCESS_HANDLE, list_li, len(list_li))
if content_type == "images":
xbmcplugin.addSortMethod(PROCESS_HANDLE, xbmcplugin.SORT_METHOD_VIDEO_TITLE)
xbmcplugin.addSortMethod(PROCESS_HANDLE, xbmcplugin.SORT_METHOD_DATE)
xbmcplugin.addSortMethod(PROCESS_HANDLE, xbmcplugin.SORT_METHOD_VIDEO_RATING)
xbmcplugin.addSortMethod(PROCESS_HANDLE, xbmcplugin.SORT_METHOD_VIDEO_RUNTIME)
xbmcplugin.setContent(PROCESS_HANDLE, content_type)
xbmcplugin.endOfDirectory(PROCESS_HANDLE)
def browse_subfolders(media, view_id, server_id=None):
"""Display submenus for jellyfin views."""
from ..views import DYNNODES
view = Jellyfin(server_id).get_client().jellyfin.get_item(view_id)
xbmcplugin.setPluginCategory(PROCESS_HANDLE, view["Name"])
nodes = DYNNODES[media]
for node in nodes:
params = {
"id": view_id,
"mode": "browse",
"type": media,
"folder": view_id if node[0] == "all" else node[0],
"server": server_id,
}
path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params))
directory(node[1] or view["Name"], path)
xbmcplugin.setContent(PROCESS_HANDLE, "files")
xbmcplugin.endOfDirectory(PROCESS_HANDLE)
def browse_letters(media, view_id, server_id=None):
"""Display letters as options."""
letters = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ"
view = Jellyfin(server_id).get_client().jellyfin.get_item(view_id)
xbmcplugin.setPluginCategory(PROCESS_HANDLE, view["Name"])
for node in letters:
params = {
"id": view_id,
"mode": "browse",
"type": media,
"folder": "firstletter-%s" % node,
"server": server_id,
}
path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params))
directory(node, path)
xbmcplugin.setContent(PROCESS_HANDLE, "files")
xbmcplugin.endOfDirectory(PROCESS_HANDLE)
def get_folder_type(item, content_type=None):
media = item["Type"]
if media == "Series":
return "seasons"
elif media == "Season":
return "episodes"
elif media == "BoxSet":
return "boxset"
elif media == "MusicArtist":
return "albums"
elif media == "MusicAlbum":
return "songs"
elif media == "CollectionFolder":
return item.get("CollectionType", "library")
elif media == "Folder" and content_type == "music":
return "albums"
def get_media_type(media):
if media == "movies":
return "Movie,BoxSet"
elif media == "homevideos":
return "Video,Folder,PhotoAlbum,Photo"
elif media == "episodes":
return "Episode"
elif media == "boxsets":
return "BoxSet"
elif media == "tvshows":
return "Series"
elif media == "music":
return "MusicArtist,MusicAlbum,Audio"
def get_fanart(item_id, path, server_id=None, api_client=None):
"""Get extra fanart for listitems. This is called by skinhelper.
Images are stored locally, due to the Kodi caching system.
"""
if not item_id and "plugin.video.jellyfin" in path:
item_id = path.split("/")[-2]
if not item_id:
return
LOG.info("[ extra fanart ] %s", item_id)
objects = Objects()
list_li = []
directory = translate_path("special://thumbnails/jellyfin/%s/" % item_id)
if not xbmcvfs.exists(directory):
xbmcvfs.mkdirs(directory)
item = api_client.get_item(item_id)
obj = objects.map(item, "Artwork")
backdrops = api.API(item).get_all_artwork(obj)
tags = obj["BackdropTags"]
for index, backdrop in enumerate(backdrops):
tag = tags[index]
fanart = os.path.join(directory, "fanart%s.jpg" % tag)
li = xbmcgui.ListItem(tag, path=fanart)
xbmcvfs.copy(backdrop, fanart)
list_li.append((fanart, li, False))
else:
LOG.debug("cached backdrop found")
dirs, files = xbmcvfs.listdir(directory)
for file in files:
fanart = os.path.join(directory, file)
li = xbmcgui.ListItem(file, path=fanart)
list_li.append((fanart, li, False))
xbmcplugin.addDirectoryItems(PROCESS_HANDLE, list_li, len(list_li))
xbmcplugin.endOfDirectory(PROCESS_HANDLE)
def get_video_extras(item_id, path, server_id=None, api_client=None):
"""Returns the video files for the item as plugin listing, can be used
to browse actual files or video extras, etc.
"""
if not item_id and "plugin.video.jellyfin" in path:
item_id = path.split("/")[-2]
if not item_id:
return
# TODO implement????
# Jellyfin(server_id).get_client().jellyfin.get_item(item_id)
"""
def getVideoFiles(jellyfinId,jellyfinPath):
#returns the video files for the item as plugin listing, can be used for browsing the actual files or videoextras etc.
jellyfin = jellyfinserver.Read_JellyfinServer()
if not jellyfinId:
if "plugin.video.jellyfin" in jellyfinPath:
jellyfinId = jellyfinPath.split("/")[-2]
if jellyfinId:
item = jellyfin.getItem(jellyfinId)
putils = playutils.PlayUtils(item)
if putils.isDirectPlay():
#only proceed if we can access the files directly. TODO: copy local on the fly if accessed outside
filelocation = putils.directPlay()
if not filelocation.endswith("/"):
filelocation = filelocation.rpartition("/")[0]
dirs, files = xbmcvfs.listdir(filelocation)
for file in files:
file = filelocation + file
li = xbmcgui.ListItem(file, path=file)
xbmcplugin.addDirectoryItem(handle=PROCESS_HANDLE, url=file, listitem=li)
for dir in dirs:
dir = filelocation + dir
li = xbmcgui.ListItem(dir, path=dir)
xbmcplugin.addDirectoryItem(handle=PROCESS_HANDLE, url=dir, listitem=li, isFolder=True)
#xbmcplugin.endOfDirectory(PROCESS_HANDLE)
"""
def get_next_episodes(item_id, limit):
"""Only for synced content."""
with Database("jellyfin") as jellyfindb:
db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor)
library = db.get_view_name(item_id)
if not library:
return
result = JSONRPC("VideoLibrary.GetTVShows").execute(
{
"sort": {"order": "descending", "method": "lastplayed"},
"filter": {
"and": [
{"operator": "true", "field": "inprogress", "value": ""},
{"operator": "is", "field": "tag", "value": "%s" % library},
]
},
"properties": ["title", "studio", "mpaa", "file", "art"],
}
)
try:
items = result["result"]["tvshows"]
except (KeyError, TypeError):
return
list_li = []
for item in items:
if settings("ignoreSpecialsNextEpisodes.bool"):
params = {
"tvshowid": item["tvshowid"],
"sort": {"method": "episode"},
"filter": {
"and": [
{"operator": "lessthan", "field": "playcount", "value": "1"},
{"operator": "greaterthan", "field": "season", "value": "0"},
]
},
"properties": [
"title",
"playcount",
"season",
"episode",
"showtitle",
"plot",
"file",
"rating",
"resume",
"tvshowid",
"art",
"streamdetails",
"firstaired",
"runtime",
"writer",
"dateadded",
"lastplayed",
],
"limits": {"end": 1},
}
else:
params = {
"tvshowid": item["tvshowid"],
"sort": {"method": "episode"},
"filter": {"operator": "lessthan", "field": "playcount", "value": "1"},
"properties": [
"title",
"playcount",
"season",
"episode",
"showtitle",
"plot",
"file",
"rating",
"resume",
"tvshowid",
"art",
"streamdetails",
"firstaired",
"runtime",
"writer",
"dateadded",
"lastplayed",
],
"limits": {"end": 1},
}
result = JSONRPC("VideoLibrary.GetEpisodes").execute(params)
try:
episodes = result["result"]["episodes"]
except (KeyError, TypeError):
pass
else:
for episode in episodes:
li = create_listitem(episode)
list_li.append((episode["file"], li))
if len(list_li) == limit:
break
xbmcplugin.addDirectoryItems(PROCESS_HANDLE, list_li, len(list_li))
xbmcplugin.setContent(PROCESS_HANDLE, "episodes")
xbmcplugin.endOfDirectory(PROCESS_HANDLE)
def create_listitem(item):
"""Listitem based on jsonrpc items."""
title = item["title"]
label2 = ""
li = xbmcgui.ListItem(title)
li.setProperty("IsPlayable", "true")
metadata = {
"Title": title,
"duration": str(item["runtime"] / 60),
"Plot": item["plot"],
"Playcount": item["playcount"],
}
if "showtitle" in item:
metadata["TVshowTitle"] = item["showtitle"]
label2 = item["showtitle"]
if "episodeid" in item:
# Listitem of episode
metadata["mediatype"] = "episode"
metadata["dbid"] = item["episodeid"]
# TODO: Review once Krypton is RC - probably no longer needed if there's dbid
if "episode" in item:
episode = item["episode"]
metadata["Episode"] = episode
if "season" in item:
season = item["season"]
metadata["Season"] = season
if season and episode:
episodeno = "s%.2de%.2d" % (season, episode)
li.setProperty("episodeno", episodeno)
label2 = "%s - %s" % (label2, episodeno) if label2 else episodeno
if "firstaired" in item:
metadata["Premiered"] = item["firstaired"]
if "rating" in item:
metadata["Rating"] = str(round(float(item["rating"]), 1))
if "director" in item:
metadata["Director"] = " / ".join(item["director"])
if "writer" in item:
metadata["Writer"] = " / ".join(item["writer"])
if "cast" in item:
cast = []
castandrole = []
for person in item["cast"]:
name = person["name"]
cast.append(name)
castandrole.append((name, person["role"]))
metadata["Cast"] = cast
metadata["CastAndRole"] = castandrole
li.setLabel2(label2)
li.setInfo(type="Video", infoLabels=metadata)
li.setProperty("resumetime", str(item["resume"]["position"]))
li.setProperty("totaltime", str(item["resume"]["total"]))
li.setArt(item["art"])
li.setProperty("dbid", str(item["episodeid"]))
li.setProperty("fanart_image", item["art"].get("tvshow.fanart", ""))
for key, value in item["streamdetails"].items():
for stream in value:
li.addStreamInfo(key, stream)
return li
def add_user(api_client):
"""Add or remove users from the default server session."""
if not window("jellyfin_online.bool"):
return
session = api_client.get_device(client.get_device_id())
users = api_client.get_users()
current = session[0]["AdditionalUsers"]
result = dialog(
"select",
translate(33061),
[translate(33062), translate(33063)] if current else [translate(33062)],
)
if result < 0:
return
if not result: # Add user
eligible = [
x
for x in users
if x["Id"] not in [current_user["UserId"] for current_user in current]
]
resp = dialog("select", translate(33064), [x["Name"] for x in eligible])
if resp < 0:
return
user = eligible[resp]
event("AddUser", {"Id": user["Id"], "Add": True})
else: # Remove user
resp = dialog("select", translate(33064), [x["UserName"] for x in current])
if resp < 0:
return
user = current[resp]
event("AddUser", {"Id": user["UserId"], "Add": False})
def get_themes(api_client):
"""Add theme media locally, via strm. This is only for tv tunes.
If another script is used, adjust this code.
"""
from ..helper.utils import normalize_string
from ..helper.playutils import PlayUtils
from ..helper.xmls import tvtunes_nfo
library = translate_path(
"special://profile/addon_data/plugin.video.jellyfin/library"
)
play = settings("useDirectPaths") == "1"
if not xbmcvfs.exists(library + "/"):
xbmcvfs.mkdir(library)
if xbmc.getCondVisibility("System.HasAddon(script.tvtunes)"):
tvtunes = xbmcaddon.Addon(id="script.tvtunes")
tvtunes.setSetting("custom_path_enable", "true")
tvtunes.setSetting("custom_path", library)
LOG.info("TV Tunes custom path is enabled and set.")
else:
dialog("ok", "{jellyfin}", translate(33152))
return
with Database("jellyfin") as jellyfindb:
all_views = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_views()
views = [
x.view_id
for x in all_views
if x.media_type in ("movies", "tvshows", "mixed")
]
items = {}
server = api_client.config.data["auth.server"]
for view in views:
result = api_client.get_items_theme_video(view)
for item in result["Items"]:
folder = normalize_string(item["Name"])
items[item["Id"]] = folder
result = api_client.get_items_theme_song(view)
for item in result["Items"]:
folder = normalize_string(item["Name"])
items[item["Id"]] = folder
for item in items:
nfo_path = os.path.join(library, items[item])
nfo_file = os.path.join(nfo_path, "tvtunes.nfo")
if not xbmcvfs.exists(nfo_path):
xbmcvfs.mkdir(nfo_path)
themes = api_client.get_themes(item)
paths = []
for theme in (
themes["ThemeVideosResult"]["Items"] + themes["ThemeSongsResult"]["Items"]
):
putils = PlayUtils(theme, False, None, server, api_client)
if play:
paths.append(putils.direct_play(theme["MediaSources"][0]))
else:
paths.append(putils.direct_url(theme["MediaSources"][0]))
tvtunes_nfo(nfo_file, paths)
dialog(
"notification",
heading="{jellyfin}",
message=translate(33153),
icon="{jellyfin}",
time=1000,
sound=False,
)
def delete_item():
"""Delete keymap action."""
from . import context
context.Context(delete=True)
def backup():
"""Jellyfin backup."""
from ..helper.utils import delete_folder, copytree
path = settings("backupPath")
folder_name = "Kodi%s.%s" % (
kodi_version(),
xbmc.getInfoLabel("System.Date(dd-mm-yy)"),
)
folder_name = dialog("input", heading=translate(33089), defaultt=folder_name)
if not folder_name:
return
backup = os.path.join(path, folder_name)
if xbmcvfs.exists(backup + "/"):
if not dialog("yesno", "{jellyfin}", translate(33090)):
return backup()
delete_folder(backup)
addon_data = translate_path("special://profile/addon_data/plugin.video.jellyfin")
destination_data = os.path.join(backup, "addon_data", "plugin.video.jellyfin")
destination_databases = os.path.join(backup, "Database")
if not xbmcvfs.mkdirs(path) or not xbmcvfs.mkdirs(destination_databases):
LOG.info("Unable to create all directories")
dialog(
"notification",
heading="{jellyfin}",
icon="{jellyfin}",
message=translate(33165),
sound=False,
)
return
copytree(addon_data, destination_data)
databases = Objects().objects
db = translate_path(databases["jellyfin"])
xbmcvfs.copy(db, os.path.join(destination_databases, db.rsplit("\\", 1)[1]))
LOG.info("copied jellyfin.db")
db = translate_path(databases["video"])
filename = db.rsplit("\\", 1)[1]
xbmcvfs.copy(db, os.path.join(destination_databases, filename))
LOG.info("copied %s", filename)
if settings("enableMusic.bool"):
db = translate_path(databases["music"])
filename = db.rsplit("\\", 1)[1]
xbmcvfs.copy(db, os.path.join(destination_databases, filename))
LOG.info("copied %s", filename)
LOG.info("backup completed")
dialog("ok", "{jellyfin}", "%s %s" % (translate(33091), backup))
def get_filtered_section(
parent_id=None,
media=None,
limit=None,
recursive=None,
sort=None,
sort_order=None,
filters=None,
extra=None,
server_id=None,
api_client=None,
):
"""Get dynamic listings."""
params = {
"ParentId": parent_id,
"IncludeItemTypes": media,
"IsMissing": False,
"Recursive": recursive if recursive is not None else True,
"Limit": limit,
"SortBy": sort or "SortName",
"SortOrder": sort_order or "Ascending",
"ImageTypeLimit": 1,
"IsVirtualUnaired": False,
"Fields": browse_info(),
}
if filters:
if "Boxsets" in filters:
filters.remove("Boxsets")
params["CollapseBoxSetItems"] = settings("groupedSets.bool")
params["Filters"] = ",".join(filters)
if settings("getCast.bool"):
params["Fields"] += ",People"
if media and "Photo" in media:
params["Fields"] += ",Width,Height"
if extra is not None:
params.update(extra)
return api_client._get("Users/{UserId}/Items", params)
def browse_info():
return (
"DateCreated,EpisodeCount,SeasonCount,Path,Genres,Studios,Taglines,MediaStreams,Overview,Etag,"
"ProductionLocations,Width,Height,RecursiveItemCount,ChildCount"
)