# -*- coding: utf-8 -*- from __future__ import division, absolute_import, print_function, unicode_literals ################################################################################################# import os import shutil import xml.etree.ElementTree as etree from six.moves.urllib.parse import urlencode from kodi_six import xbmc, xbmcvfs from database import Database, jellyfin_db, get_sync, save_sync from helper import translate, api, window, event from jellyfin import Jellyfin from helper import LazyLogger ################################################################################################# LOG = LazyLogger(__name__) NODES = { 'tvshows': [ ('all', None), ('recent', translate(30170)), ('recentepisodes', translate(30175)), ('inprogress', translate(30171)), ('inprogressepisodes', translate(30178)), ('nextepisodes', translate(30179)), ('genres', 135), ('random', translate(30229)), ('recommended', translate(30230)) ], 'movies': [ ('all', None), ('recent', translate(30174)), ('inprogress', translate(30177)), ('unwatched', translate(30189)), ('sets', 20434), ('genres', 135), ('random', translate(30229)), ('recommended', translate(30230)) ], 'musicvideos': [ ('all', None), ('recent', translate(30256)), ('inprogress', translate(30257)), ('unwatched', translate(30258)) ] } DYNNODES = { 'tvshows': [ ('all', None), ('RecentlyAdded', translate(30170)), ('recentepisodes', translate(30175)), ('InProgress', translate(30171)), ('inprogressepisodes', translate(30178)), ('nextepisodes', translate(30179)), ('Genres', translate(135)), ('Random', translate(30229)), ('recommended', translate(30230)) ], 'movies': [ ('all', None), ('RecentlyAdded', translate(30174)), ('InProgress', translate(30177)), ('Boxsets', translate(20434)), ('Favorite', translate(33168)), ('FirstLetter', translate(33171)), ('Genres', translate(135)), ('Random', translate(30229)), # ('Recommended', translate(30230)) ], 'musicvideos': [ ('all', None), ('RecentlyAdded', translate(30256)), ('InProgress', translate(30257)), ('Unwatched', translate(30258)) ], 'homevideos': [ ('all', None), ('RecentlyAdded', translate(33167)), ('InProgress', translate(33169)), ('Favorite', translate(33168)) ], 'books': [ ('all', None), ('RecentlyAdded', translate(33167)), ('InProgress', translate(33169)), ('Favorite', translate(33168)) ], 'audiobooks': [ ('all', None), ('RecentlyAdded', translate(33167)), ('InProgress', translate(33169)), ('Favorite', translate(33168)) ], 'music': [ ('all', None), ('RecentlyAdded', translate(33167)), ('Favorite', translate(33168)) ] } ################################################################################################# def verify_kodi_defaults(): ''' Make sure we have the kodi default folder in place. ''' source_base_path = xbmc.translatePath("special://xbmc/system/library/video") dest_base_path = xbmc.translatePath("special://profile/library/video") # Make sure the files exist in the local profile. # TODO: Investigate why this is needed. # I would think Kodi pulls data from the default profile # if we don't do this. for source_path, dirs, files in os.walk(source_base_path): relative_path = os.path.relpath(source_path, source_base_path) dest_path = os.path.join(dest_base_path, relative_path) if not os.path.exists(dest_path): os.mkdir(dest_path) for file_name in files: dest_file = os.path.join(dest_path, file_name) copy = False if not os.path.exists(dest_file): copy = True elif os.path.splitext(file_name)[1].lower() == '.xml': try: etree.parse(dest_file) except etree.ParseError: LOG.warning("Unable to parse `{}`, recovering from default.".format(dest_file)) copy = True if copy: source_file = os.path.join(source_path, file_name) LOG.debug("Copying `{}` -> `{}`".format(source_file, dest_file)) xbmcvfs.copy(source_file, dest_file) # This code seems to enforce a fixed ordering. # Is it really desirable to force this on users? # The default (system wide) order is [10, 20, 30] in Kodi 19. for index, node in enumerate(['movies', 'tvshows', 'musicvideos']): file_name = os.path.join(dest_base_path, node, "index.xml") if xbmcvfs.exists(file_name): try: tree = etree.parse(file_name) except etree.ParseError: LOG.error("Unable to parse `{}`".format(file_name)) LOG.exception("We ensured the file was OK above, something is wrong!") tree.getroot().set('order', str(17 + index)) tree.write(file_name) playlist_path = xbmc.translatePath("special://profile/playlists/video") if not xbmcvfs.exists(playlist_path): xbmcvfs.mkdirs(playlist_path) class Views(object): sync = None limit = 25 media_folders = None def __init__(self): self.sync = get_sync() self.server = Jellyfin() def add_library(self, view): ''' Add entry to view table in jellyfin database. ''' with Database('jellyfin') as jellyfindb: jellyfin_db.JellyfinDatabase(jellyfindb.cursor).add_view(view['Id'], view['Name'], view['Media']) def remove_library(self, view_id): ''' Remove entry from view table in jellyfin database. ''' with Database('jellyfin') as jellyfindb: jellyfin_db.JellyfinDatabase(jellyfindb.cursor).remove_view(view_id) self.delete_playlist_by_id(view_id) self.delete_node_by_id(view_id) def get_libraries(self): try: libraries = self.server.jellyfin.get_media_folders()['Items'] library_ids = [x['Id'] for x in libraries] for view in self.server.jellyfin.get_views()['Items']: if view['Id'] not in library_ids: libraries.append(view) except Exception as error: LOG.exception(error) raise IndexError("Unable to retrieve libraries: %s" % error) return libraries def get_views(self): ''' Get the media folders. Add or remove them. Do not proceed if issue getting libraries. ''' try: libraries = self.get_libraries() except IndexError as error: LOG.exception(error) return self.sync['SortedViews'] = [x['Id'] for x in libraries] for library in libraries: if library['Type'] == 'Channel': library['Media'] = "channels" else: library['Media'] = library.get('OriginalCollectionType', library.get('CollectionType', "mixed")) self.add_library(library) with Database('jellyfin') as jellyfindb: views = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_views() removed = [] for view in views: if view[0] not in self.sync['SortedViews']: removed.append(view[0]) if removed: event('RemoveLibrary', {'Id': ','.join(removed)}) save_sync(self.sync) def get_nodes(self): ''' Set up playlists, video nodes, window prop. ''' node_path = xbmc.translatePath("special://profile/library/video") playlist_path = xbmc.translatePath("special://profile/playlists/video") index = 0 with Database('jellyfin') as jellyfindb: db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) for library in self.sync['Whitelist']: library = library.replace('Mixed:', "") view = db.get_view(library) if view: view = {'Id': library, 'Name': view[0], 'Tag': view[0], 'Media': view[1]} if view['Media'] == 'mixed': for media in ('movies', 'tvshows'): temp_view = dict(view) temp_view['Media'] = media self.add_playlist(playlist_path, temp_view, True) self.add_nodes(node_path, temp_view, True) index += 1 # Compensate for the duplicate. else: if view['Media'] in ('movies', 'tvshows', 'musicvideos'): self.add_playlist(playlist_path, view) if view['Media'] not in ('music',): self.add_nodes(node_path, view) index += 1 for single in [{'Name': translate('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies"}, {'Name': translate('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows"}, {'Name': translate('fav_episodes'), 'Tag': "Favorite episodes", 'Media': "episodes"}]: self.add_single_node(node_path, index, "favorites", single) index += 1 self.window_nodes() def add_playlist(self, path, view, mixed=False): ''' Create or update the xps file. ''' file = os.path.join(path, "jellyfin%s%s.xsp" % (view['Media'], view['Id'])) try: if os.path.isfile(file): xml = etree.parse(file).getroot() else: xml = etree.Element('smartplaylist', {'type': view['Media']}) etree.SubElement(xml, 'name') etree.SubElement(xml, 'match') except Exception: LOG.warning("Unable to parse file '%s'", file) xml = etree.Element('smartplaylist', {'type': view['Media']}) etree.SubElement(xml, 'name') etree.SubElement(xml, 'match') name = xml.find('name') name.text = view['Name'] if not mixed else "%s (%s)" % (view['Name'], view['Media']) match = xml.find('match') match.text = "all" for rule in xml.findall('.//value'): if rule.text == view['Tag']: break else: rule = etree.SubElement(xml, 'rule', {'field': "tag", 'operator': "is"}) etree.SubElement(rule, 'value').text = view['Tag'] tree = etree.ElementTree(xml) tree.write(file) def add_nodes(self, path, view, mixed=False): ''' Create or update the video node file. ''' folder = os.path.join(path, "jellyfin%s%s" % (view['Media'], view['Id'])) if not xbmcvfs.exists(folder): xbmcvfs.mkdir(folder) self.node_index(folder, view, mixed) if view['Media'] == 'tvshows': self.node_tvshow(folder, view) else: self.node(folder, view) def add_single_node(self, path, index, item_type, view): file = os.path.join(path, "jellyfin_%s.xml" % view['Tag'].replace(" ", "")) try: if os.path.isfile(file): xml = etree.parse(file).getroot() else: xml = self.node_root('folder' if item_type == 'favorites' and view['Media'] == 'episodes' else 'filter', index) etree.SubElement(xml, 'label') etree.SubElement(xml, 'match') etree.SubElement(xml, 'content') except Exception: LOG.warning("Unable to parse file '%s'", file) xml = self.node_root('folder' if item_type == 'favorites' and view['Media'] == 'episodes' else 'filter', index) etree.SubElement(xml, 'label') etree.SubElement(xml, 'match') etree.SubElement(xml, 'content') label = xml.find('label') label.text = view['Name'] content = xml.find('content') content.text = view['Media'] match = xml.find('match') match.text = "all" if view['Media'] != 'episodes': for rule in xml.findall('.//value'): if rule.text == view['Tag']: break else: rule = etree.SubElement(xml, 'rule', {'field': "tag", 'operator': "is"}) etree.SubElement(rule, 'value').text = view['Tag'] if item_type == 'favorites' and view['Media'] == 'episodes': path = self.window_browse(view, 'FavEpisodes') self.node_favepisodes(xml, path) else: self.node_all(xml) tree = etree.ElementTree(xml) tree.write(file) def node_root(self, root, index): ''' Create the root element ''' if root == 'main': element = etree.Element('node', {'order': str(index)}) elif root == 'filter': element = etree.Element('node', {'order': str(index), 'type': "filter"}) else: element = etree.Element('node', {'order': str(index), 'type': "folder"}) etree.SubElement(element, 'icon').text = "special://home/addons/plugin.video.jellyfin/resources/icon.png" return element def node_index(self, folder, view, mixed=False): file = os.path.join(folder, "index.xml") index = self.sync['SortedViews'].index(view['Id']) try: if os.path.isfile(file): xml = etree.parse(file).getroot() xml.set('order', str(index)) else: xml = self.node_root('main', index) etree.SubElement(xml, 'label') except Exception as error: LOG.exception(error) xml = self.node_root('main', index) etree.SubElement(xml, 'label') label = xml.find('label') label.text = view['Name'] if not mixed else "%s (%s)" % (view['Name'], translate(view['Media'])) tree = etree.ElementTree(xml) tree.write(file) def node(self, folder, view): for node in NODES[view['Media']]: xml_name = node[0] xml_label = node[1] or view['Name'] file = os.path.join(folder, "%s.xml" % xml_name) self.add_node(NODES[view['Media']].index(node), file, view, xml_name, xml_label) def node_tvshow(self, folder, view): for node in NODES[view['Media']]: xml_name = node[0] xml_label = node[1] or view['Name'] xml_index = NODES[view['Media']].index(node) file = os.path.join(folder, "%s.xml" % xml_name) if xml_name == 'nextepisodes': path = self.window_nextepisodes(view) self.add_dynamic_node(xml_index, file, view, xml_name, xml_label, path) else: self.add_node(xml_index, file, view, xml_name, xml_label) def add_node(self, index, file, view, node, name): try: if os.path.isfile(file): xml = etree.parse(file).getroot() else: xml = self.node_root('filter', index) etree.SubElement(xml, 'label') etree.SubElement(xml, 'match') etree.SubElement(xml, 'content') except Exception: LOG.warning("Unable to parse file '%s'", file) xml = self.node_root('filter', index) etree.SubElement(xml, 'label') etree.SubElement(xml, 'match') etree.SubElement(xml, 'content') label = xml.find('label') label.text = str(name) if type(name) == int else name content = xml.find('content') content.text = view['Media'] match = xml.find('match') match.text = "all" for rule in xml.findall('.//value'): if rule.text == view['Tag']: break else: rule = etree.SubElement(xml, 'rule', {'field': "tag", 'operator': "is"}) etree.SubElement(rule, 'value').text = view['Tag'] getattr(self, 'node_' + node)(xml) # get node function based on node type tree = etree.ElementTree(xml) tree.write(file) def add_dynamic_node(self, index, file, view, node, name, path): try: if os.path.isfile(file): xml = etree.parse(file).getroot() else: xml = self.node_root('folder', index) etree.SubElement(xml, 'label') etree.SubElement(xml, 'content') except Exception: LOG.warning("Unable to parse file '%s'", file) xml = self.node_root('folder', index) etree.SubElement(xml, 'label') etree.SubElement(xml, 'content') # Migration for https://github.com/jellyfin/jellyfin-kodi/issues/239 if xml.attrib.get('type') == 'filter': xml.attrib = {'type': 'folder', 'order': '5'} label = xml.find('label') label.text = name getattr(self, 'node_' + node)(xml, path) tree = etree.ElementTree(xml) tree.write(file) def node_all(self, root): for rule in root.findall('.//order'): if rule.text == "sorttitle": break else: etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" def node_nextepisodes(self, root, path): for rule in root.findall('.//path'): rule.text = path break else: etree.SubElement(root, 'path').text = path for rule in root.findall('.//content'): rule.text = "episodes" break else: etree.SubElement(root, 'content').text = "episodes" def node_recent(self, root): for rule in root.findall('.//order'): if rule.text == "dateadded": break else: etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" for rule in root.findall('.//limit'): rule.text = str(self.limit) break else: etree.SubElement(root, 'limit').text = str(self.limit) for rule in root.findall('.//rule'): if rule.attrib['field'] == 'playcount': rule.find('value').text = "0" break else: rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) etree.SubElement(rule, 'value').text = "0" def node_inprogress(self, root): for rule in root.findall('.//rule'): if rule.attrib['field'] == 'inprogress': break else: etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) for rule in root.findall('.//limit'): rule.text = str(self.limit) break else: etree.SubElement(root, 'limit').text = str(self.limit) def node_genres(self, root): for rule in root.findall('.//order'): if rule.text == "sorttitle": break else: etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" for rule in root.findall('.//group'): rule.text = "genres" break else: etree.SubElement(root, 'group').text = "genres" def node_unwatched(self, root): for rule in root.findall('.//order'): if rule.text == "sorttitle": break else: etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" for rule in root.findall('.//rule'): if rule.attrib['field'] == 'playcount': rule.find('value').text = "0" break else: rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"}) etree.SubElement(rule, 'value').text = "0" def node_sets(self, root): for rule in root.findall('.//order'): if rule.text == "sorttitle": break else: etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle" for rule in root.findall('.//group'): rule.text = "sets" break else: etree.SubElement(root, 'group').text = "sets" def node_random(self, root): for rule in root.findall('.//order'): if rule.text == "random": break else: etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random" for rule in root.findall('.//limit'): rule.text = str(self.limit) break else: etree.SubElement(root, 'limit').text = str(self.limit) def node_recommended(self, root): for rule in root.findall('.//order'): if rule.text == "rating": break else: etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating" for rule in root.findall('.//limit'): rule.text = str(self.limit) break else: etree.SubElement(root, 'limit').text = str(self.limit) for rule in root.findall('.//rule'): if rule.attrib['field'] == 'playcount': rule.find('value').text = "0" break else: rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) etree.SubElement(rule, 'value').text = "0" for rule in root.findall('.//rule'): if rule.attrib['field'] == 'rating': rule.find('value').text = "7" break else: rule = etree.SubElement(root, 'rule', {'field': "rating", 'operator': "greaterthan"}) etree.SubElement(rule, 'value').text = "7" def node_recentepisodes(self, root): for rule in root.findall('.//order'): if rule.text == "dateadded": break else: etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded" for rule in root.findall('.//limit'): rule.text = str(self.limit) break else: etree.SubElement(root, 'limit').text = str(self.limit) for rule in root.findall('.//rule'): if rule.attrib['field'] == 'playcount': rule.find('value').text = "0" break else: rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"}) etree.SubElement(rule, 'value').text = "0" content = root.find('content') content.text = "episodes" def node_inprogressepisodes(self, root): for rule in root.findall('.//limit'): rule.text = str(self.limit) break else: etree.SubElement(root, 'limit').text = str(self.limit) for rule in root.findall('.//rule'): if rule.attrib['field'] == 'inprogress': break else: etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"}) content = root.find('content') content.text = "episodes" def node_favepisodes(self, root, path): for rule in root.findall('.//path'): rule.text = path break else: etree.SubElement(root, 'path').text = path for rule in root.findall('.//content'): rule.text = "episodes" break else: etree.SubElement(root, 'content').text = "episodes" def order_media_folders(self, folders): ''' Returns a list of sorted media folders based on the Jellyfin views. Insert them in SortedViews and remove Views that are not in media folders. ''' if not folders: return folders sorted_views = list(self.sync['SortedViews']) unordered = [x[0] for x in folders] grouped = [x for x in unordered if x not in sorted_views] for library in grouped: sorted_views.append(library) sorted_folders = [x for x in sorted_views if x in unordered] return [folders[unordered.index(x)] for x in sorted_folders] def window_nodes(self): ''' Just read from the database and populate based on SortedViews Setup the window properties that reflect the jellyfin server views and more. ''' self.window_clear() self.window_clear('Jellyfin.wnodes') with Database('jellyfin') as jellyfindb: libraries = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_views() libraries = self.order_media_folders(libraries or []) index = 0 windex = 0 try: self.media_folders = self.get_libraries() except IndexError as error: LOG.exception(error) for library in (libraries or []): view = {'Id': library[0], 'Name': library[1], 'Tag': library[1], 'Media': library[2]} if library[0] in [x.replace('Mixed:', "") for x in self.sync['Whitelist']]: # Synced libraries if view['Media'] in ('movies', 'tvshows', 'musicvideos', 'mixed'): if view['Media'] == 'mixed': for media in ('movies', 'tvshows'): for node in NODES[media]: temp_view = dict(view) temp_view['Media'] = media temp_view['Name'] = "%s (%s)" % (view['Name'], translate(media)) self.window_node(index, temp_view, *node) self.window_wnode(windex, temp_view, *node) # Add one to compensate for the duplicate. index += 1 windex += 1 else: for node in NODES[view['Media']]: self.window_node(index, view, *node) if view['Media'] in ('movies', 'tvshows'): self.window_wnode(windex, view, *node) if view['Media'] in ('movies', 'tvshows'): windex += 1 elif view['Media'] == 'music': self.window_node(index, view, 'music') else: # Dynamic entry if view['Media'] in ('homevideos', 'books', 'playlists'): self.window_wnode(windex, view, 'browse') windex += 1 self.window_node(index, view, 'browse') index += 1 for single in [{'Name': translate('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies"}, {'Name': translate('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows"}, {'Name': translate('fav_episodes'), 'Tag': "Favorite episodes", 'Media': "episodes"}]: self.window_single_node(index, "favorites", single) index += 1 window('Jellyfin.nodes.total', str(index)) window('Jellyfin.wnodes.total', str(windex)) def window_node(self, index, view, node=None, node_label=None): ''' Leads to another listing of nodes. ''' if view['Media'] in ('homevideos', 'photos'): path = self.window_browse(view, None if node in ('all', 'browse') else node) elif node == 'nextepisodes': path = self.window_nextepisodes(view) elif node == 'music': path = self.window_music(view) elif node == 'browse': path = self.window_browse(view) else: path = self.window_path(view, node) if node == 'music': window_path = "ActivateWindow(Music,%s,return)" % path elif node in ('browse', 'homevideos', 'photos'): window_path = path else: window_path = "ActivateWindow(Videos,%s,return)" % path node_label = translate(node_label) if type(node_label) == int else node_label node_label = node_label or view['Name'] if node in ('all', 'music'): window_prop = "Jellyfin.nodes.%s" % index window('%s.index' % window_prop, path.replace('all.xml', "")) # dir window('%s.title' % window_prop, view['Name']) window('%s.content' % window_prop, path) elif node == 'browse': window_prop = "Jellyfin.nodes.%s" % index window('%s.title' % window_prop, view['Name']) else: window_prop = "Jellyfin.nodes.%s.%s" % (index, node) window('%s.title' % window_prop, node_label) window('%s.content' % window_prop, path) window('%s.id' % window_prop, view['Id']) window('%s.path' % window_prop, window_path) window('%s.type' % window_prop, view['Media']) self.window_artwork(window_prop, view['Id']) def window_single_node(self, index, item_type, view): ''' Single destination node. ''' path = "library://video/jellyfin_%s.xml" % view['Tag'].replace(" ", "") window_path = "ActivateWindow(Videos,%s,return)" % path window_prop = "Jellyfin.nodes.%s" % index window('%s.title' % window_prop, view['Name']) window('%s.path' % window_prop, window_path) window('%s.content' % window_prop, path) window('%s.type' % window_prop, item_type) def window_wnode(self, index, view, node=None, node_label=None): ''' Similar to window_node, but does not contain music, musicvideos. Contains books, audiobooks. ''' if view['Media'] in ('homevideos', 'photos', 'books', 'playlists'): path = self.window_browse(view, None if node in ('all', 'browse') else node) else: path = self.window_path(view, node) if node in ('browse', 'homevideos', 'photos', 'books', 'playlists'): window_path = path else: window_path = "ActivateWindow(Videos,%s,return)" % path node_label = translate(node_label) if type(node_label) == int else node_label node_label = node_label or view['Name'] if node == 'all': window_prop = "Jellyfin.wnodes.%s" % index window('%s.index' % window_prop, path.replace('all.xml', "")) # dir window('%s.title' % window_prop, view['Name']) window('%s.content' % window_prop, path) elif node == 'browse': window_prop = "Jellyfin.wnodes.%s" % index window('%s.title' % window_prop, view['Name']) window('%s.content' % window_prop, path) else: window_prop = "Jellyfin.wnodes.%s.%s" % (index, node) window('%s.title' % window_prop, node_label) window('%s.content' % window_prop, path) window('%s.id' % window_prop, view['Id']) window('%s.path' % window_prop, window_path) window('%s.type' % window_prop, view['Media']) self.window_artwork(window_prop, view['Id']) LOG.debug("--[ wnode/%s/%s ] %s", index, window('%s.title' % window_prop), window('%s.artwork' % window_prop)) def window_artwork(self, prop, view_id): if not self.server.logged_in: window('%s.artwork' % prop, clear=True) elif self.server.logged_in and self.media_folders is not None: for library in self.media_folders: if library['Id'] == view_id and 'Primary' in library.get('ImageTags', {}): server_address = self.server.auth.get_server_info(self.server.auth.server_id)['address'] artwork = api.API(None, server_address).get_artwork(view_id, 'Primary') window('%s.artwork' % prop, artwork) break else: window('%s.artwork' % prop, clear=True) def window_path(self, view, node): return "library://video/jellyfin%s%s/%s.xml" % (view['Media'], view['Id'], node) def window_music(self, view): return "library://music/" def window_nextepisodes(self, view): params = { 'id': view['Id'], 'mode': "nextepisodes", 'limit': self.limit } return "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) def window_browse(self, view, node=None): params = { 'mode': "browse", 'type': view['Media'] } if view.get('Id'): params['id'] = view['Id'] if node: params['folder'] = node return "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) def window_clear(self, name=None): ''' Clearing window prop setup for Views. ''' total = int(window((name or 'Jellyfin.nodes') + '.total') or 0) props = [ "index", "id", "path", "artwork", "title", "content", "type" "inprogress.content", "inprogress.title", "inprogress.content", "inprogress.path", "nextepisodes.title", "nextepisodes.content", "nextepisodes.path", "unwatched.title", "unwatched.content", "unwatched.path", "recent.title", "recent.content", "recent.path", "recentepisodes.title", "recentepisodes.content", "recentepisodes.path", "inprogressepisodes.title", "inprogressepisodes.content", "inprogressepisodes.path" ] for i in range(total): for prop in props: window('Jellyfin.nodes.%s.%s' % (str(i), prop), clear=True) for prop in props: window('Jellyfin.nodes.%s' % prop, clear=True) def delete_playlist(self, path): xbmcvfs.delete(path) LOG.info("DELETE playlist %s", path) def delete_playlists(self): ''' Remove all jellyfin playlists. ''' path = xbmc.translatePath("special://profile/playlists/video/") _, files = xbmcvfs.listdir(path) for file in files: if file.startswith('jellyfin'): self.delete_playlist(os.path.join(path, file)) def delete_playlist_by_id(self, view_id): ''' Remove playlist based based on view_id. ''' path = xbmc.translatePath("special://profile/playlists/video/") _, files = xbmcvfs.listdir(path) for file in files: file = file if file.startswith('jellyfin') and file.endswith('%s.xsp' % view_id): self.delete_playlist(os.path.join(path, file)) def delete_node(self, path): xbmcvfs.delete(path) LOG.info("DELETE node %s", path) def delete_nodes(self): ''' Remove node and children files. ''' path = xbmc.translatePath("special://profile/library/video/") dirs, files = xbmcvfs.listdir(path) for file in files: if file.startswith('jellyfin'): self.delete_node(os.path.join(path, file)) for directory in dirs: if directory.startswith('jellyfin'): _, files = xbmcvfs.listdir(os.path.join(path, directory)) for file in files: self.delete_node(os.path.join(path, directory, file)) xbmcvfs.rmdir(os.path.join(path, directory)) def delete_node_by_id(self, view_id): ''' Remove node and children files based on view_id. ''' path = xbmc.translatePath("special://profile/library/video/") dirs, files = xbmcvfs.listdir(path) for directory in dirs: if directory.startswith('jellyfin') and directory.endswith(view_id): _, files = xbmcvfs.listdir(os.path.join(path, directory)) for file in files: self.delete_node(os.path.join(path, directory, file)) xbmcvfs.rmdir(os.path.join(path, directory))