Movie Sync Alpha version

This commit is contained in:
tomkat83 2015-12-28 18:47:16 +01:00
parent 488c1a0a19
commit 88f649d36d
4 changed files with 620 additions and 97 deletions

View file

@ -50,6 +50,8 @@ from threading import Thread
import Queue
import traceback
import re
try:
import xml.etree.cElementTree as etree
except ImportError:
@ -85,6 +87,8 @@ class PlexAPI():
self.plexversion = client.getVersion()
self.platform = client.getPlatform()
self.doUtils = downloadutils.DownloadUtils()
def logMsg(self, msg, lvl=1):
className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
@ -156,7 +160,7 @@ class PlexAPI():
url = url + '/clients'
self.logMsg("CheckConnection called for url %s with a token" % url, 2)
r = downloadutils.DownloadUtils().downloadUrl(
r = self.doUtils.downloadUrl(
url,
authenticate=False,
headerOptions={'X-Plex-Token': token}
@ -1233,33 +1237,467 @@ class PlexAPI():
})
return serverlist
def GetPlexCollections(self, type):
# Build a list of the user views
def GetPlexCollections(self, mediatype):
"""
Input:
mediatype String or list of strings with possible values
'movie', 'show', 'artist', 'photo'
Output:
Collection containing only mediatype. List with entry of the form:
{
'name': xxx Plex title for the media section
'type': xxx Plex type: 'movie', 'show', 'artist', 'photo'
'id': xxx Plex unique key for the section
}
"""
collections = []
url = "{server}/library/sections"
jsondata = self.doUtils.downloadUrl(url)
try:
result = jsondata['_children']
except:
except KeyError:
pass
else:
for item in result:
# name = item['Name']
name = item['title']
# contentType = item['Type']
contentType = item['type']
itemtype = contentType
content = itemtype
contentID = item['key']
if itemtype == type and name not in ("Collections", "Trailers"):
if contentType in mediatype:
name = item['title']
contentId = item['key']
collections.append({
'title': name,
'type': itemtype,
'id': contentID,
'content': content
'name': name,
'type': contentType,
'id': str(contentId)
})
return collections
def GetPlexSectionResults(self, viewId):
"""
Returns a list (raw API dump) of all Plex movies in the Plex section
with key = viewId.
"""
result = []
url = "{server}/library/sections/%s/all" % viewId
jsondata = self.doUtils.downloadUrl(url)
try:
result = jsondata['_children']
except KeyError:
pass
return result
def GetPlexMetadata(self, key):
"""
Returns raw API metadata for key.
"""
result = []
url = "{server}" + key
jsondata = self.doUtils.downloadUrl(url)
try:
result = jsondata['_children'][0]
except KeyError:
self.logMsg("Error retrieving metadata for %s" % url, 1)
pass
return result
class API():
def __init__(self, item):
self.item = item
self.clientinfo = clientinfo.ClientInfo()
self.addonName = self.clientinfo.getAddonName()
def logMsg(self, msg, lvl=1):
className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
def convert_date(self, stamp):
"""
convert_date(stamp) converts a Unix time stamp (seconds passed since
January 1 1970) to a propper, human-readable time stamp
"""
# DATEFORMAT = xbmc.getRegion('dateshort')
# TIMEFORMAT = xbmc.getRegion('meridiem')
# date_time = time.localtime(stamp)
# if DATEFORMAT[1] == 'd':
# localdate = time.strftime('%d-%m-%Y', date_time)
# elif DATEFORMAT[1] == 'm':
# localdate = time.strftime('%m-%d-%Y', date_time)
# else:
# localdate = time.strftime('%Y-%m-%d', date_time)
# if TIMEFORMAT != '/':
# localtime = time.strftime('%I:%M%p', date_time)
# else:
# localtime = time.strftime('%H:%M', date_time)
# return localtime + ' ' + localdate
DATEFORMAT = xbmc.getRegion('dateshort')
TIMEFORMAT = xbmc.getRegion('meridiem')
date_time = time.localtime(stamp)
localdate = time.strftime('%Y-%m-%d', date_time)
return localdate
def getChecksum(self):
"""
Maybe get rid of viewOffset = (resume point)?!?
"""
item = self.item
checksum = "%s%s%s%s%s" % (
item['key'],
item['updatedAt'],
item.get('viewCount', ""),
item.get('lastViewedAt', ""),
item.get('viewOffset', "")
)
return checksum
def getDateCreated(self):
item = self.item
try:
dateadded = item['addedAt']
dateadded = self.convert_date(dateadded)
except KeyError:
dateadded = None
return dateadded
def getUserData(self):
item = self.item
# Default
favorite = False
playcount = None
played = False
lastPlayedDate = None
resume = 0
rating = 0
try:
playcount = int(item['viewCount'])
except KeyError:
playcount = None
if playcount:
played = True
try:
lastPlayedDate = int(item['lastViewedAt'])
lastPlayedDate = self.convert_date(lastPlayedDate)
except KeyError:
lastPlayedDate = None
try:
resume = int(item['viewOffset'])
except KeyError:
resume = 0
return {
'Favorite': favorite,
'PlayCount': playcount,
'Played': played,
'LastPlayedDate': lastPlayedDate,
'Resume': resume,
'Rating': rating
}
def getPeople(self):
"""
returns a dictionary of lists of people found in item.
{
'Director': list,
'Writer': list,
'Cast': list,
'Producer': list
}
"""
item = self.item
item = item['_children']
# Process People
director = []
writer = []
cast = []
producer = []
for entry in item:
if entry['_elementType'] == 'Director':
director.append(entry['tag'])
elif entry['_elementType'] == 'Writer':
writer.append(entry['tag'])
elif entry['_elementType'] == 'Role':
cast.append(entry['tag'])
elif entry['_elementType'] == 'Producer':
producer.append(entry['tag'])
return {
'Director': director,
'Writer': writer,
'Cast': cast,
'Producer': producer
}
def getPeopleList(self):
"""
Returns a list of people from item, with a list item of the form
{
'Name': xxx,
'Type': xxx,
'Id': xxx
('Role': xxx for cast/actors only)
}
"""
item = self.item['_children']
people = []
# Key of library: Plex-identifier. Value represents the Kodi/emby side
people_of_interest = {
'Director': 'Director',
'Writer': 'Writer',
'Role': 'Actor',
'Producer': 'Producer'
}
for entry in item:
if entry['_elementType'] in people_of_interest.keys():
name = entry['tag']
name_id = entry['id']
Type = entry['_elementType']
Type = people_of_interest[Type]
if Type == 'Actor':
Role = entry['role']
people.append({
'Name': name,
'Type': Type,
'Id': name_id,
'Role': Role
})
else:
people.append({
'Name': name,
'Type': Type,
'Id': name_id
})
return people
def getGenres(self):
"""
returns a list of genres found in item. (Not a string!!)
"""
item = self.item
item = item['_children']
genre = []
for entry in item:
if entry['_elementType'] == 'Genre':
genre.append(entry['tag'])
return genre
def getProvider(self, providername):
"""
provider = getProvider(self, item, providername)
providername: imdb, tvdb, musicBrainzArtist, musicBrainzAlbum,
musicBrainzTrackId
Return IMDB: "tt1234567"
"""
item = self.item
imdb_regex = re.compile(r'''(
imdb:// # imdb tag, which will be followed be tt1234567
(tt\d{7}) # actual IMDB ID, e.g. tt1234567
\?? # zero or one ?
(.*) # rest, e.g. language setting
)''', re.VERBOSE)
try:
if "Imdb" in providername:
provider = imdb_regex.findall(item['guid'])
provider = provider[0][1]
elif "tvdb" in providername:
provider = item['ProviderIds']['Tvdb']
elif "musicBrainzArtist" in providername:
provider = item['ProviderIds']['MusicBrainzArtist']
elif "musicBrainzAlbum" in providername:
provider = item['ProviderIds']['MusicBrainzAlbum']
elif "musicBrainzTrackId" in providername:
provider = item['ProviderIds']['MusicBrainzTrackId']
except:
provider = None
return provider
def GetTitle(self):
item = self.item
title = item['title']
try:
sorttitle = item['titleSort']
except KeyError:
sorttitle = title
return title, sorttitle
def getRuntime(self):
"""
Resume point of time and runtime/totaltime. Rounded to 6th decimal.
Assumption: time for both resume and runtime is measured in
milliseconds on the Plex side and in seconds on the Kodi side.
"""
item = self.item
time_factor = 1/1000
runtime = item['duration'] * time_factor
try:
resume = item['viewOffset'] * time_factor
except KeyError:
resume = 0
resume = round(float(resume), 6)
runtime = round(float(runtime), 6)
return resume, runtime
def getMpaa(self):
# Convert more complex cases
item = self.item
try:
mpaa = item['contentRating']
except KeyError:
mpaa = None
if mpaa in ("NR", "UR"):
# Kodi seems to not like NR, but will accept Rated Not Rated
mpaa = "Rated Not Rated"
return mpaa
def getCountry(self):
"""
Returns a list of all countries found in item.
"""
item = self.item
item = item['_children']
country = []
for entry in item:
if entry['_elementType'] == 'Country':
country.append(entry['tag'])
return country
def getStudios(self):
item = self.item
studio = []
try:
studio.append(self.getStudio(item['studio']))
except KeyError:
pass
return studio
def getStudio(self, studioName):
# Convert studio for Kodi to properly detect them
studios = {
'abc (us)': "ABC",
'fox (us)': "FOX",
'mtv (us)': "MTV",
'showcase (ca)': "Showcase",
'wgn america': "WGN"
}
return studios.get(studioName.lower(), studioName)
def joinList(self, listobject):
"""
Smart-joins the list into a single string using a " / " separator.
If the list is empty, smart_join returns an empty string.
"""
string = " / ".join(listobject)
return string
def getFilePath(self):
item = self.item
try:
filepath = item['key']
except KeyError:
filepath = ""
else:
if "\\\\" in filepath:
# append smb protocol
filepath = filepath.replace("\\\\", "smb://")
filepath = filepath.replace("\\", "/")
if item.get('VideoType'):
videotype = item['VideoType']
# Specific format modification
if 'Dvd'in videotype:
filepath = "%s/VIDEO_TS/VIDEO_TS.IFO" % filepath
elif 'Bluray' in videotype:
filepath = "%s/BDMV/index.bdmv" % filepath
if "\\" in filepath:
# Local path scenario, with special videotype
filepath = filepath.replace("/", "\\")
return filepath
def getMediaStreams(self):
item = self.item
item = item['_children']
videotracks = []
audiotracks = []
subtitlelanguages = []
MediaStreams = []
aspectratio = None
for entry in item:
if entry['_elementType'] == 'Media':
MediaStreams.append(entry)
try:
aspectratio = entry['aspectRatio']
except KeyError:
pass
# Abort if no Media found
if not MediaStreams:
return
# Loop over parts:
# TODO: what if several Media tags exist?!?
for part in MediaStreams[0]['_children']:
container = part['container'].lower()
for mediaStream in part['_children']:
try:
type = mediaStream['streamType']
except KeyError:
type = None
if type == 1: # Video streams
videotrack = {}
videotrack['videocodec'] = mediaStream['codec'].lower()
if "msmpeg4" in videotrack['videocodec']:
videotrack['videocodec'] = "divx"
elif "mpeg4" in videotrack['videocodec']:
# if "simple profile" in profile or profile == "":
# videotrack['videocodec'] = "xvid"
pass
elif "h264" in videotrack['videocodec']:
if container in ("mp4", "mov", "m4v"):
videotrack['videocodec'] = "avc1"
videotrack['height'] = mediaStream.get('height')
videotrack['width'] = mediaStream.get('width')
# TODO: 3d Movies?!?
# videotrack['Video3DFormat'] = item.get('Video3DFormat')
try:
aspectratio = mediaStream['aspectRatio']
except KeyError:
if not aspectratio:
aspectratio = round(float(videotrack['width'] / videotrack['height']), 6)
videotrack['aspectratio'] = aspectratio
# TODO: Video 3d format
videotrack['video3DFormat'] = None
videotracks.append(videotrack)
elif type == 2: # Audio streams
audiotrack = {}
audiotrack['audiocodec'] = mediaStream['codec'].lower()
profile = mediaStream['codecID'].lower()
if "dca" in audiotrack['audiocodec'] and "dts-hd ma" in profile:
audiotrack['audiocodec'] = "dtshd_ma"
audiotrack['channels'] = mediaStream.get('channels')
try:
audiotrack['audiolanguage'] = mediaStream.get('language')
except KeyError:
audiotrack['audiolanguage'] = 'unknown'
audiotracks.append(audiotrack)
elif type == 3: # Subtitle streams
try:
subtitlelanguages.append(mediaStream['language'])
except:
subtitlelanguages.append("Unknown")
return {
'video': videotracks,
'audio': audiotracks,
'subtitle': subtitlelanguages
}

View file

@ -427,9 +427,7 @@ class Artwork():
server = self.server
id = item['Id']
artworks = item['ImageTags']
backdrops = item['BackdropImageTags']
id = item['key']
maxHeight = 10000
maxWidth = 10000
@ -453,26 +451,20 @@ class Artwork():
}
# Process backdrops
backdropIndex = 0
for backdroptag in backdrops:
artwork = (
"%s/emby/Items/%s/Images/Backdrop/%s?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (server, id, backdropIndex,
maxWidth, maxHeight, backdroptag, customquery))
allartworks['Backdrop'].append(artwork)
backdropIndex += 1
# Process the rest of the artwork
for art 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"
% (server, id, art, maxWidth, maxHeight, tag, customquery))
allartworks[art] = artwork
# Get background artwork URL
try:
background = item['art']
background = "%s%s" % (server, background)
except KeyError:
background = ""
allartworks['Backdrop'].append(background)
# Get primary "thumb" pictures:
try:
primary = item['thumb']
primary = "%s%s" % (server, primary)
except KeyError:
primary = ""
allartworks['Primary'] = primary
# Process parent items if the main item is missing artwork
if parentInfo:

View file

@ -19,6 +19,8 @@ import embydb_functions as embydb
import kodidb_functions as kodidb
import read_embyserver as embyserver
import PlexAPI
##################################################################################################
@ -270,19 +272,19 @@ class Movies(Items):
emby_db = self.emby_db
kodi_db = self.kodi_db
artwork = self.artwork
API = api.API(item)
API = PlexAPI.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']
itemid = item['key']
emby_dbitem = emby_db.getItem_byId(itemid)
try:
movieid = emby_dbitem[0]
fileid = emby_dbitem[1]
pathid = emby_dbitem[2]
self.logMsg("movieid: %s fileid: %s pathid: %s" % (movieid, fileid, pathid), 1)
except TypeError:
update_item = False
self.logMsg("movieid: %s not found." % itemid, 2)
@ -290,11 +292,6 @@ class Movies(Items):
kodicursor.execute("select coalesce(max(idMovie),0) from movie")
movieid = kodicursor.fetchone()[0] + 1
if not viewtag or not viewid:
# Get view tag from emby
viewtag, viewid, mediatype = self.emby.getView_embyId(itemid)
self.logMsg("View tag found: %s" % viewtag, 2)
# fileId information
checksum = API.getChecksum()
dateadded = API.getDateCreated()
@ -304,52 +301,31 @@ class Movies(Items):
# item details
people = API.getPeople()
writer = " / ".join(people['Writer'])
director = " / ".join(people['Director'])
genres = item['Genres']
title = item['Name']
plot = API.getOverview()
shortplot = item.get('ShortOverview')
tagline = API.getTagline()
votecount = item.get('VoteCount')
rating = item.get('CommunityRating')
year = item.get('ProductionYear')
writer = API.joinList(people['Writer'])
director = API.joinList(people['Director'])
genres = API.getGenres()
title, sorttitle = API.GetTitle()
plot = item['summary']
shortplot = None
tagline = item.get('tagline', '')
votecount = 0
rating = item.get('audienceRating', None)
year = item.get('year', None)
imdb = API.getProvider('Imdb')
sorttitle = item['SortName']
runtime = API.getRuntime()
resume, runtime = API.getRuntime()
mpaa = API.getMpaa()
genre = " / ".join(genres)
country = API.getCountry()
genre = API.joinList(genres)
countries = API.getCountry()
country = API.joinList(countries)
studios = API.getStudios()
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.doUtils.downloadUrl(url)
trailer = "plugin://plugin.video.plexkodiconnect/trailer/?id=%s&mode=play" % result[0]['Id']
else:
# Try to get the youtube trailer
try:
trailer = item['RemoteTrailers'][0]['Url']
except (KeyError, IndexError):
trailer = None
else:
try:
trailerId = trailer.rsplit('=', 1)[1]
except IndexError:
self.logMsg("Failed to process trailer: %s" % trailer)
trailer = None
else:
trailer = "plugin://plugin.video.youtube/play/?video_id=%s" % trailerId
# TODO: trailers
trailer = None
##### GET THE FILE AND PATH #####
playurl = API.getFilePath()
@ -455,9 +431,11 @@ class Movies(Items):
kodicursor.execute(query, (pathid, filename, dateadded, fileid))
# Process countries
kodi_db.addCountries(movieid, item['ProductionLocations'], "movie")
kodi_db.addCountries(movieid, countries, "movie")
# Process cast
people = artwork.getPeopleArtwork(item['People'])
people = API.getPeopleList()
# TODO: get IMDB pictures?
people = artwork.getPeopleArtwork(people)
kodi_db.addPeople(movieid, people, "movie")
# Process genres
kodi_db.addGenres(movieid, genres, "movie")
@ -469,13 +447,13 @@ class Movies(Items):
# Process studios
kodi_db.addStudios(movieid, studios, "movie")
# Process tags: view, emby tags
tags = [viewtag]
tags.extend(item['Tags'])
if userdata['Favorite']:
tags.append("Favorite movies")
kodi_db.addTags(movieid, tags, "movie")
# tags = [viewtag]
# tags.extend(item['Tags'])
# if userdata['Favorite']:
# tags.append("Favorite movies")
# kodi_db.addTags(movieid, tags, "movie")
# Process playstates
resume = API.adjustResume(userdata['Resume'])
# resume = API.adjustResume(userdata['Resume'])
total = round(float(runtime), 6)
kodi_db.addPlaystate(fileid, resume, total, playcount, dateplayed)

View file

@ -21,6 +21,8 @@ import read_embyserver as embyserver
import userclient
import videonodes
import PlexAPI
##################################################################################################
@ -51,6 +53,7 @@ class LibrarySync(threading.Thread):
self.user = userclient.UserClient()
self.emby = embyserver.Read_EmbyServer()
self.vnodes = videonodes.VideoNodes()
self.plx = PlexAPI.PlexAPI()
threading.Thread.__init__(self)
@ -76,7 +79,7 @@ class LibrarySync(threading.Thread):
if utils.settings('SyncInstallRunDone') == "true":
# Validate views
self.refreshViews()
# self.refreshViews()
completed = False
# Verify if server plugin is installed.
if utils.settings('serverSync') == "true":
@ -235,7 +238,7 @@ class LibrarySync(threading.Thread):
}
process = {
'movies': self.movies,
'movies': self.PlexMovies,
}
for itemtype in process:
startTime = datetime.now()
@ -461,6 +464,118 @@ class LibrarySync(threading.Thread):
utils.window('Emby.nodes.total', str(totalnodes))
def PlexMovies(self, embycursor, kodicursor, pdialog, compare=False):
# Get movies from emby
emby = self.emby
emby_db = embydb.Embydb_Functions(embycursor)
movies = itemtypes.Movies(embycursor, kodicursor)
views = self.plx.GetPlexCollections('movies')
self.logMsg("Media folders: %s" % views, 1)
if compare:
# Pull the list of movies and boxsets in Kodi
try:
all_kodimovies = dict(emby_db.getChecksum('Movie'))
except ValueError:
all_kodimovies = {}
try:
all_kodisets = dict(emby_db.getChecksum('BoxSet'))
except ValueError:
all_kodisets = {}
all_embymoviesIds = set()
all_embyboxsetsIds = set()
updatelist = []
##### PROCESS MOVIES #####
for view in views:
if self.shouldStop():
return False
# Get items per view
viewId = view['id']
viewName = view['name']
if pdialog:
pdialog.update(
heading=self.addonName,
message="Gathering movies from view: %s..." % viewName)
if compare:
# Manual sync
if pdialog:
pdialog.update(
heading=self.addonName,
message="Comparing movies from view: %s..." % viewName)
all_embymovies = self.plx.GetPlexSectionResults(viewId)
embymovies = []
for embymovie in all_embymovies:
if self.shouldStop():
return False
API = PlexAPI.API(embymovie)
itemid = embymovie['key']
all_embymoviesIds.add(itemid)
if all_kodimovies.get(itemid) != API.getChecksum():
# Only update if movie is not in Kodi or checksum is different
updatelist.append(itemid)
embymovies.append(embymovie)
self.logMsg("Movies to update for %s: %s" % (viewName, updatelist), 1)
total = len(updatelist)
del updatelist[:]
else:
# Initial or repair sync
embymovies = self.plx.GetPlexMovies(viewId)
total = len(embymovies)
if pdialog:
pdialog.update(heading="Processing %s / %s items" % (viewName, total))
count = 0
for embymovie in embymovies:
# Process individual movies
if self.shouldStop():
return False
title = embymovie['title']
if pdialog:
percentage = int((float(count) / float(total))*100)
pdialog.update(percentage, message=title)
count += 1
detailed = self.plx.GetPlexMetadata(embymovie['key'])
movies.add_update(detailed, viewName, viewId)
else:
self.logMsg("Movies finished.", 2)
##### PROCESS DELETES #####
if compare:
# Manual sync, process deletes
for kodimovie in all_kodimovies:
if kodimovie not in all_embymoviesIds:
movies.remove(kodimovie)
else:
self.logMsg("Movies compare finished.", 1)
for boxset in all_kodisets:
if boxset not in all_embyboxsetsIds:
movies.remove(boxset)
else:
self.logMsg("Boxsets compare finished.", 1)
return True
def movies(self, embycursor, kodicursor, pdialog, compare=False):
# Get movies from emby
emby = self.emby