mirror of
https://github.com/jellyfin/jellyfin-kodi.git
synced 2025-05-07 18:08:50 +00:00
New hybrid method
This commit is contained in:
parent
7f5084c62e
commit
ace50b34dc
279 changed files with 39526 additions and 19994 deletions
20
resources/lib/helper/__init__.py
Normal file
20
resources/lib/helper/__init__.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from translate import _
|
||||
from exceptions import LibraryException
|
||||
|
||||
from utils import addon_id
|
||||
from utils import window
|
||||
from utils import settings
|
||||
from utils import dialog
|
||||
from utils import find
|
||||
from utils import event
|
||||
from utils import validate
|
||||
from utils import values
|
||||
from utils import JSONRPC
|
||||
from utils import indent
|
||||
from utils import write_xml
|
||||
|
||||
from wrapper import progress
|
||||
from wrapper import catch
|
||||
from wrapper import stop
|
||||
from wrapper import emby_item
|
||||
from wrapper import library_check
|
494
resources/lib/helper/api.py
Normal file
494
resources/lib/helper/api.py
Normal file
|
@ -0,0 +1,494 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##################################################################################################
|
||||
|
||||
import json
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from . import settings
|
||||
|
||||
##################################################################################################
|
||||
|
||||
LOG = logging.getLogger("EMBY."+__name__)
|
||||
|
||||
##################################################################################################
|
||||
|
||||
|
||||
class API(object):
|
||||
|
||||
|
||||
def __init__(self, item, server):
|
||||
|
||||
''' Get item information in special cases.
|
||||
server is the server address
|
||||
'''
|
||||
self.item = item
|
||||
self.server = server
|
||||
|
||||
def get_playcount(self, played, playcount):
|
||||
|
||||
''' Convert Emby played/playcount into
|
||||
the Kodi equivalent. The playcount is tied to the watch status.
|
||||
'''
|
||||
return (playcount or 1) if played else None
|
||||
|
||||
|
||||
|
||||
|
||||
def get_userdata(self):
|
||||
# Default
|
||||
favorite = False
|
||||
likes = None
|
||||
playcount = None
|
||||
played = False
|
||||
last_played = None
|
||||
resume = 0
|
||||
|
||||
try:
|
||||
userdata = self.item['UserData']
|
||||
except KeyError: # No userdata found.
|
||||
pass
|
||||
else:
|
||||
favorite = userdata['IsFavorite']
|
||||
likes = userdata.get('Likes')
|
||||
|
||||
last_played = userdata.get('LastPlayedDate')
|
||||
if last_played:
|
||||
last_played = last_played.split('.')[0].replace('T', " ")
|
||||
|
||||
if userdata['Played']:
|
||||
# Playcount is tied to the watch status
|
||||
played = True
|
||||
playcount = userdata['PlayCount']
|
||||
if playcount == 0:
|
||||
playcount = 1
|
||||
|
||||
if last_played is None:
|
||||
last_played = self.get_date_created()
|
||||
|
||||
playback_position = userdata.get('PlaybackPositionTicks')
|
||||
if playback_position:
|
||||
resume = playback_position / 10000000.0
|
||||
|
||||
return {
|
||||
|
||||
'Favorite': favorite,
|
||||
'Likes': likes,
|
||||
'PlayCount': playcount,
|
||||
'Played': played,
|
||||
'LastPlayedDate': last_played,
|
||||
'Resume': resume
|
||||
}
|
||||
|
||||
def get_people(self):
|
||||
# Process People
|
||||
director = []
|
||||
writer = []
|
||||
cast = []
|
||||
|
||||
if 'People' in self.item:
|
||||
for person in self.item['People']:
|
||||
|
||||
type_ = person['Type']
|
||||
name = person['Name']
|
||||
|
||||
if type_ == 'Director':
|
||||
director.append(name)
|
||||
elif type_ == 'Actor':
|
||||
cast.append(name)
|
||||
elif type_ in ('Writing', 'Writer'):
|
||||
writer.append(name)
|
||||
|
||||
return {
|
||||
|
||||
'Director': director,
|
||||
'Writer': writer,
|
||||
'Cast': cast
|
||||
}
|
||||
|
||||
def get_actors(self):
|
||||
cast = []
|
||||
|
||||
if 'People' in self.item:
|
||||
self.get_people_artwork(self.item['People'])
|
||||
|
||||
for person in self.item['People']:
|
||||
|
||||
if person['Type'] == "Actor":
|
||||
cast.append({
|
||||
'name': person['Name'],
|
||||
'role': person.get('Role', "Unknown"),
|
||||
'order': len(cast) + 1,
|
||||
'thumbnail': person['imageurl']
|
||||
})
|
||||
|
||||
return cast
|
||||
|
||||
def get_media_streams(self):
|
||||
|
||||
video_tracks = []
|
||||
audio_tracks = []
|
||||
subtitle_languages = []
|
||||
|
||||
try:
|
||||
media_streams = self.item['MediaSources'][0]['MediaStreams']
|
||||
|
||||
except KeyError:
|
||||
if not self.item.get("MediaStreams"):
|
||||
return None
|
||||
media_streams = self.item['MediaStreams']
|
||||
|
||||
for media_stream in media_streams:
|
||||
# Sort through Video, Audio, Subtitle
|
||||
stream_type = media_stream['Type']
|
||||
|
||||
if stream_type == "Video":
|
||||
self._video_stream(video_tracks, media_stream)
|
||||
|
||||
elif stream_type == "Audio":
|
||||
self._audio_stream(audio_tracks, media_stream)
|
||||
|
||||
elif stream_type == "Subtitle":
|
||||
subtitle_languages.append(media_stream.get('Language', "Unknown"))
|
||||
|
||||
return {
|
||||
|
||||
'video': video_tracks,
|
||||
'audio': audio_tracks,
|
||||
'subtitle': subtitle_languages
|
||||
}
|
||||
|
||||
def media_streams(self, video, audio, subtitles):
|
||||
return {
|
||||
'video': video or [],
|
||||
'audio': audio or [],
|
||||
'subtitle': subtitles or []
|
||||
}
|
||||
|
||||
def video_streams(self, tracks, container=None):
|
||||
|
||||
if container:
|
||||
container = container.split(',')[0]
|
||||
|
||||
for track in tracks:
|
||||
|
||||
track.update({
|
||||
'codec': track.get('Codec', "").lower(),
|
||||
'profile': track.get('Profile', "").lower(),
|
||||
'height': track.get('Height'),
|
||||
'width': track.get('Width'),
|
||||
'3d': self.item.get('Video3DFormat'),
|
||||
'aspect': 1.85
|
||||
})
|
||||
|
||||
if "msmpeg4" in track['codec']:
|
||||
track['codec'] = "divx"
|
||||
|
||||
elif "mpeg4" in track['codec']:
|
||||
if "simple profile" in track['profile'] or not track['profile']:
|
||||
track['codec'] = "xvid"
|
||||
|
||||
elif "h264" in track['codec']:
|
||||
if container in ('mp4', 'mov', 'm4v'):
|
||||
track['codec'] = "avc1"
|
||||
|
||||
try:
|
||||
width, height = self.item.get('AspectRatio', track.get('AspectRatio', "0")).split(':')
|
||||
track['aspect'] = round(float(width) / float(height), 6)
|
||||
except (ValueError, ZeroDivisionError):
|
||||
|
||||
if track['width'] and track['height']:
|
||||
track['aspect'] = round(float(track['width'] / track['height']), 6)
|
||||
|
||||
track['duration'] = self.get_runtime()
|
||||
|
||||
return tracks
|
||||
|
||||
def audio_streams(self, tracks):
|
||||
|
||||
for track in tracks:
|
||||
|
||||
track.update({
|
||||
'codec': track.get('Codec', "").lower(),
|
||||
'profile': track.get('Profile', "").lower(),
|
||||
'channels': track.get('Channels'),
|
||||
'language': track.get('Language')
|
||||
})
|
||||
|
||||
if "dts-hd ma" in track['profile']:
|
||||
track['codec'] = "dtshd_ma"
|
||||
|
||||
elif "dts-hd hra" in track['profile']:
|
||||
track['codec'] = "dtshd_hra"
|
||||
|
||||
return tracks
|
||||
|
||||
def get_runtime(self):
|
||||
|
||||
try:
|
||||
runtime = self.item['RunTimeTicks'] / 10000000.0
|
||||
|
||||
except KeyError:
|
||||
runtime = self.item.get('CumulativeRunTimeTicks', 0) / 10000000.0
|
||||
|
||||
return runtime
|
||||
|
||||
@classmethod
|
||||
def adjust_resume(cls, resume_seconds):
|
||||
|
||||
resume = 0
|
||||
if resume_seconds:
|
||||
resume = round(float(resume_seconds), 6)
|
||||
jumpback = int(settings('resumeJumpBack'))
|
||||
if resume > jumpback:
|
||||
# To avoid negative bookmark
|
||||
resume = resume - jumpback
|
||||
|
||||
return resume
|
||||
|
||||
def get_studios(self):
|
||||
# Process Studios
|
||||
studios = []
|
||||
try:
|
||||
studio = self.item['SeriesStudio']
|
||||
studios.append(self.validate_studio(studio))
|
||||
|
||||
except KeyError:
|
||||
for studio in self.item['Studios']:
|
||||
|
||||
name = studio['Name']
|
||||
studios.append(self.validate_studio(name))
|
||||
|
||||
return studios
|
||||
|
||||
def validate_studio(self, studio_name):
|
||||
# Convert studio for Kodi to properly detect them
|
||||
studios = {
|
||||
|
||||
'abc (us)': "ABC",
|
||||
'fox (us)': "FOX",
|
||||
'mtv (us)': "MTV",
|
||||
'showcase (ca)': "Showcase",
|
||||
'wgn america': "WGN",
|
||||
'bravo (us)': "Bravo",
|
||||
'tnt (us)': "TNT",
|
||||
'comedy central': "Comedy Central (US)"
|
||||
}
|
||||
return studios.get(studio_name.lower(), studio_name)
|
||||
|
||||
def get_genres(self):
|
||||
|
||||
all_genres = ""
|
||||
genres = self.item.get('Genres', self.item.get('SeriesGenres'))
|
||||
|
||||
if genres:
|
||||
all_genres = " / ".join(genres)
|
||||
|
||||
return all_genres
|
||||
|
||||
def get_date_created(self):
|
||||
|
||||
try:
|
||||
date_added = self.item['DateCreated']
|
||||
date_added = date_added.split('.')[0].replace('T', " ")
|
||||
except KeyError:
|
||||
date_added = None
|
||||
|
||||
return date_added
|
||||
|
||||
def get_premiere_date(self):
|
||||
|
||||
try:
|
||||
premiere = self.item['PremiereDate']
|
||||
premiere = premiere.split('.')[0].replace('T', " ")
|
||||
except KeyError:
|
||||
premiere = None
|
||||
|
||||
return premiere
|
||||
|
||||
def get_overview(self, overview=None):
|
||||
|
||||
overview = overview or self.item.get('Overview')
|
||||
|
||||
if not overview:
|
||||
return
|
||||
|
||||
overview = overview.replace("\"", "\'")
|
||||
overview = overview.replace("\n", "[CR]")
|
||||
overview = overview.replace("\r", " ")
|
||||
overview = overview.replace("<br>", "[CR]")
|
||||
|
||||
return overview
|
||||
|
||||
def get_tagline(self):
|
||||
|
||||
try:
|
||||
tagline = self.item['Taglines'][0]
|
||||
except IndexError:
|
||||
tagline = None
|
||||
|
||||
return tagline
|
||||
|
||||
def get_provider(self, name):
|
||||
|
||||
try:
|
||||
provider = self.item['ProviderIds'][name]
|
||||
except KeyError:
|
||||
provider = None
|
||||
|
||||
return provider
|
||||
|
||||
def get_mpaa(self):
|
||||
# Convert more complex cases
|
||||
mpaa = self.item.get('OfficialRating', "")
|
||||
|
||||
if mpaa in ("NR", "UR"):
|
||||
# Kodi seems to not like NR, but will accept Not Rated
|
||||
mpaa = "Not Rated"
|
||||
|
||||
if "FSK-" in mpaa:
|
||||
mpaa = mpaa.replace("-", " ")
|
||||
|
||||
return mpaa
|
||||
|
||||
def get_country(self):
|
||||
|
||||
try:
|
||||
country = self.item['ProductionLocations'][0]
|
||||
except (IndexError, KeyError):
|
||||
country = None
|
||||
|
||||
return country
|
||||
|
||||
def get_file_path(self, path=None):
|
||||
|
||||
if path is None:
|
||||
path = self.item.get('Path')
|
||||
|
||||
if not path:
|
||||
return ""
|
||||
|
||||
if path.startswith('\\\\'):
|
||||
path = path.replace('\\\\', "smb://", 1).replace('\\\\', "\\").replace('\\', "/")
|
||||
|
||||
if 'Container' in self.item:
|
||||
|
||||
if self.item['Container'] == 'dvd':
|
||||
path = "%s/VIDEO_TS/VIDEO_TS.IFO" % path
|
||||
elif self.item['Container'] == 'bluray':
|
||||
path = "%s/BDMV/index.bdmv" % path
|
||||
|
||||
path = path.replace('\\\\', "\\")
|
||||
|
||||
if '\\' in path:
|
||||
path = path.replace('/', "\\")
|
||||
|
||||
if '://' in path:
|
||||
protocol = path.split('://')[0]
|
||||
path = path.replace(protocol, protocol.lower())
|
||||
|
||||
return path
|
||||
|
||||
def get_user_artwork(self, user_id):
|
||||
|
||||
''' Get emby user profile picture.
|
||||
'''
|
||||
return "%s/emby/Users/%s/Images/Primary?Format=original" % (self.server, user_id)
|
||||
|
||||
def get_people_artwork(self, people):
|
||||
|
||||
''' Get people (actor, director, etc) artwork.
|
||||
'''
|
||||
for person in people:
|
||||
|
||||
if 'PrimaryImageTag' in person:
|
||||
|
||||
query = "&MaxWidth=400&MaxHeight=400&Index=0"
|
||||
person['imageurl'] = self.get_artwork(person['Id'], "Primary", person['PrimaryImageTag'], query)
|
||||
else:
|
||||
person['imageurl'] = None
|
||||
|
||||
return people
|
||||
|
||||
def get_all_artwork(self, obj, parent_info=False):
|
||||
|
||||
''' Get all artwork possible. If parent_info is True,
|
||||
it will fill missing artwork with parent artwork.
|
||||
|
||||
obj is from objects.Objects().map(item, 'Artwork')
|
||||
'''
|
||||
query = ""
|
||||
all_artwork = {
|
||||
'Primary': "",
|
||||
'BoxRear': "",
|
||||
'Art': "",
|
||||
'Banner': "",
|
||||
'Logo': "",
|
||||
'Thumb': "",
|
||||
'Disc': "",
|
||||
'Backdrop': []
|
||||
}
|
||||
|
||||
if settings('compressArt.bool'):
|
||||
query = "&Quality=90"
|
||||
|
||||
if not settings('enableCoverArt.bool'):
|
||||
query += "&EnableImageEnhancers=false"
|
||||
|
||||
all_artwork['Backdrop'] = self.get_backdrops(obj['Id'], obj['BackdropTags'] or [], query)
|
||||
|
||||
for artwork in obj['Tags'] or []:
|
||||
all_artwork[artwork] = self.get_artwork(obj['Id'], artwork, obj['Tags'][artwork])
|
||||
|
||||
if parent_info:
|
||||
if not all_artwork['Backdrop'] and obj['ParentBackdropId']:
|
||||
all_artwork['Backdrop'] = self.get_backdrops(obj['ParentBackdropId'], obj['ParentBackdropTags'], query)
|
||||
|
||||
for art in ('Logo', 'Art', 'Thumb'):
|
||||
if not all_artwork[art] and obj['Parent%sId' % art]:
|
||||
all_artwork[art] = self.get_artwork(obj['Parent%sId' % art], art, obj['Parent%sTag' % art], query)
|
||||
|
||||
if obj.get('SeriesTag'):
|
||||
all_artwork['Series.Primary'] = self.get_artwork(obj['SeriesId'], "Primary", obj['SeriesTag'], query)
|
||||
|
||||
if not all_artwork['Primary']:
|
||||
all_artwork['Primary'] = all_artwork['Series.Primary']
|
||||
|
||||
elif not all_artwork['Primary'] and obj.get('AlbumId'):
|
||||
all_artwork['Primary'] = self.get_artwork(obj['AlbumId'], "Primary", obj['AlbumTag'], query)
|
||||
|
||||
return all_artwork
|
||||
|
||||
def get_backdrops(self, item_id, tags, query=None):
|
||||
|
||||
''' Get backdrops based of "BackdropImageTags" in the emby object.
|
||||
'''
|
||||
backdrops = []
|
||||
|
||||
if item_id is None:
|
||||
return backdrops
|
||||
|
||||
for index, tag in enumerate(tags):
|
||||
|
||||
artwork = "%s/emby/Items/%s/Images/Backdrop/%s?Format=original&Tag=%s%s" % (self.server, item_id, index, tag, query or "")
|
||||
backdrops.append(artwork)
|
||||
|
||||
return backdrops
|
||||
|
||||
def get_artwork(self, item_id, image, tag=None, query=None):
|
||||
|
||||
''' Get any type of artwork: Primary, Art, Banner, Logo, Thumb, Disc
|
||||
'''
|
||||
if item_id is None:
|
||||
return ""
|
||||
|
||||
url = "%s/emby/Items/%s/Images/%s/0?Format=original" % (self.server, item_id, image)
|
||||
|
||||
if tag is not None:
|
||||
url += "&Tag=%s" % tag
|
||||
|
||||
if query is not None:
|
||||
url += query or ""
|
||||
|
||||
return url
|
10
resources/lib/helper/exceptions.py
Normal file
10
resources/lib/helper/exceptions.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
#################################################################################################
|
||||
|
||||
class LibraryException(Exception):
|
||||
# Emby library sync exception
|
||||
def __init__(self, status):
|
||||
self.status = status
|
||||
|
||||
|
96
resources/lib/helper/loghandler.py
Normal file
96
resources/lib/helper/loghandler.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##################################################################################################
|
||||
|
||||
import logging
|
||||
import xbmc
|
||||
|
||||
import database
|
||||
from . import window
|
||||
|
||||
##################################################################################################
|
||||
|
||||
|
||||
def config():
|
||||
|
||||
logger = logging.getLogger('EMBY')
|
||||
logger.addHandler(LogHandler())
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class LogHandler(logging.StreamHandler):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
logging.StreamHandler.__init__(self)
|
||||
self.setFormatter(MyFormatter())
|
||||
|
||||
self.sensitive = {'Token': [], 'Server': []}
|
||||
|
||||
for server in database.get_credentials()['Servers']:
|
||||
|
||||
if server.get('AccessToken'):
|
||||
self.sensitive['Token'].append(server['AccessToken'])
|
||||
|
||||
self.sensitive['Server'].append(server['LocalAddress'].split('://')[1])
|
||||
self.sensitive['Server'].append(server['RemoteAddress'].split('://')[1])
|
||||
|
||||
if server.get('ManualAddress'):
|
||||
self.sensitive['Server'].append(server['ManualAddress'].split('://')[1])
|
||||
|
||||
|
||||
def emit(self, record):
|
||||
|
||||
if self._get_log_level(record.levelno):
|
||||
string = self.format(record)
|
||||
|
||||
for server in self.sensitive['Server']:
|
||||
string = string.replace(server or "{server}", "{emby-server}")
|
||||
|
||||
for token in self.sensitive['Token']:
|
||||
string = string.replace(token or "{token}", "{emby-token}")
|
||||
try:
|
||||
xbmc.log(string, level=xbmc.LOGNOTICE)
|
||||
except UnicodeEncodeError:
|
||||
xbmc.log(string.encode('utf-8'), level=xbmc.LOGNOTICE)
|
||||
|
||||
@classmethod
|
||||
def _get_log_level(cls, level):
|
||||
|
||||
levels = {
|
||||
logging.ERROR: 0,
|
||||
logging.WARNING: 0,
|
||||
logging.INFO: 1,
|
||||
logging.DEBUG: 2
|
||||
}
|
||||
try:
|
||||
log_level = int(window('emby_logLevel'))
|
||||
except ValueError:
|
||||
log_level = 0
|
||||
|
||||
return log_level >= levels[level]
|
||||
|
||||
|
||||
class MyFormatter(logging.Formatter):
|
||||
|
||||
def __init__(self, fmt="%(name)s -> %(message)s"):
|
||||
|
||||
logging.Formatter.__init__(self, fmt)
|
||||
|
||||
def format(self, record):
|
||||
|
||||
# Save the original format configured by the user
|
||||
# when the logger formatter was instantiated
|
||||
format_orig = self._fmt
|
||||
|
||||
# Replace the original format with one customized by logging level
|
||||
if record.levelno in (logging.DEBUG, logging.ERROR):
|
||||
self._fmt = '%(name)s -> %(levelname)s:: %(message)s'
|
||||
|
||||
# Call the original formatter class to do the grunt work
|
||||
result = logging.Formatter.format(self, record)
|
||||
|
||||
# Restore the original format configured by the user
|
||||
self._fmt = format_orig
|
||||
|
||||
return result
|
475
resources/lib/helper/playutils.py
Normal file
475
resources/lib/helper/playutils.py
Normal file
|
@ -0,0 +1,475 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
#################################################################################################
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
import xbmcgui
|
||||
|
||||
import api
|
||||
import database
|
||||
import client
|
||||
from . import _, settings, window
|
||||
from libraries import requests
|
||||
from downloader import TheVoid
|
||||
from emby import Emby
|
||||
|
||||
#################################################################################################
|
||||
|
||||
LOG = logging.getLogger("EMBY."+__name__)
|
||||
|
||||
#################################################################################################
|
||||
|
||||
|
||||
def set_properties(item, method, server_id=None):
|
||||
|
||||
''' Set all properties for playback detection.
|
||||
'''
|
||||
info = item.get('PlaybackInfo') or {}
|
||||
|
||||
current = window('emby_play.json') or []
|
||||
current.append({
|
||||
'Type': item['Type'],
|
||||
'Id': item['Id'],
|
||||
'Path': info['Path'],
|
||||
'PlayMethod': method,
|
||||
'MediaSourceId': info.get('MediaSourceId', item['Id']),
|
||||
'Runtime': item.get('RunTimeTicks'),
|
||||
'PlaySessionId': info.get('PlaySessionId', str(uuid4()).replace("-", "")),
|
||||
'ServerId': server_id,
|
||||
'DeviceId': client.get_device_id(),
|
||||
'SubsMapping': info.get('Subtitles'),
|
||||
'AudioStreamIndex': info.get('AudioStreamIndex'),
|
||||
'SubtitleStreamIndex': info.get('SubtitleStreamIndex')
|
||||
})
|
||||
|
||||
window('emby_play.json', current)
|
||||
|
||||
class PlayUtils(object):
|
||||
|
||||
|
||||
def __init__(self, item, force_transcode=False, server_id=None, server=None, token=None):
|
||||
|
||||
''' Item will be updated with the property PlaybackInfo, which
|
||||
holds all the playback information.
|
||||
'''
|
||||
self.item = item
|
||||
self.item['PlaybackInfo'] = {}
|
||||
self.info = {
|
||||
'ServerId': server_id,
|
||||
'ServerAddress': server,
|
||||
'ForceTranscode': force_transcode,
|
||||
'Token': token or TheVoid('GetToken', {'ServerId': server_id}).get()
|
||||
}
|
||||
|
||||
def get_sources(self, source_id=None):
|
||||
|
||||
''' Return sources based on the optional source_id or the device profile.
|
||||
'''
|
||||
params = {
|
||||
'ServerId': self.info['ServerId'],
|
||||
'Id': self.item['Id'],
|
||||
'Profile': self.get_device_profile()
|
||||
}
|
||||
info = TheVoid('GetPlaybackInfo', params).get()
|
||||
LOG.info(info)
|
||||
self.info['PlaySessionId'] = info['PlaySessionId']
|
||||
sources = []
|
||||
|
||||
if not info.get('MediaSources'):
|
||||
LOG.info("No MediaSources found.")
|
||||
|
||||
elif source_id:
|
||||
for source in info:
|
||||
|
||||
if source['Id'] == source_id:
|
||||
sources.append(source)
|
||||
|
||||
break
|
||||
|
||||
elif not self.is_selection(info) or len(info['MediaSources']) == 1:
|
||||
|
||||
LOG.info("Skip source selection.")
|
||||
sources.append(info['MediaSources'][0])
|
||||
|
||||
else:
|
||||
sources.extend([x for x in info['MediaSources']])
|
||||
|
||||
return sources
|
||||
|
||||
def select_source(self, sources):
|
||||
|
||||
if len(sources) > 1:
|
||||
selection = []
|
||||
|
||||
for source in sources:
|
||||
selection.append(source.get('Name', "na"))
|
||||
|
||||
resp = xbmcgui.Dialog().select(_(33130), selection)
|
||||
if resp > -1:
|
||||
source = sources[resp]
|
||||
else:
|
||||
log.info("No media source selected.")
|
||||
return False
|
||||
else:
|
||||
source = sources[0]
|
||||
|
||||
self.get(source)
|
||||
|
||||
return source
|
||||
|
||||
def is_selection(self, sources):
|
||||
|
||||
''' Do not allow source selection for.
|
||||
'''
|
||||
if self.item['MediaType'] != 'Video':
|
||||
LOG.debug("MediaType is not a video.")
|
||||
|
||||
return False
|
||||
|
||||
elif self.item['Type'] == 'TvChannel':
|
||||
LOG.debug("TvChannel detected.")
|
||||
|
||||
return False
|
||||
|
||||
elif len(sources) == 1 and sources[0]['Type'] == 'Placeholder':
|
||||
LOG.debug("Placeholder detected.")
|
||||
|
||||
return False
|
||||
|
||||
elif 'SourceType' in self.item and self.item['SourceType'] != 'Library':
|
||||
LOG.debug("SourceType not from library.")
|
||||
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_file_exists(self, source):
|
||||
|
||||
path = self.direct_play(source)
|
||||
|
||||
if xbmcvfs.exists(self.info['Path']):
|
||||
LOG.info("Path exists.")
|
||||
|
||||
return True
|
||||
|
||||
LOG.info("Failed to find file.")
|
||||
|
||||
return False
|
||||
|
||||
def is_strm(self, source):
|
||||
|
||||
if source['Container'] == 'strm' or self.item['Path'].endswith('.strm'):
|
||||
LOG.info("strm detected")
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get(self, source):
|
||||
|
||||
''' The server returns sources based on the MaxStreamingBitrate value and other filters.
|
||||
'''
|
||||
self.info['MediaSourceId'] = source['Id']
|
||||
|
||||
if source['RequiresOpening']:
|
||||
source = self.live_stream(source)
|
||||
|
||||
if source['SupportsDirectPlay'] and (self.is_strm(source) or not settings('playFromStream.bool') and self.is_file_exists(source)):
|
||||
|
||||
LOG.info("--[ direct play ]")
|
||||
self.direct_play(source)
|
||||
|
||||
elif source['SupportsDirectStream']:
|
||||
|
||||
LOG.info("--[ direct stream ]")
|
||||
self.direct_url(source)
|
||||
|
||||
else:
|
||||
LOG.info("--[ transcode ]")
|
||||
self.transcode(source)
|
||||
|
||||
self.info['AudioStreamIndex'] = source.get('DefaultAudioStreamIndex')
|
||||
self.info['SubtitleStreamIndex'] = source.get('DefaultSubtitleStreamIndex')
|
||||
self.item['PlaybackInfo'].update(self.info)
|
||||
|
||||
def live_stream(self, source):
|
||||
|
||||
''' Get live stream media info.
|
||||
'''
|
||||
params = {
|
||||
'ServerId': self.info['ServerId'],
|
||||
'Id': self.item['Id'],
|
||||
'Profile': self.get_device_profile(),
|
||||
'PlaySessionId': self.info['PlaySessionId'],
|
||||
'Token': source['OpenToken']
|
||||
}
|
||||
info = TheVoid('GetLiveStream', params).get()
|
||||
LOG.info(info)
|
||||
|
||||
if info['RequiresClosing']:
|
||||
self.info['LiveStreamId'] = info['LiveStreamId']
|
||||
|
||||
return info['MediaSource']
|
||||
|
||||
def transcode(self, source):
|
||||
|
||||
if not 'TranscodingUrl' in source:
|
||||
raise Exception("use get_sources to get transcoding url")
|
||||
|
||||
self.info['Method'] = "Transcode"
|
||||
base, params = source['TranscodingUrl'].split('?')
|
||||
self.info['Path'] = "%s/emby%s?%s" % (self.info['ServerAddress'], base.replace('stream', "master"), params)
|
||||
self.info['Path'] += "&maxWidth=%s&maxHeight=%s" % (self.get_resolution())
|
||||
|
||||
return self.info['Path']
|
||||
|
||||
def direct_play(self, source):
|
||||
|
||||
API = api.API(self.item, self.info['ServerAddress'])
|
||||
self.info['Method'] = "DirectPlay"
|
||||
self.info['Path'] = API.get_file_path()
|
||||
|
||||
return self.info['Path']
|
||||
|
||||
def direct_url(self, source):
|
||||
|
||||
self.info['Method'] = "DirectStream"
|
||||
|
||||
if self.item['Type'] == "Audio":
|
||||
self.info['Path'] = ("%s/emby/Audio/%s/stream.%s?static=true&api_key=%s" %
|
||||
(self.info['ServerAddress'], self.item['Id'],
|
||||
source['Container'].split(',')[0],
|
||||
self.info['Token']))
|
||||
else:
|
||||
self.info['Path'] = ("%s/emby/Videos/%s/stream?static=true&api_key=%s" %
|
||||
(self.info['ServerAddress'], self.item['Id'], self.info['Token']))
|
||||
|
||||
return self.info['Path']
|
||||
|
||||
def get_bitrate(self):
|
||||
|
||||
''' Get the video quality based on add-on settings.
|
||||
Max bit rate supported by server: 2147483 (max signed 32bit integer)
|
||||
'''
|
||||
bitrate = [664, 996, 1320, 2000, 3200,
|
||||
4700, 6200, 7700, 9200, 10700,
|
||||
12200, 13700, 15200, 16700, 18200,
|
||||
20000, 25000, 30000, 35000, 40000,
|
||||
100000, 1000000, 2147483]
|
||||
return bitrate[int(settings('videoBitrate') or 22)]
|
||||
|
||||
def get_resolution(self):
|
||||
return int(xbmc.getInfoLabel('System.ScreenWidth')), int(xbmc.getInfoLabel('System.ScreenHeight'))
|
||||
|
||||
def get_device_profile(self):
|
||||
|
||||
''' Get device profile based on the add-on settings.
|
||||
'''
|
||||
profile = {
|
||||
"Name": "Kodi",
|
||||
"MaxStreamingBitrate": self.get_bitrate() * 1000,
|
||||
"MusicStreamingTranscodingBitrate": 1280000,
|
||||
"TimelineOffsetSeconds": 5,
|
||||
"TranscodingProfiles": [
|
||||
{
|
||||
"Type": "Audio"
|
||||
},
|
||||
{
|
||||
"Container": "m3u8",
|
||||
"Type": "Video"
|
||||
},
|
||||
{
|
||||
"Container": "jpeg",
|
||||
"Type": "Photo"
|
||||
}
|
||||
],
|
||||
"DirectPlayProfiles": [
|
||||
{
|
||||
"Type": "Video"
|
||||
},
|
||||
{
|
||||
"Type": "Video",
|
||||
"Container": "strm"
|
||||
},
|
||||
{
|
||||
"Type": "Audio"
|
||||
},
|
||||
{
|
||||
"Type": "Photo"
|
||||
}
|
||||
],
|
||||
"ResponseProfiles": [],
|
||||
"ContainerProfiles": [],
|
||||
"CodecProfiles": [],
|
||||
"SubtitleProfiles": [
|
||||
{
|
||||
"Format": "srt",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "srt",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "ass",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "ass",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "sub",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "sub",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "ssa",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "ssa",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "smi",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "smi",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "pgssub",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "pgssub",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "dvdsub",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "dvdsub",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "pgs",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "pgs",
|
||||
"Method": "External"
|
||||
}
|
||||
]
|
||||
}
|
||||
if settings('transcode_h265.bool'):
|
||||
profile['DirectPlayProfiles'][0]['VideoCodec'] = "h264,mpeg4,mpeg2video"
|
||||
|
||||
if settings('transcodeHi10P.bool'):
|
||||
profile['CodecProfiles'].append(
|
||||
{
|
||||
'Type': 'Video',
|
||||
'Conditions': [
|
||||
{
|
||||
'Condition': "LessThanEqual",
|
||||
'Property': "VideoBitDepth",
|
||||
'Value': "8"
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
if self.info['ForceTranscode'] or self.item['LocationType'] == 'Remote':
|
||||
profile['DirectPlayProfiles'] = [
|
||||
{
|
||||
"Type": "Video",
|
||||
"Container": "strm"
|
||||
}
|
||||
]
|
||||
|
||||
return profile
|
||||
|
||||
def set_external_subs(self, source, listitem):
|
||||
|
||||
''' Try to download external subs locally so we can label them.
|
||||
Since Emby returns all possible tracks together, sort them.
|
||||
IsTextSubtitleStream if true, is available to download from server.
|
||||
'''
|
||||
if not source['MediaStreams']:
|
||||
return
|
||||
|
||||
subs = []
|
||||
mapping = {}
|
||||
kodi = 0
|
||||
|
||||
for stream in source['MediaStreams']:
|
||||
|
||||
if stream['Type'] == 'Subtitle' and stream['IsExternal'] and stream['IsTextSubtitleStream']:
|
||||
index = stream['Index']
|
||||
|
||||
if 'DeliveryUrl' in stream:
|
||||
url = "%s/emby%s" % (self.info['ServerAddress'], stream['DeliveryUrl'])
|
||||
else:
|
||||
url = ("%s/emby/Videos/%s/%s/Subtitles/%s/Stream.%s?api_key=%s" %
|
||||
(self.info['ServerAddress'], self.item['Id'], source['Id'], index, stream['Codec'], self.info['Token']))
|
||||
|
||||
if url is None:
|
||||
continue
|
||||
|
||||
LOG.info("[ subtitles/%s ] %s", index, url)
|
||||
|
||||
if 'Language' in stream:
|
||||
filename = "Stream.%s.%s" % (stream['Language'].encode('utf-8'), stream['Codec'])
|
||||
|
||||
try:
|
||||
subs.append(self.download_external_subs(url, filename))
|
||||
except Exception as error:
|
||||
LOG.error(error)
|
||||
subs.append(url)
|
||||
else:
|
||||
subs.append(url)
|
||||
|
||||
mapping[kodi] = index
|
||||
kodi += 1
|
||||
|
||||
listitem.setSubtitles(subs)
|
||||
self.item['PlaybackInfo']['Subtitles'] = mapping
|
||||
|
||||
|
||||
@classmethod
|
||||
def download_external_subs(cls, src, filename):
|
||||
|
||||
''' Download external subtitles to temp folder
|
||||
to be able to have proper names to streams.
|
||||
'''
|
||||
temp = xbmc.translatePath("special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8')
|
||||
|
||||
if not xbmcvfs.exists(temp):
|
||||
xbmcvfs.mkdir(temp)
|
||||
|
||||
path = os.path.join(temp, filename)
|
||||
|
||||
try:
|
||||
response = requests.get(src, stream=True)
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
raise
|
||||
else:
|
||||
response.encoding = 'utf-8'
|
||||
with open(path, 'wb') as f:
|
||||
f.write(response.content)
|
||||
del response
|
||||
|
||||
return path
|
53
resources/lib/helper/translate.py
Normal file
53
resources/lib/helper/translate.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
##################################################################################################
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
|
||||
##################################################################################################
|
||||
|
||||
LOG = logging.getLogger('EMBY.'+__name__)
|
||||
|
||||
##################################################################################################
|
||||
|
||||
def _(string):
|
||||
|
||||
''' Get add-on string. Returns in unicode.
|
||||
'''
|
||||
if type(string) != int:
|
||||
string = STRINGS[string]
|
||||
|
||||
result = xbmcaddon.Addon('plugin.video.emby').getLocalizedString(string)
|
||||
|
||||
if not result:
|
||||
result = xbmc.getLocalizedString(string)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
STRINGS = {
|
||||
'addon_name': 29999,
|
||||
'playback_mode': 30511,
|
||||
'empty_user': 30613,
|
||||
'empty_user_pass': 30608,
|
||||
'empty_server': 30617,
|
||||
'network_credentials': 30517,
|
||||
'invalid_auth': 33009,
|
||||
'addon_mode': 33036,
|
||||
'native_mode': 33037,
|
||||
'cancel': 30606,
|
||||
'username': 30024,
|
||||
'password': 30602,
|
||||
'gathering': 33021,
|
||||
'boxsets': 30185,
|
||||
'movies': 30302,
|
||||
'tvshows': 30305,
|
||||
'fav_movies': 30180,
|
||||
'fav_tvshows': 30181,
|
||||
'fav_episodes': 30182
|
||||
}
|
389
resources/lib/helper/utils.py
Normal file
389
resources/lib/helper/utils.py
Normal file
|
@ -0,0 +1,389 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
#################################################################################################
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
from uuid import uuid4
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
import xbmcvfs
|
||||
|
||||
from . import _
|
||||
|
||||
#################################################################################################
|
||||
|
||||
LOG = logging.getLogger("EMBY."+__name__)
|
||||
|
||||
#################################################################################################
|
||||
|
||||
def addon_id():
|
||||
return "plugin.video.emby"
|
||||
|
||||
def kodi_version():
|
||||
return xbmc.getInfoLabel('System.BuildVersion')[:2]
|
||||
|
||||
def window(key, value=None, clear=False, window_id=10000):
|
||||
|
||||
''' Get or set window properties.
|
||||
'''
|
||||
window = xbmcgui.Window(window_id)
|
||||
|
||||
if clear:
|
||||
|
||||
LOG.debug("--[ window clear: %s ]", key)
|
||||
window.clearProperty(key)
|
||||
elif value is not None:
|
||||
if key.endswith('.json'):
|
||||
|
||||
key = key.replace('.json', "")
|
||||
value = json.dumps(value)
|
||||
|
||||
elif key.endswith('.bool'):
|
||||
|
||||
key = key.replace('.bool', "")
|
||||
value = "true" if value else "false"
|
||||
|
||||
window.setProperty(key, value)
|
||||
else:
|
||||
result = window.getProperty(key.replace('.json', "").replace('.bool', ""))
|
||||
|
||||
if result:
|
||||
if key.endswith('.json'):
|
||||
result = json.loads(result)
|
||||
elif key.endswith('.bool'):
|
||||
result = result in ("true", "1")
|
||||
|
||||
return result
|
||||
|
||||
def settings(setting, value=None):
|
||||
|
||||
''' Get or add add-on settings.
|
||||
getSetting returns unicode object.
|
||||
'''
|
||||
addon = xbmcaddon.Addon(addon_id())
|
||||
|
||||
if value is not None:
|
||||
if setting.endswith('.bool'):
|
||||
|
||||
setting = setting.replace('.bool', "")
|
||||
value = "true" if value else "false"
|
||||
|
||||
addon.setSetting(setting, value)
|
||||
else:
|
||||
result = addon.getSetting(setting.replace('.bool', ""))
|
||||
|
||||
if result and setting.endswith('.bool'):
|
||||
result = result in ("true", "1")
|
||||
|
||||
return result
|
||||
|
||||
def create_id():
|
||||
return uuid4()
|
||||
|
||||
def find(dict, item):
|
||||
|
||||
''' Find value in dictionary.
|
||||
'''
|
||||
if item in dict:
|
||||
return dict[item]
|
||||
|
||||
for key,value in sorted(dict.iteritems(), key=lambda (k,v): (v,k)):
|
||||
if re.match(key, item):
|
||||
return dict[key]
|
||||
|
||||
def event(method, data=None):
|
||||
|
||||
''' Data is a dictionary.
|
||||
'''
|
||||
data = data or {}
|
||||
xbmc.executebuiltin('NotifyAll(plugin.video.emby, %s, "[%s]")' % (method, json.dumps(data).replace('"', '\\"')))
|
||||
LOG.debug("---[ event: %s ] %s", method, data)
|
||||
|
||||
def dialog(dialog_type, *args, **kwargs):
|
||||
|
||||
d = xbmcgui.Dialog()
|
||||
|
||||
if "icon" in kwargs:
|
||||
kwargs['icon'] = kwargs['icon'].replace("{emby}",
|
||||
"special://home/addons/plugin.video.emby/icon.png")
|
||||
if "heading" in kwargs:
|
||||
kwargs['heading'] = kwargs['heading'].replace("{emby}", _('addon_name'))
|
||||
|
||||
types = {
|
||||
'yesno': d.yesno,
|
||||
'ok': d.ok,
|
||||
'notification': d.notification,
|
||||
'input': d.input,
|
||||
'select': d.select,
|
||||
'numeric': d.numeric,
|
||||
'multi': d.multiselect
|
||||
}
|
||||
return types[dialog_type](*args, **kwargs)
|
||||
|
||||
def should_stop():
|
||||
|
||||
''' Checkpoint during the sync process.
|
||||
'''
|
||||
if xbmc.Monitor().abortRequested():
|
||||
return True
|
||||
|
||||
if window('emby_should_stop.bool'):
|
||||
LOG.info("exiiiiitttinggg")
|
||||
return True
|
||||
|
||||
if not window('emby_online.bool'):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
class JSONRPC(object):
|
||||
|
||||
version = 1
|
||||
jsonrpc = "2.0"
|
||||
|
||||
def __init__(self, method, **kwargs):
|
||||
|
||||
self.method = method
|
||||
|
||||
for arg in kwargs:
|
||||
self.arg = arg
|
||||
|
||||
def _query(self):
|
||||
|
||||
query = {
|
||||
'jsonrpc': self.jsonrpc,
|
||||
'id': self.version,
|
||||
'method': self.method,
|
||||
}
|
||||
if self.params is not None:
|
||||
query['params'] = self.params
|
||||
|
||||
return json.dumps(query)
|
||||
|
||||
def execute(self, params=None):
|
||||
|
||||
self.params = params
|
||||
return json.loads(xbmc.executeJSONRPC(self._query()))
|
||||
|
||||
def validate(path):
|
||||
|
||||
''' Verify if path is accessible.
|
||||
'''
|
||||
if window('emby_pathverified.bool'):
|
||||
return True
|
||||
|
||||
path = path if os.path.supports_unicode_filenames else path.encode('utf-8')
|
||||
|
||||
if not xbmcvfs.exists(path):
|
||||
if dialog(type_="yesno",
|
||||
heading="{emby}",
|
||||
line1="%s %s. %s" % (_(33047), path, _(33048))):
|
||||
|
||||
return False
|
||||
|
||||
window('emby_pathverified', "true")
|
||||
|
||||
return True
|
||||
|
||||
def values(item, keys):
|
||||
|
||||
''' Grab the values in the item for a list of keys {key},{key1}....
|
||||
If the key has no brackets, the key will be passed as is.
|
||||
'''
|
||||
return (item[key.replace('{', "").replace('}', "")] if type(key) == str and key.startswith('{') else key for key in keys)
|
||||
|
||||
def indent(elem, level=0):
|
||||
|
||||
''' Prettify xml docs.
|
||||
'''
|
||||
try:
|
||||
i = "\n" + level * " "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
for elem in elem:
|
||||
indent(elem, level + 1)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def write_xml(content, file):
|
||||
with open(file, 'w') as infile:
|
||||
|
||||
content = content.replace("'", '"')
|
||||
content = content.replace('?>', ' standalone="yes" ?>', 1)
|
||||
infile.write(content)
|
||||
|
||||
def delete_build():
|
||||
|
||||
''' Delete objects from kodi cache
|
||||
'''
|
||||
LOG.debug("--[ delete objects ]")
|
||||
path = xbmc.translatePath('special://temp/emby/').decode('utf-8')
|
||||
dirs, files = xbmcvfs.listdir(path)
|
||||
|
||||
delete_recursive(path, dirs)
|
||||
|
||||
for file in files:
|
||||
xbmcvfs.delete(os.path.join(path, file.decode('utf-8')))
|
||||
|
||||
def delete_recursive(path, dirs):
|
||||
|
||||
''' Delete files and dirs recursively.
|
||||
'''
|
||||
for directory in dirs:
|
||||
|
||||
dirs2, files = xbmcvfs.listdir(os.path.join(path, directory.decode('utf-8')))
|
||||
|
||||
for file in files:
|
||||
xbmcvfs.delete(os.path.join(path, directory.decode('utf-8'), file.decode('utf-8')))
|
||||
|
||||
delete_recursive(os.path.join(path, directory.decode('utf-8')), dirs2)
|
||||
xbmcvfs.rmdir(os.path.join(path, directory.decode('utf-8')))
|
||||
|
||||
def normalize_string(text):
|
||||
|
||||
''' For theme media, do not modify unless
|
||||
modified in TV Tunes.
|
||||
'''
|
||||
text = text.replace(":", "")
|
||||
text = text.replace("/", "-")
|
||||
text = text.replace("\\", "-")
|
||||
text = text.replace("<", "")
|
||||
text = text.replace(">", "")
|
||||
text = text.replace("*", "")
|
||||
text = text.replace("?", "")
|
||||
text = text.replace('|', "")
|
||||
text = text.strip()
|
||||
# Remove dots from the last character as windows can not have directories
|
||||
# with dots at the end
|
||||
text = text.rstrip('.')
|
||||
text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
|
||||
|
||||
return text
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
#################################################################################################
|
||||
# Utility methods
|
||||
|
||||
def getScreensaver():
|
||||
# Get the current screensaver value
|
||||
result = JSONRPC('Settings.getSettingValue').execute({'setting': "screensaver.mode"})
|
||||
try:
|
||||
return result['result']['value']
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
def setScreensaver(value):
|
||||
# Toggle the screensaver
|
||||
params = {
|
||||
'setting': "screensaver.mode",
|
||||
'value': value
|
||||
}
|
||||
result = JSONRPC('Settings.setSettingValue').execute(params)
|
||||
log.info("Toggling screensaver: %s %s" % (value, result))
|
||||
|
||||
def convertDate(date):
|
||||
try:
|
||||
date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ")
|
||||
except (ImportError, TypeError):
|
||||
# TypeError: attribute of type 'NoneType' is not callable
|
||||
# Known Kodi/python error
|
||||
date = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6]))
|
||||
|
||||
return date
|
||||
|
||||
|
||||
def indent(elem, level=0):
|
||||
# Prettify xml trees
|
||||
i = "\n" + level*" "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
for elem in elem:
|
||||
indent(elem, level+1)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
def profiling(sortby="cumulative"):
|
||||
# Will print results to Kodi log
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
import cProfile
|
||||
import pstats
|
||||
|
||||
pr = cProfile.Profile()
|
||||
|
||||
pr.enable()
|
||||
result = func(*args, **kwargs)
|
||||
pr.disable()
|
||||
|
||||
s = StringIO.StringIO()
|
||||
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
|
||||
ps.print_stats()
|
||||
log.info(s.getvalue())
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
#################################################################################################
|
||||
# Addon utilities
|
||||
|
||||
|
||||
def verify_advancedsettings():
|
||||
# Track the existance of <cleanonupdate>true</cleanonupdate>
|
||||
# incompatible with plugin paths
|
||||
log.info("verifying advanced settings")
|
||||
if settings('useDirectPaths') != "0": return
|
||||
|
||||
path = xbmc.translatePath("special://userdata/").decode('utf-8')
|
||||
xmlpath = "%sadvancedsettings.xml" % path
|
||||
|
||||
try:
|
||||
xmlparse = etree.parse(xmlpath)
|
||||
except: # Document is blank or missing
|
||||
return
|
||||
else:
|
||||
root = xmlparse.getroot()
|
||||
|
||||
video = root.find('videolibrary')
|
||||
if video is not None:
|
||||
cleanonupdate = video.find('cleanonupdate')
|
||||
if cleanonupdate is not None and cleanonupdate.text == "true":
|
||||
log.warn("cleanonupdate disabled")
|
||||
video.remove(cleanonupdate)
|
||||
|
||||
try:
|
||||
indent(root)
|
||||
except: pass
|
||||
etree.ElementTree(root).write(xmlpath)
|
||||
|
||||
xbmcgui.Dialog().ok(heading=language(29999), line1=language(33097))
|
||||
xbmc.executebuiltin('RestartApp')
|
||||
return True
|
||||
return
|
||||
"""
|
146
resources/lib/helper/wrapper.py
Normal file
146
resources/lib/helper/wrapper.py
Normal file
|
@ -0,0 +1,146 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
#################################################################################################
|
||||
|
||||
import logging
|
||||
|
||||
import xbmcgui
|
||||
|
||||
from . import _, LibraryException
|
||||
from utils import should_stop
|
||||
|
||||
#################################################################################################
|
||||
|
||||
LOG = logging.getLogger("EMBY."+__name__)
|
||||
|
||||
#################################################################################################
|
||||
|
||||
def progress(message=None):
|
||||
|
||||
''' Will start and close the progress dialog.
|
||||
'''
|
||||
def decorator(func):
|
||||
def wrapper(self, item=None, *args, **kwargs):
|
||||
|
||||
dialog = xbmcgui.DialogProgressBG()
|
||||
|
||||
if item and type(item) == dict:
|
||||
|
||||
dialog.create(_('addon_name'), "%s %s" % (_('gathering'), item['Name']))
|
||||
LOG.info("Processing %s: %s", item['Name'], item['Id'])
|
||||
else:
|
||||
dialog.create(_('addon_name'), message)
|
||||
LOG.info("Processing %s", message)
|
||||
|
||||
if item:
|
||||
args = (item,) + args
|
||||
|
||||
kwargs['dialog'] = dialog
|
||||
result = func(self, *args, **kwargs)
|
||||
dialog.close()
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def catch(errors=(Exception,)):
|
||||
|
||||
''' Wrapper to catch exceptions and return using catch
|
||||
'''
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except errors as error:
|
||||
LOG.exception(error)
|
||||
|
||||
raise Exception("Caught exception")
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def stop(default=None):
|
||||
|
||||
''' Wrapper to catch exceptions and return using catch
|
||||
'''
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
try:
|
||||
if should_stop():
|
||||
raise Exception
|
||||
|
||||
except Exception as error:
|
||||
|
||||
if default is not None:
|
||||
return default
|
||||
|
||||
raise LibraryException("StopCalled")
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def emby_item():
|
||||
|
||||
''' Wrapper to retrieve the emby_db item.
|
||||
'''
|
||||
def decorator(func):
|
||||
def wrapper(self, item, *args, **kwargs):
|
||||
e_item = self.emby_db.get_item_by_id(item['Id'] if type(item) == dict else item)
|
||||
|
||||
return func(self, item, e_item=e_item, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def library_check():
|
||||
|
||||
''' Wrapper to retrieve the library
|
||||
'''
|
||||
def decorator(func):
|
||||
def wrapper(self, item, *args, **kwargs):
|
||||
from database import get_sync
|
||||
|
||||
sync = get_sync()
|
||||
|
||||
if kwargs.get('library') is None:
|
||||
|
||||
if 'e_item' in kwargs:
|
||||
try:
|
||||
view_id = kwargs['e_item'][7]
|
||||
view_name = self.emby_db.get_view_name(view_id)
|
||||
view = {'Name': view_name, 'Id': view_id}
|
||||
except Exception:
|
||||
view = None
|
||||
|
||||
if view is None:
|
||||
ancestors = self.server['api'].get_ancestors(item['Id'])
|
||||
|
||||
if not ancestors:
|
||||
|
||||
return
|
||||
|
||||
for ancestor in ancestors:
|
||||
if ancestor['Type'] == 'CollectionFolder':
|
||||
|
||||
view = self.emby_db.get_view_name(ancestor['Id'])
|
||||
view = {'Id': None, 'Name': None} if view is None else {'Name': ancestor['Name'], 'Id': ancestor['Id']}
|
||||
|
||||
break
|
||||
|
||||
if view['Id'] not in sync['Whitelist']:
|
||||
LOG.info("Library %s is not synced. Skip update.", view['Id'])
|
||||
|
||||
return
|
||||
|
||||
kwargs['library'] = view
|
||||
|
||||
return func(self, item, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
381
resources/lib/helper/xmls.py
Normal file
381
resources/lib/helper/xmls.py
Normal file
|
@ -0,0 +1,381 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
#################################################################################################
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
import xbmc
|
||||
|
||||
from . import _, indent, write_xml
|
||||
|
||||
#################################################################################################
|
||||
|
||||
LOG = logging.getLogger("EMBY."+__name__)
|
||||
|
||||
#################################################################################################
|
||||
|
||||
def sources():
|
||||
|
||||
''' Create master lock compatible sources.
|
||||
Also add the kodi.emby.media source.
|
||||
'''
|
||||
path = xbmc.translatePath("special://profile/").decode('utf-8')
|
||||
file = os.path.join(path, 'sources.xml')
|
||||
|
||||
try:
|
||||
xml = etree.parse(file).getroot()
|
||||
except Exception:
|
||||
|
||||
xml = etree.Element('sources')
|
||||
video = etree.SubElement(xml, 'video')
|
||||
etree.SubElement(video, 'default', attrib={'pathversion': "1"})
|
||||
|
||||
video = xml.find('video')
|
||||
count = 2
|
||||
|
||||
for source in xml.findall('.//path'):
|
||||
if source.text == 'smb://':
|
||||
count -= 1
|
||||
|
||||
if count == 0:
|
||||
break
|
||||
else:
|
||||
for i in range(0, count):
|
||||
source = etree.SubElement(video, 'source')
|
||||
etree.SubElement(source, 'name').text = "Emby"
|
||||
etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://"
|
||||
etree.SubElement(source, 'allowsharing').text = "true"
|
||||
|
||||
for source in xml.findall('.//path'):
|
||||
if source.text == 'http://kodi.emby.media':
|
||||
break
|
||||
else:
|
||||
source = etree.SubElement(video, 'source')
|
||||
etree.SubElement(source, 'name').text = "kodi.emby.media"
|
||||
etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "http://kodi.emby.media"
|
||||
etree.SubElement(source, 'allowsharing').text = "true"
|
||||
|
||||
indent(xml)
|
||||
write_xml(etree.tostring(xml, 'UTF-8'), file)
|
||||
|
||||
def tvtunes_nfo(path, urls):
|
||||
|
||||
''' Create tvtunes.nfo
|
||||
'''
|
||||
try:
|
||||
xml = etree.parse(path).getroot()
|
||||
except Exception:
|
||||
xml = etree.Element('tvtunes')
|
||||
|
||||
for elem in xml.getiterator('tvtunes'):
|
||||
for file in list(elem):
|
||||
elem.remove(file)
|
||||
|
||||
for url in urls:
|
||||
etree.SubElement(xml, 'file').text = url
|
||||
|
||||
indent(xml)
|
||||
write_xml(etree.tostring(xml, 'UTF-8'), path)
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#################################################################################################
|
||||
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import sqlite3
|
||||
import StringIO
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib
|
||||
import unicodedata
|
||||
import xml.etree.ElementTree as etree
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
import xbmcvfs
|
||||
|
||||
#################################################################################################
|
||||
|
||||
log = logging.getLogger("EMBY."+__name__)
|
||||
|
||||
#################################################################################################
|
||||
|
||||
|
||||
|
||||
|
||||
#################################################################################################
|
||||
# Utility methods
|
||||
|
||||
def getScreensaver():
|
||||
# Get the current screensaver value
|
||||
result = JSONRPC('Settings.getSettingValue').execute({'setting': "screensaver.mode"})
|
||||
try:
|
||||
return result['result']['value']
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
def setScreensaver(value):
|
||||
# Toggle the screensaver
|
||||
params = {
|
||||
'setting': "screensaver.mode",
|
||||
'value': value
|
||||
}
|
||||
result = JSONRPC('Settings.setSettingValue').execute(params)
|
||||
log.info("Toggling screensaver: %s %s" % (value, result))
|
||||
|
||||
def convertDate(date):
|
||||
try:
|
||||
date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ")
|
||||
except (ImportError, TypeError):
|
||||
# TypeError: attribute of type 'NoneType' is not callable
|
||||
# Known Kodi/python error
|
||||
date = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6]))
|
||||
|
||||
return date
|
||||
|
||||
def normalize_string(text):
|
||||
# For theme media, do not modify unless
|
||||
# modified in TV Tunes
|
||||
text = text.replace(":", "")
|
||||
text = text.replace("/", "-")
|
||||
text = text.replace("\\", "-")
|
||||
text = text.replace("<", "")
|
||||
text = text.replace(">", "")
|
||||
text = text.replace("*", "")
|
||||
text = text.replace("?", "")
|
||||
text = text.replace('|', "")
|
||||
text = text.strip()
|
||||
# Remove dots from the last character as windows can not have directories
|
||||
# with dots at the end
|
||||
text = text.rstrip('.')
|
||||
text = unicodedata.normalize('NFKD', unicode(text, 'utf-8')).encode('ascii', 'ignore')
|
||||
|
||||
return text
|
||||
|
||||
def indent(elem, level=0):
|
||||
# Prettify xml trees
|
||||
i = "\n" + level*" "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
for elem in elem:
|
||||
indent(elem, level+1)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
def profiling(sortby="cumulative"):
|
||||
# Will print results to Kodi log
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
import cProfile
|
||||
import pstats
|
||||
|
||||
pr = cProfile.Profile()
|
||||
|
||||
pr.enable()
|
||||
result = func(*args, **kwargs)
|
||||
pr.disable()
|
||||
|
||||
s = StringIO.StringIO()
|
||||
ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
|
||||
ps.print_stats()
|
||||
log.info(s.getvalue())
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
#################################################################################################
|
||||
# Addon utilities
|
||||
|
||||
def sourcesXML():
|
||||
# To make Master lock compatible
|
||||
path = xbmc.translatePath("special://profile/").decode('utf-8')
|
||||
xmlpath = "%ssources.xml" % path
|
||||
|
||||
try:
|
||||
xmlparse = etree.parse(xmlpath)
|
||||
except: # Document is blank or missing
|
||||
root = etree.Element('sources')
|
||||
else:
|
||||
root = xmlparse.getroot()
|
||||
|
||||
|
||||
video = root.find('video')
|
||||
if video is None:
|
||||
video = etree.SubElement(root, 'video')
|
||||
etree.SubElement(video, 'default', attrib={'pathversion': "1"})
|
||||
|
||||
# Add elements
|
||||
count = 2
|
||||
for source in root.findall('.//path'):
|
||||
if source.text == "smb://":
|
||||
count -= 1
|
||||
|
||||
if count == 0:
|
||||
# sources already set
|
||||
break
|
||||
else:
|
||||
# Missing smb:// occurences, re-add.
|
||||
for i in range(0, count):
|
||||
source = etree.SubElement(video, 'source')
|
||||
etree.SubElement(source, 'name').text = "Emby"
|
||||
etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://"
|
||||
etree.SubElement(source, 'allowsharing').text = "true"
|
||||
# Prettify and write to file
|
||||
try:
|
||||
indent(root)
|
||||
except: pass
|
||||
etree.ElementTree(root).write(xmlpath)
|
||||
|
||||
def passwordsXML():
|
||||
|
||||
# To add network credentials
|
||||
path = xbmc.translatePath("special://userdata/").decode('utf-8')
|
||||
xmlpath = "%spasswords.xml" % path
|
||||
|
||||
try:
|
||||
xmlparse = etree.parse(xmlpath)
|
||||
except: # Document is blank or missing
|
||||
root = etree.Element('passwords')
|
||||
else:
|
||||
root = xmlparse.getroot()
|
||||
|
||||
dialog = xbmcgui.Dialog()
|
||||
credentials = settings('networkCreds')
|
||||
if credentials:
|
||||
# Present user with options
|
||||
option = dialog.select(language(33075), [language(33076), language(33077)])
|
||||
|
||||
if option < 0:
|
||||
# User cancelled dialog
|
||||
return
|
||||
|
||||
elif option == 1:
|
||||
# User selected remove
|
||||
for paths in root.getiterator('passwords'):
|
||||
for path in paths:
|
||||
if path.find('.//from').text == "smb://%s/" % credentials:
|
||||
paths.remove(path)
|
||||
log.info("Successfully removed credentials for: %s" % credentials)
|
||||
etree.ElementTree(root).write(xmlpath)
|
||||
break
|
||||
else:
|
||||
log.info("Failed to find saved server: %s in passwords.xml" % credentials)
|
||||
|
||||
settings('networkCreds', value="")
|
||||
xbmcgui.Dialog().notification(
|
||||
heading=language(29999),
|
||||
message="%s %s" % (language(33078), credentials),
|
||||
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||
time=1000,
|
||||
sound=False)
|
||||
return
|
||||
|
||||
elif option == 0:
|
||||
# User selected to modify
|
||||
server = dialog.input(language(33083), credentials)
|
||||
if not server:
|
||||
return
|
||||
else:
|
||||
# No credentials added
|
||||
dialog.ok(heading=language(29999), line1=language(33082))
|
||||
server = dialog.input(language(33084))
|
||||
if not server:
|
||||
return
|
||||
|
||||
# Network username
|
||||
user = dialog.input(language(33079))
|
||||
if not user:
|
||||
return
|
||||
# Network password
|
||||
password = dialog.input(heading=language(33080), option=xbmcgui.ALPHANUM_HIDE_INPUT)
|
||||
if not password:
|
||||
return
|
||||
|
||||
# Add elements
|
||||
for path in root.findall('.//path'):
|
||||
if path.find('.//from').text.lower() == "smb://%s/" % server.lower():
|
||||
# Found the server, rewrite credentials
|
||||
topath = "smb://%s:%s@%s/" % (user, password, server)
|
||||
path.find('.//to').text = topath.replace("\\", ";")
|
||||
break
|
||||
else:
|
||||
# Server not found, add it.
|
||||
path = etree.SubElement(root, 'path')
|
||||
etree.SubElement(path, 'from', attrib={'pathversion': "1"}).text = "smb://%s/" % server
|
||||
topath = "smb://%s:%s@%s/" % (user, password, server)
|
||||
etree.SubElement(path, 'to', attrib={'pathversion': "1"}).text = topath.replace("\\", ";")
|
||||
# Force Kodi to see the credentials without restarting
|
||||
xbmcvfs.exists(topath)
|
||||
|
||||
# Add credentials
|
||||
settings('networkCreds', value=server)
|
||||
log.info("Added server: %s to passwords.xml" % server)
|
||||
# Prettify and write to file
|
||||
try:
|
||||
indent(root)
|
||||
except: pass
|
||||
etree.ElementTree(root).write(xmlpath)
|
||||
|
||||
dialog.notification(
|
||||
heading=language(29999),
|
||||
message="%s %s" % (language(33081), server),
|
||||
icon="special://home/addons/plugin.video.emby/icon.png",
|
||||
time=1000,
|
||||
sound=False)
|
||||
|
||||
def verify_advancedsettings():
|
||||
# Track the existance of <cleanonupdate>true</cleanonupdate>
|
||||
# incompatible with plugin paths
|
||||
log.info("verifying advanced settings")
|
||||
if settings('useDirectPaths') != "0": return
|
||||
|
||||
path = xbmc.translatePath("special://userdata/").decode('utf-8')
|
||||
xmlpath = "%sadvancedsettings.xml" % path
|
||||
|
||||
try:
|
||||
xmlparse = etree.parse(xmlpath)
|
||||
except: # Document is blank or missing
|
||||
return
|
||||
else:
|
||||
root = xmlparse.getroot()
|
||||
|
||||
video = root.find('videolibrary')
|
||||
if video is not None:
|
||||
cleanonupdate = video.find('cleanonupdate')
|
||||
if cleanonupdate is not None and cleanonupdate.text == "true":
|
||||
log.warn("cleanonupdate disabled")
|
||||
video.remove(cleanonupdate)
|
||||
|
||||
try:
|
||||
indent(root)
|
||||
except: pass
|
||||
etree.ElementTree(root).write(xmlpath)
|
||||
|
||||
xbmcgui.Dialog().ok(heading=language(29999), line1=language(33097))
|
||||
xbmc.executebuiltin('RestartApp')
|
||||
return True
|
||||
return
|
||||
"""
|
Loading…
Add table
Add a link
Reference in a new issue