This commit is contained in:
angelblue05 2016-10-28 00:02:47 -05:00 committed by GitHub
parent d582888ffb
commit ba22e26c06
80 changed files with 11580 additions and 6945 deletions

View file

@ -14,45 +14,32 @@ log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class API():
class API(object):
def __init__(self, item):
# item is the api response
self.item = item
def getUserData(self):
def get_userdata(self):
# Default
favorite = False
likes = None
playcount = None
played = False
lastPlayedDate = None
last_played = None
resume = 0
userrating = 0
try:
userdata = self.item['UserData']
except KeyError: # No userdata found.
pass
else:
favorite = userdata['IsFavorite']
likes = userdata.get('Likes')
# Userrating is based on likes and favourite
if favorite:
userrating = 5
elif likes:
userrating = 3
elif likes == False:
userrating = 0
else:
userrating = 1
lastPlayedDate = userdata.get('LastPlayedDate')
if lastPlayedDate:
lastPlayedDate = lastPlayedDate.split('.')[0].replace('T', " ")
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
@ -61,12 +48,12 @@ class API():
if playcount == 0:
playcount = 1
if lastPlayedDate is None:
lastPlayedDate = self.getDateCreated()
if last_played is None:
last_played = self.get_date_created()
playbackPosition = userdata.get('PlaybackPositionTicks')
if playbackPosition:
resume = playbackPosition / 10000000.0
playback_position = userdata.get('PlaybackPositionTicks')
if playback_position:
resume = playback_position / 10000000.0
return {
@ -74,12 +61,11 @@ class API():
'Likes': likes,
'PlayCount': playcount,
'Played': played,
'LastPlayedDate': lastPlayedDate,
'Resume': resume,
'UserRating': userrating
'LastPlayedDate': last_played,
'Resume': resume
}
def getPeople(self):
def get_people(self):
# Process People
director = []
writer = []
@ -87,21 +73,19 @@ class API():
try:
people = self.item['People']
except KeyError:
pass
else:
for person in people:
type = person['Type']
type_ = person['Type']
name = person['Name']
if "Director" in type:
if type_ == 'Director':
director.append(name)
elif "Actor" in type:
elif type_ == 'Actor':
cast.append(name)
elif type in ("Writing", "Writer"):
elif type_ in ('Writing', 'Writer'):
writer.append(name)
return {
@ -111,101 +95,115 @@ class API():
'Cast': cast
}
def getMediaStreams(self):
videotracks = []
audiotracks = []
subtitlelanguages = []
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
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']
codec = media_stream.get('Codec', "").lower()
profile = media_stream.get('Profile', "").lower()
if stream_type == "Video":
# Height, Width, Codec, AspectRatio, AspectFloat, 3D
track = {
'codec': codec,
'height': media_stream.get('Height'),
'width': media_stream.get('Width'),
'video3DFormat': self.item.get('Video3DFormat'),
'aspect': 1.85
}
try:
container = self.item['MediaSources'][0]['Container'].lower()
except:
container = ""
# Sort codec vs container/profile
if "msmpeg4" in codec:
track['codec'] = "divx"
elif "mpeg4" in codec:
if "simple profile" in profile or not profile:
track['codec'] = "xvid"
elif "h264" in codec:
if container in ("mp4", "mov", "m4v"):
track['codec'] = "avc1"
# Aspect ratio
if self.item.get('AspectRatio'):
# Metadata AR
aspect = self.item['AspectRatio']
else: # File AR
aspect = media_stream.get('AspectRatio', "0")
try:
aspectwidth, aspectheight = aspect.split(':')
track['aspect'] = round(float(aspectwidth) / float(aspectheight), 6)
except (ValueError, ZeroDivisionError):
width = track.get('width')
height = track.get('height')
if width and height:
track['aspect'] = round(float(width / height), 6)
else:
track['aspect'] = 1.85
if self.item.get("RunTimeTicks"):
track['duration'] = self.item.get("RunTimeTicks") / 10000000.0
videotracks.append(track)
self._video_stream(video_tracks, media_stream)
elif stream_type == "Audio":
# Codec, Channels, language
track = {
'codec': codec,
'channels': media_stream.get('Channels'),
'language': media_stream.get('Language')
}
if "dca" in codec and "dts-hd ma" in profile:
track['codec'] = "dtshd_ma"
audiotracks.append(track)
self._audio_stream(audio_tracks, media_stream)
elif stream_type == "Subtitle":
# Language
subtitlelanguages.append(media_stream.get('Language', "Unknown"))
subtitle_languages.append(media_stream.get('Language', "Unknown"))
return {
'video': videotracks,
'audio': audiotracks,
'subtitle': subtitlelanguages
'video': video_tracks,
'audio': audio_tracks,
'subtitle': subtitle_languages
}
def getRuntime(self):
def _video_stream(self, video_tracks, stream):
codec = stream.get('Codec', "").lower()
profile = stream.get('Profile', "").lower()
# Height, Width, Codec, AspectRatio, AspectFloat, 3D
track = {
'codec': codec,
'height': stream.get('Height'),
'width': stream.get('Width'),
'video3DFormat': self.item.get('Video3DFormat'),
'aspect': 1.85
}
try:
container = self.item['MediaSources'][0]['Container'].lower()
except Exception:
container = ""
# Sort codec vs container/profile
if "msmpeg4" in codec:
track['codec'] = "divx"
elif "mpeg4" in codec:
if "simple profile" in profile or not profile:
track['codec'] = "xvid"
elif "h264" in codec:
if container in ("mp4", "mov", "m4v"):
track['codec'] = "avc1"
# Aspect ratio
if 'AspectRatio' in self.item:
# Metadata AR
aspect = self.item['AspectRatio']
else: # File AR
aspect = stream.get('AspectRatio', "0")
try:
aspect_width, aspect_height = aspect.split(':')
track['aspect'] = round(float(aspect_width) / float(aspect_height), 6)
except (ValueError, ZeroDivisionError):
width = track.get('width')
height = track.get('height')
if width and height:
track['aspect'] = round(float(width / height), 6)
else:
track['aspect'] = 1.85
if 'RunTimeTicks' in self.item:
track['duration'] = self.get_runtime()
video_tracks.append(track)
def _audio_stream(self, audio_tracks, stream):
codec = stream.get('Codec', "").lower()
profile = stream.get('Profile', "").lower()
# Codec, Channels, language
track = {
'codec': codec,
'channels': stream.get('Channels'),
'language': stream.get('Language')
}
if "dca" in codec and "dts-hd ma" in profile:
track['codec'] = "dtshd_ma"
audio_tracks.append(track)
def get_runtime(self):
try:
runtime = self.item['RunTimeTicks'] / 10000000.0
@ -214,7 +212,8 @@ class API():
return runtime
def adjustResume(self, resume_seconds):
@classmethod
def adjust_resume(cls, resume_seconds):
resume = 0
if resume_seconds:
@ -226,24 +225,23 @@ class API():
return resume
def getStudios(self):
def get_studios(self):
# Process Studios
studios = []
try:
studio = self.item['SeriesStudio']
studios.append(self.verifyStudio(studio))
studios.append(self.verify_studio(studio))
except KeyError:
studioList = self.item['Studios']
for studio in studioList:
for studio in self.item['Studios']:
name = studio['Name']
studios.append(self.verifyStudio(name))
studios.append(self.verify_studio(name))
return studios
def verifyStudio(self, studioName):
@classmethod
def verify_studio(cls, studio_name):
# Convert studio for Kodi to properly detect them
studios = {
@ -254,9 +252,9 @@ class API():
'wgn america': "WGN"
}
return studios.get(studioName.lower(), studioName)
return studios.get(studio_name.lower(), studio_name)
def getChecksum(self):
def get_checksum(self):
# Use the etags checksum and userdata
userdata = self.item['UserData']
@ -265,7 +263,7 @@ class API():
self.item['Etag'],
userdata['Played'],
userdata['IsFavorite'],
userdata.get('Likes',''),
userdata.get('Likes', ""),
userdata['PlaybackPositionTicks'],
userdata.get('UnplayedItemCount', ""),
userdata.get('LastPlayedDate', "")
@ -273,7 +271,7 @@ class API():
return checksum
def getGenres(self):
def get_genres(self):
all_genres = ""
genres = self.item.get('Genres', self.item.get('SeriesGenres'))
@ -282,17 +280,17 @@ class API():
return all_genres
def getDateCreated(self):
def get_date_created(self):
try:
dateadded = self.item['DateCreated']
dateadded = dateadded.split('.')[0].replace('T', " ")
date_added = self.item['DateCreated']
date_added = date_added.split('.')[0].replace('T', " ")
except KeyError:
dateadded = None
date_added = None
return dateadded
return date_added
def getPremiereDate(self):
def get_premiere_date(self):
try:
premiere = self.item['PremiereDate']
@ -302,7 +300,7 @@ class API():
return premiere
def getOverview(self):
def get_overview(self):
try:
overview = self.item['Overview']
@ -314,7 +312,7 @@ class API():
return overview
def getTagline(self):
def get_tagline(self):
try:
tagline = self.item['Taglines'][0]
@ -323,16 +321,16 @@ class API():
return tagline
def getProvider(self, providername):
def get_provider(self, name):
try:
provider = self.item['ProviderIds'][providername]
provider = self.item['ProviderIds'][name]
except KeyError:
provider = None
return provider
def getMpaa(self):
def get_mpaa(self):
# Convert more complex cases
mpaa = self.item.get('OfficialRating', "")
@ -340,18 +338,21 @@ class API():
# Kodi seems to not like NR, but will accept Not Rated
mpaa = "Not Rated"
if "FSK-" in mpaa:
mpaa = mpaa.replace("-", " ")
return mpaa
def getCountry(self):
def get_country(self):
try:
country = self.item['ProductionLocations'][0]
except IndexError:
except (IndexError, KeyError):
country = None
return country
def getFilePath(self):
def get_file_path(self):
try:
filepath = self.item['Path']

View file

@ -2,9 +2,7 @@
#################################################################################################
import json
import logging
import requests
import os
import urllib
from sqlite3 import OperationalError
@ -12,9 +10,10 @@ from sqlite3 import OperationalError
import xbmc
import xbmcgui
import xbmcvfs
import requests
import image_cache_thread
from utils import window, settings, language as lang, kodiSQL
from utils import window, settings, dialog, language as lang, kodiSQL, JSONRPC
##################################################################################################
@ -23,58 +22,54 @@ log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class Artwork():
class Artwork(object):
xbmc_host = 'localhost'
xbmc_port = None
xbmc_username = None
xbmc_password = None
imageCacheThreads = []
imageCacheLimitThreads = 0
image_cache_threads = []
image_cache_limit = 0
def __init__(self):
self.enableTextureCache = settings('enableTextureCache') == "true"
self.imageCacheLimitThreads = int(settings('imageCacheLimit'))
self.imageCacheLimitThreads = int(self.imageCacheLimitThreads * 5)
log.info("Using Image Cache Thread Count: %s" % self.imageCacheLimitThreads)
self.enable_texture_cache = settings('enableTextureCache') == "true"
self.image_cache_limit = int(settings('imageCacheLimit')) * 5
log.debug("image cache thread count: %s", self.image_cache_limit)
if not self.xbmc_port and self.enableTextureCache:
self.setKodiWebServerDetails()
if not self.xbmc_port and self.enable_texture_cache:
self._set_webserver_details()
self.userId = window('emby_currUser')
self.server = window('emby_server%s' % self.userId)
self.user_id = window('emby_currUser')
self.server = window('emby_server%s' % self.user_id)
def double_urlencode(self, text):
def _double_urlencode(self, text):
text = self.single_urlencode(text)
text = self.single_urlencode(text)
return text
def single_urlencode(self, text):
@classmethod
def single_urlencode(cls, text):
# urlencode needs a utf- string
text = urllib.urlencode({'blahblahblah':text.encode("utf-8")})
text = urllib.urlencode({'blahblahblah': text.encode('utf-8')})
text = text[13:]
return text.decode("utf-8") #return the result again as unicode
return text.decode('utf-8') #return the result again as unicode
def setKodiWebServerDetails(self):
def _set_webserver_details(self):
# Get the Kodi webserver details - used to set the texture cache
get_setting_value = JSONRPC('Settings.GetSettingValue')
web_query = {
"jsonrpc": "2.0",
"id": 1,
"method": "Settings.GetSettingValue",
"params": {
"setting": "services.webserver"
}
"setting": "services.webserver"
}
result = xbmc.executeJSONRPC(json.dumps(web_query))
result = json.loads(result)
result = get_setting_value.execute(web_query)
try:
xbmc_webserver_enabled = result['result']['value']
except (KeyError, TypeError):
@ -82,95 +77,61 @@ class Artwork():
if not xbmc_webserver_enabled:
# Enable the webserver, it is disabled
set_setting_value = JSONRPC('Settings.SetSettingValue')
web_port = {
"jsonrpc": "2.0",
"id": 1,
"method": "Settings.SetSettingValue",
"params": {
"setting": "services.webserverport",
"value": 8080
}
"setting": "services.webserverport",
"value": 8080
}
result = xbmc.executeJSONRPC(json.dumps(web_port))
set_setting_value.execute(web_port)
self.xbmc_port = 8080
web_user = {
"jsonrpc": "2.0",
"id": 1,
"method": "Settings.SetSettingValue",
"params": {
"setting": "services.webserver",
"value": True
}
"setting": "services.webserver",
"value": True
}
result = xbmc.executeJSONRPC(json.dumps(web_user))
set_setting_value.execute(web_user)
self.xbmc_username = "kodi"
# Webserver already enabled
web_port = {
"jsonrpc": "2.0",
"id": 1,
"method": "Settings.GetSettingValue",
"params": {
"setting": "services.webserverport"
}
"setting": "services.webserverport"
}
result = xbmc.executeJSONRPC(json.dumps(web_port))
result = json.loads(result)
result = get_setting_value.execute(web_port)
try:
self.xbmc_port = result['result']['value']
except TypeError:
except (TypeError, KeyError):
pass
web_user = {
"jsonrpc": "2.0",
"id": 1,
"method": "Settings.GetSettingValue",
"params": {
"setting": "services.webserverusername"
}
"setting": "services.webserverusername"
}
result = xbmc.executeJSONRPC(json.dumps(web_user))
result = json.loads(result)
result = get_setting_value.execute(web_user)
try:
self.xbmc_username = result['result']['value']
except TypeError:
except (TypeError, KeyError):
pass
web_pass = {
"jsonrpc": "2.0",
"id": 1,
"method": "Settings.GetSettingValue",
"params": {
"setting": "services.webserverpassword"
}
"setting": "services.webserverpassword"
}
result = xbmc.executeJSONRPC(json.dumps(web_pass))
result = json.loads(result)
result = get_setting_value.execute(web_pass)
try:
self.xbmc_password = result['result']['value']
except TypeError:
except (TypeError, KeyError):
pass
def fullTextureCacheSync(self):
def texture_cache_sync(self):
# This method will sync all Kodi artwork to textures13.db
# and cache them locally. This takes diskspace!
dialog = xbmcgui.Dialog()
if not dialog.yesno(
heading=lang(29999),
line1=lang(33042)):
if not dialog(type_="yesno",
heading="{emby}",
line1=lang(33042)):
return
log.info("Doing Image Cache Sync")
@ -179,135 +140,147 @@ class Artwork():
pdialog.create(lang(29999), lang(33043))
# ask to rest all existing or not
if dialog.yesno(lang(29999), lang(33044)):
log.info("Resetting all cache data first.")
# Remove all existing textures first
path = xbmc.translatePath('special://thumbnails/').decode('utf-8')
if xbmcvfs.exists(path):
allDirs, allFiles = xbmcvfs.listdir(path)
for dir in allDirs:
allDirs, allFiles = xbmcvfs.listdir(path+dir)
for file in allFiles:
if os.path.supports_unicode_filenames:
path = os.path.join(path+dir.decode('utf-8'),file.decode('utf-8'))
xbmcvfs.delete(path)
else:
xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file))
# remove all existing data from texture DB
connection = kodiSQL('texture')
cursor = connection.cursor()
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
rows = cursor.fetchall()
for row in rows:
tableName = row[0]
if tableName != "version":
cursor.execute("DELETE FROM " + tableName)
connection.commit()
cursor.close()
if dialog(type_="yesno", heading="{emby}", line1=lang(33044)):
log.info("Resetting all cache data first")
self.delete_cache()
# Cache all entries in video DB
connection = kodiSQL('video')
cursor = connection.cursor()
self._cache_all_video_entries(pdialog)
# Cache all entries in music DB
self._cache_all_music_entries(pdialog)
pdialog.update(100, "%s %s" % (lang(33046), len(self.image_cache_threads)))
log.info("Waiting for all threads to exit")
while len(self.image_cache_threads):
for thread in self.image_cache_threads:
if thread.is_finished:
self.image_cache_threads.remove(thread)
pdialog.update(100, "%s %s" % (lang(33046), len(self.image_cache_threads)))
log.info("Waiting for all threads to exit: %s", len(self.image_cache_threads))
xbmc.sleep(500)
pdialog.close()
def _cache_all_video_entries(self, pdialog):
conn = kodiSQL('video')
cursor = conn.cursor()
cursor.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors
result = cursor.fetchall()
total = len(result)
log.info("Image cache sync about to process %s images" % total)
log.info("Image cache sync about to process %s images", total)
cursor.close()
count = 0
for url in result:
if pdialog.iscanceled():
break
percentage = int((float(count) / float(total))*100)
message = "%s of %s (%s)" % (count, total, self.imageCacheThreads)
message = "%s of %s (%s)" % (count, total, len(self.image_cache_threads))
pdialog.update(percentage, "%s %s" % (lang(33045), message))
self.cacheTexture(url[0])
self.cache_texture(url[0])
count += 1
# Cache all entries in music DB
connection = kodiSQL('music')
cursor = connection.cursor()
def _cache_all_music_entries(self, pdialog):
conn = kodiSQL('music')
cursor = conn.cursor()
cursor.execute("SELECT url FROM art")
result = cursor.fetchall()
total = len(result)
log.info("Image cache sync about to process %s images" % total)
log.info("Image cache sync about to process %s images", total)
cursor.close()
count = 0
for url in result:
if pdialog.iscanceled():
break
percentage = int((float(count) / float(total))*100)
message = "%s of %s" % (count, total)
pdialog.update(percentage, "%s %s" % (lang(33045), message))
self.cacheTexture(url[0])
self.cache_texture(url[0])
count += 1
pdialog.update(100, "%s %s" % (lang(33046), len(self.imageCacheThreads)))
log.info("Waiting for all threads to exit")
while len(self.imageCacheThreads):
for thread in self.imageCacheThreads:
if thread.isFinished:
self.imageCacheThreads.remove(thread)
pdialog.update(100, "%s %s" % (lang(33046), len(self.imageCacheThreads)))
log.info("Waiting for all threads to exit: %s" % len(self.imageCacheThreads))
xbmc.sleep(500)
pdialog.close()
@classmethod
def delete_cache(cls):
# Remove all existing textures first
path = xbmc.translatePath('special://thumbnails/').decode('utf-8')
if xbmcvfs.exists(path):
dirs, ignore_files = xbmcvfs.listdir(path)
for directory in dirs:
ignore_dirs, files = xbmcvfs.listdir(path + directory)
for file_ in files:
def addWorkerImageCacheThread(self, url):
if os.path.supports_unicode_filenames:
filename = os.path.join(path + directory.decode('utf-8'),
file_.decode('utf-8'))
else:
filename = os.path.join(path.encode('utf-8') + directory, file_)
xbmcvfs.delete(filename)
log.debug("deleted: %s", filename)
# remove all existing data from texture DB
conn = kodiSQL('texture')
cursor = conn.cursor()
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
rows = cursor.fetchall()
for row in rows:
table_name = row[0]
if table_name != "version":
cursor.execute("DELETE FROM " + table_name)
conn.commit()
cursor.close()
def _add_worker_image_thread(self, url):
while True:
# removed finished
for thread in self.imageCacheThreads:
if thread.isFinished:
self.imageCacheThreads.remove(thread)
for thread in self.image_cache_threads:
if thread.is_finished:
self.image_cache_threads.remove(thread)
# add a new thread or wait and retry if we hit our limit
if len(self.imageCacheThreads) < self.imageCacheLimitThreads:
newThread = image_cache_thread.image_cache_thread()
newThread.setUrl(self.double_urlencode(url))
newThread.setHost(self.xbmc_host, self.xbmc_port)
newThread.setAuth(self.xbmc_username, self.xbmc_password)
newThread.start()
self.imageCacheThreads.append(newThread)
if len(self.image_cache_threads) < self.image_cache_limit:
new_thread = image_cache_thread.ImageCacheThread()
new_thread.set_url(self._double_urlencode(url))
new_thread.set_host(self.xbmc_host, self.xbmc_port)
new_thread.set_auth(self.xbmc_username, self.xbmc_password)
new_thread.start()
self.image_cache_threads.append(new_thread)
return
else:
log.info("Waiting for empty queue spot: %s" % len(self.imageCacheThreads))
log.info("Waiting for empty queue spot: %s", len(self.image_cache_threads))
xbmc.sleep(50)
def cacheTexture(self, url):
def cache_texture(self, url):
# Cache a single image url to the texture cache
if url and self.enableTextureCache:
log.debug("Processing: %s" % url)
if url and self.enable_texture_cache:
log.debug("Processing: %s", url)
if not self.imageCacheLimitThreads:
# Add image to texture cache by simply calling it at the http endpoint
url = self.double_urlencode(url)
try: # Extreme short timeouts so we will have a exception.
response = requests.head(
url=("http://%s:%s/image/image://%s"
% (self.xbmc_host, self.xbmc_port, url)),
auth=(self.xbmc_username, self.xbmc_password),
timeout=(0.01, 0.01))
# We don't need the result
except: pass
if not self.image_cache_limit:
url = self._double_urlencode(url)
try: # Add image to texture cache by simply calling it at the http endpoint
requests.head(url=("http://%s:%s/image/image://%s"
% (self.xbmc_host, self.xbmc_port, url)),
auth=(self.xbmc_username, self.xbmc_password),
timeout=(0.01, 0.01))
except Exception: # We don't need the result
pass
else:
self.addWorkerImageCacheThread(url)
self._add_worker_image_thread(url)
def addArtwork(self, artwork, kodiId, mediaType, cursor):
def add_artwork(self, artwork, kodi_id, media_type, cursor):
# Kodi conversion table
kodiart = {
kodi_artwork = {
'Primary': ["thumb", "poster"],
'Banner': "banner",
@ -318,15 +291,14 @@ class Artwork():
'Backdrop': "fanart",
'BoxRear': "poster"
}
# Artwork is a dictionary
for art in artwork:
for artwork_type in artwork:
if art == "Backdrop":
if artwork_type == 'Backdrop':
# Backdrop entry is a list
# Process extra fanart for artwork downloader (fanart, fanart1, fanart2...)
backdrops = artwork[art]
backdropsNumber = len(backdrops)
backdrops = artwork[artwork_type]
backdrops_number = len(backdrops)
query = ' '.join((
@ -336,10 +308,10 @@ class Artwork():
"AND media_type = ?",
"AND type LIKE ?"
))
cursor.execute(query, (kodiId, mediaType, "fanart%",))
cursor.execute(query, (kodi_id, media_type, "fanart%",))
rows = cursor.fetchall()
if len(rows) > backdropsNumber:
if len(rows) > backdrops_number:
# More backdrops in database. Delete extra fanart.
query = ' '.join((
@ -348,47 +320,40 @@ class Artwork():
"AND media_type = ?",
"AND type LIKE ?"
))
cursor.execute(query, (kodiId, mediaType, "fanart_",))
cursor.execute(query, (kodi_id, media_type, "fanart_",))
# Process backdrops and extra fanart
index = ""
for backdrop in backdrops:
self.addOrUpdateArt(
imageUrl=backdrop,
kodiId=kodiId,
mediaType=mediaType,
imageType="%s%s" % ("fanart", index),
cursor=cursor)
for index, backdrop in enumerate(backdrops):
if backdropsNumber > 1:
try: # Will only fail on the first try, str to int.
index += 1
except TypeError:
index = 1
self.add_update_art(image_url=backdrop,
kodi_id=kodi_id,
media_type=media_type,
image_type=("fanart" if not index else "%s%s"
% ("fanart", index)),
cursor=cursor)
elif art == "Primary":
elif artwork_type == 'Primary':
# Primary art is processed as thumb and poster for Kodi.
for artType in kodiart[art]:
self.addOrUpdateArt(
imageUrl=artwork[art],
kodiId=kodiId,
mediaType=mediaType,
imageType=artType,
cursor=cursor)
for art_type in kodi_artwork[artwork_type]:
self.add_update_art(image_url=artwork[artwork_type],
kodi_id=kodi_id,
media_type=media_type,
image_type=art_type,
cursor=cursor)
elif kodiart.get(art):
elif artwork_type in kodi_artwork:
# Process the rest artwork type that Kodi can use
self.addOrUpdateArt(
imageUrl=artwork[art],
kodiId=kodiId,
mediaType=mediaType,
imageType=kodiart[art],
cursor=cursor)
self.add_update_art(image_url=artwork[artwork_type],
kodi_id=kodi_id,
media_type=media_type,
image_type=kodi_artwork[artwork_type],
cursor=cursor)
def addOrUpdateArt(self, imageUrl, kodiId, mediaType, imageType, cursor):
def add_update_art(self, image_url, kodi_id, media_type, image_type, cursor):
# Possible that the imageurl is an empty string
if imageUrl:
cacheimage = False
if image_url:
cache_image = False
query = ' '.join((
@ -398,13 +363,13 @@ class Artwork():
"AND media_type = ?",
"AND type = ?"
))
cursor.execute(query, (kodiId, mediaType, imageType,))
cursor.execute(query, (kodi_id, media_type, image_type,))
try: # Update the artwork
url = cursor.fetchone()[0]
except TypeError: # Add the artwork
cacheimage = True
log.debug("Adding Art Link for kodiId: %s (%s)" % (kodiId, imageUrl))
cache_image = True
log.debug("Adding Art Link for kodiId: %s (%s)", kodi_id, image_url)
query = (
'''
@ -413,20 +378,21 @@ class Artwork():
VALUES (?, ?, ?, ?)
'''
)
cursor.execute(query, (kodiId, mediaType, imageType, imageUrl))
cursor.execute(query, (kodi_id, media_type, image_type, image_url))
else: # Only cache artwork if it changed
if url != imageUrl:
cacheimage = True
if url != image_url:
cache_image = True
# Only for the main backdrop, poster
if (window('emby_initialScan') != "true" and
imageType in ("fanart", "poster")):
image_type in ("fanart", "poster")):
# Delete current entry before updating with the new one
self.deleteCachedArtwork(url)
self.delete_cached_artwork(url)
log.info("Updating Art url for %s kodiId: %s (%s) -> (%s)"
% (imageType, kodiId, url, imageUrl))
log.info("Updating Art url for %s kodiId: %s (%s) -> (%s)",
image_type, kodi_id, url, image_url)
query = ' '.join((
@ -436,13 +402,13 @@ class Artwork():
"AND media_type = ?",
"AND type = ?"
))
cursor.execute(query, (imageUrl, kodiId, mediaType, imageType))
cursor.execute(query, (image_url, kodi_id, media_type, image_type))
# Cache fanart and poster in Kodi texture cache
if cacheimage and imageType in ("fanart", "poster"):
self.cacheTexture(imageUrl)
if cache_image and image_type in ("fanart", "poster"):
self.cache_texture(image_url)
def deleteArtwork(self, kodiId, mediaType, cursor):
def delete_artwork(self, kodi_id, media_type, cursor):
query = ' '.join((
@ -451,85 +417,83 @@ class Artwork():
"WHERE media_id = ?",
"AND media_type = ?"
))
cursor.execute(query, (kodiId, mediaType,))
cursor.execute(query, (kodi_id, media_type,))
rows = cursor.fetchall()
for row in rows:
url = row[0]
imageType = row[1]
if imageType in ("poster", "fanart"):
self.deleteCachedArtwork(url)
image_type = row[1]
if image_type in ("poster", "fanart"):
self.delete_cached_artwork(url)
def deleteCachedArtwork(self, url):
@classmethod
def delete_cached_artwork(cls, url):
# Only necessary to remove and apply a new backdrop or poster
connection = kodiSQL('texture')
cursor = connection.cursor()
conn = kodiSQL('texture')
cursor = conn.cursor()
try:
cursor.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,))
cachedurl = cursor.fetchone()[0]
cached_url = cursor.fetchone()[0]
except TypeError:
log.info("Could not find cached url.")
log.info("Could not find cached url")
except OperationalError:
log.info("Database is locked. Skip deletion process.")
else: # Delete thumbnail as well as the entry
thumbnails = xbmc.translatePath("special://thumbnails/%s" % cachedurl).decode('utf-8')
log.info("Deleting cached thumbnail: %s" % thumbnails)
thumbnails = xbmc.translatePath("special://thumbnails/%s" % cached_url).decode('utf-8')
log.info("Deleting cached thumbnail: %s", thumbnails)
xbmcvfs.delete(thumbnails)
try:
cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
connection.commit()
conn.commit()
except OperationalError:
log.debug("Issue deleting url from cache. Skipping.")
finally:
cursor.close()
def getPeopleArtwork(self, people):
def get_people_artwork(self, people):
# append imageurl if existing
for person in people:
personId = person['Id']
tag = person.get('PrimaryImageTag')
image = ""
if tag:
person_id = person['Id']
if "PrimaryImageTag" in person:
image = (
"%s/emby/Items/%s/Images/Primary?"
"MaxWidth=400&MaxHeight=400&Index=0&Tag=%s"
% (self.server, personId, tag))
% (self.server, person_id, person['PrimaryImageTag']))
person['imageurl'] = image
return people
def getUserArtwork(self, itemId, itemType):
def get_user_artwork(self, item_id, item_type):
# Load user information set by UserClient
image = ("%s/emby/Users/%s/Images/%s?Format=original"
% (self.server, itemId, itemType))
return image
return "%s/emby/Users/%s/Images/%s?Format=original" % (self.server, item_id, item_type)
def getAllArtwork(self, item, parentInfo=False):
def get_all_artwork(self, item, parent_info=False):
itemid = item['Id']
item_id = item['Id']
artworks = item['ImageTags']
backdrops = item.get('BackdropImageTags', [])
maxHeight = 10000
maxWidth = 10000
customquery = ""
max_height = 10000
max_width = 10000
custom_query = ""
if settings('compressArt') == "true":
customquery = "&Quality=90"
custom_query = "&Quality=90"
if settings('enableCoverArt') == "false":
customquery += "&EnableImageEnhancers=false"
custom_query += "&EnableImageEnhancers=false"
allartworks = {
all_artwork = {
'Primary': "",
'Art': "",
@ -540,71 +504,53 @@ class Artwork():
'Backdrop': []
}
def get_backdrops(item_id, backdrops):
for index, tag in enumerate(backdrops):
artwork = ("%s/emby/Items/%s/Images/Backdrop/%s?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (self.server, item_id, index, max_width, max_height,
tag, custom_query))
all_artwork['Backdrop'].append(artwork)
def get_artwork(item_id, type_, tag):
artwork = ("%s/emby/Items/%s/Images/%s/0?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (self.server, item_id, type_, max_width, max_height, tag, custom_query))
all_artwork[type_] = artwork
# Process backdrops
for index, tag in enumerate(backdrops):
artwork = (
"%s/emby/Items/%s/Images/Backdrop/%s?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (self.server, itemid, index, maxWidth, maxHeight, tag, customquery))
allartworks['Backdrop'].append(artwork)
get_backdrops(item_id, backdrops)
# Process the rest of the artwork
for art in artworks:
for artwork in artworks:
# Filter backcover
if art != "BoxRear":
tag = artworks[art]
artwork = (
"%s/emby/Items/%s/Images/%s/0?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (self.server, itemid, art, maxWidth, maxHeight, tag, customquery))
allartworks[art] = artwork
if artwork != "BoxRear":
get_artwork(item_id, artwork, artworks[artwork])
# Process parent items if the main item is missing artwork
if parentInfo:
if parent_info:
# Process parent backdrops
if not allartworks['Backdrop']:
if not all_artwork['Backdrop']:
parentId = item.get('ParentBackdropItemId')
if parentId:
# If there is a parentId, go through the parent backdrop list
parentbackdrops = item['ParentBackdropImageTags']
for index, tag in enumerate(parentbackdrops):
artwork = (
"%s/emby/Items/%s/Images/Backdrop/%s?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (self.server, parentId, index, maxWidth, maxHeight, tag, customquery))
allartworks['Backdrop'].append(artwork)
if 'ParentBackdropItemId' in item:
# If there is a parent_id, go through the parent backdrop list
get_backdrops(item['ParentBackdropItemId'], item['ParentBackdropImageTags'])
# Process the rest of the artwork
parentartwork = ['Logo', 'Art', 'Thumb']
for parentart in parentartwork:
for parent_artwork in ('Logo', 'Art', 'Thumb'):
if not allartworks[parentart]:
if not all_artwork[parent_artwork]:
parentId = item.get('Parent%sItemId' % parentart)
if parentId:
parentTag = item['Parent%sImageTag' % parentart]
artwork = (
"%s/emby/Items/%s/Images/%s/0?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (self.server, parentId, parentart,
maxWidth, maxHeight, parentTag, customquery))
allartworks[parentart] = artwork
if 'Parent%sItemId' % parent_artwork in item:
get_artwork(item['Parent%sItemId' % parent_artwork], parent_artwork,
item['Parent%sImageTag' % parent_artwork])
# Parent album works a bit differently
if not allartworks['Primary']:
if not all_artwork['Primary']:
parentId = item.get('AlbumId')
if parentId and item.get('AlbumPrimaryImageTag'):
if 'AlbumId' in item and 'AlbumPrimaryImageTag' in item:
get_artwork(item['AlbumId'], 'Primary', item['AlbumPrimaryImageTag'])
parentTag = item['AlbumPrimaryImageTag']
artwork = (
"%s/emby/Items/%s/Images/Primary/0?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (self.server, parentId, maxWidth, maxHeight, parentTag, customquery))
allartworks['Primary'] = artwork
return allartworks
return all_artwork

View file

@ -19,40 +19,39 @@ log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class ClientInfo():
class ClientInfo(object):
def __init__(self):
self.addon = xbmcaddon.Addon()
self.addonName = self.getAddonName()
self.addon = xbmcaddon.Addon(self.get_addon_id())
@staticmethod
def get_addon_id():
return "plugin.video.emby"
def getAddonName(self):
def get_addon_name(self):
# Used for logging
return self.addon.getAddonInfo('name').upper()
def getAddonId(self):
return "plugin.video.emby"
def getVersion(self):
def get_version(self):
return self.addon.getAddonInfo('version')
def getDeviceName(self):
@classmethod
def get_device_name(cls):
if settings('deviceNameOpt') == "false":
# Use Kodi's deviceName
deviceName = xbmc.getInfoLabel('System.FriendlyName').decode('utf-8')
device_name = xbmc.getInfoLabel('System.FriendlyName').decode('utf-8')
else:
deviceName = settings('deviceName')
deviceName = deviceName.replace("\"", "_")
deviceName = deviceName.replace("/", "_")
device_name = settings('deviceName')
device_name = device_name.replace("\"", "_")
device_name = device_name.replace("/", "_")
return deviceName
return device_name
def getPlatform(self):
@classmethod
def get_platform(cls):
if xbmc.getCondVisibility('system.platform.osx'):
return "OSX"
@ -62,42 +61,52 @@ class ClientInfo():
return "iOS"
elif xbmc.getCondVisibility('system.platform.windows'):
return "Windows"
elif xbmc.getCondVisibility('system.platform.linux'):
return "Linux/RPi"
elif xbmc.getCondVisibility('system.platform.android'):
elif xbmc.getCondVisibility('system.platform.android'):
return "Linux/Android"
elif xbmc.getCondVisibility('system.platform.linux.raspberrypi'):
return "Linux/RPi"
elif xbmc.getCondVisibility('system.platform.linux'):
return "Linux"
else:
return "Unknown"
def getDeviceId(self, reset=False):
def get_device_id(self, reset=False):
clientId = window('emby_deviceId')
if clientId:
return clientId
client_id = window('emby_deviceId')
if client_id:
return client_id
addon_path = self.addon.getAddonInfo('path').decode('utf-8')
if os.path.supports_unicode_filenames:
path = os.path.join(addon_path, "machine_guid")
else:
path = os.path.join(addon_path.encode('utf-8'), "machine_guid")
GUID_file = xbmc.translatePath(path).decode('utf-8')
if reset and xbmcvfs.exists(GUID_file):
emby_guid = xbmc.translatePath("special://temp/emby_guid").decode('utf-8')
###$ Begin migration $###
if not xbmcvfs.exists(emby_guid):
addon_path = self.addon.getAddonInfo('path').decode('utf-8')
if os.path.supports_unicode_filenames:
path = os.path.join(addon_path, "machine_guid")
else:
path = os.path.join(addon_path.encode('utf-8'), "machine_guid")
guid_file = xbmc.translatePath(path).decode('utf-8')
if xbmcvfs.exists(guid_file):
xbmcvfs.copy(guid_file, emby_guid)
log.info("guid migration completed")
###$ End migration $###
if reset and xbmcvfs.exists(emby_guid):
# Reset the file
xbmcvfs.delete(GUID_file)
xbmcvfs.delete(emby_guid)
GUID = xbmcvfs.File(GUID_file)
clientId = GUID.read()
if not clientId:
log.info("Generating a new deviceid...")
clientId = str("%012X" % uuid4())
GUID = xbmcvfs.File(GUID_file, 'w')
GUID.write(clientId)
guid = xbmcvfs.File(emby_guid)
client_id = guid.read()
if not client_id:
log.info("Generating a new guid...")
client_id = str("%012X" % uuid4())
guid = xbmcvfs.File(emby_guid, 'w')
guid.write(client_id)
GUID.close()
guid.close()
log.info("DeviceId loaded: %s" % clientId)
window('emby_deviceId', value=clientId)
return clientId
log.info("DeviceId loaded: %s", client_id)
window('emby_deviceId', value=client_id)
return client_id

View file

@ -1,257 +0,0 @@
# -*- coding: utf-8 -*-
##################################################################################################
import json
import requests
import logging
import clientinfo
from utils import window
##################################################################################################
# Disable requests logging
from requests.packages.urllib3.exceptions import InsecureRequestWarning, InsecurePlatformWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
requests.packages.urllib3.disable_warnings(InsecurePlatformWarning)
log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class ConnectUtils():
# Borg - multiple instances, shared state
_shared_state = {}
clientInfo = clientinfo.ClientInfo()
# Requests session
c = None
timeout = 30
def __init__(self):
self.__dict__ = self._shared_state
def setUserId(self, userId):
# Reserved for userclient only
self.userId = userId
log.debug("Set connect userId: %s" % userId)
def setServer(self, server):
# Reserved for userclient only
self.server = server
log.debug("Set connect server: %s" % server)
def setToken(self, token):
# Reserved for userclient only
self.token = token
log.debug("Set connect token: %s" % token)
def startSession(self):
self.deviceId = self.clientInfo.getDeviceId()
# User is identified from this point
# Attach authenticated header to the session
verify = False
header = self.getHeader()
# If user enabled host certificate verification
try:
verify = self.sslverify
if self.sslclient is not None:
verify = self.sslclient
except:
log.info("Could not load SSL settings.")
# Start session
self.c = requests.Session()
self.c.headers = header
self.c.verify = verify
# Retry connections to the server
self.c.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.c.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
log.info("Requests session started on: %s" % self.server)
def stopSession(self):
try:
self.c.close()
except Exception:
log.warn("Requests session could not be terminated")
def getHeader(self, authenticate=True):
version = self.clientInfo.getVersion()
if not authenticate:
# If user is not authenticated
header = {
'X-Application': "Kodi/%s" % version,
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Accept': "application/json"
}
log.info("Header: %s" % header)
else:
token = self.token
# Attached to the requests session
header = {
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Accept': "application/json",
'X-Application': "Kodi/%s" % version,
'X-Connect-UserToken': token
}
log.info("Header: %s" % header)
return header
def doUrl(self, url, data=None, postBody=None, rtype="GET",
parameters=None, authenticate=True, timeout=None):
log.debug("=== ENTER connectUrl ===")
default_link = ""
if timeout is None:
timeout = self.timeout
# Get requests session
try:
# If connect user is authenticated
if authenticate:
try:
c = self.c
# Replace for the real values
url = url.replace("{server}", self.server)
url = url.replace("{UserId}", self.userId)
# Prepare request
if rtype == "GET":
r = c.get(url, json=postBody, params=parameters, timeout=timeout)
elif rtype == "POST":
r = c.post(url, data=data, timeout=timeout)
elif rtype == "DELETE":
r = c.delete(url, json=postBody, timeout=timeout)
except AttributeError:
# request session does not exists
self.server = "https://connect.emby.media/service"
self.userId = window('embyco_currUser')
self.token = window('embyco_accessToken%s' % self.userId)
header = self.getHeader()
verifyssl = False
# If user enables ssl verification
try:
verifyssl = self.sslverify
if self.sslclient is not None:
verifyssl = self.sslclient
except AttributeError:
pass
# Prepare request
if rtype == "GET":
r = requests.get(url,
json=postBody,
params=parameters,
headers=header,
timeout=timeout,
verify=verifyssl)
elif rtype == "POST":
r = requests.post(url,
data=data,
headers=header,
timeout=timeout,
verify=verifyssl)
# If user is not authenticated
else:
header = self.getHeader(authenticate=False)
verifyssl = False
# If user enables ssl verification
try:
verifyssl = self.sslverify
if self.sslclient is not None:
verifyssl = self.sslclient
except AttributeError:
pass
# Prepare request
if rtype == "GET":
r = requests.get(url,
json=postBody,
params=parameters,
headers=header,
timeout=timeout,
verify=verifyssl)
elif rtype == "POST":
r = requests.post(url,
data=data,
headers=header,
timeout=timeout,
verify=verifyssl)
##### THE RESPONSE #####
log.info(r.url)
log.info(r)
if r.status_code == 204:
# No body in the response
log.info("====== 204 Success ======")
elif r.status_code == requests.codes.ok:
try:
# UNICODE - JSON object
r = r.json()
log.info("====== 200 Success ======")
log.info("Response: %s" % r)
return r
except:
if r.headers.get('content-type') != "text/html":
log.info("Unable to convert the response for: %s" % url)
else:
r.raise_for_status()
##### EXCEPTIONS #####
except requests.exceptions.ConnectionError as e:
# Make the addon aware of status
pass
except requests.exceptions.ConnectTimeout as e:
log.warn("Server timeout at: %s" % url)
except requests.exceptions.HTTPError as e:
if r.status_code == 401:
# Unauthorized
pass
elif r.status_code in (301, 302):
# Redirects
pass
elif r.status_code == 400:
# Bad requests
pass
except requests.exceptions.SSLError as e:
log.warn("Invalid SSL certificate for: %s" % url)
except requests.exceptions.RequestException as e:
log.warn("Unknown error connecting to: %s" % url)
return default_link

View file

@ -0,0 +1,816 @@
# -*- coding: utf-8 -*-
#################################################################################################
import hashlib
import json
import logging
import requests
import socket
import time
from datetime import datetime
import credentials as cred
#################################################################################################
# Disable requests logging
from requests.packages.urllib3.exceptions import InsecureRequestWarning, InsecurePlatformWarning, SNIMissingWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
requests.packages.urllib3.disable_warnings(InsecurePlatformWarning)
requests.packages.urllib3.disable_warnings(SNIMissingWarning)
log = logging.getLogger("EMBY."+__name__.split('.')[-1])
#################################################################################################
ConnectionState = {
'Unavailable': 0,
'ServerSelection': 1,
'ServerSignIn': 2,
'SignedIn': 3,
'ConnectSignIn': 4,
'ServerUpdateNeeded': 5
}
ConnectionMode = {
'Local': 0,
'Remote': 1,
'Manual': 2
}
#################################################################################################
def getServerAddress(server, mode):
modes = {
ConnectionMode['Local']: server.get('LocalAddress'),
ConnectionMode['Remote']: server.get('RemoteAddress'),
ConnectionMode['Manual']: server.get('ManualAddress')
}
return (modes.get(mode) or
server.get('ManualAddress',server.get('LocalAddress',server.get('RemoteAddress'))))
class ConnectionManager(object):
default_timeout = 20
apiClients = []
minServerVersion = "3.0.5930"
connectUser = None
def __init__(self, appName, appVersion, deviceName, deviceId, capabilities=None, devicePixelRatio=None):
log.info("Begin ConnectionManager constructor")
self.credentialProvider = cred.Credentials()
self.appName = appName
self.appVersion = appVersion
self.deviceName = deviceName
self.deviceId = deviceId
self.capabilities = capabilities
self.devicePixelRatio = devicePixelRatio
def setFilePath(self, path):
# Set where to save persistant data
self.credentialProvider.setPath(path)
def _getAppVersion(self):
return self.appVersion
def _getCapabilities(self):
return self.capabilities
def _getDeviceId(self):
return self.deviceId
def _connectUserId(self):
return self.credentialProvider.getCredentials().get('ConnectUserId')
def _connectToken(self):
return self.credentialProvider.getCredentials().get('ConnectAccessToken')
def getServerInfo(self, id_):
servers = self.credentialProvider.getCredentials()['Servers']
for s in servers:
if s['Id'] == id_:
return s
def _getLastUsedServer(self):
servers = self.credentialProvider.getCredentials()['Servers']
if not len(servers):
return
try:
servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True)
except TypeError:
servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True)
return servers[0]
def _mergeServers(self, list1, list2):
for i in range(0, len(list2), 1):
try:
self.credentialProvider.addOrUpdateServer(list1, list2[i])
except KeyError:
continue
return list1
def _connectUser(self):
return self.connectUser
def _resolveFailure(self):
return {
'State': ConnectionState['Unavailable'],
'ConnectUser': self._connectUser()
}
def _getMinServerVersion(self, val=None):
if val is not None:
self.minServerVersion = val
return self.minServerVersion
def _updateServerInfo(self, server, systemInfo):
server['Name'] = systemInfo['ServerName']
server['Id'] = systemInfo['Id']
if systemInfo.get('LocalAddress'):
server['LocalAddress'] = systemInfo['LocalAddress']
if systemInfo.get('WanAddress'):
server['RemoteAddress'] = systemInfo['WanAddress']
if systemInfo.get('MacAddress'):
server['WakeOnLanInfos'] = [{'MacAddress': systemInfo['MacAddress']}]
def _getHeaders(self, request):
headers = request.setdefault('headers', {})
if request.get('dataType') == "json":
headers['Accept'] = "application/json"
request.pop('dataType')
headers['X-Application'] = self._addAppInfoToConnectRequest()
headers['Content-type'] = request.get('contentType',
'application/x-www-form-urlencoded; charset=UTF-8')
def requestUrl(self, request):
if not request:
raise AttributeError("Request cannot be null")
self._getHeaders(request)
request['timeout'] = request.get('timeout') or self.default_timeout
request['verify'] = request.get('ssl') or False
action = request['type']
request.pop('type', None)
request.pop('ssl', None)
log.debug("ConnectionManager requesting %s" % request)
try:
r = self._requests(action, **request)
log.info("ConnectionManager response status: %s" % r.status_code)
r.raise_for_status()
except Exception as e: # Elaborate on exceptions?
log.error(e)
raise
else:
try:
return r.json()
except ValueError:
r.content # Read response to release connection
return
def _requests(self, action, **kwargs):
if action == "GET":
r = requests.get(**kwargs)
elif action == "POST":
r = requests.post(**kwargs)
return r
def getEmbyServerUrl(self, baseUrl, handler):
return "%s/emby/%s" % (baseUrl, handler)
def getConnectUrl(self, handler):
return "https://connect.emby.media/service/%s" % handler
def _findServers(self, foundServers):
servers = []
for foundServer in foundServers:
server = self._convertEndpointAddressToManualAddress(foundServer)
info = {
'Id': foundServer['Id'],
'LocalAddress': server or foundServer['Address'],
'Name': foundServer['Name']
}
info['LastConnectionMode'] = ConnectionMode['Manual'] if info.get('ManualAddress') else ConnectionMode['Local']
servers.append(info)
else:
return servers
def _convertEndpointAddressToManualAddress(self, info):
if info.get('Address') and info.get('EndpointAddress'):
address = info['EndpointAddress'].split(':')[0]
# Determine the port, if any
parts = info['Address'].split(':')
if len(parts) > 1:
portString = parts[len(parts)-1]
try:
address += ":%s" % int(portString)
return self._normalizeAddress(address)
except ValueError:
pass
return None
def _serverDiscovery(self):
MULTI_GROUP = ("<broadcast>", 7359)
MESSAGE = "who is EmbyServer?"
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(1.0) # This controls the socket.timeout exception
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1)
log.debug("MultiGroup : %s" % str(MULTI_GROUP))
log.debug("Sending UDP Data: %s" % MESSAGE)
servers = []
try:
sock.sendto(MESSAGE, MULTI_GROUP)
except Exception as error:
log.error(error)
return servers
while True:
try:
data, addr = sock.recvfrom(1024) # buffer size
servers.append(json.loads(data))
except socket.timeout:
log.info("Found Servers: %s" % servers)
return servers
except Exception as e:
log.error("Error trying to find servers: %s" % e)
return servers
def _normalizeAddress(self, address):
# Attempt to correct bad input
address = address.strip()
address = address.lower()
if 'http' not in address:
address = "http://%s" % address
return address
def connectToAddress(self, address, options={}):
if not address:
return False
address = self._normalizeAddress(address)
def _onFail():
log.error("connectToAddress %s failed" % address)
return self._resolveFailure()
try:
publicInfo = self._tryConnect(address, options=options)
except Exception:
return _onFail()
else:
log.info("connectToAddress %s succeeded" % address)
server = {
'ManualAddress': address,
'LastConnectionMode': ConnectionMode['Manual']
}
self._updateServerInfo(server, publicInfo)
server = self.connectToServer(server, options)
if server is False:
return _onFail()
else:
return server
def onAuthenticated(self, result, options={}):
credentials = self.credentialProvider.getCredentials()
for s in credentials['Servers']:
if s['Id'] == result['ServerId']:
server = s
break
else: # Server not found?
return
if options.get('updateDateLastAccessed') is not False:
server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
server['UserId'] = result['User']['Id']
server['AccessToken'] = result['AccessToken']
self.credentialProvider.addOrUpdateServer(credentials['Servers'], server)
self._saveUserInfoIntoCredentials(server, result['User'])
self.credentialProvider.getCredentials(credentials)
def _tryConnect(self, url, timeout=None, options={}):
url = self.getEmbyServerUrl(url, "system/info/public")
log.info("tryConnect url: %s" % url)
return self.requestUrl({
'type': "GET",
'url': url,
'dataType': "json",
'timeout': timeout,
'ssl': options.get('ssl')
})
def _addAppInfoToConnectRequest(self):
return "%s/%s" % (self.appName, self.appVersion)
def _getConnectServers(self, credentials):
log.info("Begin getConnectServers")
servers = []
if not credentials.get('ConnectAccessToken') or not credentials.get('ConnectUserId'):
return servers
url = self.getConnectUrl("servers?userId=%s" % credentials['ConnectUserId'])
request = {
'type': "GET",
'url': url,
'dataType': "json",
'headers': {
'X-Connect-UserToken': credentials['ConnectAccessToken']
}
}
for server in self.requestUrl(request):
servers.append({
'ExchangeToken': server['AccessKey'],
'ConnectServerId': server['Id'],
'Id': server['SystemId'],
'Name': server['Name'],
'RemoteAddress': server['Url'],
'LocalAddress': server['LocalAddress'],
'UserLinkType': "Guest" if server['UserType'].lower() == "guest" else "LinkedUser",
})
return servers
def getAvailableServers(self):
log.info("Begin getAvailableServers")
# Clone the array
credentials = self.credentialProvider.getCredentials()
connectServers = self._getConnectServers(credentials)
foundServers = self._findServers(self._serverDiscovery())
servers = list(credentials['Servers'])
self._mergeServers(servers, foundServers)
self._mergeServers(servers, connectServers)
servers = self._filterServers(servers, connectServers)
try:
servers.sort(key=lambda x: datetime.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ"), reverse=True)
except TypeError:
servers.sort(key=lambda x: datetime(*(time.strptime(x['DateLastAccessed'], "%Y-%m-%dT%H:%M:%SZ")[0:6])), reverse=True)
credentials['Servers'] = servers
self.credentialProvider.getCredentials(credentials)
return servers
def _filterServers(self, servers, connectServers):
filtered = []
for server in servers:
# It's not a connect server, so assume it's still valid
if server.get('ExchangeToken') is None:
filtered.append(server)
continue
for connectServer in connectServers:
if server['Id'] == connectServer['Id']:
filtered.append(server)
break
else:
return filtered
def _getConnectPasswordHash(self, password):
password = self._cleanConnectPassword(password)
return hashlib.md5(password).hexdigest()
def _saveUserInfoIntoCredentials(self, server, user):
info = {
'Id': user['Id'],
'IsSignedInOffline': True
}
self.credentialProvider.addOrUpdateUser(server, info)
def _compareVersions(self, a, b):
"""
-1 a is smaller
1 a is larger
0 equal
"""
a = a.split('.')
b = b.split('.')
for i in range(0, max(len(a), len(b)), 1):
try:
aVal = a[i]
except IndexError:
aVal = 0
try:
bVal = b[i]
except IndexError:
bVal = 0
if aVal < bVal:
return -1
if aVal > bVal:
return 1
return 0
def connectToServer(self, server, options={}):
log.info("begin connectToServer")
tests = []
if server.get('LastConnectionMode') is not None:
#tests.append(server['LastConnectionMode'])
pass
if ConnectionMode['Manual'] not in tests:
tests.append(ConnectionMode['Manual'])
if ConnectionMode['Local'] not in tests:
tests.append(ConnectionMode['Local'])
if ConnectionMode['Remote'] not in tests:
tests.append(ConnectionMode['Remote'])
# TODO: begin to wake server
log.info("beginning connection tests")
return self._testNextConnectionMode(tests, 0, server, options)
def _stringEqualsIgnoreCase(self, str1, str2):
return (str1 or "").lower() == (str2 or "").lower()
def _testNextConnectionMode(self, tests, index, server, options):
if index >= len(tests):
log.info("Tested all connection modes. Failing server connection.")
return self._resolveFailure()
mode = tests[index]
address = getServerAddress(server, mode)
enableRetry = False
skipTest = False
timeout = self.default_timeout
if mode == ConnectionMode['Local']:
enableRetry = True
timeout = 8
if self._stringEqualsIgnoreCase(address, server.get('ManualAddress')):
log.info("skipping LocalAddress test because it is the same as ManualAddress")
skipTest = True
elif mode == ConnectionMode['Manual']:
if self._stringEqualsIgnoreCase(address, server.get('LocalAddress')):
enableRetry = True
timeout = 8
if skipTest or not address:
log.info("skipping test at index: %s" % index)
return self._testNextConnectionMode(tests, index+1, server, options)
log.info("testing connection mode %s with server %s" % (mode, server['Name']))
try:
result = self._tryConnect(address, timeout, options)
except Exception:
log.error("test failed for connection mode %s with server %s" % (mode, server['Name']))
if enableRetry:
# TODO: wake on lan and retry
return self._testNextConnectionMode(tests, index+1, server, options)
else:
return self._testNextConnectionMode(tests, index+1, server, options)
else:
if self._compareVersions(self._getMinServerVersion(), result['Version']) == 1:
log.warn("minServerVersion requirement not met. Server version: %s" % result['Version'])
return {
'State': ConnectionState['ServerUpdateNeeded'],
'Servers': [server]
}
else:
log.info("calling onSuccessfulConnection with connection mode %s with server %s"
% (mode, server['Name']))
return self._onSuccessfulConnection(server, result, mode, options)
def _onSuccessfulConnection(self, server, systemInfo, connectionMode, options):
credentials = self.credentialProvider.getCredentials()
if credentials.get('ConnectAccessToken') and options.get('enableAutoLogin') is not False:
if self._ensureConnectUser(credentials) is not False:
if server.get('ExchangeToken'):
self._addAuthenticationInfoFromConnect(server, connectionMode, credentials, options)
return self._afterConnectValidated(server, credentials, systemInfo, connectionMode, True, options)
def _afterConnectValidated(self, server, credentials, systemInfo, connectionMode, verifyLocalAuthentication, options):
if options.get('enableAutoLogin') is False:
server['UserId'] = None
server['AccessToken'] = None
elif (verifyLocalAuthentication and server.get('AccessToken') and
options.get('enableAutoLogin') is not False):
if self._validateAuthentication(server, connectionMode, options) is not False:
return self._afterConnectValidated(server, credentials, systemInfo, connectionMode, False, options)
return
self._updateServerInfo(server, systemInfo)
server['LastConnectionMode'] = connectionMode
if options.get('updateDateLastAccessed') is not False:
server['DateLastAccessed'] = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
self.credentialProvider.addOrUpdateServer(credentials['Servers'], server)
self.credentialProvider.getCredentials(credentials)
result = {
'Servers': [],
'ConnectUser': self._connectUser()
}
result['State'] = ConnectionState['SignedIn'] if (server.get('AccessToken') and options.get('enableAutoLogin') is not False) else ConnectionState['ServerSignIn']
result['Servers'].append(server)
# Connected
return result
def _validateAuthentication(self, server, connectionMode, options={}):
url = getServerAddress(server, connectionMode)
request = {
'type': "GET",
'url': self.getEmbyServerUrl(url, "System/Info"),
'ssl': options.get('ssl'),
'dataType': "json",
'headers': {
'X-MediaBrowser-Token': server['AccessToken']
}
}
try:
systemInfo = self.requestUrl(request)
self._updateServerInfo(server, systemInfo)
if server.get('UserId'):
user = self.requestUrl({
'type': "GET",
'url': self.getEmbyServerUrl(url, "users/%s" % server['UserId']),
'ssl': options.get('ssl'),
'dataType': "json",
'headers': {
'X-MediaBrowser-Token': server['AccessToken']
}
})
except Exception:
server['UserId'] = None
server['AccessToken'] = None
return False
def loginToConnect(self, username, password):
if not username:
raise AttributeError("username cannot be empty")
if not password:
raise AttributeError("password cannot be empty")
md5 = self._getConnectPasswordHash(password)
request = {
'type': "POST",
'url': self.getConnectUrl("user/authenticate"),
'data': {
'nameOrEmail': username,
'password': md5
},
'dataType': "json"
}
try:
result = self.requestUrl(request)
except Exception as e: # Failed to login
log.error(e)
return False
else:
credentials = self.credentialProvider.getCredentials()
credentials['ConnectAccessToken'] = result['AccessToken']
credentials['ConnectUserId'] = result['User']['Id']
credentials['ConnectUser'] = result['User']['DisplayName']
self.credentialProvider.getCredentials(credentials)
# Signed in
self._onConnectUserSignIn(result['User'])
return result
def _onConnectUserSignIn(self, user):
self.connectUser = user
log.info("connectusersignedin %s" % user)
def _getConnectUser(self, userId, accessToken):
if not userId:
raise AttributeError("null userId")
if not accessToken:
raise AttributeError("null accessToken")
url = self.getConnectUrl('user?id=%s' % userId)
return self.requestUrl({
'type': "GET",
'url': url,
'dataType': "json",
'headers': {
'X-Connect-UserToken': accessToken
}
})
def _addAuthenticationInfoFromConnect(self, server, connectionMode, credentials, options={}):
if not server.get('ExchangeToken'):
raise KeyError("server['ExchangeToken'] cannot be null")
if not credentials.get('ConnectUserId'):
raise KeyError("credentials['ConnectUserId'] cannot be null")
url = getServerAddress(server, connectionMode)
url = self.getEmbyServerUrl(url, "Connect/Exchange?format=json")
auth = ('MediaBrowser Client="%s", Device="%s", DeviceId="%s", Version="%s"'
% (self.appName, self.deviceName, self.deviceId, self.appVersion))
try:
auth = self.requestUrl({
'url': url,
'type': "GET",
'dataType': "json",
'ssl': options.get('ssl'),
'params': {
'ConnectUserId': credentials['ConnectUserId']
},
'headers': {
'X-MediaBrowser-Token': server['ExchangeToken'],
'X-Emby-Authorization': auth
}
})
except Exception:
server['UserId'] = None
server['AccessToken'] = None
return False
else:
server['UserId'] = auth['LocalUserId']
server['AccessToken'] = auth['AccessToken']
return auth
def _ensureConnectUser(self, credentials):
if self.connectUser and self.connectUser['Id'] == credentials['ConnectUserId']:
return
elif credentials.get('ConnectUserId') and credentials.get('ConnectAccessToken'):
self.connectUser = None
try:
result = self._getConnectUser(credentials['ConnectUserId'], credentials['ConnectAccessToken'])
self._onConnectUserSignIn(result)
except Exception:
return False
def connect(self, options={}):
log.info("Begin connect")
servers = self.getAvailableServers()
return self._connectToServers(servers, options)
def _connectToServers(self, servers, options):
log.info("Begin connectToServers, with %s servers" % len(servers))
if len(servers) == 1:
result = self.connectToServer(servers[0], options)
if result and result.get('State') == ConnectionState['Unavailable']:
result['State'] = ConnectionState['ConnectSignIn'] if result['ConnectUser'] == None else ConnectionState['ServerSelection']
log.info("resolving connectToServers with result['State']: %s" % result)
return result
firstServer = self._getLastUsedServer()
# See if we have any saved credentials and can auto sign in
if firstServer:
result = self.connectToServer(firstServer, options)
if result and result.get('State') == ConnectionState['SignedIn']:
return result
# Return loaded credentials if exists
credentials = self.credentialProvider.getCredentials()
self._ensureConnectUser(credentials)
return {
'Servers': servers,
'State': ConnectionState['ConnectSignIn'] if (not len(servers) and not self._connectUser()) else ConnectionState['ServerSelection'],
'ConnectUser': self._connectUser()
}
def _cleanConnectPassword(self, password):
password = password or ""
password = password.replace("&", '&amp;')
password = password.replace("/", '&#092;')
password = password.replace("!", '&#33;')
password = password.replace("$", '&#036;')
password = password.replace("\"", '&quot;')
password = password.replace("<", '&lt;')
password = password.replace(">", '&gt;')
password = password.replace("'", '&#39;')
return password
def clearData(self):
log.info("connection manager clearing data")
self.connectUser = None
credentials = self.credentialProvider.getCredentials()
credentials['ConnectAccessToken'] = None
credentials['ConnectUserId'] = None
credentials['Servers'] = []
self.credentialProvider.getCredentials(credentials)

View file

@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
#################################################################################################
import json
import logging
import os
import time
from datetime import datetime
#################################################################################################
log = logging.getLogger("EMBY."+__name__.split('.')[-1])
#################################################################################################
class Credentials(object):
_shared_state = {} # Borg
credentials = None
path = ""
def __init__(self):
self.__dict__ = self._shared_state
def setPath(self, path):
# Path to save persistant data.txt
self.path = path
def _ensure(self):
if self.credentials is None:
try:
with open(os.path.join(self.path, 'data.txt')) as infile:
self.credentials = json.load(infile)
except Exception as e: # File is either empty or missing
log.warn(e)
self.credentials = {}
log.info("credentials initialized with: %s" % self.credentials)
self.credentials['Servers'] = self.credentials.setdefault('Servers', [])
def _get(self):
self._ensure()
return self.credentials
def _set(self, data):
if data:
self.credentials = data
# Set credentials to file
with open(os.path.join(self.path, 'data.txt'), 'w') as outfile:
json.dump(data, outfile, indent=4, ensure_ascii=False)
else:
self._clear()
log.info("credentialsupdated")
def _clear(self):
self.credentials = None
# Remove credentials from file
with open(os.path.join(self.path, 'data.txt'), 'w'): pass
def getCredentials(self, data=None):
if data is not None:
self._set(data)
return self._get()
def addOrUpdateServer(self, list_, server):
if server.get('Id') is None:
raise KeyError("Server['Id'] cannot be null or empty")
# Add default DateLastAccessed if doesn't exist.
server.setdefault('DateLastAccessed', "2001-01-01T00:00:00Z")
for existing in list_:
if existing['Id'] == server['Id']:
# Merge the data
if server.get('DateLastAccessed'):
if self._dateObject(server['DateLastAccessed']) > self._dateObject(existing['DateLastAccessed']):
existing['DateLastAccessed'] = server['DateLastAccessed']
if server.get('UserLinkType'):
existing['UserLinkType'] = server['UserLinkType']
if server.get('AccessToken'):
existing['AccessToken'] = server['AccessToken']
existing['UserId'] = server['UserId']
if server.get('ExchangeToken'):
existing['ExchangeToken'] = server['ExchangeToken']
if server.get('RemoteAddress'):
existing['RemoteAddress'] = server['RemoteAddress']
if server.get('ManualAddress'):
existing['ManualAddress'] = server['ManualAddress']
if server.get('LocalAddress'):
existing['LocalAddress'] = server['LocalAddress']
if server.get('Name'):
existing['Name'] = server['Name']
if server.get('WakeOnLanInfos'):
existing['WakeOnLanInfos'] = server['WakeOnLanInfos']
if server.get('LastConnectionMode') is not None:
existing['LastConnectionMode'] = server['LastConnectionMode']
if server.get('ConnectServerId'):
existing['ConnectServerId'] = server['ConnectServerId']
return existing
else:
list_.append(server)
return server
def addOrUpdateUser(self, server, user):
for existing in server.setdefault('Users', []):
if existing['Id'] == user['Id']:
# Merge the data
existing['IsSignedInOffline'] = True
break
else:
server['Users'].append(user)
def _dateObject(self, date):
# Convert string to date
try:
date_obj = 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_obj = datetime(*(time.strptime(date, "%Y-%m-%dT%H:%M:%SZ")[0:6]))
return date_obj

View file

@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import xbmc
import xbmcaddon
import xbmcvfs
import clientinfo
import read_embyserver as embyserver
import connect.connectionmanager as connectionmanager
from dialogs import ServerConnect, UsersConnect, LoginConnect, LoginManual, ServerManual
from ga_client import GoogleAnalytics
from utils import window
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
addon = xbmcaddon.Addon(id='plugin.video.emby')
STATE = connectionmanager.ConnectionState
XML_PATH = (addon.getAddonInfo('path'), "default", "1080i")
##################################################################################################
class ConnectManager(object):
_shared_state = {} # Borg
state = {}
def __init__(self):
self.__dict__ = self._shared_state
client_info = clientinfo.ClientInfo()
self.emby = embyserver.Read_EmbyServer()
version = client_info.get_version()
device_name = client_info.get_device_name()
device_id = client_info.get_device_id()
self._connect = connectionmanager.ConnectionManager(appName="Kodi",
appVersion=version,
deviceName=device_name,
deviceId=device_id)
path = xbmc.translatePath(
"special://profile/addon_data/plugin.video.emby/").decode('utf-8')
if not xbmcvfs.exists(path):
xbmcvfs.mkdirs(path)
self._connect.setFilePath(path)
if window('emby_state.json'):
self.state = window('emby_state.json')
elif not self.state:
self.state = self._connect.connect()
log.info("Started with: %s", self.state)
window('emby_state.json', value=self.state)
def update_state(self):
self.state = self._connect.connect({'updateDateLastAccessed': False})
return self.get_state()
def get_state(self):
window('emby_state.json', value=self.state)
return self.state
def get_server(self, server, options={}):
self.state = self._connect.connectToAddress(server, options)
return self.get_state()
@classmethod
def get_address(cls, server):
return connectionmanager.getServerAddress(server, server['LastConnectionMode'])
def clear_data(self):
self._connect.clearData()
def select_servers(self):
# Will return selected server or raise error
state = self._connect.connect({'enableAutoLogin': False})
user = state.get('ConnectUser') or {}
dialog = ServerConnect("script-emby-connect-server.xml", *XML_PATH)
kwargs = {
'connect_manager': self._connect,
'username': user.get('DisplayName', ""),
'user_image': user.get('ImageUrl'),
'servers': state.get('Servers') or [],
'emby_connect': False if user else True
}
dialog.set_args(**kwargs)
dialog.doModal()
if dialog.is_server_selected():
log.debug("Server selected")
return dialog.get_server()
elif dialog.is_connect_login():
log.debug("Login with Emby Connect")
try: # Login to emby connect
self.login_connect()
except RuntimeError:
pass
return self.select_servers()
elif dialog.is_manual_server():
log.debug("Add manual server")
try: # Add manual server address
return self.manual_server()
except RuntimeError:
return self.select_servers()
else:
raise RuntimeError("No server selected")
def manual_server(self):
# Return server or raise error
dialog = ServerManual("script-emby-connect-server-manual.xml", *XML_PATH)
dialog.set_connect_manager(self._connect)
dialog.doModal()
if dialog.is_connected():
return dialog.get_server()
else:
raise RuntimeError("Server is not connected")
def login_connect(self):
# Return connect user or raise error
dialog = LoginConnect("script-emby-connect-login.xml", *XML_PATH)
dialog.set_connect_manager(self._connect)
dialog.doModal()
self.update_state()
if dialog.is_logged_in():
return dialog.get_user()
else:
raise RuntimeError("Connect user is not logged in")
def login(self, server=None):
ga = GoogleAnalytics()
ga.sendEventData("Connect", "UserLogin")
# Return user or raise error
server = server or self.state['Servers'][0]
server_address = connectionmanager.getServerAddress(server, server['LastConnectionMode'])
users = self.emby.getUsers(server_address)
if not users:
try:
return self.login_manual(server_address)
except RuntimeError:
raise RuntimeError("No user selected")
dialog = UsersConnect("script-emby-connect-users.xml", *XML_PATH)
dialog.set_server(server_address)
dialog.set_users(users)
dialog.doModal()
if dialog.is_user_selected():
user = dialog.get_user()
username = user['Name']
if user['HasPassword']:
log.debug("User has password, present manual login")
try:
return self.login_manual(server_address, username)
except RuntimeError:
return self.login(server)
else:
user = self.emby.loginUser(server_address, username)
self._connect.onAuthenticated(user)
return user
elif dialog.is_manual_login():
try:
return self.login_manual(server_address)
except RuntimeError:
return self.login(server)
else:
raise RuntimeError("No user selected")
def login_manual(self, server, user=None):
# Return manual login user authenticated or raise error
dialog = LoginManual("script-emby-connect-login-manual.xml", *XML_PATH)
dialog.set_server(server)
dialog.set_user(user)
dialog.doModal()
if dialog.is_logged_in():
user = dialog.get_user()
self._connect.onAuthenticated(user)
return user
else:
raise RuntimeError("User is not authenticated")
def update_token(self, server):
credentials = self._connect.credentialProvider.getCredentials()
self._connect.credentialProvider.addOrUpdateServer(credentials['Servers'], server)
for server in self.get_state()['Servers']:
for cred_server in credentials['Servers']:
if server['Id'] == cred_server['Id']:
# Update token saved in current state
server.update(cred_server)
# Update the token in data.txt
self._connect.credentialProvider.getCredentials(credentials)
def get_connect_servers(self):
connect_servers = []
servers = self._connect.getAvailableServers()
for server in servers:
if 'ExchangeToken' in server:
result = self.connect_server(server)
if result['State'] == STATE['SignedIn']:
connect_servers.append(server)
log.info(connect_servers)
return connect_servers
def connect_server(self, server):
return self._connect.connectToServer(server, {'updateDateLastAccessed': False})

View file

@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
#################################################################################################
import logging
import xbmc
import xbmcaddon
import api
import read_embyserver as embyserver
import embydb_functions as embydb
import musicutils as musicutils
from utils import settings, dialog, language as lang, kodiSQL
from dialogs import context
#################################################################################################
log = logging.getLogger("EMBY."+__name__)
OPTIONS = {
'Refresh': lang(30410),
'Delete': lang(30409),
'Addon': lang(30408),
'AddFav': lang(30405),
'RemoveFav': lang(30406),
'RateSong': lang(30407),
'Transcode': lang(30412)
}
#################################################################################################
class ContextMenu(object):
_selected_option = None
def __init__(self):
self.emby = embyserver.Read_EmbyServer()
self.kodi_id = xbmc.getInfoLabel('ListItem.DBID').decode('utf-8')
self.item_type = self._get_item_type()
self.item_id = self._get_item_id(self.kodi_id, self.item_type)
log.info("Found item_id: %s item_type: %s", self.item_id, self.item_type)
if self.item_id:
self.item = self.emby.getItem(self.item_id)
self.api = api.API(self.item)
if self._select_menu():
self._action_menu()
if self._selected_option in (OPTIONS['Delete'], OPTIONS['AddFav'],
OPTIONS['RemoveFav'], OPTIONS['RateSong']):
log.info("refreshing container")
xbmc.sleep(500)
xbmc.executebuiltin('Container.Refresh')
@classmethod
def _get_item_type(cls):
item_type = xbmc.getInfoLabel('ListItem.DBTYPE').decode('utf-8')
if not item_type:
if xbmc.getCondVisibility('Container.Content(albums)'):
item_type = "album"
elif xbmc.getCondVisibility('Container.Content(artists)'):
item_type = "artist"
elif xbmc.getCondVisibility('Container.Content(songs)'):
item_type = "song"
elif xbmc.getCondVisibility('Container.Content(pictures)'):
item_type = "picture"
else:
log.info("item_type is unknown")
return item_type
@classmethod
def _get_item_id(cls, kodi_id, item_type):
item_id = xbmc.getInfoLabel('ListItem.Property(embyid)')
if not item_id and kodi_id and item_type:
conn = kodiSQL('emby')
cursor = conn.cursor()
emby_db = embydb.Embydb_Functions(cursor)
item = emby_db.getItem_byKodiId(kodi_id, item_type)
cursor.close()
try:
item_id = item[0]
except TypeError:
pass
return item_id
def _select_menu(self):
# Display select dialog
userdata = self.api.get_userdata()
options = []
if self.item_type in ("movie", "episode", "song"):
#options.append(OPTIONS['Transcode'])
pass
if userdata['Favorite']:
# Remove from emby favourites
options.append(OPTIONS['RemoveFav'])
else:
# Add to emby favourites
options.append(OPTIONS['AddFav'])
if self.item_type == "song":
# Set custom song rating
options.append(OPTIONS['RateSong'])
# Refresh item
options.append(OPTIONS['Refresh'])
# Delete item
options.append(OPTIONS['Delete'])
# Addon settings
options.append(OPTIONS['Addon'])
addon = xbmcaddon.Addon('plugin.video.emby')
context_menu = context.ContextMenu("script-emby-context.xml", addon.getAddonInfo('path'),
"default", "1080i")
context_menu.set_options(options)
context_menu.doModal()
if context_menu.is_selected():
self._selected_option = context_menu.get_selected()
return self._selected_option
def _action_menu(self):
selected = self._selected_option
if selected == OPTIONS['Transcode']:
pass
elif selected == OPTIONS['Refresh']:
self.emby.refreshItem(self.item_id)
elif selected == OPTIONS['AddFav']:
self.emby.updateUserRating(self.item_id, favourite=True)
elif selected == OPTIONS['RemoveFav']:
self.emby.updateUserRating(self.item_id, favourite=False)
elif selected == OPTIONS['RateSong']:
self._rate_song()
elif selected == OPTIONS['Addon']:
xbmc.executebuiltin('Addon.OpenSettings(plugin.video.emby)')
elif selected == OPTIONS['Delete']:
self._delete_item()
def _rate_song(self):
conn = kodiSQL('music')
cursor = conn.cursor()
query = "SELECT rating FROM song WHERE idSong = ?"
cursor.execute(query, (self.kodi_id,))
try:
value = cursor.fetchone()[0]
current_value = int(round(float(value), 0))
except TypeError:
pass
else:
new_value = dialog("numeric", 0, lang(30411), str(current_value))
if new_value > -1:
new_value = int(new_value)
if new_value > 5:
new_value = 5
if settings('enableUpdateSongRating') == "true":
musicutils.updateRatingToFile(new_value, self.api.get_file_path())
query = "UPDATE song SET rating = ? WHERE idSong = ?"
cursor.execute(query, (new_value, self.kodi_id,))
conn.commit()
finally:
cursor.close()
def _delete_item(self):
delete = True
if settings('skipContextMenu') != "true":
if not dialog(type_="yesno", heading="{emby}", line1=lang(33041)):
log.info("User skipped deletion for: %s", self.item_id)
delete = False
if delete:
log.info("Deleting request: %s", self.item_id)
self.emby.deleteItem(self.item_id)

View file

@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
##################################################################################################
import os
import xbmcgui
import xbmcaddon
##################################################################################################
addon = xbmcaddon.Addon('plugin.video.emby')
ACTION_BACK = 92
SIGN_IN = 200
REMIND_LATER = 201
class LoginConnect(xbmcgui.WindowXMLDialog):
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def __add_editcontrol(self, x, y, height, width, password=0):
media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
control = xbmcgui.ControlEdit(0,0,0,0,
label="User",
font="font10",
textColor="ff464646",
focusTexture=os.path.join(media, "button-focus.png"),
noFocusTexture=os.path.join(media, "button-focus.png"),
isPassword=password)
control.setPosition(x,y)
control.setHeight(height)
control.setWidth(width)
self.addControl(control)
return control
def onInit(self):
self.user_field = self.__add_editcontrol(685,385,40,500)
self.setFocus(self.user_field)
self.password_field = self.__add_editcontrol(685,470,40,500, password=1)
self.signin_button = self.getControl(SIGN_IN)
self.remind_button = self.getControl(REMIND_LATER)
self.user_field.controlUp(self.remind_button)
self.user_field.controlDown(self.password_field)
self.password_field.controlUp(self.user_field)
self.password_field.controlDown(self.signin_button)
self.signin_button.controlUp(self.password_field)
self.remind_button.controlDown(self.user_field)
def onClick(self, control):
if control == SIGN_IN:
# Sign in to emby connect
self.user = self.user_field.getText()
__password = self.password_field.getText()
### REVIEW ONCE CONNECT MODULE IS MADE
self.close()
elif control == REMIND_LATER:
# Remind me later
self.close()
def onAction(self, action):
if action == ACTION_BACK:
self.close()

View file

@ -0,0 +1,6 @@
# Dummy file to make this directory a package.
from serverconnect import ServerConnect
from usersconnect import UsersConnect
from loginconnect import LoginConnect
from loginmanual import LoginManual
from servermanual import ServerManual

View file

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import os
import xbmcgui
import xbmcaddon
from utils import window
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
addon = xbmcaddon.Addon('plugin.video.emby')
ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92
ACTION_SELECT_ITEM = 7
ACTION_MOUSE_LEFT_CLICK = 100
LIST = 155
USER_IMAGE = 150
##################################################################################################
class ContextMenu(xbmcgui.WindowXMLDialog):
_options = []
selected_option = None
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def set_options(self, options=[]):
self._options = options
def is_selected(self):
return True if self.selected_option else False
def get_selected(self):
return self.selected_option
def onInit(self):
if window('EmbyUserImage'):
self.getControl(USER_IMAGE).setImage(window('EmbyUserImage'))
height = 479 + (len(self._options) * 55)
log.info("options: %s", self._options)
self.list_ = self.getControl(LIST)
for option in self._options:
self.list_.addItem(self._add_listitem(option))
self.background = self._add_editcontrol(730, height, 30, 450)
self.setFocus(self.list_)
def onAction(self, action):
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close()
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK):
if self.getFocusId() == LIST:
option = self.list_.getSelectedItem()
self.selected_option = option.getLabel()
log.info('option selected: %s', self.selected_option)
self.close()
def _add_editcontrol(self, x, y, height, width, password=0):
media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
control = xbmcgui.ControlImage(0, 0, 0, 0,
filename=os.path.join(media, "white.png"),
aspectRatio=0,
colorDiffuse="ff111111")
control.setPosition(x, y)
control.setHeight(height)
control.setWidth(width)
self.addControl(control)
return control
@classmethod
def _add_listitem(cls, label):
return xbmcgui.ListItem(label)

View file

@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import os
import xbmcgui
import xbmcaddon
from utils import language as lang
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
addon = xbmcaddon.Addon('plugin.video.emby')
ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92
SIGN_IN = 200
CANCEL = 201
ERROR_TOGGLE = 202
ERROR_MSG = 203
ERROR = {
'Invalid': 1,
'Empty': 2
}
##################################################################################################
class LoginConnect(xbmcgui.WindowXMLDialog):
_user = None
error = None
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def set_connect_manager(self, connect_manager):
self.connect_manager = connect_manager
def is_logged_in(self):
return True if self._user else False
def get_user(self):
return self._user
def onInit(self):
self.user_field = self._add_editcontrol(725, 385, 40, 500)
self.setFocus(self.user_field)
self.password_field = self._add_editcontrol(725, 470, 40, 500, password=1)
self.signin_button = self.getControl(SIGN_IN)
self.remind_button = self.getControl(CANCEL)
self.error_toggle = self.getControl(ERROR_TOGGLE)
self.error_msg = self.getControl(ERROR_MSG)
self.user_field.controlUp(self.remind_button)
self.user_field.controlDown(self.password_field)
self.password_field.controlUp(self.user_field)
self.password_field.controlDown(self.signin_button)
self.signin_button.controlUp(self.password_field)
self.remind_button.controlDown(self.user_field)
def onClick(self, control):
if control == SIGN_IN:
# Sign in to emby connect
self._disable_error()
user = self.user_field.getText()
password = self.password_field.getText()
if not user or not password:
# Display error
self._error(ERROR['Empty'], lang(30608))
log.error("Username or password cannot be null")
elif self._login(user, password):
self.close()
elif control == CANCEL:
# Remind me later
self.close()
def onAction(self, action):
if (self.error == ERROR['Empty']
and self.user_field.getText() and self.password_field.getText()):
self._disable_error()
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close()
def _add_editcontrol(self, x, y, height, width, password=0):
media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
control = xbmcgui.ControlEdit(0, 0, 0, 0,
label="User",
font="font10",
textColor="ff525252",
focusTexture=os.path.join(media, "button-focus.png"),
noFocusTexture=os.path.join(media, "button-focus.png"),
isPassword=password)
control.setPosition(x, y)
control.setHeight(height)
control.setWidth(width)
self.addControl(control)
return control
def _login(self, username, password):
result = self.connect_manager.loginToConnect(username, password)
if result is False:
self._error(ERROR['Invalid'], lang(33009))
return False
else:
self._user = result
return True
def _error(self, state, message):
self.error = state
self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('True')
def _disable_error(self):
self.error = None
self.error_toggle.setVisibleCondition('False')

View file

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import os
import xbmcgui
import xbmcaddon
import read_embyserver as embyserver
from utils import language as lang
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
addon = xbmcaddon.Addon('plugin.video.emby')
ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92
SIGN_IN = 200
CANCEL = 201
ERROR_TOGGLE = 202
ERROR_MSG = 203
ERROR = {
'Invalid': 1,
'Empty': 2
}
##################################################################################################
class LoginManual(xbmcgui.WindowXMLDialog):
_user = None
error = None
username = None
def __init__(self, *args, **kwargs):
self.emby = embyserver.Read_EmbyServer()
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def is_logged_in(self):
return True if self._user else False
def set_server(self, server):
self.server = server
def set_user(self, user):
self.username = user or {}
def get_user(self):
return self._user
def onInit(self):
self.signin_button = self.getControl(SIGN_IN)
self.cancel_button = self.getControl(CANCEL)
self.error_toggle = self.getControl(ERROR_TOGGLE)
self.error_msg = self.getControl(ERROR_MSG)
self.user_field = self._add_editcontrol(725, 400, 40, 500)
self.password_field = self._add_editcontrol(725, 475, 40, 500, password=1)
if self.username:
self.user_field.setText(self.username)
self.setFocus(self.password_field)
else:
self.setFocus(self.user_field)
self.user_field.controlUp(self.cancel_button)
self.user_field.controlDown(self.password_field)
self.password_field.controlUp(self.user_field)
self.password_field.controlDown(self.signin_button)
self.signin_button.controlUp(self.password_field)
self.cancel_button.controlDown(self.user_field)
def onClick(self, control):
if control == SIGN_IN:
# Sign in to emby connect
self._disable_error()
user = self.user_field.getText()
password = self.password_field.getText()
if not user:
# Display error
self._error(ERROR['Empty'], lang(30613))
log.error("Username cannot be null")
elif self._login(user, password):
self.close()
elif control == CANCEL:
# Remind me later
self.close()
def onAction(self, action):
if self.error == ERROR['Empty'] and self.user_field.getText():
self._disable_error()
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close()
def _add_editcontrol(self, x, y, height, width, password=0):
media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
control = xbmcgui.ControlEdit(0, 0, 0, 0,
label="User",
font="font10",
textColor="ff525252",
focusTexture=os.path.join(media, "button-focus.png"),
noFocusTexture=os.path.join(media, "button-focus.png"),
isPassword=password)
control.setPosition(x, y)
control.setHeight(height)
control.setWidth(width)
self.addControl(control)
return control
def _login(self, username, password):
result = self.emby.loginUser(self.server, username, password)
if not result:
self._error(ERROR['Invalid'], lang(33009))
return False
else:
self._user = result
return True
def _error(self, state, message):
self.error = state
self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('True')
def _disable_error(self):
self.error = None
self.error_toggle.setVisibleCondition('False')

View file

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import xbmc
import xbmcgui
import connect.connectionmanager as connectionmanager
from utils import language as lang
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
CONN_STATE = connectionmanager.ConnectionState
ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92
ACTION_SELECT_ITEM = 7
ACTION_MOUSE_LEFT_CLICK = 100
USER_IMAGE = 150
USER_NAME = 151
LIST = 155
CANCEL = 201
MESSAGE_BOX = 202
MESSAGE = 203
BUSY = 204
EMBY_CONNECT = 205
MANUAL_SERVER = 206
##################################################################################################
class ServerConnect(xbmcgui.WindowXMLDialog):
username = ""
user_image = None
servers = []
_selected_server = None
_connect_login = False
_manual_server = False
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def set_args(self, **kwargs):
# connect_manager, username, user_image, servers, emby_connect
for key, value in kwargs.iteritems():
setattr(self, key, value)
def is_server_selected(self):
return True if self._selected_server else False
def get_server(self):
return self._selected_server
def is_connect_login(self):
return self._connect_login
def is_manual_server(self):
return self._manual_server
def onInit(self):
self.message = self.getControl(MESSAGE)
self.message_box = self.getControl(MESSAGE_BOX)
self.busy = self.getControl(BUSY)
self.list_ = self.getControl(LIST)
for server in self.servers:
server_type = "wifi" if server.get('ExchangeToken') else "network"
self.list_.addItem(self._add_listitem(server['Name'], server['Id'], server_type))
self.getControl(USER_NAME).setLabel("%s %s" % (lang(33000), self.username.decode('utf-8')))
if self.user_image is not None:
self.getControl(USER_IMAGE).setImage(self.user_image)
if not self.emby_connect: # Change connect user
self.getControl(EMBY_CONNECT).setLabel("[UPPERCASE][B]"+lang(30618)+"[/B][/UPPERCASE]")
if self.servers:
self.setFocus(self.list_)
@classmethod
def _add_listitem(cls, label, server_id, server_type):
item = xbmcgui.ListItem(label)
item.setProperty('id', server_id)
item.setProperty('server_type', server_type)
return item
def onAction(self, action):
if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR):
self.close()
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK):
if self.getFocusId() == LIST:
server = self.list_.getSelectedItem()
selected_id = server.getProperty('id')
log.info('Server Id selected: %s', selected_id)
if self._connect_server(selected_id):
self.message_box.setVisibleCondition('False')
self.close()
def onClick(self, control):
if control == EMBY_CONNECT:
self.connect_manager.clearData()
self._connect_login = True
self.close()
elif control == MANUAL_SERVER:
self._manual_server = True
self.close()
elif control == CANCEL:
self.close()
def _connect_server(self, server_id):
server = self.connect_manager.getServerInfo(server_id)
self.message.setLabel("%s %s..." % (lang(30610), server['Name']))
self.message_box.setVisibleCondition('True')
self.busy.setVisibleCondition('True')
result = self.connect_manager.connectToServer(server)
if result['State'] == CONN_STATE['Unavailable']:
self.busy.setVisibleCondition('False')
self.message.setLabel(lang(30609))
return False
else:
xbmc.sleep(1000)
self._selected_server = result['Servers'][0]
return True

View file

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import os
import xbmcgui
import xbmcaddon
import connect.connectionmanager as connectionmanager
from utils import language as lang
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
addon = xbmcaddon.Addon('plugin.video.emby')
CONN_STATE = connectionmanager.ConnectionState
ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92
CONNECT = 200
CANCEL = 201
ERROR_TOGGLE = 202
ERROR_MSG = 203
ERROR = {
'Invalid': 1,
'Empty': 2
}
##################################################################################################
class ServerManual(xbmcgui.WindowXMLDialog):
_server = None
error = None
def __init__(self, *args, **kwargs):
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def set_connect_manager(self, connect_manager):
self.connect_manager = connect_manager
def is_connected(self):
return True if self._server else False
def get_server(self):
return self._server
def onInit(self):
self.connect_button = self.getControl(CONNECT)
self.cancel_button = self.getControl(CANCEL)
self.error_toggle = self.getControl(ERROR_TOGGLE)
self.error_msg = self.getControl(ERROR_MSG)
self.host_field = self._add_editcontrol(725, 400, 40, 500)
self.port_field = self._add_editcontrol(725, 525, 40, 500)
self.port_field.setText('8096')
self.setFocus(self.host_field)
self.host_field.controlUp(self.cancel_button)
self.host_field.controlDown(self.port_field)
self.port_field.controlUp(self.host_field)
self.port_field.controlDown(self.connect_button)
self.connect_button.controlUp(self.port_field)
self.cancel_button.controlDown(self.host_field)
def onClick(self, control):
if control == CONNECT:
# Sign in to emby connect
self._disable_error()
server = self.host_field.getText()
port = self.port_field.getText()
if not server or not port:
# Display error
self._error(ERROR['Empty'], lang(30617))
log.error("Server or port cannot be null")
elif self._connect_to_server(server, port):
self.close()
elif control == CANCEL:
# Remind me later
self.close()
def onAction(self, action):
if self.error == ERROR['Empty'] and self.host_field.getText() and self.port_field.getText():
self._disable_error()
if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU):
self.close()
def _add_editcontrol(self, x, y, height, width):
media = os.path.join(addon.getAddonInfo('path'), 'resources', 'skins', 'default', 'media')
control = xbmcgui.ControlEdit(0, 0, 0, 0,
label="User",
font="font10",
textColor="ffc2c2c2",
focusTexture=os.path.join(media, "button-focus.png"),
noFocusTexture=os.path.join(media, "button-focus.png"))
control.setPosition(x, y)
control.setHeight(height)
control.setWidth(width)
self.addControl(control)
return control
def _connect_to_server(self, server, port):
server_address = "%s:%s" % (server, port)
self._message("%s %s..." % (lang(30610), server_address))
result = self.connect_manager.connectToAddress(server_address)
if result['State'] == CONN_STATE['Unavailable']:
self._message(lang(30609))
return False
else:
self._server = result['Servers'][0]
return True
def _message(self, message):
self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('True')
def _error(self, state, message):
self.error = state
self.error_msg.setLabel(message)
self.error_toggle.setVisibleCondition('True')
def _disable_error(self):
self.error = None
self.error_toggle.setVisibleCondition('False')

View file

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import xbmc
import xbmcgui
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
ACTION_PARENT_DIR = 9
ACTION_PREVIOUS_MENU = 10
ACTION_BACK = 92
ACTION_SELECT_ITEM = 7
ACTION_MOUSE_LEFT_CLICK = 100
LIST = 155
MANUAL = 200
CANCEL = 201
##################################################################################################
class UsersConnect(xbmcgui.WindowXMLDialog):
_user = None
_manual_login = False
def __init__(self, *args, **kwargs):
self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
def set_server(self, server):
self.server = server
def set_users(self, users):
self.users = users
def is_user_selected(self):
return True if self._user else False
def get_user(self):
return self._user
def is_manual_login(self):
return self._manual_login
def onInit(self):
self.list_ = self.getControl(LIST)
for user in self.users:
user_image = ("userflyoutdefault2.png" if 'PrimaryImageTag' not in user
else self._get_user_artwork(user['Id'], 'Primary'))
self.list_.addItem(self._add_listitem(user['Name'], user['Id'], user_image))
self.setFocus(self.list_)
def _add_listitem(self, label, user_id, user_image):
item = xbmcgui.ListItem(label)
item.setProperty('id', user_id)
if self.kodi_version > 15:
item.setArt({'Icon': user_image})
else:
item.setIconImage(user_image)
return item
def onAction(self, action):
if action in (ACTION_BACK, ACTION_PREVIOUS_MENU, ACTION_PARENT_DIR):
self.close()
if action in (ACTION_SELECT_ITEM, ACTION_MOUSE_LEFT_CLICK):
if self.getFocusId() == LIST:
user = self.list_.getSelectedItem()
selected_id = user.getProperty('id')
log.info('User Id selected: %s', selected_id)
for user in self.users:
if user['Id'] == selected_id:
self._user = user
break
self.close()
def onClick(self, control):
if control == MANUAL:
self._manual_login = True
self.close()
elif control == CANCEL:
self.close()
def _get_user_artwork(self, user_id, item_type):
# Load user information set by UserClient
return "%s/emby/Users/%s/Images/%s?Format=original" % (self.server, user_id, item_type)

View file

@ -3,14 +3,14 @@
##################################################################################################
import json
import requests
import logging
import requests
import xbmc
import xbmcgui
import clientinfo
from utils import window, settings
import connect.connectionmanager as connectionmanager
from utils import window, settings, language as lang
##################################################################################################
@ -24,52 +24,71 @@ log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class DownloadUtils():
class DownloadUtils(object):
# Borg - multiple instances, shared state
_shared_state = {}
clientInfo = clientinfo.ClientInfo()
# Requests session
s = None
session = {}
session_requests = None
servers = {} # Multi server setup
default_timeout = 30
def __init__(self):
self.__dict__ = self._shared_state
self.client_info = clientinfo.ClientInfo()
def setUsername(self, username):
def set_session(self, **kwargs):
# Reserved for userclient only
self.username = username
log.debug("Set username: %s" % username)
info = {}
for key in kwargs:
info[key] = kwargs[key]
def setUserId(self, userId):
self.session.update(info)
window('emby_server.json', value=self.session)
log.debug("Set info for server %s: %s", self.session['ServerId'], self.session)
def add_server(self, server, ssl):
# Reserved for userclient only
self.userId = userId
log.debug("Set userId: %s" % userId)
server_id = server['Id']
info = {
'UserId': server['UserId'],
'Server': connectionmanager.getServerAddress(server, server['LastConnectionMode']),
'Token': server['AccessToken'],
'SSL': ssl
}
for server_info in self.servers:
if server_info == server_id:
server_info.update(info)
# Set window prop
self._set_server_properties(server_id, server['Name'], info)
log.info("updating %s to available servers: %s", server_id, self.servers)
break
else:
self.servers[server_id] = info
self._set_server_properties(server_id, server['Name'], json.dumps(info))
log.info("adding %s to available servers: %s", server_id, self.servers)
def setServer(self, server):
def reset_server(self, server_id):
# Reserved for userclient only
self.server = server
log.debug("Set server: %s" % server)
for server in self.servers:
if server['ServerId'] == server_id:
self.servers.pop(server)
window('emby_server%s.json' % server_id, clear=True)
window('emby_server%s.name' % server_id, clear=True)
log.info("removing %s from available servers", server_id)
def setToken(self, token):
# Reserved for userclient only
self.token = token
log.debug("Set token: %s" % token)
def setSSL(self, ssl, sslclient):
# Reserved for userclient only
self.sslverify = ssl
self.sslclient = sslclient
log.debug("Verify SSL host certificate: %s" % ssl)
log.debug("SSL client side certificate: %s" % sslclient)
def postCapabilities(self, deviceId):
@staticmethod
def _set_server_properties(server_id, name, info):
window('emby_server%s.json' % server_id, value=info)
window('emby_server%s.name' % server_id, value=name)
def post_capabilities(self, device_id):
# Post settings to session
url = "{server}/emby/Sessions/Capabilities/Full?format=json"
data = {
@ -90,171 +109,126 @@ class DownloadUtils():
)
}
log.debug("Capabilities URL: %s" % url)
log.debug("Postdata: %s" % data)
self.downloadUrl(url, postBody=data, action_type="POST")
log.debug("Posted capabilities to %s" % self.server)
log.debug("Posted capabilities to %s", self.session['Server'])
# Attempt at getting sessionId
url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId
url = "{server}/emby/Sessions?DeviceId=%s&format=json" % device_id
result = self.downloadUrl(url)
try:
sessionId = result[0]['Id']
session_id = result[0]['Id']
except (KeyError, TypeError):
log.info("Failed to retrieve sessionId.")
log.error("Failed to retrieve the session id.")
else:
log.debug("Session: %s" % result)
log.info("SessionId: %s" % sessionId)
window('emby_sessionId', value=sessionId)
log.info("SessionId: %s", session_id)
window('emby_sessionId', value=session_id)
# Post any permanent additional users
additionalUsers = settings('additionalUsers')
if additionalUsers:
additional_users = settings('additionalUsers')
if additional_users:
additionalUsers = additionalUsers.split(',')
log.info("List of permanent users added to the session: %s" % additionalUsers)
additional_users = additional_users.split(',')
log.info("List of permanent users added to the session: %s", additional_users)
# Get the user list from server to get the userId
url = "{server}/emby/Users?format=json"
result = self.downloadUrl(url)
for additional in additionalUsers:
addUser = additional.decode('utf-8').lower()
for additional in additional_users:
add_user = additional.decode('utf-8').lower()
# Compare to server users to list of permanent additional users
for user in result:
username = user['Name'].lower()
if username in addUser:
userId = user['Id']
url = (
"{server}/emby/Sessions/%s/Users/%s?format=json"
% (sessionId, userId)
)
if username in add_user:
user_id = user['Id']
url = ("{server}/emby/Sessions/%s/Users/%s?format=json"
% (session_id, user_id))
self.downloadUrl(url, postBody={}, action_type="POST")
def startSession(self):
self.deviceId = self.clientInfo.getDeviceId()
def start_session(self):
# User is identified from this point
# Attach authenticated header to the session
verify = False
header = self.getHeader()
# If user enabled host certificate verification
try:
verify = self.sslverify
if self.sslclient is not None:
verify = self.sslclient
except:
log.info("Could not load SSL settings.")
# Start session
self.s = requests.Session()
self.s.headers = header
self.s.verify = verify
session = requests.Session()
session.headers = self.get_header()
session.verify = self.session['SSL']
# Retry connections to the server
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
session.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
session.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
self.session_requests = session
log.info("Requests session started on: %s" % self.server)
log.info("requests session started on: %s", self.session['Server'])
def stopSession(self):
def stop_session(self):
try:
self.s.close()
except Exception:
log.warn("Requests session could not be terminated.")
self.session_requests.close()
except Exception as error:
log.error(error)
log.warn("requests session could not be terminated")
def getHeader(self, authenticate=True):
def get_header(self, server_id=None, authenticate=True):
deviceName = self.clientInfo.getDeviceName()
deviceName = deviceName.encode('utf-8')
deviceId = self.clientInfo.getDeviceId()
version = self.clientInfo.getVersion()
device_name = self.client_info.get_device_name().encode('utf-8')
device_id = self.client_info.get_device_id()
version = self.client_info.get_version()
if authenticate:
user = self._get_session_info(server_id)
user_id = user['UserId']
token = user['Token']
auth = (
'MediaBrowser UserId="%s", Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
% (self.userId, deviceName, deviceId, version))
% (user_id, device_name, device_id, version)
)
header = {
'Content-type': 'application/json',
'Accept-encoding': 'gzip',
'Accept-Charset': 'UTF-8,*',
'Authorization': auth,
'X-MediaBrowser-Token': self.token
'X-MediaBrowser-Token': token
}
else:
# If user is not authenticated
auth = (
'MediaBrowser Client="Kodi", Device="%s", DeviceId="%s", Version="%s"'
% (deviceName, deviceId, version))
header = {
'Content-type': 'application/json',
'Accept-encoding': 'gzip',
'Accept-Charset': 'UTF-8,*',
'Authorization': auth
}
% (device_name, device_id, version)
)
header = {'Authorization': auth}
header.update({
'Content-type': 'application/json',
'Accept-encoding': 'gzip',
'Accept-Charset': 'UTF-8,*',
})
return header
def downloadUrl(self, url, postBody=None, action_type="GET", parameters=None,
authenticate=True):
authenticate=True, server_id=None):
log.debug("===== ENTER downloadUrl =====")
session = requests
kwargs = {}
default_link = ""
try:
if authenticate:
if self.s is not None:
session = self.s
else:
# request session does not exists
# Get user information
self.userId = window('emby_currUser')
self.server = window('emby_server%s' % self.userId)
self.token = window('emby_accessToken%s' % self.userId)
verifyssl = False
# IF user enables ssl verification
if settings('sslverify') == "true":
verifyssl = True
if settings('sslcert') != "None":
verifyssl = settings('sslcert')
kwargs.update({
'verify': verifyssl,
'headers': self.getHeader()
})
# Replace for the real values
url = url.replace("{server}", self.server)
url = url.replace("{UserId}", self.userId)
else: # User is not authenticated
# If user enables ssl verification
try:
verifyssl = self.sslverify
if self.sslclient is not None:
verifyssl = self.sslclient
except AttributeError:
verifyssl = False
# Ensure server info is loaded
self._ensure_server(server_id)
server = self.session if server_id is None else self.servers[server_id]
if server_id is None and self.session_requests is not None: # Main server
session = self.session_requests
else:
session = requests
kwargs.update({
'verify': verifyssl,
'headers': self.getHeader(authenticate=False)
'verify': server['SSL'],
'headers': self.get_header(server_id, authenticate)
})
# Replace for the real values
url = url.replace("{server}", server['Server'])
url = url.replace("{UserId}", server['UserId'])
##### PREPARE REQUEST #####
kwargs.update({
'url': url,
@ -265,95 +239,128 @@ class DownloadUtils():
##### THE RESPONSE #####
log.debug(kwargs)
r = self._requests(action_type, session, **kwargs)
response = self._requests(action_type, session, **kwargs)
#response = requests.get('http://httpbin.org/status/400')
if r.status_code == 204:
if response.status_code == 204:
# No body in the response
log.debug("====== 204 Success ======")
# Read response to release connection
r.content
response.content
elif r.status_code == requests.codes.ok:
elif response.status_code == requests.codes.ok:
try:
# UNICODE - JSON object
r = r.json()
response = response.json()
log.debug("====== 200 Success ======")
log.debug("Response: %s" % r)
return r
log.debug("Response: %s", response)
return response
except:
if r.headers.get('content-type') != "text/html":
log.info("Unable to convert the response for: %s" % url)
except Exception:
if response.headers.get('content-type') != "text/html":
log.info("Unable to convert the response for: %s", url)
else: # Bad status code
log.error("=== Bad status response: %s ===" % r.status_code)
r.raise_for_status()
log.error("=== Bad status response: %s ===", response.status_code)
response.raise_for_status()
##### EXCEPTIONS #####
except requests.exceptions.ConnectionError as e:
except requests.exceptions.SSLError as error:
log.error("invalid SSL certificate for: %s", url)
except requests.exceptions.ConnectTimeout as error:
log.error("Server timeout at: %s", url)
except requests.exceptions.ConnectionError as error:
# Make the addon aware of status
if window('emby_online') != "false":
log.warn("Server unreachable at: %s" % url)
log.error("Server unreachable at: %s", url)
window('emby_online', value="false")
except requests.exceptions.ConnectTimeout as e:
log.warn("Server timeout at: %s" % url)
except requests.exceptions.HTTPError as error:
except requests.exceptions.HTTPError as e:
if response.status_code == 400:
log.error("Malformed request: %s", error)
raise Warning('400')
if r.status_code == 401:
if response.status_code == 401:
# Unauthorized
status = window('emby_serverStatus')
if 'X-Application-Error-Code' in r.headers:
if 'X-Application-Error-Code' in response.headers:
# Emby server errors
if r.headers['X-Application-Error-Code'] == "ParentalControl":
if response.headers['X-Application-Error-Code'] == "ParentalControl":
# Parental control - access restricted
if status != "restricted":
xbmcgui.Dialog().notification(heading=lang(29999),
message="Access restricted.",
icon=xbmcgui.NOTIFICATION_ERROR,
time=5000)
window('emby_serverStatus', value="restricted")
xbmcgui.Dialog().notification(
heading="Emby server",
message="Access restricted.",
icon=xbmcgui.NOTIFICATION_ERROR,
time=5000)
return False
raise Warning('restricted')
elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException":
elif (response.headers['X-Application-Error-Code'] ==
"UnauthorizedAccessException"):
# User tried to do something his emby account doesn't allow
pass
elif status not in ("401", "Auth"):
# Tell userclient token has been revoked.
window('emby_serverStatus', value="401")
log.warn("HTTP Error: %s" % e)
xbmcgui.Dialog().notification(
heading="Error connecting",
message="Unauthorized.",
icon=xbmcgui.NOTIFICATION_ERROR)
return 401
log.error("HTTP Error: %s", error)
xbmcgui.Dialog().notification(heading="Error connecting",
message="Unauthorized.",
icon=xbmcgui.NOTIFICATION_ERROR)
raise Warning('401')
elif r.status_code in (301, 302):
# Redirects
pass
elif r.status_code == 400:
# Bad requests
pass
except requests.exceptions.SSLError as e:
log.warn("Invalid SSL certificate for: %s" % url)
except requests.exceptions.RequestException as e:
log.warn("Unknown error connecting to: %s" % url)
except requests.exceptions.RequestException as error:
log.error("unknown error connecting to: %s", url)
return default_link
def _requests(self, action, session=requests, **kwargs):
def _ensure_server(self, server_id=None):
if server_id is None and self.session_requests is None:
if not self.session:
server = self._get_session_info()
self.session = server
elif server_id and server_id not in self.servers:
if server_id not in self.servers:
server = self._get_session_info(server_id)
self.servers[server_id] = server
return True
@classmethod
def _get_session_info(cls, server_id=None):
info = {
'UserId': "",
'Server': "",
'Token': "",
'SSL': False
}
if server_id is None: # Main server
server = window('emby_server.json')
else: # Other connect servers
server = window('emby_server%s.json' % server_id)
if server:
info.update(server)
return info
@classmethod
def _requests(cls, action, session, **kwargs):
if action == "GET":
r = session.get(**kwargs)
response = session.get(**kwargs)
elif action == "POST":
r = session.post(**kwargs)
response = session.post(**kwargs)
elif action == "DELETE":
r = session.delete(**kwargs)
response = session.delete(**kwargs)
return r
return response

View file

@ -20,6 +20,22 @@ class Embydb_Functions():
self.embycursor = embycursor
def get_version(self, version=None):
if version is not None:
self.embycursor.execute("DELETE FROM version")
query = "INSERT INTO version(idVersion) VALUES (?)"
self.embycursor.execute(query, (version,))
else:
query = "SELECT idVersion FROM version"
self.embycursor.execute(query)
try:
version = self.embycursor.fetchone()[0]
except TypeError:
pass
return version
def getViews(self):
views = []
@ -155,11 +171,22 @@ class Embydb_Functions():
self.embycursor.execute(query, (mediafolderid,))
return self.embycursor.fetchall()
def get_item_by_view(self, view_id):
query = ' '.join((
"SELECT emby_id",
"FROM emby",
"WHERE media_folder = ?"
))
self.embycursor.execute(query, (view_id,))
return self.embycursor.fetchall()
def getItem_byKodiId(self, kodiid, mediatype):
query = ' '.join((
"SELECT emby_id, parent_id",
"SELECT emby_id, parent_id, media_folder",
"FROM emby",
"WHERE kodi_id = ?",
"AND media_type = ?"
@ -191,7 +218,7 @@ class Embydb_Functions():
self.embycursor.execute(query, (parentid, mediatype,))
return self.embycursor.fetchall()
def getChecksum(self, mediatype):
def get_checksum(self, mediatype):
query = ' '.join((
@ -202,6 +229,18 @@ class Embydb_Functions():
self.embycursor.execute(query, (mediatype,))
return self.embycursor.fetchall()
def get_checksum_by_view(self, media_type, view_id):
query = ' '.join((
"SELECT emby_id, checksum",
"FROM emby",
"WHERE emby_type = ?",
"AND media_folder = ?"
))
self.embycursor.execute(query, (media_type, view_id,))
return self.embycursor.fetchall()
def getMediaType_byId(self, embyid):
query = ' '.join((

View file

@ -5,6 +5,7 @@
import json
import logging
import os
import shutil
import sys
import urlparse
@ -17,6 +18,7 @@ import xbmcplugin
import artwork
import utils
import clientinfo
import connectmanager
import downloadutils
import librarysync
import read_embyserver as embyserver
@ -25,7 +27,7 @@ import playlist
import playbackutils as pbutils
import playutils
import api
from utils import window, settings, language as lang
from utils import window, settings, dialog, language as lang
#################################################################################################
@ -76,23 +78,15 @@ def doMainListing():
''' because we do not use seperate entrypoints for each content type,
we need to figure out which items to show in each listing.
for now we just only show picture nodes in the picture library
video nodes in the video library and all nodes in any other window '''
'''if path and xbmc.getCondVisibility("Window.IsActive(Pictures)") and node == "photos":
addDirectoryItem(label, path)
elif path and xbmc.getCondVisibility("Window.IsActive(VideoLibrary)")
and node != "photos":
addDirectoryItem(label, path)
elif path and not xbmc.getCondVisibility("Window.IsActive(VideoLibrary) |
Window.IsActive(Pictures) | Window.IsActive(MusicLibrary)"):
addDirectoryItem(label, path)'''
video nodes in the video library and all nodes in any other window
'''
if path:
if xbmc.getCondVisibility("Window.IsActive(Pictures)") and node == "photos":
addDirectoryItem(label, path)
elif xbmc.getCondVisibility("Window.IsActive(VideoLibrary)") and node != "photos":
elif xbmc.getCondVisibility("Window.IsActive(Videos)") and node != "photos":
addDirectoryItem(label, path)
elif not xbmc.getCondVisibility("Window.IsActive(VideoLibrary) | Window.IsActive(Pictures) | Window.IsActive(MusicLibrary)"):
elif not xbmc.getCondVisibility("Window.IsActive(Videos) | Window.IsActive(Pictures) | Window.IsActive(Music)"):
addDirectoryItem(label, path)
# experimental live tv nodes
@ -102,7 +96,14 @@ def doMainListing():
addDirectoryItem(lang(33052),
"plugin://plugin.video.emby/?mode=browsecontent&type=recordings&folderid=root")
# some extra entries for settings and stuff. TODO --> localize the labels
'''
TODO: Create plugin listing for servers
servers = window('emby_servers.json')
if servers:
for server in servers:
log.info(window('emby_server%s.name' % server))
addDirectoryItem(window('emby_server%s.name' % server), "plugin://plugin.video.emby/?mode=%s" % server)'''
addDirectoryItem(lang(30517), "plugin://plugin.video.emby/?mode=passwords")
addDirectoryItem(lang(33053), "plugin://plugin.video.emby/?mode=settings")
addDirectoryItem(lang(33054), "plugin://plugin.video.emby/?mode=adduser")
@ -112,9 +113,82 @@ def doMainListing():
addDirectoryItem(lang(33058), "plugin://plugin.video.emby/?mode=reset")
addDirectoryItem(lang(33059), "plugin://plugin.video.emby/?mode=texturecache")
addDirectoryItem(lang(33060), "plugin://plugin.video.emby/?mode=thememedia")
if settings('backupPath'):
addDirectoryItem(lang(33092), "plugin://plugin.video.emby/?mode=backup")
xbmcplugin.endOfDirectory(int(sys.argv[1]))
def emby_connect():
# Login user to emby connect
connect = connectmanager.ConnectManager()
try:
connectUser = connect.login_connect()
except RuntimeError:
return
else:
user = connectUser['User']
token = connectUser['AccessToken']
username = user['Name']
dialog(type_="notification",
heading="{emby}",
message="%s %s" % (lang(33000), username.decode('utf-8')),
icon=user.get('ImageUrl') or "{emby}",
time=2000,
sound=False)
settings('connectUsername', value=username)
def emby_backup():
# Create a backup at specified location
path = settings('backupPath')
# filename
default_value = "Kodi%s.%s" % (xbmc.getInfoLabel('System.BuildVersion')[:2],
xbmc.getInfoLabel('System.Date(dd-mm-yy)'))
filename = dialog(type_="input",
heading=lang(33089),
defaultt=default_value)
if not filename:
return
backup = os.path.join(path, filename)
log.info("Backup: %s", backup)
# Create directory
if xbmcvfs.exists(backup+"\\"):
log.info("Existing directory!")
if not dialog(type_="yesno",
heading="{emby}",
line1=lang(33090)):
return emby_backup()
shutil.rmtree(backup)
# Addon_data
shutil.copytree(src=xbmc.translatePath(
"special://profile/addon_data/plugin.video.emby").decode('utf-8'),
dst=os.path.join(backup, "addon_data", "plugin.video.emby"))
# Database files
database = os.path.join(backup, "Database")
xbmcvfs.mkdir(database)
# Emby database
shutil.copy(src=xbmc.translatePath("special://database/emby.db").decode('utf-8'),
dst=database)
# Videos database
shutil.copy(src=utils.getKodiVideoDBPath(),
dst=database)
# Music database
if settings('enableMusic') == "true":
shutil.copy(src=utils.getKodiMusicDBPath(),
dst=database)
dialog(type_="ok",
heading="{emby}",
line1="%s: %s" % (lang(33091), backup))
##### Generate a new deviceId
def resetDeviceId():
@ -123,7 +197,7 @@ def resetDeviceId():
deviceId_old = window('emby_deviceId')
try:
window('emby_deviceId', clear=True)
deviceId = clientinfo.ClientInfo().getDeviceId(reset=True)
deviceId = clientinfo.ClientInfo().get_device_id(reset=True)
except Exception as e:
log.error("Failed to generate a new device Id: %s" % e)
dialog.ok(
@ -185,11 +259,15 @@ def deleteItem():
##### ADD ADDITIONAL USERS #####
def addUser():
if window('emby_online') != "true":
log.info("server is offline")
return
doUtils = downloadutils.DownloadUtils()
art = artwork.Artwork()
clientInfo = clientinfo.ClientInfo()
deviceId = clientInfo.getDeviceId()
deviceName = clientInfo.getDeviceName()
deviceId = clientInfo.get_device_id()
deviceName = clientInfo.get_device_name()
userid = window('emby_currUser')
dialog = xbmcgui.Dialog()
@ -290,16 +368,21 @@ def addUser():
break
window('EmbyAdditionalUserImage.%s' % i, clear=True)
url = "{server}/emby/Sessions?DeviceId=%s" % deviceId
url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId
result = doUtils.downloadUrl(url)
additionalUsers = result[0]['AdditionalUsers']
try:
additionalUsers = result[0]['AdditionalUsers']
except (KeyError, TypeError) as error:
log.error(error)
additionaluser = []
count = 0
for additionaluser in additionalUsers:
userid = additionaluser['UserId']
url = "{server}/emby/Users/%s?format=json" % userid
result = doUtils.downloadUrl(url)
window('EmbyAdditionalUserImage.%s' % count,
value=art.getUserArtwork(result['Id'], 'Primary'))
value=art.get_user_artwork(result['Id'], 'Primary'))
window('EmbyAdditionalUserPosition.%s' % userid, value=str(count))
count +=1
@ -326,9 +409,7 @@ def getThemeMedia():
xbmcvfs.mkdir(library)
# Set custom path for user
tvtunes_path = xbmc.translatePath(
"special://profile/addon_data/script.tvtunes/").decode('utf-8')
if xbmcvfs.exists(tvtunes_path):
if xbmc.getCondVisibility('System.HasAddon(script.tvtunes)'):
tvtunes = xbmcaddon.Addon(id="script.tvtunes")
tvtunes.setSetting('custom_path_enable', "true")
tvtunes.setSetting('custom_path', library)
@ -461,6 +542,10 @@ def getThemeMedia():
##### REFRESH EMBY PLAYLISTS #####
def refreshPlaylist():
if window('emby_online') != "true":
log.info("server is offline")
return
lib = librarysync.LibrarySync()
dialog = xbmcgui.Dialog()
try:
@ -478,7 +563,7 @@ def refreshPlaylist():
sound=False)
except Exception as e:
log.error("Refresh playlists/nodes failed: %s" % e)
log.exception("Refresh playlists/nodes failed: %s" % e)
dialog.notification(
heading=lang(29999),
message=lang(33070),
@ -544,6 +629,9 @@ def BrowseContent(viewname, browse_type="", folderid=""):
listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="Random", recursive=True, limit=150, sortorder="Descending")
elif filter_type == "recommended":
listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[0], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite")
elif folderid == "favepisodes":
xbmcplugin.setContent(int(sys.argv[1]), 'episodes')
listing = emby.getFilteredSection(None, itemtype="Episode", sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite")
elif filter_type == "sets":
listing = emby.getFilteredSection(folderid, itemtype=itemtype.split(",")[1], sortby="SortName", recursive=True, limit=25, sortorder="Ascending", filter_type="IsFavorite")
else:
@ -588,7 +676,7 @@ def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.
li.setProperty("embyid",itemid)
allart = art.getAllArtwork(item)
allart = art.get_all_artwork(item)
if item["Type"] == "Photo":
#listitem setup for pictures...
@ -601,16 +689,16 @@ def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.
li.setArt( {"fanart": img_path}) #add image as fanart for use with skinhelper auto thumb/backgrund creation
li.setInfo('pictures', infoLabels={ "picturepath": img_path, "date": premieredate, "size": picture.get("Size"), "exif:width": str(picture.get("Width")), "exif:height": str(picture.get("Height")), "title": title})
li.setThumbnailImage(img_path)
li.setProperty("plot",API.getOverview())
li.setProperty("plot",API.get_overview())
li.setIconImage('DefaultPicture.png')
else:
#normal video items
li.setProperty('IsPlayable', 'true')
path = "%s?id=%s&mode=play" % (sys.argv[0], item.get("Id"))
li.setProperty("path",path)
genre = API.getGenres()
genre = API.get_genres()
overlay = 0
userdata = API.getUserData()
userdata = API.get_userdata()
runtime = item.get("RunTimeTicks",0)/ 10000000.0
seektime = userdata['Resume']
if seektime:
@ -625,7 +713,7 @@ def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.
playcount = 0
rating = item.get('CommunityRating')
if not rating: rating = userdata['UserRating']
if not rating: rating = 0
# Populate the extradata list and artwork
extradata = {
@ -635,7 +723,7 @@ def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.
'genre': genre,
'playcount': str(playcount),
'title': title,
'plot': API.getOverview(),
'plot': API.get_overview(),
'Overlay': str(overlay),
'duration': runtime
}
@ -652,7 +740,7 @@ def createListItemFromEmbyItem(item,art=artwork.Artwork(),doUtils=downloadutils.
else:
pbutils.PlaybackUtils(item).setArtwork(li)
mediastreams = API.getMediaStreams()
mediastreams = API.get_media_streams()
videostreamFound = False
if mediastreams:
for key, value in mediastreams.iteritems():
@ -722,6 +810,12 @@ def createListItem(item):
'Playcount': item['playcount']
}
if "episodeid" in item:
# Listitem of episode
metadata['mediatype'] = "episode"
metadata['dbid'] = item['episodeid']
# TODO: Review once Krypton is RC - probably no longer needed if there's dbid
if "episode" in item:
episode = item['episode']
metadata['Episode'] = episode
@ -769,7 +863,7 @@ def createListItem(item):
for key, value in item['streamdetails'].iteritems():
for stream in value:
li.addStreamInfo(key, stream)
return li
##### GET NEXTUP EPISODES FOR TAGNAME #####
@ -1064,7 +1158,7 @@ def getExtraFanArt(embyId,embyPath):
xbmcvfs.mkdirs(fanartDir)
item = emby.getItem(embyId)
if item:
backdrops = art.getAllArtwork(item)['Backdrop']
backdrops = art.get_all_artwork(item)['Backdrop']
tags = item['BackdropImageTags']
count = 0
for backdrop in backdrops:

164
resources/lib/ga_client.py Normal file
View file

@ -0,0 +1,164 @@
import sys
import os
import traceback
import requests
import logging
import clientinfo
import md5
import xbmc
import platform
import xbmcgui
from utils import window, settings, language as lang
log = logging.getLogger("EMBY."+__name__)
# for info on the metrics that can be sent to Google Analytics
# https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#events
class GoogleAnalytics():
testing = False
def __init__(self):
client_info = clientinfo.ClientInfo()
self.version = client_info.get_version()
self.device_id = client_info.get_device_id()
# user agent string, used for OS and Kodi version identification
kodi_ver = xbmc.getInfoLabel("System.BuildVersion")
if(not kodi_ver):
kodi_ver = "na"
kodi_ver = kodi_ver.strip()
if(kodi_ver.find(" ") > 0):
kodi_ver = kodi_ver[0:kodi_ver.find(" ")]
self.userAgent = "Kodi/" + kodi_ver + " (" + self.getUserAgentOS() + ")"
# Use set user name
self.user_name = settings('username') or settings('connectUsername') or 'None'
# use md5 for client and user for analytics
self.device_id = md5.new(self.device_id).hexdigest()
self.user_name = md5.new(self.user_name).hexdigest()
# resolution
self.screen_mode = xbmc.getInfoLabel("System.ScreenMode")
self.screen_height = xbmc.getInfoLabel("System.ScreenHeight")
self.screen_width = xbmc.getInfoLabel("System.ScreenWidth")
self.lang = xbmc.getInfoLabel("System.Language")
def getUserAgentOS(self):
if xbmc.getCondVisibility('system.platform.osx'):
return "Mac OS X"
elif xbmc.getCondVisibility('system.platform.ios'):
return "iOS"
elif xbmc.getCondVisibility('system.platform.windows'):
return "Windows NT"
elif xbmc.getCondVisibility('system.platform.android'):
return "Android"
elif xbmc.getCondVisibility('system.platform.linux.raspberrypi'):
return "Linux Rpi"
elif xbmc.getCondVisibility('system.platform.linux'):
return "Linux"
else:
return "Other"
def formatException(self):
exc_type, exc_obj, exc_tb = sys.exc_info()
latestStackFrame = None
allStackFrames = traceback.extract_tb(exc_tb)
if(len(allStackFrames) > 0):
latestStackFrame = allStackFrames[-1]
log.error(str(latestStackFrame))
errorType = "NA"
errorFile = "NA"
if(latestStackFrame != None):
fileName = os.path.split(latestStackFrame[0])[1]
codeLine = "NA"
if(len(latestStackFrame) > 3 and latestStackFrame[3] != None):
codeLine = latestStackFrame[3].strip()
errorFile = "%s:%s(%s)(%s)" % (fileName, latestStackFrame[1], exc_obj.message, codeLine)
errorFile = errorFile[0:499]
errorType = "%s" % (exc_type.__name__)
log.error(errorType + " - " + errorFile)
del(exc_type, exc_obj, exc_tb)
return errorType, errorFile
def getBaseData(self):
# all the data we can send to Google Analytics
data = {}
data['v'] = '1'
data['tid'] = 'UA-85356267-1' # tracking id, this is the account ID
data['ds'] = 'plugin' # data source
data['an'] = 'Kodi4Emby' # App Name
data['aid'] = '1' # App ID
data['av'] = self.version # App Version
#data['aiid'] = '1.1' # App installer ID
data['cid'] = self.device_id # Client ID
#data['uid'] = self.user_name # User ID
data['ua'] = self.userAgent # user agent string
# add width and height, only add if full screen
if(self.screen_mode.lower().find("window") == -1):
data['sr'] = str(self.screen_width) + "x" + str(self.screen_height)
data["ul"] = self.lang
return data
def sendScreenView(self, name):
data = self.getBaseData()
data['t'] = 'screenview' # action type
data['cd'] = name
self.sendData(data)
def sendEventData(self, eventCategory, eventAction, eventLabel=None):
data = self.getBaseData()
data['t'] = 'event' # action type
data['ec'] = eventCategory # Event Category
data['ea'] = eventAction # Event Action
if(eventLabel != None):
data['el'] = eventLabel # Event Label
self.sendData(data)
def sendData(self, data):
log.info("GA: " + str(data))
if(settings('metricLogging') == "false"):
return
if(self.testing):
url = "https://www.google-analytics.com/debug/collect" # test URL
else:
url = "https://www.google-analytics.com/collect" # prod URL
try:
r = requests.post(url, data)
except Exception as error:
log.error(error)
if(self.testing):
log.info("GA: " + r.text.encode('utf-8'))

View file

@ -3,8 +3,8 @@
#################################################################################################
import logging
import requests
import threading
import requests
#################################################################################################
@ -12,49 +12,49 @@ log = logging.getLogger("EMBY."+__name__)
#################################################################################################
class image_cache_thread(threading.Thread):
class ImageCacheThread(threading.Thread):
url_to_process = None
is_finished = False
urlToProcess = None
isFinished = False
xbmc_host = ""
xbmc_port = ""
xbmc_username = ""
xbmc_password = ""
def __init__(self):
threading.Thread.__init__(self)
def setUrl(self, url):
self.urlToProcess = url
def setHost(self, host, port):
def set_url(self, url):
self.url_to_process = url
def set_host(self, host, port):
self.xbmc_host = host
self.xbmc_port = port
def setAuth(self, user, pwd):
self.xbmc_username = user
self.xbmc_password = pwd
def set_auth(self, username, password):
self.xbmc_username = username
self.xbmc_password = password
def run(self):
log.debug("Image Caching Thread Processing: %s" % self.urlToProcess)
log.debug("Image Caching Thread Processing: %s", self.url_to_process)
try:
response = requests.head(
url=(
"http://%s:%s/image/image://%s"
% (self.xbmc_host, self.xbmc_port, self.urlToProcess)),
auth=(self.xbmc_username, self.xbmc_password),
timeout=(35.1, 35.1))
requests.head(
url=("http://%s:%s/image/image://%s"
% (self.xbmc_host, self.xbmc_port, self.url_to_process)),
auth=(self.xbmc_username, self.xbmc_password),
timeout=(35.1, 35.1))
# We don't need the result
except: pass
except Exception:
pass
log.debug("Image Caching Thread Exited")
self.isFinished = True
self.is_finished = True

View file

@ -2,174 +2,153 @@
#################################################################################################
import json
import logging
import socket
import xbmc
import xbmcgui
import xbmcaddon
import clientinfo
import downloadutils
import connectmanager
import connect.connectionmanager as connectionmanager
import userclient
from utils import settings, language as lang, passwordsXML
#################################################################################################
log = logging.getLogger("EMBY."+__name__)
STATE = connectionmanager.ConnectionState
#################################################################################################
class InitialSetup():
class InitialSetup(object):
def __init__(self):
self.addonId = clientinfo.ClientInfo().getAddonId()
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.userClient = userclient.UserClient()
self.addon_id = clientinfo.ClientInfo().get_addon_id()
self.user_client = userclient.UserClient()
self.connectmanager = connectmanager.ConnectManager()
def setup(self):
# Check server, user, direct paths, music, direct stream if not direct path.
addonId = self.addonId
dialog = xbmcgui.Dialog()
##### SERVER INFO #####
log.debug("Initial setup called.")
server = self.userClient.getServer()
log.debug("Initial setup called")
if server:
log.debug("Server is already set.")
return
log.debug("Looking for server...")
server = self.getServerDetails()
log.debug("Found: %s" % server)
try:
prefix, ip, port = server.replace("/", "").split(":")
except Exception: # Failed to retrieve server information
log.error("getServerDetails failed.")
xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId)
return
else:
server_confirm = dialog.yesno(
heading=lang(29999),
line1=lang(33034),
line2="%s %s" % (lang(30169), server))
if server_confirm:
# Correct server found
log.info("Server is selected. Saving the information.")
settings('ipaddress', value=ip)
settings('port', value=port)
if prefix == "https":
settings('https', value="true")
else:
# User selected no or cancelled the dialog
log.info("No server selected.")
xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId)
return
##### USER INFO #####
log.info("Getting user list.")
result = self.doUtils("%s/emby/Users/Public?format=json" % server, authenticate=False)
if result == "":
log.info("Unable to connect to %s" % server)
if self._server_verification() and settings('userId'):
# Setup is already completed
return
log.debug("Response: %s" % result)
# Process the list of users
usernames = []
users_hasPassword = []
for user in result:
# Username
name = user['Name']
usernames.append(name)
# Password
if user['HasPassword']:
name = "%s (secure)" % name
users_hasPassword.append(name)
log.info("Presenting user list: %s" % users_hasPassword)
user_select = dialog.select(lang(30200), users_hasPassword)
if user_select > -1:
selected_user = usernames[user_select]
log.info("Selected user: %s" % selected_user)
settings('username', value=selected_user)
else:
log.info("No user selected.")
xbmc.executebuiltin('Addon.OpenSettings(%s)' % addonId)
if not self._user_identification():
# User failed to identify
return
##### ADDITIONAL PROMPTS #####
directPaths = dialog.yesno(
heading=lang(30511),
line1=lang(33035),
nolabel=lang(33036),
yeslabel=lang(33037))
if directPaths:
log.info("User opted to use direct paths.")
direct_paths = dialog.yesno(heading=lang(30511),
line1=lang(33035),
nolabel=lang(33036),
yeslabel=lang(33037))
if direct_paths:
log.info("User opted to use direct paths")
settings('useDirectPaths', value="1")
# ask for credentials
credentials = dialog.yesno(
heading=lang(30517),
line1= lang(33038))
credentials = dialog.yesno(heading=lang(30517), line1=lang(33038))
if credentials:
log.info("Presenting network credentials dialog.")
log.info("Presenting network credentials dialog")
passwordsXML()
musicDisabled = dialog.yesno(
heading=lang(29999),
line1=lang(33039))
if musicDisabled:
log.info("User opted to disable Emby music library.")
music_disabled = dialog.yesno(heading=lang(29999), line1=lang(33039))
if music_disabled:
log.info("User opted to disable Emby music library")
settings('enableMusic', value="false")
else:
# Only prompt if the user didn't select direct paths for videos
if not directPaths:
musicAccess = dialog.yesno(
heading=lang(29999),
line1=lang(33040))
if musicAccess:
log.info("User opted to direct stream music.")
if not direct_paths:
music_access = dialog.yesno(heading=lang(29999), line1=lang(33040))
if music_access:
log.info("User opted to direct stream music")
settings('streamMusic', value="true")
def getServerDetails(self):
log.info("Getting Server Details from Network")
MULTI_GROUP = ("<broadcast>", 7359)
MESSAGE = "who is EmbyServer?"
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(6.0)
def _server_verification(self):
###$ Begin migration $###
if settings('server') == "":
self.user_client.get_server()
log.info("server migration completed")
self.user_client.get_userid()
self.user_client.get_token()
###$ End migration $###
current_server = self.user_client.get_server()
if current_server and not settings('serverId'):
server = self.connectmanager.get_server(current_server,
{'ssl': self.user_client.get_ssl()})
log.info("Detected: %s", server)
try:
server_id = server['Servers'][0]['Id']
settings('serverId', value=server_id)
except Exception as error:
log.error(error)
if current_server:
current_state = self.connectmanager.get_state()
try:
for server in current_state['Servers']:
if server['Id'] == settings('serverId'):
# Update token
server['UserId'] = settings('userId') or None
server['AccessToken'] = settings('token') or None
self.connectmanager.update_token(server)
server_address = self.connectmanager.get_address(server)
self._set_server(server_address, server)
log.info("Found server!")
except Exception as error:
log.error(error)
return True
return False
def _user_identification(self):
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1)
log.debug("MultiGroup : %s" % str(MULTI_GROUP))
log.debug("Sending UDP Data: %s" % MESSAGE)
sock.sendto(MESSAGE, MULTI_GROUP)
try:
data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes
log.info("Received Response: %s" % data)
except Exception:
log.error("No UDP Response")
return None
else:
# Get the address
data = json.loads(data)
return data['Address']
server = self.connectmanager.select_servers()
log.info("Server: %s", server)
server_address = self.connectmanager.get_address(server)
self._set_server(server_address, server)
if not server.get('AccessToken') and not server.get('UserId'):
user = self.connectmanager.login(server)
log.info("User authenticated: %s", user)
settings('username', value=user['User']['Name'])
self._set_user(user['User']['Id'], user['AccessToken'])
else: # Logged with Emby Connect
user = self.connectmanager.get_state()
settings('connectUsername', value=user['ConnectUser']['Name'])
self._set_user(server['UserId'], server['AccessToken'])
return True
except RuntimeError as error:
log.exception(error)
xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addon_id)
return False
@classmethod
def _set_server(cls, server_address, server):
settings('serverName', value=server['Name'])
settings('serverId', value=server['Id'])
settings('server', value=server_address)
@classmethod
def _set_user(cls, user_id, token):
settings('userId', value=user_id)
settings('token', value=token)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,6 @@ import logging
import xbmc
import xbmcgui
import clientinfo
import downloadutils
import embydb_functions as embydb
import playbackutils as pbutils
@ -26,194 +25,153 @@ class KodiMonitor(xbmc.Monitor):
def __init__(self):
self.clientInfo = clientinfo.ClientInfo()
self.addonName = self.clientInfo.getAddonName()
self.doUtils = downloadutils.DownloadUtils()
xbmc.Monitor.__init__(self)
log.info("Kodi monitor started.")
self.download = downloadutils.DownloadUtils().downloadUrl
log.info("Kodi monitor started")
def onScanStarted(self, library):
log.debug("Kodi library scan %s running." % library)
log.debug("Kodi library scan %s running", library)
if library == "video":
window('emby_kodiScan', value="true")
def onScanFinished(self, library):
log.debug("Kodi library scan %s finished." % library)
log.debug("Kodi library scan %s finished", library)
if library == "video":
window('emby_kodiScan', clear=True)
def onSettingsChanged(self):
# Monitor emby settings
# Review reset setting at a later time, need to be adjusted to account for initial setup
# changes.
'''currentPath = utils.settings('useDirectPaths')
if utils.window('emby_pluginpath') != currentPath:
# Plugin path value changed. Offer to reset
log("Changed to playback mode detected", 1)
utils.window('emby_pluginpath', value=currentPath)
resp = xbmcgui.Dialog().yesno(
heading="Playback mode change detected",
line1=(
"Detected the playback mode has changed. The database "
"needs to be recreated for the change to be applied. "
"Proceed?"))
if resp:
utils.reset()'''
currentLog = settings('logLevel')
if window('emby_logLevel') != currentLog:
current_log_level = settings('logLevel')
if window('emby_logLevel') != current_log_level:
# The log level changed, set new prop
log.info("New log level: %s" % currentLog)
window('emby_logLevel', value=currentLog)
log.info("New log level: %s", current_log_level)
window('emby_logLevel', value=current_log_level)
current_context = "true" if settings('enableContext') == "true" else ""
if window('emby_context') != current_context:
log.info("New context setting: %s", current_context)
window('emby_context', value=current_context)
def onNotification(self, sender, method, data):
doUtils = self.doUtils
if method not in ("Playlist.OnAdd"):
log.info("Method: %s Data: %s" % (method, data))
if method not in ('Playlist.OnAdd', 'Player.OnStop', 'Player.OnClear'):
log.info("Method: %s Data: %s", method, data)
if data:
data = json.loads(data,'utf-8')
data = json.loads(data, 'utf-8')
if method == 'Player.OnPlay':
self._on_play_(data)
if method == "Player.OnPlay":
# Set up report progress for emby playback
item = data.get('item')
try:
kodiid = item['id']
item_type = item['type']
except (KeyError, TypeError):
log.info("Item is invalid for playstate update.")
else:
if ((settings('useDirectPaths') == "1" and not item_type == "song") or
(item_type == "song" and settings('enableMusic') == "true")):
# Set up properties for player
embyconn = kodiSQL('emby')
embycursor = embyconn.cursor()
emby_db = embydb.Embydb_Functions(embycursor)
emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type)
try:
itemid = emby_dbitem[0]
except TypeError:
log.info("No kodiId returned.")
else:
url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid
result = doUtils.downloadUrl(url)
log.debug("Item: %s" % result)
elif method == 'VideoLibrary.OnUpdate':
self._video_update(data)
playurl = None
count = 0
while not playurl and count < 2:
try:
playurl = xbmc.Player().getPlayingFile()
except RuntimeError:
count += 1
xbmc.sleep(200)
else:
listItem = xbmcgui.ListItem()
playback = pbutils.PlaybackUtils(result)
if item_type == "song" and settings('streamMusic') == "true":
window('emby_%s.playmethod' % playurl, value="DirectStream")
else:
window('emby_%s.playmethod' % playurl, value="DirectPlay")
# Set properties for player.py
playback.setProperties(playurl, listItem)
finally:
embycursor.close()
elif method == "VideoLibrary.OnUpdate":
# Manually marking as watched/unwatched
playcount = data.get('playcount')
item = data.get('item')
try:
kodiid = item['id']
item_type = item['type']
except (KeyError, TypeError):
log.info("Item is invalid for playstate update.")
else:
# Send notification to the server.
embyconn = kodiSQL('emby')
embycursor = embyconn.cursor()
emby_db = embydb.Embydb_Functions(embycursor)
emby_dbitem = emby_db.getItem_byKodiId(kodiid, item_type)
try:
itemid = emby_dbitem[0]
except TypeError:
log.info("Could not find itemid in emby database.")
else:
# Stop from manually marking as watched unwatched, with actual playback.
if window('emby_skipWatched%s' % itemid) == "true":
# property is set in player.py
window('emby_skipWatched%s' % itemid, clear=True)
else:
# notify the server
url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % itemid
if playcount != 0:
doUtils.downloadUrl(url, action_type="POST")
log.info("Mark as watched for itemid: %s" % itemid)
else:
doUtils.downloadUrl(url, action_type="DELETE")
log.info("Mark as unwatched for itemid: %s" % itemid)
finally:
embycursor.close()
elif method == "VideoLibrary.OnRemove":
# Removed function, because with plugin paths + clean library, it will wipe
# entire library if user has permissions. Instead, use the emby context menu available
# in Isengard and higher version
pass
'''try:
kodiid = data['id']
type = data['type']
except (KeyError, TypeError):
log("Item is invalid for emby deletion.", 1)
else:
# Send the delete action to the server.
embyconn = utils.kodiSQL('emby')
embycursor = embyconn.cursor()
emby_db = embydb.Embydb_Functions(embycursor)
emby_dbitem = emby_db.getItem_byKodiId(kodiid, type)
try:
itemid = emby_dbitem[0]
except TypeError:
log("Could not find itemid in emby database.", 1)
else:
if utils.settings('skipContextMenu') != "true":
resp = xbmcgui.Dialog().yesno(
heading="Confirm delete",
line1="Delete file on Emby Server?")
if not resp:
log("User skipped deletion.", 1)
embycursor.close()
return
url = "{server}/emby/Items/%s?format=json" % itemid
log("Deleting request: %s" % itemid)
doUtils.downloadUrl(url, action_type="DELETE")
finally:
embycursor.close()'''
elif method == "System.OnSleep":
elif method == 'System.OnSleep':
# Connection is going to sleep
log.info("Marking the server as offline. System.OnSleep activated.")
window('emby_online', value="sleep")
elif method == "System.OnWake":
# Allow network to wake up
xbmc.sleep(10000)
window('emby_online', value="false")
elif method == 'System.OnWake':
self._system_wake()
elif method == 'GUI.OnScreensaverDeactivated':
self._screensaver_deactivated()
def _on_play_(self, data):
# Set up report progress for emby playback
try:
item = data['item']
kodi_id = item['id']
item_type = item['type']
except (KeyError, TypeError):
log.info("Item is invalid for playstate update")
else:
if ((settings('useDirectPaths') == "1" and not item_type == "song") or
(item_type == "song" and settings('enableMusic') == "true")):
# Set up properties for player
item_id = self._get_item_id(kodi_id, item_type)
if item_id:
url = "{server}/emby/Users/{UserId}/Items/%s?format=json" % item_id
result = self.download(url)
log.debug("Item: %s", result)
playurl = None
count = 0
while not playurl and count < 2:
try:
playurl = xbmc.Player().getPlayingFile()
except RuntimeError:
count += 1
xbmc.sleep(200)
else:
listitem = xbmcgui.ListItem()
playback = pbutils.PlaybackUtils(result)
if item_type == "song" and settings('streamMusic') == "true":
window('emby_%s.playmethod' % playurl, value="DirectStream")
else:
window('emby_%s.playmethod' % playurl, value="DirectPlay")
# Set properties for player.py
playback.setProperties(playurl, listitem)
def _video_update(self, data):
# Manually marking as watched/unwatched
try:
item = data['item']
kodi_id = item['id']
item_type = item['type']
except (KeyError, TypeError):
log.info("Item is invalid for playstate update")
else:
# Send notification to the server.
item_id = self._get_item_id(kodi_id, item_type)
if item_id:
# Stop from manually marking as watched unwatched, with actual playback.
if window('emby_skipWatched%s' % item_id) == "true":
# property is set in player.py
window('emby_skipWatched%s' % item_id, clear=True)
else:
# notify the server
url = "{server}/emby/Users/{UserId}/PlayedItems/%s?format=json" % item_id
if data.get('playcount') != 0:
self.download(url, action_type="POST")
log.info("Mark as watched for itemid: %s", item_id)
else:
self.download(url, action_type="DELETE")
log.info("Mark as unwatched for itemid: %s", item_id)
@classmethod
def _system_wake(cls):
# Allow network to wake up
xbmc.sleep(10000)
window('emby_online', value="false")
window('emby_onWake', value="true")
@classmethod
def _screensaver_deactivated(cls):
if settings('dbSyncScreensaver') == "true":
xbmc.sleep(5000)
window('emby_onWake', value="true")
@classmethod
def _get_item_id(cls, kodi_id, item_type):
elif method == "GUI.OnScreensaverDeactivated":
if settings('dbSyncScreensaver') == "true":
xbmc.sleep(5000);
window('emby_onWake', value="true")
item_id = None
conn = kodiSQL('emby')
cursor = conn.cursor()
emby_db = embydb.Embydb_Functions(cursor)
db_item = emby_db.getItem_byKodiId(kodi_id, item_type)
cursor.close()
elif method == "Playlist.OnClear":
pass
try:
item_id = db_item[0]
except TypeError:
log.info("Could not retrieve item Id")
return item_id

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@ from utils import window
def config():
logger = logging.getLogger('EMBY')
logger.addHandler(LogHandler())
logger.setLevel(logging.DEBUG)
@ -20,19 +20,20 @@ def config():
class LogHandler(logging.StreamHandler):
def __init__(self):
logging.StreamHandler.__init__(self)
self.setFormatter(MyFormatter())
def emit(self, record):
if self._getLogLevel(record.levelno):
if self._get_log_level(record.levelno):
try:
xbmc.log(self.format(record))
xbmc.log(self.format(record), level=xbmc.LOGNOTICE)
except UnicodeEncodeError:
xbmc.log(self.format(record).encode('utf-8'))
xbmc.log(self.format(record).encode('utf-8'), level=xbmc.LOGNOTICE)
def _getLogLevel(self, level):
@classmethod
def _get_log_level(cls, level):
levels = {
logging.ERROR: 0,
@ -41,17 +42,17 @@ class LogHandler(logging.StreamHandler):
logging.DEBUG: 2
}
try:
logLevel = int(window('emby_logLevel'))
log_level = int(window('emby_logLevel'))
except ValueError:
logLevel = 0
log_level = 0
return logLevel >= levels[level]
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):
@ -70,4 +71,4 @@ class MyFormatter(logging.Formatter):
# Restore the original format configured by the user
self._fmt = format_orig
return result
return result

View file

@ -74,7 +74,7 @@ def getAdditionalSongTags(embyid, emby_rating, API, kodicursor, emby_db, enablei
emby = embyserver.Read_EmbyServer()
previous_values = None
filename = API.getFilePath()
filename = API.get_file_path()
rating = 0
emby_rating = int(round(emby_rating, 0))

View file

@ -0,0 +1,5 @@
# Dummy file to make this directory a package.
from movies import Movies
from musicvideos import MusicVideos
from tvshows import TVShows
from music import Music

View file

@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import os
import xbmc
import xbmcvfs
import api
import artwork
import downloadutils
import read_embyserver as embyserver
from ga_client import GoogleAnalytics
from utils import window, settings, dialog, language as lang, should_stop
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
ga = GoogleAnalytics()
##################################################################################################
def catch_except(errors=(Exception, ), default_value=False):
# Will wrap method with try/except and print parameters for easier debugging
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except errors as error:
errStrings = ga.formatException()
ga.sendEventData("Exception", errStrings[0], errStrings[1])
log.exception(error)
log.error("function: %s \n args: %s \n kwargs: %s",
func.__name__, args, kwargs)
return default_value
return wrapper
return decorator
class Items(object):
pdialog = None
title = None
count = 0
total = 0
def __init__(self):
self.artwork = artwork.Artwork()
self.emby = embyserver.Read_EmbyServer()
self.do_url = downloadutils.DownloadUtils().downloadUrl
self.should_stop = should_stop
self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
self.direct_path = settings('useDirectPaths') == "1"
self.content_msg = settings('newContent') == "true"
@classmethod
def path_validation(cls, path):
# Verify if direct path is accessible or not
if not os.path.supports_unicode_filenames:
path = path.encode('utf-8')
if window('emby_pathverified') != "true" and not xbmcvfs.exists(path):
if dialog(type_="yesno",
heading="{emby}",
line1="%s %s. %s" % (lang(33047), path, lang(33048))):
window('emby_shouldStop', value="true")
return False
return True
def content_pop(self, name):
# It's possible for the time to be 0. It should be considered disabled in this case.
if not self.pdialog and self.content_msg and self.new_time:
dialog(type_="notification",
heading="{emby}",
message="%s %s" % (lang(33049), name),
icon="{emby}",
time=self.new_time,
sound=False)
def update_pdialog(self):
if self.pdialog:
percentage = int((float(self.count) / float(self.total))*100)
self.pdialog.update(percentage, message=self.title)
def add_all(self, item_type, items, view=None):
if self.should_stop():
return False
total = items['TotalRecordCount'] if 'TotalRecordCount' in items else len(items)
items = items['Items'] if 'Items' in items else items
if self.pdialog and view:
self.pdialog.update(heading="Processing %s / %s items" % (view['name'], total))
process = self._get_func(item_type, "added")
if view:
process(items, total, view)
else:
process(items, total)
def process_all(self, item_type, action, items, total=None, view=None):
log.debug("Processing %s: %s", action, items)
process = self._get_func(item_type, action)
self.total = total or len(items)
self.count = 0
for item in items:
if self.should_stop():
return False
if not process:
continue
self.title = item.get('Name', "unknown")
self.update_pdialog()
process(item)
self.count += 1
def remove_all(self, item_type, items):
log.debug("Processing removal: %s", items)
process = self._get_func(item_type, "remove")
for item in items:
process(item)
def added(self, items, total=None, update=True):
# Generator for newly added content
if update:
self.total = total or len(items)
self.count = 0
for item in items:
if self.should_stop():
break
self.title = item.get('Name', "unknown")
yield item
self.update_pdialog()
if update:
self.count += 1
def compare(self, item_type, items, compare_to, view=None):
view_name = view['name'] if view else item_type
update_list = self._compare_checksum(items, compare_to)
log.info("Update for %s: %s", view_name, update_list)
if self.should_stop():
return False
emby_items = self.emby.getFullItems(update_list)
total = len(update_list)
if self.pdialog:
self.pdialog.update(heading="Processing %s / %s items" % (view_name, total))
# Process additions and updates
if emby_items:
self.process_all(item_type, "update", emby_items, total, view)
# Process deletes
if compare_to:
self.remove_all(item_type, compare_to.items())
return True
def _compare_checksum(self, items, compare_to):
update_list = list()
for item in items:
if self.should_stop():
break
item_id = item['Id']
if compare_to.get(item_id) != api.API(item).get_checksum():
# Only update if item is not in Kodi or checksum is different
update_list.append(item_id)
compare_to.pop(item_id, None)
return update_list

View file

@ -0,0 +1,813 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import xbmc
import artwork
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class KodiItems(object):
def __init__(self):
self.artwork = artwork.Artwork()
self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
def create_entry_path(self):
self.cursor.execute("select coalesce(max(idPath),0) from path")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_file(self):
self.cursor.execute("select coalesce(max(idFile),0) from files")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_person(self):
self.cursor.execute("select coalesce(max(actor_id),0) from actor")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_genre(self):
self.cursor.execute("select coalesce(max(genre_id),0) from genre")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_studio(self):
self.cursor.execute("select coalesce(max(studio_id),0) from studio")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_bookmark(self):
self.cursor.execute("select coalesce(max(idBookmark),0) from bookmark")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_tag(self):
self.cursor.execute("select coalesce(max(tag_id),0) from tag")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def add_path(self, path):
path_id = self.get_path(path)
if path_id is None:
# Create a new entry
path_id = self.create_entry_path()
query = (
'''
INSERT INTO path(idPath, strPath)
VALUES (?, ?)
'''
)
self.cursor.execute(query, (path_id, path))
return path_id
def get_path(self, path):
query = ' '.join((
"SELECT idPath",
"FROM path",
"WHERE strPath = ?"
))
self.cursor.execute(query, (path,))
try:
path_id = self.cursor.fetchone()[0]
except TypeError:
path_id = None
return path_id
def update_path(self, path_id, path, media_type, scraper):
query = ' '.join((
"UPDATE path",
"SET strPath = ?, strContent = ?, strScraper = ?, noUpdate = ?",
"WHERE idPath = ?"
))
self.cursor.execute(query, (path, media_type, scraper, 1, path_id))
def remove_path(self, path_id):
self.cursor.execute("DELETE FROM path WHERE idPath = ?", (path_id,))
def add_file(self, filename, path_id):
query = ' '.join((
"SELECT idFile",
"FROM files",
"WHERE strFilename = ?",
"AND idPath = ?"
))
self.cursor.execute(query, (filename, path_id,))
try:
file_id = self.cursor.fetchone()[0]
except TypeError:
# Create a new entry
file_id = self.create_entry_file()
query = (
'''
INSERT INTO files(idFile, idPath, strFilename)
VALUES (?, ?, ?)
'''
)
self.cursor.execute(query, (file_id, path_id, filename))
return file_id
def update_file(self, file_id, filename, path_id, date_added):
query = ' '.join((
"UPDATE files",
"SET idPath = ?, strFilename = ?, dateAdded = ?",
"WHERE idFile = ?"
))
self.cursor.execute(query, (path_id, filename, date_added, file_id))
def remove_file(self, path, filename):
path_id = self.get_path(path)
if path_id is not None:
query = ' '.join((
"DELETE FROM files",
"WHERE idPath = ?",
"AND strFilename = ?"
))
self.cursor.execute(query, (path_id, filename,))
def get_filename(self, file_id):
query = ' '.join((
"SELECT strFilename",
"FROM files",
"WHERE idFile = ?"
))
self.cursor.execute(query, (file_id,))
try:
filename = self.cursor.fetchone()[0]
except TypeError:
filename = ""
return filename
def add_people(self, kodi_id, people, media_type):
def add_thumbnail(person_id, person, type_):
thumbnail = person['imageurl']
if thumbnail:
art = type_.lower()
if "writing" in art:
art = "writer"
self.artwork.add_update_art(thumbnail, person_id, art, "thumb", self.cursor)
def add_link(link_type, person_id, kodi_id, media_type):
query = (
"INSERT OR REPLACE INTO " + link_type + "(actor_id, media_id, media_type)"
"VALUES (?, ?, ?)"
)
self.cursor.execute(query, (person_id, kodi_id, media_type))
cast_order = 1
if self.kodi_version > 14:
for person in people:
name = person['Name']
type_ = person['Type']
person_id = self._get_person(name)
# Link person to content
if type_ == "Actor":
role = person.get('Role')
query = (
'''
INSERT OR REPLACE INTO actor_link(
actor_id, media_id, media_type, role, cast_order)
VALUES (?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (person_id, kodi_id, media_type, role, cast_order))
cast_order += 1
elif type_ == "Director":
add_link("director_link", person_id, kodi_id, media_type)
elif type_ in ("Writing", "Writer"):
add_link("writer_link", person_id, kodi_id, media_type)
elif type_ == "Artist":
add_link("actor_link", person_id, kodi_id, media_type)
add_thumbnail(person_id, person, type_)
else:
# TODO: Remove Helix code when Krypton is RC
for person in people:
name = person['Name']
type_ = person['Type']
query = ' '.join((
"SELECT idActor",
"FROM actors",
"WHERE strActor = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (name,))
try:
person_id = self.cursor.fetchone()[0]
except TypeError:
# Cast entry does not exists
self.cursor.execute("select coalesce(max(idActor),0) from actors")
person_id = self.cursor.fetchone()[0] + 1
query = "INSERT INTO actors(idActor, strActor) values(?, ?)"
self.cursor.execute(query, (person_id, name))
log.debug("Add people to media, processing: %s", name)
finally:
# Link person to content
if type_ == "Actor":
role = person.get('Role')
if media_type == "movie":
query = (
'''
INSERT OR REPLACE INTO actorlinkmovie(
idActor, idMovie, strRole, iOrder)
VALUES (?, ?, ?, ?)
'''
)
elif media_type == "tvshow":
query = (
'''
INSERT OR REPLACE INTO actorlinktvshow(
idActor, idShow, strRole, iOrder)
VALUES (?, ?, ?, ?)
'''
)
elif media_type == "episode":
query = (
'''
INSERT OR REPLACE INTO actorlinkepisode(
idActor, idEpisode, strRole, iOrder)
VALUES (?, ?, ?, ?)
'''
)
else: return # Item is invalid
self.cursor.execute(query, (person_id, kodi_id, role, cast_order))
cast_order += 1
elif type_ == "Director":
if media_type == "movie":
query = (
'''
INSERT OR REPLACE INTO directorlinkmovie(idDirector, idMovie)
VALUES (?, ?)
'''
)
elif media_type == "tvshow":
query = (
'''
INSERT OR REPLACE INTO directorlinktvshow(idDirector, idShow)
VALUES (?, ?)
'''
)
elif media_type == "musicvideo":
query = (
'''
INSERT OR REPLACE INTO directorlinkmusicvideo(idDirector, idMVideo)
VALUES (?, ?)
'''
)
elif media_type == "episode":
query = (
'''
INSERT OR REPLACE INTO directorlinkepisode(idDirector, idEpisode)
VALUES (?, ?)
'''
)
else: return # Item is invalid
self.cursor.execute(query, (person_id, kodi_id))
elif type_ in ("Writing", "Writer"):
if media_type == "movie":
query = (
'''
INSERT OR REPLACE INTO writerlinkmovie(idWriter, idMovie)
VALUES (?, ?)
'''
)
elif media_type == "episode":
query = (
'''
INSERT OR REPLACE INTO writerlinkepisode(idWriter, idEpisode)
VALUES (?, ?)
'''
)
else: return # Item is invalid
self.cursor.execute(query, (person_id, kodi_id))
elif type_ == "Artist":
query = (
'''
INSERT OR REPLACE INTO artistlinkmusicvideo(idArtist, idMVideo)
VALUES (?, ?)
'''
)
self.cursor.execute(query, (person_id, kodi_id))
add_thumbnail(person_id, person, type_)
def _add_person(self, name):
person_id = self.create_entry_person()
query = "INSERT INTO actor(actor_id, name) values(?, ?)"
self.cursor.execute(query, (person_id, name))
log.debug("Add people to media, processing: %s", name)
return person_id
def _get_person(self, name):
query = ' '.join((
"SELECT actor_id",
"FROM actor",
"WHERE name = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (name,))
try:
person_id = self.cursor.fetchone()[0]
except TypeError:
person_id = self._add_person(name)
return person_id
def add_genres(self, kodi_id, genres, media_type):
if self.kodi_version > 14:
# Delete current genres for clean slate
query = ' '.join((
"DELETE FROM genre_link",
"WHERE media_id = ?",
"AND media_type = ?"
))
self.cursor.execute(query, (kodi_id, media_type,))
# Add genres
for genre in genres:
genre_id = self._get_genre(genre)
query = (
'''
INSERT OR REPLACE INTO genre_link(
genre_id, media_id, media_type)
VALUES (?, ?, ?)
'''
)
self.cursor.execute(query, (genre_id, kodi_id, media_type))
else:
# TODO: Remove Helix code when Krypton is RC
# Delete current genres for clean slate
if media_type == "movie":
self.cursor.execute("DELETE FROM genrelinkmovie WHERE idMovie = ?", (kodi_id,))
elif media_type == "tvshow":
self.cursor.execute("DELETE FROM genrelinktvshow WHERE idShow = ?", (kodi_id,))
elif media_type == "musicvideo":
self.cursor.execute("DELETE FROM genrelinkmusicvideo WHERE idMVideo = ?", (kodi_id,))
# Add genres
for genre in genres:
query = ' '.join((
"SELECT idGenre",
"FROM genre",
"WHERE strGenre = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (genre,))
try:
genre_id = self.cursor.fetchone()[0]
except TypeError:
# Create genre in database
self.cursor.execute("select coalesce(max(idGenre),0) from genre")
genre_id = self.cursor.fetchone()[0] + 1
query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)"
self.cursor.execute(query, (genre_id, genre))
log.debug("Add Genres to media, processing: %s", genre)
finally:
# Assign genre to item
if media_type == "movie":
query = (
'''
INSERT OR REPLACE into genrelinkmovie(idGenre, idMovie)
VALUES (?, ?)
'''
)
elif media_type == "tvshow":
query = (
'''
INSERT OR REPLACE into genrelinktvshow(idGenre, idShow)
VALUES (?, ?)
'''
)
elif media_type == "musicvideo":
query = (
'''
INSERT OR REPLACE into genrelinkmusicvideo(idGenre, idMVideo)
VALUES (?, ?)
'''
)
else: return # Item is invalid
self.cursor.execute(query, (genre_id, kodi_id))
def _add_genre(self, genre):
genre_id = self.create_entry_genre()
query = "INSERT INTO genre(genre_id, name) values(?, ?)"
self.cursor.execute(query, (genre_id, genre))
log.debug("Add Genres to media, processing: %s", genre)
return genre_id
def _get_genre(self, genre):
query = ' '.join((
"SELECT genre_id",
"FROM genre",
"WHERE name = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (genre,))
try:
genre_id = self.cursor.fetchone()[0]
except TypeError:
genre_id = self._add_genre(genre)
return genre_id
def add_studios(self, kodi_id, studios, media_type):
if self.kodi_version > 14:
for studio in studios:
studio_id = self._get_studio(studio)
query = (
'''
INSERT OR REPLACE INTO studio_link(studio_id, media_id, media_type)
VALUES (?, ?, ?)
''')
self.cursor.execute(query, (studio_id, kodi_id, media_type))
else:
# TODO: Remove Helix code when Krypton is RC
for studio in studios:
query = ' '.join((
"SELECT idstudio",
"FROM studio",
"WHERE strstudio = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (studio,))
try:
studio_id = self.cursor.fetchone()[0]
except TypeError:
# Studio does not exists.
self.cursor.execute("select coalesce(max(idstudio),0) from studio")
studio_id = self.cursor.fetchone()[0] + 1
query = "INSERT INTO studio(idstudio, strstudio) values(?, ?)"
self.cursor.execute(query, (studio_id, studio))
log.debug("Add Studios to media, processing: %s", studio)
finally: # Assign studio to item
if media_type == "movie":
query = (
'''
INSERT OR REPLACE INTO studiolinkmovie(idstudio, idMovie)
VALUES (?, ?)
''')
elif media_type == "musicvideo":
query = (
'''
INSERT OR REPLACE INTO studiolinkmusicvideo(idstudio, idMVideo)
VALUES (?, ?)
''')
elif media_type == "tvshow":
query = (
'''
INSERT OR REPLACE INTO studiolinktvshow(idstudio, idShow)
VALUES (?, ?)
''')
elif media_type == "episode":
query = (
'''
INSERT OR REPLACE INTO studiolinkepisode(idstudio, idEpisode)
VALUES (?, ?)
''')
self.cursor.execute(query, (studio_id, kodi_id))
def _add_studio(self, studio):
studio_id = self.create_entry_studio()
query = "INSERT INTO studio(studio_id, name) values(?, ?)"
self.cursor.execute(query, (studio_id, studio))
log.debug("Add Studios to media, processing: %s", studio)
return studio_id
def _get_studio(self, studio):
query = ' '.join((
"SELECT studio_id",
"FROM studio",
"WHERE name = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (studio,))
try:
studio_id = self.cursor.fetchone()[0]
except TypeError:
studio_id = self._add_studio(studio)
return studio_id
def add_streams(self, file_id, streams, runtime):
# First remove any existing entries
self.cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (file_id,))
if streams:
# Video details
for track in streams['video']:
query = (
'''
INSERT INTO streamdetails(
idFile, iStreamType, strVideoCodec, fVideoAspect,
iVideoWidth, iVideoHeight, iVideoDuration ,strStereoMode)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (file_id, 0, track['codec'], track['aspect'],
track['width'], track['height'], runtime,
track['video3DFormat']))
# Audio details
for track in streams['audio']:
query = (
'''
INSERT INTO streamdetails(
idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage)
VALUES (?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (file_id, 1, track['codec'], track['channels'],
track['language']))
# Subtitles details
for track in streams['subtitle']:
query = (
'''
INSERT INTO streamdetails(idFile, iStreamType, strSubtitleLanguage)
VALUES (?, ?, ?)
'''
)
self.cursor.execute(query, (file_id, 2, track))
def add_playstate(self, file_id, resume, total, playcount, date_played):
# Delete existing resume point
self.cursor.execute("DELETE FROM bookmark WHERE idFile = ?", (file_id,))
# Set watched count
self.set_playcount(file_id, playcount, date_played)
if resume:
bookmark_id = self.create_entry_bookmark()
query = (
'''
INSERT INTO bookmark(
idBookmark, idFile, timeInSeconds, totalTimeInSeconds, player, type)
VALUES (?, ?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (bookmark_id, file_id, resume, total, "DVDPlayer", 1))
def set_playcount(self, file_id, playcount, date_played):
query = ' '.join((
"UPDATE files",
"SET playCount = ?, lastPlayed = ?",
"WHERE idFile = ?"
))
self.cursor.execute(query, (playcount, date_played, file_id))
def add_tags(self, kodi_id, tags, media_type):
if self.kodi_version > 14:
query = ' '.join((
"DELETE FROM tag_link",
"WHERE media_id = ?",
"AND media_type = ?"
))
self.cursor.execute(query, (kodi_id, media_type))
# Add tags
log.debug("Adding Tags: %s", tags)
for tag in tags:
tag_id = self.get_tag(kodi_id, tag, media_type)
else:
# TODO: Remove Helix code when Krypton is RC
query = ' '.join((
"DELETE FROM taglinks",
"WHERE idMedia = ?",
"AND media_type = ?"
))
self.cursor.execute(query, (kodi_id, media_type))
# Add tags
log.debug("Adding Tags: %s", tags)
for tag in tags:
tag_id = self.get_tag_old(kodi_id, tag, media_type)
def _add_tag(self, tag):
tag_id = self.create_entry_tag()
query = "INSERT INTO tag(tag_id, name) values(?, ?)"
self.cursor.execute(query, (tag_id, tag))
log.debug("Create tag_id: %s name: %s", tag_id, tag)
return tag_id
def get_tag(self, kodi_id, tag, media_type):
if self.kodi_version > 14:
query = ' '.join((
"SELECT tag_id",
"FROM tag",
"WHERE name = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (tag,))
try:
tag_id = self.cursor.fetchone()[0]
except TypeError:
tag_id = self._add_tag(tag)
query = (
'''
INSERT OR REPLACE INTO tag_link(tag_id, media_id, media_type)
VALUES (?, ?, ?)
'''
)
self.cursor.execute(query, (tag_id, kodi_id, media_type))
else:
# TODO: Remove Helix code when Krypton is RC
tag_id = self.get_tag_old(kodi_id, tag, media_type)
return tag_id
def get_tag_old(self, kodi_id, tag, media_type):
# TODO: Remove Helix code when Krypton is RC
query = ' '.join((
"SELECT idTag",
"FROM tag",
"WHERE strTag = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (tag,))
try:
tag_id = self.cursor.fetchone()[0]
except TypeError:
# Create the tag
self.cursor.execute("select coalesce(max(idTag),0) from tag")
tag_id = self.cursor.fetchone()[0] + 1
query = "INSERT INTO tag(idTag, strTag) values(?, ?)"
self.cursor.execute(query, (tag_id, name))
log.debug("Create idTag: %s name: %s", tag_id, name)
finally:
# Assign tag to item
query = (
'''
INSERT OR REPLACE INTO taglinks(
idTag, idMedia, media_type)
VALUES (?, ?, ?)
'''
)
self.cursor.execute(query, (tag_id, kodi_id, media_type))
return tag_id
def remove_tag(self, kodi_id, tag, media_type):
if self.kodi_version > 14:
query = ' '.join((
"SELECT tag_id",
"FROM tag",
"WHERE name = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (tag,))
try:
tag_id = self.cursor.fetchone()[0]
except TypeError:
return
else:
query = ' '.join((
"DELETE FROM tag_link",
"WHERE media_id = ?",
"AND media_type = ?",
"AND tag_id = ?"
))
self.cursor.execute(query, (kodi_id, media_type, tag_id,))
else:
# TODO: Remove Helix code when Krypton is RC
query = ' '.join((
"SELECT idTag",
"FROM tag",
"WHERE strTag = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (tag,))
try:
tag_id = self.cursor.fetchone()[0]
except TypeError:
return
else:
query = ' '.join((
"DELETE FROM taglinks",
"WHERE idMedia = ?",
"AND media_type = ?",
"AND idTag = ?"
))
self.cursor.execute(query, (kodi_id, media_type, tag_id,))

View file

@ -0,0 +1,225 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
from _kodi_common import KodiItems
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class KodiMovies(KodiItems):
def __init__(self, cursor):
self.cursor = cursor
KodiItems.__init__(self)
def create_entry(self):
self.cursor.execute("select coalesce(max(idMovie),0) from movie")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_set(self):
self.cursor.execute("select coalesce(max(idSet),0) from sets")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_country(self):
self.cursor.execute("select coalesce(max(country_id),0) from country")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def get_movie(self, kodi_id):
query = "SELECT * FROM movie WHERE idMovie = ?"
self.cursor.execute(query, (kodi_id,))
try:
kodi_id = self.cursor.fetchone()[0]
except TypeError:
kodi_id = None
return kodi_id
def add_movie(self, *args):
query = (
'''
INSERT INTO movie(
idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07,
c09, c10, c11, c12, c14, c15, c16, c18, c19, c21)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (args))
def add_movie_17(self, *args):
# Create the movie entry
query = (
'''
INSERT INTO movie(
idMovie, idFile, c00, c01, c02, c03, c04, c05, c06, c07,
c09, c10, c11, c12, c14, c15, c16, c18, c19, c21, premiered)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (args))
def update_movie(self, *args):
query = ' '.join((
"UPDATE movie",
"SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,",
"c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?,",
"c16 = ?, c18 = ?, c19 = ?, c21 = ?",
"WHERE idMovie = ?"
))
self.cursor.execute(query, (args))
def update_movie_17(self, *args):
query = ' '.join((
"UPDATE movie",
"SET c00 = ?, c01 = ?, c02 = ?, c03 = ?, c04 = ?, c05 = ?, c06 = ?,",
"c07 = ?, c09 = ?, c10 = ?, c11 = ?, c12 = ?, c14 = ?, c15 = ?,",
"c16 = ?, c18 = ?, c19 = ?, c21 = ?, premiered = ?",
"WHERE idMovie = ?"
))
self.cursor.execute(query, (args))
def remove_movie(self, kodi_id, file_id):
self.cursor.execute("DELETE FROM movie WHERE idMovie = ?", (kodi_id,))
self.cursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,))
def add_countries(self, kodi_id, countries):
if self.kodi_version > 14:
for country in countries:
country_id = self._get_country(country)
query = (
'''
INSERT OR REPLACE INTO country_link(country_id, media_id, media_type)
VALUES (?, ?, ?)
'''
)
self.cursor.execute(query, (country_id, kodi_id, "movie"))
else:
# TODO: Remove Helix code when Krypton is RC
for country in countries:
query = ' '.join((
"SELECT idCountry",
"FROM country",
"WHERE strCountry = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (country,))
try:
country_id = self.cursor.fetchone()[0]
except TypeError:
# Create a new entry
self.cursor.execute("select coalesce(max(idCountry),0) from country")
country_id = self.cursor.fetchone()[0] + 1
query = "INSERT INTO country(idCountry, strCountry) values(?, ?)"
self.cursor.execute(query, (country_id, country))
log.debug("Add country to media, processing: %s", country)
query = (
'''
INSERT OR REPLACE INTO countrylinkmovie(idCountry, idMovie)
VALUES (?, ?)
'''
)
self.cursor.execute(query, (country_id, kodi_id))
def _add_country(self, country):
country_id = self.create_entry_country()
query = "INSERT INTO country(country_id, name) values(?, ?)"
self.cursor.execute(query, (country_id, country))
log.debug("Add country to media, processing: %s", country)
return country_id
def _get_country(self, country):
query = ' '.join((
"SELECT country_id",
"FROM country",
"WHERE name = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (country,))
try:
country_id = self.cursor.fetchone()[0]
except TypeError:
country_id = self._add_country(country)
return country_id
def add_boxset(self, boxset):
query = ' '.join((
"SELECT idSet",
"FROM sets",
"WHERE strSet = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (boxset,))
try:
set_id = self.cursor.fetchone()[0]
except TypeError:
set_id = self._add_boxset(boxset)
return set_id
def _add_boxset(self, boxset):
set_id = self.create_entry_set()
query = "INSERT INTO sets(idSet, strSet) values(?, ?)"
self.cursor.execute(query, (set_id, boxset))
log.debug("Adding boxset: %s", boxset)
return set_id
def set_boxset(self, set_id, movie_id):
query = ' '.join((
"UPDATE movie",
"SET idSet = ?",
"WHERE idMovie = ?"
))
self.cursor.execute(query, (set_id, movie_id,))
def remove_from_boxset(self, movie_id):
query = ' '.join((
"UPDATE movie",
"SET idSet = null",
"WHERE idMovie = ?"
))
self.cursor.execute(query, (movie_id,))
def remove_boxset(self, kodi_id):
self.cursor.execute("DELETE FROM sets WHERE idSet = ?", (kodi_id,))

View file

@ -0,0 +1,406 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
from _kodi_common import KodiItems
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class KodiMusic(KodiItems):
def __init__(self, cursor):
self.cursor = cursor
KodiItems.__init__(self)
def create_entry(self):
self.cursor.execute("select coalesce(max(idArtist),0) from artist")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_album(self):
self.cursor.execute("select coalesce(max(idAlbum),0) from album")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_song(self):
self.cursor.execute("select coalesce(max(idSong),0) from song")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_genre(self):
self.cursor.execute("select coalesce(max(idGenre),0) from genre")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def update_path(self, path_id, path):
query = "UPDATE path SET strPath = ? WHERE idPath = ?"
self.cursor.execute(query, (path, path_id))
def add_role(self):
query = (
'''
INSERT OR REPLACE INTO role(idRole, strRole)
VALUES (?, ?)
'''
)
self.cursor.execute(query, (1, 'Composer'))
def get_artist(self, name, musicbrainz):
query = ' '.join((
"SELECT idArtist, strArtist",
"FROM artist",
"WHERE strMusicBrainzArtistID = ?"
))
self.cursor.execute(query, (musicbrainz,))
try:
result = self.cursor.fetchone()
artist_id = result[0]
artist_name = result[1]
except TypeError:
artist_id = self._add_artist(name, musicbrainz)
else:
if artist_name != name:
self.update_artist_name(artist_id, name)
return artist_id
def _add_artist(self, name, musicbrainz):
query = ' '.join((
# Safety check, when musicbrainz does not exist
"SELECT idArtist",
"FROM artist",
"WHERE strArtist = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (name,))
try:
artist_id = self.cursor.fetchone()[0]
except TypeError:
artist_id = self.create_entry()
query = (
'''
INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID)
VALUES (?, ?, ?)
'''
)
self.cursor.execute(query, (artist_id, name, musicbrainz))
return artist_id
def update_artist_name(self, kodi_id, name):
query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?"
self.cursor.execute(query, (name, kodi_id,))
def update_artist_16(self, *args):
query = ' '.join((
"UPDATE artist",
"SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,",
"lastScraped = ?",
"WHERE idArtist = ?"
))
self.cursor.execute(query, (args))
def update_artist(self, *args):
query = ' '.join((
"UPDATE artist",
"SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,",
"lastScraped = ?, dateAdded = ?",
"WHERE idArtist = ?"
))
self.cursor.execute(query, (args))
def link_artist(self, kodi_id, album_id, name):
query = (
'''
INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist)
VALUES (?, ?, ?)
'''
)
self.cursor.execute(query, (kodi_id, album_id, name))
def add_discography(self, kodi_id, album, year):
query = (
'''
INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear)
VALUES (?, ?, ?)
'''
)
self.cursor.execute(query, (kodi_id, album, year))
def get_album(self, name, musicbrainz):
query = ' '.join((
"SELECT idAlbum",
"FROM album",
"WHERE strMusicBrainzAlbumID = ?"
))
self.cursor.execute(query, (musicbrainz,))
try:
album_id = self.cursor.fetchone()[0]
except TypeError:
album_id = self._add_album(name, musicbrainz)
return album_id
def _add_album(self, name, musicbrainz):
album_id = self.create_entry_album()
if self.kodi_version > 14:
query = (
'''
INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType)
VALUES (?, ?, ?, ?)
'''
)
self.cursor.execute(query, (album_id, name, musicbrainz, "album"))
else:
# TODO: Remove Helix code when Krypton is RC
query = (
'''
INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID)
VALUES (?, ?, ?)
'''
)
self.cursor.execute(query, (album_id, name, musicbrainz))
return album_id
def update_album(self, *args):
query = ' '.join((
"UPDATE album",
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
"iRating = ?, lastScraped = ?, strReleaseType = ?",
"WHERE idAlbum = ?"
))
self.cursor.execute(query, (args))
def update_album_17(self, *args):
query = ' '.join((
"UPDATE album",
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
"iUserrating = ?, lastScraped = ?, strReleaseType = ?",
"WHERE idAlbum = ?"
))
self.cursor.execute(query, (args))
def update_album_15(self, *args):
query = ' '.join((
"UPDATE album",
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
"iRating = ?, lastScraped = ?, dateAdded = ?, strReleaseType = ?",
"WHERE idAlbum = ?"
))
self.cursor.execute(query, (args))
def update_album_14(self, *args):
# TODO: Remove Helix code when Krypton is RC
query = ' '.join((
"UPDATE album",
"SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,",
"iRating = ?, lastScraped = ?, dateAdded = ?",
"WHERE idAlbum = ?"
))
self.cursor.execute(query, (args))
def get_album_artist(self, album_id, artists):
query = ' '.join((
"SELECT strArtists",
"FROM album",
"WHERE idAlbum = ?"
))
self.cursor.execute(query, (album_id,))
try:
curr_artists = self.cursor.fetchone()[0]
except TypeError:
return
if curr_artists != artists:
self._update_album_artist(album_id, artists)
def _update_album_artist(self, album_id, artists):
query = "UPDATE album SET strArtists = ? WHERE idAlbum = ?"
self.cursor.execute(query, (artists, album_id))
def add_single(self, *args):
query = (
'''
INSERT INTO album(idAlbum, strGenres, iYear, strReleaseType)
VALUES (?, ?, ?, ?)
'''
)
self.cursor.execute(query, (args))
def add_single_15(self, *args):
query = (
'''
INSERT INTO album(idAlbum, strGenres, iYear, dateAdded, strReleaseType)
VALUES (?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (args))
def add_single_14(self, *args):
# TODO: Remove Helix code when Krypton is RC
query = (
'''
INSERT INTO album(idAlbum, strGenres, iYear, dateAdded)
VALUES (?, ?, ?, ?)
'''
)
self.cursor.execute(query, (args))
def add_song(self, *args):
query = (
'''
INSERT INTO song(
idSong, idAlbum, idPath, strArtists, strGenres, strTitle, iTrack,
iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed,
rating)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (args))
def update_song(self, *args):
query = ' '.join((
"UPDATE song",
"SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, iTrack = ?,",
"iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,",
"rating = ?, comment = ?",
"WHERE idSong = ?"
))
self.cursor.execute(query, (args))
def link_song_artist(self, kodi_id, song_id, index, artist):
if self.kodi_version > 16:
query = (
'''
INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist)
VALUES (?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (kodi_id, song_id, 1, index, artist))
else:
query = (
'''
INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist)
VALUES (?, ?, ?, ?)
'''
)
self.cursor.execute(query, (kodi_id, song_id, index, artist))
def link_song_album(self, song_id, album_id, track, title, duration):
query = (
'''
INSERT OR REPLACE INTO albuminfosong(
idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration)
VALUES (?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (song_id, album_id, track, title, duration))
def rate_song(self, kodi_id, playcount, rating, date_played):
query = "UPDATE song SET iTimesPlayed = ?, lastplayed = ?, rating = ? WHERE idSong = ?"
self.cursor.execute(query, (playcount, date_played, rating, kodi_id))
def add_genres(self, kodi_id, genres, media_type):
if media_type == "album":
# Delete current genres for clean slate
query = ' '.join((
"DELETE FROM album_genre",
"WHERE idAlbum = ?"
))
self.cursor.execute(query, (kodi_id,))
for genre in genres:
genre_id = self.get_genre(genre)
query = "INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) values(?, ?)"
self.cursor.execute(query, (genre_id, kodi_id))
elif media_type == "song":
# Delete current genres for clean slate
query = ' '.join((
"DELETE FROM song_genre",
"WHERE idSong = ?"
))
self.cursor.execute(query, (kodi_id,))
for genre in genres:
genre_id = self.get_genre(genre)
query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)"
self.cursor.execute(query, (genre_id, kodi_id))
def get_genre(self, genre):
query = ' '.join((
"SELECT idGenre",
"FROM genre",
"WHERE strGenre = ?",
"COLLATE NOCASE"
))
self.cursor.execute(query, (genre,))
try:
genre_id = self.cursor.fetchone()[0]
except TypeError:
genre_id = self._add_genre(genre)
return genre_id
def _add_genre(self, genre):
genre_id = self.create_entry_genre()
query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)"
self.cursor.execute(query, (genre_id, genre))
return genre_id
def remove_artist(self, kodi_id):
self.cursor.execute("DELETE FROM artist WHERE idArtist = ?", (kodi_id,))
def remove_album(self, kodi_id):
self.cursor.execute("DELETE FROM album WHERE idAlbum = ?", (kodi_id,))
def remove_song(self, kodi_id):
self.cursor.execute("DELETE FROM song WHERE idSong = ?", (kodi_id,))

View file

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
from _kodi_common import KodiItems
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class KodiMusicVideos(KodiItems):
def __init__(self, cursor):
self.cursor = cursor
KodiItems.__init__(self)
def create_entry(self):
self.cursor.execute("select coalesce(max(idMVideo),0) from musicvideo")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def get_musicvideo(self, kodi_id):
query = "SELECT * FROM musicvideo WHERE idMVideo = ?"
self.cursor.execute(query, (kodi_id,))
try:
kodi_id = self.cursor.fetchone()[0]
except TypeError:
kodi_id = None
return kodi_id
def add_musicvideo(self, *args):
query = (
'''
INSERT INTO musicvideo(
idMVideo, idFile, c00, c04, c05, c06, c07, c08, c09, c10, c11, c12)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (args))
def update_musicvideo(self, *args):
query = ' '.join((
"UPDATE musicvideo",
"SET c00 = ?, c04 = ?, c05 = ?, c06 = ?, c07 = ?, c08 = ?, c09 = ?, c10 = ?,",
"c11 = ?, c12 = ?"
"WHERE idMVideo = ?"
))
self.cursor.execute(query, (args))
def remove_musicvideo(self, kodi_id, file_id):
self.cursor.execute("DELETE FROM musicvideo WHERE idMVideo = ?", (kodi_id,))
self.cursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,))

View file

@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
from _kodi_common import KodiItems
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class KodiTVShows(KodiItems):
def __init__(self, cursor):
self.cursor = cursor
KodiItems.__init__(self)
def create_entry(self):
self.cursor.execute("select coalesce(max(idShow),0) from tvshow")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_season(self):
self.cursor.execute("select coalesce(max(idSeason),0) from seasons")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def create_entry_episode(self):
self.cursor.execute("select coalesce(max(idEpisode),0) from episode")
kodi_id = self.cursor.fetchone()[0] + 1
return kodi_id
def get_tvshow(self, kodi_id):
query = "SELECT * FROM tvshow WHERE idShow = ?"
self.cursor.execute(query, (kodi_id,))
try:
kodi_id = self.cursor.fetchone()[0]
except TypeError:
kodi_id = None
return kodi_id
def get_episode(self, kodi_id):
query = "SELECT * FROM episode WHERE idEpisode = ?"
self.cursor.execute(query, (kodi_id,))
try:
kodi_id = self.cursor.fetchone()[0]
except TypeError:
kodi_id = None
return kodi_id
def add_tvshow(self, *args):
query = (
'''
INSERT INTO tvshow(idShow, c00, c01, c04, c05, c08, c09, c12, c13, c14, c15)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (args))
def update_tvshow(self, *args):
query = ' '.join((
"UPDATE tvshow",
"SET c00 = ?, c01 = ?, c04 = ?, c05 = ?, c08 = ?, c09 = ?,",
"c12 = ?, c13 = ?, c14 = ?, c15 = ?",
"WHERE idShow = ?"
))
self.cursor.execute(query, (args))
def link_tvshow(self, show_id, path_id):
query = "INSERT OR REPLACE INTO tvshowlinkpath(idShow, idPath) values(?, ?)"
self.cursor.execute(query, (show_id, path_id))
def get_season(self, show_id, number, name=None):
query = ' '.join((
"SELECT idSeason",
"FROM seasons",
"WHERE idShow = ?",
"AND season = ?"
))
self.cursor.execute(query, (show_id, number,))
try:
season_id = self.cursor.fetchone()[0]
except TypeError:
season_id = self._add_season(show_id, number)
if self.kodi_version > 15 and name is not None:
query = "UPDATE seasons SET name = ? WHERE idSeason = ?"
self.cursor.execute(query, (name, season_id))
return season_id
def _add_season(self, show_id, number):
season_id = self.create_entry_season()
query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)"
self.cursor.execute(query, (season_id, show_id, number))
return season_id
def add_episode(self, *args):
query = (
'''
INSERT INTO episode(
idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14,
idShow, c15, c16)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (args))
def add_episode_16(self, *args):
query = (
'''
INSERT INTO episode(
idEpisode, idFile, c00, c01, c03, c04, c05, c09, c10, c12, c13, c14,
idShow, c15, c16, idSeason)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
)
self.cursor.execute(query, (args))
def update_episode(self, *args):
query = ' '.join((
"UPDATE episode",
"SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,",
"c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idShow = ?",
"WHERE idEpisode = ?"
))
self.cursor.execute(query, (args))
def update_episode_16(self, *args):
query = ' '.join((
"UPDATE episode",
"SET c00 = ?, c01 = ?, c03 = ?, c04 = ?, c05 = ?, c09 = ?, c10 = ?,",
"c12 = ?, c13 = ?, c14 = ?, c15 = ?, c16 = ?, idSeason = ?, idShow = ?",
"WHERE idEpisode = ?"
))
self.cursor.execute(query, (args))
def remove_tvshow(self, kodi_id):
self.cursor.execute("DELETE FROM tvshow WHERE idShow = ?", (kodi_id,))
def remove_season(self, kodi_id):
self.cursor.execute("DELETE FROM seasons WHERE idSeason = ?", (kodi_id,))
def remove_episode(self, kodi_id, file_id):
self.cursor.execute("DELETE FROM episode WHERE idEpisode = ?", (kodi_id,))
self.cursor.execute("DELETE FROM files WHERE idFile = ?", (file_id,))

View file

@ -0,0 +1,453 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import urllib
import api
import embydb_functions as embydb
import _kodi_movies
from _common import Items, catch_except
from utils import window, settings, language as lang
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class Movies(Items):
def __init__(self, embycursor, kodicursor, pdialog=None):
self.embycursor = embycursor
self.emby_db = embydb.Embydb_Functions(self.embycursor)
self.kodicursor = kodicursor
self.kodi_db = _kodi_movies.KodiMovies(self.kodicursor)
self.pdialog = pdialog
self.new_time = int(settings('newvideotime'))*1000
Items.__init__(self)
def _get_func(self, item_type, action):
if item_type == "Movie":
actions = {
'added': self.added,
'update': self.add_update,
'userdata': self.updateUserdata,
'remove': self.remove
}
elif item_type == "BoxSet":
actions = {
'added': self.added_boxset,
'update': self.add_updateBoxset,
'remove': self.remove
}
else:
log.info("Unsupported item_type: %s", item_type)
actions = {}
return actions.get(action)
def compare_all(self):
# Pull the list of movies and boxsets in Kodi
views = self.emby_db.getView_byType('movies')
views += self.emby_db.getView_byType('mixed')
log.info("Media folders: %s", views)
# Process movies
for view in views:
if self.should_stop():
return False
if not self.compare_movies(view):
return False
# Process boxsets
if not self.compare_boxsets():
return False
return True
def compare_movies(self, view):
view_id = view['id']
view_name = view['name']
if self.pdialog:
self.pdialog.update(heading=lang(29999), message="%s %s..." % (lang(33026), view_name))
movies = dict(self.emby_db.get_checksum_by_view("Movie", view_id))
emby_movies = self.emby.getMovies(view_id, basic=True, dialog=self.pdialog)
return self.compare("Movie", emby_movies['Items'], movies, view)
def compare_boxsets(self):
if self.pdialog:
self.pdialog.update(heading=lang(29999), message=lang(33027))
boxsets = dict(self.emby_db.get_checksum('BoxSet'))
emby_boxsets = self.emby.getBoxset(dialog=self.pdialog)
return self.compare("BoxSet", emby_boxsets['Items'], boxsets)
def added(self, items, total=None, view=None):
for item in super(Movies, self).added(items, total):
if self.add_update(item, view):
self.content_pop(item.get('Name', "unknown"))
def added_boxset(self, items, total=None):
for item in super(Movies, self).added(items, total):
self.add_updateBoxset(item)
@catch_except()
def add_update(self, item, view=None):
# Process single movie
emby_db = self.emby_db
artwork = self.artwork
API = api.API(item)
# If the item already exist in the local Kodi DB we'll perform a full item update
# If the item doesn't exist, we'll add it to the database
update_item = True
itemid = item['Id']
emby_dbitem = emby_db.getItem_byId(itemid)
try:
movieid = emby_dbitem[0]
fileid = emby_dbitem[1]
pathid = emby_dbitem[2]
log.info("movieid: %s fileid: %s pathid: %s", movieid, fileid, pathid)
except TypeError:
update_item = False
log.debug("movieid: %s not found", itemid)
# movieid
movieid = self.kodi_db.create_entry()
else:
if self.kodi_db.get_movie(movieid) is None:
# item is not found, let's recreate it.
update_item = False
log.info("movieid: %s missing from Kodi, repairing the entry", movieid)
if not view:
# Get view tag from emby
viewtag, viewid, mediatype = self.emby.getView_embyId(itemid)
log.debug("View tag found: %s", viewtag)
else:
viewtag = view['name']
viewid = view['id']
# fileId information
checksum = API.get_checksum()
dateadded = API.get_date_created()
userdata = API.get_userdata()
playcount = userdata['PlayCount']
dateplayed = userdata['LastPlayedDate']
# item details
people = API.get_people()
writer = " / ".join(people['Writer'])
director = " / ".join(people['Director'])
genres = item['Genres']
title = item['Name']
plot = API.get_overview()
shortplot = item.get('ShortOverview')
tagline = API.get_tagline()
votecount = item.get('VoteCount')
rating = item.get('CommunityRating')
year = item.get('ProductionYear')
imdb = API.get_provider('Imdb')
sorttitle = item['SortName']
runtime = API.get_runtime()
mpaa = API.get_mpaa()
genre = " / ".join(genres)
country = API.get_country()
studios = API.get_studios()
try:
studio = studios[0]
except IndexError:
studio = None
if item.get('LocalTrailerCount'):
# There's a local trailer
url = (
"{server}/emby/Users/{UserId}/Items/%s/LocalTrailers?format=json"
% itemid
)
result = self.do_url(url)
try:
trailer = "plugin://plugin.video.emby/trailer/?id=%s&mode=play" % result[0]['Id']
except IndexError:
log.info("Failed to process local trailer.")
trailer = None
else:
# Try to get the youtube trailer
try:
trailer = item['RemoteTrailers'][0]['Url']
except (KeyError, IndexError):
trailer = None
else:
try:
trailer_id = trailer.rsplit('=', 1)[1]
except IndexError:
log.info("Failed to process trailer: %s", trailer)
trailer = None
else:
trailer = "plugin://plugin.video.youtube/play/?video_id=%s" % trailer_id
##### GET THE FILE AND PATH #####
playurl = API.get_file_path()
if "\\" in playurl:
# Local path
filename = playurl.rsplit("\\", 1)[1]
else: # Network share
filename = playurl.rsplit("/", 1)[1]
if self.direct_path:
# Direct paths is set the Kodi way
if not self.path_validation(playurl):
return False
path = playurl.replace(filename, "")
window('emby_pathverified', value="true")
else:
# Set plugin path and media flags using real filename
path = "plugin://plugin.video.emby.movies/"
params = {
'filename': filename.encode('utf-8'),
'id': itemid,
'dbid': movieid,
'mode': "play"
}
filename = "%s?%s" % (path, urllib.urlencode(params))
##### UPDATE THE MOVIE #####
if update_item:
log.info("UPDATE movie itemid: %s - Title: %s", itemid, title)
# Update the movie entry
if self.kodi_version > 16:
self.kodi_db.update_movie_17(title, plot, shortplot, tagline, votecount, rating,
writer, year, imdb, sorttitle, runtime, mpaa, genre,
director, title, studio, trailer, country, year,
movieid)
else:
self.kodi_db.update_movie(title, plot, shortplot, tagline, votecount, rating,
writer, year, imdb, sorttitle, runtime, mpaa, genre,
director, title, studio, trailer, country, movieid)
# Update the checksum in emby table
emby_db.updateReference(itemid, checksum)
##### OR ADD THE MOVIE #####
else:
log.info("ADD movie itemid: %s - Title: %s", itemid, title)
# Add path
pathid = self.kodi_db.add_path(path)
# Add the file
fileid = self.kodi_db.add_file(filename, pathid)
# Create the movie entry
if self.kodi_version > 16:
self.kodi_db.add_movie_17(movieid, fileid, title, plot, shortplot, tagline,
votecount, rating, writer, year, imdb, sorttitle,
runtime, mpaa, genre, director, title, studio, trailer,
country, year)
else:
self.kodi_db.add_movie(movieid, fileid, title, plot, shortplot, tagline,
votecount, rating, writer, year, imdb, sorttitle,
runtime, mpaa, genre, director, title, studio, trailer,
country)
# Create the reference in emby table
emby_db.addReference(itemid, movieid, "Movie", "movie", fileid, pathid, None,
checksum, viewid)
# Update the path
self.kodi_db.update_path(pathid, path, "movies", "metadata.local")
# Update the file
self.kodi_db.update_file(fileid, filename, pathid, dateadded)
# Process countries
if 'ProductionLocations' in item:
self.kodi_db.add_countries(movieid, item['ProductionLocations'])
# Process cast
people = artwork.get_people_artwork(item['People'])
self.kodi_db.add_people(movieid, people, "movie")
# Process genres
self.kodi_db.add_genres(movieid, genres, "movie")
# Process artwork
artwork.add_artwork(artwork.get_all_artwork(item), movieid, "movie", self.kodicursor)
# Process stream details
streams = API.get_media_streams()
self.kodi_db.add_streams(fileid, streams, runtime)
# Process studios
self.kodi_db.add_studios(movieid, studios, "movie")
# Process tags: view, emby tags
tags = [viewtag]
tags.extend(item['Tags'])
if userdata['Favorite']:
tags.append("Favorite movies")
log.info("Applied tags: %s", tags)
self.kodi_db.add_tags(movieid, tags, "movie")
# Process playstates
resume = API.adjust_resume(userdata['Resume'])
total = round(float(runtime), 6)
self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed)
return True
def add_updateBoxset(self, boxset):
emby = self.emby
emby_db = self.emby_db
artwork = self.artwork
API = api.API(boxset)
boxsetid = boxset['Id']
title = boxset['Name']
checksum = API.get_checksum()
emby_dbitem = emby_db.getItem_byId(boxsetid)
try:
setid = emby_dbitem[0]
except TypeError:
setid = self.kodi_db.add_boxset(title)
# Process artwork
artwork.add_artwork(artwork.get_all_artwork(boxset), setid, "set", self.kodicursor)
# Process movies inside boxset
current_movies = emby_db.getItemId_byParentId(setid, "movie")
process = []
try:
# Try to convert tuple to dictionary
current = dict(current_movies)
except ValueError:
current = {}
# Sort current titles
for current_movie in current:
process.append(current_movie)
# New list to compare
for movie in emby.getMovies_byBoxset(boxsetid)['Items']:
itemid = movie['Id']
if not current.get(itemid):
# Assign boxset to movie
emby_dbitem = emby_db.getItem_byId(itemid)
try:
movieid = emby_dbitem[0]
except TypeError:
log.info("Failed to add: %s to boxset", movie['Name'])
continue
log.info("New addition to boxset %s: %s", title, movie['Name'])
self.kodi_db.set_boxset(setid, movieid)
# Update emby reference
emby_db.updateParentId(itemid, setid)
else:
# Remove from process, because the item still belongs
process.remove(itemid)
# Process removals from boxset
for movie in process:
movieid = current[movie]
log.info("Remove from boxset %s: %s", title, movieid)
self.kodi_db.remove_from_boxset(movieid)
# Update emby reference
emby_db.updateParentId(movie, None)
# Update the reference in the emby table
emby_db.addReference(boxsetid, setid, "BoxSet", mediatype="set", checksum=checksum)
def updateUserdata(self, item):
# This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
# Poster with progress bar
emby_db = self.emby_db
API = api.API(item)
# Get emby information
itemid = item['Id']
checksum = API.get_checksum()
userdata = API.get_userdata()
runtime = API.get_runtime()
# Get Kodi information
emby_dbitem = emby_db.getItem_byId(itemid)
try:
movieid = emby_dbitem[0]
fileid = emby_dbitem[1]
log.info("Update playstate for movie: %s fileid: %s", item['Name'], fileid)
except TypeError:
return
# Process favorite tags
if userdata['Favorite']:
self.kodi_db.get_tag(movieid, "Favorite movies", "movie")
else:
self.kodi_db.remove_tag(movieid, "Favorite movies", "movie")
# Process playstates
playcount = userdata['PlayCount']
dateplayed = userdata['LastPlayedDate']
resume = API.adjust_resume(userdata['Resume'])
total = round(float(runtime), 6)
log.debug("%s New resume point: %s", itemid, resume)
self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed)
emby_db.updateReference(itemid, checksum)
def remove(self, itemid):
# Remove movieid, fileid, emby reference
emby_db = self.emby_db
artwork = self.artwork
emby_dbitem = emby_db.getItem_byId(itemid)
try:
kodiid = emby_dbitem[0]
fileid = emby_dbitem[1]
mediatype = emby_dbitem[4]
log.info("Removing %sid: %s fileid: %s", mediatype, kodiid, fileid)
except TypeError:
return
# Remove the emby reference
emby_db.removeItem(itemid)
# Remove artwork
artwork.delete_artwork(kodiid, mediatype, self.kodicursor)
if mediatype == "movie":
self.kodi_db.remove_movie(kodiid, fileid)
elif mediatype == "set":
# Delete kodi boxset
boxset_movies = emby_db.getItem_byParentId(kodiid, "movie")
for movie in boxset_movies:
embyid = movie[0]
movieid = movie[1]
self.kodi_db.remove_from_boxset(movieid)
# Update emby reference
emby_db.updateParentId(embyid, None)
self.kodi_db.remove_boxset(kodiid)
log.info("Deleted %s %s from kodi database", mediatype, itemid)

View file

@ -0,0 +1,709 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
from datetime import datetime
import api
import embydb_functions as embydb
import musicutils
import _kodi_music
from _common import Items, catch_except
from utils import window, settings, language as lang
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class Music(Items):
def __init__(self, embycursor, kodicursor, pdialog=None):
self.embycursor = embycursor
self.emby_db = embydb.Embydb_Functions(self.embycursor)
self.kodicursor = kodicursor
self.kodi_db = _kodi_music.KodiMusic(self.kodicursor)
self.pdialog = pdialog
self.new_time = int(settings('newmusictime'))*1000
self.directstream = settings('streamMusic') == "true"
self.enableimportsongrating = settings('enableImportSongRating') == "true"
self.enableexportsongrating = settings('enableExportSongRating') == "true"
self.enableupdatesongrating = settings('enableUpdateSongRating') == "true"
self.userid = window('emby_currUser')
self.server = window('emby_server%s' % self.userid)
Items.__init__(self)
def _get_func(self, item_type, action):
if item_type == "MusicAlbum":
actions = {
'added': self.added_album,
'update': self.add_updateAlbum,
'userdata': self.updateUserdata,
'remove': self.remove
}
elif item_type in ("MusicArtist", "AlbumArtist"):
actions = {
'added': self.added,
'update': self.add_updateArtist,
'remove': self.remove
}
elif item_type == "Audio":
actions = {
'added': self.added_song,
'update': self.add_updateSong,
'userdata': self.updateUserdata,
'remove': self.remove
}
else:
log.info("Unsupported item_type: %s", item_type)
actions = {}
return actions.get(action)
def compare_all(self):
# Pull the list of artists, albums, songs
views = self.emby_db.getView_byType('music')
for view in views:
# Process artists
self.compare_artists(view)
# Process albums
self.compare_albums()
# Process songs
self.compare_songs()
return True
def compare_artists(self, view):
all_embyartistsIds = set()
update_list = list()
if self.pdialog:
self.pdialog.update(heading=lang(29999), message="%s Artists..." % lang(33031))
artists = dict(self.emby_db.get_checksum('MusicArtist'))
album_artists = dict(self.emby_db.get_checksum('AlbumArtist'))
emby_artists = self.emby.getArtists(view['id'], dialog=self.pdialog)
for item in emby_artists['Items']:
if self.should_stop():
return False
item_id = item['Id']
API = api.API(item)
all_embyartistsIds.add(item_id)
if item_id in artists:
if artists[item_id] != API.get_checksum():
# Only update if artist is not in Kodi or checksum is different
update_list.append(item_id)
elif album_artists.get(item_id) != API.get_checksum():
# Only update if artist is not in Kodi or checksum is different
update_list.append(item_id)
#compare_to.pop(item_id, None)
log.info("Update for Artist: %s", update_list)
emby_items = self.emby.getFullItems(update_list)
total = len(update_list)
if self.pdialog:
self.pdialog.update(heading="Processing Artists / %s items" % total)
# Process additions and updates
if emby_items:
self.process_all("MusicArtist", "update", emby_items, total)
# Process removals
for artist in artists:
if artist not in all_embyartistsIds and artists[artist] is not None:
self.remove(artist)
def compare_albums(self):
if self.pdialog:
self.pdialog.update(heading=lang(29999), message="%s Albums..." % lang(33031))
albums = dict(self.emby_db.get_checksum('MusicAlbum'))
emby_albums = self.emby.getAlbums(basic=True, dialog=self.pdialog)
return self.compare("MusicAlbum", emby_albums['Items'], albums)
def compare_songs(self):
if self.pdialog:
self.pdialog.update(heading=lang(29999), message="%s Songs..." % lang(33031))
songs = dict(self.emby_db.get_checksum('Audio'))
emby_songs = self.emby.getSongs(basic=True, dialog=self.pdialog)
return self.compare("Audio", emby_songs['Items'], songs)
def added(self, items, total=None):
for item in super(Music, self).added(items, total):
if self.add_updateArtist(item):
# Add albums
all_albums = self.emby.getAlbumsbyArtist(item['Id'])
self.added_album(all_albums['Items'])
def added_album(self, items, total=None):
update = True if not self.total else False
for item in super(Music, self).added(items, total, update):
self.title = "%s - %s" % (item.get('AlbumArtist', "unknown"), self.title)
if self.add_updateAlbum(item):
# Add songs
all_songs = self.emby.getSongsbyAlbum(item['Id'])
self.added_song(all_songs['Items'])
def added_song(self, items, total=None):
update = True if not self.total else False
for item in super(Music, self).added(items, total, update):
self.title = "%s - %s" % (item.get('AlbumArtist', "unknown"), self.title)
if self.add_updateSong(item):
self.content_pop(self.title)
@catch_except()
def add_updateArtist(self, item, artisttype="MusicArtist"):
# Process a single artist
kodicursor = self.kodicursor
emby_db = self.emby_db
artwork = self.artwork
API = api.API(item)
update_item = True
itemid = item['Id']
emby_dbitem = emby_db.getItem_byId(itemid)
try:
artistid = emby_dbitem[0]
except TypeError:
update_item = False
log.debug("artistid: %s not found", itemid)
else:
pass
##### The artist details #####
lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
dateadded = API.get_date_created()
checksum = API.get_checksum()
name = item['Name']
musicBrainzId = API.get_provider('MusicBrainzArtist')
genres = " / ".join(item.get('Genres'))
bio = API.get_overview()
# Associate artwork
artworks = artwork.get_all_artwork(item, parent_info=True)
thumb = artworks['Primary']
backdrops = artworks['Backdrop'] # List
if thumb:
thumb = "<thumb>%s</thumb>" % thumb
if backdrops:
fanart = "<fanart>%s</fanart>" % backdrops[0]
else:
fanart = ""
##### UPDATE THE ARTIST #####
if update_item:
log.info("UPDATE artist itemid: %s - Name: %s", itemid, name)
# Update the checksum in emby table
emby_db.updateReference(itemid, checksum)
##### OR ADD THE ARTIST #####
else:
log.info("ADD artist itemid: %s - Name: %s", itemid, name)
# safety checks: It looks like Emby supports the same artist multiple times.
# Kodi doesn't allow that. In case that happens we just merge the artist entries.
artistid = self.kodi_db.get_artist(name, musicBrainzId)
# Create the reference in emby table
emby_db.addReference(itemid, artistid, artisttype, "artist", checksum=checksum)
# Process the artist
if self.kodi_version > 15:
self.kodi_db.update_artist_16(genres, bio, thumb, fanart, lastScraped, artistid)
else:
self.kodi_db.update_artist(genres, bio, thumb, fanart, lastScraped, dateadded, artistid)
# Update artwork
artwork.add_artwork(artworks, artistid, "artist", kodicursor)
return True
@catch_except()
def add_updateAlbum(self, item):
# Process a single artist
emby = self.emby
kodicursor = self.kodicursor
emby_db = self.emby_db
artwork = self.artwork
API = api.API(item)
update_item = True
itemid = item['Id']
emby_dbitem = emby_db.getItem_byId(itemid)
try:
albumid = emby_dbitem[0]
except TypeError:
update_item = False
log.debug("albumid: %s not found", itemid)
##### The album details #####
lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
dateadded = API.get_date_created()
userdata = API.get_userdata()
checksum = API.get_checksum()
name = item['Name']
musicBrainzId = API.get_provider('MusicBrainzAlbum')
year = item.get('ProductionYear')
genres = item.get('Genres')
genre = " / ".join(genres)
bio = API.get_overview()
rating = 0
artists = item['AlbumArtists']
artistname = []
for artist in artists:
artistname.append(artist['Name'])
artistname = " / ".join(artistname)
# Associate artwork
artworks = artwork.get_all_artwork(item, parent_info=True)
thumb = artworks['Primary']
if thumb:
thumb = "<thumb>%s</thumb>" % thumb
##### UPDATE THE ALBUM #####
if update_item:
log.info("UPDATE album itemid: %s - Name: %s", itemid, name)
# Update the checksum in emby table
emby_db.updateReference(itemid, checksum)
##### OR ADD THE ALBUM #####
else:
log.info("ADD album itemid: %s - Name: %s", itemid, name)
# safety checks: It looks like Emby supports the same artist multiple times.
# Kodi doesn't allow that. In case that happens we just merge the artist entries.
albumid = self.kodi_db.get_album(name, musicBrainzId)
# Create the reference in emby table
emby_db.addReference(itemid, albumid, "MusicAlbum", "album", checksum=checksum)
# Process the album info
if self.kodi_version == 17:
# Kodi Krypton
self.kodi_db.update_album_17(artistname, year, genre, bio, thumb, rating, lastScraped,
"album", albumid)
elif self.kodi_version == 16:
# Kodi Jarvis
self.kodi_db.update_album(artistname, year, genre, bio, thumb, rating, lastScraped,
"album", albumid)
elif self.kodi_version == 15:
# Kodi Isengard
self.kodi_db.update_album_15(artistname, year, genre, bio, thumb, rating, lastScraped,
dateadded, "album", albumid)
else:
# TODO: Remove Helix code when Krypton is RC
self.kodi_db.update_album_14(artistname, year, genre, bio, thumb, rating, lastScraped,
dateadded, albumid)
# Assign main artists to album
for artist in item['AlbumArtists']:
artistname = artist['Name']
artistId = artist['Id']
emby_dbartist = emby_db.getItem_byId(artistId)
try:
artistid = emby_dbartist[0]
except TypeError:
# Artist does not exist in emby database, create the reference
artist = emby.getItem(artistId)
self.add_updateArtist(artist, artisttype="AlbumArtist")
emby_dbartist = emby_db.getItem_byId(artistId)
artistid = emby_dbartist[0]
else:
# Best take this name over anything else.
self.kodi_db.update_artist_name(artistid, artistname)
# Add artist to album
self.kodi_db.link_artist(artistid, albumid, artistname)
# Update emby reference with parentid
emby_db.updateParentId(artistId, albumid)
for artist in item['ArtistItems']:
artistId = artist['Id']
emby_dbartist = emby_db.getItem_byId(artistId)
try:
artistid = emby_dbartist[0]
except TypeError:
pass
else:
# Update discography
self.kodi_db.add_discography(artistid, name, year)
# Add genres
self.kodi_db.add_genres(albumid, genres, "album")
# Update artwork
artwork.add_artwork(artworks, albumid, "album", kodicursor)
return True
@catch_except()
def add_updateSong(self, item):
# Process single song
kodicursor = self.kodicursor
emby = self.emby
emby_db = self.emby_db
artwork = self.artwork
API = api.API(item)
update_item = True
itemid = item['Id']
emby_dbitem = emby_db.getItem_byId(itemid)
try:
songid = emby_dbitem[0]
pathid = emby_dbitem[2]
albumid = emby_dbitem[3]
except TypeError:
update_item = False
log.debug("songid: %s not found", itemid)
songid = self.kodi_db.create_entry_song()
##### The song details #####
checksum = API.get_checksum()
dateadded = API.get_date_created()
userdata = API.get_userdata()
playcount = userdata['PlayCount']
dateplayed = userdata['LastPlayedDate']
# item details
title = item['Name']
musicBrainzId = API.get_provider('MusicBrainzTrackId')
genres = item.get('Genres')
genre = " / ".join(genres)
artists = " / ".join(item['Artists'])
tracknumber = item.get('IndexNumber', 0)
disc = item.get('ParentIndexNumber', 1)
if disc == 1:
track = tracknumber
else:
track = disc*2**16 + tracknumber
year = item.get('ProductionYear')
duration = API.get_runtime()
rating = 0
#if enabled, try to get the rating from file and/or emby
if not self.directstream:
rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating)
else:
hasEmbeddedCover = False
comment = API.get_overview()
##### GET THE FILE AND PATH #####
if self.directstream:
path = "%s/emby/Audio/%s/" % (self.server, itemid)
extensions = ['mp3', 'aac', 'ogg', 'oga', 'webma', 'wma', 'flac']
if 'Container' in item and item['Container'].lower() in extensions:
filename = "stream.%s?static=true" % item['Container']
else:
filename = "stream.mp3?static=true"
else:
playurl = API.get_file_path()
if "\\" in playurl:
# Local path
filename = playurl.rsplit("\\", 1)[1]
else: # Network share
filename = playurl.rsplit("/", 1)[1]
# Direct paths is set the Kodi way
if not self.path_validation(playurl):
return False
path = playurl.replace(filename, "")
window('emby_pathverified', value="true")
##### UPDATE THE SONG #####
if update_item:
log.info("UPDATE song itemid: %s - Title: %s", itemid, title)
# Update path
self.kodi_db.update_path(pathid, path)
# Update the song entry
self.kodi_db.update_song(albumid, artists, genre, title, track, duration, year,
filename, playcount, dateplayed, rating, comment, songid)
# Update the checksum in emby table
emby_db.updateReference(itemid, checksum)
##### OR ADD THE SONG #####
else:
log.info("ADD song itemid: %s - Title: %s", itemid, title)
# Add path
pathid = self.kodi_db.add_path(path)
try:
# Get the album
emby_dbalbum = emby_db.getItem_byId(item['AlbumId'])
albumid = emby_dbalbum[0]
except KeyError:
# Verify if there's an album associated.
album_name = item.get('Album')
if album_name:
log.info("Creating virtual music album for song: %s", itemid)
albumid = self.kodi_db.get_album(album_name, API.get_provider('MusicBrainzAlbum'))
emby_db.addReference("%salbum%s" % (itemid, albumid), albumid, "MusicAlbum_", "album")
else:
# No album Id associated to the song.
log.error("Song itemid: %s has no albumId associated", itemid)
return False
except TypeError:
# No album found. Let's create it
log.info("Album database entry missing.")
emby_albumId = item['AlbumId']
album = emby.getItem(emby_albumId)
self.add_updateAlbum(album)
emby_dbalbum = emby_db.getItem_byId(emby_albumId)
try:
albumid = emby_dbalbum[0]
log.info("Found albumid: %s", albumid)
except TypeError:
# No album found, create a single's album
log.info("Failed to add album. Creating singles.")
album_id = self.kodi_db.create_entry_album()
if self.kodi_version == 16:
self.kodi_db.add_single(albumid, genre, year, "single")
elif self.kodi_version == 15:
self.kodi_db.add_single_15(albumid, genre, year, dateadded, "single")
else:
# TODO: Remove Helix code when Krypton is RC
self.kodi_db.add_single_14(albumid, genre, year, dateadded)
# Create the song entry
self.kodi_db.add_song(songid, albumid, pathid, artists, genre, title, track, duration,
year, filename, musicBrainzId, playcount, dateplayed, rating)
# Create the reference in emby table
emby_db.addReference(itemid, songid, "Audio", "song", pathid=pathid, parentid=albumid,
checksum=checksum)
# Link song to album
self.kodi_db.link_song_album(songid, albumid, track, title, duration)
# Create default role
if self.kodi_version > 16:
self.kodi_db.add_role()
# Link song to artists
for index, artist in enumerate(item['ArtistItems']):
artist_name = artist['Name']
artist_eid = artist['Id']
artist_edb = emby_db.getItem_byId(artist_eid)
try:
artistid = artist_edb[0]
except TypeError:
# Artist is missing from emby database, add it.
artist_full = emby.getItem(artist_eid)
self.add_updateArtist(artist_full)
artist_edb = emby_db.getItem_byId(artist_eid)
artistid = artist_edb[0]
finally:
# Link song to artist
self.kodi_db.link_song_artist(artistid, songid, index, artist_name)
# Verify if album artist exists
album_artists = []
for artist in item['AlbumArtists']:
artist_name = artist['Name']
album_artists.append(artist_name)
artist_eid = artist['Id']
artist_edb = emby_db.getItem_byId(artist_eid)
try:
artistid = artist_edb[0]
except TypeError:
# Artist is missing from emby database, add it.
artist_full = emby.getItem(artist_eid)
self.add_updateArtist(artist_full)
artist_edb = emby_db.getItem_byId(artist_eid)
artistid = artist_edb[0]
finally:
# Link artist to album
self.kodi_db.link_artist(artistid, albumid, artist_name)
# Update discography
if item.get('Album'):
self.kodi_db.add_discography(artistid, item['Album'], 0)
# Artist names
album_artists = " / ".join(album_artists)
self.kodi_db.get_album_artist(albumid, album_artists)
# Add genres
self.kodi_db.add_genres(songid, genres, "song")
# Update artwork
allart = artwork.get_all_artwork(item, parent_info=True)
if hasEmbeddedCover:
allart["Primary"] = "image://music@" + artwork.single_urlencode(playurl)
artwork.add_artwork(allart, songid, "song", kodicursor)
if item.get('AlbumId') is None:
# Update album artwork
artwork.add_artwork(allart, albumid, "album", kodicursor)
return True
def updateUserdata(self, item):
# This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
# Poster with progress bar
kodicursor = self.kodicursor
emby_db = self.emby_db
API = api.API(item)
# Get emby information
itemid = item['Id']
checksum = API.get_checksum()
userdata = API.get_userdata()
rating = 0
# Get Kodi information
emby_dbitem = emby_db.getItem_byId(itemid)
try:
kodiid = emby_dbitem[0]
mediatype = emby_dbitem[4]
log.info("Update playstate for %s: %s", mediatype, item['Name'])
except TypeError:
return
if mediatype == "song":
#should we ignore this item ?
#happens when userdata updated by ratings method
if window("ignore-update-%s" %itemid):
window("ignore-update-%s" %itemid,clear=True)
return
# Process playstates
playcount = userdata['PlayCount']
dateplayed = userdata['LastPlayedDate']
#process item ratings
rating, comment, hasEmbeddedCover = musicutils.getAdditionalSongTags(itemid, rating, API, kodicursor, emby_db, self.enableimportsongrating, self.enableexportsongrating, self.enableupdatesongrating)
self.kodi_db.rate_song(playcount, dateplayed, rating, kodiid)
emby_db.updateReference(itemid, checksum)
def remove(self, itemid):
# Remove kodiid, fileid, pathid, emby reference
emby_db = self.emby_db
emby_dbitem = emby_db.getItem_byId(itemid)
try:
kodiid = emby_dbitem[0]
mediatype = emby_dbitem[4]
log.info("Removing %s kodiid: %s", mediatype, kodiid)
except TypeError:
return
##### PROCESS ITEM #####
# Remove the emby reference
emby_db.removeItem(itemid)
##### IF SONG #####
if mediatype == "song":
# Delete song
self.removeSong(kodiid)
# This should only address single song scenario, where server doesn't actually
# create an album for the song.
emby_db.removeWildItem(itemid)
for item in emby_db.getItem_byWildId(itemid):
item_kid = item[0]
item_mediatype = item[1]
if item_mediatype == "album":
childs = emby_db.getItem_byParentId(item_kid, "song")
if not childs:
# Delete album
self.removeAlbum(item_kid)
##### IF ALBUM #####
elif mediatype == "album":
# Delete songs, album
album_songs = emby_db.getItem_byParentId(kodiid, "song")
for song in album_songs:
self.removeSong(song[1])
else:
# Remove emby songs
emby_db.removeItems_byParentId(kodiid, "song")
# Remove the album
self.removeAlbum(kodiid)
##### IF ARTIST #####
elif mediatype == "artist":
# Delete songs, album, artist
albums = emby_db.getItem_byParentId(kodiid, "album")
for album in albums:
albumid = album[1]
album_songs = emby_db.getItem_byParentId(albumid, "song")
for song in album_songs:
self.removeSong(song[1])
else:
# Remove emby song
emby_db.removeItems_byParentId(albumid, "song")
# Remove emby artist
emby_db.removeItems_byParentId(albumid, "artist")
# Remove kodi album
self.removeAlbum(albumid)
else:
# Remove emby albums
emby_db.removeItems_byParentId(kodiid, "album")
# Remove artist
self.removeArtist(kodiid)
log.info("Deleted %s: %s from kodi database", mediatype, itemid)
def removeSong(self, kodi_id):
self.artwork.delete_artwork(kodi_id, "song", self.kodicursor)
self.kodi_db.remove_song(kodi_id)
def removeAlbum(self, kodi_id):
self.artwork.delete_artwork(kodi_id, "album", self.kodicursor)
self.kodi_db.remove_album(kodi_id)
def removeArtist(self, kodi_id):
self.artwork.delete_artwork(kodi_id, "artist", self.kodicursor)
self.kodi_db.remove_artist(kodi_id)

View file

@ -0,0 +1,299 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import urllib
import api
import embydb_functions as embydb
import _kodi_musicvideos
from _common import Items, catch_except
from utils import window, settings, language as lang
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class MusicVideos(Items):
def __init__(self, embycursor, kodicursor, pdialog=None):
self.embycursor = embycursor
self.emby_db = embydb.Embydb_Functions(self.embycursor)
self.kodicursor = kodicursor
self.kodi_db = _kodi_musicvideos.KodiMusicVideos(self.kodicursor)
self.pdialog = pdialog
self.new_time = int(settings('newvideotime'))*1000
Items.__init__(self)
def _get_func(self, item_type, action):
if item_type == "MusicVideo":
actions = {
'added': self.added,
'update': self.add_update,
'userdata': self.updateUserdata,
'remove': self.remove
}
else:
log.info("Unsupported item_type: %s", item_type)
actions = {}
return actions.get(action)
def compare_all(self):
# Pull the list of musicvideos in Kodi
views = self.emby_db.getView_byType('musicvideos')
log.info("Media folders: %s", views)
for view in views:
if self.should_stop():
return False
if not self.compare_mvideos(view):
return False
return True
def compare_mvideos(self, view):
view_id = view['id']
view_name = view['name']
if self.pdialog:
self.pdialog.update(heading=lang(29999), message="%s %s..." % (lang(33028), view_name))
mvideos = dict(self.emby_db.get_checksum_by_view('MusicVideo', view_id))
emby_mvideos = self.emby.getMusicVideos(view_id, basic=True, dialog=self.pdialog)
return self.compare("MusicVideo", emby_mvideos['Items'], mvideos, view)
def added(self, items, total=None, view=None):
for item in super(MusicVideos, self).added(items, total):
if self.add_update(item, view):
self.content_pop(item.get('Name', "unknown"))
@catch_except()
def add_update(self, item, view=None):
# Process single music video
kodicursor = self.kodicursor
emby_db = self.emby_db
artwork = self.artwork
API = api.API(item)
# If the item already exist in the local Kodi DB we'll perform a full item update
# If the item doesn't exist, we'll add it to the database
update_item = True
itemid = item['Id']
emby_dbitem = emby_db.getItem_byId(itemid)
try:
mvideoid = emby_dbitem[0]
fileid = emby_dbitem[1]
pathid = emby_dbitem[2]
log.info("mvideoid: %s fileid: %s pathid: %s", mvideoid, fileid, pathid)
except TypeError:
update_item = False
log.debug("mvideoid: %s not found", itemid)
# mvideoid
mvideoid = self.kodi_db.create_entry()
else:
if self.kodi_db.get_musicvideo(mvideoid) is None:
# item is not found, let's recreate it.
update_item = False
log.info("mvideoid: %s missing from Kodi, repairing the entry.", mvideoid)
if not view:
# Get view tag from emby
viewtag, viewid, mediatype = self.emby.getView_embyId(itemid)
log.debug("View tag found: %s", viewtag)
else:
viewtag = view['name']
viewid = view['id']
# fileId information
checksum = API.get_checksum()
dateadded = API.get_date_created()
userdata = API.get_userdata()
playcount = userdata['PlayCount']
dateplayed = userdata['LastPlayedDate']
# item details
runtime = API.get_runtime()
plot = API.get_overview()
title = item['Name']
year = item.get('ProductionYear')
genres = item['Genres']
genre = " / ".join(genres)
studios = API.get_studios()
studio = " / ".join(studios)
artist = " / ".join(item.get('Artists'))
album = item.get('Album')
track = item.get('Track')
people = API.get_people()
director = " / ".join(people['Director'])
##### GET THE FILE AND PATH #####
playurl = API.get_file_path()
if "\\" in playurl:
# Local path
filename = playurl.rsplit("\\", 1)[1]
else: # Network share
filename = playurl.rsplit("/", 1)[1]
if self.direct_path:
# Direct paths is set the Kodi way
if not self.path_validation(playurl):
return False
path = playurl.replace(filename, "")
window('emby_pathverified', value="true")
else:
# Set plugin path and media flags using real filename
path = "plugin://plugin.video.emby.musicvideos/"
params = {
'filename': filename.encode('utf-8'),
'id': itemid,
'dbid': mvideoid,
'mode': "play"
}
filename = "%s?%s" % (path, urllib.urlencode(params))
##### UPDATE THE MUSIC VIDEO #####
if update_item:
log.info("UPDATE mvideo itemid: %s - Title: %s", itemid, title)
# Update the music video entry
self.kodi_db.update_musicvideo(title, runtime, director, studio, year, plot, album,
artist, genre, track, mvideoid)
# Update the checksum in emby table
emby_db.updateReference(itemid, checksum)
##### OR ADD THE MUSIC VIDEO #####
else:
log.info("ADD mvideo itemid: %s - Title: %s", itemid, title)
# Add path
pathid = self.kodi_db.add_path(path)
# Add the file
fileid = self.kodi_db.add_file(filename, pathid)
# Create the musicvideo entry
self.kodi_db.add_musicvideo(mvideoid, fileid, title, runtime, director, studio,
year, plot, album, artist, genre, track)
# Create the reference in emby table
emby_db.addReference(itemid, mvideoid, "MusicVideo", "musicvideo", fileid, pathid,
checksum=checksum, mediafolderid=viewid)
# Update the path
self.kodi_db.update_path(pathid, path, "musicvideos", "metadata.local")
# Update the file
self.kodi_db.update_file(fileid, filename, pathid, dateadded)
# Process cast
people = item['People']
artists = item['ArtistItems']
for artist in artists:
artist['Type'] = "Artist"
people.extend(artists)
people = artwork.get_people_artwork(people)
self.kodi_db.add_people(mvideoid, people, "musicvideo")
# Process genres
self.kodi_db.add_genres(mvideoid, genres, "musicvideo")
# Process artwork
artwork.add_artwork(artwork.get_all_artwork(item), mvideoid, "musicvideo", kodicursor)
# Process stream details
streams = API.get_media_streams()
self.kodi_db.add_streams(fileid, streams, runtime)
# Process studios
self.kodi_db.add_studios(mvideoid, studios, "musicvideo")
# Process tags: view, emby tags
tags = [viewtag]
tags.extend(item['Tags'])
if userdata['Favorite']:
tags.append("Favorite musicvideos")
self.kodi_db.add_tags(mvideoid, tags, "musicvideo")
# Process playstates
resume = API.adjust_resume(userdata['Resume'])
total = round(float(runtime), 6)
self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed)
return True
def updateUserdata(self, item):
# This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
# Poster with progress bar
emby_db = self.emby_db
API = api.API(item)
# Get emby information
itemid = item['Id']
checksum = API.get_checksum()
userdata = API.get_userdata()
runtime = API.get_runtime()
# Get Kodi information
emby_dbitem = emby_db.getItem_byId(itemid)
try:
mvideoid = emby_dbitem[0]
fileid = emby_dbitem[1]
log.info("Update playstate for musicvideo: %s fileid: %s", item['Name'], fileid)
except TypeError:
return
# Process favorite tags
if userdata['Favorite']:
self.kodi_db.get_tag(mvideoid, "Favorite musicvideos", "musicvideo")
else:
self.kodi_db.remove_tag(mvideoid, "Favorite musicvideos", "musicvideo")
# Process playstates
playcount = userdata['PlayCount']
dateplayed = userdata['LastPlayedDate']
resume = API.adjust_resume(userdata['Resume'])
total = round(float(runtime), 6)
self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed)
emby_db.updateReference(itemid, checksum)
def remove(self, itemid):
# Remove mvideoid, fileid, pathid, emby reference
emby_db = self.emby_db
kodicursor = self.kodicursor
artwork = self.artwork
emby_dbitem = emby_db.getItem_byId(itemid)
try:
mvideoid = emby_dbitem[0]
fileid = emby_dbitem[1]
pathid = emby_dbitem[2]
log.info("Removing mvideoid: %s fileid: %s pathid: %s", mvideoid, fileid, pathid)
except TypeError:
return
# Remove the emby reference
emby_db.removeItem(itemid)
# Remove artwork
artwork.delete_artwork(mvideoid, "musicvideo", self.kodicursor)
self.kodi_db.remove_musicvideo(mvideoid, fileid)
if self.direct_path:
self.kodi_db.remove_path(pathid)
log.info("Deleted musicvideo %s from kodi database", itemid)

View file

@ -0,0 +1,818 @@
# -*- coding: utf-8 -*-
##################################################################################################
import logging
import urllib
from ntpath import dirname
import api
import embydb_functions as embydb
import _kodi_tvshows
from _common import Items, catch_except
from utils import window, settings, language as lang
##################################################################################################
log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class TVShows(Items):
def __init__(self, embycursor, kodicursor, pdialog=None):
self.embycursor = embycursor
self.emby_db = embydb.Embydb_Functions(self.embycursor)
self.kodicursor = kodicursor
self.kodi_db = _kodi_tvshows.KodiTVShows(self.kodicursor)
self.pdialog = pdialog
self.new_time = int(settings('newvideotime'))*1000
Items.__init__(self)
def _get_func(self, item_type, action):
if item_type == "Series":
actions = {
'added': self.added,
'update': self.add_update,
'userdata': self.updateUserdata,
'remove': self.remove
}
elif item_type == "Season":
actions = {
'added': self.added_season,
'update': self.add_updateSeason,
'remove': self.remove
}
elif item_type == "Episode":
actions = {
'added': self.added_episode,
'update': self.add_updateEpisode,
'userdata': self.updateUserdata,
'remove': self.remove
}
else:
log.info("Unsupported item_type: %s", item_type)
actions = {}
return actions.get(action)
def compare_all(self):
# Pull the list of movies and boxsets in Kodi
pdialog = self.pdialog
views = self.emby_db.getView_byType('tvshows')
views += self.emby_db.getView_byType('mixed')
log.info("Media folders: %s", views)
# Pull the list of tvshows and episodes in Kodi
try:
all_koditvshows = dict(self.emby_db.get_checksum('Series'))
except ValueError:
all_koditvshows = {}
log.info("all_koditvshows = %s", all_koditvshows)
try:
all_kodiepisodes = dict(self.emby_db.get_checksum('Episode'))
except ValueError:
all_kodiepisodes = {}
all_embytvshowsIds = set()
all_embyepisodesIds = set()
updatelist = []
# TODO: Review once series pooling is explicitely returned in api
for view in views:
if self.should_stop():
return False
# Get items per view
viewId = view['id']
viewName = view['name']
if pdialog:
pdialog.update(
heading=lang(29999),
message="%s %s..." % (lang(33029), viewName))
all_embytvshows = self.emby.getShows(viewId, basic=True, dialog=pdialog)
for embytvshow in all_embytvshows['Items']:
if self.should_stop():
return False
API = api.API(embytvshow)
itemid = embytvshow['Id']
all_embytvshowsIds.add(itemid)
if all_koditvshows.get(itemid) != API.get_checksum():
# Only update if movie is not in Kodi or checksum is different
updatelist.append(itemid)
log.info("TVShows to update for %s: %s", viewName, updatelist)
embytvshows = self.emby.getFullItems(updatelist)
self.total = len(updatelist)
del updatelist[:]
if pdialog:
pdialog.update(heading="Processing %s / %s items" % (viewName, self.total))
self.count = 0
for embytvshow in embytvshows:
# Process individual show
if self.should_stop():
return False
itemid = embytvshow['Id']
title = embytvshow['Name']
all_embytvshowsIds.add(itemid)
self.update_pdialog()
self.add_update(embytvshow, view)
self.count += 1
else:
# Get all episodes in view
if pdialog:
pdialog.update(
heading=lang(29999),
message="%s %s..." % (lang(33030), viewName))
all_embyepisodes = self.emby.getEpisodes(viewId, basic=True, dialog=pdialog)
for embyepisode in all_embyepisodes['Items']:
if self.should_stop():
return False
API = api.API(embyepisode)
itemid = embyepisode['Id']
all_embyepisodesIds.add(itemid)
if "SeriesId" in embyepisode:
all_embytvshowsIds.add(embyepisode['SeriesId'])
if all_kodiepisodes.get(itemid) != API.get_checksum():
# Only update if movie is not in Kodi or checksum is different
updatelist.append(itemid)
log.info("Episodes to update for %s: %s", viewName, updatelist)
embyepisodes = self.emby.getFullItems(updatelist)
self.total = len(updatelist)
del updatelist[:]
self.count = 0
for episode in embyepisodes:
# Process individual episode
if self.should_stop():
return False
self.title = "%s - %s" % (episode.get('SeriesName', "Unknown"), episode['Name'])
self.add_updateEpisode(episode)
self.count += 1
##### PROCESS DELETES #####
log.info("all_embytvshowsIds = %s ", all_embytvshowsIds)
for koditvshow in all_koditvshows:
if koditvshow not in all_embytvshowsIds:
self.remove(koditvshow)
log.info("TVShows compare finished.")
for kodiepisode in all_kodiepisodes:
if kodiepisode not in all_embyepisodesIds:
self.remove(kodiepisode)
log.info("Episodes compare finished.")
return True
def added(self, items, total=None, view=None):
for item in super(TVShows, self).added(items, total):
if self.add_update(item, view):
# Add episodes
all_episodes = self.emby.getEpisodesbyShow(item['Id'])
self.added_episode(all_episodes['Items'])
def added_season(self, items, total=None, view=None):
update = True if not self.total else False
for item in super(TVShows, self).added(items, total, update):
self.title = "%s - %s" % (item.get('SeriesName', "Unknown"), self.title)
if self.add_updateSeason(item):
# Add episodes
all_episodes = self.emby.getEpisodesbySeason(item['Id'])
self.added_episode(all_episodes['Items'])
def added_episode(self, items, total=None, view=None):
update = True if not self.total else False
for item in super(TVShows, self).added(items, total, update):
self.title = "%s - %s" % (item.get('SeriesName', "Unknown"), self.title)
if self.add_updateEpisode(item):
self.content_pop(self.title)
@catch_except()
def add_update(self, item, view=None):
# Process single tvshow
kodicursor = self.kodicursor
emby = self.emby
emby_db = self.emby_db
artwork = self.artwork
API = api.API(item)
if settings('syncEmptyShows') == "false" and not item.get('RecursiveItemCount'):
log.info("Skipping empty show: %s", item['Name'])
return
# If the item already exist in the local Kodi DB we'll perform a full item update
# If the item doesn't exist, we'll add it to the database
update_item = True
force_episodes = False
itemid = item['Id']
emby_dbitem = emby_db.getItem_byId(itemid)
try:
showid = emby_dbitem[0]
pathid = emby_dbitem[2]
log.info("showid: %s pathid: %s", showid, pathid)
except TypeError:
update_item = False
log.debug("showid: %s not found", itemid)
showid = self.kodi_db.create_entry()
else:
# Verification the item is still in Kodi
if self.kodi_db.get_tvshow(showid) is None:
# item is not found, let's recreate it.
update_item = False
log.info("showid: %s missing from Kodi, repairing the entry", showid)
# Force re-add episodes after the show is re-created.
force_episodes = True
if view is None:
# Get view tag from emby
viewtag, viewid, mediatype = emby.getView_embyId(itemid)
log.debug("View tag found: %s", viewtag)
else:
viewtag = view['name']
viewid = view['id']
# fileId information
checksum = API.get_checksum()
userdata = API.get_userdata()
# item details
genres = item['Genres']
title = item['Name']
plot = API.get_overview()
rating = item.get('CommunityRating')
premieredate = API.get_premiere_date()
tvdb = API.get_provider('Tvdb')
sorttitle = item['SortName']
mpaa = API.get_mpaa()
genre = " / ".join(genres)
studios = API.get_studios()
studio = " / ".join(studios)
# Verify series pooling
if not update_item and tvdb:
query = "SELECT idShow FROM tvshow WHERE C12 = ?"
kodicursor.execute(query, (tvdb,))
try:
temp_showid = kodicursor.fetchone()[0]
except TypeError:
pass
else:
emby_other = emby_db.getItem_byKodiId(temp_showid, "tvshow")
if emby_other and viewid == emby_other[2]:
log.info("Applying series pooling for %s", title)
emby_other_item = emby_db.getItem_byId(emby_other[0])
showid = emby_other_item[0]
pathid = emby_other_item[2]
log.info("showid: %s pathid: %s", showid, pathid)
# Create the reference in emby table
emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid,
checksum=checksum, mediafolderid=viewid)
update_item = True
##### GET THE FILE AND PATH #####
playurl = API.get_file_path()
if self.direct_path:
# Direct paths is set the Kodi way
if "\\" in playurl:
# Local path
path = "%s\\" % playurl
toplevelpath = "%s\\" % dirname(dirname(path))
else:
# Network path
path = "%s/" % playurl
toplevelpath = "%s/" % dirname(dirname(path))
if not self.path_validation(path):
return False
window('emby_pathverified', value="true")
else:
# Set plugin path
toplevelpath = "plugin://plugin.video.emby.tvshows/"
path = "%s%s/" % (toplevelpath, itemid)
##### UPDATE THE TVSHOW #####
if update_item:
log.info("UPDATE tvshow itemid: %s - Title: %s", itemid, title)
# Update the tvshow entry
self.kodi_db.update_tvshow(title, plot, rating, premieredate, genre, title,
tvdb, mpaa, studio, sorttitle, showid)
# Update the checksum in emby table
emby_db.updateReference(itemid, checksum)
##### OR ADD THE TVSHOW #####
else:
log.info("ADD tvshow itemid: %s - Title: %s", itemid, title)
# Add top path
toppathid = self.kodi_db.add_path(toplevelpath)
self.kodi_db.update_path(toppathid, toplevelpath, "tvshows", "metadata.local")
# Add path
pathid = self.kodi_db.add_path(path)
# Create the tvshow entry
self.kodi_db.add_tvshow(showid, title, plot, rating, premieredate, genre,
title, tvdb, mpaa, studio, sorttitle)
# Create the reference in emby table
emby_db.addReference(itemid, showid, "Series", "tvshow", pathid=pathid,
checksum=checksum, mediafolderid=viewid)
# Link the path
self.kodi_db.link_tvshow(showid, pathid)
# Update the path
self.kodi_db.update_path(pathid, path, None, None)
# Process cast
people = artwork.get_people_artwork(item['People'])
self.kodi_db.add_people(showid, people, "tvshow")
# Process genres
self.kodi_db.add_genres(showid, genres, "tvshow")
# Process artwork
artwork.add_artwork(artwork.get_all_artwork(item), showid, "tvshow", kodicursor)
# Process studios
self.kodi_db.add_studios(showid, studios, "tvshow")
# Process tags: view, emby tags
tags = [viewtag]
tags.extend(item['Tags'])
if userdata['Favorite']:
tags.append("Favorite tvshows")
self.kodi_db.add_tags(showid, tags, "tvshow")
# Process seasons
all_seasons = emby.getSeasons(itemid)
for season in all_seasons['Items']:
self.add_updateSeason(season, showid=showid)
else:
# Finally, refresh the all season entry
seasonid = self.kodi_db.get_season(showid, -1)
# Process artwork
artwork.add_artwork(artwork.get_all_artwork(item), seasonid, "season", kodicursor)
if force_episodes:
# We needed to recreate the show entry. Re-add episodes now.
log.info("Repairing episodes for showid: %s %s", showid, title)
all_episodes = emby.getEpisodesbyShow(itemid)
self.added_episode(all_episodes['Items'], None)
return True
def add_updateSeason(self, item, showid=None):
kodicursor = self.kodicursor
emby_db = self.emby_db
artwork = self.artwork
seasonnum = item.get('IndexNumber', 1)
if showid is None:
try:
seriesId = item['SeriesId']
showid = emby_db.getItem_byId(seriesId)[0]
except KeyError:
return
except TypeError:
# Show is missing, update show instead.
show = self.emby.getItem(seriesId)
self.add_update(show)
return
seasonid = self.kodi_db.get_season(showid, seasonnum, item['Name'])
if item['LocationType'] != "Virtual":
# Create the reference in emby table
emby_db.addReference(item['Id'], seasonid, "Season", "season", parentid=showid)
# Process artwork
artwork.add_artwork(artwork.get_all_artwork(item), seasonid, "season", kodicursor)
return True
@catch_except()
def add_updateEpisode(self, item):
# Process single episode
kodicursor = self.kodicursor
emby_db = self.emby_db
artwork = self.artwork
API = api.API(item)
if item.get('LocationType') == "Virtual": # TODO: Filter via api instead
log.info("Skipping virtual episode: %s", item['Name'])
return
# If the item already exist in the local Kodi DB we'll perform a full item update
# If the item doesn't exist, we'll add it to the database
update_item = True
itemid = item['Id']
emby_dbitem = emby_db.getItem_byId(itemid)
try:
episodeid = emby_dbitem[0]
fileid = emby_dbitem[1]
pathid = emby_dbitem[2]
log.info("episodeid: %s fileid: %s pathid: %s", episodeid, fileid, pathid)
except TypeError:
update_item = False
log.debug("episodeid: %s not found", itemid)
# episodeid
episodeid = self.kodi_db.create_entry_episode()
else:
# Verification the item is still in Kodi
if self.kodi_db.get_episode(episodeid) is None:
# item is not found, let's recreate it.
update_item = False
log.info("episodeid: %s missing from Kodi, repairing the entry", episodeid)
# fileId information
checksum = API.get_checksum()
dateadded = API.get_date_created()
userdata = API.get_userdata()
playcount = userdata['PlayCount']
dateplayed = userdata['LastPlayedDate']
# item details
people = API.get_people()
writer = " / ".join(people['Writer'])
director = " / ".join(people['Director'])
title = item['Name']
plot = API.get_overview()
rating = item.get('CommunityRating')
runtime = API.get_runtime()
premieredate = API.get_premiere_date()
# episode details
try:
seriesId = item['SeriesId']
except KeyError:
# Missing seriesId, skip
log.error("Skipping: %s. SeriesId is missing.", itemid)
return False
season = item.get('ParentIndexNumber')
episode = item.get('IndexNumber', -1)
if season is None:
if item.get('AbsoluteEpisodeNumber'):
# Anime scenario
season = 1
episode = item['AbsoluteEpisodeNumber']
else:
season = -1 if "Specials" not in item['Path'] else 0
# Specials ordering within season
if item.get('AirsAfterSeasonNumber'):
airsBeforeSeason = item['AirsAfterSeasonNumber']
airsBeforeEpisode = 4096 # Kodi default number for afterseason ordering
else:
airsBeforeSeason = item.get('AirsBeforeSeasonNumber')
airsBeforeEpisode = item.get('AirsBeforeEpisodeNumber')
# Append multi episodes to title
if item.get('IndexNumberEnd'):
title = "| %02d | %s" % (item['IndexNumberEnd'], title)
# Get season id
show = emby_db.getItem_byId(seriesId)
try:
showid = show[0]
except TypeError:
# Show is missing from database
show = self.emby.getItem(seriesId)
self.add_update(show)
show = emby_db.getItem_byId(seriesId)
try:
showid = show[0]
except TypeError:
log.error("Skipping: %s. Unable to add series: %s", itemid, seriesId)
return False
seasonid = self.kodi_db.get_season(showid, season)
##### GET THE FILE AND PATH #####
playurl = API.get_file_path()
if "\\" in playurl:
# Local path
filename = playurl.rsplit("\\", 1)[1]
else: # Network share
filename = playurl.rsplit("/", 1)[1]
if self.direct_path:
# Direct paths is set the Kodi way
if not self.path_validation(playurl):
return False
path = playurl.replace(filename, "")
window('emby_pathverified', value="true")
else:
# Set plugin path and media flags using real filename
path = "plugin://plugin.video.emby.tvshows/%s/" % seriesId
params = {
'filename': filename.encode('utf-8'),
'id': itemid,
'dbid': episodeid,
'mode': "play"
}
filename = "%s?%s" % (path, urllib.urlencode(params))
##### UPDATE THE EPISODE #####
if update_item:
log.info("UPDATE episode itemid: %s - Title: %s", itemid, title)
# Update the movie entry
if self.kodi_version in (16, 17):
# Kodi Jarvis, Krypton
self.kodi_db.update_episode_16(title, plot, rating, writer, premieredate, runtime,
director, season, episode, title, airsBeforeSeason,
airsBeforeEpisode, seasonid, showid, episodeid)
else:
self.kodi_db.update_episode(title, plot, rating, writer, premieredate, runtime,
director, season, episode, title, airsBeforeSeason,
airsBeforeEpisode, showid, episodeid)
# Update the checksum in emby table
emby_db.updateReference(itemid, checksum)
# Update parentid reference
emby_db.updateParentId(itemid, seasonid)
##### OR ADD THE EPISODE #####
else:
log.info("ADD episode itemid: %s - Title: %s", itemid, title)
# Add path
pathid = self.kodi_db.add_path(path)
# Add the file
fileid = self.kodi_db.add_file(filename, pathid)
# Create the episode entry
if self.kodi_version in (16, 17):
# Kodi Jarvis, Krypton
self.kodi_db.add_episode_16(episodeid, fileid, title, plot, rating, writer,
premieredate, runtime, director, season, episode, title,
showid, airsBeforeSeason, airsBeforeEpisode, seasonid)
else:
self.kodi_db.add_episode(episodeid, fileid, title, plot, rating, writer,
premieredate, runtime, director, season, episode, title,
showid, airsBeforeSeason, airsBeforeEpisode)
# Create the reference in emby table
emby_db.addReference(itemid, episodeid, "Episode", "episode", fileid, pathid,
seasonid, checksum)
# Update the path
self.kodi_db.update_path(pathid, path, None, None)
# Update the file
self.kodi_db.update_file(fileid, filename, pathid, dateadded)
# Process cast
people = artwork.get_people_artwork(item['People'])
self.kodi_db.add_people(episodeid, people, "episode")
# Process artwork
artworks = artwork.get_all_artwork(item)
artwork.add_update_art(artworks['Primary'], episodeid, "episode", "thumb", kodicursor)
# Process stream details
streams = API.get_media_streams()
self.kodi_db.add_streams(fileid, streams, runtime)
# Process playstates
resume = API.adjust_resume(userdata['Resume'])
total = round(float(runtime), 6)
self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed)
if not self.direct_path and resume:
# Create additional entry for widgets. This is only required for plugin/episode.
temppathid = self.kodi_db.get_path("plugin://plugin.video.emby.tvshows/")
tempfileid = self.kodi_db.add_file(filename, temppathid)
self.kodi_db.update_file(tempfileid, filename, temppathid, dateadded)
self.kodi_db.add_playstate(tempfileid, resume, total, playcount, dateplayed)
return True
def updateUserdata(self, item):
# This updates: Favorite, LastPlayedDate, Playcount, PlaybackPositionTicks
# Poster with progress bar
emby_db = self.emby_db
API = api.API(item)
# Get emby information
itemid = item['Id']
checksum = API.get_checksum()
userdata = API.get_userdata()
runtime = API.get_runtime()
dateadded = API.get_date_created()
# Get Kodi information
emby_dbitem = emby_db.getItem_byId(itemid)
try:
kodiid = emby_dbitem[0]
fileid = emby_dbitem[1]
mediatype = emby_dbitem[4]
log.info("Update playstate for %s: %s fileid: %s", mediatype, item['Name'], fileid)
except TypeError:
return
# Process favorite tags
if mediatype == "tvshow":
if userdata['Favorite']:
self.kodi_db.get_tag(kodiid, "Favorite tvshows", "tvshow")
else:
self.kodi_db.remove_tag(kodiid, "Favorite tvshows", "tvshow")
elif mediatype == "episode":
# Process playstates
playcount = userdata['PlayCount']
dateplayed = userdata['LastPlayedDate']
resume = API.adjust_resume(userdata['Resume'])
total = round(float(runtime), 6)
log.debug("%s New resume point: %s", itemid, resume)
self.kodi_db.add_playstate(fileid, resume, total, playcount, dateplayed)
if not self.direct_path and not resume:
# Make sure there's no other bookmarks created by widget.
filename = self.kodi_db.get_filename(fileid)
self.kodi_db.remove_file("plugin://plugin.video.emby.tvshows/", filename)
if not self.direct_path and resume:
# Create additional entry for widgets. This is only required for plugin/episode.
filename = self.kodi_db.get_filename(fileid)
temppathid = self.kodi_db.get_path("plugin://plugin.video.emby.tvshows/")
tempfileid = self.kodi_db.add_file(filename, temppathid)
self.kodi_db.update_file(tempfileid, filename, temppathid, dateadded)
self.kodi_db.add_playstate(tempfileid, resume, total, playcount, dateplayed)
emby_db.updateReference(itemid, checksum)
def remove(self, itemid):
# Remove showid, fileid, pathid, emby reference
emby_db = self.emby_db
kodicursor = self.kodicursor
emby_dbitem = emby_db.getItem_byId(itemid)
try:
kodiid = emby_dbitem[0]
fileid = emby_dbitem[1]
parentid = emby_dbitem[3]
mediatype = emby_dbitem[4]
log.info("Removing %s kodiid: %s fileid: %s", mediatype, kodiid, fileid)
except TypeError:
return
##### PROCESS ITEM #####
# Remove the emby reference
emby_db.removeItem(itemid)
##### IF EPISODE #####
if mediatype == "episode":
# Delete kodi episode and file, verify season and tvshow
self.removeEpisode(kodiid, fileid)
# Season verification
season = emby_db.getItem_byKodiId(parentid, "season")
try:
showid = season[1]
except TypeError:
return
season_episodes = emby_db.getItem_byParentId(parentid, "episode")
if not season_episodes:
self.removeSeason(parentid)
emby_db.removeItem(season[0])
# Show verification
show = emby_db.getItem_byKodiId(showid, "tvshow")
query = ' '.join((
"SELECT totalCount",
"FROM tvshowcounts",
"WHERE idShow = ?"
))
kodicursor.execute(query, (showid,))
result = kodicursor.fetchone()
if result and result[0] is None:
# There's no episodes left, delete show and any possible remaining seasons
seasons = emby_db.getItem_byParentId(showid, "season")
for season in seasons:
self.removeSeason(season[1])
else:
# Delete emby season entries
emby_db.removeItems_byParentId(showid, "season")
self.removeShow(showid)
emby_db.removeItem(show[0])
##### IF TVSHOW #####
elif mediatype == "tvshow":
# Remove episodes, seasons, tvshow
seasons = emby_db.getItem_byParentId(kodiid, "season")
for season in seasons:
seasonid = season[1]
season_episodes = emby_db.getItem_byParentId(seasonid, "episode")
for episode in season_episodes:
self.removeEpisode(episode[1], episode[2])
else:
# Remove emby episodes
emby_db.removeItems_byParentId(seasonid, "episode")
else:
# Remove emby seasons
emby_db.removeItems_byParentId(kodiid, "season")
# Remove tvshow
self.removeShow(kodiid)
##### IF SEASON #####
elif mediatype == "season":
# Remove episodes, season, verify tvshow
season_episodes = emby_db.getItem_byParentId(kodiid, "episode")
for episode in season_episodes:
self.removeEpisode(episode[1], episode[2])
else:
# Remove emby episodes
emby_db.removeItems_byParentId(kodiid, "episode")
# Remove season
self.removeSeason(kodiid)
# Show verification
seasons = emby_db.getItem_byParentId(parentid, "season")
if not seasons:
# There's no seasons, delete the show
self.removeShow(parentid)
emby_db.removeItem_byKodiId(parentid, "tvshow")
log.info("Deleted %s: %s from kodi database", mediatype, itemid)
def removeShow(self, kodiid):
kodicursor = self.kodicursor
self.artwork.delete_artwork(kodiid, "tvshow", kodicursor)
self.kodi_db.remove_tvshow(kodiid)
log.debug("Removed tvshow: %s", kodiid)
def removeSeason(self, kodiid):
kodicursor = self.kodicursor
self.artwork.delete_artwork(kodiid, "season", kodicursor)
self.kodi_db.remove_season(kodiid)
log.debug("Removed season: %s", kodiid)
def removeEpisode(self, kodiid, fileid):
kodicursor = self.kodicursor
self.artwork.delete_artwork(kodiid, "episode", kodicursor)
self.kodi_db.remove_episode(kodiid, fileid)
log.debug("Removed episode: %s", kodiid)

View file

@ -4,11 +4,15 @@
import json
import logging
import requests
import os
import shutil
import sys
import xbmc
import xbmcgui
import xbmcplugin
import xbmcvfs
import api
import artwork
@ -60,6 +64,8 @@ class PlaybackUtils():
self.setProperties(playurl, listitem)
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
# TODO: Review once Krypton is RC, no need for workaround.
############### ORGANIZE CURRENT PLAYLIST ################
homeScreen = xbmc.getCondVisibility('Window.IsActive(home)')
@ -78,8 +84,8 @@ class PlaybackUtils():
############### RESUME POINT ################
userdata = self.API.getUserData()
seektime = self.API.adjustResume(userdata['Resume'])
userdata = self.API.get_userdata()
seektime = self.API.adjust_resume(userdata['Resume'])
# We need to ensure we add the intro and additional parts only once.
# Otherwise we get a loop.
@ -94,9 +100,9 @@ class PlaybackUtils():
dummyPlaylist = True
playlist.add(playurl, listitem, index=startPos)
# Remove the original item from playlist
self.pl.removefromPlaylist(startPos+1)
self.pl.remove_from_playlist(startPos+1)
# Readd the original item to playlist - via jsonrpc so we have full metadata
self.pl.insertintoPlaylist(currentPosition+1, dbid, self.item['Type'].lower())
self.pl.insert_to_playlist(currentPosition+1, dbid, self.item['Type'].lower())
currentPosition += 1
############### -- CHECK FOR INTROS ################
@ -127,7 +133,7 @@ class PlaybackUtils():
pbutils = PlaybackUtils(intro)
pbutils.setProperties(introPlayurl, introListItem)
self.pl.insertintoPlaylist(currentPosition, url=introPlayurl)
self.pl.insert_to_playlist(currentPosition, url=introPlayurl)
introsPlaylist = True
currentPosition += 1
@ -138,7 +144,7 @@ class PlaybackUtils():
# Extend our current playlist with the actual item to play
# only if there's no playlist first
log.info("Adding main item to playlist.")
self.pl.addtoPlaylist(dbid, self.item['Type'].lower())
self.pl.add_to_playlist(dbid, self.item['Type'].lower())
# Ensure that additional parts are played after the main item
currentPosition += 1
@ -162,7 +168,7 @@ class PlaybackUtils():
pbutils.setArtwork(additionalListItem)
playlist.add(additionalPlayurl, additionalListItem, index=currentPosition)
self.pl.verifyPlaylist()
self.pl.verify_playlist()
currentPosition += 1
if dummyPlaylist:
@ -177,7 +183,7 @@ class PlaybackUtils():
log.debug("Resetting properties playback flag.")
window('emby_playbackProps', clear=True)
#self.pl.verifyPlaylist()
#self.pl.verify_playlist()
########## SETUP MAIN ITEM ##########
# For transcoding only, ask for audio/subs pref
@ -246,6 +252,9 @@ class PlaybackUtils():
except (TypeError, KeyError, IndexError):
return
temp = xbmc.translatePath(
"special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8')
kodiindex = 0
for stream in mediastreams:
@ -258,10 +267,21 @@ class PlaybackUtils():
# Direct stream
url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
% (self.server, itemid, itemid, index))
if "Language" in stream:
filename = "Stream.%s.srt" % stream['Language']
try:
path = self._download_external_subs(url, temp, filename)
externalsubs.append(path)
except Exception as e:
log.error(e)
continue
else:
externalsubs.append(url)
# map external subtitles for mapping
mapping[kodiindex] = index
externalsubs.append(url)
kodiindex += 1
mapping = json.dumps(mapping)
@ -269,9 +289,30 @@ class PlaybackUtils():
return externalsubs
def _download_external_subs(self, src, dst, filename):
if not xbmcvfs.exists(dst):
xbmcvfs.mkdir(dst)
path = os.path.join(dst, filename)
try:
response = requests.get(src, stream=True)
response.encoding = 'utf-8'
response.raise_for_status()
except Exception as e:
del response
raise
else:
with open(path, 'wb') as f:
f.write(response.content)
del response
return path
def setArtwork(self, listItem):
# Set up item and item info
allartwork = self.artwork.getAllArtwork(self.item, parentInfo=True)
allartwork = self.artwork.get_all_artwork(self.item, parent_info=True)
# Set artwork for listitem
arttypes = {
@ -306,28 +347,32 @@ class PlaybackUtils():
else:
listItem.setArt({arttype: path})
def setListItem(self, listItem):
def setListItem(self, listItem, dbid=None):
people = self.API.getPeople()
studios = self.API.getStudios()
people = self.API.get_people()
studios = self.API.get_studios()
metadata = {
'title': self.item.get('Name', "Missing name"),
'year': self.item.get('ProductionYear'),
'plot': self.API.getOverview(),
'plot': self.API.get_overview(),
'director': people.get('Director'),
'writer': people.get('Writer'),
'mpaa': self.API.getMpaa(),
'mpaa': self.API.get_mpaa(),
'genre': " / ".join(self.item['Genres']),
'studio': " / ".join(studios),
'aired': self.API.getPremiereDate(),
'aired': self.API.get_premiere_date(),
'rating': self.item.get('CommunityRating'),
'votes': self.item.get('VoteCount')
}
if "Episode" in self.item['Type']:
# Only for tv shows
# For Kodi Krypton
metadata['mediatype'] = "episode"
metadata['dbid'] = dbid
thumbId = self.item.get('SeriesId')
season = self.item.get('ParentIndexNumber', -1)
episode = self.item.get('IndexNumber', -1)
@ -337,6 +382,11 @@ class PlaybackUtils():
metadata['season'] = season
metadata['episode'] = episode
if "Movie" in self.item['Type']:
# For Kodi Krypton
metadata['mediatype'] = "movie"
metadata['dbid'] = dbid
listItem.setProperty('IsPlayable', 'true')
listItem.setProperty('IsFolder', 'false')
listItem.setLabel(metadata['title'])

View file

@ -6,6 +6,7 @@ import json
import logging
import xbmc
import xbmcvfs
import xbmcgui
import clientinfo
@ -13,6 +14,7 @@ import downloadutils
import kodidb_functions as kodidb
import websocket_client as wsc
from utils import window, settings, language as lang
from ga_client import GoogleAnalytics
#################################################################################################
@ -27,7 +29,6 @@ class Player(xbmc.Player):
_shared_state = {}
played_info = {}
playStats = {}
currentFile = None
@ -37,14 +38,11 @@ class Player(xbmc.Player):
self.clientInfo = clientinfo.ClientInfo()
self.doUtils = downloadutils.DownloadUtils().downloadUrl
self.ws = wsc.WebSocket_Client()
self.ws = wsc.WebSocketClient()
self.xbmcplayer = xbmc.Player()
log.debug("Starting playback monitor.")
def GetPlayStats(self):
return self.playStats
xbmc.Player.__init__(self)
def onPlayBackStarted(self):
# Will be called when xbmc starts playing a file
@ -231,20 +229,9 @@ class Player(xbmc.Player):
self.played_info[currentFile] = data
log.info("ADDING_FILE: %s" % self.played_info)
# log some playback stats
'''if(itemType != None):
if(self.playStats.get(itemType) != None):
count = self.playStats.get(itemType) + 1
self.playStats[itemType] = count
else:
self.playStats[itemType] = 1
if(playMethod != None):
if(self.playStats.get(playMethod) != None):
count = self.playStats.get(playMethod) + 1
self.playStats[playMethod] = count
else:
self.playStats[playMethod] = 1'''
ga = GoogleAnalytics()
ga.sendEventData("PlayAction", itemType, playMethod)
ga.sendScreenView(itemType)
def reportPlayback(self):
@ -367,7 +354,7 @@ class Player(xbmc.Player):
# Report progress via websocketclient
postdata = json.dumps(postdata)
log.debug("Report: %s" % postdata)
self.ws.sendProgressUpdate(postdata)
self.ws.send_progress_update(postdata)
def onPlayBackPaused(self):
@ -441,6 +428,8 @@ class Player(xbmc.Player):
# Prevent manually mark as watched in Kodi monitor
window('emby_skipWatched%s' % itemid, value="true")
self.stopPlayback(data)
if currentPosition and runtime:
try:
percentComplete = (currentPosition * 10000000) / int(runtime)
@ -473,16 +462,24 @@ class Player(xbmc.Player):
else:
log.info("User skipped deletion.")
self.stopPlayback(data)
# Stop transcoding
if playMethod == "Transcode":
log.info("Transcoding for %s terminated." % itemid)
deviceId = self.clientInfo.getDeviceId()
deviceId = self.clientInfo.get_device_id()
url = "{server}/emby/Videos/ActiveEncodings?DeviceId=%s" % deviceId
self.doUtils(url, action_type="DELETE")
path = xbmc.translatePath(
"special://profile/addon_data/plugin.video.emby/temp/").decode('utf-8')
dirs, files = xbmcvfs.listdir(path)
for file in files:
xbmcvfs.delete("%s%s" % (path, file))
self.played_info.clear()
ga = GoogleAnalytics()
ga.sendEventData("PlayAction", "Stopped")
def stopPlayback(self, data):
@ -499,4 +496,11 @@ class Player(xbmc.Player):
'MediaSourceId': itemId,
'PositionTicks': positionTicks
}
self.doUtils(url, postBody=postdata, action_type="POST")
self.doUtils(url, postBody=postdata, action_type="POST")
#If needed, close any livestreams
livestreamid = window("emby_%s.livestreamid" % self.currentFile)
if livestreamid:
url = "{server}/emby/LiveStreams/Close"
postdata = { 'LiveStreamId': livestreamid }
self.doUtils(url, postBody=postdata, action_type="POST")

View file

@ -2,18 +2,16 @@
#################################################################################################
import json
import logging
import xbmc
import xbmcgui
import xbmcplugin
import playutils
import playbackutils
import embydb_functions as embydb
import read_embyserver as embyserver
from utils import window, settings, language as lang, kodiSQL
from utils import window, kodiSQL, JSONRPC
#################################################################################################
@ -22,169 +20,141 @@ log = logging.getLogger("EMBY."+__name__)
#################################################################################################
class Playlist():
class Playlist(object):
def __init__(self):
self.userid = window('emby_currUser')
self.server = window('emby_server%s' % self.userid)
self.emby = embyserver.Read_EmbyServer()
def playAll(self, itemids, startat):
def play_all(self, item_ids, start_at):
embyconn = kodiSQL('emby')
embycursor = embyconn.cursor()
emby_db = embydb.Embydb_Functions(embycursor)
conn = kodiSQL('emby')
cursor = conn.cursor()
emby_db = embydb.Embydb_Functions(cursor)
player = xbmc.Player()
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
playlist.clear()
log.info("---*** PLAY ALL ***---")
log.info("Items: %s and start at: %s" % (itemids, startat))
log.info("Items: %s and start at: %s", item_ids, start_at)
started = False
window('emby_customplaylist', value="true")
if startat != 0:
if start_at:
# Seek to the starting position
window('emby_customplaylist.seektime', str(startat))
window('emby_customplaylist.seektime', str(start_at))
for itemid in itemids:
embydb_item = emby_db.getItem_byId(itemid)
for item_id in item_ids:
log.info("Adding %s to playlist", item_id)
item = emby_db.getItem_byId(item_id)
try:
dbid = embydb_item[0]
mediatype = embydb_item[4]
db_id = item[0]
media_type = item[4]
except TypeError:
# Item is not found in our database, add item manually
log.info("Item was not found in the database, manually adding item.")
item = self.emby.getItem(itemid)
self.addtoPlaylist_xbmc(playlist, item)
else:
# Add to playlist
self.addtoPlaylist(dbid, mediatype)
log.info("Item was not found in the database, manually adding item")
item = self.emby.getItem(item_id)
self.add_to_xbmc_playlist(playlist, item)
log.info("Adding %s to playlist." % itemid)
else: # Add to playlist
self.add_to_playlist(db_id, media_type)
if not started:
started = True
player.play(playlist)
self.verifyPlaylist()
embycursor.close()
self.verify_playlist()
cursor.close()
def modifyPlaylist(self, itemids):
def modify_playlist(self, item_ids):
embyconn = kodiSQL('emby')
embycursor = embyconn.cursor()
emby_db = embydb.Embydb_Functions(embycursor)
conn = kodiSQL('emby')
cursor = conn.cursor()
emby_db = embydb.Embydb_Functions(cursor)
log.info("---*** ADD TO PLAYLIST ***---")
log.info("Items: %s" % itemids)
log.info("Items: %s", item_ids)
player = xbmc.Player()
playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
for itemid in itemids:
embydb_item = emby_db.getItem_byId(itemid)
for item_id in item_ids:
log.info("Adding %s to playlist", item_id)
item = emby_db.getItem_byId(item_id)
try:
dbid = embydb_item[0]
mediatype = embydb_item[4]
db_id = item[0]
media_type = item[4]
except TypeError:
# Item is not found in our database, add item manually
item = self.emby.getItem(itemid)
self.addtoPlaylist_xbmc(playlist, item)
else:
# Add to playlist
self.addtoPlaylist(dbid, mediatype)
item = self.emby.getItem(item_id)
self.add_to_xbmc_playlist(playlist, item)
log.info("Adding %s to playlist." % itemid)
else: # Add to playlist
self.add_to_playlist(db_id, media_type)
self.verifyPlaylist()
embycursor.close()
self.verify_playlist()
cursor.close()
return playlist
def addtoPlaylist(self, dbid=None, mediatype=None, url=None):
pl = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.Add",
'params': {
'playlistid': 1
}
}
if dbid is not None:
pl['params']['item'] = {'%sid' % mediatype: int(dbid)}
else:
pl['params']['item'] = {'file': url}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))
def addtoPlaylist_xbmc(self, playlist, item):
@classmethod
def add_to_xbmc_playlist(cls, playlist, item):
playurl = playutils.PlayUtils(item).getPlayUrl()
if not playurl:
# Playurl failed
log.info("Failed to retrieve playurl.")
log.info("Failed to retrieve playurl")
return
log.info("Playurl: %s" % playurl)
log.info("Playurl: %s", playurl)
listitem = xbmcgui.ListItem()
playbackutils.PlaybackUtils(item).setProperties(playurl, listitem)
playlist.add(playurl, listitem)
def insertintoPlaylist(self, position, dbid=None, mediatype=None, url=None):
@classmethod
def add_to_playlist(cls, db_id=None, media_type=None, url=None):
pl = {
params = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.Insert",
'params': {
'playlistid': 1,
'position': position
}
'playlistid': 1
}
if dbid is not None:
pl['params']['item'] = {'%sid' % mediatype: int(dbid)}
if db_id is not None:
params['item'] = {'%sid' % media_type: int(db_id)}
else:
pl['params']['item'] = {'file': url}
params['item'] = {'file': url}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))
log.debug(JSONRPC('Playlist.Add').execute(params))
def verifyPlaylist(self):
@classmethod
def insert_to_playlist(cls, position, db_id=None, media_type=None, url=None):
pl = {
params = {
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.GetItems",
'params': {
'playlistid': 1
}
'playlistid': 1,
'position': position
}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))
if db_id is not None:
params['item'] = {'%sid' % media_type: int(db_id)}
else:
params['item'] = {'file': url}
def removefromPlaylist(self, position):
log.debug(JSONRPC('Playlist.Insert').execute(params))
pl = {
@classmethod
def verify_playlist(cls):
log.debug(JSONRPC('Playlist.GetItems').execute({'playlistid': 1}))
'jsonrpc': "2.0",
'id': 1,
'method': "Playlist.Remove",
'params': {
@classmethod
def remove_from_playlist(cls, position):
'playlistid': 1,
'position': position
}
params = {
'playlistid': 1,
'position': position
}
log.debug(xbmc.executeJSONRPC(json.dumps(pl)))
log.debug(JSONRPC('Playlist.Remove').execute(params))

View file

@ -10,6 +10,7 @@ import xbmcgui
import xbmcvfs
import clientinfo
import downloadutils
from utils import window, settings, language as lang
#################################################################################################
@ -29,7 +30,37 @@ class PlayUtils():
self.userid = window('emby_currUser')
self.server = window('emby_server%s' % self.userid)
self.doUtils = downloadutils.DownloadUtils().downloadUrl
def getPlayUrlNew(self):
'''
New style to retrieve the best playback method based on sending the profile to the server
Based on capabilities the correct path is returned, including livestreams that need to be opened by the server
TODO: Close livestream if needed (RequiresClosing in livestream source)
'''
playurl = None
pbinfo = self.getPlaybackInfo()
if pbinfo:
xbmc.log("getPlayUrl pbinfo: %s" %(pbinfo))
if pbinfo["Protocol"] == "SupportsDirectPlay":
playmethod = "DirectPlay"
elif pbinfo["Protocol"] == "SupportsDirectStream":
playmethod = "DirectStream"
elif pbinfo.get('LiveStreamId'):
playmethod = "LiveStream"
else:
playmethod = "Transcode"
playurl = pbinfo["Path"]
xbmc.log("getPlayUrl playmethod: %s - playurl: %s" %(playmethod, playurl))
window('emby_%s.playmethod' % playurl, value=playmethod)
if pbinfo["RequiresClosing"] and pbinfo.get('LiveStreamId'):
window('emby_%s.livestreamid' % playurl, value=pbinfo["LiveStreamId"])
return playurl
def getPlayUrl(self):
@ -40,7 +71,8 @@ class PlayUtils():
# Play LiveTV or recordings
log.info("File protocol is http (livetv).")
playurl = "%s/emby/Videos/%s/stream.ts?audioCodec=copy&videoCodec=copy" % (self.server, self.item['Id'])
window('emby_%s.playmethod' % playurl, value="Transcode")
window('emby_%s.playmethod' % playurl, value="DirectPlay")
elif self.item.get('MediaSources') and self.item['MediaSources'][0]['Protocol'] == "Http":
# Only play as http, used for channels, or online hosting of content
@ -60,6 +92,7 @@ class PlayUtils():
log.info("File is direct streaming.")
playurl = self.directStream()
playurl = playurl.encode('utf-8')
# Set playmethod property
window('emby_%s.playmethod' % playurl, value="DirectStream")
@ -261,7 +294,7 @@ class PlayUtils():
playurl = self.directPlay()
else:
itemid = self.item['Id']
deviceId = self.clientInfo.getDeviceId()
deviceId = self.clientInfo.get_device_id()
playurl = (
"%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s"
% (self.server, itemid, itemid)
@ -293,6 +326,9 @@ class PlayUtils():
'13': 16700,
'14': 18200,
'15': 20000,
'16': 25000,
'17': 30000,
'18': 35000,
'16': 40000,
'17': 100000,
'18': 1000000
@ -406,3 +442,190 @@ class PlayUtils():
playurlprefs += "&AudioBitrate=192000"
return playurlprefs
def getPlaybackInfo(self):
#Gets the playback Info for the current item
url = "{server}/emby/Items/%s/PlaybackInfo?format=json" %self.item['Id']
body = {
"UserId": self.userid,
"DeviceProfile": self.getDeviceProfile(),
"StartTimeTicks": 0, #TODO
"AudioStreamIndex": None, #TODO
"SubtitleStreamIndex": None, #TODO
"MediaSourceId": None,
"LiveStreamId": None
}
pbinfo = self.doUtils(url, postBody=body, action_type="POST")
xbmc.log("getPlaybackInfo: %s" %pbinfo)
mediaSource = self.getOptimalMediaSource(pbinfo["MediaSources"])
if mediaSource and mediaSource["RequiresOpening"]:
mediaSource = self.getLiveStream(pbinfo["PlaySessionId"], mediaSource)
return mediaSource
def getOptimalMediaSource(self, mediasources):
'''
Select the best possible mediasource for playback
Because we posted our deviceprofile to the server,
only streams will be returned that can actually be played by this client so no need to check bitrates etc.
'''
preferredStreamOrder = ["SupportsDirectPlay","SupportsDirectStream","SupportsTranscoding"]
bestSource = {}
for prefstream in preferredStreamOrder:
for source in mediasources:
if source[prefstream] == True:
if prefstream == "SupportsDirectPlay":
#always prefer direct play
alt_playurl = self.checkDirectPlayPath(source["Path"])
if alt_playurl:
bestSource = source
source["Path"] = alt_playurl
elif bestSource.get("BitRate",0) < source.get("Bitrate",0):
#prefer stream with highest bitrate for http sources
bestSource = source
elif not source.get("Bitrate") and source.get("RequiresOpening"):
#livestream
bestSource = source
xbmc.log("getOptimalMediaSource: %s" %bestSource)
return bestSource
def getLiveStream(self, playSessionId, mediaSource):
url = "{server}/emby/LiveStreams/Open?format=json"
body = {
"UserId": self.userid,
"DeviceProfile": self.getDeviceProfile(),
"ItemId": self.item["Id"],
"PlaySessionId": playSessionId,
"OpenToken": mediaSource["OpenToken"],
"StartTimeTicks": 0, #TODO
"AudioStreamIndex": None, #TODO
"SubtitleStreamIndex": None #TODO
}
streaminfo = self.doUtils(url, postBody=body, action_type="POST")
xbmc.log("getLiveStream: %s" %streaminfo)
return streaminfo["MediaSource"]
def checkDirectPlayPath(self, playurl):
if self.item.get('VideoType'):
# Specific format modification
if self.item['VideoType'] == "Dvd":
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
elif self.item['VideoType'] == "BluRay":
playurl = "%s/BDMV/index.bdmv" % playurl
# Assign network protocol
if playurl.startswith('\\\\'):
playurl = playurl.replace("\\\\", "smb://")
playurl = playurl.replace("\\", "/")
if xbmcvfs.exists(playurl):
return playurl
else:
return None
def getDeviceProfile(self):
return {
"Name": "Kodi",
"MaxStreamingBitrate": self.getBitrate()*1000,
"MusicStreamingTranscodingBitrate": 1280000,
"TimelineOffsetSeconds": 5,
"Identification": {
"ModelName": "Kodi",
"Headers": [
{
"Name": "User-Agent",
"Value": "Kodi",
"Match": 2
}
]
},
"TranscodingProfiles": [
{
"Container": "mp3",
"AudioCodec": "mp3",
"Type": 0
},
{
"Container": "ts",
"AudioCodec": "aac",
"VideoCodec": "h264",
"Type": 1
},
{
"Container": "jpeg",
"Type": 2
}
],
"DirectPlayProfiles": [
{
"Container": "",
"Type": 0
},
{
"Container": "",
"Type": 1
},
{
"Container": "",
"Type": 2
}
],
"ResponseProfiles": [],
"ContainerProfiles": [],
"CodecProfiles": [],
"SubtitleProfiles": [
{
"Format": "srt",
"Method": 2
},
{
"Format": "sub",
"Method": 2
},
{
"Format": "srt",
"Method": 1
},
{
"Format": "ass",
"Method": 1,
"DidlMode": ""
},
{
"Format": "ssa",
"Method": 1,
"DidlMode": ""
},
{
"Format": "smi",
"Method": 1,
"DidlMode": ""
},
{
"Format": "dvdsub",
"Method": 1,
"DidlMode": ""
},
{
"Format": "pgs",
"Method": 1,
"DidlMode": ""
},
{
"Format": "pgssub",
"Method": 1,
"DidlMode": ""
},
{
"Format": "sub",
"Method": 1,
"DidlMode": ""
}
]
}

View file

@ -3,6 +3,7 @@
#################################################################################################
import logging
import hashlib
import xbmc
@ -123,7 +124,7 @@ class Read_EmbyServer():
return [viewName, viewId, mediatype]
def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True,
limit=None, sortorder="Ascending", filter=""):
limit=None, sortorder="Ascending", filter_type=""):
params = {
'ParentId': parentid,
@ -135,7 +136,7 @@ class Read_EmbyServer():
'Limit': limit,
'SortBy': sortby,
'SortOrder': sortorder,
'Filters': filter,
'Filters': filter_type,
'Fields': (
"Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines,"
@ -185,7 +186,7 @@ class Read_EmbyServer():
url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json"
return self.doUtils(url, parameters=params)
def getSection(self, parentid, itemtype=None, sortby="SortName", basic=False, dialog=None):
def getSection(self, parentid, itemtype=None, sortby="SortName", artist_id=None, basic=False, dialog=None):
items = {
@ -198,6 +199,7 @@ class Read_EmbyServer():
params = {
'ParentId': parentid,
'ArtistIds': artist_id,
'IncludeItemTypes': itemtype,
'CollapseBoxSetItems': False,
'IsVirtualUnaired': False,
@ -224,6 +226,7 @@ class Read_EmbyServer():
params = {
'ParentId': parentid,
'ArtistIds': artist_id,
'IncludeItemTypes': itemtype,
'CollapseBoxSetItems': False,
'IsVirtualUnaired': False,
@ -246,9 +249,13 @@ class Read_EmbyServer():
"Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers,"
"MediaSources,VoteCount"
)
result = self.doUtils(url, parameters=params)
try:
result = self.doUtils(url, parameters=params)
items['Items'].extend(result['Items'])
except Warning as error:
if "400" in error:
log.info("Something went wrong, aborting request.")
index += jump
except TypeError:
# Something happened to the connection
if not throttled:
@ -322,25 +329,14 @@ class Read_EmbyServer():
else:
for item in items:
item['Name'] = item['Name']
if item['Type'] == "Channel":
if item['Type'] in ("Channel", "PlaylistsFolder"):
# Filter view types
continue
# 3/4/2016 OriginalCollectionType is added
itemtype = item.get('OriginalCollectionType', item.get('CollectionType', "mixed"))
# 11/29/2015 Remove this once OriginalCollectionType is added to stable server.
# Assumed missing is mixed then.
'''if itemtype is None:
url = "{server}/emby/Library/MediaFolders?format=json"
result = self.doUtils(url)
for folder in result['Items']:
if item['Id'] == folder['Id']:
itemtype = folder.get('CollectionType', "mixed")'''
if item['Name'] not in ('Collections', 'Trailers'):
if item['Name'] not in ('Collections', 'Trailers', 'Playlists'):
if sortedlist:
views.append({
@ -441,7 +437,7 @@ class Read_EmbyServer():
return self.getSection(seasonId, "Episode")
def getArtists(self, dialog=None):
def getArtists(self, parent_id=None, dialog=None):
items = {
@ -453,6 +449,7 @@ class Read_EmbyServer():
url = "{server}/emby/Artists?UserId={UserId}&format=json"
params = {
'ParentId': parent_id,
'Recursive': True,
'Limit': 1
}
@ -465,13 +462,14 @@ class Read_EmbyServer():
log.debug("%s:%s Failed to retrieve the server response." % (url, params))
else:
index = 1
index = 0
jump = self.limitIndex
while index < total:
# Get items by chunk to increase retrieval speed at scale
params = {
'ParentId': parent_id,
'Recursive': True,
'IsVirtualUnaired': False,
'IsMissing': False,
@ -501,7 +499,7 @@ class Read_EmbyServer():
def getAlbumsbyArtist(self, artistId):
return self.getSection(artistId, "MusicAlbum", sortby="DateCreated")
return self.getSection(None, "MusicAlbum", sortby="DateCreated", artist_id=artistId)
def getSongs(self, basic=False, dialog=None):
@ -571,4 +569,20 @@ class Read_EmbyServer():
def deleteItem(self, itemid):
url = "{server}/emby/Items/%s?format=json" % itemid
self.doUtils(url, action_type="DELETE")
self.doUtils(url, action_type="DELETE")
def getUsers(self, server):
url = "%s/emby/Users/Public?format=json" % server
users = self.doUtils(url, authenticate=False)
return users or []
def loginUser(self, server, username, password=None):
password = password or ""
url = "%s/emby/Users/AuthenticateByName?format=json" % server
data = {'username': username, 'password': hashlib.sha1(password).hexdigest()}
user = self.doUtils(url, postBody=data, action_type="POST", authenticate=False)
return user

View file

@ -0,0 +1,320 @@
# -*- coding: utf-8 -*-
#################################################################################################
import logging
import sys
import time
import _strptime # Workaround for threads using datetime: _striptime is locked
from datetime import datetime
import xbmc
import userclient
import clientinfo
import initialsetup
import kodimonitor
import librarysync
import player
import videonodes
import websocket_client as wsc
from utils import window, settings, dialog, language as lang
from ga_client import GoogleAnalytics
import md5
#################################################################################################
log = logging.getLogger("EMBY."+__name__)
#################################################################################################
class Service(object):
startup = False
server_online = True
warn_auth = True
userclient_running = False
userclient_thread = None
websocket_running = False
websocket_thread = None
library_running = False
library_thread = None
last_progress = datetime.today()
lastMetricPing = time.time()
def __init__(self):
self.client_info = clientinfo.ClientInfo()
self.addon_name = self.client_info.get_addon_name()
log_level = settings('logLevel')
window('emby_logLevel', value=str(log_level))
window('emby_kodiProfile', value=xbmc.translatePath('special://profile'))
context_menu = "true" if settings('enableContext') == "true" else ""
window('emby_context', value=context_menu)
# Initial logging
log.warn("======== START %s ========", self.addon_name)
log.warn("Python Version: %s", sys.version)
log.warn("Platform: %s", self.client_info.get_platform())
log.warn("KODI Version: %s", xbmc.getInfoLabel('System.BuildVersion'))
log.warn("%s Version: %s", self.addon_name, self.client_info.get_version())
log.warn("Using plugin paths: %s", settings('useDirectPaths') == "0")
log.warn("Log Level: %s", log_level)
# Reset window props for profile switch
properties = [
"emby_online", "emby_state.json", "emby_serverStatus", "emby_onWake",
"emby_syncRunning", "emby_dbCheck", "emby_kodiScan",
"emby_shouldStop", "emby_currUser", "emby_dbScan", "emby_sessionId",
"emby_initialScan", "emby_customplaylist", "emby_playbackProps"
]
for prop in properties:
window(prop, clear=True)
# Clear video nodes properties
videonodes.VideoNodes().clearProperties()
# Set the minimum database version
window('emby_minDBVersion', value="1.1.63")
def service_entry_point(self):
# Important: Threads depending on abortRequest will not trigger
# if profile switch happens more than once.
self.monitor = kodimonitor.KodiMonitor()
self.kodi_player = player.Player()
kodi_profile = xbmc.translatePath('special://profile')
# Server auto-detect
initialsetup.InitialSetup().setup()
# Initialize important threads
self.userclient_thread = userclient.UserClient()
user_client = self.userclient_thread
self.websocket_thread = wsc.WebSocketClient()
self.library_thread = librarysync.LibrarySync()
while not self.monitor.abortRequested():
if window('emby_kodiProfile') != kodi_profile:
# Profile change happened, terminate this thread and others
log.info("Kodi profile was: %s and changed to: %s. Terminating old Emby thread.",
kodi_profile, window('emby_kodiProfile'))
raise RuntimeError("Kodi profile changed detected")
# Before proceeding, need to make sure:
# 1. Server is online
# 2. User is set
# 3. User has access to the server
if window('emby_online') == "true":
# Emby server is online
# Verify if user is set and has access to the server
if user_client.get_user() is not None and user_client.get_access():
# If an item is playing
if self.kodi_player.isPlaying():
# ping metrics server to keep sessions alive while playing
# ping every 5 min
timeSinceLastPing = time.time() - self.lastMetricPing
if(timeSinceLastPing > 300):
self.lastMetricPing = time.time()
ga = GoogleAnalytics()
ga.sendEventData("PlayAction", "PlayPing")
self._report_progress()
elif not self.startup:
self.startup = self._startup()
else:
if (user_client.get_user() is None) and self.warn_auth:
# Alert user is not authenticated and suppress future warning
self.warn_auth = False
log.info("Not authenticated yet.")
# User access is restricted.
# Keep verifying until access is granted
# unless server goes offline or Kodi is shut down.
self._access_check()
else:
# Wait until Emby server is online
# or Kodi is shut down.
self._server_online_check()
if self.monitor.waitForAbort(1):
# Abort was requested while waiting. We should exit
break
##### Emby thread is terminating. #####
self.shutdown()
def _startup(self):
serverId = settings('serverId')
if(serverId != None):
serverId = md5.new(serverId).hexdigest()
ga = GoogleAnalytics()
ga.sendEventData("Application", "Startup", serverId)
# Start up events
self.warn_auth = True
username = self.userclient_thread.get_username()
if settings('connectMsg') == "true" and username:
# Get additional users
add_users = settings('additionalUsers')
if add_users:
add_users = ", "+", ".join(add_users.split(','))
dialog(type_="notification",
heading="{emby}",
message=("%s %s%s"
% (lang(33000), username.decode('utf-8'),
add_users.decode('utf-8'))),
icon="{emby}",
time=2000,
sound=False)
# Start the Websocket Client
self.websocket_running = True
self.websocket_thread.start()
# Start the syncing thread
self.library_running = True
self.library_thread.start()
return True
def _server_online_check(self):
# Set emby_online true/false property
user_client = self.userclient_thread
while not self.monitor.abortRequested():
if user_client.get_server() is None:
# No server info set in add-on settings
pass
elif not user_client.verify_server():
# Server is offline.
# Alert the user and suppress future warning
if self.server_online:
log.info("Server is offline")
window('emby_online', value="false")
if settings('offlineMsg') == "true":
dialog(type_="notification",
heading=lang(33001),
message="%s %s" % (self.addon_name, lang(33002)),
icon="{emby}",
sound=False)
self.server_online = False
elif window('emby_online') in ("sleep", "reset"):
# device going to sleep
if self.websocket_running:
self.websocket_thread.stop_client()
self.websocket_thread = wsc.WebSocketClient()
self.websocket_running = False
if self.library_running:
self.library_thread.stopThread()
self.library_thread = librarysync.LibrarySync()
self.library_running = False
else:
# Server is online
if not self.server_online:
# Server was offline when Kodi started.
# Wait for server to be fully established.
if self.monitor.waitForAbort(5):
# Abort was requested while waiting.
break
# Alert the user that server is online.
dialog(type_="notification",
heading="{emby}",
message=lang(33003),
icon="{emby}",
time=2000,
sound=False)
self.server_online = True
window('emby_online', value="true")
log.info("Server is online and ready")
# Start the userclient thread
if not self.userclient_running:
self.userclient_running = True
user_client.start()
break
if self.monitor.waitForAbort(1):
# Abort was requested while waiting.
break
def _access_check(self):
# Keep verifying until access is granted
# unless server goes offline or Kodi is shut down.
while not self.userclient_thread.get_access():
if window('emby_online') != "true":
# Server went offline
break
if self.monitor.waitForAbort(5):
# Abort was requested while waiting. We should exit
break
def _report_progress(self):
# Update and report playback progress
kodi_player = self.kodi_player
try:
play_time = kodi_player.getTime()
filename = kodi_player.currentFile
# Update positionticks
if filename in kodi_player.played_info:
kodi_player.played_info[filename]['currentPosition'] = play_time
difference = datetime.today() - self.last_progress
difference_seconds = difference.seconds
# Report progress to Emby server
if difference_seconds > 3:
kodi_player.reportPlayback()
self.last_progress = datetime.today()
elif window('emby_command') == "true":
# Received a remote control command that
# requires updating immediately
window('emby_command', clear=True)
kodi_player.reportPlayback()
self.last_progress = datetime.today()
except Exception as error:
log.exception(error)
def shutdown(self):
#ga = GoogleAnalytics()
#ga.sendEventData("Application", "Shutdown")
if self.userclient_running:
self.userclient_thread.stop_client()
if self.library_running:
self.library_thread.stopThread()
if self.websocket_running:
self.websocket_thread.stop_client()
log.warn("======== STOP %s ========", self.addon_name)

View file

@ -2,18 +2,17 @@
##################################################################################################
import hashlib
import json
import logging
import threading
import xbmc
import xbmcgui
import xbmcaddon
import xbmcvfs
import artwork
import clientinfo
import connectmanager
import downloadutils
import read_embyserver as embyserver
from utils import window, settings, language as lang
##################################################################################################
@ -22,426 +21,303 @@ log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class UserClient(threading.Thread):
# Borg - multiple instances, shared state
_shared_state = {}
_shared_state = {} # Borg
stop_thread = False
auth = True
retry = 0
_stop_thread = False
_user = None
_server = None
currUser = None
currUserId = None
currServer = None
currToken = None
HasAccess = True
AdditionalUser = []
userSettings = None
_auth = True
_has_access = True
def __init__(self):
self.__dict__ = self._shared_state
self.addon = xbmcaddon.Addon()
self.doUtils = downloadutils.DownloadUtils()
self.doutils = downloadutils.DownloadUtils()
self.download = self.doutils.downloadUrl
self.emby = embyserver.Read_EmbyServer()
self.connectmanager = connectmanager.ConnectManager()
threading.Thread.__init__(self)
@classmethod
def get_username(cls):
return settings('username') or settings('connectUsername') or None
def getAdditionalUsers(self):
def get_user(self, data=None):
additionalUsers = settings('additionalUsers')
if data is not None:
self._user = data
self._set_user_server()
if additionalUsers:
self.AdditionalUser = additionalUsers.split(',')
return self._user
def getUsername(self):
def get_server_details(self):
return self._server
username = settings('username')
@classmethod
def get_server(cls):
if not username:
log.debug("No username saved.")
return ""
return username
def getLogLevel(self):
try:
logLevel = int(settings('logLevel'))
except ValueError:
logLevel = 0
return logLevel
def getUserId(self):
username = self.getUsername()
w_userId = window('emby_currUser')
s_userId = settings('userId%s' % username)
# Verify the window property
if w_userId:
if not s_userId:
# Save access token if it's missing from settings
settings('userId%s' % username, value=w_userId)
log.debug("Returning userId from WINDOW for username: %s UserId: %s"
% (username, w_userId))
return w_userId
# Verify the settings
elif s_userId:
log.debug("Returning userId from SETTINGS for username: %s userId: %s"
% (username, s_userId))
return s_userId
# No userId found
else:
log.info("No userId saved for username: %s." % username)
def getServer(self, prefix=True):
alternate = settings('altip') == "true"
if alternate:
# Alternate host
HTTPS = settings('secondhttps') == "true"
host = settings('secondipaddress')
port = settings('secondport')
else:
# Original host
HTTPS = settings('https') == "true"
###$ Begin migration $###
if settings('server') == "":
http = "https" if settings('https') == "true" else "http"
host = settings('ipaddress')
port = settings('port')
server = host + ":" + port
if host and port:
settings('server', value="%s://%s:%s" % (http, host, port))
log.info("server address migration completed")
###$ End migration $###
if not host:
log.debug("No server information saved.")
return False
return settings('server') or None
# If https is true
if prefix and HTTPS:
server = "https://%s" % server
return server
# If https is false
elif prefix and not HTTPS:
server = "http://%s" % server
return server
# If only the host:port is required
elif not prefix:
return server
def verify_server(self):
def getToken(self):
username = self.getUsername()
userId = self.getUserId()
w_token = window('emby_accessToken%s' % userId)
s_token = settings('accessToken')
# Verify the window property
if w_token:
if not s_token:
# Save access token if it's missing from settings
settings('accessToken', value=w_token)
log.debug("Returning accessToken from WINDOW for username: %s accessToken: %s"
% (username, w_token))
return w_token
# Verify the settings
elif s_token:
log.debug("Returning accessToken from SETTINGS for username: %s accessToken: %s"
% (username, s_token))
window('emby_accessToken%s' % username, value=s_token)
return s_token
else:
log.info("No token found.")
return ""
def getSSLverify(self):
# Verify host certificate
s_sslverify = settings('sslverify')
if settings('altip') == "true":
s_sslverify = settings('secondsslverify')
if s_sslverify == "true":
url = "%s/emby/Users/Public?format=json" % self.get_server()
result = self.download(url, authenticate=False)
if result != "": # Specific verification, due to possibility of returning empty dict
return True
else:
return False
def getSSL(self):
# Client side certificate
s_cert = settings('sslcert')
if settings('altip') == "true":
s_cert = settings('secondsslcert')
if s_cert == "None":
return None
else:
return s_cert
def setUserPref(self):
doUtils = self.doUtils.downloadUrl
result = doUtils("{server}/emby/Users/{UserId}?format=json")
self.userSettings = result
# Set user image for skin display
if result.get('PrimaryImageTag'):
window('EmbyUserImage', value=artwork.Artwork().getUserArtwork(result['Id'], 'Primary'))
# Set resume point max
result = doUtils("{server}/emby/System/Configuration?format=json")
settings('markPlayed', value=str(result['MaxResumePct']))
def getPublicUsers(self):
# Get public Users
url = "%s/emby/Users/Public?format=json" % self.getServer()
result = self.doUtils.downloadUrl(url, authenticate=False)
if result != "":
return result
else:
# Server connection failed
return False
@classmethod
def get_ssl(cls):
"""
Returns boolean value or path to certificate
True: Verify ssl
False: Don't verify connection
"""
certificate = settings('sslcert')
if certificate != "None":
return certificate
def hasAccess(self):
# hasAccess is verified in service.py
result = self.doUtils.downloadUrl("{server}/emby/Users?format=json")
return True if settings('sslverify') == "true" else False
if result == False:
# Access is restricted, set in downloadutils.py via exception
log.info("Access is restricted.")
self.HasAccess = False
def get_access(self):
elif window('emby_online') != "true":
# Server connection failed
pass
if not self._has_access:
self._set_access()
elif window('emby_serverStatus') == "restricted":
log.info("Access is granted.")
self.HasAccess = True
window('emby_serverStatus', clear=True)
xbmcgui.Dialog().notification(lang(29999), lang(33007))
return self._has_access
def loadCurrUser(self, authenticated=False):
def _set_access(self):
doUtils = self.doUtils
username = self.getUsername()
userId = self.getUserId()
try:
self.download("{server}/emby/Users?format=json")
except Warning as error:
if self._has_access and "restricted" in error:
self._has_access = False
log.info("access is restricted")
else:
if not self._has_access:
self._has_access = True
window('emby_serverStatus', clear=True)
log.info("access is granted")
xbmcgui.Dialog().notification(lang(29999), lang(33007))
# Only to be used if token exists
self.currUserId = userId
self.currServer = self.getServer()
self.currToken = self.getToken()
self.ssl = self.getSSLverify()
self.sslcert = self.getSSL()
@classmethod
def get_userid(cls):
# Test the validity of current token
if authenticated == False:
url = "%s/emby/Users/%s?format=json" % (self.currServer, userId)
window('emby_currUser', value=userId)
window('emby_accessToken%s' % userId, value=self.currToken)
result = doUtils.downloadUrl(url)
###$ Begin migration $###
if settings('userId') == "":
settings('userId', value=settings('userId%s' % settings('username')))
log.info("userid migration completed")
###$ End migration $###
if result == 401:
# Token is no longer valid
self.resetClient()
return False
return settings('userId') or None
# Set to windows property
window('emby_currUser', value=userId)
window('emby_accessToken%s' % userId, value=self.currToken)
window('emby_server%s' % userId, value=self.currServer)
window('emby_server_%s' % userId, value=self.getServer(prefix=False))
@classmethod
def get_token(cls):
# Set DownloadUtils values
doUtils.setUsername(username)
doUtils.setUserId(self.currUserId)
doUtils.setServer(self.currServer)
doUtils.setToken(self.currToken)
doUtils.setSSL(self.ssl, self.sslcert)
# parental control - let's verify if access is restricted
self.hasAccess()
# Start DownloadUtils session
doUtils.startSession()
self.getAdditionalUsers()
# Set user preferences in settings
self.currUser = username
self.setUserPref()
###$ Begin migration $###
if settings('token') == "":
settings('token', value=settings('accessToken'))
log.info("token migration completed")
###$ End migration $###
return settings('token') or None
def authenticate(self):
def _set_user_server(self):
dialog = xbmcgui.Dialog()
user = self.download("{server}/emby/Users/{UserId}?format=json")
settings('username', value=user['Name'])
self._user = user
# Get /profile/addon_data
addondir = xbmc.translatePath(self.addon.getAddonInfo('profile')).decode('utf-8')
hasSettings = xbmcvfs.exists("%ssettings.xml" % addondir)
if "PrimaryImageTag" in self._user:
window('EmbyUserImage',
value=artwork.Artwork().get_user_artwork(self._user['Id'], 'Primary'))
username = self.getUsername()
server = self.getServer()
self._server = self.download("{server}/emby/System/Configuration?format=json")
settings('markPlayed', value=str(self._server['MaxResumePct']))
# If there's no settings.xml
if not hasSettings:
log.info("No settings.xml found.")
self.auth = False
return
# If no user information
elif not server or not username:
log.info("Missing server information.")
self.auth = False
return
# If there's a token, load the user
elif self.getToken():
result = self.loadCurrUser()
def _authenticate(self):
if result == False:
pass
if not self.get_server() or not self.get_username():
log.info('missing server or user information')
self._auth = False
elif self.get_token():
try:
self._load_user()
except Warning:
log.info("token is invalid")
self._reset_client()
else:
log.info("Current user: %s" % self.currUser)
log.info("Current userId: %s" % self.currUserId)
log.debug("Current accessToken: %s" % self.currToken)
log.info("current user: %s", self.get_username())
log.info("current userid: %s", self.get_userid())
log.debug("current token: %s", self.get_token())
return
##### AUTHENTICATE USER #####
users = self.getPublicUsers()
password = ""
# Find user in list
for user in users:
name = user['Name']
if username.decode('utf-8') in name:
# If user has password
if user['HasPassword'] == True:
password = dialog.input(
heading="%s %s" % (lang(33008), username.decode('utf-8')),
option=xbmcgui.ALPHANUM_HIDE_INPUT)
# If password dialog is cancelled
if not password:
log.warn("No password entered.")
window('emby_serverStatus', value="Stop")
self.auth = False
return
break
else:
# Manual login, user is hidden
password = dialog.input(
heading="%s %s" % (lang(33008), username.decode('utf-8')),
option=xbmcgui.ALPHANUM_HIDE_INPUT)
sha1 = hashlib.sha1(password)
sha1 = sha1.hexdigest()
# Authenticate username and password
data = {'username': username, 'password': sha1}
log.debug(data)
url = "%s/emby/Users/AuthenticateByName?format=json" % server
result = self.doUtils.downloadUrl(url, postBody=data, action_type="POST", authenticate=False)
server = self.get_server()
username = self.get_username().decode('utf-8')
try:
log.info("Auth response: %s" % result)
accessToken = result['AccessToken']
except (KeyError, TypeError):
log.info("Failed to retrieve the api key.")
accessToken = None
if accessToken is not None:
self.currUser = username
dialog.notification(lang(29999),
"%s %s!" % (lang(33000), self.currUser.decode('utf-8')))
settings('accessToken', value=accessToken)
settings('userId%s' % username, value=result['User']['Id'])
log.info("User Authenticated: %s" % accessToken)
self.loadCurrUser(authenticated=True)
window('emby_serverStatus', clear=True)
self.retry = 0
user = self.connectmanager.login_manual(server, username)
except RuntimeError:
window('emby_serverStatus', value="stop")
self._auth = False
return
else:
log.error("User authentication failed.")
settings('accessToken', value="")
settings('userId%s' % username, value="")
dialog.ok(lang(33001), lang(33009))
log.info("user: %s", user)
settings('username', value=user['User']['Name'])
settings('token', value=user['AccessToken'])
settings('userId', value=user['User']['Id'])
xbmcgui.Dialog().notification(lang(29999),
"%s %s!" % (lang(33000), username))
self._load_user(authenticated=True)
window('emby_serverStatus', clear=True)
# Give two attempts at entering password
if self.retry == 2:
log.info("Too many retries. "
"You can retry by resetting attempts in the addon settings.")
window('emby_serverStatus', value="Stop")
dialog.ok(lang(33001), lang(33010))
def _load_user(self, authenticated=False):
self.retry += 1
self.auth = False
doutils = self.doutils
def resetClient(self):
userid = self.get_userid()
server = self.get_server()
token = self.get_token()
log.info("Reset UserClient authentication.")
if self.currToken is not None:
# In case of 401, removed saved token
settings('accessToken', value="")
window('emby_accessToken%s' % self.getUserId(), clear=True)
self.currToken = None
log.info("User token has been removed.")
# Set properties
# TODO: Remove old reference once code converted
window('emby_currUser', value=userid)
window('emby_server%s' % userid, value=server)
window('emby_accessToken%s' % userid, value=token)
self.auth = True
self.currUser = None
server_json = {
'UserId': userid,
'Server': server,
'ServerId': settings('serverId'),
'Token': token,
'SSL': self.get_ssl()
}
# Set downloadutils.py values
doutils.set_session(**server_json)
# Test the validity of the current token
if not authenticated:
try:
self.download("{server}/emby/Users/{UserId}?format=json")
except Warning as error:
if "401" in error:
# Token is not longer valid
raise
# verify user access
self._set_access()
# Start downloadutils.py session
doutils.start_session()
# Set _user and _server
self._set_user_server()
def load_connect_servers(self):
# Set connect servers
if not settings('connectUsername'):
return
servers = self.connectmanager.get_connect_servers()
added_servers = []
for server in servers:
if server['Id'] != settings('serverId'):
# TODO: SSL setup
self.doutils.add_server(server, False)
added_servers.append(server['Id'])
# Set properties
log.info(added_servers)
window('emby_servers.json', value=added_servers)
def _reset_client(self):
log.info("reset UserClient authentication")
settings('accessToken', value="")
window('emby_accessToken', clear=True)
log.info("user token revoked.")
self._user = None
self.auth = None
current_state = self.connectmanager.get_state()
for server in current_state['Servers']:
if server['Id'] == settings('serverId'):
# Update token
server['AccessToken'] = None
self.connectmanager.update_token(server)
def run(self):
monitor = xbmc.Monitor()
log.warn("----===## Starting UserClient ##===----")
while not monitor.abortRequested():
log.warn("----====# Starting UserClient #====----")
while not self._stop_thread:
status = window('emby_serverStatus')
if status:
# Verify the connection status to server
if status == "restricted":
# Parental control is restricting access
self.HasAccess = False
self._has_access = False
elif status == "401":
# Unauthorized access, revoke token
window('emby_serverStatus', value="Auth")
self.resetClient()
window('emby_serverStatus', value="auth")
self._reset_client()
if self.auth and (self.currUser is None):
if self._auth and self._user is None:
# Try to authenticate user
status = window('emby_serverStatus')
if not status or status == "Auth":
if not status or status == "auth":
# Set auth flag because we no longer need
# to authenticate the user
self.auth = False
self.authenticate()
self._auth = False
self._authenticate()
if not self.auth and (self.currUser is None):
if not self._auth and self._user is None:
# If authenticate failed.
server = self.getServer()
username = self.getUsername()
server = self.get_server()
username = self.get_username()
status = window('emby_serverStatus')
# The status Stop is for when user cancelled password dialog.
if server and username and status != "Stop":
if server and username and status != "stop":
# Only if there's information found to login
log.debug("Server found: %s" % server)
log.debug("Username found: %s" % username)
self.auth = True
if self.stop_thread == True:
# If stopping the client didn't work
break
log.info("Server found: %s", server)
log.info("Username found: %s", username)
self._auth = True
if monitor.waitForAbort(1):
# Abort was requested while waiting. We should exit
break
self.doUtils.stopSession()
log.warn("##===---- UserClient Stopped ----===##")
self.doutils.stop_session()
log.warn("#====---- UserClient Stopped ----====#")
def stopClient(self):
# When emby for kodi terminates
self.stop_thread = True
def stop_client(self):
self._stop_thread = True

View file

@ -2,14 +2,14 @@
#################################################################################################
import cProfile
import inspect
import json
import logging
import pstats
import sqlite3
import StringIO
import os
import sys
import time
import unicodedata
import xml.etree.ElementTree as etree
@ -18,6 +18,7 @@ from datetime import datetime
import xbmc
import xbmcaddon
import xbmcgui
import xbmcplugin
import xbmcvfs
#################################################################################################
@ -27,16 +28,21 @@ log = logging.getLogger("EMBY."+__name__)
#################################################################################################
# Main methods
def window(property, value=None, clear=False, window_id=10000):
def window(property_, value=None, clear=False, window_id=10000):
# Get or set window property
WINDOW = xbmcgui.Window(window_id)
if clear:
WINDOW.clearProperty(property)
WINDOW.clearProperty(property_)
elif value is not None:
WINDOW.setProperty(property, value)
if ".json" in property_:
value = json.dumps(value)
WINDOW.setProperty(property_, value)
else:
return WINDOW.getProperty(property)
result = WINDOW.getProperty(property_)
if result and ".json" in property_:
result = json.loads(result)
return result
def settings(setting, value=None):
# Get or add addon setting
@ -51,9 +57,69 @@ def language(string_id):
# Central string retrieval - unicode
return xbmcaddon.Addon(id='plugin.video.emby').getLocalizedString(string_id)
def 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}", language(29999))
types = {
'yesno': d.yesno,
'ok': d.ok,
'notification': d.notification,
'input': d.input,
'select': d.select,
'numeric': d.numeric
}
return types[type_](*args, **kwargs)
class JSONRPC(object):
id_ = 1
jsonrpc = "2.0"
def __init__(self, method, **kwargs):
self.method = method
for arg in kwargs: # id_(int), jsonrpc(str)
self.arg = arg
def _query(self):
query = {
'jsonrpc': self.jsonrpc,
'id': self.id_,
'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()))
#################################################################################################
# Database related methods
def should_stop():
# Checkpoint during the syncing process
if xbmc.Monitor().abortRequested():
return True
elif window('emby_shouldStop') == "true":
return True
else: # Keep going
return False
def kodiSQL(media_type="video"):
if media_type == "emby":
@ -161,32 +227,19 @@ def querySQL(query, args=None, cursor=None, conntype=None):
def getScreensaver():
# Get the current screensaver value
query = {
'jsonrpc': "2.0",
'id': 0,
'method': "Settings.getSettingValue",
'params': {
'setting': "screensaver.mode"
}
}
return json.loads(xbmc.executeJSONRPC(json.dumps(query)))['result']['value']
result = JSONRPC('Settings.getSettingValues').execute({'setting': "screensaver.mode"})
try:
return result['result']['value']
except KeyError:
return ""
def setScreensaver(value):
# Toggle the screensaver
query = {
'jsonrpc': "2.0",
'id': 0,
'method': "Settings.setSettingValue",
'params': {
'setting': "screensaver.mode",
'value': value
}
params = {
'setting': "screensaver.mode",
'value': value
}
result = xbmc.executeJSONRPC(json.dumps(query))
result = JSONRPC('Settings.setSettingValue').execute(params)
log.info("Toggling screensaver: %s %s" % (value, result))
def convertDate(date):
@ -254,11 +307,28 @@ def indent(elem, level=0):
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def catch_except(errors=(Exception, ), default_value=False):
# Will wrap method with try/except and print parameters for easier debugging
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except errors as error:
log.exception(error)
log.error("function: %s \n args: %s \n kwargs: %s",
func.__name__, args, kwargs)
return default_value
return wrapper
return decorator
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()
@ -286,6 +356,7 @@ def reset():
return
# first stop any db sync
window('emby_online', value="reset")
window('emby_shouldStop', value="true")
count = 10
while window('emby_dbScan') == "true":
@ -340,36 +411,16 @@ def reset():
cursor.execute("DELETE FROM " + tablename)
cursor.execute('DROP table IF EXISTS emby')
cursor.execute('DROP table IF EXISTS view')
cursor.execute("DROP table IF EXISTS version")
connection.commit()
cursor.close()
# Offer to wipe cached thumbnails
resp = dialog.yesno(language(29999), language(33086))
if resp:
log.warn("Resetting all cached artwork.")
if dialog.yesno(language(29999), language(33086)):
log.warn("Resetting all cached artwork")
# Remove all existing textures first
path = xbmc.translatePath("special://thumbnails/").decode('utf-8')
if xbmcvfs.exists(path):
allDirs, allFiles = xbmcvfs.listdir(path)
for dir in allDirs:
allDirs, allFiles = xbmcvfs.listdir(path+dir)
for file in allFiles:
if os.path.supports_unicode_filenames:
xbmcvfs.delete(os.path.join(path+dir.decode('utf-8'),file.decode('utf-8')))
else:
xbmcvfs.delete(os.path.join(path.encode('utf-8')+dir,file))
# remove all existing data from texture DB
connection = kodiSQL('texture')
cursor = connection.cursor()
cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"')
rows = cursor.fetchall()
for row in rows:
tableName = row[0]
if(tableName != "version"):
cursor.execute("DELETE FROM " + tableName)
connection.commit()
cursor.close()
import artwork
artwork.Artwork().delete_cache()
# reset the install run flag
settings('SyncInstallRunDone', value="false")
@ -377,15 +428,18 @@ def reset():
# Remove emby info
resp = dialog.yesno(language(29999), language(33087))
if resp:
import connectmanager
# Delete the settings
addon = xbmcaddon.Addon()
addondir = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8')
addondir = xbmc.translatePath(
"special://profile/addon_data/plugin.video.emby/").decode('utf-8')
dataPath = "%ssettings.xml" % addondir
xbmcvfs.delete(dataPath)
log.info("Deleting: settings.xml")
connectmanager.ConnectManager().clear_data()
dialog.ok(heading=language(29999), line1=language(33088))
xbmc.executebuiltin('RestartApp')
return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, xbmcgui.ListItem())
def sourcesXML():
# To make Master lock compatible

View file

@ -62,23 +62,26 @@ class VideoNodes(object):
# Verify the video directory
if not xbmcvfs.exists(path):
shutil.copytree(
src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
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:
log.error(error)
xbmcvfs.exists(path)
if delete:
dirs, files = xbmcvfs.listdir(nodepath)
for file in files:
xbmcvfs.delete(nodepath + file)
log.info("Sucessfully removed videonode: %s." % tagname)
return
# Create the node directory
if not xbmcvfs.exists(nodepath) and not mediatype == "photos":
# We need to copy over the default items
xbmcvfs.mkdirs(nodepath)
else:
if delete:
dirs, files = xbmcvfs.listdir(nodepath)
for file in files:
xbmcvfs.delete(nodepath + file)
log.info("Sucessfully removed videonode: %s." % tagname)
return
# Create index entry
nodeXML = "%sindex.xml" % nodepath
@ -319,7 +322,7 @@ class VideoNodes(object):
nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
nodeXML = "%semby_%s.xml" % (nodepath, cleantagname)
path = "library://video/emby_%s.xml" % cleantagname
windowpath = "ActivateWindow(Video,%s,return)" % path
windowpath = "ActivateWindow(Videos,%s,return)" % path
# Create the video node directory
if not xbmcvfs.exists(nodepath):
@ -333,6 +336,7 @@ class VideoNodes(object):
'Favorite movies': 30180,
'Favorite tvshows': 30181,
'Favorite episodes': 30182,
'channels': 30173
}
label = lang(labels[tagname])
@ -349,6 +353,9 @@ class VideoNodes(object):
if itemtype == "channels":
root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2)
etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=0&mode=channels"
elif itemtype == "favourites" and mediatype == "episodes":
root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2)
etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s&folderid=favepisodes" %(tagname, mediatype)
else:
root = self.commonRoot(order=1, label=label, tagname=tagname)
etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"

View file

@ -8,14 +8,13 @@ import threading
import websocket
import xbmc
import xbmcgui
import clientinfo
import downloadutils
import librarysync
import playlist
import userclient
from utils import window, settings, language as lang
from utils import window, settings, dialog, language as lang, JSONRPC
##################################################################################################
@ -24,278 +23,316 @@ log = logging.getLogger("EMBY."+__name__)
##################################################################################################
class WebSocket_Client(threading.Thread):
class WebSocketClient(threading.Thread):
_shared_state = {}
client = None
stopWebsocket = False
_client = None
_stop_websocket = False
def __init__(self):
self.__dict__ = self._shared_state
self.monitor = xbmc.Monitor()
self.doUtils = downloadutils.DownloadUtils()
self.clientInfo = clientinfo.ClientInfo()
self.deviceId = self.clientInfo.getDeviceId()
self.librarySync = librarysync.LibrarySync()
self.doutils = downloadutils.DownloadUtils()
self.client_info = clientinfo.ClientInfo()
self.device_id = self.client_info.get_device_id()
self.library_sync = librarysync.LibrarySync()
threading.Thread.__init__(self)
def sendProgressUpdate(self, data):
def send_progress_update(self, data):
log.debug("sendProgressUpdate")
try:
messageData = {
message = {
'MessageType': "ReportPlaybackProgress",
'Data': data
}
messageString = json.dumps(messageData)
self.client.send(messageString)
log.debug("Message data: %s" % messageString)
message_str = json.dumps(message)
self._client.send(message_str)
log.debug("Message data: %s", message_str)
except Exception as e:
log.exception(e)
except Exception as error:
log.exception(error)
def on_message(self, ws, message):
result = json.loads(message)
messageType = result['MessageType']
message_type = result['MessageType']
data = result['Data']
dialog = xbmcgui.Dialog()
if messageType not in ('SessionEnded'):
if message_type not in ('NotificationAdded', 'SessionEnded', 'RestartRequired',
'PackageInstalling'):
# Mute certain events
log.info("Message: %s" % message)
log.info("Message: %s", message)
if messageType == "Play":
if message_type == 'Play':
# A remote control play command has been sent from the server.
itemIds = data['ItemIds']
command = data['PlayCommand']
self._play(data)
pl = playlist.Playlist()
if command == "PlayNow":
dialog.notification(
heading=lang(29999),
message="%s %s" % (len(itemIds), lang(33004)),
icon="special://home/addons/plugin.video.emby/icon.png",
sound=False)
startat = data.get('StartPositionTicks', 0)
pl.playAll(itemIds, startat)
elif command == "PlayNext":
dialog.notification(
heading=lang(29999),
message="%s %s" % (len(itemIds), lang(33005)),
icon="special://home/addons/plugin.video.emby/icon.png",
sound=False)
newplaylist = pl.modifyPlaylist(itemIds)
player = xbmc.Player()
if not player.isPlaying():
# Only start the playlist if nothing is playing
player.play(newplaylist)
elif messageType == "Playstate":
elif message_type == 'Playstate':
# A remote control update playstate command has been sent from the server.
command = data['Command']
player = xbmc.Player()
self._playstate(data)
actions = {
'Stop': player.stop,
'Unpause': player.pause,
'Pause': player.pause,
'NextTrack': player.playnext,
'PreviousTrack': player.playprevious,
'Seek': player.seekTime
}
action = actions[command]
if command == "Seek":
seekto = data['SeekPositionTicks']
seektime = seekto / 10000000.0
action(seektime)
log.info("Seek to %s." % seektime)
else:
action()
log.info("Command: %s completed." % command)
window('emby_command', value="true")
elif messageType == "UserDataChanged":
elif message_type == "UserDataChanged":
# A user changed their personal rating for an item, or their playstate was updated
userdata_list = data['UserDataList']
self.librarySync.triage_items("userdata", userdata_list)
self.library_sync.triage_items("userdata", userdata_list)
elif messageType == "LibraryChanged":
librarySync = self.librarySync
processlist = {
elif message_type == "LibraryChanged":
self._library_changed(data)
'added': data['ItemsAdded'],
'update': data['ItemsUpdated'],
'remove': data['ItemsRemoved']
}
for action in processlist:
librarySync.triage_items(action, processlist[action])
elif message_type == "GeneralCommand":
self._general_commands(data)
elif messageType == "GeneralCommand":
command = data['Name']
arguments = data['Arguments']
elif message_type == "ServerRestarting":
self._server_restarting()
if command in ('Mute', 'Unmute', 'SetVolume',
'SetSubtitleStreamIndex', 'SetAudioStreamIndex'):
player = xbmc.Player()
# These commands need to be reported back
if command == "Mute":
xbmc.executebuiltin('Mute')
elif command == "Unmute":
xbmc.executebuiltin('Mute')
elif command == "SetVolume":
volume = arguments['Volume']
xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume)
elif command == "SetAudioStreamIndex":
index = int(arguments['Index'])
player.setAudioStream(index - 1)
elif command == "SetSubtitleStreamIndex":
embyindex = int(arguments['Index'])
currentFile = player.getPlayingFile()
mapping = window('emby_%s.indexMapping' % currentFile)
if mapping:
externalIndex = json.loads(mapping)
# If there's external subtitles added via playbackutils
for index in externalIndex:
if externalIndex[index] == embyindex:
player.setSubtitleStream(int(index))
break
else:
# User selected internal subtitles
external = len(externalIndex)
audioTracks = len(player.getAvailableAudioStreams())
player.setSubtitleStream(external + embyindex - audioTracks - 1)
else:
# Emby merges audio and subtitle index together
audioTracks = len(player.getAvailableAudioStreams())
player.setSubtitleStream(index - audioTracks - 1)
# Let service know
window('emby_command', value="true")
elif command == "DisplayMessage":
header = arguments['Header']
text = arguments['Text']
dialog.notification(
heading=header,
message=text,
icon="special://home/addons/plugin.video.emby/icon.png",
time=4000)
elif command == "SendString":
string = arguments['String']
text = {
'jsonrpc': "2.0",
'id': 0,
'method': "Input.SendText",
'params': {
'text': "%s" % string,
'done': False
}
}
result = xbmc.executeJSONRPC(json.dumps(text))
else:
builtin = {
'ToggleFullscreen': 'Action(FullScreen)',
'ToggleOsdMenu': 'Action(OSD)',
'ToggleContextMenu': 'Action(ContextMenu)',
'MoveUp': 'Action(Up)',
'MoveDown': 'Action(Down)',
'MoveLeft': 'Action(Left)',
'MoveRight': 'Action(Right)',
'Select': 'Action(Select)',
'Back': 'Action(back)',
'GoHome': 'ActivateWindow(Home)',
'PageUp': 'Action(PageUp)',
'NextLetter': 'Action(NextLetter)',
'GoToSearch': 'VideoLibrary.Search',
'GoToSettings': 'ActivateWindow(Settings)',
'PageDown': 'Action(PageDown)',
'PreviousLetter': 'Action(PrevLetter)',
'TakeScreenshot': 'TakeScreenshot',
'ToggleMute': 'Mute',
'VolumeUp': 'Action(VolumeUp)',
'VolumeDown': 'Action(VolumeDown)',
}
action = builtin.get(command)
if action:
xbmc.executebuiltin(action)
elif messageType == "ServerRestarting":
if settings('supressRestartMsg') == "true":
dialog.notification(
heading=lang(29999),
message=lang(33006),
icon="special://home/addons/plugin.video.emby/icon.png")
elif messageType == "UserConfigurationUpdated":
elif message_type == "UserConfigurationUpdated":
# Update user data set in userclient
userclient.UserClient().userSettings = data
self.librarySync.refresh_views = True
userclient.UserClient().get_user(data)
self.library_sync.refresh_views = True
elif message_type == "ServerShuttingDown":
# Server went offline
window('emby_online', value="false")
@classmethod
def _play(cls, data):
item_ids = data['ItemIds']
command = data['PlayCommand']
playlist_ = playlist.Playlist()
if command == 'PlayNow':
startat = data.get('StartPositionTicks', 0)
playlist_.play_all(item_ids, startat)
dialog(type_="notification",
heading="{emby}",
message="%s %s" % (len(item_ids), lang(33004)),
icon="{emby}",
sound=False)
elif command == 'PlayNext':
new_playlist = playlist_.modify_playlist(item_ids)
dialog(type_="notification",
heading="{emby}",
message="%s %s" % (len(item_ids), lang(33005)),
icon="{emby}",
sound=False)
player = xbmc.Player()
if not player.isPlaying():
# Only start the playlist if nothing is playing
player.play(new_playlist)
@classmethod
def _playstate(cls, data):
command = data['Command']
player = xbmc.Player()
actions = {
'Stop': player.stop,
'Unpause': player.pause,
'Pause': player.pause,
'NextTrack': player.playnext,
'PreviousTrack': player.playprevious
}
if command == 'Seek':
seek_to = data['SeekPositionTicks']
seek_time = seek_to / 10000000.0
player.seekTime(seek_time)
log.info("Seek to %s", seek_time)
elif command in actions:
actions[command]()
log.info("Command: %s completed", command)
else:
log.info("Unknown command: %s", command)
return
window('emby_command', value="true")
def _library_changed(self, data):
process_list = {
'added': data['ItemsAdded'],
'update': data['ItemsUpdated'],
'remove': data['ItemsRemoved']
}
for action in process_list:
self.library_sync.triage_items(action, process_list[action])
@classmethod
def _general_commands(cls, data):
command = data['Name']
arguments = data['Arguments']
if command in ('Mute', 'Unmute', 'SetVolume',
'SetSubtitleStreamIndex', 'SetAudioStreamIndex'):
player = xbmc.Player()
# These commands need to be reported back
if command == 'Mute':
xbmc.executebuiltin('Mute')
elif command == 'Unmute':
xbmc.executebuiltin('Mute')
elif command == 'SetVolume':
volume = arguments['Volume']
xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume)
elif command == 'SetAudioStreamIndex':
index = int(arguments['Index'])
player.setAudioStream(index - 1)
elif command == 'SetSubtitleStreamIndex':
emby_index = int(arguments['Index'])
current_file = player.getPlayingFile()
mapping = window('emby_%s.indexMapping' % current_file)
if emby_index == -1:
player.showSubtitles(False)
elif mapping:
external_index = json.loads(mapping)
# If there's external subtitles added via playbackutils
for index in external_index:
if external_index[index] == emby_index:
player.setSubtitleStream(int(index))
break
else:
# User selected internal subtitles
external = len(external_index)
audio_tracks = len(player.getAvailableAudioStreams())
player.setSubtitleStream(external + emby_index - audio_tracks - 1)
else:
# Emby merges audio and subtitle index together
audio_tracks = len(player.getAvailableAudioStreams())
player.setSubtitleStream(emby_index - audio_tracks - 1)
# Let service know
window('emby_command', value="true")
elif command == 'DisplayMessage':
header = arguments['Header']
text = arguments['Text']
dialog(type_="notification",
heading=header,
message=text,
icon="{emby}",
time=int(settings('displayMessage'))*1000)
elif command == 'SendString':
params = {
'text': arguments['String'],
'done': False
}
JSONRPC('Input.SendText').execute(params)
elif command in ('MoveUp', 'MoveDown', 'MoveRight', 'MoveLeft'):
# Commands that should wake up display
actions = {
'MoveUp': "Input.Up",
'MoveDown': "Input.Down",
'MoveRight': "Input.Right",
'MoveLeft': "Input.Left"
}
JSONRPC(actions[command]).execute()
elif command == 'GoHome':
JSONRPC('GUI.ActivateWindow').execute({'window': "home"})
else:
builtin = {
'ToggleFullscreen': 'Action(FullScreen)',
'ToggleOsdMenu': 'Action(OSD)',
'ToggleContextMenu': 'Action(ContextMenu)',
'Select': 'Action(Select)',
'Back': 'Action(back)',
'PageUp': 'Action(PageUp)',
'NextLetter': 'Action(NextLetter)',
'GoToSearch': 'VideoLibrary.Search',
'GoToSettings': 'ActivateWindow(Settings)',
'PageDown': 'Action(PageDown)',
'PreviousLetter': 'Action(PrevLetter)',
'TakeScreenshot': 'TakeScreenshot',
'ToggleMute': 'Mute',
'VolumeUp': 'Action(VolumeUp)',
'VolumeDown': 'Action(VolumeDown)',
}
if command in builtin:
xbmc.executebuiltin(builtin[command])
@classmethod
def _server_restarting(cls):
if settings('supressRestartMsg') == "true":
dialog(type_="notification",
heading="{emby}",
message=lang(33006),
icon="{emby}")
window('emby_online', value="false")
def on_close(self, ws):
log.debug("Closed.")
log.debug("closed")
def on_open(self, ws):
self.doUtils.postCapabilities(self.deviceId)
self.doutils.post_capabilities(self.device_id)
def on_error(self, ws, error):
if "10061" in str(error):
# Server is offline
pass
else:
log.debug("Error: %s" % error)
log.debug("Error: %s", error)
def run(self):
loglevel = int(window('emby_logLevel'))
# websocket.enableTrace(True)
userId = window('emby_currUser')
server = window('emby_server%s' % userId)
token = window('emby_accessToken%s' % userId)
user_id = window('emby_currUser')
server = window('emby_server%s' % user_id)
token = window('emby_accessToken%s' % user_id)
# Get the appropriate prefix for the websocket
if "https" in server:
server = server.replace('https', "wss")
else:
server = server.replace('http', "ws")
websocket_url = "%s?api_key=%s&deviceId=%s" % (server, token, self.deviceId)
log.info("websocket url: %s" % websocket_url)
websocket_url = "%s?api_key=%s&deviceId=%s" % (server, token, self.device_id)
log.info("websocket url: %s", websocket_url)
self.client = websocket.WebSocketApp(websocket_url,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close)
self.client.on_open = self.on_open
self._client = websocket.WebSocketApp(websocket_url,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close)
self._client.on_open = self.on_open
log.warn("----===## Starting WebSocketClient ##===----")
while not self.monitor.abortRequested():
self.client.run_forever(ping_interval=10)
if self.stopWebsocket:
if window('emby_online') == "true":
self._client.run_forever(ping_interval=10)
if self._stop_websocket:
break
if self.monitor.waitForAbort(5):
@ -304,8 +341,8 @@ class WebSocket_Client(threading.Thread):
log.warn("##===---- WebSocketClient Stopped ----===##")
def stopClient(self):
def stop_client(self):
self.stopWebsocket = True
self.client.close()
log.info("Stopping thread.")
self._stop_websocket = True
self._client.close()
log.info("Stopping thread")