commit
c5a720aea9
27 changed files with 2015 additions and 2188 deletions
|
@ -15,7 +15,7 @@ from xbmcgui import ListItem
|
||||||
from . import utils
|
from . import utils
|
||||||
from . import path_ops
|
from . import path_ops
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from .plex_api import API
|
from .plex_api import API, mass_api
|
||||||
from . import plex_functions as PF
|
from . import plex_functions as PF
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
# Be careful - your using app in another Python instance!
|
# Be careful - your using app in another Python instance!
|
||||||
|
@ -217,12 +217,12 @@ def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None,
|
||||||
# Need to chain keys for navigation
|
# Need to chain keys for navigation
|
||||||
widgets.KEY = key
|
widgets.KEY = key
|
||||||
# Process all items to show
|
# Process all items to show
|
||||||
widgets.attach_kodi_ids(xml)
|
all_items = mass_api(xml)
|
||||||
all_items = widgets.process_method_on_list(widgets.generate_item, xml)
|
all_items = utils.process_method_on_list(widgets.generate_item, all_items)
|
||||||
all_items = widgets.process_method_on_list(widgets.prepare_listitem,
|
all_items = utils.process_method_on_list(widgets.prepare_listitem,
|
||||||
all_items)
|
all_items)
|
||||||
# fill that listing...
|
# fill that listing...
|
||||||
all_items = widgets.process_method_on_list(widgets.create_listitem,
|
all_items = utils.process_method_on_list(widgets.create_listitem,
|
||||||
all_items)
|
all_items)
|
||||||
xbmcplugin.addDirectoryItems(int(sys.argv[1]), all_items, len(all_items))
|
xbmcplugin.addDirectoryItems(int(sys.argv[1]), all_items, len(all_items))
|
||||||
# end directory listing
|
# end directory listing
|
||||||
|
@ -397,13 +397,13 @@ def hub(content_type):
|
||||||
for entry in reversed(xml):
|
for entry in reversed(xml):
|
||||||
api = API(entry)
|
api = API(entry)
|
||||||
append = False
|
append = False
|
||||||
if content_type == 'video' and api.plex_type() in v.PLEX_VIDEOTYPES:
|
if content_type == 'video' and api.plex_type in v.PLEX_VIDEOTYPES:
|
||||||
append = True
|
append = True
|
||||||
elif content_type == 'audio' and api.plex_type() in v.PLEX_AUDIOTYPES:
|
elif content_type == 'audio' and api.plex_type in v.PLEX_AUDIOTYPES:
|
||||||
append = True
|
append = True
|
||||||
elif content_type == 'image' and api.plex_type() == v.PLEX_TYPE_PHOTO:
|
elif content_type == 'image' and api.plex_type == v.PLEX_TYPE_PHOTO:
|
||||||
append = True
|
append = True
|
||||||
elif content_type != 'image' and api.plex_type() == v.PLEX_TYPE_PLAYLIST:
|
elif content_type != 'image' and api.plex_type == v.PLEX_TYPE_PLAYLIST:
|
||||||
append = True
|
append = True
|
||||||
elif content_type is None:
|
elif content_type is None:
|
||||||
# Needed for widgets, where no content_type is provided
|
# Needed for widgets, where no content_type is provided
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Movie(ItemBase):
|
||||||
Process single movie
|
Process single movie
|
||||||
"""
|
"""
|
||||||
api = API(xml)
|
api = API(xml)
|
||||||
plex_id = api.plex_id()
|
plex_id = api.plex_id
|
||||||
# Cannot parse XML, abort
|
# Cannot parse XML, abort
|
||||||
if not plex_id:
|
if not plex_id:
|
||||||
LOG.error('Cannot parse XML data for movie: %s', xml.attrib)
|
LOG.error('Cannot parse XML data for movie: %s', xml.attrib)
|
||||||
|
@ -35,20 +35,6 @@ class Movie(ItemBase):
|
||||||
update_item = False
|
update_item = False
|
||||||
kodi_id = self.kodidb.new_movie_id()
|
kodi_id = self.kodidb.new_movie_id()
|
||||||
|
|
||||||
userdata = api.userdata()
|
|
||||||
playcount = userdata['PlayCount']
|
|
||||||
dateplayed = userdata['LastPlayedDate']
|
|
||||||
resume = userdata['Resume']
|
|
||||||
runtime = userdata['Runtime']
|
|
||||||
rating = userdata['Rating']
|
|
||||||
|
|
||||||
title = api.title()
|
|
||||||
people = api.people()
|
|
||||||
genres = api.genre_list()
|
|
||||||
collections = api.collection_list()
|
|
||||||
countries = api.country_list()
|
|
||||||
studios = api.music_studio_list()
|
|
||||||
|
|
||||||
# GET THE FILE AND PATH #####
|
# GET THE FILE AND PATH #####
|
||||||
do_indirect = not app.SYNC.direct_paths
|
do_indirect = not app.SYNC.direct_paths
|
||||||
if app.SYNC.direct_paths:
|
if app.SYNC.direct_paths:
|
||||||
|
@ -58,7 +44,7 @@ class Movie(ItemBase):
|
||||||
# Something went wrong, trying to use non-direct paths
|
# Something went wrong, trying to use non-direct paths
|
||||||
do_indirect = True
|
do_indirect = True
|
||||||
else:
|
else:
|
||||||
playurl = api.validate_playurl(playurl, api.plex_type())
|
playurl = api.validate_playurl(playurl, api.plex_type)
|
||||||
if playurl is None:
|
if playurl is None:
|
||||||
return False
|
return False
|
||||||
if '\\' in playurl:
|
if '\\' in playurl:
|
||||||
|
@ -92,7 +78,7 @@ class Movie(ItemBase):
|
||||||
self.kodidb.update_ratings(kodi_id,
|
self.kodidb.update_ratings(kodi_id,
|
||||||
v.KODI_TYPE_MOVIE,
|
v.KODI_TYPE_MOVIE,
|
||||||
"default",
|
"default",
|
||||||
rating,
|
api.rating(),
|
||||||
api.votecount(),
|
api.votecount(),
|
||||||
rating_id)
|
rating_id)
|
||||||
# update new uniqueid Kodi 17
|
# update new uniqueid Kodi 17
|
||||||
|
@ -109,13 +95,13 @@ class Movie(ItemBase):
|
||||||
uniqueid = -1
|
uniqueid = -1
|
||||||
self.kodidb.modify_people(kodi_id,
|
self.kodidb.modify_people(kodi_id,
|
||||||
v.KODI_TYPE_MOVIE,
|
v.KODI_TYPE_MOVIE,
|
||||||
api.people_list())
|
api.people())
|
||||||
if app.SYNC.artwork:
|
if app.SYNC.artwork:
|
||||||
self.kodidb.modify_artwork(api.artwork(),
|
self.kodidb.modify_artwork(api.artwork(),
|
||||||
kodi_id,
|
kodi_id,
|
||||||
v.KODI_TYPE_MOVIE)
|
v.KODI_TYPE_MOVIE)
|
||||||
else:
|
else:
|
||||||
LOG.info("ADD movie plex_id: %s - %s", plex_id, title)
|
LOG.info("ADD movie plex_id: %s - %s", plex_id, api.title())
|
||||||
file_id = self.kodidb.add_file(filename,
|
file_id = self.kodidb.add_file(filename,
|
||||||
kodi_pathid,
|
kodi_pathid,
|
||||||
api.date_created())
|
api.date_created())
|
||||||
|
@ -124,7 +110,7 @@ class Movie(ItemBase):
|
||||||
kodi_id,
|
kodi_id,
|
||||||
v.KODI_TYPE_MOVIE,
|
v.KODI_TYPE_MOVIE,
|
||||||
"default",
|
"default",
|
||||||
rating,
|
api.rating(),
|
||||||
api.votecount())
|
api.votecount())
|
||||||
if api.provider('imdb') is not None:
|
if api.provider('imdb') is not None:
|
||||||
uniqueid = self.kodidb.add_uniqueid_id()
|
uniqueid = self.kodidb.add_uniqueid_id()
|
||||||
|
@ -137,7 +123,7 @@ class Movie(ItemBase):
|
||||||
uniqueid = -1
|
uniqueid = -1
|
||||||
self.kodidb.add_people(kodi_id,
|
self.kodidb.add_people(kodi_id,
|
||||||
v.KODI_TYPE_MOVIE,
|
v.KODI_TYPE_MOVIE,
|
||||||
api.people_list())
|
api.people())
|
||||||
if app.SYNC.artwork:
|
if app.SYNC.artwork:
|
||||||
self.kodidb.add_artwork(api.artwork(),
|
self.kodidb.add_artwork(api.artwork(),
|
||||||
kodi_id,
|
kodi_id,
|
||||||
|
@ -146,37 +132,39 @@ class Movie(ItemBase):
|
||||||
# Update Kodi's main entry
|
# Update Kodi's main entry
|
||||||
self.kodidb.add_movie(kodi_id,
|
self.kodidb.add_movie(kodi_id,
|
||||||
file_id,
|
file_id,
|
||||||
title,
|
api.title(),
|
||||||
api.plot(),
|
api.plot(),
|
||||||
api.shortplot(),
|
api.shortplot(),
|
||||||
api.tagline(),
|
api.tagline(),
|
||||||
api.votecount(),
|
api.votecount(),
|
||||||
rating_id,
|
rating_id,
|
||||||
api.list_to_string(people['Writer']),
|
api.list_to_string(api.writers()),
|
||||||
api.year(),
|
api.year(),
|
||||||
uniqueid,
|
uniqueid,
|
||||||
api.sorttitle(),
|
api.sorttitle(),
|
||||||
runtime,
|
api.runtime(),
|
||||||
api.content_rating(),
|
api.content_rating(),
|
||||||
api.list_to_string(genres),
|
api.list_to_string(api.genres()),
|
||||||
api.list_to_string(people['Director']),
|
api.list_to_string(api.directors()),
|
||||||
title,
|
api.title(),
|
||||||
api.list_to_string(studios),
|
api.list_to_string(api.studios()),
|
||||||
api.trailer(),
|
api.trailer(),
|
||||||
api.list_to_string(countries),
|
api.list_to_string(api.countries()),
|
||||||
playurl,
|
playurl,
|
||||||
kodi_pathid,
|
kodi_pathid,
|
||||||
api.premiere_date(),
|
api.premiere_date(),
|
||||||
userdata['UserRating'])
|
api.userrating())
|
||||||
|
|
||||||
self.kodidb.modify_countries(kodi_id, v.KODI_TYPE_MOVIE, countries)
|
self.kodidb.modify_countries(kodi_id,
|
||||||
self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_MOVIE, genres)
|
v.KODI_TYPE_MOVIE,
|
||||||
|
api.countries())
|
||||||
|
self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_MOVIE, api.genres())
|
||||||
|
|
||||||
self.kodidb.modify_streams(file_id, api.mediastreams(), runtime)
|
self.kodidb.modify_streams(file_id, api.mediastreams(), api.runtime())
|
||||||
self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_MOVIE, studios)
|
self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_MOVIE, api.studios())
|
||||||
tags = [section_name]
|
tags = [section_name]
|
||||||
if collections:
|
if api.collections():
|
||||||
for plex_set_id, set_name in collections:
|
for plex_set_id, set_name in api.collections():
|
||||||
set_api = None
|
set_api = None
|
||||||
tags.append(set_name)
|
tags.append(set_name)
|
||||||
# Add any sets from Plex collection tags
|
# Add any sets from Plex collection tags
|
||||||
|
@ -211,10 +199,10 @@ class Movie(ItemBase):
|
||||||
self.kodidb.modify_tags(kodi_id, v.KODI_TYPE_MOVIE, tags)
|
self.kodidb.modify_tags(kodi_id, v.KODI_TYPE_MOVIE, tags)
|
||||||
# Process playstate
|
# Process playstate
|
||||||
self.kodidb.set_resume(file_id,
|
self.kodidb.set_resume(file_id,
|
||||||
resume,
|
api.resume_point(),
|
||||||
runtime,
|
api.runtime(),
|
||||||
playcount,
|
api.viewcount(),
|
||||||
dateplayed)
|
api.lastplayed())
|
||||||
self.plexdb.add_movie(plex_id=plex_id,
|
self.plexdb.add_movie(plex_id=plex_id,
|
||||||
checksum=api.checksum(),
|
checksum=api.checksum(),
|
||||||
section_id=section_id,
|
section_id=section_id,
|
||||||
|
@ -267,19 +255,17 @@ class Movie(ItemBase):
|
||||||
"""
|
"""
|
||||||
api = API(xml_element)
|
api = API(xml_element)
|
||||||
# Get key and db entry on the Kodi db side
|
# Get key and db entry on the Kodi db side
|
||||||
db_item = self.plexdb.item_by_id(api.plex_id(), plex_type)
|
db_item = self.plexdb.item_by_id(api.plex_id, plex_type)
|
||||||
if not db_item:
|
if not db_item:
|
||||||
LOG.info('Item not yet synced: %s', xml_element.attrib)
|
LOG.info('Item not yet synced: %s', xml_element.attrib)
|
||||||
return False
|
return False
|
||||||
# Grab the user's viewcount, resume points etc. from PMS' answer
|
|
||||||
userdata = api.userdata()
|
|
||||||
# Write to Kodi DB
|
# Write to Kodi DB
|
||||||
self.kodidb.set_resume(db_item['kodi_fileid'],
|
self.kodidb.set_resume(db_item['kodi_fileid'],
|
||||||
userdata['Resume'],
|
api.resume_point(),
|
||||||
userdata['Runtime'],
|
api.runtime(),
|
||||||
userdata['PlayCount'],
|
api.viewcount(),
|
||||||
userdata['LastPlayedDate'])
|
api.lastplayed())
|
||||||
self.kodidb.update_userrating(db_item['kodi_id'],
|
self.kodidb.update_userrating(db_item['kodi_id'],
|
||||||
db_item['kodi_type'],
|
db_item['kodi_type'],
|
||||||
userdata['UserRating'])
|
api.userrating())
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -42,18 +42,17 @@ class MusicMixin(object):
|
||||||
"""
|
"""
|
||||||
api = API(xml_element)
|
api = API(xml_element)
|
||||||
# Get key and db entry on the Kodi db side
|
# Get key and db entry on the Kodi db side
|
||||||
db_item = self.plexdb.item_by_id(api.plex_id(), plex_type)
|
db_item = self.plexdb.item_by_id(api.plex_id, plex_type)
|
||||||
if not db_item:
|
if not db_item:
|
||||||
LOG.info('Item not yet synced: %s', xml_element.attrib)
|
LOG.info('Item not yet synced: %s', xml_element.attrib)
|
||||||
return False
|
return False
|
||||||
# Grab the user's viewcount, resume points etc. from PMS' answer
|
# Grab the user's viewcount, resume points etc. from PMS' answer
|
||||||
userdata = api.userdata()
|
|
||||||
self.kodidb.update_userrating(db_item['kodi_id'],
|
self.kodidb.update_userrating(db_item['kodi_id'],
|
||||||
db_item['kodi_type'],
|
db_item['kodi_type'],
|
||||||
userdata['UserRating'])
|
api.userrating())
|
||||||
if plex_type == v.PLEX_TYPE_SONG:
|
if plex_type == v.PLEX_TYPE_SONG:
|
||||||
self.kodidb.set_playcount(userdata['PlayCount'],
|
self.kodidb.set_playcount(api.viewcount(),
|
||||||
userdata['LastPlayedDate'],
|
api.lastplayed(),
|
||||||
db_item['kodi_id'],)
|
db_item['kodi_id'],)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -160,7 +159,7 @@ class Artist(MusicMixin, ItemBase):
|
||||||
Process a single artist
|
Process a single artist
|
||||||
"""
|
"""
|
||||||
api = API(xml)
|
api = API(xml)
|
||||||
plex_id = api.plex_id()
|
plex_id = api.plex_id
|
||||||
if not plex_id:
|
if not plex_id:
|
||||||
LOG.error('Cannot process artist %s', xml.attrib)
|
LOG.error('Cannot process artist %s', xml.attrib)
|
||||||
return
|
return
|
||||||
|
@ -198,7 +197,7 @@ class Artist(MusicMixin, ItemBase):
|
||||||
# Kodi doesn't allow that. In case that happens we just merge the
|
# Kodi doesn't allow that. In case that happens we just merge the
|
||||||
# artist entries.
|
# artist entries.
|
||||||
kodi_id = self.kodidb.add_artist(api.title(), musicBrainzId)
|
kodi_id = self.kodidb.add_artist(api.title(), musicBrainzId)
|
||||||
self.kodidb.update_artist(api.list_to_string(api.genre_list()),
|
self.kodidb.update_artist(api.list_to_string(api.genres()),
|
||||||
api.plot(),
|
api.plot(),
|
||||||
thumb,
|
thumb,
|
||||||
fanart,
|
fanart,
|
||||||
|
@ -224,7 +223,7 @@ class Album(MusicMixin, ItemBase):
|
||||||
avoid infinite loops
|
avoid infinite loops
|
||||||
"""
|
"""
|
||||||
api = API(xml)
|
api = API(xml)
|
||||||
plex_id = api.plex_id()
|
plex_id = api.plex_id
|
||||||
if not plex_id:
|
if not plex_id:
|
||||||
LOG.error('Error processing album: %s', xml.attrib)
|
LOG.error('Error processing album: %s', xml.attrib)
|
||||||
return
|
return
|
||||||
|
@ -274,11 +273,9 @@ class Album(MusicMixin, ItemBase):
|
||||||
compilation = 1
|
compilation = 1
|
||||||
break
|
break
|
||||||
name = api.title()
|
name = api.title()
|
||||||
userdata = api.userdata()
|
|
||||||
# Not yet implemented by Plex, let's use unique last.fm or gracenote
|
# Not yet implemented by Plex, let's use unique last.fm or gracenote
|
||||||
musicBrainzId = None
|
musicBrainzId = None
|
||||||
genres = api.genre_list()
|
genre = api.list_to_string(api.genres())
|
||||||
genre = api.list_to_string(genres)
|
|
||||||
if app.SYNC.artwork:
|
if app.SYNC.artwork:
|
||||||
artworks = api.artwork()
|
artworks = api.artwork()
|
||||||
if 'poster' in artworks:
|
if 'poster' in artworks:
|
||||||
|
@ -300,8 +297,8 @@ class Album(MusicMixin, ItemBase):
|
||||||
compilation,
|
compilation,
|
||||||
api.plot(),
|
api.plot(),
|
||||||
thumb,
|
thumb,
|
||||||
api.music_studio(),
|
api.list_to_string(api.studios()),
|
||||||
userdata['UserRating'],
|
api.userrating(),
|
||||||
timing.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
'album',
|
'album',
|
||||||
kodi_id)
|
kodi_id)
|
||||||
|
@ -314,8 +311,8 @@ class Album(MusicMixin, ItemBase):
|
||||||
compilation,
|
compilation,
|
||||||
api.plot(),
|
api.plot(),
|
||||||
thumb,
|
thumb,
|
||||||
api.music_studio(),
|
api.list_to_string(api.studios()),
|
||||||
userdata['UserRating'],
|
api.userrating(),
|
||||||
timing.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
'album',
|
'album',
|
||||||
kodi_id)
|
kodi_id)
|
||||||
|
@ -333,8 +330,8 @@ class Album(MusicMixin, ItemBase):
|
||||||
compilation,
|
compilation,
|
||||||
api.plot(),
|
api.plot(),
|
||||||
thumb,
|
thumb,
|
||||||
api.music_studio(),
|
api.list_to_string(api.studios()),
|
||||||
userdata['UserRating'],
|
api.userrating(),
|
||||||
timing.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
'album')
|
'album')
|
||||||
else:
|
else:
|
||||||
|
@ -347,15 +344,15 @@ class Album(MusicMixin, ItemBase):
|
||||||
compilation,
|
compilation,
|
||||||
api.plot(),
|
api.plot(),
|
||||||
thumb,
|
thumb,
|
||||||
api.music_studio(),
|
api.list_to_string(api.studios()),
|
||||||
userdata['UserRating'],
|
api.userrating(),
|
||||||
timing.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
'album')
|
'album')
|
||||||
self.kodidb.add_albumartist(artist_id, kodi_id, api.artist_name())
|
self.kodidb.add_albumartist(artist_id, kodi_id, api.artist_name())
|
||||||
if v.KODIVERSION < 18:
|
if v.KODIVERSION < 18:
|
||||||
self.kodidb.add_discography(artist_id, name, api.year())
|
self.kodidb.add_discography(artist_id, name, api.year())
|
||||||
self.kodidb.add_music_genres(kodi_id,
|
self.kodidb.add_music_genres(kodi_id,
|
||||||
genres,
|
api.genres(),
|
||||||
v.KODI_TYPE_ALBUM)
|
v.KODI_TYPE_ALBUM)
|
||||||
if app.SYNC.artwork:
|
if app.SYNC.artwork:
|
||||||
self.kodidb.modify_artwork(artworks,
|
self.kodidb.modify_artwork(artworks,
|
||||||
|
@ -378,7 +375,7 @@ class Album(MusicMixin, ItemBase):
|
||||||
section_name=section_name,
|
section_name=section_name,
|
||||||
section_id=section_id,
|
section_id=section_id,
|
||||||
album_xml=xml,
|
album_xml=xml,
|
||||||
genres=genres,
|
genres=api.genres(),
|
||||||
genre=genre,
|
genre=genre,
|
||||||
compilation=compilation)
|
compilation=compilation)
|
||||||
|
|
||||||
|
@ -391,7 +388,7 @@ class Song(MusicMixin, ItemBase):
|
||||||
Process single song/track
|
Process single song/track
|
||||||
"""
|
"""
|
||||||
api = API(xml)
|
api = API(xml)
|
||||||
plex_id = api.plex_id()
|
plex_id = api.plex_id
|
||||||
if not plex_id:
|
if not plex_id:
|
||||||
LOG.error('Error processing song: %s', xml.attrib)
|
LOG.error('Error processing song: %s', xml.attrib)
|
||||||
return
|
return
|
||||||
|
@ -492,11 +489,6 @@ class Song(MusicMixin, ItemBase):
|
||||||
# Not yet implemented by Plex
|
# Not yet implemented by Plex
|
||||||
musicBrainzId = None
|
musicBrainzId = None
|
||||||
comment = None
|
comment = None
|
||||||
userdata = api.userdata()
|
|
||||||
playcount = userdata['PlayCount']
|
|
||||||
if playcount is None:
|
|
||||||
# This is different to Video DB!
|
|
||||||
playcount = 0
|
|
||||||
# Getting artists name is complicated
|
# Getting artists name is complicated
|
||||||
if compilation is not None:
|
if compilation is not None:
|
||||||
if compilation == 0:
|
if compilation == 0:
|
||||||
|
@ -506,7 +498,7 @@ class Song(MusicMixin, ItemBase):
|
||||||
else:
|
else:
|
||||||
# compilation not set
|
# compilation not set
|
||||||
artists = xml.get('originalTitle', api.grandparent_title())
|
artists = xml.get('originalTitle', api.grandparent_title())
|
||||||
tracknumber = api.track_number() or 0
|
tracknumber = api.index() or 0
|
||||||
disc = api.disc_number() or 1
|
disc = api.disc_number() or 1
|
||||||
if disc == 1:
|
if disc == 1:
|
||||||
track = tracknumber
|
track = tracknumber
|
||||||
|
@ -532,7 +524,7 @@ class Song(MusicMixin, ItemBase):
|
||||||
# Something went wrong, trying to use non-direct paths
|
# Something went wrong, trying to use non-direct paths
|
||||||
do_indirect = True
|
do_indirect = True
|
||||||
else:
|
else:
|
||||||
playurl = api.validate_playurl(playurl, api.plex_type())
|
playurl = api.validate_playurl(playurl, api.plex_type)
|
||||||
if playurl is None:
|
if playurl is None:
|
||||||
return False
|
return False
|
||||||
if "\\" in playurl:
|
if "\\" in playurl:
|
||||||
|
@ -562,12 +554,12 @@ class Song(MusicMixin, ItemBase):
|
||||||
genre,
|
genre,
|
||||||
title,
|
title,
|
||||||
track,
|
track,
|
||||||
userdata['Runtime'],
|
api.runtime(),
|
||||||
year,
|
year,
|
||||||
filename,
|
filename,
|
||||||
playcount,
|
api.viewcount(),
|
||||||
userdata['LastPlayedDate'],
|
api.lastplayed(),
|
||||||
userdata['UserRating'],
|
api.userrating(),
|
||||||
comment,
|
comment,
|
||||||
mood,
|
mood,
|
||||||
api.date_created(),
|
api.date_created(),
|
||||||
|
@ -578,12 +570,12 @@ class Song(MusicMixin, ItemBase):
|
||||||
genre,
|
genre,
|
||||||
title,
|
title,
|
||||||
track,
|
track,
|
||||||
userdata['Runtime'],
|
api.runtime(),
|
||||||
year,
|
year,
|
||||||
filename,
|
filename,
|
||||||
playcount,
|
api.viewcount(),
|
||||||
userdata['LastPlayedDate'],
|
api.lastplayed(),
|
||||||
userdata['UserRating'],
|
api.userrating(),
|
||||||
comment,
|
comment,
|
||||||
mood,
|
mood,
|
||||||
api.date_created(),
|
api.date_created(),
|
||||||
|
@ -603,13 +595,13 @@ class Song(MusicMixin, ItemBase):
|
||||||
genre,
|
genre,
|
||||||
title,
|
title,
|
||||||
track,
|
track,
|
||||||
userdata['Runtime'],
|
api.runtime(),
|
||||||
year,
|
year,
|
||||||
filename,
|
filename,
|
||||||
musicBrainzId,
|
musicBrainzId,
|
||||||
playcount,
|
api.viewcount(),
|
||||||
userdata['LastPlayedDate'],
|
api.lastplayed(),
|
||||||
userdata['UserRating'],
|
api.userrating(),
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
mood,
|
mood,
|
||||||
|
@ -622,13 +614,13 @@ class Song(MusicMixin, ItemBase):
|
||||||
genre,
|
genre,
|
||||||
title,
|
title,
|
||||||
track,
|
track,
|
||||||
userdata['Runtime'],
|
api.runtime(),
|
||||||
year,
|
year,
|
||||||
filename,
|
filename,
|
||||||
musicBrainzId,
|
musicBrainzId,
|
||||||
playcount,
|
api.viewcount(),
|
||||||
userdata['LastPlayedDate'],
|
api.lastplayed(),
|
||||||
userdata['UserRating'],
|
api.userrating(),
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
mood,
|
mood,
|
||||||
|
@ -639,7 +631,7 @@ class Song(MusicMixin, ItemBase):
|
||||||
parent_id,
|
parent_id,
|
||||||
track,
|
track,
|
||||||
title,
|
title,
|
||||||
userdata['Runtime'])
|
api.runtime())
|
||||||
# Link song to artists
|
# Link song to artists
|
||||||
artist_name = api.grandparent_title()
|
artist_name = api.grandparent_title()
|
||||||
# Do the actual linking
|
# Do the actual linking
|
||||||
|
|
|
@ -18,27 +18,26 @@ class TvShowMixin(object):
|
||||||
"""
|
"""
|
||||||
api = API(xml_element)
|
api = API(xml_element)
|
||||||
# Get key and db entry on the Kodi db side
|
# Get key and db entry on the Kodi db side
|
||||||
db_item = self.plexdb.item_by_id(api.plex_id(), plex_type)
|
db_item = self.plexdb.item_by_id(api.plex_id, plex_type)
|
||||||
if not db_item:
|
if not db_item:
|
||||||
LOG.info('Item not yet synced: %s', xml_element.attrib)
|
LOG.info('Item not yet synced: %s', xml_element.attrib)
|
||||||
return False
|
return False
|
||||||
# Grab the user's viewcount, resume points etc. from PMS' answer
|
# Grab the user's viewcount, resume points etc. from PMS' answer
|
||||||
userdata = api.userdata()
|
|
||||||
self.kodidb.update_userrating(db_item['kodi_id'],
|
self.kodidb.update_userrating(db_item['kodi_id'],
|
||||||
db_item['kodi_type'],
|
db_item['kodi_type'],
|
||||||
userdata['UserRating'])
|
api.userrating())
|
||||||
if plex_type == v.PLEX_TYPE_EPISODE:
|
if plex_type == v.PLEX_TYPE_EPISODE:
|
||||||
self.kodidb.set_resume(db_item['kodi_fileid'],
|
self.kodidb.set_resume(db_item['kodi_fileid'],
|
||||||
userdata['Resume'],
|
api.resume_point(),
|
||||||
userdata['Runtime'],
|
api.runtime(),
|
||||||
userdata['PlayCount'],
|
api.viewcount(),
|
||||||
userdata['LastPlayedDate'])
|
api.lastplayed())
|
||||||
if db_item['kodi_fileid_2']:
|
if db_item['kodi_fileid_2']:
|
||||||
self.kodidb.set_resume(db_item['kodi_fileid_2'],
|
self.kodidb.set_resume(db_item['kodi_fileid_2'],
|
||||||
userdata['Resume'],
|
api.resume_point(),
|
||||||
userdata['Runtime'],
|
api.runtime(),
|
||||||
userdata['PlayCount'],
|
api.viewcount(),
|
||||||
userdata['LastPlayedDate'])
|
api.lastplayed())
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def remove(self, plex_id, plex_type=None):
|
def remove(self, plex_id, plex_type=None):
|
||||||
|
@ -149,7 +148,7 @@ class Show(TvShowMixin, ItemBase):
|
||||||
Process a single show
|
Process a single show
|
||||||
"""
|
"""
|
||||||
api = API(xml)
|
api = API(xml)
|
||||||
plex_id = api.plex_id()
|
plex_id = api.plex_id
|
||||||
if not plex_id:
|
if not plex_id:
|
||||||
LOG.error("Cannot parse XML data for TV show: %s", xml.attrib)
|
LOG.error("Cannot parse XML data for TV show: %s", xml.attrib)
|
||||||
return
|
return
|
||||||
|
@ -162,16 +161,11 @@ class Show(TvShowMixin, ItemBase):
|
||||||
kodi_id = show['kodi_id']
|
kodi_id = show['kodi_id']
|
||||||
kodi_pathid = show['kodi_pathid']
|
kodi_pathid = show['kodi_pathid']
|
||||||
|
|
||||||
genres = api.genre_list()
|
|
||||||
genre = api.list_to_string(genres)
|
|
||||||
studios = api.music_studio_list()
|
|
||||||
studio = api.list_to_string(studios)
|
|
||||||
|
|
||||||
# GET THE FILE AND PATH #####
|
# GET THE FILE AND PATH #####
|
||||||
if app.SYNC.direct_paths:
|
if app.SYNC.direct_paths:
|
||||||
# Direct paths is set the Kodi way
|
# Direct paths is set the Kodi way
|
||||||
playurl = api.validate_playurl(api.tv_show_path(),
|
playurl = api.validate_playurl(api.tv_show_path(),
|
||||||
api.plex_type(),
|
api.plex_type,
|
||||||
folder=True)
|
folder=True)
|
||||||
if playurl is None:
|
if playurl is None:
|
||||||
return
|
return
|
||||||
|
@ -197,7 +191,7 @@ class Show(TvShowMixin, ItemBase):
|
||||||
self.kodidb.update_ratings(kodi_id,
|
self.kodidb.update_ratings(kodi_id,
|
||||||
v.KODI_TYPE_SHOW,
|
v.KODI_TYPE_SHOW,
|
||||||
"default",
|
"default",
|
||||||
api.audience_rating(),
|
api.rating(),
|
||||||
api.votecount(),
|
api.votecount(),
|
||||||
rating_id)
|
rating_id)
|
||||||
if api.provider('tvdb') is not None:
|
if api.provider('tvdb') is not None:
|
||||||
|
@ -213,7 +207,7 @@ class Show(TvShowMixin, ItemBase):
|
||||||
uniqueid = -1
|
uniqueid = -1
|
||||||
self.kodidb.modify_people(kodi_id,
|
self.kodidb.modify_people(kodi_id,
|
||||||
v.KODI_TYPE_SHOW,
|
v.KODI_TYPE_SHOW,
|
||||||
api.people_list())
|
api.people())
|
||||||
if app.SYNC.artwork:
|
if app.SYNC.artwork:
|
||||||
self.kodidb.modify_artwork(api.artwork(),
|
self.kodidb.modify_artwork(api.artwork(),
|
||||||
kodi_id,
|
kodi_id,
|
||||||
|
@ -223,11 +217,11 @@ class Show(TvShowMixin, ItemBase):
|
||||||
api.plot(),
|
api.plot(),
|
||||||
rating_id,
|
rating_id,
|
||||||
api.premiere_date(),
|
api.premiere_date(),
|
||||||
genre,
|
api.list_to_string(api.genres()),
|
||||||
api.title(),
|
api.title(),
|
||||||
uniqueid,
|
uniqueid,
|
||||||
api.content_rating(),
|
api.content_rating(),
|
||||||
studio,
|
api.list_to_string(api.studios()),
|
||||||
api.sorttitle(),
|
api.sorttitle(),
|
||||||
kodi_id)
|
kodi_id)
|
||||||
# OR ADD THE TVSHOW #####
|
# OR ADD THE TVSHOW #####
|
||||||
|
@ -240,7 +234,7 @@ class Show(TvShowMixin, ItemBase):
|
||||||
kodi_id,
|
kodi_id,
|
||||||
v.KODI_TYPE_SHOW,
|
v.KODI_TYPE_SHOW,
|
||||||
"default",
|
"default",
|
||||||
api.audience_rating(),
|
api.rating(),
|
||||||
api.votecount())
|
api.votecount())
|
||||||
if api.provider('tvdb'):
|
if api.provider('tvdb'):
|
||||||
uniqueid = self.kodidb.add_uniqueid_id()
|
uniqueid = self.kodidb.add_uniqueid_id()
|
||||||
|
@ -253,7 +247,7 @@ class Show(TvShowMixin, ItemBase):
|
||||||
uniqueid = -1
|
uniqueid = -1
|
||||||
self.kodidb.add_people(kodi_id,
|
self.kodidb.add_people(kodi_id,
|
||||||
v.KODI_TYPE_SHOW,
|
v.KODI_TYPE_SHOW,
|
||||||
api.people_list())
|
api.people())
|
||||||
if app.SYNC.artwork:
|
if app.SYNC.artwork:
|
||||||
self.kodidb.add_artwork(api.artwork(),
|
self.kodidb.add_artwork(api.artwork(),
|
||||||
kodi_id,
|
kodi_id,
|
||||||
|
@ -264,18 +258,18 @@ class Show(TvShowMixin, ItemBase):
|
||||||
api.plot(),
|
api.plot(),
|
||||||
rating_id,
|
rating_id,
|
||||||
api.premiere_date(),
|
api.premiere_date(),
|
||||||
genre,
|
api.list_to_string(api.genres()),
|
||||||
api.title(),
|
api.title(),
|
||||||
uniqueid,
|
uniqueid,
|
||||||
api.content_rating(),
|
api.content_rating(),
|
||||||
studio,
|
api.list_to_string(api.studios()),
|
||||||
api.sorttitle())
|
api.sorttitle())
|
||||||
self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_SHOW, genres)
|
self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_SHOW, api.genres())
|
||||||
# Process studios
|
# Process studios
|
||||||
self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_SHOW, studios)
|
self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_SHOW, api.studios())
|
||||||
# Process tags: view, PMS collection tags
|
# Process tags: view, PMS collection tags
|
||||||
tags = [section_name]
|
tags = [section_name]
|
||||||
tags.extend([i for _, i in api.collection_list()])
|
tags.extend([i for _, i in api.collections()])
|
||||||
self.kodidb.modify_tags(kodi_id, v.KODI_TYPE_SHOW, tags)
|
self.kodidb.modify_tags(kodi_id, v.KODI_TYPE_SHOW, tags)
|
||||||
self.plexdb.add_show(plex_id=plex_id,
|
self.plexdb.add_show(plex_id=plex_id,
|
||||||
checksum=api.checksum(),
|
checksum=api.checksum(),
|
||||||
|
@ -292,7 +286,7 @@ class Season(TvShowMixin, ItemBase):
|
||||||
Process a single season of a certain tv show
|
Process a single season of a certain tv show
|
||||||
"""
|
"""
|
||||||
api = API(xml)
|
api = API(xml)
|
||||||
plex_id = api.plex_id()
|
plex_id = api.plex_id
|
||||||
if not plex_id:
|
if not plex_id:
|
||||||
LOG.error('Error getting plex_id for season, skipping: %s',
|
LOG.error('Error getting plex_id for season, skipping: %s',
|
||||||
xml.attrib)
|
xml.attrib)
|
||||||
|
@ -339,7 +333,7 @@ class Season(TvShowMixin, ItemBase):
|
||||||
v.KODI_TYPE_SEASON)
|
v.KODI_TYPE_SEASON)
|
||||||
else:
|
else:
|
||||||
LOG.info('ADD season plex_id %s - %s', plex_id, api.title())
|
LOG.info('ADD season plex_id %s - %s', plex_id, api.title())
|
||||||
kodi_id = self.kodidb.add_season(parent_id, api.season_number())
|
kodi_id = self.kodidb.add_season(parent_id, api.index())
|
||||||
if app.SYNC.artwork:
|
if app.SYNC.artwork:
|
||||||
self.kodidb.add_artwork(artwork,
|
self.kodidb.add_artwork(artwork,
|
||||||
kodi_id,
|
kodi_id,
|
||||||
|
@ -360,7 +354,7 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
Process single episode
|
Process single episode
|
||||||
"""
|
"""
|
||||||
api = API(xml)
|
api = API(xml)
|
||||||
plex_id = api.plex_id()
|
plex_id = api.plex_id
|
||||||
if not plex_id:
|
if not plex_id:
|
||||||
LOG.error('Error getting plex_id for episode, skipping: %s',
|
LOG.error('Error getting plex_id for episode, skipping: %s',
|
||||||
xml.attrib)
|
xml.attrib)
|
||||||
|
@ -376,58 +370,48 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
old_kodi_fileid_2 = episode['kodi_fileid_2']
|
old_kodi_fileid_2 = episode['kodi_fileid_2']
|
||||||
kodi_pathid = episode['kodi_pathid']
|
kodi_pathid = episode['kodi_pathid']
|
||||||
|
|
||||||
peoples = api.people()
|
|
||||||
director = api.list_to_string(peoples['Director'])
|
|
||||||
writer = api.list_to_string(peoples['Writer'])
|
|
||||||
userdata = api.userdata()
|
|
||||||
show_id, season_id, _, season_no, episode_no = api.episode_data()
|
|
||||||
|
|
||||||
if season_no is None:
|
|
||||||
season_no = -1
|
|
||||||
if episode_no is None:
|
|
||||||
episode_no = -1
|
|
||||||
airs_before_season = "-1"
|
airs_before_season = "-1"
|
||||||
airs_before_episode = "-1"
|
airs_before_episode = "-1"
|
||||||
|
|
||||||
# The grandparent TV show
|
# The grandparent TV show
|
||||||
show = self.plexdb.show(show_id)
|
show = self.plexdb.show(api.show_id())
|
||||||
if not show:
|
if not show:
|
||||||
LOG.warn('Grandparent TV show %s not found in DB, adding it', show_id)
|
LOG.warn('Grandparent TV show %s not found in DB, adding it', api.show_id())
|
||||||
show_xml = PF.GetPlexMetadata(show_id)
|
show_xml = PF.GetPlexMetadata(api.show_id())
|
||||||
try:
|
try:
|
||||||
show_xml[0].attrib
|
show_xml[0].attrib
|
||||||
except (TypeError, IndexError, AttributeError):
|
except (TypeError, IndexError, AttributeError):
|
||||||
LOG.error("Grandparent tvshow %s xml download failed", show_id)
|
LOG.error("Grandparent tvshow %s xml download failed", api.show_id())
|
||||||
return False
|
return False
|
||||||
Show(self.last_sync,
|
Show(self.last_sync,
|
||||||
plexdb=self.plexdb,
|
plexdb=self.plexdb,
|
||||||
kodidb=self.kodidb).add_update(show_xml[0],
|
kodidb=self.kodidb).add_update(show_xml[0],
|
||||||
section_name,
|
section_name,
|
||||||
section_id)
|
section_id)
|
||||||
show = self.plexdb.show(show_id)
|
show = self.plexdb.show(api.show_id())
|
||||||
if not show:
|
if not show:
|
||||||
LOG.error('Still could not find grandparent tv show %s', show_id)
|
LOG.error('Still could not find grandparent tv show %s', api.show_id())
|
||||||
return
|
return
|
||||||
grandparent_id = show['kodi_id']
|
grandparent_id = show['kodi_id']
|
||||||
|
|
||||||
# The parent Season
|
# The parent Season
|
||||||
season = self.plexdb.season(season_id)
|
season = self.plexdb.season(api.season_id())
|
||||||
if not season and season_id:
|
if not season and api.season_id():
|
||||||
LOG.warn('Parent season %s not found in DB, adding it', season_id)
|
LOG.warn('Parent season %s not found in DB, adding it', api.season_id())
|
||||||
season_xml = PF.GetPlexMetadata(season_id)
|
season_xml = PF.GetPlexMetadata(api.season_id())
|
||||||
try:
|
try:
|
||||||
season_xml[0].attrib
|
season_xml[0].attrib
|
||||||
except (TypeError, IndexError, AttributeError):
|
except (TypeError, IndexError, AttributeError):
|
||||||
LOG.error("Parent season %s xml download failed", season_id)
|
LOG.error("Parent season %s xml download failed", api.season_id())
|
||||||
return False
|
return False
|
||||||
Season(self.last_sync,
|
Season(self.last_sync,
|
||||||
plexdb=self.plexdb,
|
plexdb=self.plexdb,
|
||||||
kodidb=self.kodidb).add_update(season_xml[0],
|
kodidb=self.kodidb).add_update(season_xml[0],
|
||||||
section_name,
|
section_name,
|
||||||
section_id)
|
section_id)
|
||||||
season = self.plexdb.season(season_id)
|
season = self.plexdb.season(api.season_id())
|
||||||
if not season:
|
if not season:
|
||||||
LOG.error('Still could not find parent season %s', season_id)
|
LOG.error('Still could not find parent season %s', api.season_id())
|
||||||
return
|
return
|
||||||
parent_id = season['kodi_id'] if season else None
|
parent_id = season['kodi_id'] if season else None
|
||||||
|
|
||||||
|
@ -453,7 +437,7 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
# Set plugin path - do NOT use "intermediate" paths for the show
|
# Set plugin path - do NOT use "intermediate" paths for the show
|
||||||
# as with direct paths!
|
# as with direct paths!
|
||||||
filename = api.file_name(force_first_media=True)
|
filename = api.file_name(force_first_media=True)
|
||||||
path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, show_id)
|
path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, api.show_id())
|
||||||
filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s'
|
filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s'
|
||||||
% (path, plex_id, v.PLEX_TYPE_EPISODE, filename))
|
% (path, plex_id, v.PLEX_TYPE_EPISODE, filename))
|
||||||
playurl = filename
|
playurl = filename
|
||||||
|
@ -491,7 +475,7 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
self.kodidb.update_ratings(kodi_id,
|
self.kodidb.update_ratings(kodi_id,
|
||||||
v.KODI_TYPE_EPISODE,
|
v.KODI_TYPE_EPISODE,
|
||||||
"default",
|
"default",
|
||||||
userdata['Rating'],
|
api.rating(),
|
||||||
api.votecount(),
|
api.votecount(),
|
||||||
ratingid)
|
ratingid)
|
||||||
if api.provider('tvdb'):
|
if api.provider('tvdb'):
|
||||||
|
@ -507,7 +491,7 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
uniqueid = -1
|
uniqueid = -1
|
||||||
self.kodidb.modify_people(kodi_id,
|
self.kodidb.modify_people(kodi_id,
|
||||||
v.KODI_TYPE_EPISODE,
|
v.KODI_TYPE_EPISODE,
|
||||||
api.people_list())
|
api.people())
|
||||||
if app.SYNC.artwork:
|
if app.SYNC.artwork:
|
||||||
self.kodidb.modify_artwork(api.artwork(),
|
self.kodidb.modify_artwork(api.artwork(),
|
||||||
kodi_id,
|
kodi_id,
|
||||||
|
@ -515,12 +499,12 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
self.kodidb.update_episode(api.title(),
|
self.kodidb.update_episode(api.title(),
|
||||||
api.plot(),
|
api.plot(),
|
||||||
ratingid,
|
ratingid,
|
||||||
writer,
|
api.list_to_string(api.writers()),
|
||||||
api.premiere_date(),
|
api.premiere_date(),
|
||||||
api.runtime(),
|
api.runtime(),
|
||||||
director,
|
api.list_to_string(api.directors()),
|
||||||
season_no,
|
api.season_number(),
|
||||||
episode_no,
|
api.index(),
|
||||||
api.title(),
|
api.title(),
|
||||||
airs_before_season,
|
airs_before_season,
|
||||||
airs_before_episode,
|
airs_before_episode,
|
||||||
|
@ -528,25 +512,25 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
kodi_pathid,
|
kodi_pathid,
|
||||||
kodi_fileid, # and NOT kodi_fileid_2
|
kodi_fileid, # and NOT kodi_fileid_2
|
||||||
parent_id,
|
parent_id,
|
||||||
userdata['UserRating'],
|
api.userrating(),
|
||||||
kodi_id)
|
kodi_id)
|
||||||
self.kodidb.set_resume(kodi_fileid,
|
self.kodidb.set_resume(kodi_fileid,
|
||||||
api.resume_point(),
|
api.resume_point(),
|
||||||
api.runtime(),
|
api.runtime(),
|
||||||
userdata['PlayCount'],
|
api.viewcount(),
|
||||||
userdata['LastPlayedDate'])
|
api.lastplayed())
|
||||||
if not app.SYNC.direct_paths:
|
if not app.SYNC.direct_paths:
|
||||||
self.kodidb.set_resume(kodi_fileid_2,
|
self.kodidb.set_resume(kodi_fileid_2,
|
||||||
api.resume_point(),
|
api.resume_point(),
|
||||||
api.runtime(),
|
api.runtime(),
|
||||||
userdata['PlayCount'],
|
api.viewcount(),
|
||||||
userdata['LastPlayedDate'])
|
api.lastplayed())
|
||||||
self.plexdb.add_episode(plex_id=plex_id,
|
self.plexdb.add_episode(plex_id=plex_id,
|
||||||
checksum=api.checksum(),
|
checksum=api.checksum(),
|
||||||
section_id=section_id,
|
section_id=section_id,
|
||||||
show_id=show_id,
|
show_id=api.show_id(),
|
||||||
grandparent_id=grandparent_id,
|
grandparent_id=grandparent_id,
|
||||||
season_id=season_id,
|
season_id=api.season_id(),
|
||||||
parent_id=parent_id,
|
parent_id=parent_id,
|
||||||
kodi_id=kodi_id,
|
kodi_id=kodi_id,
|
||||||
kodi_fileid=kodi_fileid,
|
kodi_fileid=kodi_fileid,
|
||||||
|
@ -571,7 +555,7 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
kodi_id,
|
kodi_id,
|
||||||
v.KODI_TYPE_EPISODE,
|
v.KODI_TYPE_EPISODE,
|
||||||
"default",
|
"default",
|
||||||
userdata['Rating'],
|
api.rating(),
|
||||||
api.votecount())
|
api.votecount())
|
||||||
if api.provider('tvdb'):
|
if api.provider('tvdb'):
|
||||||
uniqueid = self.kodidb.add_uniqueid_id()
|
uniqueid = self.kodidb.add_uniqueid_id()
|
||||||
|
@ -582,7 +566,7 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
"tvdb")
|
"tvdb")
|
||||||
self.kodidb.add_people(kodi_id,
|
self.kodidb.add_people(kodi_id,
|
||||||
v.KODI_TYPE_EPISODE,
|
v.KODI_TYPE_EPISODE,
|
||||||
api.people_list())
|
api.people())
|
||||||
if app.SYNC.artwork:
|
if app.SYNC.artwork:
|
||||||
self.kodidb.add_artwork(api.artwork(),
|
self.kodidb.add_artwork(api.artwork(),
|
||||||
kodi_id,
|
kodi_id,
|
||||||
|
@ -592,12 +576,12 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
api.title(),
|
api.title(),
|
||||||
api.plot(),
|
api.plot(),
|
||||||
rating_id,
|
rating_id,
|
||||||
writer,
|
api.list_to_string(api.writers()),
|
||||||
api.premiere_date(),
|
api.premiere_date(),
|
||||||
api.runtime(),
|
api.runtime(),
|
||||||
director,
|
api.list_to_string(api.directors()),
|
||||||
season_no,
|
api.season_number(),
|
||||||
episode_no,
|
api.index(),
|
||||||
api.title(),
|
api.title(),
|
||||||
grandparent_id,
|
grandparent_id,
|
||||||
airs_before_season,
|
airs_before_season,
|
||||||
|
@ -605,24 +589,24 @@ class Episode(TvShowMixin, ItemBase):
|
||||||
playurl,
|
playurl,
|
||||||
kodi_pathid,
|
kodi_pathid,
|
||||||
parent_id,
|
parent_id,
|
||||||
userdata['UserRating'])
|
api.userrating())
|
||||||
self.kodidb.set_resume(kodi_fileid,
|
self.kodidb.set_resume(kodi_fileid,
|
||||||
api.resume_point(),
|
api.resume_point(),
|
||||||
api.runtime(),
|
api.runtime(),
|
||||||
userdata['PlayCount'],
|
api.viewcount(),
|
||||||
userdata['LastPlayedDate'])
|
api.lastplayed())
|
||||||
if not app.SYNC.direct_paths:
|
if not app.SYNC.direct_paths:
|
||||||
self.kodidb.set_resume(kodi_fileid_2,
|
self.kodidb.set_resume(kodi_fileid_2,
|
||||||
api.resume_point(),
|
api.resume_point(),
|
||||||
api.runtime(),
|
api.runtime(),
|
||||||
userdata['PlayCount'],
|
api.viewcount(),
|
||||||
userdata['LastPlayedDate'])
|
api.lastplayed())
|
||||||
self.plexdb.add_episode(plex_id=plex_id,
|
self.plexdb.add_episode(plex_id=plex_id,
|
||||||
checksum=api.checksum(),
|
checksum=api.checksum(),
|
||||||
section_id=section_id,
|
section_id=section_id,
|
||||||
show_id=show_id,
|
show_id=api.show_id(),
|
||||||
grandparent_id=grandparent_id,
|
grandparent_id=grandparent_id,
|
||||||
season_id=season_id,
|
season_id=api.season_id(),
|
||||||
parent_id=parent_id,
|
parent_id=parent_id,
|
||||||
kodi_id=kodi_id,
|
kodi_id=kodi_id,
|
||||||
kodi_fileid=kodi_fileid,
|
kodi_fileid=kodi_fileid,
|
||||||
|
|
|
@ -129,7 +129,7 @@ def process_fanart(plex_id, plex_type, refresh=False):
|
||||||
db_item['kodi_type'])
|
db_item['kodi_type'])
|
||||||
# Additional fanart for sets/collections
|
# Additional fanart for sets/collections
|
||||||
if plex_type == v.PLEX_TYPE_MOVIE:
|
if plex_type == v.PLEX_TYPE_MOVIE:
|
||||||
for _, setname in api.collection_list():
|
for _, setname in api.collections():
|
||||||
LOG.debug('Getting artwork for movie set %s', setname)
|
LOG.debug('Getting artwork for movie set %s', setname)
|
||||||
with KodiVideoDB() as kodidb:
|
with KodiVideoDB() as kodidb:
|
||||||
setid = kodidb.create_collection(setname)
|
setid = kodidb.create_collection(setname)
|
||||||
|
|
|
@ -56,7 +56,7 @@ class GetMetadataTask(common.fullsync_mixin, backgroundthread.Task):
|
||||||
[(utils.cast(int, x.get('index')),
|
[(utils.cast(int, x.get('index')),
|
||||||
utils.cast(int, x.get('ratingKey'))) for x in COLLECTION_MATCH]
|
utils.cast(int, x.get('ratingKey'))) for x in COLLECTION_MATCH]
|
||||||
item['children'] = {}
|
item['children'] = {}
|
||||||
for plex_set_id, set_name in api.collection_list():
|
for plex_set_id, set_name in api.collections():
|
||||||
if self.isCanceled():
|
if self.isCanceled():
|
||||||
return
|
return
|
||||||
if plex_set_id not in COLLECTION_XMLS:
|
if plex_set_id not in COLLECTION_XMLS:
|
||||||
|
|
|
@ -177,7 +177,7 @@ class Section(object):
|
||||||
api = API(xml_element)
|
api = API(xml_element)
|
||||||
self.section_id = utils.cast(int, xml_element.get('key'))
|
self.section_id = utils.cast(int, xml_element.get('key'))
|
||||||
self.name = api.title()
|
self.name = api.title()
|
||||||
self.section_type = api.plex_type()
|
self.section_type = api.plex_type
|
||||||
self.icon = api.one_artwork('composite')
|
self.icon = api.one_artwork('composite')
|
||||||
self.artwork = api.one_artwork('art')
|
self.artwork = api.one_artwork('art')
|
||||||
self.thumb = api.one_artwork('thumb')
|
self.thumb = api.one_artwork('thumb')
|
||||||
|
|
|
@ -313,9 +313,8 @@ def process_playing(data):
|
||||||
plex_id)
|
plex_id)
|
||||||
continue
|
continue
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
userdata = api.userdata()
|
session['duration'] = api.runtime()
|
||||||
session['duration'] = userdata['Runtime']
|
session['viewCount'] = api.viewcount()
|
||||||
session['viewCount'] = userdata['PlayCount']
|
|
||||||
# Sometimes, Plex tells us resume points in milliseconds and
|
# Sometimes, Plex tells us resume points in milliseconds and
|
||||||
# not in seconds - thank you very much!
|
# not in seconds - thank you very much!
|
||||||
if message['viewOffset'] > session['duration']:
|
if message['viewOffset'] > session['duration']:
|
||||||
|
|
|
@ -332,11 +332,11 @@ def _prep_playlist_stack(xml, resume):
|
||||||
for i, item in enumerate(xml):
|
for i, item in enumerate(xml):
|
||||||
api = API(item)
|
api = API(item)
|
||||||
if (app.PLAYSTATE.context_menu_play is False and
|
if (app.PLAYSTATE.context_menu_play is False and
|
||||||
api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
|
api.plex_type not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
|
||||||
# If user chose to play via PMS or force transcode, do not
|
# If user chose to play via PMS or force transcode, do not
|
||||||
# use the item path stored in the Kodi DB
|
# use the item path stored in the Kodi DB
|
||||||
with PlexDB(lock=False) as plexdb:
|
with PlexDB(lock=False) as plexdb:
|
||||||
db_item = plexdb.item_by_id(api.plex_id(), api.plex_type())
|
db_item = plexdb.item_by_id(api.plex_id, api.plex_type)
|
||||||
kodi_id = db_item['kodi_id'] if db_item else None
|
kodi_id = db_item['kodi_id'] if db_item else None
|
||||||
kodi_type = db_item['kodi_type'] if db_item else None
|
kodi_type = db_item['kodi_type'] if db_item else None
|
||||||
else:
|
else:
|
||||||
|
@ -349,7 +349,7 @@ def _prep_playlist_stack(xml, resume):
|
||||||
kodi_id = None
|
kodi_id = None
|
||||||
kodi_type = None
|
kodi_type = None
|
||||||
for part, _ in enumerate(item[0]):
|
for part, _ in enumerate(item[0]):
|
||||||
api.set_part_number(part)
|
api.part = part
|
||||||
if kodi_id is None:
|
if kodi_id is None:
|
||||||
# Need to redirect again to PKC to conclude playback
|
# Need to redirect again to PKC to conclude playback
|
||||||
path = api.path(force_addon=True, force_first_media=True)
|
path = api.path(force_addon=True, force_first_media=True)
|
||||||
|
@ -361,7 +361,7 @@ def _prep_playlist_stack(xml, resume):
|
||||||
# 'plugin.video.plexkodiconnect', 1)
|
# 'plugin.video.plexkodiconnect', 1)
|
||||||
# path = path.replace('plugin.video.plexkodiconnect.movies',
|
# path = path.replace('plugin.video.plexkodiconnect.movies',
|
||||||
# 'plugin.video.plexkodiconnect', 1)
|
# 'plugin.video.plexkodiconnect', 1)
|
||||||
listitem = api.create_listitem()
|
listitem = api.listitem()
|
||||||
listitem.setPath(path.encode('utf-8'))
|
listitem.setPath(path.encode('utf-8'))
|
||||||
else:
|
else:
|
||||||
# Will add directly via the Kodi DB
|
# Will add directly via the Kodi DB
|
||||||
|
@ -458,16 +458,16 @@ def _conclude_playback(playqueue, pos):
|
||||||
return PKC listitem attached to result
|
return PKC listitem attached to result
|
||||||
"""
|
"""
|
||||||
LOG.info('Concluding playback for playqueue position %s', pos)
|
LOG.info('Concluding playback for playqueue position %s', pos)
|
||||||
listitem = transfer.PKCListItem()
|
|
||||||
item = playqueue.items[pos]
|
item = playqueue.items[pos]
|
||||||
if item.xml is not None:
|
if item.xml is not None:
|
||||||
# Got a Plex element
|
# Got a Plex element
|
||||||
api = API(item.xml)
|
api = API(item.xml)
|
||||||
api.set_part_number(item.part)
|
api.part = item.part or 0
|
||||||
api.create_listitem(listitem)
|
listitem = api.listitem(listitem=transfer.PKCListItem)
|
||||||
playutils = PlayUtils(api, item)
|
playutils = PlayUtils(api, item)
|
||||||
playurl = playutils.getPlayUrl()
|
playurl = playutils.getPlayUrl()
|
||||||
else:
|
else:
|
||||||
|
listitem = transfer.PKCListItem()
|
||||||
api = None
|
api = None
|
||||||
playurl = item.file
|
playurl = item.file
|
||||||
if not playurl:
|
if not playurl:
|
||||||
|
@ -514,10 +514,9 @@ def process_indirect(key, offset, resolve=True):
|
||||||
return
|
return
|
||||||
|
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
listitem = transfer.PKCListItem()
|
listitem = api.listitem(listitem=transfer.PKCListItem)
|
||||||
api.create_listitem(listitem)
|
|
||||||
playqueue = PQ.get_playqueue_from_type(
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
|
||||||
playqueue.clear()
|
playqueue.clear()
|
||||||
item = PL.Playlist_Item()
|
item = PL.Playlist_Item()
|
||||||
item.xml = xml[0]
|
item.xml = xml[0]
|
||||||
|
@ -574,7 +573,7 @@ def play_xml(playqueue, xml, offset=None, start_plex_id=None):
|
||||||
else playqueue.selectedItemID
|
else playqueue.selectedItemID
|
||||||
for startpos, video in enumerate(xml):
|
for startpos, video in enumerate(xml):
|
||||||
api = API(video)
|
api = API(video)
|
||||||
if api.plex_id() == start_item:
|
if api.plex_id == start_item:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
startpos = 0
|
startpos = 0
|
||||||
|
|
|
@ -423,8 +423,8 @@ def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None):
|
||||||
"""
|
"""
|
||||||
item = Playlist_Item()
|
item = Playlist_Item()
|
||||||
api = API(xml_video_element)
|
api = API(xml_video_element)
|
||||||
item.plex_id = api.plex_id()
|
item.plex_id = api.plex_id
|
||||||
item.plex_type = api.plex_type()
|
item.plex_type = api.plex_type
|
||||||
# item.id will only be set if you passed in an xml_video_element from e.g.
|
# item.id will only be set if you passed in an xml_video_element from e.g.
|
||||||
# a playQueue
|
# a playQueue
|
||||||
item.id = api.item_id()
|
item.id = api.item_id()
|
||||||
|
|
|
@ -170,32 +170,32 @@ def _full_sync():
|
||||||
return False
|
return False
|
||||||
api = API(xml_playlist)
|
api = API(xml_playlist)
|
||||||
try:
|
try:
|
||||||
old_plex_ids.remove(api.plex_id())
|
old_plex_ids.remove(api.plex_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
if not sync_plex_playlist(xml=xml_playlist):
|
if not sync_plex_playlist(xml=xml_playlist):
|
||||||
continue
|
continue
|
||||||
playlist = db.get_playlist(plex_id=api.plex_id())
|
playlist = db.get_playlist(plex_id=api.plex_id)
|
||||||
if not playlist:
|
if not playlist:
|
||||||
LOG.debug('New Plex playlist %s discovered: %s',
|
LOG.debug('New Plex playlist %s discovered: %s',
|
||||||
api.plex_id(), api.title())
|
api.plex_id, api.title())
|
||||||
try:
|
try:
|
||||||
kodi_pl.create(api.plex_id())
|
kodi_pl.create(api.plex_id)
|
||||||
except PlaylistError:
|
except PlaylistError:
|
||||||
LOG.info('Skipping creation of playlist %s', api.plex_id())
|
LOG.info('Skipping creation of playlist %s', api.plex_id)
|
||||||
elif playlist.plex_updatedat != api.updated_at():
|
elif playlist.plex_updatedat != api.updated_at():
|
||||||
LOG.debug('Detected changed Plex playlist %s: %s',
|
LOG.debug('Detected changed Plex playlist %s: %s',
|
||||||
api.plex_id(), api.title())
|
api.plex_id, api.title())
|
||||||
# Since we are DELETING a playlist, we need to catch with path!
|
# Since we are DELETING a playlist, we need to catch with path!
|
||||||
try:
|
try:
|
||||||
kodi_pl.delete(playlist)
|
kodi_pl.delete(playlist)
|
||||||
except PlaylistError:
|
except PlaylistError:
|
||||||
LOG.info('Skipping recreation of playlist %s', api.plex_id())
|
LOG.info('Skipping recreation of playlist %s', api.plex_id)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
kodi_pl.create(api.plex_id())
|
kodi_pl.create(api.plex_id)
|
||||||
except PlaylistError:
|
except PlaylistError:
|
||||||
LOG.info('Could not recreate playlist %s', api.plex_id())
|
LOG.info('Could not recreate playlist %s', api.plex_id)
|
||||||
# Get rid of old Plex playlists that were deleted on the Plex side
|
# Get rid of old Plex playlists that were deleted on the Plex side
|
||||||
for plex_id in old_plex_ids:
|
for plex_id in old_plex_ids:
|
||||||
if isCanceled():
|
if isCanceled():
|
||||||
|
|
|
@ -35,7 +35,7 @@ def create(plex_id):
|
||||||
raise PlaylistError('Could not get Plex playlist %s' % plex_id)
|
raise PlaylistError('Could not get Plex playlist %s' % plex_id)
|
||||||
api = API(xml_metadata[0])
|
api = API(xml_metadata[0])
|
||||||
playlist = Playlist()
|
playlist = Playlist()
|
||||||
playlist.plex_id = api.plex_id()
|
playlist.plex_id = api.plex_id
|
||||||
playlist.kodi_type = v.KODI_PLAYLIST_TYPE_FROM_PLEX[api.playlist_type()]
|
playlist.kodi_type = v.KODI_PLAYLIST_TYPE_FROM_PLEX[api.playlist_type()]
|
||||||
playlist.plex_name = api.title()
|
playlist.plex_name = api.title()
|
||||||
playlist.plex_updatedat = api.updated_at()
|
playlist.plex_updatedat = api.updated_at()
|
||||||
|
@ -104,24 +104,16 @@ def _write_playlist_to_file(playlist, xml):
|
||||||
text = '#EXTCPlayListM3U::M3U\n'
|
text = '#EXTCPlayListM3U::M3U\n'
|
||||||
for element in xml:
|
for element in xml:
|
||||||
api = API(element)
|
api = API(element)
|
||||||
append_season_episode = False
|
if api.plex_type == v.PLEX_TYPE_EPISODE:
|
||||||
if api.plex_type() == v.PLEX_TYPE_EPISODE:
|
if api.season_number() is not None and api.index() is not None:
|
||||||
_, _, show, season_no, episode_no = api.episode_data()
|
|
||||||
try:
|
|
||||||
season_no = int(season_no)
|
|
||||||
episode_no = int(episode_no)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
append_season_episode = True
|
|
||||||
if append_season_episode:
|
|
||||||
text += ('#EXTINF:%s,%s S%.2dE%.2d - %s\n%s\n'
|
text += ('#EXTINF:%s,%s S%.2dE%.2d - %s\n%s\n'
|
||||||
% (api.runtime(), show, season_no, episode_no,
|
% (api.runtime(), api.show_title(),
|
||||||
|
api.season_number(), api.index(),
|
||||||
api.title(), api.path()))
|
api.title(), api.path()))
|
||||||
else:
|
else:
|
||||||
# Only append the TV show name
|
# Only append the TV show name
|
||||||
text += ('#EXTINF:%s,%s - %s\n%s\n'
|
text += ('#EXTINF:%s,%s - %s\n%s\n'
|
||||||
% (api.runtime(), show, api.title(), api.path()))
|
% (api.runtime(), api.show_title(), api.title(), api.path()))
|
||||||
else:
|
else:
|
||||||
text += ('#EXTINF:%s,%s\n%s\n'
|
text += ('#EXTINF:%s,%s\n%s\n'
|
||||||
% (api.runtime(), api.title(), api.path()))
|
% (api.runtime(), api.title(), api.path()))
|
||||||
|
|
|
@ -68,7 +68,7 @@ def initialize(playlist, plex_id):
|
||||||
plex_id)
|
plex_id)
|
||||||
raise PlaylistError('Could not initialize Plex playlist %s', plex_id)
|
raise PlaylistError('Could not initialize Plex playlist %s', plex_id)
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
playlist.plex_id = api.plex_id()
|
playlist.plex_id = api.plex_id
|
||||||
playlist.plex_updatedat = api.updated_at()
|
playlist.plex_updatedat = api.updated_at()
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ def add_items(playlist, plex_ids):
|
||||||
raise PlaylistError('Could not add items to a new Plex playlist %s' %
|
raise PlaylistError('Could not add items to a new Plex playlist %s' %
|
||||||
playlist)
|
playlist)
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
playlist.plex_id = api.plex_id()
|
playlist.plex_id = api.plex_id
|
||||||
playlist.plex_updatedat = api.updated_at()
|
playlist.plex_updatedat = api.updated_at()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None):
|
||||||
playqueue.clear()
|
playqueue.clear()
|
||||||
for i, child in enumerate(xml):
|
for i, child in enumerate(xml):
|
||||||
api = API(child)
|
api = API(child)
|
||||||
PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id())
|
PL.add_item_to_playlist(playqueue, i, plex_id=api.plex_id)
|
||||||
playqueue.plex_transient_token = transient_token
|
playqueue.plex_transient_token = transient_token
|
||||||
LOG.debug('Firing up Kodi player')
|
LOG.debug('Firing up Kodi player')
|
||||||
app.APP.player.play(playqueue.kodi_pl, None, False, 0)
|
app.APP.player.play(playqueue.kodi_pl, None, False, 0)
|
||||||
|
|
|
@ -66,7 +66,7 @@ class PlayUtils():
|
||||||
if path is not None and path.endswith('.strm'):
|
if path is not None and path.endswith('.strm'):
|
||||||
LOG.info('.strm file detected')
|
LOG.info('.strm file detected')
|
||||||
playurl = self.api.validate_playurl(path,
|
playurl = self.api.validate_playurl(path,
|
||||||
self.api.plex_type(),
|
self.api.plex_type,
|
||||||
force_check=True)
|
force_check=True)
|
||||||
return playurl
|
return playurl
|
||||||
# set to either 'Direct Stream=1' or 'Transcode=2'
|
# set to either 'Direct Stream=1' or 'Transcode=2'
|
||||||
|
@ -78,7 +78,7 @@ class PlayUtils():
|
||||||
if self.mustTranscode():
|
if self.mustTranscode():
|
||||||
return
|
return
|
||||||
return self.api.validate_playurl(path,
|
return self.api.validate_playurl(path,
|
||||||
self.api.plex_type(),
|
self.api.plex_type,
|
||||||
force_check=True)
|
force_check=True)
|
||||||
|
|
||||||
def mustTranscode(self):
|
def mustTranscode(self):
|
||||||
|
@ -93,7 +93,7 @@ class PlayUtils():
|
||||||
- video bitrate above specified settings bitrate
|
- video bitrate above specified settings bitrate
|
||||||
if the corresponding file settings are set to 'true'
|
if the corresponding file settings are set to 'true'
|
||||||
"""
|
"""
|
||||||
if self.api.plex_type() in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG):
|
if self.api.plex_type in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG):
|
||||||
LOG.info('Plex clip or music track, not transcoding')
|
LOG.info('Plex clip or music track, not transcoding')
|
||||||
return False
|
return False
|
||||||
videoCodec = self.api.video_codec()
|
videoCodec = self.api.video_codec()
|
||||||
|
@ -139,7 +139,7 @@ class PlayUtils():
|
||||||
|
|
||||||
def isDirectStream(self):
|
def isDirectStream(self):
|
||||||
# Never transcode Music
|
# Never transcode Music
|
||||||
if self.api.plex_type() == 'track':
|
if self.api.plex_type == 'track':
|
||||||
return True
|
return True
|
||||||
# set to 'Transcode=2'
|
# set to 'Transcode=2'
|
||||||
if utils.settings('playType') == "2":
|
if utils.settings('playType') == "2":
|
||||||
|
|
File diff suppressed because it is too large
Load diff
31
resources/lib/plex_api/__init__.py
Normal file
31
resources/lib/plex_api/__init__.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
plex_api interfaces with all Plex Media Server (and plex.tv) xml responses
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
|
from .base import Base
|
||||||
|
from .artwork import Artwork
|
||||||
|
from .file import File
|
||||||
|
from .media import Media
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
from ..plex_db import PlexDB
|
||||||
|
|
||||||
|
|
||||||
|
class API(Base, Artwork, File, Media, User):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def mass_api(xml):
|
||||||
|
"""
|
||||||
|
Pass in an entire XML PMS response with e.g. several movies or episodes
|
||||||
|
Will Look-up Kodi ids in the Plex.db for every element (thus speeding up
|
||||||
|
this process for several PMS items!)
|
||||||
|
"""
|
||||||
|
apis = [API(x) for x in xml]
|
||||||
|
with PlexDB(lock=False) as plexdb:
|
||||||
|
for api in apis:
|
||||||
|
api.check_db(plexdb=plexdb)
|
||||||
|
return apis
|
428
resources/lib/plex_api/artwork.py
Normal file
428
resources/lib/plex_api/artwork.py
Normal file
|
@ -0,0 +1,428 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from logging import getLogger
|
||||||
|
from re import sub
|
||||||
|
|
||||||
|
from ..kodi_db import KodiVideoDB, KodiMusicDB
|
||||||
|
from ..downloadutils import DownloadUtils as DU
|
||||||
|
from .. import utils, variables as v, app
|
||||||
|
|
||||||
|
LOG = getLogger('PLEX.api')
|
||||||
|
|
||||||
|
|
||||||
|
class Artwork(object):
|
||||||
|
def one_artwork(self, art_kind, aspect=None):
|
||||||
|
"""
|
||||||
|
aspect can be: 'square', '16:9', 'poster'. Defaults to 'poster'
|
||||||
|
"""
|
||||||
|
aspect = 'poster' if not aspect else aspect
|
||||||
|
if aspect == 'poster':
|
||||||
|
width = 1000
|
||||||
|
height = 1500
|
||||||
|
elif aspect == '16:9':
|
||||||
|
width = 1920
|
||||||
|
height = 1080
|
||||||
|
elif aspect == 'square':
|
||||||
|
width = 1000
|
||||||
|
height = 1000
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('aspect ratio not yet implemented: %s'
|
||||||
|
% aspect)
|
||||||
|
artwork = self.xml.get(art_kind)
|
||||||
|
if not artwork or artwork.startswith('http'):
|
||||||
|
return artwork
|
||||||
|
if '/composite/' in artwork:
|
||||||
|
try:
|
||||||
|
# e.g. Plex collections where artwork already contains width and
|
||||||
|
# height. Need to upscale for better resolution
|
||||||
|
artwork, args = artwork.split('?')
|
||||||
|
args = dict(utils.parse_qsl(args))
|
||||||
|
width = int(args.get('width', 400))
|
||||||
|
height = int(args.get('height', 400))
|
||||||
|
# Adjust to 4k resolution 1920x1080
|
||||||
|
scaling = 1920.0 / float(max(width, height))
|
||||||
|
width = int(scaling * width)
|
||||||
|
height = int(scaling * height)
|
||||||
|
except ValueError:
|
||||||
|
# e.g. playlists
|
||||||
|
pass
|
||||||
|
artwork = '%s?width=%s&height=%s' % (artwork, width, height)
|
||||||
|
artwork = ('%s/photo/:/transcode?width=1920&height=1920&'
|
||||||
|
'minSize=1&upscale=0&url=%s'
|
||||||
|
% (app.CONN.server, utils.quote(artwork)))
|
||||||
|
artwork = self.attach_plex_token_to_url(artwork)
|
||||||
|
return artwork
|
||||||
|
|
||||||
|
def artwork_episode(self, full_artwork):
|
||||||
|
"""
|
||||||
|
Episodes are special, they only get the thumb, because all the other
|
||||||
|
artwork will be saved under season and show EXCEPT if you're
|
||||||
|
constructing a listitem and the item has NOT been synched to the Kodi db
|
||||||
|
"""
|
||||||
|
artworks = {}
|
||||||
|
# Item is currently NOT in the Kodi DB
|
||||||
|
art = self.one_artwork('thumb')
|
||||||
|
if art:
|
||||||
|
artworks['thumb'] = art
|
||||||
|
if not full_artwork:
|
||||||
|
# For episodes, only get the thumb. Everything else stemms from
|
||||||
|
# either the season or the show
|
||||||
|
return artworks
|
||||||
|
for kodi_artwork, plex_artwork in \
|
||||||
|
v.KODI_TO_PLEX_ARTWORK_EPISODE.iteritems():
|
||||||
|
art = self.one_artwork(plex_artwork)
|
||||||
|
if art:
|
||||||
|
artworks[kodi_artwork] = art
|
||||||
|
return artworks
|
||||||
|
|
||||||
|
def artwork(self, kodi_id=None, kodi_type=None, full_artwork=False):
|
||||||
|
"""
|
||||||
|
Gets the URLs to the Plex artwork. Dict keys will be missing if there
|
||||||
|
is no corresponding artwork.
|
||||||
|
Pass kodi_id and kodi_type to grab the artwork saved in the Kodi DB
|
||||||
|
(thus potentially more artwork, e.g. clearart, discart).
|
||||||
|
|
||||||
|
Output ('max' version)
|
||||||
|
{
|
||||||
|
'thumb'
|
||||||
|
'poster'
|
||||||
|
'banner'
|
||||||
|
'clearart'
|
||||||
|
'clearlogo'
|
||||||
|
'fanart'
|
||||||
|
}
|
||||||
|
'landscape' and 'icon' might be implemented later
|
||||||
|
Passing full_artwork=True returns ALL the artwork for the item, so not
|
||||||
|
just 'thumb' for episodes, but also season and show artwork
|
||||||
|
"""
|
||||||
|
if self.plex_type == v.PLEX_TYPE_EPISODE:
|
||||||
|
return self.artwork_episode(full_artwork)
|
||||||
|
artworks = {}
|
||||||
|
if kodi_id:
|
||||||
|
# in Kodi database, potentially with additional e.g. clearart
|
||||||
|
if self.plex_type in v.PLEX_VIDEOTYPES:
|
||||||
|
with KodiVideoDB(lock=False) as kodidb:
|
||||||
|
return kodidb.get_art(kodi_id, kodi_type)
|
||||||
|
else:
|
||||||
|
with KodiMusicDB(lock=False) as kodidb:
|
||||||
|
return kodidb.get_art(kodi_id, kodi_type)
|
||||||
|
|
||||||
|
for kodi_artwork, plex_artwork in v.KODI_TO_PLEX_ARTWORK.iteritems():
|
||||||
|
art = self.one_artwork(plex_artwork)
|
||||||
|
if art:
|
||||||
|
artworks[kodi_artwork] = art
|
||||||
|
if self.plex_type in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_ALBUM):
|
||||||
|
# Get parent item artwork if the main item is missing artwork
|
||||||
|
if 'fanart' not in artworks:
|
||||||
|
art = self.one_artwork('parentArt')
|
||||||
|
if art:
|
||||||
|
artworks['fanart1'] = art
|
||||||
|
if 'poster' not in artworks:
|
||||||
|
art = self.one_artwork('parentThumb')
|
||||||
|
if art:
|
||||||
|
artworks['poster'] = art
|
||||||
|
if self.plex_type in (v.PLEX_TYPE_SONG,
|
||||||
|
v.PLEX_TYPE_ALBUM,
|
||||||
|
v.PLEX_TYPE_ARTIST):
|
||||||
|
# need to set poster also as thumb
|
||||||
|
art = self.one_artwork('thumb')
|
||||||
|
if art:
|
||||||
|
artworks['thumb'] = art
|
||||||
|
if self.plex_type == v.PLEX_TYPE_PLAYLIST:
|
||||||
|
art = self.one_artwork('composite')
|
||||||
|
if art:
|
||||||
|
artworks['thumb'] = art
|
||||||
|
return artworks
|
||||||
|
|
||||||
|
def fanart_artwork(self, artworks):
|
||||||
|
"""
|
||||||
|
Downloads additional fanart from third party sources (well, link to
|
||||||
|
fanart only).
|
||||||
|
"""
|
||||||
|
external_id = self.retrieve_external_item_id()
|
||||||
|
if external_id is not None:
|
||||||
|
artworks = self.lookup_fanart_tv(external_id[0], artworks)
|
||||||
|
return artworks
|
||||||
|
|
||||||
|
def set_artwork(self):
|
||||||
|
"""
|
||||||
|
Gets the URLs to the Plex artwork, or empty string if not found.
|
||||||
|
Only call on movies!
|
||||||
|
"""
|
||||||
|
artworks = {}
|
||||||
|
# Plex does not get much artwork - go ahead and get the rest from
|
||||||
|
# fanart tv only for movie or tv show
|
||||||
|
external_id = self.retrieve_external_item_id(collection=True)
|
||||||
|
if external_id is not None:
|
||||||
|
external_id, poster, background = external_id
|
||||||
|
if poster is not None:
|
||||||
|
artworks['poster'] = poster
|
||||||
|
if background is not None:
|
||||||
|
artworks['fanart'] = background
|
||||||
|
artworks = self.lookup_fanart_tv(external_id, artworks)
|
||||||
|
else:
|
||||||
|
LOG.info('Did not find a set/collection ID on TheMovieDB using %s.'
|
||||||
|
' Artwork will be missing.', self.title())
|
||||||
|
return artworks
|
||||||
|
|
||||||
|
def retrieve_external_item_id(self, collection=False):
|
||||||
|
"""
|
||||||
|
Returns the set
|
||||||
|
media_id [unicode]: the item's IMDB id for movies or tvdb id for
|
||||||
|
TV shows
|
||||||
|
poster [unicode]: path to the item's poster artwork
|
||||||
|
background [unicode]: path to the item's background artwork
|
||||||
|
|
||||||
|
The last two might be None if not found. Generally None is returned
|
||||||
|
if unsuccessful.
|
||||||
|
|
||||||
|
If not found in item's Plex metadata, check themovidedb.org.
|
||||||
|
"""
|
||||||
|
item = self.xml.attrib
|
||||||
|
media_type = self.plex_type
|
||||||
|
media_id = None
|
||||||
|
# Return the saved Plex id's, if applicable
|
||||||
|
# Always seek collection's ids since not provided by PMS
|
||||||
|
if collection is False:
|
||||||
|
if media_type == v.PLEX_TYPE_MOVIE:
|
||||||
|
media_id = self.provider('imdb')
|
||||||
|
elif media_type == v.PLEX_TYPE_SHOW:
|
||||||
|
media_id = self.provider('tvdb')
|
||||||
|
if media_id is not None:
|
||||||
|
return media_id, None, None
|
||||||
|
LOG.info('Plex did not provide ID for IMDB or TVDB. Start '
|
||||||
|
'lookup process')
|
||||||
|
else:
|
||||||
|
LOG.debug('Start movie set/collection lookup on themoviedb with %s',
|
||||||
|
item.get('title', ''))
|
||||||
|
|
||||||
|
api_key = utils.settings('themoviedbAPIKey')
|
||||||
|
if media_type == v.PLEX_TYPE_SHOW:
|
||||||
|
media_type = 'tv'
|
||||||
|
title = self.title()
|
||||||
|
# if the title has the year in remove it as tmdb cannot deal with it...
|
||||||
|
# replace e.g. 'The Americans (2015)' with 'The Americans'
|
||||||
|
title = sub(r'\s*\(\d{4}\)$', '', title, count=1)
|
||||||
|
url = 'https://api.themoviedb.org/3/search/%s' % media_type
|
||||||
|
parameters = {
|
||||||
|
'api_key': api_key,
|
||||||
|
'language': v.KODILANGUAGE,
|
||||||
|
'query': title.encode('utf-8')
|
||||||
|
}
|
||||||
|
data = DU().downloadUrl(url,
|
||||||
|
authenticate=False,
|
||||||
|
parameters=parameters,
|
||||||
|
timeout=7)
|
||||||
|
try:
|
||||||
|
data.get('test')
|
||||||
|
except AttributeError:
|
||||||
|
LOG.warning('Could not download data from FanartTV')
|
||||||
|
return
|
||||||
|
if not data.get('results'):
|
||||||
|
LOG.info('No match found on themoviedb for type: %s, title: %s',
|
||||||
|
media_type, title)
|
||||||
|
return
|
||||||
|
|
||||||
|
year = item.get('year')
|
||||||
|
match_found = None
|
||||||
|
# find year match
|
||||||
|
if year:
|
||||||
|
for entry in data['results']:
|
||||||
|
if year in entry.get('first_air_date', ''):
|
||||||
|
match_found = entry
|
||||||
|
break
|
||||||
|
elif year in entry.get('release_date', ''):
|
||||||
|
match_found = entry
|
||||||
|
break
|
||||||
|
# find exact match based on title, if we haven't found a year match
|
||||||
|
if match_found is None:
|
||||||
|
LOG.info('No themoviedb match found using year %s', year)
|
||||||
|
replacements = (
|
||||||
|
' ',
|
||||||
|
'-',
|
||||||
|
'&',
|
||||||
|
',',
|
||||||
|
':',
|
||||||
|
';'
|
||||||
|
)
|
||||||
|
for entry in data['results']:
|
||||||
|
name = entry.get('name', entry.get('title', ''))
|
||||||
|
original_name = entry.get('original_name', '')
|
||||||
|
title_alt = title.lower()
|
||||||
|
name_alt = name.lower()
|
||||||
|
org_name_alt = original_name.lower()
|
||||||
|
for replace_string in replacements:
|
||||||
|
title_alt = title_alt.replace(replace_string, '')
|
||||||
|
name_alt = name_alt.replace(replace_string, '')
|
||||||
|
org_name_alt = org_name_alt.replace(replace_string, '')
|
||||||
|
if name == title or original_name == title:
|
||||||
|
# match found for exact title name
|
||||||
|
match_found = entry
|
||||||
|
break
|
||||||
|
elif (name.split(' (')[0] == title or title_alt == name_alt or
|
||||||
|
title_alt == org_name_alt):
|
||||||
|
# match found with substituting some stuff
|
||||||
|
match_found = entry
|
||||||
|
break
|
||||||
|
|
||||||
|
# if a match was not found, we accept the closest match from TMDB
|
||||||
|
if match_found is None and data.get('results'):
|
||||||
|
LOG.info('Using very first match from themoviedb')
|
||||||
|
match_found = entry = data.get('results')[0]
|
||||||
|
|
||||||
|
if match_found is None:
|
||||||
|
LOG.info('Still no themoviedb match for type: %s, title: %s, '
|
||||||
|
'year: %s', media_type, title, year)
|
||||||
|
LOG.debug('themoviedb answer was %s', data['results'])
|
||||||
|
return
|
||||||
|
|
||||||
|
LOG.info('Found themoviedb match for %s: %s',
|
||||||
|
item.get('title'), match_found)
|
||||||
|
|
||||||
|
tmdb_id = str(entry.get('id', ''))
|
||||||
|
if tmdb_id == '':
|
||||||
|
LOG.error('No themoviedb ID found, aborting')
|
||||||
|
return
|
||||||
|
|
||||||
|
if media_type == 'multi' and entry.get('media_type'):
|
||||||
|
media_type = entry.get('media_type')
|
||||||
|
name = entry.get('name', entry.get('title'))
|
||||||
|
# lookup external tmdb_id and perform artwork lookup on fanart.tv
|
||||||
|
parameters = {'api_key': api_key}
|
||||||
|
if media_type == 'movie':
|
||||||
|
url = 'https://api.themoviedb.org/3/movie/%s' % tmdb_id
|
||||||
|
parameters['append_to_response'] = 'videos'
|
||||||
|
elif media_type == 'tv':
|
||||||
|
url = 'https://api.themoviedb.org/3/tv/%s' % tmdb_id
|
||||||
|
parameters['append_to_response'] = 'external_ids,videos'
|
||||||
|
media_id, poster, background = None, None, None
|
||||||
|
for language in [v.KODILANGUAGE, 'en']:
|
||||||
|
parameters['language'] = language
|
||||||
|
data = DU().downloadUrl(url,
|
||||||
|
authenticate=False,
|
||||||
|
parameters=parameters,
|
||||||
|
timeout=7)
|
||||||
|
try:
|
||||||
|
data.get('test')
|
||||||
|
except AttributeError:
|
||||||
|
LOG.warning('Could not download %s with parameters %s',
|
||||||
|
url, parameters)
|
||||||
|
continue
|
||||||
|
if collection is False:
|
||||||
|
if data.get('imdb_id'):
|
||||||
|
media_id = str(data.get('imdb_id'))
|
||||||
|
break
|
||||||
|
if (data.get('external_ids') and
|
||||||
|
data['external_ids'].get('tvdb_id')):
|
||||||
|
media_id = str(data['external_ids']['tvdb_id'])
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if not data.get('belongs_to_collection'):
|
||||||
|
continue
|
||||||
|
media_id = data.get('belongs_to_collection').get('id')
|
||||||
|
if not media_id:
|
||||||
|
continue
|
||||||
|
media_id = str(media_id)
|
||||||
|
LOG.debug('Retrieved collections tmdb id %s for %s',
|
||||||
|
media_id, title)
|
||||||
|
url = 'https://api.themoviedb.org/3/collection/%s' % media_id
|
||||||
|
data = DU().downloadUrl(url,
|
||||||
|
authenticate=False,
|
||||||
|
parameters=parameters,
|
||||||
|
timeout=7)
|
||||||
|
try:
|
||||||
|
data.get('poster_path')
|
||||||
|
except AttributeError:
|
||||||
|
LOG.debug('Could not find TheMovieDB poster paths for %s'
|
||||||
|
' in the language %s', title, language)
|
||||||
|
continue
|
||||||
|
if not poster and data.get('poster_path'):
|
||||||
|
poster = ('https://image.tmdb.org/t/p/original%s' %
|
||||||
|
data.get('poster_path'))
|
||||||
|
if not background and data.get('backdrop_path'):
|
||||||
|
background = ('https://image.tmdb.org/t/p/original%s' %
|
||||||
|
data.get('backdrop_path'))
|
||||||
|
return media_id, poster, background
|
||||||
|
|
||||||
|
def lookup_fanart_tv(self, media_id, artworks):
|
||||||
|
"""
|
||||||
|
perform artwork lookup on fanart.tv
|
||||||
|
|
||||||
|
media_id: IMDB id for movies, tvdb id for TV shows
|
||||||
|
"""
|
||||||
|
api_key = utils.settings('FanArtTVAPIKey')
|
||||||
|
typus = self.plex_type
|
||||||
|
if typus == v.PLEX_TYPE_SHOW:
|
||||||
|
typus = 'tv'
|
||||||
|
|
||||||
|
if typus == v.PLEX_TYPE_MOVIE:
|
||||||
|
url = 'http://webservice.fanart.tv/v3/movies/%s?api_key=%s' \
|
||||||
|
% (media_id, api_key)
|
||||||
|
elif typus == 'tv':
|
||||||
|
url = 'http://webservice.fanart.tv/v3/tv/%s?api_key=%s' \
|
||||||
|
% (media_id, api_key)
|
||||||
|
else:
|
||||||
|
# Not supported artwork
|
||||||
|
return artworks
|
||||||
|
data = DU().downloadUrl(url, authenticate=False, timeout=15)
|
||||||
|
try:
|
||||||
|
data.get('test')
|
||||||
|
except AttributeError:
|
||||||
|
LOG.error('Could not download data from FanartTV')
|
||||||
|
return artworks
|
||||||
|
|
||||||
|
fanart_tv_types = list(v.FANART_TV_TO_KODI_TYPE)
|
||||||
|
|
||||||
|
if typus == v.PLEX_TYPE_ARTIST:
|
||||||
|
fanart_tv_types.append(("thumb", "folder"))
|
||||||
|
else:
|
||||||
|
fanart_tv_types.append(("thumb", "thumb"))
|
||||||
|
|
||||||
|
prefixes = (
|
||||||
|
"hd" + typus,
|
||||||
|
"hd",
|
||||||
|
typus,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
for fanart_tv_type, kodi_type in fanart_tv_types:
|
||||||
|
# Skip the ones we already have
|
||||||
|
if kodi_type in artworks:
|
||||||
|
continue
|
||||||
|
for prefix in prefixes:
|
||||||
|
fanarttvimage = prefix + fanart_tv_type
|
||||||
|
if fanarttvimage not in data:
|
||||||
|
continue
|
||||||
|
# select image in preferred language
|
||||||
|
for entry in data[fanarttvimage]:
|
||||||
|
if entry.get("lang") == v.KODILANGUAGE:
|
||||||
|
artworks[kodi_type] = \
|
||||||
|
entry.get("url", "").replace(' ', '%20')
|
||||||
|
break
|
||||||
|
# just grab the first english OR undefinded one as fallback
|
||||||
|
# (so we're actually grabbing the more popular one)
|
||||||
|
if kodi_type not in artworks:
|
||||||
|
for entry in data[fanarttvimage]:
|
||||||
|
if entry.get("lang") in ("en", "00"):
|
||||||
|
artworks[kodi_type] = \
|
||||||
|
entry.get("url", "").replace(' ', '%20')
|
||||||
|
break
|
||||||
|
|
||||||
|
# grab extrafanarts in list
|
||||||
|
fanartcount = 1 if 'fanart' in artworks else ''
|
||||||
|
for prefix in prefixes:
|
||||||
|
fanarttvimage = prefix + 'background'
|
||||||
|
if fanarttvimage not in data:
|
||||||
|
continue
|
||||||
|
for entry in data[fanarttvimage]:
|
||||||
|
if entry.get("url") is None:
|
||||||
|
continue
|
||||||
|
artworks['fanart%s' % fanartcount] = \
|
||||||
|
entry['url'].replace(' ', '%20')
|
||||||
|
try:
|
||||||
|
fanartcount += 1
|
||||||
|
except TypeError:
|
||||||
|
fanartcount = 1
|
||||||
|
if fanartcount >= v.MAX_BACKGROUND_COUNT:
|
||||||
|
break
|
||||||
|
return artworks
|
644
resources/lib/plex_api/base.py
Normal file
644
resources/lib/plex_api/base.py
Normal file
|
@ -0,0 +1,644 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from logging import getLogger
|
||||||
|
from re import sub
|
||||||
|
|
||||||
|
import xbmcgui
|
||||||
|
|
||||||
|
from ..utils import cast
|
||||||
|
from ..plex_db import PlexDB
|
||||||
|
from .. import utils, timing, variables as v, app, plex_functions as PF
|
||||||
|
from .. import widgets
|
||||||
|
|
||||||
|
LOG = getLogger('PLEX.api')
|
||||||
|
|
||||||
|
|
||||||
|
class Base(object):
|
||||||
|
"""
|
||||||
|
Processes a Plex media server's XML response
|
||||||
|
|
||||||
|
xml: xml.etree.ElementTree element
|
||||||
|
"""
|
||||||
|
def __init__(self, xml):
|
||||||
|
self.xml = xml
|
||||||
|
# which media part in the XML response shall we look at if several
|
||||||
|
# media files are present for the SAME video? (e.g. a 4k and a 1080p
|
||||||
|
# version)
|
||||||
|
self.part = 0
|
||||||
|
self.mediastream = None
|
||||||
|
# Make sure we're only checking our Plex DB once
|
||||||
|
self._checked_db = False
|
||||||
|
# In order to run through the leaves of the xml only once
|
||||||
|
self._scanned_children = False
|
||||||
|
self._genres = []
|
||||||
|
self._countries = []
|
||||||
|
self._collections = []
|
||||||
|
self._people = []
|
||||||
|
self._cast = []
|
||||||
|
self._directors = []
|
||||||
|
self._writers = []
|
||||||
|
self._producers = []
|
||||||
|
self._locations = []
|
||||||
|
self._coll_match = None
|
||||||
|
# Plex DB attributes
|
||||||
|
self._section_id = None
|
||||||
|
self._kodi_id = None
|
||||||
|
self._last_sync = None
|
||||||
|
self._last_checksum = None
|
||||||
|
self._kodi_fileid = None
|
||||||
|
self._kodi_pathid = None
|
||||||
|
self._fanart_synced = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tag(self):
|
||||||
|
"""
|
||||||
|
Returns the xml etree tag, e.g. 'Directory', 'Playlist', 'Hub', 'Video'
|
||||||
|
"""
|
||||||
|
return self.xml.tag
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attrib(self):
|
||||||
|
"""
|
||||||
|
Returns the xml etree attrib dict
|
||||||
|
"""
|
||||||
|
return self.xml.attrib
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plex_id(self):
|
||||||
|
"""
|
||||||
|
Returns the Plex ratingKey as an integer or None
|
||||||
|
"""
|
||||||
|
return cast(int, self.xml.get('ratingKey'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plex_type(self):
|
||||||
|
"""
|
||||||
|
Returns the type of media, e.g. 'movie' or 'clip' for trailers as
|
||||||
|
Unicode or None.
|
||||||
|
"""
|
||||||
|
return self.xml.get('type')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def section_id(self):
|
||||||
|
self.check_db()
|
||||||
|
return self._section_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kodi_id(self):
|
||||||
|
self.check_db()
|
||||||
|
return self._kodi_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kodi_type(self):
|
||||||
|
return v.KODITYPE_FROM_PLEXTYPE[self.plex_type]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_sync(self):
|
||||||
|
self.check_db()
|
||||||
|
return self._last_sync
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_checksum(self):
|
||||||
|
self.check_db()
|
||||||
|
return self._last_checksum
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kodi_fileid(self):
|
||||||
|
self.check_db()
|
||||||
|
return self._kodi_fileid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kodi_pathid(self):
|
||||||
|
self.check_db()
|
||||||
|
return self._kodi_pathid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fanart_synced(self):
|
||||||
|
self.check_db()
|
||||||
|
return self._fanart_synced
|
||||||
|
|
||||||
|
def check_db(self, plexdb=None):
|
||||||
|
"""
|
||||||
|
Check's whether we synched this item to Kodi. If so, then retrieve the
|
||||||
|
appropriate Kodi info like the kodi_id and kodi_fileid
|
||||||
|
|
||||||
|
Pass in a plexdb DB-connection for a faster lookup
|
||||||
|
"""
|
||||||
|
if self._checked_db:
|
||||||
|
return
|
||||||
|
self._checked_db = True
|
||||||
|
if self.plex_type == v.PLEX_TYPE_CLIP:
|
||||||
|
# Clips won't ever be synched to Kodi
|
||||||
|
return
|
||||||
|
if plexdb:
|
||||||
|
db_item = plexdb.item_by_id(self.plex_id, self.plex_type)
|
||||||
|
else:
|
||||||
|
with PlexDB(lock=False) as plexdb:
|
||||||
|
db_item = plexdb.item_by_id(self.plex_id, self.plex_type)
|
||||||
|
if not db_item:
|
||||||
|
return
|
||||||
|
self._section_id = db_item['section_id']
|
||||||
|
self._kodi_id = db_item['kodi_id']
|
||||||
|
self._last_sync = db_item['last_sync']
|
||||||
|
self._last_checksum = db_item['checksum']
|
||||||
|
if 'kodi_fileid' in db_item:
|
||||||
|
self._kodi_fileid = db_item['kodi_fileid']
|
||||||
|
if 'kodi_pathid' in db_item:
|
||||||
|
self._kodi_pathid = db_item['kodi_pathid']
|
||||||
|
if 'fanart_synced' in db_item:
|
||||||
|
self._fanart_synced = db_item['fanart_synced']
|
||||||
|
|
||||||
|
def path_and_plex_id(self):
|
||||||
|
"""
|
||||||
|
Returns the Plex key such as '/library/metadata/246922' or None
|
||||||
|
"""
|
||||||
|
return self.xml.get('key')
|
||||||
|
|
||||||
|
def item_id(self):
|
||||||
|
"""
|
||||||
|
Returns current playQueueItemID or if unsuccessful the playListItemID
|
||||||
|
as int.
|
||||||
|
If not found, None is returned
|
||||||
|
"""
|
||||||
|
return (cast(int, self.xml.get('playQueueItemID')) or
|
||||||
|
cast(int, self.xml.get('playListItemID')))
|
||||||
|
|
||||||
|
def playlist_type(self):
|
||||||
|
"""
|
||||||
|
Returns the playlist type ('video', 'audio') or None
|
||||||
|
"""
|
||||||
|
return self.xml.get('playlistType')
|
||||||
|
|
||||||
|
def library_section_id(self):
|
||||||
|
"""
|
||||||
|
Returns the id of the Plex library section (for e.g. a movies section)
|
||||||
|
as an int or None
|
||||||
|
"""
|
||||||
|
return cast(int, self.xml.get('librarySectionID'))
|
||||||
|
|
||||||
|
def guid_html_escaped(self):
|
||||||
|
"""
|
||||||
|
Returns the 'guid' attribute, e.g.
|
||||||
|
'com.plexapp.agents.thetvdb://76648/2/4?lang=en'
|
||||||
|
as an HTML-escaped string or None
|
||||||
|
"""
|
||||||
|
guid = self.xml.get('guid')
|
||||||
|
return utils.escape_html(guid) if guid else None
|
||||||
|
|
||||||
|
def date_created(self):
|
||||||
|
"""
|
||||||
|
Returns the date when this library item was created in Kodi-time as
|
||||||
|
unicode
|
||||||
|
|
||||||
|
If not found, returns 2000-01-01 10:00:00
|
||||||
|
"""
|
||||||
|
res = self.xml.get('addedAt')
|
||||||
|
return timing.plex_date_to_kodi(res) if res else '2000-01-01 10:00:00'
|
||||||
|
|
||||||
|
def updated_at(self):
|
||||||
|
"""
|
||||||
|
Returns the last time this item was updated as an int, e.g.
|
||||||
|
1524739868 or None
|
||||||
|
"""
|
||||||
|
return cast(int, self.xml.get('updatedAt'))
|
||||||
|
|
||||||
|
def checksum(self):
|
||||||
|
"""
|
||||||
|
Returns the unique int <ratingKey><updatedAt>. If updatedAt is not set,
|
||||||
|
addedAt is used.
|
||||||
|
"""
|
||||||
|
return int('%s%s' % (self.xml.get('ratingKey'),
|
||||||
|
self.xml.get('updatedAt') or
|
||||||
|
self.xml.get('addedAt', '1541572987')))
|
||||||
|
|
||||||
|
def title(self):
|
||||||
|
"""
|
||||||
|
Returns the title of the element as unicode or 'Missing Title'
|
||||||
|
"""
|
||||||
|
return self.xml.get('title', 'Missing Title')
|
||||||
|
|
||||||
|
def sorttitle(self):
|
||||||
|
"""
|
||||||
|
Returns an item's sorting name/title or the title itself if not found
|
||||||
|
"Missing Title" if both are not present
|
||||||
|
"""
|
||||||
|
return self.xml.get('titleSort',
|
||||||
|
self.xml.get('title', 'Missing Title'))
|
||||||
|
|
||||||
|
def plex_media_streams(self):
|
||||||
|
"""
|
||||||
|
Returns the media streams directly from the PMS xml.
|
||||||
|
Mind to set self.mediastream and self.part before calling this method!
|
||||||
|
"""
|
||||||
|
return self.xml[self.mediastream][self.part]
|
||||||
|
|
||||||
|
def plot(self):
|
||||||
|
"""
|
||||||
|
Returns the plot or None.
|
||||||
|
"""
|
||||||
|
return self.xml.get('summary')
|
||||||
|
|
||||||
|
def tagline(self):
|
||||||
|
"""
|
||||||
|
Returns a shorter tagline of the plot or None
|
||||||
|
"""
|
||||||
|
return self.xml.get('tagline')
|
||||||
|
|
||||||
|
def shortplot(self):
|
||||||
|
"""
|
||||||
|
Not yet implemented - returns None
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def premiere_date(self):
|
||||||
|
"""
|
||||||
|
Returns the "originallyAvailableAt", e.g. "2018-11-16" or None
|
||||||
|
"""
|
||||||
|
return self.xml.get('originallyAvailableAt')
|
||||||
|
|
||||||
|
def kodi_premiere_date(self):
|
||||||
|
"""
|
||||||
|
Takes Plex' originallyAvailableAt of the form "yyyy-mm-dd" and returns
|
||||||
|
Kodi's "dd.mm.yyyy" or None
|
||||||
|
"""
|
||||||
|
date = self.premiere_date()
|
||||||
|
if date is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
date = sub(r'(\d+)-(\d+)-(\d+)', r'\3.\2.\1', date)
|
||||||
|
except Exception:
|
||||||
|
date = None
|
||||||
|
return date
|
||||||
|
|
||||||
|
def year(self):
|
||||||
|
"""
|
||||||
|
Returns the production(?) year ("year") as Unicode or None
|
||||||
|
"""
|
||||||
|
return self.xml.get('year')
|
||||||
|
|
||||||
|
def studios(self):
|
||||||
|
"""
|
||||||
|
Returns a list of the 'studio' - currently only ever 1 entry.
|
||||||
|
Or returns an empty list
|
||||||
|
"""
|
||||||
|
return [self.xml.get('studio')] if self.xml.get('studio') else []
|
||||||
|
|
||||||
|
def content_rating(self):
|
||||||
|
"""
|
||||||
|
Get the content rating or None
|
||||||
|
"""
|
||||||
|
mpaa = self.xml.get('contentRating')
|
||||||
|
if not mpaa:
|
||||||
|
return
|
||||||
|
# Convert more complex cases
|
||||||
|
if mpaa in ('NR', 'UR'):
|
||||||
|
# Kodi seems to not like NR, but will accept Rated Not Rated
|
||||||
|
mpaa = 'Rated Not Rated'
|
||||||
|
elif mpaa.startswith('gb/'):
|
||||||
|
mpaa = mpaa.replace('gb/', 'UK:', 1)
|
||||||
|
return mpaa
|
||||||
|
|
||||||
|
def rating(self):
|
||||||
|
"""
|
||||||
|
Returns the rating [float] first from 'audienceRating', if that fails
|
||||||
|
from 'rating'.
|
||||||
|
Returns 0.0 if both are not found
|
||||||
|
"""
|
||||||
|
return cast(float, self.xml.get('audienceRating',
|
||||||
|
self.xml.get('rating'))) or 0.0
|
||||||
|
|
||||||
|
def votecount(self):
|
||||||
|
"""
|
||||||
|
Not implemented by Plex yet - returns None
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def runtime(self):
|
||||||
|
"""
|
||||||
|
Returns the total duration of the element in seconds as int.
|
||||||
|
0 if not found
|
||||||
|
"""
|
||||||
|
runtime = cast(float, self.xml.get('duration')) or 0.0
|
||||||
|
return int(runtime * v.PLEX_TO_KODI_TIMEFACTOR)
|
||||||
|
|
||||||
|
def leave_count(self):
|
||||||
|
"""
|
||||||
|
Returns the following dict or None
|
||||||
|
{
|
||||||
|
'totalepisodes': unicode('leafCount'),
|
||||||
|
'watchedepisodes': unicode('viewedLeafCount'),
|
||||||
|
'unwatchedepisodes': unicode(totalepisodes - watchedepisodes)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
total = int(self.xml.attrib['leafCount'])
|
||||||
|
watched = int(self.xml.attrib['viewedLeafCount'])
|
||||||
|
return {
|
||||||
|
'totalepisodes': unicode(total),
|
||||||
|
'watchedepisodes': unicode(watched),
|
||||||
|
'unwatchedepisodes': unicode(total - watched)
|
||||||
|
}
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Stuff having to do with parent and grandparent items
|
||||||
|
######################################################
|
||||||
|
def index(self):
|
||||||
|
"""
|
||||||
|
Returns the 'index' of the element [int]. Depicts e.g. season number of
|
||||||
|
the season or the track number of the song
|
||||||
|
"""
|
||||||
|
return cast(int, self.xml.get('index'))
|
||||||
|
|
||||||
|
def show_id(self):
|
||||||
|
"""
|
||||||
|
Returns the episode's tv show's Plex id [int] or None
|
||||||
|
"""
|
||||||
|
return self.grandparent_id()
|
||||||
|
|
||||||
|
def show_title(self):
|
||||||
|
"""
|
||||||
|
Returns the episode's tv show's name/title [unicode] or None
|
||||||
|
"""
|
||||||
|
return self.grandparent_title()
|
||||||
|
|
||||||
|
def season_id(self):
|
||||||
|
"""
|
||||||
|
Returns the episode's season's Plex id [int] or None
|
||||||
|
"""
|
||||||
|
return self.parent_id()
|
||||||
|
|
||||||
|
def season_number(self):
|
||||||
|
"""
|
||||||
|
Returns the episode's season number (e.g. season '2') as an int or None
|
||||||
|
"""
|
||||||
|
return self.parent_index()
|
||||||
|
|
||||||
|
def artist_name(self):
|
||||||
|
"""
|
||||||
|
Returns the artist name for an album: first it attempts to return
|
||||||
|
'parentTitle', if that failes 'originalTitle'
|
||||||
|
"""
|
||||||
|
return self.xml.get('parentTitle', self.xml.get('originalTitle'))
|
||||||
|
|
||||||
|
def parent_id(self):
|
||||||
|
"""
|
||||||
|
Returns the 'parentRatingKey' as int or None
|
||||||
|
"""
|
||||||
|
return cast(int, self.xml.get('parentRatingKey'))
|
||||||
|
|
||||||
|
def parent_index(self):
|
||||||
|
"""
|
||||||
|
Returns the 'parentRatingKey' as int or None
|
||||||
|
"""
|
||||||
|
return cast(int, self.xml.get('parentIndex'))
|
||||||
|
|
||||||
|
def grandparent_id(self):
|
||||||
|
"""
|
||||||
|
Returns the ratingKey for the corresponding grandparent, e.g. a TV show
|
||||||
|
for episodes, or None
|
||||||
|
"""
|
||||||
|
return cast(int, self.xml.get('grandparentRatingKey'))
|
||||||
|
|
||||||
|
def grandparent_title(self):
|
||||||
|
"""
|
||||||
|
Returns the title for the corresponding grandparent, e.g. a TV show
|
||||||
|
name for episodes, or None
|
||||||
|
"""
|
||||||
|
return self.xml.get('grandparentTitle')
|
||||||
|
|
||||||
|
def disc_number(self):
|
||||||
|
"""
|
||||||
|
Returns the song's disc number as an int or None if not found
|
||||||
|
"""
|
||||||
|
return self.parent_index()
|
||||||
|
|
||||||
|
def _scan_children(self):
|
||||||
|
"""
|
||||||
|
Ensures that we're scanning the xml's subelements only once
|
||||||
|
"""
|
||||||
|
if self._scanned_children:
|
||||||
|
return
|
||||||
|
self._scanned_children = True
|
||||||
|
cast_order = 0
|
||||||
|
for child in self.xml:
|
||||||
|
if child.tag == 'Role':
|
||||||
|
self._cast.append((child.get('tag'),
|
||||||
|
child.get('thumb'),
|
||||||
|
child.get('role'),
|
||||||
|
cast_order))
|
||||||
|
cast_order += 1
|
||||||
|
elif child.tag == 'Genre':
|
||||||
|
self._genres.append(child.get('tag'))
|
||||||
|
elif child.tag == 'Country':
|
||||||
|
self._countries.append(child.get('tag'))
|
||||||
|
elif child.tag == 'Director':
|
||||||
|
self._directors.append(child.get('tag'))
|
||||||
|
elif child.tag == 'Writer':
|
||||||
|
self._writers.append(child.get('tag'))
|
||||||
|
elif child.tag == 'Producer':
|
||||||
|
self._producers.append(child.get('tag'))
|
||||||
|
elif child.tag == 'Location':
|
||||||
|
self._locations.append(child.get('path'))
|
||||||
|
elif child.tag == 'Collection':
|
||||||
|
self._collections.append((cast(int, child.get('id')),
|
||||||
|
child.get('tag')))
|
||||||
|
|
||||||
|
def cast(self):
|
||||||
|
"""
|
||||||
|
Returns a list of tuples of the cast:
|
||||||
|
[(<name of actor [unicode]>,
|
||||||
|
<thumb url [unicode, may be None]>,
|
||||||
|
<role [unicode, may be None]>,
|
||||||
|
<order of appearance [int]>)]
|
||||||
|
"""
|
||||||
|
self._scan_children()
|
||||||
|
return self._cast
|
||||||
|
|
||||||
|
def genres(self):
|
||||||
|
"""
|
||||||
|
Returns a list of genres found
|
||||||
|
"""
|
||||||
|
self._scan_children()
|
||||||
|
return self._genres
|
||||||
|
|
||||||
|
def countries(self):
|
||||||
|
"""
|
||||||
|
Returns a list of all countries
|
||||||
|
"""
|
||||||
|
self._scan_children()
|
||||||
|
return self._countries
|
||||||
|
|
||||||
|
def directors(self):
|
||||||
|
"""
|
||||||
|
Returns a list of all directors
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._scan_children()
|
||||||
|
return self._directors
|
||||||
|
|
||||||
|
def writers(self):
|
||||||
|
"""
|
||||||
|
Returns a list of all writers
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._scan_children()
|
||||||
|
return self._writers
|
||||||
|
|
||||||
|
def producers(self):
|
||||||
|
"""
|
||||||
|
Returns a list of all producers
|
||||||
|
"""
|
||||||
|
self._scan_children()
|
||||||
|
return self._producers
|
||||||
|
|
||||||
|
def tv_show_path(self):
|
||||||
|
"""
|
||||||
|
Returns the direct path to the TV show, e.g. '\\NAS\tv\series'
|
||||||
|
or None
|
||||||
|
"""
|
||||||
|
self._scan_children()
|
||||||
|
if self._locations:
|
||||||
|
return self._locations[0]
|
||||||
|
|
||||||
|
def collections(self):
|
||||||
|
"""
|
||||||
|
Returns a list of tuples of the collection id and tags or an empty list
|
||||||
|
[(<collection id 1>, <collection name 1>), ...]
|
||||||
|
"""
|
||||||
|
self._scan_children()
|
||||||
|
return self._collections
|
||||||
|
|
||||||
|
def people(self):
|
||||||
|
"""
|
||||||
|
Returns a dict with lists of tuples:
|
||||||
|
{
|
||||||
|
'actor': [(<name of actor [unicode]>,
|
||||||
|
<thumb url [unicode, may be None]>,
|
||||||
|
<role [unicode, may be None]>,
|
||||||
|
<order of appearance [int]>)]
|
||||||
|
'director': [..., (<name>, ), ...],
|
||||||
|
'writer': [..., (<name>, ), ...]
|
||||||
|
}
|
||||||
|
Everything in unicode, except <cast order> which is an int.
|
||||||
|
Only <art-url> and <role> may be None if not found.
|
||||||
|
"""
|
||||||
|
self._scan_children()
|
||||||
|
return {
|
||||||
|
'actor': self._cast,
|
||||||
|
'director': [(x, ) for x in self._directors],
|
||||||
|
'writer': [(x, ) for x in self._writers]
|
||||||
|
}
|
||||||
|
|
||||||
|
def provider(self, providername=None):
|
||||||
|
"""
|
||||||
|
providername: e.g. 'imdb', 'tvdb'
|
||||||
|
|
||||||
|
Return IMDB, e.g. "tt0903624". Returns None if not found
|
||||||
|
"""
|
||||||
|
item = self.xml.get('guid')
|
||||||
|
if not item:
|
||||||
|
return
|
||||||
|
if providername == 'imdb':
|
||||||
|
regex = utils.REGEX_IMDB
|
||||||
|
elif providername == 'tvdb':
|
||||||
|
# originally e.g. com.plexapp.agents.thetvdb://276564?lang=en
|
||||||
|
regex = utils.REGEX_TVDB
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('Not implemented: %s' % providername)
|
||||||
|
|
||||||
|
provider = regex.findall(item)
|
||||||
|
try:
|
||||||
|
provider = provider[0]
|
||||||
|
except IndexError:
|
||||||
|
provider = None
|
||||||
|
return provider
|
||||||
|
|
||||||
|
def extras(self):
|
||||||
|
"""
|
||||||
|
Returns an iterator for etree elements for each extra, e.g. trailers
|
||||||
|
Returns None if no extras are found
|
||||||
|
"""
|
||||||
|
extras = self.xml.find('Extras')
|
||||||
|
if not extras:
|
||||||
|
return
|
||||||
|
return (x for x in extras)
|
||||||
|
|
||||||
|
def trailer(self):
|
||||||
|
"""
|
||||||
|
Returns the URL for a single trailer (local trailer preferred; first
|
||||||
|
trailer found returned) or an add-on path to list all Plex extras
|
||||||
|
if the user setting showExtrasInsteadOfTrailer is set.
|
||||||
|
Returns None if nothing is found.
|
||||||
|
"""
|
||||||
|
url = None
|
||||||
|
for extras in self.xml.iterfind('Extras'):
|
||||||
|
# There will always be only 1 extras element
|
||||||
|
if (len(extras) > 0 and
|
||||||
|
app.SYNC.show_extras_instead_of_playing_trailer):
|
||||||
|
return ('plugin://%s?mode=route_to_extras&plex_id=%s'
|
||||||
|
% (v.ADDON_ID, self.plex_id))
|
||||||
|
for extra in extras:
|
||||||
|
typus = cast(int, extra.get('extraType'))
|
||||||
|
if typus != 1:
|
||||||
|
# Skip non-trailers
|
||||||
|
continue
|
||||||
|
if extra.get('guid', '').startswith('file:'):
|
||||||
|
url = extra.get('ratingKey')
|
||||||
|
# Always prefer local trailers (first one listed)
|
||||||
|
break
|
||||||
|
elif not url:
|
||||||
|
url = extra.get('ratingKey')
|
||||||
|
if url:
|
||||||
|
url = ('plugin://%s.movies/?plex_id=%s&plex_type=%s&mode=play'
|
||||||
|
% (v.ADDON_ID, url, v.PLEX_TYPE_CLIP))
|
||||||
|
return url
|
||||||
|
|
||||||
|
def listitem(self, listitem=xbmcgui.ListItem):
|
||||||
|
"""
|
||||||
|
Returns a xbmcgui.ListItem() (or PKCListItem) for this Plex element
|
||||||
|
"""
|
||||||
|
item = widgets.generate_item(self)
|
||||||
|
item = widgets.prepare_listitem(item)
|
||||||
|
return widgets.create_listitem(item, as_tuple=False, listitem=listitem)
|
||||||
|
|
||||||
|
def collections_match(self, section_id):
|
||||||
|
"""
|
||||||
|
Downloads one additional xml from the PMS in order to return a list of
|
||||||
|
tuples [(collection_id, plex_id), ...] for all collections of the
|
||||||
|
current item's Plex library sectin
|
||||||
|
Pass in the collection id of e.g. the movie's metadata
|
||||||
|
"""
|
||||||
|
if self._coll_match is None:
|
||||||
|
self._coll_match = PF.collections(section_id)
|
||||||
|
if self._coll_match is None:
|
||||||
|
LOG.error('Could not download collections for %s',
|
||||||
|
self.library_section_id())
|
||||||
|
self._coll_match = []
|
||||||
|
self._coll_match = \
|
||||||
|
[(utils.cast(int, x.get('index')),
|
||||||
|
utils.cast(int, x.get('ratingKey'))) for x in self._coll_match]
|
||||||
|
return self._coll_match
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def attach_plex_token_to_url(url):
|
||||||
|
"""
|
||||||
|
Returns an extended URL with the Plex token included as 'X-Plex-Token='
|
||||||
|
|
||||||
|
url may or may not already contain a '?'
|
||||||
|
"""
|
||||||
|
if not app.ACCOUNT.pms_token:
|
||||||
|
return url
|
||||||
|
if '?' not in url:
|
||||||
|
return "%s?X-Plex-Token=%s" % (url, app.ACCOUNT.pms_token)
|
||||||
|
else:
|
||||||
|
return "%s&X-Plex-Token=%s" % (url, app.ACCOUNT.pms_token)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_to_string(input_list):
|
||||||
|
"""
|
||||||
|
Concatenates input_list (list of unicodes) with a separator ' / '
|
||||||
|
Returns None if the list was empty
|
||||||
|
"""
|
||||||
|
return ' / '.join(input_list) or None
|
168
resources/lib/plex_api/file.py
Normal file
168
resources/lib/plex_api/file.py
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
|
from ..utils import cast
|
||||||
|
from .. import utils, variables as v, app
|
||||||
|
|
||||||
|
|
||||||
|
def _transcode_image_path(key, AuthToken, path, width, height):
|
||||||
|
"""
|
||||||
|
Transcode Image support
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
key
|
||||||
|
AuthToken
|
||||||
|
path - source path of current XML: path[srcXML]
|
||||||
|
width
|
||||||
|
height
|
||||||
|
result:
|
||||||
|
final path to image file
|
||||||
|
"""
|
||||||
|
# external address - can we get a transcoding request for external images?
|
||||||
|
if key.startswith('http'):
|
||||||
|
path = key
|
||||||
|
elif key.startswith('/'): # internal full path.
|
||||||
|
path = 'http://127.0.0.1:32400' + key
|
||||||
|
else: # internal path, add-on
|
||||||
|
path = 'http://127.0.0.1:32400' + path + '/' + key
|
||||||
|
# This is bogus (note the extra path component) but ATV is stupid when it
|
||||||
|
# comes to caching images, it doesn't use querystrings. Fortunately PMS is
|
||||||
|
# lenient...
|
||||||
|
transcode_path = ('/photo/:/transcode/%sx%s/%s'
|
||||||
|
% (width, height, utils.quote_plus(path)))
|
||||||
|
args = {
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
'url': path
|
||||||
|
}
|
||||||
|
if AuthToken:
|
||||||
|
args['X-Plex-Token'] = AuthToken
|
||||||
|
return utils.extend_url(transcode_path, args)
|
||||||
|
|
||||||
|
|
||||||
|
class File(object):
|
||||||
|
def path(self, force_first_media=True, force_addon=False,
|
||||||
|
direct_paths=None):
|
||||||
|
"""
|
||||||
|
Returns a "fully qualified path": add-on paths or direct paths
|
||||||
|
depending on the current settings. Will NOT valide the playurl
|
||||||
|
Returns unicode or None if something went wrong.
|
||||||
|
|
||||||
|
Pass direct_path=True if you're calling from another Plex python
|
||||||
|
instance - because otherwise direct paths will evaluate to False!
|
||||||
|
"""
|
||||||
|
direct_paths = direct_paths or app.SYNC.direct_paths
|
||||||
|
filename = self.file_path(force_first_media=force_first_media)
|
||||||
|
if (not direct_paths or force_addon or
|
||||||
|
self.plex_type == v.PLEX_TYPE_CLIP):
|
||||||
|
if filename and '/' in filename:
|
||||||
|
filename = filename.rsplit('/', 1)
|
||||||
|
elif filename:
|
||||||
|
filename = filename.rsplit('\\', 1)
|
||||||
|
try:
|
||||||
|
filename = filename[1]
|
||||||
|
except (TypeError, IndexError):
|
||||||
|
filename = None
|
||||||
|
# Set plugin path and media flags using real filename
|
||||||
|
if self.plex_type == v.PLEX_TYPE_EPISODE:
|
||||||
|
# need to include the plex show id in the path
|
||||||
|
path = ('plugin://plugin.video.plexkodiconnect.tvshows/%s/'
|
||||||
|
% self.grandparent_id())
|
||||||
|
else:
|
||||||
|
path = 'plugin://%s/' % v.ADDON_TYPE[self.plex_type]
|
||||||
|
path = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s'
|
||||||
|
% (path, self.plex_id, self.plex_type, filename))
|
||||||
|
else:
|
||||||
|
# Direct paths is set the Kodi way
|
||||||
|
path = self.validate_playurl(filename,
|
||||||
|
self.plex_type,
|
||||||
|
omit_check=True)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def directory_path(self, section_id=None, plex_type=None, old_key=None,
|
||||||
|
synched=True):
|
||||||
|
key = self.xml.get('fastKey')
|
||||||
|
if not key:
|
||||||
|
key = self.xml.get('key')
|
||||||
|
if old_key:
|
||||||
|
key = '%s/%s' % (old_key, key)
|
||||||
|
elif not key.startswith('/'):
|
||||||
|
key = '/library/sections/%s/%s' % (section_id, key)
|
||||||
|
params = {
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': key,
|
||||||
|
'plex_type': plex_type or self.plex_type
|
||||||
|
}
|
||||||
|
if not synched:
|
||||||
|
# No item to be found in the Kodi DB
|
||||||
|
params['synched'] = 'false'
|
||||||
|
if self.xml.get('prompt'):
|
||||||
|
# User input needed, e.g. search for a movie or episode
|
||||||
|
params['prompt'] = self.xml.get('prompt')
|
||||||
|
if section_id:
|
||||||
|
params['id'] = section_id
|
||||||
|
return utils.extend_url('plugin://%s/' % v.ADDON_ID, params)
|
||||||
|
|
||||||
|
def file_name(self, force_first_media=False):
|
||||||
|
"""
|
||||||
|
Returns only the filename, e.g. 'movie.mkv' as unicode or None if not
|
||||||
|
found
|
||||||
|
"""
|
||||||
|
ans = self.file_path(force_first_media=force_first_media)
|
||||||
|
if ans is None:
|
||||||
|
return
|
||||||
|
if "\\" in ans:
|
||||||
|
# Local path
|
||||||
|
filename = ans.rsplit("\\", 1)[1]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# Network share
|
||||||
|
filename = ans.rsplit("/", 1)[1]
|
||||||
|
except IndexError:
|
||||||
|
# E.g. certain Plex channels
|
||||||
|
filename = None
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def file_path(self, force_first_media=False):
|
||||||
|
"""
|
||||||
|
Returns the direct path to this item, e.g. '\\NAS\movies\movie.mkv'
|
||||||
|
as unicode or None
|
||||||
|
|
||||||
|
force_first_media=True:
|
||||||
|
will always use 1st media stream, e.g. when several different
|
||||||
|
files are present for the same PMS item
|
||||||
|
"""
|
||||||
|
if self.mediastream is None and force_first_media is False:
|
||||||
|
if self.mediastream_number() is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if force_first_media is False:
|
||||||
|
ans = cast(str, self.xml[self.mediastream][self.part].attrib['file'])
|
||||||
|
else:
|
||||||
|
ans = cast(str, self.xml[0][self.part].attrib['file'])
|
||||||
|
except (TypeError, AttributeError, IndexError, KeyError):
|
||||||
|
return
|
||||||
|
return utils.unquote(ans)
|
||||||
|
|
||||||
|
def get_picture_path(self):
|
||||||
|
"""
|
||||||
|
Returns the item's picture path (transcode, if necessary) as string.
|
||||||
|
Will always use addon paths, never direct paths
|
||||||
|
"""
|
||||||
|
path = self.xml[0][0].get('key')
|
||||||
|
extension = path[path.rfind('.'):].lower()
|
||||||
|
if app.SYNC.force_transcode_pix or extension not in v.KODI_SUPPORTED_IMAGES:
|
||||||
|
# Let Plex transcode
|
||||||
|
# max width/height supported by plex image transcoder is 1920x1080
|
||||||
|
path = app.CONN.server + _transcode_image_path(
|
||||||
|
path,
|
||||||
|
app.ACCOUNT.pms_token,
|
||||||
|
"%s%s" % (app.CONN.server, path),
|
||||||
|
1920,
|
||||||
|
1080)
|
||||||
|
else:
|
||||||
|
path = self.attach_plex_token_to_url('%s%s' % (app.CONN.server, path))
|
||||||
|
# Attach Plex id to url to let it be picked up by our playqueue agent
|
||||||
|
# later
|
||||||
|
return '%s&plex_id=%s' % (path, self.plex_id)
|
410
resources/lib/plex_api/media.py
Normal file
410
resources/lib/plex_api/media.py
Normal file
|
@ -0,0 +1,410 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from ..utils import cast
|
||||||
|
from ..downloadutils import DownloadUtils as DU
|
||||||
|
from .. import utils, variables as v, app, path_ops, clientinfo
|
||||||
|
|
||||||
|
LOG = getLogger('PLEX.api')
|
||||||
|
|
||||||
|
|
||||||
|
class Media(object):
|
||||||
|
def should_stream(self):
|
||||||
|
"""
|
||||||
|
Returns True if the item's 'optimizedForStreaming' is set, False other-
|
||||||
|
wise
|
||||||
|
"""
|
||||||
|
return cast(bool, self.xml[0].get('optimizedForStreaming')) or False
|
||||||
|
|
||||||
|
def _from_part_or_media(self, key):
|
||||||
|
"""
|
||||||
|
Retrieves XML data 'key' first from the active part. If unsuccessful,
|
||||||
|
tries to retrieve the data from the Media response part.
|
||||||
|
|
||||||
|
If all fails, None is returned.
|
||||||
|
"""
|
||||||
|
return self.xml[0][self.part].get(key, self.xml[0].get(key))
|
||||||
|
|
||||||
|
def video_codec(self):
|
||||||
|
"""
|
||||||
|
Returns the video codec and resolution for the child and part selected.
|
||||||
|
If any data is not found on a part-level, the Media-level data is
|
||||||
|
returned.
|
||||||
|
If that also fails (e.g. for old trailers, None is returned)
|
||||||
|
|
||||||
|
Output:
|
||||||
|
{
|
||||||
|
'videocodec': xxx, e.g. 'h264'
|
||||||
|
'resolution': xxx, e.g. '720' or '1080'
|
||||||
|
'height': xxx, e.g. '816'
|
||||||
|
'width': xxx, e.g. '1920'
|
||||||
|
'aspectratio': xxx, e.g. '1.78'
|
||||||
|
'bitrate': xxx, e.g. '10642'
|
||||||
|
'container': xxx e.g. 'mkv',
|
||||||
|
'bitDepth': xxx e.g. '8', '10'
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
answ = {
|
||||||
|
'videocodec': self._from_part_or_media('videoCodec'),
|
||||||
|
'resolution': self._from_part_or_media('videoResolution'),
|
||||||
|
'height': self._from_part_or_media('height'),
|
||||||
|
'width': self._from_part_or_media('width'),
|
||||||
|
'aspectratio': self._from_part_or_media('aspectratio'),
|
||||||
|
'bitrate': self._from_part_or_media('bitrate'),
|
||||||
|
'container': self._from_part_or_media('container'),
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
answ['bitDepth'] = self.xml[0][self.part][self.mediastream].get('bitDepth')
|
||||||
|
except (TypeError, AttributeError, KeyError, IndexError):
|
||||||
|
answ['bitDepth'] = None
|
||||||
|
return answ
|
||||||
|
|
||||||
|
def mediastreams(self):
|
||||||
|
"""
|
||||||
|
Returns the media streams for metadata purposes
|
||||||
|
|
||||||
|
Output: each track contains a dictionaries
|
||||||
|
{
|
||||||
|
'video': videotrack-list, 'codec', 'height', 'width',
|
||||||
|
'aspect', 'video3DFormat'
|
||||||
|
'audio': audiotrack-list, 'codec', 'channels',
|
||||||
|
'language'
|
||||||
|
'subtitle': list of subtitle languages (or "Unknown")
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
videotracks = []
|
||||||
|
audiotracks = []
|
||||||
|
subtitlelanguages = []
|
||||||
|
try:
|
||||||
|
# Sometimes, aspectratio is on the "toplevel"
|
||||||
|
aspect = cast(float, self.xml[0].get('aspectRatio'))
|
||||||
|
except IndexError:
|
||||||
|
# There is no stream info at all, returning empty
|
||||||
|
return {
|
||||||
|
'video': videotracks,
|
||||||
|
'audio': audiotracks,
|
||||||
|
'subtitle': subtitlelanguages
|
||||||
|
}
|
||||||
|
# Loop over parts
|
||||||
|
for child in self.xml[0]:
|
||||||
|
container = child.get('container')
|
||||||
|
# Loop over Streams
|
||||||
|
for stream in child:
|
||||||
|
media_type = int(stream.get('streamType', 999))
|
||||||
|
track = {}
|
||||||
|
if media_type == 1: # Video streams
|
||||||
|
if 'codec' in stream.attrib:
|
||||||
|
track['codec'] = stream.get('codec').lower()
|
||||||
|
if "msmpeg4" in track['codec']:
|
||||||
|
track['codec'] = "divx"
|
||||||
|
elif "mpeg4" in track['codec']:
|
||||||
|
pass
|
||||||
|
elif "h264" in track['codec']:
|
||||||
|
if container in ("mp4", "mov", "m4v"):
|
||||||
|
track['codec'] = "avc1"
|
||||||
|
track['height'] = cast(int, stream.get('height'))
|
||||||
|
track['width'] = cast(int, stream.get('width'))
|
||||||
|
# track['Video3DFormat'] = item.get('Video3DFormat')
|
||||||
|
track['aspect'] = cast(float,
|
||||||
|
stream.get('aspectRatio') or aspect)
|
||||||
|
track['duration'] = self.runtime()
|
||||||
|
track['video3DFormat'] = None
|
||||||
|
videotracks.append(track)
|
||||||
|
elif media_type == 2: # Audio streams
|
||||||
|
if 'codec' in stream.attrib:
|
||||||
|
track['codec'] = stream.get('codec').lower()
|
||||||
|
if ("dca" in track['codec'] and
|
||||||
|
"ma" in stream.get('profile', '').lower()):
|
||||||
|
track['codec'] = "dtshd_ma"
|
||||||
|
track['channels'] = cast(int, stream.get('channels'))
|
||||||
|
# 'unknown' if we cannot get language
|
||||||
|
track['language'] = stream.get('languageCode',
|
||||||
|
utils.lang(39310).lower())
|
||||||
|
audiotracks.append(track)
|
||||||
|
elif media_type == 3: # Subtitle streams
|
||||||
|
# 'unknown' if we cannot get language
|
||||||
|
subtitlelanguages.append(
|
||||||
|
stream.get('languageCode', utils.lang(39310)).lower())
|
||||||
|
return {
|
||||||
|
'video': videotracks,
|
||||||
|
'audio': audiotracks,
|
||||||
|
'subtitle': subtitlelanguages
|
||||||
|
}
|
||||||
|
|
||||||
|
def mediastream_number(self):
|
||||||
|
"""
|
||||||
|
Returns the Media stream as an int (mostly 0). Will let the user choose
|
||||||
|
if several media streams are present for a PMS item (if settings are
|
||||||
|
set accordingly)
|
||||||
|
|
||||||
|
Returns None if the user aborted selection (leaving self.mediastream at
|
||||||
|
its default of None)
|
||||||
|
"""
|
||||||
|
# How many streams do we have?
|
||||||
|
count = 0
|
||||||
|
for entry in self.xml.iterfind('./Media'):
|
||||||
|
count += 1
|
||||||
|
if (count > 1 and (
|
||||||
|
(self.plex_type != v.PLEX_TYPE_CLIP and
|
||||||
|
utils.settings('bestQuality') == 'false')
|
||||||
|
or
|
||||||
|
(self.plex_type == v.PLEX_TYPE_CLIP and
|
||||||
|
utils.settings('bestTrailer') == 'false'))):
|
||||||
|
# Several streams/files available.
|
||||||
|
dialoglist = []
|
||||||
|
for entry in self.xml.iterfind('./Media'):
|
||||||
|
# Get additional info (filename / languages)
|
||||||
|
if 'file' in entry[0].attrib:
|
||||||
|
option = entry[0].get('file')
|
||||||
|
option = path_ops.basename(option)
|
||||||
|
else:
|
||||||
|
option = self.title() or ''
|
||||||
|
# Languages of audio streams
|
||||||
|
languages = []
|
||||||
|
for stream in entry[0]:
|
||||||
|
if (cast(int, stream.get('streamType')) == 1 and
|
||||||
|
'language' in stream.attrib):
|
||||||
|
language = stream.get('language')
|
||||||
|
languages.append(language)
|
||||||
|
languages = ', '.join(languages)
|
||||||
|
if languages:
|
||||||
|
if option:
|
||||||
|
option = '%s (%s): ' % (option, languages)
|
||||||
|
else:
|
||||||
|
option = '%s: ' % languages
|
||||||
|
else:
|
||||||
|
option = '%s ' % option
|
||||||
|
if 'videoResolution' in entry.attrib:
|
||||||
|
res = entry.get('videoResolution')
|
||||||
|
option = '%s%sp ' % (option, res)
|
||||||
|
if 'videoCodec' in entry.attrib:
|
||||||
|
codec = entry.get('videoCodec')
|
||||||
|
option = '%s%s' % (option, codec)
|
||||||
|
option = option.strip() + ' - '
|
||||||
|
if 'audioProfile' in entry.attrib:
|
||||||
|
profile = entry.get('audioProfile')
|
||||||
|
option = '%s%s ' % (option, profile)
|
||||||
|
if 'audioCodec' in entry.attrib:
|
||||||
|
codec = entry.get('audioCodec')
|
||||||
|
option = '%s%s ' % (option, codec)
|
||||||
|
option = cast(str, option.strip())
|
||||||
|
dialoglist.append(option)
|
||||||
|
media = utils.dialog('select', 'Select stream', dialoglist)
|
||||||
|
LOG.info('User chose media stream number: %s', media)
|
||||||
|
if media == -1:
|
||||||
|
LOG.info('User cancelled media stream selection')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
media = 0
|
||||||
|
self.mediastream = media
|
||||||
|
return media
|
||||||
|
|
||||||
|
def transcode_video_path(self, action, quality=None):
|
||||||
|
"""
|
||||||
|
|
||||||
|
To be called on a VIDEO level of PMS xml response!
|
||||||
|
|
||||||
|
Transcode Video support; returns the URL to get a media started
|
||||||
|
|
||||||
|
Input:
|
||||||
|
action 'DirectStream' or 'Transcode'
|
||||||
|
|
||||||
|
quality: {
|
||||||
|
'videoResolution': e.g. '1024x768',
|
||||||
|
'videoQuality': e.g. '60',
|
||||||
|
'maxVideoBitrate': e.g. '2000' (in kbits)
|
||||||
|
}
|
||||||
|
(one or several of these options)
|
||||||
|
Output:
|
||||||
|
final URL to pull in PMS transcoder
|
||||||
|
|
||||||
|
TODO: mediaIndex
|
||||||
|
"""
|
||||||
|
if self.mediastream is None and self.mediastream_number() is None:
|
||||||
|
return
|
||||||
|
quality = {} if quality is None else quality
|
||||||
|
xargs = clientinfo.getXArgsDeviceInfo()
|
||||||
|
# For DirectPlay, path/key of PART is needed
|
||||||
|
# trailers are 'clip' with PMS xmls
|
||||||
|
if action == "DirectStream":
|
||||||
|
path = self.xml[self.mediastream][self.part].get('key')
|
||||||
|
url = app.CONN.server + path
|
||||||
|
# e.g. Trailers already feature an '?'!
|
||||||
|
return utils.extend_url(url, xargs)
|
||||||
|
|
||||||
|
# For Transcoding
|
||||||
|
headers = {
|
||||||
|
'X-Plex-Platform': 'Android',
|
||||||
|
'X-Plex-Platform-Version': '7.0',
|
||||||
|
'X-Plex-Product': 'Plex for Android',
|
||||||
|
'X-Plex-Version': '5.8.0.475'
|
||||||
|
}
|
||||||
|
# Path/key to VIDEO item of xml PMS response is needed, not part
|
||||||
|
path = self.xml.get('key')
|
||||||
|
transcode_path = app.CONN.server + \
|
||||||
|
'/video/:/transcode/universal/start.m3u8'
|
||||||
|
args = {
|
||||||
|
'audioBoost': utils.settings('audioBoost'),
|
||||||
|
'autoAdjustQuality': 0,
|
||||||
|
'directPlay': 0,
|
||||||
|
'directStream': 1,
|
||||||
|
'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls'
|
||||||
|
'session': v.PKC_MACHINE_IDENTIFIER, # TODO: create new unique id
|
||||||
|
'fastSeek': 1,
|
||||||
|
'path': path,
|
||||||
|
'mediaIndex': self.mediastream,
|
||||||
|
'partIndex': self.part,
|
||||||
|
'hasMDE': 1,
|
||||||
|
'location': 'lan',
|
||||||
|
'subtitleSize': utils.settings('subtitleSize')
|
||||||
|
}
|
||||||
|
LOG.debug("Setting transcode quality to: %s", quality)
|
||||||
|
xargs.update(headers)
|
||||||
|
xargs.update(args)
|
||||||
|
xargs.update(quality)
|
||||||
|
return utils.extend_url(transcode_path, xargs)
|
||||||
|
|
||||||
|
def cache_external_subs(self):
|
||||||
|
"""
|
||||||
|
Downloads external subtitles temporarily to Kodi and returns a list
|
||||||
|
of their paths
|
||||||
|
"""
|
||||||
|
externalsubs = []
|
||||||
|
try:
|
||||||
|
mediastreams = self.xml[0][self.part]
|
||||||
|
except (TypeError, KeyError, IndexError):
|
||||||
|
return
|
||||||
|
kodiindex = 0
|
||||||
|
fileindex = 0
|
||||||
|
for stream in mediastreams:
|
||||||
|
# Since plex returns all possible tracks together, have to pull
|
||||||
|
# only external subtitles - only for these a 'key' exists
|
||||||
|
if cast(int, stream.get('streamType')) != 3:
|
||||||
|
# Not a subtitle
|
||||||
|
continue
|
||||||
|
# Only set for additional external subtitles NOT lying beside video
|
||||||
|
key = stream.get('key')
|
||||||
|
# Only set for dedicated subtitle files lying beside video
|
||||||
|
# ext = stream.attrib.get('format')
|
||||||
|
if key:
|
||||||
|
# We do know the language - temporarily download
|
||||||
|
if stream.get('languageCode') is not None:
|
||||||
|
language = stream.get('languageCode')
|
||||||
|
codec = stream.get('codec')
|
||||||
|
path = self.download_external_subtitles(
|
||||||
|
"{server}%s" % key,
|
||||||
|
"subtitle%02d.%s.%s" % (fileindex, language, codec))
|
||||||
|
fileindex += 1
|
||||||
|
# We don't know the language - no need to download
|
||||||
|
else:
|
||||||
|
path = self.attach_plex_token_to_url(
|
||||||
|
"%s%s" % (app.CONN.server, key))
|
||||||
|
externalsubs.append(path)
|
||||||
|
kodiindex += 1
|
||||||
|
LOG.info('Found external subs: %s', externalsubs)
|
||||||
|
return externalsubs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def download_external_subtitles(url, filename):
|
||||||
|
"""
|
||||||
|
One cannot pass the subtitle language for ListItems. Workaround; will
|
||||||
|
download the subtitle at url to the Kodi PKC directory in a temp dir
|
||||||
|
|
||||||
|
Returns the path to the downloaded subtitle or None
|
||||||
|
"""
|
||||||
|
path = path_ops.path.join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename)
|
||||||
|
response = DU().downloadUrl(url, return_response=True)
|
||||||
|
try:
|
||||||
|
response.status_code
|
||||||
|
except AttributeError:
|
||||||
|
LOG.error('Could not temporarily download subtitle %s', url)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
LOG.debug('Writing temp subtitle to %s', path)
|
||||||
|
with open(path_ops.encode_path(path), 'wb') as filer:
|
||||||
|
filer.write(response.content)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def validate_playurl(self, path, typus, force_check=False, folder=False,
|
||||||
|
omit_check=False):
|
||||||
|
"""
|
||||||
|
Returns a valid path for Kodi, e.g. with '\' substituted to '\\' in
|
||||||
|
Unicode. Returns None if this is not possible
|
||||||
|
|
||||||
|
path : Unicode
|
||||||
|
typus : Plex type from PMS xml
|
||||||
|
force_check : Will always try to check validity of path
|
||||||
|
Will also skip confirmation dialog if path not found
|
||||||
|
folder : Set to True if path is a folder
|
||||||
|
omit_check : Will entirely omit validity check if True
|
||||||
|
"""
|
||||||
|
if path is None:
|
||||||
|
return
|
||||||
|
typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus]
|
||||||
|
if app.SYNC.remap_path:
|
||||||
|
path = path.replace(getattr(app.SYNC, 'remapSMB%sOrg' % typus),
|
||||||
|
getattr(app.SYNC, 'remapSMB%sNew' % typus),
|
||||||
|
1)
|
||||||
|
# There might be backslashes left over:
|
||||||
|
path = path.replace('\\', '/')
|
||||||
|
elif app.SYNC.replace_smb_path:
|
||||||
|
if path.startswith('\\\\'):
|
||||||
|
path = 'smb:' + path.replace('\\', '/')
|
||||||
|
if app.SYNC.escape_path:
|
||||||
|
try:
|
||||||
|
protocol, hostname, args = path.split(':', 2)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
args = utils.quote(args)
|
||||||
|
path = '%s:%s:%s' % (protocol, hostname, args)
|
||||||
|
if (app.SYNC.path_verified and not force_check) or omit_check:
|
||||||
|
return path
|
||||||
|
|
||||||
|
# exist() needs a / or \ at the end to work for directories
|
||||||
|
if not folder:
|
||||||
|
# files
|
||||||
|
check = path_ops.exists(path)
|
||||||
|
else:
|
||||||
|
# directories
|
||||||
|
if "\\" in path:
|
||||||
|
if not path.endswith('\\'):
|
||||||
|
# Add the missing backslash
|
||||||
|
check = path_ops.exists(path + "\\")
|
||||||
|
else:
|
||||||
|
check = path_ops.exists(path)
|
||||||
|
else:
|
||||||
|
if not path.endswith('/'):
|
||||||
|
check = path_ops.exists(path + "/")
|
||||||
|
else:
|
||||||
|
check = path_ops.exists(path)
|
||||||
|
if not check:
|
||||||
|
if force_check is False:
|
||||||
|
# Validate the path is correct with user intervention
|
||||||
|
if self.ask_to_validate(path):
|
||||||
|
app.APP.stop_threads(block=False)
|
||||||
|
path = None
|
||||||
|
app.SYNC.path_verified = True
|
||||||
|
else:
|
||||||
|
path = None
|
||||||
|
elif not force_check:
|
||||||
|
# Only set the flag if we were not force-checking the path
|
||||||
|
app.SYNC.path_verified = True
|
||||||
|
return path
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ask_to_validate(url):
|
||||||
|
"""
|
||||||
|
Displays a YESNO dialog box:
|
||||||
|
Kodi can't locate file: <url>. Please verify the path.
|
||||||
|
You may need to verify your network credentials in the
|
||||||
|
add-on settings or use different Plex paths. Stop syncing?
|
||||||
|
|
||||||
|
Returns True if sync should stop, else False
|
||||||
|
"""
|
||||||
|
LOG.warn('Cannot access file: %s', url)
|
||||||
|
# Kodi cannot locate the file #s. Please verify your PKC settings. Stop
|
||||||
|
# syncing?
|
||||||
|
return utils.yesno_dialog(utils.lang(29999), utils.lang(39031) % url)
|
59
resources/lib/plex_api/user.py
Normal file
59
resources/lib/plex_api/user.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
|
from ..utils import cast
|
||||||
|
from .. import timing, variables as v, app
|
||||||
|
|
||||||
|
|
||||||
|
class User(object):
|
||||||
|
def viewcount(self):
|
||||||
|
"""
|
||||||
|
Returns the play count for the item as an int or the int 0 if not found
|
||||||
|
"""
|
||||||
|
return cast(int, self.xml.get('viewCount')) or 0
|
||||||
|
|
||||||
|
def resume_point(self):
|
||||||
|
"""
|
||||||
|
Returns the resume point of time in seconds as float. 0.0 if not found
|
||||||
|
"""
|
||||||
|
resume = cast(float, self.xml.get('viewOffset')) or 0.0
|
||||||
|
return resume * v.PLEX_TO_KODI_TIMEFACTOR
|
||||||
|
|
||||||
|
def resume_point_plex(self):
|
||||||
|
"""
|
||||||
|
Returns the resume point of time in microseconds as float.
|
||||||
|
0.0 if not found
|
||||||
|
"""
|
||||||
|
return cast(float, self.xml.get('viewOffset')) or 0.0
|
||||||
|
|
||||||
|
def userrating(self):
|
||||||
|
"""
|
||||||
|
Returns the userRating [int].
|
||||||
|
If the user chose to replace user ratings with the number of different
|
||||||
|
file versions for a specific video, that number is returned instead
|
||||||
|
(at most 10)
|
||||||
|
|
||||||
|
0 is returned if something goes wrong
|
||||||
|
"""
|
||||||
|
if (app.SYNC.indicate_media_versions is True and
|
||||||
|
self.plex_type in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_EPISODE)):
|
||||||
|
userrating = 0
|
||||||
|
for _ in self.xml.findall('./Media'):
|
||||||
|
userrating += 1
|
||||||
|
# Don't show a value of '1' - which we'll always have for normal
|
||||||
|
# Plex library items
|
||||||
|
return 0 if userrating == 1 else min(userrating, 10)
|
||||||
|
else:
|
||||||
|
return cast(int, self.xml.get('userRating')) or 0
|
||||||
|
|
||||||
|
def lastplayed(self):
|
||||||
|
"""
|
||||||
|
Returns the Kodi timestamp [unicode] for the last point of time, when
|
||||||
|
this item was played.
|
||||||
|
Returns None if this fails - item has never been played
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return timing.plex_date_to_kodi(int(self.xml.get('lastViewedAt')))
|
||||||
|
except TypeError:
|
||||||
|
pass
|
|
@ -88,10 +88,10 @@ class PlexCompanion(backgroundthread.KillableThread):
|
||||||
LOG.error('Could not download Plex metadata for: %s', data)
|
LOG.error('Could not download Plex metadata for: %s', data)
|
||||||
return
|
return
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
if api.plex_type() == v.PLEX_TYPE_ALBUM:
|
if api.plex_type == v.PLEX_TYPE_ALBUM:
|
||||||
LOG.debug('Plex music album detected')
|
LOG.debug('Plex music album detected')
|
||||||
PQ.init_playqueue_from_plex_children(
|
PQ.init_playqueue_from_plex_children(
|
||||||
api.plex_id(),
|
api.plex_id,
|
||||||
transient_token=data.get('token'))
|
transient_token=data.get('token'))
|
||||||
elif data['containerKey'].startswith('/playQueues/'):
|
elif data['containerKey'].startswith('/playQueues/'):
|
||||||
_, container_key, _ = PF.ParseContainerKey(data['containerKey'])
|
_, container_key, _ = PF.ParseContainerKey(data['containerKey'])
|
||||||
|
@ -104,7 +104,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
||||||
icon='{error}')
|
icon='{error}')
|
||||||
return
|
return
|
||||||
playqueue = PQ.get_playqueue_from_type(
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
|
||||||
playqueue.clear()
|
playqueue.clear()
|
||||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||||
playqueue.plex_transient_token = data.get('token')
|
playqueue.plex_transient_token = data.get('token')
|
||||||
|
@ -117,8 +117,8 @@ class PlexCompanion(backgroundthread.KillableThread):
|
||||||
app.CONN.plex_transient_token = data.get('token')
|
app.CONN.plex_transient_token = data.get('token')
|
||||||
if data.get('offset') != '0':
|
if data.get('offset') != '0':
|
||||||
app.PLAYSTATE.resume_playback = True
|
app.PLAYSTATE.resume_playback = True
|
||||||
playback.playback_triage(api.plex_id(),
|
playback.playback_triage(api.plex_id,
|
||||||
api.plex_type(),
|
api.plex_type,
|
||||||
resolve=False)
|
resolve=False)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -153,7 +153,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
||||||
return
|
return
|
||||||
api = API(xml[0])
|
api = API(xml[0])
|
||||||
playqueue = PQ.get_playqueue_from_type(
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type()])
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
|
||||||
update_playqueue_from_PMS(playqueue,
|
update_playqueue_from_PMS(playqueue,
|
||||||
playqueue_id=container_key,
|
playqueue_id=container_key,
|
||||||
repeat=query.get('repeat'),
|
repeat=query.get('repeat'),
|
||||||
|
|
|
@ -1029,40 +1029,3 @@ def GetUserArtworkURL(username):
|
||||||
url = user.thumb
|
url = user.thumb
|
||||||
LOG.debug("Avatar url for user %s is: %s", username, url)
|
LOG.debug("Avatar url for user %s is: %s", username, url)
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
def transcode_image_path(key, AuthToken, path, width, height):
|
|
||||||
"""
|
|
||||||
Transcode Image support
|
|
||||||
|
|
||||||
parameters:
|
|
||||||
key
|
|
||||||
AuthToken
|
|
||||||
path - source path of current XML: path[srcXML]
|
|
||||||
width
|
|
||||||
height
|
|
||||||
result:
|
|
||||||
final path to image file
|
|
||||||
"""
|
|
||||||
# external address - can we get a transcoding request for external images?
|
|
||||||
if key.startswith('http://') or key.startswith('https://'):
|
|
||||||
path = key
|
|
||||||
elif key.startswith('/'): # internal full path.
|
|
||||||
path = 'http://127.0.0.1:32400' + key
|
|
||||||
else: # internal path, add-on
|
|
||||||
path = 'http://127.0.0.1:32400' + path + '/' + key
|
|
||||||
# This is bogus (note the extra path component) but ATV is stupid when it
|
|
||||||
# comes to caching images, it doesn't use querystrings. Fortunately PMS is
|
|
||||||
# lenient...
|
|
||||||
path = path.encode('utf-8')
|
|
||||||
transcode_path = ('/photo/:/transcode/%sx%s/%s'
|
|
||||||
% (width, height, utils.quote_plus(path)))
|
|
||||||
transcode_path = transcode_path.decode('utf-8')
|
|
||||||
args = {
|
|
||||||
'width': width,
|
|
||||||
'height': height,
|
|
||||||
'url': path
|
|
||||||
}
|
|
||||||
if AuthToken:
|
|
||||||
args['X-Plex-Token'] = AuthToken
|
|
||||||
return utils.extend_url(transcode_path, args)
|
|
||||||
|
|
|
@ -134,7 +134,8 @@ def convert_pkc_to_listitem(pkc_listitem):
|
||||||
data = pkc_listitem.data
|
data = pkc_listitem.data
|
||||||
listitem = xbmcgui.ListItem(label=data.get('label'),
|
listitem = xbmcgui.ListItem(label=data.get('label'),
|
||||||
label2=data.get('label2'),
|
label2=data.get('label2'),
|
||||||
path=data.get('path'))
|
path=data.get('path'),
|
||||||
|
offscreen=True)
|
||||||
if data['info']:
|
if data['info']:
|
||||||
listitem.setInfo(**data['info'])
|
listitem.setInfo(**data['info'])
|
||||||
for stream in data['stream_info']:
|
for stream in data['stream_info']:
|
||||||
|
@ -147,6 +148,8 @@ def convert_pkc_to_listitem(pkc_listitem):
|
||||||
listitem.setProperty(key, cast(str, value))
|
listitem.setProperty(key, cast(str, value))
|
||||||
if data['subtitles']:
|
if data['subtitles']:
|
||||||
listitem.setSubtitles(data['subtitles'])
|
listitem.setSubtitles(data['subtitles'])
|
||||||
|
if data['contextmenu']:
|
||||||
|
listitem.addContextMenuItems(data['contextmenu'])
|
||||||
return listitem
|
return listitem
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,7 +160,7 @@ class PKCListItem(object):
|
||||||
|
|
||||||
WARNING: set/get path only via setPath and getPath! (not getProperty)
|
WARNING: set/get path only via setPath and getPath! (not getProperty)
|
||||||
"""
|
"""
|
||||||
def __init__(self, label=None, label2=None, path=None):
|
def __init__(self, label=None, label2=None, path=None, offscreen=True):
|
||||||
self.data = {
|
self.data = {
|
||||||
'stream_info': [], # (type, values: dict { label: value })
|
'stream_info': [], # (type, values: dict { label: value })
|
||||||
'art': {}, # dict
|
'art': {}, # dict
|
||||||
|
@ -167,9 +170,10 @@ class PKCListItem(object):
|
||||||
'path': path, # string
|
'path': path, # string
|
||||||
'property': {}, # (key, value)
|
'property': {}, # (key, value)
|
||||||
'subtitles': [], # strings
|
'subtitles': [], # strings
|
||||||
|
'contextmenu': None
|
||||||
}
|
}
|
||||||
|
|
||||||
def addContextMenuItems(self, items, replaceItems):
|
def addContextMenuItems(self, items):
|
||||||
"""
|
"""
|
||||||
Adds item(s) to the context menu for media lists.
|
Adds item(s) to the context menu for media lists.
|
||||||
|
|
||||||
|
@ -187,7 +191,7 @@ class PKCListItem(object):
|
||||||
|
|
||||||
Once you use a keyword, all following arguments require the keyword.
|
Once you use a keyword, all following arguments require the keyword.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
self.data['contextmenu'] = items
|
||||||
|
|
||||||
def addStreamInfo(self, type, values):
|
def addStreamInfo(self, type, values):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -20,6 +20,12 @@ from functools import wraps
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
import gc
|
import gc
|
||||||
|
try:
|
||||||
|
from multiprocessing.pool import ThreadPool
|
||||||
|
SUPPORTS_POOL = True
|
||||||
|
except Exception:
|
||||||
|
SUPPORTS_POOL = False
|
||||||
|
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcaddon
|
import xbmcaddon
|
||||||
|
@ -286,7 +292,16 @@ def cast(func, value):
|
||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
return value.encode('utf-8')
|
return value.encode('utf-8')
|
||||||
elif func in (int, float):
|
elif func == int:
|
||||||
|
try:
|
||||||
|
return func(value)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
# Converting e.g. '8.0' fails; need to convert to float first
|
||||||
|
return int(float(value))
|
||||||
|
except ValueError:
|
||||||
|
return float('nan')
|
||||||
|
elif func == float:
|
||||||
try:
|
try:
|
||||||
return func(value)
|
return func(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -930,6 +945,27 @@ def generate_file_md5(path):
|
||||||
return m.hexdigest().decode('utf-8')
|
return m.hexdigest().decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def process_method_on_list(method_to_run, items):
|
||||||
|
"""
|
||||||
|
helper method that processes a method on each item with pooling if the
|
||||||
|
system supports it
|
||||||
|
"""
|
||||||
|
all_items = []
|
||||||
|
if SUPPORTS_POOL:
|
||||||
|
pool = ThreadPool()
|
||||||
|
try:
|
||||||
|
all_items = pool.map(method_to_run, items)
|
||||||
|
except Exception:
|
||||||
|
# catch exception to prevent threadpool running forever
|
||||||
|
ERROR(notify=True)
|
||||||
|
pool.close()
|
||||||
|
pool.join()
|
||||||
|
else:
|
||||||
|
all_items = [method_to_run(item) for item in items]
|
||||||
|
all_items = filter(None, all_items)
|
||||||
|
return all_items
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# WRAPPERS
|
# WRAPPERS
|
||||||
|
|
||||||
|
|
|
@ -8,18 +8,11 @@ e.g. plugin://... calls. Hence be careful to only rely on window variables.
|
||||||
"""
|
"""
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
try:
|
|
||||||
from multiprocessing.pool import ThreadPool
|
|
||||||
SUPPORTS_POOL = True
|
|
||||||
except Exception:
|
|
||||||
SUPPORTS_POOL = False
|
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
import xbmcvfs
|
import xbmcvfs
|
||||||
|
|
||||||
from .plex_api import API
|
|
||||||
from .plex_db import PlexDB
|
|
||||||
from . import json_rpc as js, utils, variables as v
|
from . import json_rpc as js, utils, variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.widget')
|
LOG = getLogger('PLEX.widget')
|
||||||
|
@ -34,27 +27,6 @@ SYNCHED = True
|
||||||
KEY = None
|
KEY = None
|
||||||
|
|
||||||
|
|
||||||
def process_method_on_list(method_to_run, items):
|
|
||||||
"""
|
|
||||||
helper method that processes a method on each listitem with pooling if the
|
|
||||||
system supports it
|
|
||||||
"""
|
|
||||||
all_items = []
|
|
||||||
if SUPPORTS_POOL:
|
|
||||||
pool = ThreadPool()
|
|
||||||
try:
|
|
||||||
all_items = pool.map(method_to_run, items)
|
|
||||||
except Exception:
|
|
||||||
# catch exception to prevent threadpool running forever
|
|
||||||
utils.ERROR(notify=True)
|
|
||||||
pool.close()
|
|
||||||
pool.join()
|
|
||||||
else:
|
|
||||||
all_items = [method_to_run(item) for item in items]
|
|
||||||
all_items = filter(None, all_items)
|
|
||||||
return all_items
|
|
||||||
|
|
||||||
|
|
||||||
def get_clean_image(image):
|
def get_clean_image(image):
|
||||||
'''
|
'''
|
||||||
helper to strip all kodi tags/formatting of an image path/url
|
helper to strip all kodi tags/formatting of an image path/url
|
||||||
|
@ -82,7 +54,7 @@ def get_clean_image(image):
|
||||||
return image.decode('utf-8')
|
return image.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
def generate_item(xml_element):
|
def generate_item(api):
|
||||||
"""
|
"""
|
||||||
Meant to be consumed by metadatautils.kodidb.prepare_listitem(), and then
|
Meant to be consumed by metadatautils.kodidb.prepare_listitem(), and then
|
||||||
subsequently by metadatautils.kodidb.create_listitem()
|
subsequently by metadatautils.kodidb.create_listitem()
|
||||||
|
@ -94,20 +66,19 @@ def generate_item(xml_element):
|
||||||
The key 'file' needs to be set later with the item's path
|
The key 'file' needs to be set later with the item's path
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if xml_element.tag in ('Directory', 'Playlist', 'Hub'):
|
if api.tag in ('Directory', 'Playlist', 'Hub'):
|
||||||
return _generate_folder(xml_element)
|
return _generate_folder(api)
|
||||||
else:
|
else:
|
||||||
return _generate_content(xml_element)
|
return _generate_content(api)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Usefull to catch everything here since we're using threadpool
|
# Usefull to catch everything here since we're using threadpool
|
||||||
LOG.error('xml that caused the crash: "%s": %s',
|
LOG.error('xml that caused the crash: "%s": %s',
|
||||||
xml_element.tag, xml_element.attrib)
|
api.tag, api.attrib)
|
||||||
utils.ERROR(notify=True)
|
utils.ERROR(notify=True)
|
||||||
|
|
||||||
|
|
||||||
def _generate_folder(xml_element):
|
def _generate_folder(api):
|
||||||
'''Generates "folder"/"directory" items that user can further navigate'''
|
'''Generates "folder"/"directory" items that user can further navigate'''
|
||||||
api = API(xml_element)
|
|
||||||
art = api.artwork()
|
art = api.artwork()
|
||||||
return {
|
return {
|
||||||
'title': api.title(),
|
'title': api.title(),
|
||||||
|
@ -128,60 +99,54 @@ def _generate_folder(xml_element):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _generate_content(xml_element):
|
def _generate_content(api):
|
||||||
api = API(xml_element)
|
plex_type = api.plex_type
|
||||||
plex_type = api.plex_type()
|
if api.kodi_id:
|
||||||
kodi_type = v.KODITYPE_FROM_PLEXTYPE[plex_type]
|
|
||||||
userdata = api.userdata()
|
|
||||||
_, _, tvshowtitle, season_no, episode_no = api.episode_data()
|
|
||||||
db_item = xml_element.get('pkc_db_item')
|
|
||||||
if db_item:
|
|
||||||
# Item is synched to the Kodi db - let's use that info
|
# Item is synched to the Kodi db - let's use that info
|
||||||
# (will thus e.g. include additional artwork or metadata)
|
# (will thus e.g. include additional artwork or metadata)
|
||||||
item = js.item_details(db_item['kodi_id'], kodi_type)
|
item = js.item_details(api.kodi_id, api.kodi_type)
|
||||||
else:
|
else:
|
||||||
people = api.people()
|
|
||||||
cast = [{
|
cast = [{
|
||||||
'name': x[0],
|
'name': x[0],
|
||||||
'thumbnail': x[1],
|
'thumbnail': x[1],
|
||||||
'role': x[2],
|
'role': x[2],
|
||||||
'order': x[3],
|
'order': x[3],
|
||||||
} for x in api.people_list()['actor']]
|
} for x in api.people()['actor']]
|
||||||
item = {
|
item = {
|
||||||
'cast': cast,
|
'cast': cast,
|
||||||
'country': api.country_list(),
|
'country': api.countries(),
|
||||||
'dateadded': api.date_created(), # e.g '2019-01-03 19:40:59'
|
'dateadded': api.date_created(), # e.g '2019-01-03 19:40:59'
|
||||||
'director': people['Director'], # list of [str]
|
'director': api.directors(), # list of [str]
|
||||||
'duration': userdata['Runtime'],
|
'duration': api.runtime(),
|
||||||
'episode': episode_no,
|
'episode': api.index(),
|
||||||
# 'file': '', # e.g. 'videodb://tvshows/titles/20'
|
# 'file': '', # e.g. 'videodb://tvshows/titles/20'
|
||||||
'genre': api.genre_list(),
|
'genre': api.genres(),
|
||||||
# 'imdbnumber': '', # e.g.'341663'
|
# 'imdbnumber': '', # e.g.'341663'
|
||||||
'label': api.title(), # e.g. '1x05. Category 55 Emergency Doomsday Crisis'
|
'label': api.title(), # e.g. '1x05. Category 55 Emergency Doomsday Crisis'
|
||||||
'lastplayed': userdata['LastPlayedDate'], # e.g. '2019-01-04 16:05:03'
|
'lastplayed': api.lastplayed(), # e.g. '2019-01-04 16:05:03'
|
||||||
'mpaa': api.content_rating(), # e.g. 'TV-MA'
|
'mpaa': api.content_rating(), # e.g. 'TV-MA'
|
||||||
'originaltitle': '', # e.g. 'Titans (2018)'
|
'originaltitle': '', # e.g. 'Titans (2018)'
|
||||||
'playcount': userdata['PlayCount'], # [int]
|
'playcount': api.viewcount(), # [int]
|
||||||
'plot': api.plot(), # [str]
|
'plot': api.plot(), # [str]
|
||||||
'plotoutline': api.tagline(),
|
'plotoutline': api.tagline(),
|
||||||
'premiered': api.premiere_date(), # '2018-10-12'
|
'premiered': api.premiere_date(), # '2018-10-12'
|
||||||
'rating': api.audience_rating(), # [float]
|
'rating': api.rating(), # [float]
|
||||||
'season': season_no,
|
'season': api.season_number(),
|
||||||
'sorttitle': api.sorttitle(), # 'Titans (2018)'
|
'sorttitle': api.sorttitle(), # 'Titans (2018)'
|
||||||
'studio': api.music_studio_list(), # e.g. 'DC Universe'
|
'studio': api.studios(),
|
||||||
'tag': [], # List of tags this item belongs to
|
'tag': [], # List of tags this item belongs to
|
||||||
'tagline': api.tagline(),
|
'tagline': api.tagline(),
|
||||||
'thumbnail': '', # e.g. 'image://https%3a%2f%2fassets.tv'
|
'thumbnail': '', # e.g. 'image://https%3a%2f%2fassets.tv'
|
||||||
'title': api.title(), # 'Titans (2018)'
|
'title': api.title(), # 'Titans (2018)'
|
||||||
'type': kodi_type,
|
'type': api.kodi_type,
|
||||||
'trailer': api.trailer(),
|
'trailer': api.trailer(),
|
||||||
'tvshowtitle': tvshowtitle,
|
'tvshowtitle': api.show_title(),
|
||||||
'uniqueid': {
|
'uniqueid': {
|
||||||
'imdbnumber': api.provider('imdb') or '',
|
'imdbnumber': api.provider('imdb') or '',
|
||||||
'tvdb_id': api.provider('tvdb') or ''
|
'tvdb_id': api.provider('tvdb') or ''
|
||||||
},
|
},
|
||||||
'votes': '0', # [str]!
|
'votes': '0', # [str]!
|
||||||
'writer': people['Writer'], # list of [str]
|
'writer': api.writers(), # list of [str]
|
||||||
'year': api.year(), # [int]
|
'year': api.year(), # [int]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,18 +171,18 @@ def _generate_content(xml_element):
|
||||||
if resume:
|
if resume:
|
||||||
item['resume'] = {
|
item['resume'] = {
|
||||||
'position': resume,
|
'position': resume,
|
||||||
'total': userdata['Runtime']
|
'total': api.runtime()
|
||||||
}
|
}
|
||||||
|
|
||||||
item['icon'] = v.ICON_FROM_PLEXTYPE[plex_type]
|
item['icon'] = v.ICON_FROM_PLEXTYPE[plex_type]
|
||||||
# Some customization
|
# Some customization
|
||||||
if plex_type == v.PLEX_TYPE_EPISODE:
|
if plex_type == v.PLEX_TYPE_EPISODE:
|
||||||
# Prefix to the episode's title/label
|
# Prefix to the episode's title/label
|
||||||
if season_no is not None and episode_no is not None:
|
if api.season_number() is not None and api.index() is not None:
|
||||||
if APPEND_SXXEXX is True:
|
if APPEND_SXXEXX is True:
|
||||||
item['title'] = "S%.2dE%.2d - %s" % (season_no, episode_no, item['title'])
|
item['title'] = "S%.2dE%.2d - %s" % (api.season_number(), api.index(), item['title'])
|
||||||
if APPEND_SHOW_TITLE is True:
|
if APPEND_SHOW_TITLE is True:
|
||||||
item['title'] = "%s - %s " % (tvshowtitle, item['title'])
|
item['title'] = "%s - %s " % (api.show_title(), item['title'])
|
||||||
item['label'] = item['title']
|
item['label'] = item['title']
|
||||||
|
|
||||||
# Determine the path for this item
|
# Determine the path for this item
|
||||||
|
@ -226,14 +191,14 @@ def _generate_content(xml_element):
|
||||||
params = {
|
params = {
|
||||||
'mode': 'plex_node',
|
'mode': 'plex_node',
|
||||||
'key': key,
|
'key': key,
|
||||||
'offset': xml_element.attrib.get('viewOffset', '0'),
|
'offset': api.resume_point_plex()
|
||||||
}
|
}
|
||||||
url = utils.extend_url('plugin://%s' % v.ADDON_ID, params)
|
url = utils.extend_url('plugin://%s' % v.ADDON_ID, params)
|
||||||
elif plex_type == v.PLEX_TYPE_PHOTO:
|
elif plex_type == v.PLEX_TYPE_PHOTO:
|
||||||
url = api.get_picture_path()
|
url = api.get_picture_path()
|
||||||
else:
|
else:
|
||||||
url = api.path()
|
url = api.path()
|
||||||
if not db_item and plex_type == v.PLEX_TYPE_EPISODE:
|
if not api.kodi_id and plex_type == v.PLEX_TYPE_EPISODE:
|
||||||
# Hack - Item is not synched to the Kodi database
|
# Hack - Item is not synched to the Kodi database
|
||||||
# We CANNOT use paths that show up in the Kodi paths table!
|
# We CANNOT use paths that show up in the Kodi paths table!
|
||||||
url = url.replace('plugin.video.plexkodiconnect.tvshows',
|
url = url.replace('plugin.video.plexkodiconnect.tvshows',
|
||||||
|
@ -242,20 +207,6 @@ def _generate_content(xml_element):
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def attach_kodi_ids(xml):
|
|
||||||
"""
|
|
||||||
Attaches the kodi db_item to the xml's children, attribute 'pkc_db_item'
|
|
||||||
"""
|
|
||||||
if not SYNCHED:
|
|
||||||
return
|
|
||||||
with PlexDB(lock=False) as plexdb:
|
|
||||||
for child in xml:
|
|
||||||
api = API(child)
|
|
||||||
db_item = plexdb.item_by_id(api.plex_id(), api.plex_type())
|
|
||||||
child.set('pkc_db_item', db_item)
|
|
||||||
return xml
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_listitem(item):
|
def prepare_listitem(item):
|
||||||
"""
|
"""
|
||||||
helper to convert kodi output from json api to compatible format for
|
helper to convert kodi output from json api to compatible format for
|
||||||
|
@ -460,7 +411,8 @@ def prepare_listitem(item):
|
||||||
LOG.error('item that caused crash: %s', item)
|
LOG.error('item that caused crash: %s', item)
|
||||||
|
|
||||||
|
|
||||||
def create_listitem(item, as_tuple=True, offscreen=True):
|
def create_listitem(item, as_tuple=True, offscreen=True,
|
||||||
|
listitem=xbmcgui.ListItem):
|
||||||
"""
|
"""
|
||||||
helper to create a kodi listitem from kodi compatible dict with mediainfo
|
helper to create a kodi listitem from kodi compatible dict with mediainfo
|
||||||
|
|
||||||
|
@ -472,13 +424,13 @@ def create_listitem(item, as_tuple=True, offscreen=True):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if v.KODIVERSION > 17:
|
if v.KODIVERSION > 17:
|
||||||
liz = xbmcgui.ListItem(
|
liz = listitem(
|
||||||
label=item.get("label", ""),
|
label=item.get("label", ""),
|
||||||
label2=item.get("label2", ""),
|
label2=item.get("label2", ""),
|
||||||
path=item['file'],
|
path=item['file'],
|
||||||
offscreen=offscreen)
|
offscreen=offscreen)
|
||||||
else:
|
else:
|
||||||
liz = xbmcgui.ListItem(
|
liz = listitem(
|
||||||
label=item.get("label", ""),
|
label=item.get("label", ""),
|
||||||
label2=item.get("label2", ""),
|
label2=item.get("label2", ""),
|
||||||
path=item['file'])
|
path=item['file'])
|
||||||
|
@ -585,11 +537,9 @@ def create_listitem(item, as_tuple=True, offscreen=True):
|
||||||
liz.setInfo(type=nodetype, infoLabels=infolabels)
|
liz.setInfo(type=nodetype, infoLabels=infolabels)
|
||||||
|
|
||||||
# artwork
|
# artwork
|
||||||
liz.setArt(item.get("art", {}))
|
|
||||||
if "icon" in item:
|
if "icon" in item:
|
||||||
liz.setIconImage(item['icon'])
|
item['art']['icon'] = item['icon']
|
||||||
if "thumbnail" in item:
|
liz.setArt(item.get("art", {}))
|
||||||
liz.setThumbnailImage(item['thumbnail'])
|
|
||||||
|
|
||||||
# contextmenu
|
# contextmenu
|
||||||
if item["type"] in ["episode", "season"] and "season" in item and "tvshowid" in item:
|
if item["type"] in ["episode", "season"] and "season" in item and "tvshowid" in item:
|
||||||
|
@ -627,4 +577,3 @@ def create_main_entry(item):
|
||||||
'type': '',
|
'type': '',
|
||||||
'IsPlayable': 'false'
|
'IsPlayable': 'false'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue