mirror of
https://github.com/jellyfin/jellyfin-kodi.git
synced 2024-11-24 22:10:19 +00:00
2c4ce7e7a4
Add shortcuts. Audiobook can't be music type otherwise it break resume behavior and it won't play the right item. Has to be video type.
986 lines
32 KiB
Python
986 lines
32 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
#################################################################################################
|
|
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import urllib
|
|
import xml.etree.ElementTree as etree
|
|
|
|
import xbmc
|
|
import xbmcvfs
|
|
|
|
import downloader as server
|
|
from database import Database, emby_db, get_sync, save_sync
|
|
from objects.kodi import kodi
|
|
from helper import _, api, indent, write_xml, window, event
|
|
from emby import Emby
|
|
|
|
#################################################################################################
|
|
|
|
LOG = logging.getLogger("EMBY."+__name__)
|
|
NODES = {
|
|
'tvshows': [
|
|
('all', None),
|
|
('recent', _(30170)),
|
|
('recentepisodes', _(30175)),
|
|
('inprogress', _(30171)),
|
|
('inprogressepisodes', _(30178)),
|
|
('nextepisodes', _(30179)),
|
|
('genres', 135),
|
|
('random', _(30229)),
|
|
('recommended', _(30230))
|
|
],
|
|
'movies': [
|
|
('all', None),
|
|
('recent', _(30174)),
|
|
('inprogress', _(30177)),
|
|
('unwatched', _(30189)),
|
|
('sets', 20434),
|
|
('genres', 135),
|
|
('random', _(30229)),
|
|
('recommended', _(30230))
|
|
],
|
|
'musicvideos': [
|
|
('all', None),
|
|
('recent', _(30256)),
|
|
('inprogress', _(30257)),
|
|
('unwatched', _(30258))
|
|
]
|
|
}
|
|
DYNNODES = {
|
|
'tvshows': [
|
|
('all', None),
|
|
('RecentlyAdded', _(30170)),
|
|
('recentepisodes', _(30175)),
|
|
('InProgress', _(30171)),
|
|
('inprogressepisodes', _(30178)),
|
|
('nextepisodes', _(30179)),
|
|
('Genres', _(135)),
|
|
('Random', _(30229)),
|
|
('recommended', _(30230))
|
|
],
|
|
'movies': [
|
|
('all', None),
|
|
('RecentlyAdded', _(30174)),
|
|
('InProgress', _(30177)),
|
|
('Boxsets', _(20434)),
|
|
('Favorite', _(33168)),
|
|
('FirstLetter', _(33171)),
|
|
('Genres', _(135)),
|
|
('Random', _(30229)),
|
|
#('Recommended', _(30230))
|
|
],
|
|
'musicvideos': [
|
|
('all', None),
|
|
('RecentlyAdded', _(30256)),
|
|
('InProgress', _(30257)),
|
|
('Unwatched', _(30258))
|
|
],
|
|
'homevideos': [
|
|
('all', None),
|
|
('RecentlyAdded', _(33167)),
|
|
('InProgress', _(33169)),
|
|
('Favorite', _(33168))
|
|
],
|
|
'books': [
|
|
('all', None),
|
|
('RecentlyAdded', _(33167)),
|
|
('InProgress', _(33169)),
|
|
('Favorite', _(33168))
|
|
],
|
|
'audiobooks': [
|
|
('all', None),
|
|
('RecentlyAdded', _(33167)),
|
|
('InProgress', _(33169)),
|
|
('Favorite', _(33168))
|
|
],
|
|
'music': [
|
|
('all', None),
|
|
('RecentlyAdded', _(33167)),
|
|
('Favorite', _(33168))
|
|
]
|
|
}
|
|
|
|
#################################################################################################
|
|
|
|
|
|
def verify_kodi_defaults():
|
|
|
|
''' Make sure we have the kodi default folder in place.
|
|
'''
|
|
node_path = xbmc.translatePath("special://profile/library/video").decode('utf-8')
|
|
|
|
if not xbmcvfs.exists(node_path):
|
|
try:
|
|
shutil.copytree(
|
|
src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
|
|
dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
|
|
except Exception as error:
|
|
xbmcvfs.mkdir(node_path)
|
|
|
|
for index, node in enumerate(['movies', 'tvshows', 'musicvideos']):
|
|
file = os.path.join(node_path, node, "index.xml")
|
|
|
|
if xbmcvfs.exists(file):
|
|
|
|
xml = etree.parse(file).getroot()
|
|
xml.set('order', str(17 + index))
|
|
indent(xml)
|
|
write_xml(etree.tostring(xml, 'UTF-8'), file)
|
|
|
|
playlist_path = xbmc.translatePath("special://profile/playlists/video").decode('utf-8')
|
|
|
|
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 = Emby()
|
|
|
|
def add_library(self, view):
|
|
|
|
''' Add entry to view table in emby database.
|
|
'''
|
|
with Database('emby') as embydb:
|
|
emby_db.EmbyDatabase(embydb.cursor).add_view(view['Id'], view['Name'], view['Media'])
|
|
|
|
def remove_library(self, view_id):
|
|
|
|
''' Remove entry from view table in emby database.
|
|
'''
|
|
with Database('emby') as embydb:
|
|
emby_db.EmbyDatabase(embydb.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['api'].get_media_folders()['Items']
|
|
views = self.server['api'].get_views()['Items']
|
|
except Exception as error:
|
|
LOG.error("Unable to process libraries: %s", error)
|
|
|
|
return []
|
|
|
|
libraries.extend([x for x in views if x['Id'] not in [y['Id'] for y in libraries]])
|
|
|
|
return libraries
|
|
|
|
def get_views(self):
|
|
|
|
''' Get the media folders. Add or remove them.
|
|
'''
|
|
media = {
|
|
'movies': "Movie",
|
|
'tvshows': "Series",
|
|
'musicvideos': "MusicVideo"
|
|
}
|
|
libraries = self.get_libraries()
|
|
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('emby') as embydb:
|
|
|
|
views = emby_db.EmbyDatabase(embydb.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").decode('utf-8')
|
|
playlist_path = xbmc.translatePath("special://profile/playlists/video").decode('utf-8')
|
|
index = 0
|
|
|
|
with Database('emby') as embydb:
|
|
db = emby_db.EmbyDatabase(embydb.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)
|
|
else: # Compensate for the duplicate.
|
|
index += 1
|
|
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': _('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies"},
|
|
{'Name': _('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows"},
|
|
{'Name': _('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, "emby%s%s.xsp" % (view['Media'], view['Id']))
|
|
|
|
try:
|
|
xml = etree.parse(file).getroot()
|
|
except Exception:
|
|
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']
|
|
|
|
indent(xml)
|
|
write_xml(etree.tostring(xml, 'UTF-8'), file)
|
|
|
|
def add_nodes(self, path, view, mixed=False):
|
|
|
|
''' Create or update the video node file.
|
|
'''
|
|
folder = os.path.join(path, "emby%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, "emby_%s.xml" % view['Tag'].replace(" ", ""))
|
|
|
|
try:
|
|
xml = etree.parse(file).getroot()
|
|
except Exception:
|
|
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)
|
|
|
|
indent(xml)
|
|
write_xml(etree.tostring(xml, 'UTF-8'), 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.emby/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:
|
|
xml = etree.parse(file).getroot()
|
|
xml.set('order', str(index))
|
|
except Exception:
|
|
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'], _(view['Media']))
|
|
|
|
indent(xml)
|
|
write_xml(etree.tostring(xml, 'UTF-8'), 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:
|
|
xml = etree.parse(file).getroot()
|
|
except Exception:
|
|
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
|
|
indent(xml)
|
|
write_xml(etree.tostring(xml, 'UTF-8'), file)
|
|
|
|
def add_dynamic_node(self, index, file, view, node, name, path):
|
|
|
|
try:
|
|
xml = etree.parse(file).getroot()
|
|
except Exception:
|
|
xml = self.node_root('folder', index)
|
|
etree.SubElement(xml, 'label')
|
|
etree.SubElement(xml, 'content')
|
|
|
|
label = xml.find('label')
|
|
label.text = name
|
|
|
|
getattr(self, 'node_' + node)(xml, path)
|
|
indent(xml)
|
|
write_xml(etree.tostring(xml, 'UTF-8'), 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 Emby 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 emby server views and more.
|
|
'''
|
|
self.window_clear()
|
|
self.window_clear('Emby.wnodes')
|
|
|
|
with Database('emby') as embydb:
|
|
libraries = emby_db.EmbyDatabase(embydb.cursor).get_views()
|
|
|
|
libraries = self.order_media_folders(libraries or [])
|
|
index = 0
|
|
windex = 0
|
|
|
|
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'], _(media))
|
|
self.window_node(index, temp_view, *node)
|
|
self.window_wnode(windex, temp_view, *node)
|
|
else: # 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', 'audiobooks'):
|
|
self.window_wnode(windex, view, 'browse')
|
|
windex += 1
|
|
|
|
self.window_node(index, view, 'browse')
|
|
|
|
index += 1
|
|
|
|
for single in [{'Name': _('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies"},
|
|
{'Name': _('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows"},
|
|
{'Name': _('fav_episodes'), 'Tag': "Favorite episodes", 'Media': "episodes"}]:
|
|
|
|
self.window_single_node(index, "favorites", single)
|
|
index += 1
|
|
|
|
window('Emby.nodes.total', str(index))
|
|
window('Emby.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 = _(node_label) if type(node_label) == int else node_label
|
|
node_label = node_label or view['Name']
|
|
|
|
if node in ('all', 'music'):
|
|
|
|
window_prop = "Emby.nodes.%s" % index
|
|
window('%s.index' % window_prop, path.replace('all.xml', "")) # dir
|
|
window('%s.title' % window_prop, view['Name'].encode('utf-8'))
|
|
window('%s.content' % window_prop, path)
|
|
|
|
elif node == 'browse':
|
|
|
|
window_prop = "Emby.nodes.%s" % index
|
|
window('%s.title' % window_prop, view['Name'].encode('utf-8'))
|
|
else:
|
|
window_prop = "Emby.nodes.%s.%s" % (index, node)
|
|
window('%s.title' % window_prop, node_label.encode('utf-8'))
|
|
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/emby_%s.xml" % view['Tag'].replace(" ", "")
|
|
window_path = "ActivateWindow(Videos,%s,return)" % path
|
|
|
|
window_prop = "Emby.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', 'audiobooks'):
|
|
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', 'audiobooks'):
|
|
window_path = path
|
|
else:
|
|
window_path = "ActivateWindow(Videos,%s,return)" % path
|
|
|
|
node_label = _(node_label) if type(node_label) == int else node_label
|
|
node_label = node_label or view['Name']
|
|
|
|
if node == 'all':
|
|
|
|
window_prop = "Emby.wnodes.%s" % index
|
|
window('%s.index' % window_prop, path.replace('all.xml', "")) # dir
|
|
window('%s.title' % window_prop, view['Name'].encode('utf-8'))
|
|
window('%s.content' % window_prop, path)
|
|
|
|
elif node == 'browse':
|
|
|
|
window_prop = "Emby.wnodes.%s" % index
|
|
window('%s.title' % window_prop, view['Name'].encode('utf-8'))
|
|
window('%s.content' % window_prop, path)
|
|
else:
|
|
window_prop = "Emby.wnodes.%s.%s" % (index, node)
|
|
window('%s.title' % window_prop, node_label.encode('utf-8'))
|
|
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['connected']:
|
|
window('%s.artwork' % prop, clear=True)
|
|
|
|
elif self.server['connected']:
|
|
|
|
if self.media_folders is None:
|
|
self.media_folders = self.get_libraries()
|
|
|
|
for library in self.media_folders:
|
|
|
|
if library['Id'] == view_id and 'Primary' in library.get('ImageTags', {}):
|
|
|
|
artwork = api.API(None, self.server['auth/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/emby%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.emby/", urllib.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.emby/", urllib.urlencode(params))
|
|
|
|
def window_clear(self, name=None):
|
|
|
|
''' Clearing window prop setup for Views.
|
|
'''
|
|
total = int(window((name or 'Emby.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('Emby.nodes.%s.%s' % (str(i), prop), clear=True)
|
|
|
|
for prop in props:
|
|
window('Emby.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 emby playlists.
|
|
'''
|
|
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
|
|
_, files = xbmcvfs.listdir(path)
|
|
for file in files:
|
|
if file.decode('utf-8').startswith('emby'):
|
|
self.delete_playlist(os.path.join(path, file.decode('utf-8')))
|
|
|
|
def delete_playlist_by_id(self, view_id):
|
|
|
|
''' Remove playlist based based on view_id.
|
|
'''
|
|
path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
|
|
_, files = xbmcvfs.listdir(path)
|
|
for file in files:
|
|
file = file.decode('utf-8')
|
|
|
|
if file.startswith('emby') and file.endswith('%s.xsp' % view_id):
|
|
self.delete_playlist(os.path.join(path, file.decode('utf-8')))
|
|
|
|
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/").decode('utf-8')
|
|
dirs, files = xbmcvfs.listdir(path)
|
|
|
|
for file in files:
|
|
|
|
if file.startswith('emby'):
|
|
self.delete_node(os.path.join(path, file.decode('utf-8')))
|
|
|
|
for directory in dirs:
|
|
|
|
if directory.startswith('emby'):
|
|
_, files = xbmcvfs.listdir(os.path.join(path, directory.decode('utf-8')))
|
|
|
|
for file in files:
|
|
self.delete_node(os.path.join(path, directory.decode('utf-8'), file.decode('utf-8')))
|
|
|
|
xbmcvfs.rmdir(os.path.join(path, directory.decode('utf-8')))
|
|
|
|
def delete_node_by_id(self, view_id):
|
|
|
|
''' Remove node and children files based on view_id.
|
|
'''
|
|
path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
|
|
dirs, files = xbmcvfs.listdir(path)
|
|
|
|
for directory in dirs:
|
|
|
|
if directory.startswith('emby') and directory.endswith(view_id):
|
|
_, files = xbmcvfs.listdir(os.path.join(path, directory.decode('utf-8')))
|
|
|
|
for file in files:
|
|
self.delete_node(os.path.join(path, directory.decode('utf-8'), file.decode('utf-8')))
|
|
|
|
xbmcvfs.rmdir(os.path.join(path, directory.decode('utf-8')))
|