mirror of
https://github.com/jellyfin/jellyfin-kodi.git
synced 2025-05-14 13:25:07 +00:00
2.3.0 (#70)
This commit is contained in:
parent
d582888ffb
commit
ba22e26c06
80 changed files with 11580 additions and 6945 deletions
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
816
resources/lib/connect/connectionmanager.py
Normal file
816
resources/lib/connect/connectionmanager.py
Normal 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("&", '&')
|
||||
password = password.replace("/", '\')
|
||||
password = password.replace("!", '!')
|
||||
password = password.replace("$", '$')
|
||||
password = password.replace("\"", '"')
|
||||
password = password.replace("<", '<')
|
||||
password = password.replace(">", '>')
|
||||
password = password.replace("'", ''')
|
||||
|
||||
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)
|
147
resources/lib/connect/credentials.py
Normal file
147
resources/lib/connect/credentials.py
Normal 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
|
231
resources/lib/connectmanager.py
Normal file
231
resources/lib/connectmanager.py
Normal 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})
|
204
resources/lib/context_entry.py
Normal file
204
resources/lib/context_entry.py
Normal 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)
|
|
@ -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()
|
6
resources/lib/dialogs/__init__.py
Normal file
6
resources/lib/dialogs/__init__.py
Normal 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
|
93
resources/lib/dialogs/context.py
Normal file
93
resources/lib/dialogs/context.py
Normal 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)
|
136
resources/lib/dialogs/loginconnect.py
Normal file
136
resources/lib/dialogs/loginconnect.py
Normal 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')
|
145
resources/lib/dialogs/loginmanual.py
Normal file
145
resources/lib/dialogs/loginmanual.py
Normal 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')
|
145
resources/lib/dialogs/serverconnect.py
Normal file
145
resources/lib/dialogs/serverconnect.py
Normal 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
|
145
resources/lib/dialogs/servermanual.py
Normal file
145
resources/lib/dialogs/servermanual.py
Normal 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')
|
104
resources/lib/dialogs/usersconnect.py
Normal file
104
resources/lib/dialogs/usersconnect.py
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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((
|
||||
|
|
|
@ -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
164
resources/lib/ga_client.py
Normal 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'))
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
5
resources/lib/objects/__init__.py
Normal file
5
resources/lib/objects/__init__.py
Normal 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
|
202
resources/lib/objects/_common.py
Normal file
202
resources/lib/objects/_common.py
Normal 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
|
813
resources/lib/objects/_kodi_common.py
Normal file
813
resources/lib/objects/_kodi_common.py
Normal 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,))
|
225
resources/lib/objects/_kodi_movies.py
Normal file
225
resources/lib/objects/_kodi_movies.py
Normal 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,))
|
406
resources/lib/objects/_kodi_music.py
Normal file
406
resources/lib/objects/_kodi_music.py
Normal 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,))
|
66
resources/lib/objects/_kodi_musicvideos.py
Normal file
66
resources/lib/objects/_kodi_musicvideos.py
Normal 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,))
|
170
resources/lib/objects/_kodi_tvshows.py
Normal file
170
resources/lib/objects/_kodi_tvshows.py
Normal 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,))
|
453
resources/lib/objects/movies.py
Normal file
453
resources/lib/objects/movies.py
Normal 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)
|
709
resources/lib/objects/music.py
Normal file
709
resources/lib/objects/music.py
Normal 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)
|
299
resources/lib/objects/musicvideos.py
Normal file
299
resources/lib/objects/musicvideos.py
Normal 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)
|
818
resources/lib/objects/tvshows.py
Normal file
818
resources/lib/objects/tvshows.py
Normal 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)
|
|
@ -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'])
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
320
resources/lib/service_entry.py
Normal file
320
resources/lib/service_entry.py
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue