commit
c5a720aea9
27 changed files with 2015 additions and 2188 deletions
|
@ -15,7 +15,7 @@ from xbmcgui import ListItem
|
|||
from . import utils
|
||||
from . import path_ops
|
||||
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 variables as v
|
||||
# Be careful - your using app in another Python instance!
|
||||
|
@ -217,13 +217,13 @@ def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None,
|
|||
# Need to chain keys for navigation
|
||||
widgets.KEY = key
|
||||
# Process all items to show
|
||||
widgets.attach_kodi_ids(xml)
|
||||
all_items = widgets.process_method_on_list(widgets.generate_item, xml)
|
||||
all_items = widgets.process_method_on_list(widgets.prepare_listitem,
|
||||
all_items)
|
||||
all_items = mass_api(xml)
|
||||
all_items = utils.process_method_on_list(widgets.generate_item, all_items)
|
||||
all_items = utils.process_method_on_list(widgets.prepare_listitem,
|
||||
all_items)
|
||||
# fill that listing...
|
||||
all_items = widgets.process_method_on_list(widgets.create_listitem,
|
||||
all_items)
|
||||
all_items = utils.process_method_on_list(widgets.create_listitem,
|
||||
all_items)
|
||||
xbmcplugin.addDirectoryItems(int(sys.argv[1]), all_items, len(all_items))
|
||||
# end directory listing
|
||||
xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_UNSORTED)
|
||||
|
@ -397,13 +397,13 @@ def hub(content_type):
|
|||
for entry in reversed(xml):
|
||||
api = API(entry)
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
elif content_type is None:
|
||||
# Needed for widgets, where no content_type is provided
|
||||
|
|
|
@ -20,7 +20,7 @@ class Movie(ItemBase):
|
|||
Process single movie
|
||||
"""
|
||||
api = API(xml)
|
||||
plex_id = api.plex_id()
|
||||
plex_id = api.plex_id
|
||||
# Cannot parse XML, abort
|
||||
if not plex_id:
|
||||
LOG.error('Cannot parse XML data for movie: %s', xml.attrib)
|
||||
|
@ -35,20 +35,6 @@ class Movie(ItemBase):
|
|||
update_item = False
|
||||
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 #####
|
||||
do_indirect = not 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
|
||||
do_indirect = True
|
||||
else:
|
||||
playurl = api.validate_playurl(playurl, api.plex_type())
|
||||
playurl = api.validate_playurl(playurl, api.plex_type)
|
||||
if playurl is None:
|
||||
return False
|
||||
if '\\' in playurl:
|
||||
|
@ -92,7 +78,7 @@ class Movie(ItemBase):
|
|||
self.kodidb.update_ratings(kodi_id,
|
||||
v.KODI_TYPE_MOVIE,
|
||||
"default",
|
||||
rating,
|
||||
api.rating(),
|
||||
api.votecount(),
|
||||
rating_id)
|
||||
# update new uniqueid Kodi 17
|
||||
|
@ -109,13 +95,13 @@ class Movie(ItemBase):
|
|||
uniqueid = -1
|
||||
self.kodidb.modify_people(kodi_id,
|
||||
v.KODI_TYPE_MOVIE,
|
||||
api.people_list())
|
||||
api.people())
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.modify_artwork(api.artwork(),
|
||||
kodi_id,
|
||||
v.KODI_TYPE_MOVIE)
|
||||
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,
|
||||
kodi_pathid,
|
||||
api.date_created())
|
||||
|
@ -124,7 +110,7 @@ class Movie(ItemBase):
|
|||
kodi_id,
|
||||
v.KODI_TYPE_MOVIE,
|
||||
"default",
|
||||
rating,
|
||||
api.rating(),
|
||||
api.votecount())
|
||||
if api.provider('imdb') is not None:
|
||||
uniqueid = self.kodidb.add_uniqueid_id()
|
||||
|
@ -137,7 +123,7 @@ class Movie(ItemBase):
|
|||
uniqueid = -1
|
||||
self.kodidb.add_people(kodi_id,
|
||||
v.KODI_TYPE_MOVIE,
|
||||
api.people_list())
|
||||
api.people())
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.add_artwork(api.artwork(),
|
||||
kodi_id,
|
||||
|
@ -146,37 +132,39 @@ class Movie(ItemBase):
|
|||
# Update Kodi's main entry
|
||||
self.kodidb.add_movie(kodi_id,
|
||||
file_id,
|
||||
title,
|
||||
api.title(),
|
||||
api.plot(),
|
||||
api.shortplot(),
|
||||
api.tagline(),
|
||||
api.votecount(),
|
||||
rating_id,
|
||||
api.list_to_string(people['Writer']),
|
||||
api.list_to_string(api.writers()),
|
||||
api.year(),
|
||||
uniqueid,
|
||||
api.sorttitle(),
|
||||
runtime,
|
||||
api.runtime(),
|
||||
api.content_rating(),
|
||||
api.list_to_string(genres),
|
||||
api.list_to_string(people['Director']),
|
||||
title,
|
||||
api.list_to_string(studios),
|
||||
api.list_to_string(api.genres()),
|
||||
api.list_to_string(api.directors()),
|
||||
api.title(),
|
||||
api.list_to_string(api.studios()),
|
||||
api.trailer(),
|
||||
api.list_to_string(countries),
|
||||
api.list_to_string(api.countries()),
|
||||
playurl,
|
||||
kodi_pathid,
|
||||
api.premiere_date(),
|
||||
userdata['UserRating'])
|
||||
api.userrating())
|
||||
|
||||
self.kodidb.modify_countries(kodi_id, v.KODI_TYPE_MOVIE, countries)
|
||||
self.kodidb.modify_genres(kodi_id, v.KODI_TYPE_MOVIE, genres)
|
||||
self.kodidb.modify_countries(kodi_id,
|
||||
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_studios(kodi_id, v.KODI_TYPE_MOVIE, studios)
|
||||
self.kodidb.modify_streams(file_id, api.mediastreams(), api.runtime())
|
||||
self.kodidb.modify_studios(kodi_id, v.KODI_TYPE_MOVIE, api.studios())
|
||||
tags = [section_name]
|
||||
if collections:
|
||||
for plex_set_id, set_name in collections:
|
||||
if api.collections():
|
||||
for plex_set_id, set_name in api.collections():
|
||||
set_api = None
|
||||
tags.append(set_name)
|
||||
# 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)
|
||||
# Process playstate
|
||||
self.kodidb.set_resume(file_id,
|
||||
resume,
|
||||
runtime,
|
||||
playcount,
|
||||
dateplayed)
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
self.plexdb.add_movie(plex_id=plex_id,
|
||||
checksum=api.checksum(),
|
||||
section_id=section_id,
|
||||
|
@ -267,19 +255,17 @@ class Movie(ItemBase):
|
|||
"""
|
||||
api = API(xml_element)
|
||||
# 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:
|
||||
LOG.info('Item not yet synced: %s', xml_element.attrib)
|
||||
return False
|
||||
# Grab the user's viewcount, resume points etc. from PMS' answer
|
||||
userdata = api.userdata()
|
||||
# Write to Kodi DB
|
||||
self.kodidb.set_resume(db_item['kodi_fileid'],
|
||||
userdata['Resume'],
|
||||
userdata['Runtime'],
|
||||
userdata['PlayCount'],
|
||||
userdata['LastPlayedDate'])
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
self.kodidb.update_userrating(db_item['kodi_id'],
|
||||
db_item['kodi_type'],
|
||||
userdata['UserRating'])
|
||||
api.userrating())
|
||||
return True
|
||||
|
|
|
@ -42,18 +42,17 @@ class MusicMixin(object):
|
|||
"""
|
||||
api = API(xml_element)
|
||||
# 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:
|
||||
LOG.info('Item not yet synced: %s', xml_element.attrib)
|
||||
return False
|
||||
# Grab the user's viewcount, resume points etc. from PMS' answer
|
||||
userdata = api.userdata()
|
||||
self.kodidb.update_userrating(db_item['kodi_id'],
|
||||
db_item['kodi_type'],
|
||||
userdata['UserRating'])
|
||||
api.userrating())
|
||||
if plex_type == v.PLEX_TYPE_SONG:
|
||||
self.kodidb.set_playcount(userdata['PlayCount'],
|
||||
userdata['LastPlayedDate'],
|
||||
self.kodidb.set_playcount(api.viewcount(),
|
||||
api.lastplayed(),
|
||||
db_item['kodi_id'],)
|
||||
return True
|
||||
|
||||
|
@ -160,7 +159,7 @@ class Artist(MusicMixin, ItemBase):
|
|||
Process a single artist
|
||||
"""
|
||||
api = API(xml)
|
||||
plex_id = api.plex_id()
|
||||
plex_id = api.plex_id
|
||||
if not plex_id:
|
||||
LOG.error('Cannot process artist %s', xml.attrib)
|
||||
return
|
||||
|
@ -198,7 +197,7 @@ class Artist(MusicMixin, ItemBase):
|
|||
# Kodi doesn't allow that. In case that happens we just merge the
|
||||
# artist entries.
|
||||
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(),
|
||||
thumb,
|
||||
fanart,
|
||||
|
@ -224,7 +223,7 @@ class Album(MusicMixin, ItemBase):
|
|||
avoid infinite loops
|
||||
"""
|
||||
api = API(xml)
|
||||
plex_id = api.plex_id()
|
||||
plex_id = api.plex_id
|
||||
if not plex_id:
|
||||
LOG.error('Error processing album: %s', xml.attrib)
|
||||
return
|
||||
|
@ -274,11 +273,9 @@ class Album(MusicMixin, ItemBase):
|
|||
compilation = 1
|
||||
break
|
||||
name = api.title()
|
||||
userdata = api.userdata()
|
||||
# Not yet implemented by Plex, let's use unique last.fm or gracenote
|
||||
musicBrainzId = None
|
||||
genres = api.genre_list()
|
||||
genre = api.list_to_string(genres)
|
||||
genre = api.list_to_string(api.genres())
|
||||
if app.SYNC.artwork:
|
||||
artworks = api.artwork()
|
||||
if 'poster' in artworks:
|
||||
|
@ -300,8 +297,8 @@ class Album(MusicMixin, ItemBase):
|
|||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.music_studio(),
|
||||
userdata['UserRating'],
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album',
|
||||
kodi_id)
|
||||
|
@ -314,8 +311,8 @@ class Album(MusicMixin, ItemBase):
|
|||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.music_studio(),
|
||||
userdata['UserRating'],
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album',
|
||||
kodi_id)
|
||||
|
@ -333,8 +330,8 @@ class Album(MusicMixin, ItemBase):
|
|||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.music_studio(),
|
||||
userdata['UserRating'],
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album')
|
||||
else:
|
||||
|
@ -347,15 +344,15 @@ class Album(MusicMixin, ItemBase):
|
|||
compilation,
|
||||
api.plot(),
|
||||
thumb,
|
||||
api.music_studio(),
|
||||
userdata['UserRating'],
|
||||
api.list_to_string(api.studios()),
|
||||
api.userrating(),
|
||||
timing.unix_date_to_kodi(self.last_sync),
|
||||
'album')
|
||||
self.kodidb.add_albumartist(artist_id, kodi_id, api.artist_name())
|
||||
if v.KODIVERSION < 18:
|
||||
self.kodidb.add_discography(artist_id, name, api.year())
|
||||
self.kodidb.add_music_genres(kodi_id,
|
||||
genres,
|
||||
api.genres(),
|
||||
v.KODI_TYPE_ALBUM)
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.modify_artwork(artworks,
|
||||
|
@ -378,7 +375,7 @@ class Album(MusicMixin, ItemBase):
|
|||
section_name=section_name,
|
||||
section_id=section_id,
|
||||
album_xml=xml,
|
||||
genres=genres,
|
||||
genres=api.genres(),
|
||||
genre=genre,
|
||||
compilation=compilation)
|
||||
|
||||
|
@ -391,7 +388,7 @@ class Song(MusicMixin, ItemBase):
|
|||
Process single song/track
|
||||
"""
|
||||
api = API(xml)
|
||||
plex_id = api.plex_id()
|
||||
plex_id = api.plex_id
|
||||
if not plex_id:
|
||||
LOG.error('Error processing song: %s', xml.attrib)
|
||||
return
|
||||
|
@ -492,11 +489,6 @@ class Song(MusicMixin, ItemBase):
|
|||
# Not yet implemented by Plex
|
||||
musicBrainzId = 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
|
||||
if compilation is not None:
|
||||
if compilation == 0:
|
||||
|
@ -506,7 +498,7 @@ class Song(MusicMixin, ItemBase):
|
|||
else:
|
||||
# compilation not set
|
||||
artists = xml.get('originalTitle', api.grandparent_title())
|
||||
tracknumber = api.track_number() or 0
|
||||
tracknumber = api.index() or 0
|
||||
disc = api.disc_number() or 1
|
||||
if disc == 1:
|
||||
track = tracknumber
|
||||
|
@ -532,7 +524,7 @@ class Song(MusicMixin, ItemBase):
|
|||
# Something went wrong, trying to use non-direct paths
|
||||
do_indirect = True
|
||||
else:
|
||||
playurl = api.validate_playurl(playurl, api.plex_type())
|
||||
playurl = api.validate_playurl(playurl, api.plex_type)
|
||||
if playurl is None:
|
||||
return False
|
||||
if "\\" in playurl:
|
||||
|
@ -562,12 +554,12 @@ class Song(MusicMixin, ItemBase):
|
|||
genre,
|
||||
title,
|
||||
track,
|
||||
userdata['Runtime'],
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
playcount,
|
||||
userdata['LastPlayedDate'],
|
||||
userdata['UserRating'],
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
comment,
|
||||
mood,
|
||||
api.date_created(),
|
||||
|
@ -578,12 +570,12 @@ class Song(MusicMixin, ItemBase):
|
|||
genre,
|
||||
title,
|
||||
track,
|
||||
userdata['Runtime'],
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
playcount,
|
||||
userdata['LastPlayedDate'],
|
||||
userdata['UserRating'],
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
comment,
|
||||
mood,
|
||||
api.date_created(),
|
||||
|
@ -603,13 +595,13 @@ class Song(MusicMixin, ItemBase):
|
|||
genre,
|
||||
title,
|
||||
track,
|
||||
userdata['Runtime'],
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
musicBrainzId,
|
||||
playcount,
|
||||
userdata['LastPlayedDate'],
|
||||
userdata['UserRating'],
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
0,
|
||||
0,
|
||||
mood,
|
||||
|
@ -622,13 +614,13 @@ class Song(MusicMixin, ItemBase):
|
|||
genre,
|
||||
title,
|
||||
track,
|
||||
userdata['Runtime'],
|
||||
api.runtime(),
|
||||
year,
|
||||
filename,
|
||||
musicBrainzId,
|
||||
playcount,
|
||||
userdata['LastPlayedDate'],
|
||||
userdata['UserRating'],
|
||||
api.viewcount(),
|
||||
api.lastplayed(),
|
||||
api.userrating(),
|
||||
0,
|
||||
0,
|
||||
mood,
|
||||
|
@ -639,7 +631,7 @@ class Song(MusicMixin, ItemBase):
|
|||
parent_id,
|
||||
track,
|
||||
title,
|
||||
userdata['Runtime'])
|
||||
api.runtime())
|
||||
# Link song to artists
|
||||
artist_name = api.grandparent_title()
|
||||
# Do the actual linking
|
||||
|
|
|
@ -18,27 +18,26 @@ class TvShowMixin(object):
|
|||
"""
|
||||
api = API(xml_element)
|
||||
# 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:
|
||||
LOG.info('Item not yet synced: %s', xml_element.attrib)
|
||||
return False
|
||||
# Grab the user's viewcount, resume points etc. from PMS' answer
|
||||
userdata = api.userdata()
|
||||
self.kodidb.update_userrating(db_item['kodi_id'],
|
||||
db_item['kodi_type'],
|
||||
userdata['UserRating'])
|
||||
api.userrating())
|
||||
if plex_type == v.PLEX_TYPE_EPISODE:
|
||||
self.kodidb.set_resume(db_item['kodi_fileid'],
|
||||
userdata['Resume'],
|
||||
userdata['Runtime'],
|
||||
userdata['PlayCount'],
|
||||
userdata['LastPlayedDate'])
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
if db_item['kodi_fileid_2']:
|
||||
self.kodidb.set_resume(db_item['kodi_fileid_2'],
|
||||
userdata['Resume'],
|
||||
userdata['Runtime'],
|
||||
userdata['PlayCount'],
|
||||
userdata['LastPlayedDate'])
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
return True
|
||||
|
||||
def remove(self, plex_id, plex_type=None):
|
||||
|
@ -149,7 +148,7 @@ class Show(TvShowMixin, ItemBase):
|
|||
Process a single show
|
||||
"""
|
||||
api = API(xml)
|
||||
plex_id = api.plex_id()
|
||||
plex_id = api.plex_id
|
||||
if not plex_id:
|
||||
LOG.error("Cannot parse XML data for TV show: %s", xml.attrib)
|
||||
return
|
||||
|
@ -162,16 +161,11 @@ class Show(TvShowMixin, ItemBase):
|
|||
kodi_id = show['kodi_id']
|
||||
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 #####
|
||||
if app.SYNC.direct_paths:
|
||||
# Direct paths is set the Kodi way
|
||||
playurl = api.validate_playurl(api.tv_show_path(),
|
||||
api.plex_type(),
|
||||
api.plex_type,
|
||||
folder=True)
|
||||
if playurl is None:
|
||||
return
|
||||
|
@ -197,7 +191,7 @@ class Show(TvShowMixin, ItemBase):
|
|||
self.kodidb.update_ratings(kodi_id,
|
||||
v.KODI_TYPE_SHOW,
|
||||
"default",
|
||||
api.audience_rating(),
|
||||
api.rating(),
|
||||
api.votecount(),
|
||||
rating_id)
|
||||
if api.provider('tvdb') is not None:
|
||||
|
@ -213,7 +207,7 @@ class Show(TvShowMixin, ItemBase):
|
|||
uniqueid = -1
|
||||
self.kodidb.modify_people(kodi_id,
|
||||
v.KODI_TYPE_SHOW,
|
||||
api.people_list())
|
||||
api.people())
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.modify_artwork(api.artwork(),
|
||||
kodi_id,
|
||||
|
@ -223,11 +217,11 @@ class Show(TvShowMixin, ItemBase):
|
|||
api.plot(),
|
||||
rating_id,
|
||||
api.premiere_date(),
|
||||
genre,
|
||||
api.list_to_string(api.genres()),
|
||||
api.title(),
|
||||
uniqueid,
|
||||
api.content_rating(),
|
||||
studio,
|
||||
api.list_to_string(api.studios()),
|
||||
api.sorttitle(),
|
||||
kodi_id)
|
||||
# OR ADD THE TVSHOW #####
|
||||
|
@ -240,7 +234,7 @@ class Show(TvShowMixin, ItemBase):
|
|||
kodi_id,
|
||||
v.KODI_TYPE_SHOW,
|
||||
"default",
|
||||
api.audience_rating(),
|
||||
api.rating(),
|
||||
api.votecount())
|
||||
if api.provider('tvdb'):
|
||||
uniqueid = self.kodidb.add_uniqueid_id()
|
||||
|
@ -253,7 +247,7 @@ class Show(TvShowMixin, ItemBase):
|
|||
uniqueid = -1
|
||||
self.kodidb.add_people(kodi_id,
|
||||
v.KODI_TYPE_SHOW,
|
||||
api.people_list())
|
||||
api.people())
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.add_artwork(api.artwork(),
|
||||
kodi_id,
|
||||
|
@ -264,18 +258,18 @@ class Show(TvShowMixin, ItemBase):
|
|||
api.plot(),
|
||||
rating_id,
|
||||
api.premiere_date(),
|
||||
genre,
|
||||
api.list_to_string(api.genres()),
|
||||
api.title(),
|
||||
uniqueid,
|
||||
api.content_rating(),
|
||||
studio,
|
||||
api.list_to_string(api.studios()),
|
||||
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
|
||||
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
|
||||
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.plexdb.add_show(plex_id=plex_id,
|
||||
checksum=api.checksum(),
|
||||
|
@ -292,7 +286,7 @@ class Season(TvShowMixin, ItemBase):
|
|||
Process a single season of a certain tv show
|
||||
"""
|
||||
api = API(xml)
|
||||
plex_id = api.plex_id()
|
||||
plex_id = api.plex_id
|
||||
if not plex_id:
|
||||
LOG.error('Error getting plex_id for season, skipping: %s',
|
||||
xml.attrib)
|
||||
|
@ -339,7 +333,7 @@ class Season(TvShowMixin, ItemBase):
|
|||
v.KODI_TYPE_SEASON)
|
||||
else:
|
||||
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:
|
||||
self.kodidb.add_artwork(artwork,
|
||||
kodi_id,
|
||||
|
@ -360,7 +354,7 @@ class Episode(TvShowMixin, ItemBase):
|
|||
Process single episode
|
||||
"""
|
||||
api = API(xml)
|
||||
plex_id = api.plex_id()
|
||||
plex_id = api.plex_id
|
||||
if not plex_id:
|
||||
LOG.error('Error getting plex_id for episode, skipping: %s',
|
||||
xml.attrib)
|
||||
|
@ -376,58 +370,48 @@ class Episode(TvShowMixin, ItemBase):
|
|||
old_kodi_fileid_2 = episode['kodi_fileid_2']
|
||||
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_episode = "-1"
|
||||
|
||||
# The grandparent TV show
|
||||
show = self.plexdb.show(show_id)
|
||||
show = self.plexdb.show(api.show_id())
|
||||
if not show:
|
||||
LOG.warn('Grandparent TV show %s not found in DB, adding it', show_id)
|
||||
show_xml = PF.GetPlexMetadata(show_id)
|
||||
LOG.warn('Grandparent TV show %s not found in DB, adding it', api.show_id())
|
||||
show_xml = PF.GetPlexMetadata(api.show_id())
|
||||
try:
|
||||
show_xml[0].attrib
|
||||
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
|
||||
Show(self.last_sync,
|
||||
plexdb=self.plexdb,
|
||||
kodidb=self.kodidb).add_update(show_xml[0],
|
||||
section_name,
|
||||
section_id)
|
||||
show = self.plexdb.show(show_id)
|
||||
show = self.plexdb.show(api.show_id())
|
||||
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
|
||||
grandparent_id = show['kodi_id']
|
||||
|
||||
# The parent Season
|
||||
season = self.plexdb.season(season_id)
|
||||
if not season and season_id:
|
||||
LOG.warn('Parent season %s not found in DB, adding it', season_id)
|
||||
season_xml = PF.GetPlexMetadata(season_id)
|
||||
season = self.plexdb.season(api.season_id())
|
||||
if not season and api.season_id():
|
||||
LOG.warn('Parent season %s not found in DB, adding it', api.season_id())
|
||||
season_xml = PF.GetPlexMetadata(api.season_id())
|
||||
try:
|
||||
season_xml[0].attrib
|
||||
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
|
||||
Season(self.last_sync,
|
||||
plexdb=self.plexdb,
|
||||
kodidb=self.kodidb).add_update(season_xml[0],
|
||||
section_name,
|
||||
section_id)
|
||||
season = self.plexdb.season(season_id)
|
||||
season = self.plexdb.season(api.season_id())
|
||||
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
|
||||
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
|
||||
# as with direct paths!
|
||||
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'
|
||||
% (path, plex_id, v.PLEX_TYPE_EPISODE, filename))
|
||||
playurl = filename
|
||||
|
@ -491,7 +475,7 @@ class Episode(TvShowMixin, ItemBase):
|
|||
self.kodidb.update_ratings(kodi_id,
|
||||
v.KODI_TYPE_EPISODE,
|
||||
"default",
|
||||
userdata['Rating'],
|
||||
api.rating(),
|
||||
api.votecount(),
|
||||
ratingid)
|
||||
if api.provider('tvdb'):
|
||||
|
@ -507,7 +491,7 @@ class Episode(TvShowMixin, ItemBase):
|
|||
uniqueid = -1
|
||||
self.kodidb.modify_people(kodi_id,
|
||||
v.KODI_TYPE_EPISODE,
|
||||
api.people_list())
|
||||
api.people())
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.modify_artwork(api.artwork(),
|
||||
kodi_id,
|
||||
|
@ -515,12 +499,12 @@ class Episode(TvShowMixin, ItemBase):
|
|||
self.kodidb.update_episode(api.title(),
|
||||
api.plot(),
|
||||
ratingid,
|
||||
writer,
|
||||
api.list_to_string(api.writers()),
|
||||
api.premiere_date(),
|
||||
api.runtime(),
|
||||
director,
|
||||
season_no,
|
||||
episode_no,
|
||||
api.list_to_string(api.directors()),
|
||||
api.season_number(),
|
||||
api.index(),
|
||||
api.title(),
|
||||
airs_before_season,
|
||||
airs_before_episode,
|
||||
|
@ -528,25 +512,25 @@ class Episode(TvShowMixin, ItemBase):
|
|||
kodi_pathid,
|
||||
kodi_fileid, # and NOT kodi_fileid_2
|
||||
parent_id,
|
||||
userdata['UserRating'],
|
||||
api.userrating(),
|
||||
kodi_id)
|
||||
self.kodidb.set_resume(kodi_fileid,
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
userdata['PlayCount'],
|
||||
userdata['LastPlayedDate'])
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
if not app.SYNC.direct_paths:
|
||||
self.kodidb.set_resume(kodi_fileid_2,
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
userdata['PlayCount'],
|
||||
userdata['LastPlayedDate'])
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
self.plexdb.add_episode(plex_id=plex_id,
|
||||
checksum=api.checksum(),
|
||||
section_id=section_id,
|
||||
show_id=show_id,
|
||||
show_id=api.show_id(),
|
||||
grandparent_id=grandparent_id,
|
||||
season_id=season_id,
|
||||
season_id=api.season_id(),
|
||||
parent_id=parent_id,
|
||||
kodi_id=kodi_id,
|
||||
kodi_fileid=kodi_fileid,
|
||||
|
@ -571,7 +555,7 @@ class Episode(TvShowMixin, ItemBase):
|
|||
kodi_id,
|
||||
v.KODI_TYPE_EPISODE,
|
||||
"default",
|
||||
userdata['Rating'],
|
||||
api.rating(),
|
||||
api.votecount())
|
||||
if api.provider('tvdb'):
|
||||
uniqueid = self.kodidb.add_uniqueid_id()
|
||||
|
@ -582,7 +566,7 @@ class Episode(TvShowMixin, ItemBase):
|
|||
"tvdb")
|
||||
self.kodidb.add_people(kodi_id,
|
||||
v.KODI_TYPE_EPISODE,
|
||||
api.people_list())
|
||||
api.people())
|
||||
if app.SYNC.artwork:
|
||||
self.kodidb.add_artwork(api.artwork(),
|
||||
kodi_id,
|
||||
|
@ -592,12 +576,12 @@ class Episode(TvShowMixin, ItemBase):
|
|||
api.title(),
|
||||
api.plot(),
|
||||
rating_id,
|
||||
writer,
|
||||
api.list_to_string(api.writers()),
|
||||
api.premiere_date(),
|
||||
api.runtime(),
|
||||
director,
|
||||
season_no,
|
||||
episode_no,
|
||||
api.list_to_string(api.directors()),
|
||||
api.season_number(),
|
||||
api.index(),
|
||||
api.title(),
|
||||
grandparent_id,
|
||||
airs_before_season,
|
||||
|
@ -605,24 +589,24 @@ class Episode(TvShowMixin, ItemBase):
|
|||
playurl,
|
||||
kodi_pathid,
|
||||
parent_id,
|
||||
userdata['UserRating'])
|
||||
api.userrating())
|
||||
self.kodidb.set_resume(kodi_fileid,
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
userdata['PlayCount'],
|
||||
userdata['LastPlayedDate'])
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
if not app.SYNC.direct_paths:
|
||||
self.kodidb.set_resume(kodi_fileid_2,
|
||||
api.resume_point(),
|
||||
api.runtime(),
|
||||
userdata['PlayCount'],
|
||||
userdata['LastPlayedDate'])
|
||||
api.viewcount(),
|
||||
api.lastplayed())
|
||||
self.plexdb.add_episode(plex_id=plex_id,
|
||||
checksum=api.checksum(),
|
||||
section_id=section_id,
|
||||
show_id=show_id,
|
||||
show_id=api.show_id(),
|
||||
grandparent_id=grandparent_id,
|
||||
season_id=season_id,
|
||||
season_id=api.season_id(),
|
||||
parent_id=parent_id,
|
||||
kodi_id=kodi_id,
|
||||
kodi_fileid=kodi_fileid,
|
||||
|
|
|
@ -129,7 +129,7 @@ def process_fanart(plex_id, plex_type, refresh=False):
|
|||
db_item['kodi_type'])
|
||||
# Additional fanart for sets/collections
|
||||
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)
|
||||
with KodiVideoDB() as kodidb:
|
||||
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('ratingKey'))) for x in COLLECTION_MATCH]
|
||||
item['children'] = {}
|
||||
for plex_set_id, set_name in api.collection_list():
|
||||
for plex_set_id, set_name in api.collections():
|
||||
if self.isCanceled():
|
||||
return
|
||||
if plex_set_id not in COLLECTION_XMLS:
|
||||
|
|
|
@ -177,7 +177,7 @@ class Section(object):
|
|||
api = API(xml_element)
|
||||
self.section_id = utils.cast(int, xml_element.get('key'))
|
||||
self.name = api.title()
|
||||
self.section_type = api.plex_type()
|
||||
self.section_type = api.plex_type
|
||||
self.icon = api.one_artwork('composite')
|
||||
self.artwork = api.one_artwork('art')
|
||||
self.thumb = api.one_artwork('thumb')
|
||||
|
|
|
@ -313,9 +313,8 @@ def process_playing(data):
|
|||
plex_id)
|
||||
continue
|
||||
api = API(xml[0])
|
||||
userdata = api.userdata()
|
||||
session['duration'] = userdata['Runtime']
|
||||
session['viewCount'] = userdata['PlayCount']
|
||||
session['duration'] = api.runtime()
|
||||
session['viewCount'] = api.viewcount()
|
||||
# Sometimes, Plex tells us resume points in milliseconds and
|
||||
# not in seconds - thank you very much!
|
||||
if message['viewOffset'] > session['duration']:
|
||||
|
|
|
@ -332,11 +332,11 @@ def _prep_playlist_stack(xml, resume):
|
|||
for i, item in enumerate(xml):
|
||||
api = API(item)
|
||||
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
|
||||
# use the item path stored in the Kodi DB
|
||||
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_type = db_item['kodi_type'] if db_item else None
|
||||
else:
|
||||
|
@ -349,7 +349,7 @@ def _prep_playlist_stack(xml, resume):
|
|||
kodi_id = None
|
||||
kodi_type = None
|
||||
for part, _ in enumerate(item[0]):
|
||||
api.set_part_number(part)
|
||||
api.part = part
|
||||
if kodi_id is None:
|
||||
# Need to redirect again to PKC to conclude playback
|
||||
path = api.path(force_addon=True, force_first_media=True)
|
||||
|
@ -361,7 +361,7 @@ def _prep_playlist_stack(xml, resume):
|
|||
# 'plugin.video.plexkodiconnect', 1)
|
||||
# path = path.replace('plugin.video.plexkodiconnect.movies',
|
||||
# 'plugin.video.plexkodiconnect', 1)
|
||||
listitem = api.create_listitem()
|
||||
listitem = api.listitem()
|
||||
listitem.setPath(path.encode('utf-8'))
|
||||
else:
|
||||
# Will add directly via the Kodi DB
|
||||
|
@ -458,16 +458,16 @@ def _conclude_playback(playqueue, pos):
|
|||
return PKC listitem attached to result
|
||||
"""
|
||||
LOG.info('Concluding playback for playqueue position %s', pos)
|
||||
listitem = transfer.PKCListItem()
|
||||
item = playqueue.items[pos]
|
||||
if item.xml is not None:
|
||||
# Got a Plex element
|
||||
api = API(item.xml)
|
||||
api.set_part_number(item.part)
|
||||
api.create_listitem(listitem)
|
||||
api.part = item.part or 0
|
||||
listitem = api.listitem(listitem=transfer.PKCListItem)
|
||||
playutils = PlayUtils(api, item)
|
||||
playurl = playutils.getPlayUrl()
|
||||
else:
|
||||
listitem = transfer.PKCListItem()
|
||||
api = None
|
||||
playurl = item.file
|
||||
if not playurl:
|
||||
|
@ -514,10 +514,9 @@ def process_indirect(key, offset, resolve=True):
|
|||
return
|
||||
|
||||
api = API(xml[0])
|
||||
listitem = transfer.PKCListItem()
|
||||
api.create_listitem(listitem)
|
||||
listitem = api.listitem(listitem=transfer.PKCListItem)
|
||||
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()
|
||||
item = PL.Playlist_Item()
|
||||
item.xml = xml[0]
|
||||
|
@ -574,7 +573,7 @@ def play_xml(playqueue, xml, offset=None, start_plex_id=None):
|
|||
else playqueue.selectedItemID
|
||||
for startpos, video in enumerate(xml):
|
||||
api = API(video)
|
||||
if api.plex_id() == start_item:
|
||||
if api.plex_id == start_item:
|
||||
break
|
||||
else:
|
||||
startpos = 0
|
||||
|
|
|
@ -423,8 +423,8 @@ def playlist_item_from_xml(xml_video_element, kodi_id=None, kodi_type=None):
|
|||
"""
|
||||
item = Playlist_Item()
|
||||
api = API(xml_video_element)
|
||||
item.plex_id = api.plex_id()
|
||||
item.plex_type = api.plex_type()
|
||||
item.plex_id = api.plex_id
|
||||
item.plex_type = api.plex_type
|
||||
# item.id will only be set if you passed in an xml_video_element from e.g.
|
||||
# a playQueue
|
||||
item.id = api.item_id()
|
||||
|
|
|
@ -170,32 +170,32 @@ def _full_sync():
|
|||
return False
|
||||
api = API(xml_playlist)
|
||||
try:
|
||||
old_plex_ids.remove(api.plex_id())
|
||||
old_plex_ids.remove(api.plex_id)
|
||||
except ValueError:
|
||||
pass
|
||||
if not sync_plex_playlist(xml=xml_playlist):
|
||||
continue
|
||||
playlist = db.get_playlist(plex_id=api.plex_id())
|
||||
playlist = db.get_playlist(plex_id=api.plex_id)
|
||||
if not playlist:
|
||||
LOG.debug('New Plex playlist %s discovered: %s',
|
||||
api.plex_id(), api.title())
|
||||
api.plex_id, api.title())
|
||||
try:
|
||||
kodi_pl.create(api.plex_id())
|
||||
kodi_pl.create(api.plex_id)
|
||||
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():
|
||||
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!
|
||||
try:
|
||||
kodi_pl.delete(playlist)
|
||||
except PlaylistError:
|
||||
LOG.info('Skipping recreation of playlist %s', api.plex_id())
|
||||
LOG.info('Skipping recreation of playlist %s', api.plex_id)
|
||||
else:
|
||||
try:
|
||||
kodi_pl.create(api.plex_id())
|
||||
kodi_pl.create(api.plex_id)
|
||||
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
|
||||
for plex_id in old_plex_ids:
|
||||
if isCanceled():
|
||||
|
|
|
@ -35,7 +35,7 @@ def create(plex_id):
|
|||
raise PlaylistError('Could not get Plex playlist %s' % plex_id)
|
||||
api = API(xml_metadata[0])
|
||||
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.plex_name = api.title()
|
||||
playlist.plex_updatedat = api.updated_at()
|
||||
|
@ -104,24 +104,16 @@ def _write_playlist_to_file(playlist, xml):
|
|||
text = '#EXTCPlayListM3U::M3U\n'
|
||||
for element in xml:
|
||||
api = API(element)
|
||||
append_season_episode = False
|
||||
if api.plex_type() == v.PLEX_TYPE_EPISODE:
|
||||
_, _, 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:
|
||||
if api.plex_type == v.PLEX_TYPE_EPISODE:
|
||||
if api.season_number() is not None and api.index() is not None:
|
||||
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()))
|
||||
else:
|
||||
# Only append the TV show name
|
||||
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:
|
||||
text += ('#EXTINF:%s,%s\n%s\n'
|
||||
% (api.runtime(), api.title(), api.path()))
|
||||
|
|
|
@ -68,7 +68,7 @@ def initialize(playlist, plex_id):
|
|||
plex_id)
|
||||
raise PlaylistError('Could not initialize Plex playlist %s', plex_id)
|
||||
api = API(xml[0])
|
||||
playlist.plex_id = api.plex_id()
|
||||
playlist.plex_id = api.plex_id
|
||||
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' %
|
||||
playlist)
|
||||
api = API(xml[0])
|
||||
playlist.plex_id = api.plex_id()
|
||||
playlist.plex_id = api.plex_id
|
||||
playlist.plex_updatedat = api.updated_at()
|
||||
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None):
|
|||
playqueue.clear()
|
||||
for i, child in enumerate(xml):
|
||||
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
|
||||
LOG.debug('Firing up Kodi player')
|
||||
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'):
|
||||
LOG.info('.strm file detected')
|
||||
playurl = self.api.validate_playurl(path,
|
||||
self.api.plex_type(),
|
||||
self.api.plex_type,
|
||||
force_check=True)
|
||||
return playurl
|
||||
# set to either 'Direct Stream=1' or 'Transcode=2'
|
||||
|
@ -78,7 +78,7 @@ class PlayUtils():
|
|||
if self.mustTranscode():
|
||||
return
|
||||
return self.api.validate_playurl(path,
|
||||
self.api.plex_type(),
|
||||
self.api.plex_type,
|
||||
force_check=True)
|
||||
|
||||
def mustTranscode(self):
|
||||
|
@ -93,7 +93,7 @@ class PlayUtils():
|
|||
- video bitrate above specified settings bitrate
|
||||
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')
|
||||
return False
|
||||
videoCodec = self.api.video_codec()
|
||||
|
@ -139,7 +139,7 @@ class PlayUtils():
|
|||
|
||||
def isDirectStream(self):
|
||||
# Never transcode Music
|
||||
if self.api.plex_type() == 'track':
|
||||
if self.api.plex_type == 'track':
|
||||
return True
|
||||
# set to 'Transcode=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)
|
||||
return
|
||||
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')
|
||||
PQ.init_playqueue_from_plex_children(
|
||||
api.plex_id(),
|
||||
api.plex_id,
|
||||
transient_token=data.get('token'))
|
||||
elif data['containerKey'].startswith('/playQueues/'):
|
||||
_, container_key, _ = PF.ParseContainerKey(data['containerKey'])
|
||||
|
@ -104,7 +104,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
icon='{error}')
|
||||
return
|
||||
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()
|
||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||
playqueue.plex_transient_token = data.get('token')
|
||||
|
@ -117,8 +117,8 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
app.CONN.plex_transient_token = data.get('token')
|
||||
if data.get('offset') != '0':
|
||||
app.PLAYSTATE.resume_playback = True
|
||||
playback.playback_triage(api.plex_id(),
|
||||
api.plex_type(),
|
||||
playback.playback_triage(api.plex_id,
|
||||
api.plex_type,
|
||||
resolve=False)
|
||||
|
||||
@staticmethod
|
||||
|
@ -153,7 +153,7 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
return
|
||||
api = API(xml[0])
|
||||
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,
|
||||
playqueue_id=container_key,
|
||||
repeat=query.get('repeat'),
|
||||
|
|
|
@ -1029,40 +1029,3 @@ def GetUserArtworkURL(username):
|
|||
url = user.thumb
|
||||
LOG.debug("Avatar url for user %s is: %s", username, 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
|
||||
listitem = xbmcgui.ListItem(label=data.get('label'),
|
||||
label2=data.get('label2'),
|
||||
path=data.get('path'))
|
||||
path=data.get('path'),
|
||||
offscreen=True)
|
||||
if data['info']:
|
||||
listitem.setInfo(**data['info'])
|
||||
for stream in data['stream_info']:
|
||||
|
@ -147,6 +148,8 @@ def convert_pkc_to_listitem(pkc_listitem):
|
|||
listitem.setProperty(key, cast(str, value))
|
||||
if data['subtitles']:
|
||||
listitem.setSubtitles(data['subtitles'])
|
||||
if data['contextmenu']:
|
||||
listitem.addContextMenuItems(data['contextmenu'])
|
||||
return listitem
|
||||
|
||||
|
||||
|
@ -157,7 +160,7 @@ class PKCListItem(object):
|
|||
|
||||
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 = {
|
||||
'stream_info': [], # (type, values: dict { label: value })
|
||||
'art': {}, # dict
|
||||
|
@ -167,9 +170,10 @@ class PKCListItem(object):
|
|||
'path': path, # string
|
||||
'property': {}, # (key, value)
|
||||
'subtitles': [], # strings
|
||||
'contextmenu': None
|
||||
}
|
||||
|
||||
def addContextMenuItems(self, items, replaceItems):
|
||||
def addContextMenuItems(self, items):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
self.data['contextmenu'] = items
|
||||
|
||||
def addStreamInfo(self, type, values):
|
||||
"""
|
||||
|
|
|
@ -20,6 +20,12 @@ from functools import wraps
|
|||
import hashlib
|
||||
import re
|
||||
import gc
|
||||
try:
|
||||
from multiprocessing.pool import ThreadPool
|
||||
SUPPORTS_POOL = True
|
||||
except Exception:
|
||||
SUPPORTS_POOL = False
|
||||
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
|
@ -286,7 +292,16 @@ def cast(func, value):
|
|||
return value
|
||||
else:
|
||||
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:
|
||||
return func(value)
|
||||
except ValueError:
|
||||
|
@ -930,6 +945,27 @@ def generate_file_md5(path):
|
|||
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
|
||||
|
||||
|
|
|
@ -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 logging import getLogger
|
||||
try:
|
||||
from multiprocessing.pool import ThreadPool
|
||||
SUPPORTS_POOL = True
|
||||
except Exception:
|
||||
SUPPORTS_POOL = False
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcvfs
|
||||
|
||||
from .plex_api import API
|
||||
from .plex_db import PlexDB
|
||||
from . import json_rpc as js, utils, variables as v
|
||||
|
||||
LOG = getLogger('PLEX.widget')
|
||||
|
@ -34,27 +27,6 @@ SYNCHED = True
|
|||
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):
|
||||
'''
|
||||
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')
|
||||
|
||||
|
||||
def generate_item(xml_element):
|
||||
def generate_item(api):
|
||||
"""
|
||||
Meant to be consumed by metadatautils.kodidb.prepare_listitem(), and then
|
||||
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
|
||||
"""
|
||||
try:
|
||||
if xml_element.tag in ('Directory', 'Playlist', 'Hub'):
|
||||
return _generate_folder(xml_element)
|
||||
if api.tag in ('Directory', 'Playlist', 'Hub'):
|
||||
return _generate_folder(api)
|
||||
else:
|
||||
return _generate_content(xml_element)
|
||||
return _generate_content(api)
|
||||
except Exception:
|
||||
# Usefull to catch everything here since we're using threadpool
|
||||
LOG.error('xml that caused the crash: "%s": %s',
|
||||
xml_element.tag, xml_element.attrib)
|
||||
api.tag, api.attrib)
|
||||
utils.ERROR(notify=True)
|
||||
|
||||
|
||||
def _generate_folder(xml_element):
|
||||
def _generate_folder(api):
|
||||
'''Generates "folder"/"directory" items that user can further navigate'''
|
||||
api = API(xml_element)
|
||||
art = api.artwork()
|
||||
return {
|
||||
'title': api.title(),
|
||||
|
@ -128,60 +99,54 @@ def _generate_folder(xml_element):
|
|||
}
|
||||
|
||||
|
||||
def _generate_content(xml_element):
|
||||
api = API(xml_element)
|
||||
plex_type = api.plex_type()
|
||||
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:
|
||||
def _generate_content(api):
|
||||
plex_type = api.plex_type
|
||||
if api.kodi_id:
|
||||
# Item is synched to the Kodi db - let's use that info
|
||||
# (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:
|
||||
people = api.people()
|
||||
cast = [{
|
||||
'name': x[0],
|
||||
'thumbnail': x[1],
|
||||
'role': x[2],
|
||||
'order': x[3],
|
||||
} for x in api.people_list()['actor']]
|
||||
} for x in api.people()['actor']]
|
||||
item = {
|
||||
'cast': cast,
|
||||
'country': api.country_list(),
|
||||
'country': api.countries(),
|
||||
'dateadded': api.date_created(), # e.g '2019-01-03 19:40:59'
|
||||
'director': people['Director'], # list of [str]
|
||||
'duration': userdata['Runtime'],
|
||||
'episode': episode_no,
|
||||
'director': api.directors(), # list of [str]
|
||||
'duration': api.runtime(),
|
||||
'episode': api.index(),
|
||||
# 'file': '', # e.g. 'videodb://tvshows/titles/20'
|
||||
'genre': api.genre_list(),
|
||||
'genre': api.genres(),
|
||||
# 'imdbnumber': '', # e.g.'341663'
|
||||
'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'
|
||||
'originaltitle': '', # e.g. 'Titans (2018)'
|
||||
'playcount': userdata['PlayCount'], # [int]
|
||||
'playcount': api.viewcount(), # [int]
|
||||
'plot': api.plot(), # [str]
|
||||
'plotoutline': api.tagline(),
|
||||
'premiered': api.premiere_date(), # '2018-10-12'
|
||||
'rating': api.audience_rating(), # [float]
|
||||
'season': season_no,
|
||||
'rating': api.rating(), # [float]
|
||||
'season': api.season_number(),
|
||||
'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
|
||||
'tagline': api.tagline(),
|
||||
'thumbnail': '', # e.g. 'image://https%3a%2f%2fassets.tv'
|
||||
'title': api.title(), # 'Titans (2018)'
|
||||
'type': kodi_type,
|
||||
'type': api.kodi_type,
|
||||
'trailer': api.trailer(),
|
||||
'tvshowtitle': tvshowtitle,
|
||||
'tvshowtitle': api.show_title(),
|
||||
'uniqueid': {
|
||||
'imdbnumber': api.provider('imdb') or '',
|
||||
'tvdb_id': api.provider('tvdb') or ''
|
||||
},
|
||||
'votes': '0', # [str]!
|
||||
'writer': people['Writer'], # list of [str]
|
||||
'writer': api.writers(), # list of [str]
|
||||
'year': api.year(), # [int]
|
||||
}
|
||||
|
||||
|
@ -206,18 +171,18 @@ def _generate_content(xml_element):
|
|||
if resume:
|
||||
item['resume'] = {
|
||||
'position': resume,
|
||||
'total': userdata['Runtime']
|
||||
'total': api.runtime()
|
||||
}
|
||||
|
||||
item['icon'] = v.ICON_FROM_PLEXTYPE[plex_type]
|
||||
# Some customization
|
||||
if plex_type == v.PLEX_TYPE_EPISODE:
|
||||
# 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:
|
||||
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:
|
||||
item['title'] = "%s - %s " % (tvshowtitle, item['title'])
|
||||
item['title'] = "%s - %s " % (api.show_title(), item['title'])
|
||||
item['label'] = item['title']
|
||||
|
||||
# Determine the path for this item
|
||||
|
@ -226,14 +191,14 @@ def _generate_content(xml_element):
|
|||
params = {
|
||||
'mode': 'plex_node',
|
||||
'key': key,
|
||||
'offset': xml_element.attrib.get('viewOffset', '0'),
|
||||
'offset': api.resume_point_plex()
|
||||
}
|
||||
url = utils.extend_url('plugin://%s' % v.ADDON_ID, params)
|
||||
elif plex_type == v.PLEX_TYPE_PHOTO:
|
||||
url = api.get_picture_path()
|
||||
else:
|
||||
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
|
||||
# We CANNOT use paths that show up in the Kodi paths table!
|
||||
url = url.replace('plugin.video.plexkodiconnect.tvshows',
|
||||
|
@ -242,20 +207,6 @@ def _generate_content(xml_element):
|
|||
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):
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
@ -472,13 +424,13 @@ def create_listitem(item, as_tuple=True, offscreen=True):
|
|||
"""
|
||||
try:
|
||||
if v.KODIVERSION > 17:
|
||||
liz = xbmcgui.ListItem(
|
||||
liz = listitem(
|
||||
label=item.get("label", ""),
|
||||
label2=item.get("label2", ""),
|
||||
path=item['file'],
|
||||
offscreen=offscreen)
|
||||
else:
|
||||
liz = xbmcgui.ListItem(
|
||||
liz = listitem(
|
||||
label=item.get("label", ""),
|
||||
label2=item.get("label2", ""),
|
||||
path=item['file'])
|
||||
|
@ -585,11 +537,9 @@ def create_listitem(item, as_tuple=True, offscreen=True):
|
|||
liz.setInfo(type=nodetype, infoLabels=infolabels)
|
||||
|
||||
# artwork
|
||||
liz.setArt(item.get("art", {}))
|
||||
if "icon" in item:
|
||||
liz.setIconImage(item['icon'])
|
||||
if "thumbnail" in item:
|
||||
liz.setThumbnailImage(item['thumbnail'])
|
||||
item['art']['icon'] = item['icon']
|
||||
liz.setArt(item.get("art", {}))
|
||||
|
||||
# contextmenu
|
||||
if item["type"] in ["episode", "season"] and "season" in item and "tvshowid" in item:
|
||||
|
@ -627,4 +577,3 @@ def create_main_entry(item):
|
|||
'type': '',
|
||||
'IsPlayable': 'false'
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue