Merge pull request #722 from croneter/widget_overhaul
Giant overhaul of widgets
This commit is contained in:
commit
bda58deb6e
19 changed files with 2216 additions and 1624 deletions
39
default.py
39
default.py
|
@ -41,33 +41,18 @@ class Main():
|
||||||
elif mode == 'plex_node':
|
elif mode == 'plex_node':
|
||||||
self.play()
|
self.play()
|
||||||
|
|
||||||
elif mode == 'ondeck':
|
|
||||||
entrypoint.on_deck_episodes(itemid,
|
|
||||||
params.get('tagname'),
|
|
||||||
int(params.get('limit')))
|
|
||||||
|
|
||||||
elif mode == 'recentepisodes':
|
|
||||||
entrypoint.recent_episodes(params.get('type'),
|
|
||||||
params.get('tagname'),
|
|
||||||
int(params.get('limit')))
|
|
||||||
|
|
||||||
elif mode == 'nextup':
|
|
||||||
entrypoint.next_up_episodes(params['tagname'],
|
|
||||||
int(params['limit']))
|
|
||||||
|
|
||||||
elif mode == 'inprogressepisodes':
|
|
||||||
entrypoint.in_progress_episodes(params['tagname'],
|
|
||||||
int(params['limit']))
|
|
||||||
|
|
||||||
elif mode == 'browseplex':
|
elif mode == 'browseplex':
|
||||||
entrypoint.browse_plex(key=params.get('key'),
|
entrypoint.browse_plex(key=params.get('key'),
|
||||||
plex_section_id=params.get('id'))
|
plex_type=params.get('plex_type'),
|
||||||
|
section_id=params.get('section_id'),
|
||||||
|
synched=params.get('synched') != 'false',
|
||||||
|
prompt=params.get('prompt'))
|
||||||
|
|
||||||
elif mode == 'watchlater':
|
elif mode == 'watchlater':
|
||||||
entrypoint.watchlater()
|
entrypoint.watchlater()
|
||||||
|
|
||||||
elif mode == 'channels':
|
elif mode == 'channels':
|
||||||
entrypoint.channels()
|
entrypoint.browse_plex(key='/channels/all')
|
||||||
|
|
||||||
elif mode == 'route_to_extras':
|
elif mode == 'route_to_extras':
|
||||||
# Hack so we can store this path in the Kodi DB
|
# Hack so we can store this path in the Kodi DB
|
||||||
|
@ -86,20 +71,23 @@ class Main():
|
||||||
xbmc.executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID)
|
xbmc.executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID)
|
||||||
|
|
||||||
elif mode == 'enterPMS':
|
elif mode == 'enterPMS':
|
||||||
entrypoint.create_new_pms()
|
LOG.info('Request to manually enter new PMS address')
|
||||||
|
transfer.plex_command('enter_new_pms_address')
|
||||||
|
|
||||||
elif mode == 'reset':
|
elif mode == 'reset':
|
||||||
transfer.plex_command('RESET-PKC')
|
transfer.plex_command('RESET-PKC')
|
||||||
|
|
||||||
elif mode == 'togglePlexTV':
|
elif mode == 'togglePlexTV':
|
||||||
entrypoint.toggle_plex_tv_sign_in()
|
LOG.info('Toggle of Plex.tv sign-in requested')
|
||||||
|
transfer.plex_command('toggle_plex_tv_sign_in')
|
||||||
|
|
||||||
elif mode == 'passwords':
|
elif mode == 'passwords':
|
||||||
from resources.lib.windows import direct_path_sources
|
from resources.lib.windows import direct_path_sources
|
||||||
direct_path_sources.start()
|
direct_path_sources.start()
|
||||||
|
|
||||||
elif mode == 'switchuser':
|
elif mode == 'switchuser':
|
||||||
entrypoint.switch_plex_user()
|
LOG.info('Plex home user switch requested')
|
||||||
|
transfer.plex_command('switch_plex_user')
|
||||||
|
|
||||||
elif mode in ('manualsync', 'repair'):
|
elif mode in ('manualsync', 'repair'):
|
||||||
if mode == 'repair':
|
if mode == 'repair':
|
||||||
|
@ -114,7 +102,8 @@ class Main():
|
||||||
transfer.plex_command('textures-scan')
|
transfer.plex_command('textures-scan')
|
||||||
|
|
||||||
elif mode == 'chooseServer':
|
elif mode == 'chooseServer':
|
||||||
entrypoint.choose_pms_server()
|
LOG.info("Choosing PMS server requested, starting")
|
||||||
|
transfer.plex_command('choose_pms_server')
|
||||||
|
|
||||||
elif mode == 'deviceid':
|
elif mode == 'deviceid':
|
||||||
self.deviceid()
|
self.deviceid()
|
||||||
|
@ -139,7 +128,7 @@ class Main():
|
||||||
entrypoint.playlists(params.get('content_type'))
|
entrypoint.playlists(params.get('content_type'))
|
||||||
|
|
||||||
elif mode == 'hub':
|
elif mode == 'hub':
|
||||||
entrypoint.hub(params.get('type'))
|
entrypoint.hub(params.get('content_type'))
|
||||||
|
|
||||||
elif mode == 'select-libraries':
|
elif mode == 'select-libraries':
|
||||||
LOG.info('User requested to select Plex libraries')
|
LOG.info('User requested to select Plex libraries')
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,26 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
from json import loads, dumps
|
from json import loads, dumps
|
||||||
from xbmc import executeJSONRPC
|
from xbmc import executeJSONRPC
|
||||||
|
|
||||||
from . import timing
|
from . import kodi_constants, timing, variables as v
|
||||||
|
|
||||||
|
JSON_FROM_KODITYPE = {
|
||||||
|
v.KODI_TYPE_MOVIE: ('VideoLibrary.GetMovieDetails',
|
||||||
|
kodi_constants.FIELDS_MOVIES),
|
||||||
|
v.KODI_TYPE_SHOW: ('VideoLibrary.GetTVShowDetails',
|
||||||
|
kodi_constants.FIELDS_TVSHOWS),
|
||||||
|
v.KODI_TYPE_SEASON: ('VideoLibrary.GetSeasonDetails',
|
||||||
|
kodi_constants.FIELDS_SEASON),
|
||||||
|
v.KODI_TYPE_EPISODE: ('VideoLibrary.GetEpisodeDetails',
|
||||||
|
kodi_constants.FIELDS_EPISODES),
|
||||||
|
v.KODI_TYPE_ARTIST: ('AudioLibrary.GetArtistDetails',
|
||||||
|
kodi_constants.FIELDS_ARTISTS),
|
||||||
|
v.KODI_TYPE_ALBUM: ('AudioLibrary.GetAlbumDetails',
|
||||||
|
kodi_constants.FIELDS_ALBUMS),
|
||||||
|
v.KODI_TYPE_SONG: ('AudioLibrary.GetSongDetails',
|
||||||
|
kodi_constants.FIELDS_SONGS),
|
||||||
|
v.KODI_TYPE_SET: ('VideoLibrary.GetMovieSetDetails',
|
||||||
|
[]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class JsonRPC(object):
|
class JsonRPC(object):
|
||||||
|
@ -557,3 +576,16 @@ def settings_setsettingvalue(setting, value):
|
||||||
'setting': setting,
|
'setting': setting,
|
||||||
'value': value
|
'value': value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def item_details(kodi_id, kodi_type):
|
||||||
|
'''
|
||||||
|
Returns the Kodi item dict for this item
|
||||||
|
'''
|
||||||
|
json, fields = JSON_FROM_KODITYPE[kodi_type]
|
||||||
|
ret = JsonRPC(json).execute({'%sid' % kodi_type: kodi_id,
|
||||||
|
'properties': fields})
|
||||||
|
try:
|
||||||
|
return ret['result']['%sdetails' % kodi_type]
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
return {}
|
||||||
|
|
92
resources/lib/kodi_constants.py
Normal file
92
resources/lib/kodi_constants.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
script.module.metadatautils
|
||||||
|
kodi_constants.py
|
||||||
|
Several common constants for use with Kodi json api
|
||||||
|
'''
|
||||||
|
FIELDS_BASE = ['dateadded', 'file', 'lastplayed', 'plot', 'title', 'art',
|
||||||
|
'playcount']
|
||||||
|
FIELDS_FILE = FIELDS_BASE + ['streamdetails', 'director', 'resume', 'runtime']
|
||||||
|
FIELDS_MOVIES = FIELDS_FILE + ['plotoutline', 'sorttitle', 'cast', 'votes',
|
||||||
|
'showlink', 'top250', 'trailer', 'year', 'country', 'studio', 'set',
|
||||||
|
'genre', 'mpaa', 'setid', 'rating', 'tag', 'tagline', 'writer',
|
||||||
|
'originaltitle', 'imdbnumber', 'uniqueid']
|
||||||
|
FIELDS_TVSHOWS = FIELDS_BASE + ['sorttitle', 'mpaa', 'premiered', 'year',
|
||||||
|
'episode', 'watchedepisodes', 'votes', 'rating', 'studio', 'season',
|
||||||
|
'genre', 'cast', 'episodeguide', 'tag', 'originaltitle', 'imdbnumber']
|
||||||
|
FIELDS_SEASON = ['art', 'playcount', 'season', 'showtitle', 'episode',
|
||||||
|
'tvshowid', 'watchedepisodes', 'userrating', 'fanart', 'thumbnail']
|
||||||
|
FIELDS_EPISODES = FIELDS_FILE + ['cast', 'productioncode', 'rating', 'votes',
|
||||||
|
'episode', 'showtitle', 'tvshowid', 'season', 'firstaired', 'writer',
|
||||||
|
'originaltitle']
|
||||||
|
FIELDS_MUSICVIDEOS = FIELDS_FILE + ['genre', 'artist', 'tag', 'album', 'track',
|
||||||
|
'studio', 'year']
|
||||||
|
FIELDS_FILES = FIELDS_FILE + ['plotoutline', 'sorttitle', 'cast', 'votes',
|
||||||
|
'trailer', 'year', 'country', 'studio', 'genre', 'mpaa', 'rating',
|
||||||
|
'tagline', 'writer', 'originaltitle', 'imdbnumber', 'premiered', 'episode',
|
||||||
|
'showtitle', 'firstaired', 'watchedepisodes', 'duration', 'season']
|
||||||
|
FIELDS_SONGS = ['artist', 'displayartist', 'title', 'rating', 'fanart',
|
||||||
|
'thumbnail', 'duration', 'disc', 'playcount', 'comment', 'file', 'album',
|
||||||
|
'lastplayed', 'genre', 'musicbrainzartistid', 'track', 'dateadded']
|
||||||
|
FIELDS_ALBUMS = ['title', 'fanart', 'thumbnail', 'genre', 'displayartist',
|
||||||
|
'artist', 'musicbrainzalbumartistid', 'year', 'rating', 'artistid',
|
||||||
|
'musicbrainzalbumid', 'theme', 'description', 'type', 'style', 'playcount',
|
||||||
|
'albumlabel', 'mood', 'dateadded']
|
||||||
|
FIELDS_ARTISTS = ['born', 'formed', 'died', 'style', 'yearsactive', 'mood',
|
||||||
|
'fanart', 'thumbnail', 'musicbrainzartistid', 'disbanded', 'description',
|
||||||
|
'instrument']
|
||||||
|
FIELDS_RECORDINGS = ['art', 'channel', 'directory', 'endtime', 'file', 'genre',
|
||||||
|
'icon', 'playcount', 'plot', 'plotoutline', 'resume', 'runtime',
|
||||||
|
'starttime', 'streamurl', 'title']
|
||||||
|
FIELDS_CHANNELS = ['broadcastnow', 'channeltype', 'hidden', 'locked',
|
||||||
|
'lastplayed', 'thumbnail', 'channel']
|
||||||
|
|
||||||
|
FILTER_UNWATCHED = {
|
||||||
|
'operator': 'lessthan',
|
||||||
|
'field': 'playcount',
|
||||||
|
'value': '1'
|
||||||
|
}
|
||||||
|
FILTER_WATCHED = {
|
||||||
|
'operator': 'isnot',
|
||||||
|
'field': 'playcount',
|
||||||
|
'value': '0'
|
||||||
|
}
|
||||||
|
FILTER_RATING = {
|
||||||
|
'operator': 'greaterthan',
|
||||||
|
'field': 'rating',
|
||||||
|
'value': '7'
|
||||||
|
}
|
||||||
|
FILTER_RATING_MUSIC = {
|
||||||
|
'operator': 'greaterthan',
|
||||||
|
'field': 'rating',
|
||||||
|
'value': '3'
|
||||||
|
}
|
||||||
|
FILTER_INPROGRESS = {
|
||||||
|
'operator': 'true',
|
||||||
|
'field': 'inprogress',
|
||||||
|
'value': ''
|
||||||
|
}
|
||||||
|
SORT_RATING = {
|
||||||
|
'method': 'rating',
|
||||||
|
'order': 'descending'
|
||||||
|
}
|
||||||
|
SORT_RANDOM = {
|
||||||
|
'method': 'random',
|
||||||
|
'order': 'descending'
|
||||||
|
}
|
||||||
|
SORT_TITLE = {
|
||||||
|
'method': 'title',
|
||||||
|
'order': 'ascending'
|
||||||
|
}
|
||||||
|
SORT_DATEADDED = {
|
||||||
|
'method': 'dateadded',
|
||||||
|
'order': 'descending'
|
||||||
|
}
|
||||||
|
SORT_LASTPLAYED = {
|
||||||
|
'method': 'lastplayed',
|
||||||
|
'order': 'descending'
|
||||||
|
}
|
||||||
|
SORT_EPISODE = {
|
||||||
|
'method': 'episode'
|
||||||
|
}
|
|
@ -7,5 +7,4 @@ from .websocket import store_websocket_message, process_websocket_messages, \
|
||||||
WEBSOCKET_MESSAGES, PLAYSTATE_SESSIONS
|
WEBSOCKET_MESSAGES, PLAYSTATE_SESSIONS
|
||||||
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
||||||
from .fanart import FanartThread, FanartTask
|
from .fanart import FanartThread, FanartTask
|
||||||
from .videonodes import VideoNodes
|
from .sections import force_full_sync, delete_files, clear_window_vars
|
||||||
from .sections import force_full_sync
|
|
||||||
|
|
|
@ -166,14 +166,14 @@ class FullSync(common.fullsync_mixin):
|
||||||
app.SYNC.path_verified = False
|
app.SYNC.path_verified = False
|
||||||
try:
|
try:
|
||||||
# Sync new, updated and deleted items
|
# Sync new, updated and deleted items
|
||||||
iterator = section['iterator']
|
iterator = section.iterator
|
||||||
# Tell the processing thread about this new section
|
# Tell the processing thread about this new section
|
||||||
queue_info = InitNewSection(section['context'],
|
queue_info = InitNewSection(section.context,
|
||||||
iterator.total,
|
iterator.total,
|
||||||
iterator.get('librarySectionTitle',
|
iterator.get('librarySectionTitle',
|
||||||
iterator.get('title1')),
|
iterator.get('title1')),
|
||||||
section['section_id'],
|
section.section_id,
|
||||||
section['plex_type'])
|
section.plex_type)
|
||||||
self.queue.put(queue_info)
|
self.queue.put(queue_info)
|
||||||
last = True
|
last = True
|
||||||
# To keep track of the item-number in order to kill while loops
|
# To keep track of the item-number in order to kill while loops
|
||||||
|
@ -206,37 +206,37 @@ class FullSync(common.fullsync_mixin):
|
||||||
@utils.log_time
|
@utils.log_time
|
||||||
def playstate_per_section(self, section):
|
def playstate_per_section(self, section):
|
||||||
LOG.debug('Processing %s playstates for library section %s',
|
LOG.debug('Processing %s playstates for library section %s',
|
||||||
section['iterator'].total, section)
|
section.iterator.total, section)
|
||||||
try:
|
try:
|
||||||
# Sync new, updated and deleted items
|
# Sync new, updated and deleted items
|
||||||
iterator = section['iterator']
|
iterator = section.iterator
|
||||||
# Tell the processing thread about this new section
|
# Tell the processing thread about this new section
|
||||||
queue_info = InitNewSection(section['context'],
|
queue_info = InitNewSection(section.context,
|
||||||
iterator.total,
|
iterator.total,
|
||||||
section['section_name'],
|
section.name,
|
||||||
section['section_id'],
|
section.section_id,
|
||||||
section['plex_type'])
|
section.plex_type)
|
||||||
self.queue.put(queue_info)
|
self.queue.put(queue_info)
|
||||||
self.total = iterator.total
|
self.total = iterator.total
|
||||||
self.section_name = section['section_name']
|
self.section_name = section.name
|
||||||
self.section_type_text = utils.lang(
|
self.section_type_text = utils.lang(
|
||||||
v.TRANSLATION_FROM_PLEXTYPE[section['plex_type']])
|
v.TRANSLATION_FROM_PLEXTYPE[section.plex_type])
|
||||||
self.current = 0
|
self.current = 0
|
||||||
|
|
||||||
last = True
|
last = True
|
||||||
loop = common.tag_last(iterator)
|
loop = common.tag_last(iterator)
|
||||||
while True:
|
while True:
|
||||||
with section['context'](self.current_sync) as itemtype:
|
with section.context(self.current_sync) as itemtype:
|
||||||
for i, (last, xml_item) in enumerate(loop):
|
for i, (last, xml_item) in enumerate(loop):
|
||||||
if self.isCanceled():
|
if self.isCanceled():
|
||||||
return False
|
return False
|
||||||
if not itemtype.update_userdata(xml_item, section['plex_type']):
|
if not itemtype.update_userdata(xml_item, section.plex_type):
|
||||||
# Somehow did not sync this item yet
|
# Somehow did not sync this item yet
|
||||||
itemtype.add_update(xml_item,
|
itemtype.add_update(xml_item,
|
||||||
section_name=section['section_name'],
|
section_name=section.name,
|
||||||
section_id=section['section_id'])
|
section_id=section.section_id)
|
||||||
itemtype.plexdb.update_last_sync(int(xml_item.attrib['ratingKey']),
|
itemtype.plexdb.update_last_sync(int(xml_item.attrib['ratingKey']),
|
||||||
section['plex_type'],
|
section.plex_type,
|
||||||
self.current_sync)
|
self.current_sync)
|
||||||
self.current += 1
|
self.current += 1
|
||||||
self.update_progressbar()
|
self.update_progressbar()
|
||||||
|
@ -256,26 +256,25 @@ class FullSync(common.fullsync_mixin):
|
||||||
try:
|
try:
|
||||||
for kind in kinds:
|
for kind in kinds:
|
||||||
for section in (x for x in sections.SECTIONS
|
for section in (x for x in sections.SECTIONS
|
||||||
if x['plex_type'] == kind[1]):
|
if x.section_type == kind[1]):
|
||||||
if self.isCanceled():
|
if self.isCanceled():
|
||||||
return
|
return
|
||||||
if not section['sync_to_kodi']:
|
if not section.sync_to_kodi:
|
||||||
LOG.info('User chose to not sync section %s', section)
|
LOG.info('User chose to not sync section %s', section)
|
||||||
continue
|
continue
|
||||||
element = copy.deepcopy(section)
|
element = copy.deepcopy(section)
|
||||||
element['section_type'] = element['plex_type']
|
element.plex_type = kind[0]
|
||||||
element['plex_type'] = kind[0]
|
element.section_type = element.plex_type
|
||||||
element['element_type'] = kind[1]
|
element.context = kind[2]
|
||||||
element['context'] = kind[2]
|
element.get_children = kind[3]
|
||||||
element['get_children'] = kind[3]
|
|
||||||
if self.repair or all_items:
|
if self.repair or all_items:
|
||||||
updated_at = None
|
updated_at = None
|
||||||
else:
|
else:
|
||||||
updated_at = section['last_sync'] - UPDATED_AT_SAFETY \
|
updated_at = section.last_sync - UPDATED_AT_SAFETY \
|
||||||
if section['last_sync'] else None
|
if section.last_sync else None
|
||||||
try:
|
try:
|
||||||
element['iterator'] = PF.SectionItems(section['section_id'],
|
element.iterator = PF.SectionItems(section.section_id,
|
||||||
plex_type=kind[0],
|
plex_type=element.plex_type,
|
||||||
updated_at=updated_at,
|
updated_at=updated_at,
|
||||||
last_viewed_at=None)
|
last_viewed_at=None)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
|
@ -317,10 +316,10 @@ class FullSync(common.fullsync_mixin):
|
||||||
if section is None:
|
if section is None:
|
||||||
break
|
break
|
||||||
# Setup our variables
|
# Setup our variables
|
||||||
self.plex_type = section['plex_type']
|
self.plex_type = section.plex_type
|
||||||
self.section_type = section['section_type']
|
self.section_type = section.section_type
|
||||||
self.context = section['context']
|
self.context = section.context
|
||||||
self.get_children = section['get_children']
|
self.get_children = section.get_children
|
||||||
# Now do the heavy lifting
|
# Now do the heavy lifting
|
||||||
if self.isCanceled() or not self.addupdate_section(section):
|
if self.isCanceled() or not self.addupdate_section(section):
|
||||||
return False
|
return False
|
||||||
|
@ -329,7 +328,7 @@ class FullSync(common.fullsync_mixin):
|
||||||
# some items from the PMS
|
# some items from the PMS
|
||||||
with PlexDB() as plexdb:
|
with PlexDB() as plexdb:
|
||||||
# Set the new time mark for the next delta sync
|
# Set the new time mark for the next delta sync
|
||||||
plexdb.update_section_last_sync(section['section_id'],
|
plexdb.update_section_last_sync(section.section_id,
|
||||||
self.current_sync)
|
self.current_sync)
|
||||||
common.update_kodi_library(video=True, music=True)
|
common.update_kodi_library(video=True, music=True)
|
||||||
# In order to not delete all your songs again
|
# In order to not delete all your songs again
|
||||||
|
@ -361,10 +360,10 @@ class FullSync(common.fullsync_mixin):
|
||||||
if section is None:
|
if section is None:
|
||||||
break
|
break
|
||||||
# Setup our variables
|
# Setup our variables
|
||||||
self.plex_type = section['plex_type']
|
self.plex_type = section.plex_type
|
||||||
self.section_type = section['section_type']
|
self.section_type = section.section_type
|
||||||
self.context = section['context']
|
self.context = section.context
|
||||||
self.get_children = section['get_children']
|
self.get_children = section.get_children
|
||||||
# Now do the heavy lifting
|
# Now do the heavy lifting
|
||||||
if self.isCanceled() or not self.playstate_per_section(section):
|
if self.isCanceled() or not self.playstate_per_section(section):
|
||||||
return False
|
return False
|
||||||
|
@ -410,9 +409,6 @@ class FullSync(common.fullsync_mixin):
|
||||||
@utils.log_time
|
@utils.log_time
|
||||||
def _run(self):
|
def _run(self):
|
||||||
self.current_sync = timing.plex_now()
|
self.current_sync = timing.plex_now()
|
||||||
# Delete playlist and video node files from Kodi
|
|
||||||
utils.delete_playlists()
|
|
||||||
utils.delete_nodes()
|
|
||||||
# Get latest Plex libraries and build playlist and video node files
|
# Get latest Plex libraries and build playlist and video node files
|
||||||
if not sections.sync_from_pms(self):
|
if not sections.sync_from_pms(self):
|
||||||
return
|
return
|
||||||
|
|
373
resources/lib/library_sync/nodes.py
Normal file
373
resources/lib/library_sync/nodes.py
Normal file
|
@ -0,0 +1,373 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
from ..utils import etree
|
||||||
|
from .. import variables as v, utils
|
||||||
|
|
||||||
|
ICON_PATH = 'special://home/addons/plugin.video.plexkodiconnect/icon.png'
|
||||||
|
RECOMMENDED_SCORE_LOWER_BOUND = 7
|
||||||
|
|
||||||
|
# Logic of the following nodes:
|
||||||
|
# (node_type,
|
||||||
|
# label/node name,
|
||||||
|
# args for PKC add-on callback,
|
||||||
|
# Kodi "content",
|
||||||
|
# Bool: does this node's xml even point back to PKC add-on callback?
|
||||||
|
# )
|
||||||
|
NODE_TYPES = {
|
||||||
|
v.PLEX_TYPE_MOVIE: (
|
||||||
|
('ondeck',
|
||||||
|
utils.lang(39500), # "On Deck"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/onDeck',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'movies',
|
||||||
|
True),
|
||||||
|
('recent',
|
||||||
|
utils.lang(30174), # "Recently Added"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/recentlyAdded',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'movies',
|
||||||
|
False),
|
||||||
|
('all',
|
||||||
|
'{self.name}', # We're using this section's name
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/all',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'movies',
|
||||||
|
False),
|
||||||
|
('recommended',
|
||||||
|
utils.lang(30230), # "Recommended"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': ('/library/sections/{self.section_id}&%s'
|
||||||
|
% urllib.urlencode({'sort': 'rating:desc'})),
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'movies',
|
||||||
|
False),
|
||||||
|
('genres',
|
||||||
|
utils.lang(135), # "Genres"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/genre',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'movies',
|
||||||
|
False),
|
||||||
|
('sets',
|
||||||
|
utils.lang(39501), # "Collections"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/collection',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'movies',
|
||||||
|
False),
|
||||||
|
('random',
|
||||||
|
utils.lang(30227), # "Random"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': ('/library/sections/{self.section_id}&%s'
|
||||||
|
% urllib.urlencode({'sort': 'random'})),
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'movies',
|
||||||
|
False),
|
||||||
|
('lastplayed',
|
||||||
|
utils.lang(568), # "Last played"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/recentlyViewed',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'movies',
|
||||||
|
False),
|
||||||
|
('browse',
|
||||||
|
utils.lang(39702), # "Browse by folder"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/folder',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'movies',
|
||||||
|
True),
|
||||||
|
('more',
|
||||||
|
utils.lang(22082), # "More..."
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'movies',
|
||||||
|
True),
|
||||||
|
),
|
||||||
|
###########################################################
|
||||||
|
v.PLEX_TYPE_SHOW: (
|
||||||
|
('ondeck',
|
||||||
|
utils.lang(39500), # "On Deck"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/onDeck',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'episodes',
|
||||||
|
True),
|
||||||
|
('recent',
|
||||||
|
utils.lang(30174), # "Recently Added"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/recentlyAdded',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'episodes',
|
||||||
|
False),
|
||||||
|
('all',
|
||||||
|
'{self.name}', # We're using this section's name
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/all',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'tvshows',
|
||||||
|
False),
|
||||||
|
('recommended',
|
||||||
|
utils.lang(30230), # "Recommended"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': ('/library/sections/{self.section_id}&%s'
|
||||||
|
% urllib.urlencode({'sort': 'rating:desc'})),
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'tvshows',
|
||||||
|
False),
|
||||||
|
('genres',
|
||||||
|
utils.lang(135), # "Genres"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/genre',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'tvshows',
|
||||||
|
False),
|
||||||
|
('sets',
|
||||||
|
utils.lang(39501), # "Collections"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/collection',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'tvshows',
|
||||||
|
True), # There are no sets/collections for shows with Kodi
|
||||||
|
('random',
|
||||||
|
utils.lang(30227), # "Random"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': ('/library/sections/{self.section_id}&%s'
|
||||||
|
% urllib.urlencode({'sort': 'random'})),
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'tvshows',
|
||||||
|
False),
|
||||||
|
('lastplayed',
|
||||||
|
utils.lang(568), # "Last played"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': ('/library/sections/{self.section_id}/recentlyViewed&%s'
|
||||||
|
% urllib.urlencode({'type': v.PLEX_TYPE_NUMBER_FROM_PLEX_TYPE[v.PLEX_TYPE_EPISODE]})),
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'episodes',
|
||||||
|
False),
|
||||||
|
('browse',
|
||||||
|
utils.lang(39702), # "Browse by folder"
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}/folder',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'episodes',
|
||||||
|
True),
|
||||||
|
('more',
|
||||||
|
utils.lang(22082), # "More..."
|
||||||
|
{
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/{self.section_id}',
|
||||||
|
'plex_type': '{self.section_type}',
|
||||||
|
'section_id': '{self.section_id}'
|
||||||
|
},
|
||||||
|
'episodes',
|
||||||
|
True),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def node_pms(section, node_name, args):
|
||||||
|
"""
|
||||||
|
Nodes where the logic resides with the PMS - we're NOT building an
|
||||||
|
xml that filters and sorts, but point to PKC add-on path
|
||||||
|
"""
|
||||||
|
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||||
|
'type': 'folder'})
|
||||||
|
etree.SubElement(xml, 'label').text = node_name
|
||||||
|
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||||
|
etree.SubElement(xml, 'content').text = section.content
|
||||||
|
etree.SubElement(xml, 'path').text = section.addon_path(args)
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def node_recent(section, node_name):
|
||||||
|
xml = etree.Element('node',
|
||||||
|
attrib={'order': unicode(section.order),
|
||||||
|
'type': 'filter'})
|
||||||
|
etree.SubElement(xml, 'match').text = 'all'
|
||||||
|
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||||
|
'operator': 'is'})
|
||||||
|
etree.SubElement(rule, 'value').text = section.name
|
||||||
|
etree.SubElement(xml, 'label').text = node_name
|
||||||
|
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||||
|
etree.SubElement(xml, 'content').text = section.content
|
||||||
|
etree.SubElement(xml,
|
||||||
|
'order',
|
||||||
|
attrib={'direction':
|
||||||
|
'descending'}).text = 'dateadded'
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def node_all(section, node_name):
|
||||||
|
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||||
|
'type': 'filter'})
|
||||||
|
etree.SubElement(xml, 'match').text = 'all'
|
||||||
|
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||||
|
'operator': 'is'})
|
||||||
|
etree.SubElement(rule, 'value').text = section.name
|
||||||
|
etree.SubElement(xml, 'label').text = node_name
|
||||||
|
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||||
|
etree.SubElement(xml, 'content').text = section.content
|
||||||
|
etree.SubElement(xml,
|
||||||
|
'order',
|
||||||
|
attrib={'direction':
|
||||||
|
'ascending'}).text = 'sorttitle'
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def node_recommended(section, node_name):
|
||||||
|
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||||
|
'type': 'filter'})
|
||||||
|
etree.SubElement(xml, 'match').text = 'all'
|
||||||
|
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||||
|
'operator': 'is'})
|
||||||
|
etree.SubElement(rule, 'value').text = section.name
|
||||||
|
# rule = etree.SubElement(xml, 'rule', attrib={'field': 'rating',
|
||||||
|
# 'operator': 'greaterthan'})
|
||||||
|
# etree.SubElement(rule, 'value').text = unicode(RECOMMENDED_SCORE_LOWER_BOUND)
|
||||||
|
etree.SubElement(xml, 'label').text = node_name
|
||||||
|
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||||
|
etree.SubElement(xml, 'content').text = section.content
|
||||||
|
etree.SubElement(xml,
|
||||||
|
'order',
|
||||||
|
attrib={'direction':
|
||||||
|
'descending'}).text = 'rating'
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def node_genres(section, node_name):
|
||||||
|
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||||
|
'type': 'filter'})
|
||||||
|
etree.SubElement(xml, 'match').text = 'all'
|
||||||
|
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||||
|
'operator': 'is'})
|
||||||
|
etree.SubElement(rule, 'value').text = section.name
|
||||||
|
etree.SubElement(xml, 'label').text = node_name
|
||||||
|
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||||
|
etree.SubElement(xml, 'content').text = section.content
|
||||||
|
etree.SubElement(xml,
|
||||||
|
'order',
|
||||||
|
attrib={'direction':
|
||||||
|
'ascending'}).text = 'sorttitle'
|
||||||
|
etree.SubElement(xml, 'group').text = 'genres'
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def node_sets(section, node_name):
|
||||||
|
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||||
|
'type': 'filter'})
|
||||||
|
etree.SubElement(xml, 'match').text = 'all'
|
||||||
|
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||||
|
'operator': 'is'})
|
||||||
|
etree.SubElement(rule, 'value').text = section.name
|
||||||
|
# "Collections"
|
||||||
|
etree.SubElement(xml, 'label').text = node_name
|
||||||
|
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||||
|
etree.SubElement(xml, 'content').text = section.content
|
||||||
|
etree.SubElement(xml,
|
||||||
|
'order',
|
||||||
|
attrib={'direction':
|
||||||
|
'ascending'}).text = 'sorttitle'
|
||||||
|
etree.SubElement(xml, 'group').text = 'sets'
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def node_random(section, node_name):
|
||||||
|
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||||
|
'type': 'filter'})
|
||||||
|
etree.SubElement(xml, 'match').text = 'all'
|
||||||
|
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||||
|
'operator': 'is'})
|
||||||
|
etree.SubElement(rule, 'value').text = section.name
|
||||||
|
etree.SubElement(xml, 'label').text = node_name
|
||||||
|
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||||
|
etree.SubElement(xml, 'content').text = section.content
|
||||||
|
etree.SubElement(xml,
|
||||||
|
'order',
|
||||||
|
attrib={'direction':
|
||||||
|
'ascending'}).text = 'random'
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def node_lastplayed(section, node_name):
|
||||||
|
xml = etree.Element('node', attrib={'order': unicode(section.order),
|
||||||
|
'type': 'filter'})
|
||||||
|
etree.SubElement(xml, 'match').text = 'all'
|
||||||
|
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||||
|
'operator': 'is'})
|
||||||
|
etree.SubElement(rule, 'value').text = section.name
|
||||||
|
rule = etree.SubElement(xml, 'rule', attrib={'field': 'playcount',
|
||||||
|
'operator': 'greaterthan'})
|
||||||
|
etree.SubElement(rule, 'value').text = 0
|
||||||
|
etree.SubElement(xml, 'label').text = node_name
|
||||||
|
etree.SubElement(xml, 'icon').text = ICON_PATH
|
||||||
|
etree.SubElement(xml, 'content').text = section.content
|
||||||
|
etree.SubElement(xml,
|
||||||
|
'order',
|
||||||
|
attrib={'direction':
|
||||||
|
'descending'}).text = 'lastplayed'
|
||||||
|
return xml
|
|
@ -2,25 +2,404 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
import urllib
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from . import videonodes
|
from . import nodes
|
||||||
from ..utils import cast
|
|
||||||
from ..plex_db import PlexDB
|
from ..plex_db import PlexDB
|
||||||
|
from ..plex_api import API
|
||||||
from .. import kodi_db
|
from .. import kodi_db
|
||||||
from .. import itemtypes
|
from .. import itemtypes, path_ops
|
||||||
from .. import plex_functions as PF, music, utils, variables as v, app
|
from .. import plex_functions as PF, music, utils, variables as v, app
|
||||||
|
from ..utils import etree
|
||||||
|
|
||||||
LOG = getLogger('PLEX.sync.sections')
|
LOG = getLogger('PLEX.sync.sections')
|
||||||
|
|
||||||
BATCH_SIZE = 500
|
BATCH_SIZE = 500
|
||||||
VNODES = videonodes.VideoNodes()
|
|
||||||
PLAYLISTS = {}
|
|
||||||
NODES = {}
|
|
||||||
SECTIONS = []
|
SECTIONS = []
|
||||||
# Need a way to interrupt
|
# Need a way to interrupt our synching process
|
||||||
IS_CANCELED = None
|
IS_CANCELED = None
|
||||||
|
|
||||||
|
LIBRARY_PATH = path_ops.translate_path('special://profile/library/video/')
|
||||||
|
# The video library might not yet exist for this user - create it
|
||||||
|
if not path_ops.exists(LIBRARY_PATH):
|
||||||
|
path_ops.copy_tree(
|
||||||
|
src=path_ops.translate_path('special://xbmc/system/library/video'),
|
||||||
|
dst=LIBRARY_PATH,
|
||||||
|
preserve_mode=0) # dont copy permission bits so we have write access!
|
||||||
|
PLAYLISTS_PATH = path_ops.translate_path("special://profile/playlists/video/")
|
||||||
|
if not path_ops.exists(PLAYLISTS_PATH):
|
||||||
|
path_ops.makedirs(PLAYLISTS_PATH)
|
||||||
|
|
||||||
|
# Windows variables we set for each node
|
||||||
|
WINDOW_ARGS = ('index', 'title', 'id', 'path', 'type', 'content', 'artwork')
|
||||||
|
|
||||||
|
|
||||||
|
class Section(object):
|
||||||
|
"""
|
||||||
|
Setting the attribute section_type will automatically set content and
|
||||||
|
sync_to_kodi
|
||||||
|
"""
|
||||||
|
def __init__(self, index=None, xml_element=None, section_db_element=None):
|
||||||
|
# Unique Plex id of this Plex library section
|
||||||
|
self._section_id = None # int
|
||||||
|
# Building block for window variable
|
||||||
|
self._node = None # unicode
|
||||||
|
# Index of this section (as section_id might not be subsequent)
|
||||||
|
# This follows 1:1 the sequence in with the PMS returns the sections
|
||||||
|
self._index = None # Codacy-bug
|
||||||
|
self.index = index # int
|
||||||
|
# This section's name for the user to display
|
||||||
|
self.name = None # unicode
|
||||||
|
# Library type section (NOT the same as the KODI_TYPE_...)
|
||||||
|
# E.g. 'movies', 'tvshows', 'episodes'
|
||||||
|
self.content = None # unicode
|
||||||
|
# Setting the section_type WILL re_set sync_to_kodi!
|
||||||
|
self._section_type = None # unicode
|
||||||
|
# Do we sync all items of this section to the Kodi DB?
|
||||||
|
# This will be set with section_type!!
|
||||||
|
self.sync_to_kodi = None # bool
|
||||||
|
# For sections to be synched, the section name will be recorded as a
|
||||||
|
# tag. This is the corresponding id for this tag
|
||||||
|
self.kodi_tagid = None # int
|
||||||
|
# When was this section last successfully/completely synched to the
|
||||||
|
# Kodi database?
|
||||||
|
self.last_sync = None # int
|
||||||
|
# Path to the Kodi userdata library FOLDER for this section
|
||||||
|
self._path = None # unicode
|
||||||
|
# Path to the smart playlist for this section
|
||||||
|
self._playlist_path = None
|
||||||
|
# "Poster" for this section
|
||||||
|
self.icon = None # unicode
|
||||||
|
# Background image for this section
|
||||||
|
self.artwork = None
|
||||||
|
# Thumbnail for this section, similar for each section type
|
||||||
|
self.thumb = None
|
||||||
|
# Order number in which xmls will be listed inside Kodei
|
||||||
|
self.order = None
|
||||||
|
# Original PMS xml for this section, including children
|
||||||
|
self.xml = None
|
||||||
|
# Attributes that will be initialized later by full_sync.py
|
||||||
|
self.iterator = None
|
||||||
|
self.context = None
|
||||||
|
self.get_children = None
|
||||||
|
# A section_type encompasses possible several plex_types! E.g. shows
|
||||||
|
# contain shows, seasons, episodes
|
||||||
|
self.plex_type = None
|
||||||
|
if xml_element is not None:
|
||||||
|
self.from_xml(xml_element)
|
||||||
|
elif section_db_element:
|
||||||
|
self.from_db_element(section_db_element)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ("{{"
|
||||||
|
"'index': {self.index}, "
|
||||||
|
"'name': '{self.name}', "
|
||||||
|
"'section_id': {self.section_id}, "
|
||||||
|
"'section_type': '{self.section_type}', "
|
||||||
|
"'sync_to_kodi': {self.sync_to_kodi}, "
|
||||||
|
"'last_sync': {self.last_sync}"
|
||||||
|
"}}").format(self=self)
|
||||||
|
__str__ = __repr__
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
return (self.section_id is not None and
|
||||||
|
self.name is not None and
|
||||||
|
self.section_type is not None)
|
||||||
|
|
||||||
|
def __eq__(self, section):
|
||||||
|
return (self.section_id == section.section_id and
|
||||||
|
self.name == section.name and
|
||||||
|
self.section_type == section.section_type)
|
||||||
|
__ne__ = not __eq__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def section_id(self):
|
||||||
|
return self._section_id
|
||||||
|
|
||||||
|
@section_id.setter
|
||||||
|
def section_id(self, value):
|
||||||
|
self._section_id = value
|
||||||
|
self._path = path_ops.path.join(LIBRARY_PATH, 'Plex-%s' % value, '')
|
||||||
|
self._playlist_path = path_ops.path.join(PLAYLISTS_PATH,
|
||||||
|
'Plex %s.xsp' % value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def section_type(self):
|
||||||
|
return self._section_type
|
||||||
|
|
||||||
|
@section_type.setter
|
||||||
|
def section_type(self, value):
|
||||||
|
self._section_type = value
|
||||||
|
self.content = v.MEDIATYPE_FROM_PLEX_TYPE[value]
|
||||||
|
# Default values whether we sync or not based on the Plex type
|
||||||
|
if value == v.PLEX_TYPE_PHOTO:
|
||||||
|
self.sync_to_kodi = False
|
||||||
|
elif not app.SYNC.enable_music and value == v.PLEX_TYPE_ARTIST:
|
||||||
|
self.sync_to_kodi = False
|
||||||
|
else:
|
||||||
|
self.sync_to_kodi = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def index(self):
|
||||||
|
return self._index
|
||||||
|
|
||||||
|
@index.setter
|
||||||
|
def index(self, value):
|
||||||
|
self._index = value
|
||||||
|
self._node = 'Plex.nodes.%s' % value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def node(self):
|
||||||
|
return self._node
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return self._path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def playlist_path(self):
|
||||||
|
return self._playlist_path
|
||||||
|
|
||||||
|
def from_db_element(self, section_db_element):
|
||||||
|
self.section_id = section_db_element['section_id']
|
||||||
|
self.name = section_db_element['section_name']
|
||||||
|
self.section_type = section_db_element['plex_type']
|
||||||
|
self.kodi_tagid = section_db_element['kodi_tagid']
|
||||||
|
self.sync_to_kodi = section_db_element['sync_to_kodi']
|
||||||
|
self.last_sync = section_db_element['last_sync']
|
||||||
|
|
||||||
|
def from_xml(self, xml_element):
|
||||||
|
"""
|
||||||
|
Reads section from a PMS xml (Plex id, name, Plex type)
|
||||||
|
"""
|
||||||
|
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.icon = api.one_artwork('composite')
|
||||||
|
self.artwork = api.one_artwork('art')
|
||||||
|
self.thumb = api.one_artwork('thumb')
|
||||||
|
self.xml = xml_element
|
||||||
|
|
||||||
|
def from_plex_db(self, section_id, plexdb=None):
|
||||||
|
"""
|
||||||
|
Reads section with id section_id from the plex.db
|
||||||
|
"""
|
||||||
|
if plexdb:
|
||||||
|
section = plexdb.section(section_id)
|
||||||
|
else:
|
||||||
|
with PlexDB(lock=False) as plexdb:
|
||||||
|
section = plexdb.section(section_id)
|
||||||
|
if section:
|
||||||
|
self.from_db_element(section)
|
||||||
|
|
||||||
|
def to_plex_db(self, plexdb=None):
|
||||||
|
"""
|
||||||
|
Writes this Section to the plex.db, potentially overwriting
|
||||||
|
(INSERT OR REPLACE)
|
||||||
|
"""
|
||||||
|
if not self:
|
||||||
|
raise RuntimeError('Section not clearly defined: %s' % self)
|
||||||
|
if plexdb:
|
||||||
|
plexdb.add_section(self.section_id,
|
||||||
|
self.name,
|
||||||
|
self.section_type,
|
||||||
|
self.kodi_tagid,
|
||||||
|
self.sync_to_kodi,
|
||||||
|
self.last_sync)
|
||||||
|
else:
|
||||||
|
with PlexDB(lock=False) as plexdb:
|
||||||
|
plexdb.add_section(self.section_id,
|
||||||
|
self.name,
|
||||||
|
self.section_type,
|
||||||
|
self.kodi_tagid,
|
||||||
|
self.sync_to_kodi,
|
||||||
|
self.last_sync)
|
||||||
|
|
||||||
|
def addon_path(self, args):
|
||||||
|
"""
|
||||||
|
Returns the plugin path pointing back to PKC for key in order to browse
|
||||||
|
args is a dict. Its values may contain string info of the form
|
||||||
|
{key: '{self.<Section attribute>}'}
|
||||||
|
"""
|
||||||
|
args = copy.deepcopy(args)
|
||||||
|
for key, value in args.iteritems():
|
||||||
|
args[key] = value.format(self=self)
|
||||||
|
return 'plugin://plugin.video.plexkodiconnect?%s' % urllib.urlencode(args)
|
||||||
|
|
||||||
|
def to_kodi(self):
|
||||||
|
"""
|
||||||
|
Writes this section's nodes to the library folder in the Kodi userdata
|
||||||
|
directory
|
||||||
|
Won't do anything if self.sync_to_kodi is not True
|
||||||
|
"""
|
||||||
|
if self.index is None:
|
||||||
|
raise RuntimeError('Index not initialized')
|
||||||
|
# Main list entry for this section - which will show the different
|
||||||
|
# nodes as "submenus" once the user navigates into this section
|
||||||
|
args = {
|
||||||
|
'mode': 'browseplex',
|
||||||
|
'key': '/library/sections/%s' % self.section_id,
|
||||||
|
'plex_type': self.section_type,
|
||||||
|
'section_id': unicode(self.section_id)
|
||||||
|
}
|
||||||
|
if not self.sync_to_kodi:
|
||||||
|
args['synched'] = 'false'
|
||||||
|
addon_path = self.addon_path(args)
|
||||||
|
if self.sync_to_kodi and self.section_type in v.PLEX_VIDEOTYPES:
|
||||||
|
path = 'library://video/Plex-%s' % self.section_id
|
||||||
|
else:
|
||||||
|
# No xmls to link to - let's show the listings on the fly
|
||||||
|
path = addon_path
|
||||||
|
utils.window('%s.title' % self.node, value=self.name)
|
||||||
|
utils.window('%s.type' % self.node, value=self.content)
|
||||||
|
utils.window('%s.content' % self.node, value=path)
|
||||||
|
utils.window('%s.path' % self.node,
|
||||||
|
value='ActivateWindow(Videos,%s,return)' % path)
|
||||||
|
utils.window('%s.id' % self.node, value=str(self.section_id))
|
||||||
|
# To let the user navigate into this node when selecting widgets
|
||||||
|
utils.window('%s.addon_path' % self.node, value=addon_path)
|
||||||
|
if not self.sync_to_kodi:
|
||||||
|
self.remove_files_from_kodi()
|
||||||
|
return
|
||||||
|
if self.section_type == v.PLEX_TYPE_ARTIST:
|
||||||
|
# Todo: Write window variables for music
|
||||||
|
return
|
||||||
|
if self.section_type == v.PLEX_TYPE_PHOTO:
|
||||||
|
# Todo: Write window variables for photos
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create a dedicated directory for this section
|
||||||
|
if not path_ops.exists(self.path):
|
||||||
|
path_ops.makedirs(self.path)
|
||||||
|
# Create a tag just like the section name in the Kodi DB
|
||||||
|
with kodi_db.KodiVideoDB(lock=False) as kodidb:
|
||||||
|
self.kodi_tagid = kodidb.create_tag(self.name)
|
||||||
|
# The xmls are numbered in order of appearance
|
||||||
|
self.order = 0
|
||||||
|
if not path_ops.exists(path_ops.path.join(self.path, 'index.xml')):
|
||||||
|
LOG.debug('Creating index.xml for section %s', self.name)
|
||||||
|
xml = etree.Element('node',
|
||||||
|
attrib={'order': unicode(self.order)})
|
||||||
|
etree.SubElement(xml, 'label').text = self.name
|
||||||
|
etree.SubElement(xml, 'icon').text = self.icon or nodes.ICON_PATH
|
||||||
|
self._write_xml(xml, 'index.xml')
|
||||||
|
self.order += 1
|
||||||
|
# Create the one smart playlist for this section
|
||||||
|
if not path_ops.exists(self.playlist_path):
|
||||||
|
self._write_playlist()
|
||||||
|
# Now build all nodes for this section - potentially creating xmls
|
||||||
|
for node in nodes.NODE_TYPES[self.section_type]:
|
||||||
|
self._build_node(*node)
|
||||||
|
|
||||||
|
def _build_node(self, node_type, node_name, args, content, pms_node):
|
||||||
|
self.content = content
|
||||||
|
node_name = node_name.format(self=self)
|
||||||
|
xml_name = '%s_%s.xml' % (self.section_id, node_type)
|
||||||
|
path = path_ops.path.join(self.path, xml_name)
|
||||||
|
if not path_ops.exists(path):
|
||||||
|
if pms_node:
|
||||||
|
# Even the xml will point back to the PKC add-on
|
||||||
|
xml = nodes.node_pms(self, node_name, args)
|
||||||
|
else:
|
||||||
|
# Let's use Kodi's logic to sort/filter the Kodi library
|
||||||
|
xml = getattr(nodes, 'node_%s' % node_type)(self, node_name)
|
||||||
|
self._write_xml(xml, xml_name)
|
||||||
|
self.order += 1
|
||||||
|
self._window_node(path, node_name, node_type)
|
||||||
|
|
||||||
|
def _write_xml(self, xml, xml_name):
|
||||||
|
LOG.debug('Creating xml for section %s: %s', self.name, xml_name)
|
||||||
|
utils.indent(xml)
|
||||||
|
etree.ElementTree(xml).write(path_ops.path.join(self.path, xml_name),
|
||||||
|
encoding='utf-8',
|
||||||
|
xml_declaration=True)
|
||||||
|
|
||||||
|
def _write_playlist(self):
|
||||||
|
LOG.debug('Creating smart playlist for section %s: %s',
|
||||||
|
self.name, self.playlist_path)
|
||||||
|
xml = etree.Element('smartplaylist',
|
||||||
|
attrib={'type': v.MEDIATYPE_FROM_PLEX_TYPE[self.section_type]})
|
||||||
|
etree.SubElement(xml, 'name').text = self.name
|
||||||
|
etree.SubElement(xml, 'match').text = 'all'
|
||||||
|
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
|
||||||
|
'operator': 'is'})
|
||||||
|
etree.SubElement(rule, 'value').text = self.name
|
||||||
|
utils.indent(xml)
|
||||||
|
etree.ElementTree(xml).write(self.playlist_path, encoding='utf-8')
|
||||||
|
|
||||||
|
def _window_node(self, path, node_name, node_type):
|
||||||
|
"""
|
||||||
|
Will save this section's node to the Kodi window variables
|
||||||
|
|
||||||
|
Uses the same conventions/logic as Emby for Kodi does
|
||||||
|
"""
|
||||||
|
if self.section_type == v.PLEX_TYPE_ARTIST:
|
||||||
|
window_path = 'ActivateWindow(Music,%s,return)' % path
|
||||||
|
elif self.section_type == v.PLEX_TYPE_PHOTO:
|
||||||
|
# Check: elif node_type in ('browse', 'homevideos', 'photos'):
|
||||||
|
window_path = path
|
||||||
|
else:
|
||||||
|
window_path = 'ActivateWindow(Videos,%s,return)' % path
|
||||||
|
# if node_type == 'all':
|
||||||
|
# var = self.node
|
||||||
|
# utils.window('%s.index' % var,
|
||||||
|
# value=path.replace('%s_all.xml' % self.section_id, ''))
|
||||||
|
# utils.window('%s.title' % var, value=self.name)
|
||||||
|
# else:
|
||||||
|
var = '%s.%s' % (self.node, node_type)
|
||||||
|
utils.window('%s.index' % var, value=path)
|
||||||
|
utils.window('%s.title' % var, value=node_name)
|
||||||
|
utils.window('%s.id' % var, value=str(self.section_id))
|
||||||
|
utils.window('%s.path' % var, value=window_path)
|
||||||
|
utils.window('%s.type' % var, value=self.content)
|
||||||
|
utils.window('%s.content' % var, value=path)
|
||||||
|
utils.window('%s.artwork' % var, value=self.artwork)
|
||||||
|
|
||||||
|
def remove_files_from_kodi(self):
|
||||||
|
"""
|
||||||
|
Removes this sections from the Kodi userdata library folder (if appl.)
|
||||||
|
Also removes the smart playlist
|
||||||
|
"""
|
||||||
|
if self.section_type in (v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_PHOTO):
|
||||||
|
# No files created for these types
|
||||||
|
return
|
||||||
|
if path_ops.exists(self.path):
|
||||||
|
path_ops.rmtree(self.path, ignore_errors=True)
|
||||||
|
if path_ops.exists(self.playlist_path):
|
||||||
|
try:
|
||||||
|
path_ops.remove(self.playlist_path)
|
||||||
|
except (OSError, IOError):
|
||||||
|
LOG.warn('Could not delete smart playlist for section %s: %s',
|
||||||
|
self.name, self.playlist_path)
|
||||||
|
|
||||||
|
def remove_window_vars(self):
|
||||||
|
"""
|
||||||
|
Removes all windows variables 'Plex.nodes.<section_id>.xxx'
|
||||||
|
"""
|
||||||
|
if self.index is not None:
|
||||||
|
_clear_window_vars(self.index)
|
||||||
|
|
||||||
|
def remove_from_plex(self, plexdb=None):
|
||||||
|
"""
|
||||||
|
Removes this sections completely from the Plex DB
|
||||||
|
"""
|
||||||
|
if plexdb:
|
||||||
|
plexdb.remove_section(self.section_id)
|
||||||
|
else:
|
||||||
|
with PlexDB(lock=False) as plexdb:
|
||||||
|
plexdb.remove_section(self.section_id)
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
"""
|
||||||
|
Completely and utterly removes this section from Kodi and Plex DB
|
||||||
|
as well as from the window variables
|
||||||
|
"""
|
||||||
|
self.remove_files_from_kodi()
|
||||||
|
self.remove_window_vars()
|
||||||
|
self.remove_from_plex()
|
||||||
|
|
||||||
|
|
||||||
def force_full_sync():
|
def force_full_sync():
|
||||||
"""
|
"""
|
||||||
|
@ -32,193 +411,42 @@ def force_full_sync():
|
||||||
plexdb.force_full_sync()
|
plexdb.force_full_sync()
|
||||||
|
|
||||||
|
|
||||||
def sync_from_pms(parent_self):
|
def _save_sections_to_plex_db(sections):
|
||||||
|
with PlexDB() as plexdb:
|
||||||
|
for section in sections:
|
||||||
|
section.to_plex_db(plexdb=plexdb)
|
||||||
|
|
||||||
|
|
||||||
|
def _retrieve_old_settings(sections, old_sections):
|
||||||
"""
|
"""
|
||||||
Sync the Plex library sections
|
Overwrites the PKC settings for sections, grabing them from old_sections
|
||||||
|
if a particular section is in both sections and old_sections
|
||||||
|
|
||||||
|
Thus sets to the old values:
|
||||||
|
section.last_sync
|
||||||
|
section.kodi_tagid
|
||||||
|
section.sync_to_kodi
|
||||||
|
section.last_sync
|
||||||
"""
|
"""
|
||||||
global IS_CANCELED
|
for section in sections:
|
||||||
IS_CANCELED = parent_self.isCanceled
|
for old_section in old_sections:
|
||||||
try:
|
if section == old_section:
|
||||||
return _sync_from_pms()
|
section.last_sync = old_section.last_sync
|
||||||
finally:
|
section.kodi_tagid = old_section.kodi_tagid
|
||||||
IS_CANCELED = None
|
section.sync_to_kodi = old_section.sync_to_kodi
|
||||||
|
section.last_sync = old_section.last_sync
|
||||||
|
|
||||||
|
|
||||||
def _sync_from_pms():
|
def _delete_kodi_db_items(section):
|
||||||
global PLAYLISTS, NODES, SECTIONS
|
if section.section_type == v.PLEX_TYPE_MOVIE:
|
||||||
sections = PF.get_plex_sections()
|
|
||||||
try:
|
|
||||||
sections.attrib
|
|
||||||
except AttributeError:
|
|
||||||
LOG.error("Error download PMS sections, abort")
|
|
||||||
return False
|
|
||||||
if app.SYNC.direct_paths is True and app.SYNC.enable_music is True:
|
|
||||||
# Will reboot Kodi is new library detected
|
|
||||||
music.excludefromscan_music_folders(xml=sections)
|
|
||||||
|
|
||||||
VNODES.clearProperties()
|
|
||||||
SECTIONS = []
|
|
||||||
NODES = {
|
|
||||||
v.PLEX_TYPE_MOVIE: [],
|
|
||||||
v.PLEX_TYPE_SHOW: [],
|
|
||||||
v.PLEX_TYPE_ARTIST: [],
|
|
||||||
v.PLEX_TYPE_PHOTO: []
|
|
||||||
}
|
|
||||||
PLAYLISTS = copy.deepcopy(NODES)
|
|
||||||
with PlexDB() as plexdb:
|
|
||||||
# Backup old sections to delete them later, if needed (at the end
|
|
||||||
# of this method, only unused sections will be left in old_sections)
|
|
||||||
old_sections = list(plexdb.all_sections())
|
|
||||||
with kodi_db.KodiVideoDB() as kodidb:
|
|
||||||
for index, section in enumerate(sections):
|
|
||||||
_process_section(section,
|
|
||||||
kodidb,
|
|
||||||
plexdb,
|
|
||||||
index,
|
|
||||||
old_sections)
|
|
||||||
if old_sections:
|
|
||||||
# Section has been deleted on the PMS
|
|
||||||
delete_sections(old_sections)
|
|
||||||
# update sections for all:
|
|
||||||
with PlexDB() as plexdb:
|
|
||||||
SECTIONS = list(plexdb.all_sections())
|
|
||||||
utils.window('Plex.nodes.total', str(len(sections)))
|
|
||||||
LOG.info("Finished processing %s library sections: %s", len(sections), SECTIONS)
|
|
||||||
if app.CONN.machine_identifier != utils.settings('sections_asked_for_machine_identifier'):
|
|
||||||
LOG.info('First time connecting to this PMS, choosing libraries')
|
|
||||||
if choose_libraries():
|
|
||||||
with PlexDB() as plexdb:
|
|
||||||
SECTIONS = list(plexdb.all_sections())
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _process_section(section_xml, kodidb, plexdb, index, old_sections):
|
|
||||||
global PLAYLISTS, NODES
|
|
||||||
folder = section_xml.attrib
|
|
||||||
plex_type = folder['type']
|
|
||||||
# Only process supported formats
|
|
||||||
if plex_type not in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW,
|
|
||||||
v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_PHOTO):
|
|
||||||
LOG.error('Unsupported Plex section type: %s', folder)
|
|
||||||
return
|
|
||||||
section_id = cast(int, folder['key'])
|
|
||||||
section_name = folder['title']
|
|
||||||
# Prevent duplicate for nodes of the same type
|
|
||||||
nodes = NODES[plex_type]
|
|
||||||
# Prevent duplicate for playlists of the same type
|
|
||||||
playlists = PLAYLISTS[plex_type]
|
|
||||||
# Get current media folders from plex database
|
|
||||||
section = plexdb.section(section_id)
|
|
||||||
if not section:
|
|
||||||
LOG.info('Creating section id: %s in Plex database.', section_id)
|
|
||||||
tagid = kodidb.create_tag(section_name)
|
|
||||||
# Create playlist for the video library
|
|
||||||
if (section_name not in playlists and
|
|
||||||
plex_type in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
|
||||||
utils.playlist_xsp(plex_type, section_name, section_id)
|
|
||||||
playlists.append(section_name)
|
|
||||||
# Create the video node
|
|
||||||
if section_name not in nodes:
|
|
||||||
VNODES.viewNode(index,
|
|
||||||
section_name,
|
|
||||||
plex_type,
|
|
||||||
None,
|
|
||||||
section_id)
|
|
||||||
nodes.append(section_name)
|
|
||||||
# Add view to plex database
|
|
||||||
plexdb.add_section(section_id,
|
|
||||||
section_name,
|
|
||||||
plex_type,
|
|
||||||
tagid,
|
|
||||||
True, # Sync this new section for now
|
|
||||||
None)
|
|
||||||
else:
|
|
||||||
LOG.info('Found library section id %s, name %s, type %s, tagid %s',
|
|
||||||
section_id, section['section_name'], section['plex_type'],
|
|
||||||
section['kodi_tagid'])
|
|
||||||
# Remove views that are still valid to delete rest later
|
|
||||||
for section in old_sections:
|
|
||||||
if section['section_id'] == section_id:
|
|
||||||
old_sections.remove(section)
|
|
||||||
break
|
|
||||||
# View was modified, update with latest info
|
|
||||||
if section['section_name'] != section_name:
|
|
||||||
LOG.info('section id: %s new sectionname: %s',
|
|
||||||
section_id, section_name)
|
|
||||||
tagid = kodidb.create_tag(section_name)
|
|
||||||
|
|
||||||
# Update view with new info
|
|
||||||
plexdb.add_section(section_id,
|
|
||||||
section_name,
|
|
||||||
plex_type,
|
|
||||||
tagid,
|
|
||||||
section['sync_to_kodi'], # Use "old" setting
|
|
||||||
section['last_sync'])
|
|
||||||
|
|
||||||
if plexdb.section_id_by_name(section['section_name']) is None:
|
|
||||||
# The tag could be a combined view. Ensure there's
|
|
||||||
# no other tags with the same name before deleting
|
|
||||||
# playlist.
|
|
||||||
utils.playlist_xsp(plex_type,
|
|
||||||
section['section_name'],
|
|
||||||
section_id,
|
|
||||||
section['plex_type'],
|
|
||||||
True)
|
|
||||||
# Delete video node
|
|
||||||
if plex_type != "musicvideos":
|
|
||||||
VNODES.viewNode(
|
|
||||||
indexnumber=index,
|
|
||||||
tagname=section['section_name'],
|
|
||||||
mediatype=plex_type,
|
|
||||||
viewtype=None,
|
|
||||||
viewid=section_id,
|
|
||||||
delete=True)
|
|
||||||
# Added new playlist
|
|
||||||
if section_name not in playlists and plex_type in v.KODI_VIDEOTYPES:
|
|
||||||
utils.playlist_xsp(plex_type,
|
|
||||||
section_name,
|
|
||||||
section_id)
|
|
||||||
playlists.append(section_name)
|
|
||||||
# Add new video node
|
|
||||||
if section_name not in nodes and plex_type != "musicvideos":
|
|
||||||
VNODES.viewNode(index,
|
|
||||||
section_name,
|
|
||||||
plex_type,
|
|
||||||
None,
|
|
||||||
section_id)
|
|
||||||
nodes.append(section_name)
|
|
||||||
# Update items with new tag
|
|
||||||
for kodi_id in plexdb.kodiid_by_sectionid(section_id, plex_type):
|
|
||||||
kodidb.update_tag(
|
|
||||||
section['kodi_tagid'], tagid, kodi_id, section['plex_type'])
|
|
||||||
else:
|
|
||||||
# Validate the playlist exists or recreate it
|
|
||||||
if (section_name not in playlists and plex_type in
|
|
||||||
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
|
||||||
utils.playlist_xsp(plex_type,
|
|
||||||
section_name,
|
|
||||||
section_id)
|
|
||||||
playlists.append(section_name)
|
|
||||||
# Create the video node if not already exists
|
|
||||||
if section_name not in nodes and plex_type != "musicvideos":
|
|
||||||
VNODES.viewNode(index,
|
|
||||||
section_name,
|
|
||||||
plex_type,
|
|
||||||
None,
|
|
||||||
section_id)
|
|
||||||
nodes.append(section_name)
|
|
||||||
|
|
||||||
|
|
||||||
def _delete_kodi_db_items(section_id, section_type):
|
|
||||||
if section_type == v.PLEX_TYPE_MOVIE:
|
|
||||||
kodi_context = kodi_db.KodiVideoDB
|
kodi_context = kodi_db.KodiVideoDB
|
||||||
types = ((v.PLEX_TYPE_MOVIE, itemtypes.Movie), )
|
types = ((v.PLEX_TYPE_MOVIE, itemtypes.Movie), )
|
||||||
elif section_type == v.PLEX_TYPE_SHOW:
|
elif section.section_type == v.PLEX_TYPE_SHOW:
|
||||||
kodi_context = kodi_db.KodiVideoDB
|
kodi_context = kodi_db.KodiVideoDB
|
||||||
types = ((v.PLEX_TYPE_SHOW, itemtypes.Show),
|
types = ((v.PLEX_TYPE_SHOW, itemtypes.Show),
|
||||||
(v.PLEX_TYPE_SEASON, itemtypes.Season),
|
(v.PLEX_TYPE_SEASON, itemtypes.Season),
|
||||||
(v.PLEX_TYPE_EPISODE, itemtypes.Episode))
|
(v.PLEX_TYPE_EPISODE, itemtypes.Episode))
|
||||||
elif section_type == v.PLEX_TYPE_ARTIST:
|
elif section.section_type == v.PLEX_TYPE_ARTIST:
|
||||||
kodi_context = kodi_db.KodiMusicDB
|
kodi_context = kodi_db.KodiMusicDB
|
||||||
types = ((v.PLEX_TYPE_ARTIST, itemtypes.Artist),
|
types = ((v.PLEX_TYPE_ARTIST, itemtypes.Artist),
|
||||||
(v.PLEX_TYPE_ALBUM, itemtypes.Album),
|
(v.PLEX_TYPE_ALBUM, itemtypes.Album),
|
||||||
|
@ -226,7 +454,7 @@ def _delete_kodi_db_items(section_id, section_type):
|
||||||
for plex_type, context in types:
|
for plex_type, context in types:
|
||||||
while True:
|
while True:
|
||||||
with PlexDB() as plexdb:
|
with PlexDB() as plexdb:
|
||||||
plex_ids = list(plexdb.plexid_by_sectionid(section_id,
|
plex_ids = list(plexdb.plexid_by_sectionid(section.section_id,
|
||||||
plex_type,
|
plex_type,
|
||||||
BATCH_SIZE))
|
BATCH_SIZE))
|
||||||
with kodi_context(texture_db=True) as kodidb:
|
with kodi_context(texture_db=True) as kodidb:
|
||||||
|
@ -240,74 +468,175 @@ def _delete_kodi_db_items(section_id, section_type):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def delete_sections(old_sections):
|
def _choose_libraries(sections):
|
||||||
"""
|
|
||||||
Deletes all elements for a Plex section that has been deleted. (e.g. all
|
|
||||||
TV shows, Seasons and Episodes of a Show section)
|
|
||||||
"""
|
|
||||||
LOG.info("Removing entire Plex library sections: %s", old_sections)
|
|
||||||
for section in old_sections:
|
|
||||||
# "Deleting <section_name>"
|
|
||||||
utils.dialog('notification',
|
|
||||||
heading='{plex}',
|
|
||||||
message='%s %s' % (utils.lang(30052), section['section_name']),
|
|
||||||
icon='{plex}',
|
|
||||||
sound=False)
|
|
||||||
if section['plex_type'] == v.PLEX_TYPE_PHOTO:
|
|
||||||
# not synced - just remove the link in our Plex sections table
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if not _delete_kodi_db_items(section['section_id'], section['plex_type']):
|
|
||||||
return
|
|
||||||
# Only remove Plex entry if we've removed all items first
|
|
||||||
with PlexDB() as plexdb:
|
|
||||||
plexdb.remove_section(section['section_id'])
|
|
||||||
|
|
||||||
|
|
||||||
def choose_libraries():
|
|
||||||
"""
|
"""
|
||||||
Displays a dialog for the user to select the libraries he wants synched
|
Displays a dialog for the user to select the libraries he wants synched
|
||||||
|
|
||||||
Returns True if this was successful, False if not
|
Returns True if the user chose new sections, False if he aborted
|
||||||
"""
|
"""
|
||||||
# Re-set value in order to make sure we got the lastest user input
|
|
||||||
app.SYNC.enable_music = utils.settings('enableMusic') == 'true'
|
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
sections = []
|
selectable_sections = []
|
||||||
preselect = []
|
preselected = []
|
||||||
index = 0
|
index = 0
|
||||||
for section in SECTIONS:
|
for section in sections:
|
||||||
if not app.SYNC.enable_music and section['plex_type'] == v.PLEX_TYPE_ARTIST:
|
if not app.SYNC.enable_music and section.section_type == v.PLEX_TYPE_ARTIST:
|
||||||
LOG.info('Ignoring music section: %s', section)
|
LOG.info('Ignoring music section: %s', section)
|
||||||
continue
|
continue
|
||||||
elif section['plex_type'] == v.PLEX_TYPE_PHOTO:
|
elif section.section_type == v.PLEX_TYPE_PHOTO:
|
||||||
|
# We won't ever show Photo sections
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
sections.append(section['section_name'])
|
# Offer user the new section
|
||||||
if section['sync_to_kodi']:
|
selectable_sections.append(section.name)
|
||||||
preselect.append(index)
|
# Sections have been either preselected by the user or they are new
|
||||||
|
if section.sync_to_kodi:
|
||||||
|
preselected.append(index)
|
||||||
index += 1
|
index += 1
|
||||||
# "Select Plex libraries to sync"
|
# Don't ask the user again for this PMS even if user cancel the sync dialog
|
||||||
selected = xbmcgui.Dialog().multiselect(utils.lang(30524),
|
|
||||||
sections,
|
|
||||||
preselect=preselect,
|
|
||||||
useDetails=False)
|
|
||||||
if selected is None:
|
|
||||||
# User canceled
|
|
||||||
return False
|
|
||||||
index = 0
|
|
||||||
with PlexDB() as plexdb:
|
|
||||||
for section in SECTIONS:
|
|
||||||
if not app.SYNC.enable_music and section['plex_type'] == v.PLEX_TYPE_ARTIST:
|
|
||||||
continue
|
|
||||||
elif section['plex_type'] == v.PLEX_TYPE_PHOTO:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
sync = True if index in selected else False
|
|
||||||
plexdb.update_section_sync(section['section_id'], sync)
|
|
||||||
index += 1
|
|
||||||
sections = list(plexdb.all_sections())
|
|
||||||
LOG.info('Plex libraries to sync: %s', sections)
|
|
||||||
utils.settings('sections_asked_for_machine_identifier',
|
utils.settings('sections_asked_for_machine_identifier',
|
||||||
value=app.CONN.machine_identifier)
|
value=app.CONN.machine_identifier)
|
||||||
|
# "Select Plex libraries to sync"
|
||||||
|
selected_sections = xbmcgui.Dialog().multiselect(utils.lang(30524),
|
||||||
|
selectable_sections,
|
||||||
|
preselect=preselected,
|
||||||
|
useDetails=False)
|
||||||
|
if selectable_sections is None:
|
||||||
|
LOG.info('User chose not to select which libraries to sync')
|
||||||
|
return False
|
||||||
|
index = 0
|
||||||
|
for section in sections:
|
||||||
|
if not app.SYNC.enable_music and section.section_type == v.PLEX_TYPE_ARTIST:
|
||||||
|
continue
|
||||||
|
elif section.section_type == v.PLEX_TYPE_PHOTO:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
section.sync_to_kodi = index in selected_sections
|
||||||
|
index += 1
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def delete_playlists():
|
||||||
|
"""
|
||||||
|
Clean up the playlists
|
||||||
|
"""
|
||||||
|
path = path_ops.translate_path('special://profile/playlists/video/')
|
||||||
|
for root, _, files in path_ops.walk(path):
|
||||||
|
for file in files:
|
||||||
|
if file.startswith('Plex'):
|
||||||
|
path_ops.remove(path_ops.path.join(root, file))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_nodes():
|
||||||
|
"""
|
||||||
|
Clean up video nodes
|
||||||
|
"""
|
||||||
|
path = path_ops.translate_path("special://profile/library/video/")
|
||||||
|
for root, dirs, _ in path_ops.walk(path):
|
||||||
|
for directory in dirs:
|
||||||
|
if directory.startswith('Plex-'):
|
||||||
|
path_ops.rmtree(path_ops.path.join(root, directory))
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def delete_files():
|
||||||
|
"""
|
||||||
|
Deletes both all the Plex-xxx video node xmls as well as smart playlists
|
||||||
|
"""
|
||||||
|
delete_nodes()
|
||||||
|
delete_playlists()
|
||||||
|
|
||||||
|
|
||||||
|
def sync_from_pms(parent_self, pick_libraries=False):
|
||||||
|
"""
|
||||||
|
Sync the Plex library sections.
|
||||||
|
pick_libraries=True will prompt the user the select the libraries he
|
||||||
|
wants to sync
|
||||||
|
"""
|
||||||
|
global IS_CANCELED
|
||||||
|
LOG.info('Starting synching sections from the PMS')
|
||||||
|
IS_CANCELED = parent_self.isCanceled
|
||||||
|
try:
|
||||||
|
return _sync_from_pms(pick_libraries)
|
||||||
|
finally:
|
||||||
|
IS_CANCELED = None
|
||||||
|
LOG.info('Done synching sections from the PMS: %s', SECTIONS)
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_from_pms(pick_libraries):
|
||||||
|
global SECTIONS
|
||||||
|
# Re-set value in order to make sure we got the lastest user input
|
||||||
|
app.SYNC.enable_music = utils.settings('enableMusic') == 'true'
|
||||||
|
xml = PF.get_plex_sections()
|
||||||
|
if xml is None:
|
||||||
|
LOG.error("Error download PMS sections, abort")
|
||||||
|
return False
|
||||||
|
sections = []
|
||||||
|
old_sections = []
|
||||||
|
for i, xml_element in enumerate(xml.findall('Directory')):
|
||||||
|
sections.append(Section(index=i, xml_element=xml_element))
|
||||||
|
with PlexDB() as plexdb:
|
||||||
|
for section_db in plexdb.all_sections():
|
||||||
|
old_sections.append(Section(section_db_element=section_db))
|
||||||
|
# Update our latest PMS sections with info saved in the PMS DB
|
||||||
|
_retrieve_old_settings(sections, old_sections)
|
||||||
|
if (app.CONN.machine_identifier != utils.settings('sections_asked_for_machine_identifier') or
|
||||||
|
pick_libraries):
|
||||||
|
if not pick_libraries:
|
||||||
|
LOG.info('First time connecting to this PMS, choosing libraries')
|
||||||
|
_choose_libraries(sections)
|
||||||
|
|
||||||
|
# We got everything - save to Plex db in case Kodi restarts before we're
|
||||||
|
# done here
|
||||||
|
_save_sections_to_plex_db(sections)
|
||||||
|
# Tweak some settings so Kodi does NOT scan the music folders
|
||||||
|
if app.SYNC.direct_paths is True:
|
||||||
|
# Will reboot Kodi is new library detected
|
||||||
|
music.excludefromscan_music_folders(sections)
|
||||||
|
|
||||||
|
# Delete all old sections that are obsolete
|
||||||
|
# This will also delete sections whose name (or type) have changed
|
||||||
|
for old_section in old_sections:
|
||||||
|
for section in sections:
|
||||||
|
if old_section == section:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if not old_section.sync_to_kodi:
|
||||||
|
continue
|
||||||
|
LOG.info('Deleting entire section: %s', old_section)
|
||||||
|
# Remove all linked items
|
||||||
|
if not _delete_kodi_db_items(old_section):
|
||||||
|
return False
|
||||||
|
# Remove the section itself
|
||||||
|
old_section.remove()
|
||||||
|
|
||||||
|
# Time to write the sections to Kodi
|
||||||
|
for section in sections:
|
||||||
|
section.to_kodi()
|
||||||
|
# Counter that tells us how many sections we have - e.g. for skins and
|
||||||
|
# listings
|
||||||
|
utils.window('Plex.nodes.total', str(len(sections)))
|
||||||
|
SECTIONS = sections
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_window_vars(index):
|
||||||
|
node = 'Plex.nodes.%s' % index
|
||||||
|
utils.window('%s.title' % node, clear=True)
|
||||||
|
utils.window('%s.type' % node, clear=True)
|
||||||
|
utils.window('%s.content' % node, clear=True)
|
||||||
|
utils.window('%s.path' % node, clear=True)
|
||||||
|
utils.window('%s.id' % node, clear=True)
|
||||||
|
utils.window('%s.addon_path' % node, clear=True)
|
||||||
|
for kind in WINDOW_ARGS:
|
||||||
|
node = 'Plex.nodes.%s.%s' % (index, kind)
|
||||||
|
utils.window(node, clear=True)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_window_vars():
|
||||||
|
"""
|
||||||
|
Removes all references to sections stored in window vars 'Plex.nodes...'
|
||||||
|
"""
|
||||||
|
number_of_nodes = int(utils.window('Plex.nodes.total') or 0)
|
||||||
|
utils.window('Plex.nodes.total', clear=True)
|
||||||
|
for index in range(number_of_nodes):
|
||||||
|
_clear_window_vars(index)
|
||||||
|
|
|
@ -23,9 +23,7 @@ def sync_pms_time():
|
||||||
|
|
||||||
# Get all Plex libraries
|
# Get all Plex libraries
|
||||||
sections = PF.get_plex_sections()
|
sections = PF.get_plex_sections()
|
||||||
try:
|
if not sections:
|
||||||
sections.attrib
|
|
||||||
except AttributeError:
|
|
||||||
LOG.error("Error download PMS views, abort sync_pms_time")
|
LOG.error("Error download PMS views, abort sync_pms_time")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -1,488 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
|
||||||
from logging import getLogger
|
|
||||||
|
|
||||||
from ..utils import etree
|
|
||||||
from .. import utils, path_ops, variables as v, app
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
LOG = getLogger('PLEX.videonodes')
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
class VideoNodes(object):
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def commonRoot(order, label, tagname, roottype=1):
|
|
||||||
|
|
||||||
if roottype == 0:
|
|
||||||
# Index
|
|
||||||
root = etree.Element('node', {'order': "%s" % order})
|
|
||||||
elif roottype == 1:
|
|
||||||
# Filter
|
|
||||||
root = etree.Element('node',
|
|
||||||
{'order': "%s" % order, 'type': "filter"})
|
|
||||||
etree.SubElement(root, 'match').text = "all"
|
|
||||||
# Add tag rule
|
|
||||||
rule = etree.SubElement(root,
|
|
||||||
'rule',
|
|
||||||
{'field': "tag", 'operator': "is"})
|
|
||||||
etree.SubElement(rule, 'value').text = tagname
|
|
||||||
else:
|
|
||||||
# Folder
|
|
||||||
root = etree.Element('node',
|
|
||||||
{'order': "%s" % order, 'type': "folder"})
|
|
||||||
|
|
||||||
etree.SubElement(root, 'label').text = label
|
|
||||||
etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.plexkodiconnect/icon.png"
|
|
||||||
|
|
||||||
return root
|
|
||||||
|
|
||||||
def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid,
|
|
||||||
delete=False):
|
|
||||||
# Plex: reassign mediatype due to Kodi inner workings
|
|
||||||
# How many items do we get at most?
|
|
||||||
limit = unicode(app.APP.fetch_pms_item_number)
|
|
||||||
mediatypes = {
|
|
||||||
'movie': 'movies',
|
|
||||||
'show': 'tvshows',
|
|
||||||
'photo': 'photos',
|
|
||||||
'homevideo': 'homevideos',
|
|
||||||
'musicvideos': 'musicvideos',
|
|
||||||
'artist': 'albums'
|
|
||||||
}
|
|
||||||
mediatype = mediatypes[mediatype]
|
|
||||||
|
|
||||||
if viewtype == "mixed":
|
|
||||||
dirname = "%s-%s" % (viewid, mediatype)
|
|
||||||
else:
|
|
||||||
dirname = viewid
|
|
||||||
|
|
||||||
# Returns strings
|
|
||||||
path = path_ops.translate_path('special://profile/library/video/')
|
|
||||||
nodepath = path_ops.translate_path(
|
|
||||||
'special://profile/library/video/Plex-%s/' % dirname)
|
|
||||||
|
|
||||||
if delete:
|
|
||||||
if path_ops.exists(nodepath):
|
|
||||||
path_ops.rmtree(nodepath)
|
|
||||||
LOG.info("Sucessfully removed videonode: %s." % tagname)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Verify the video directory
|
|
||||||
if not path_ops.exists(path):
|
|
||||||
path_ops.copy_tree(
|
|
||||||
src=path_ops.translate_path(
|
|
||||||
'special://xbmc/system/library/video'),
|
|
||||||
dst=path_ops.translate_path('special://profile/library/video'),
|
|
||||||
preserve_mode=0) # do not copy permission bits!
|
|
||||||
|
|
||||||
# Create the node directory
|
|
||||||
if mediatype != "photos":
|
|
||||||
if not path_ops.exists(nodepath):
|
|
||||||
# folder does not exist yet
|
|
||||||
LOG.debug('Creating folder %s' % nodepath)
|
|
||||||
path_ops.makedirs(nodepath)
|
|
||||||
|
|
||||||
# Create index entry
|
|
||||||
nodeXML = "%sindex.xml" % nodepath
|
|
||||||
# Set windows property
|
|
||||||
path = "library://video/Plex-%s/" % dirname
|
|
||||||
for i in range(1, indexnumber):
|
|
||||||
# Verify to make sure we don't create duplicates
|
|
||||||
if utils.window('Plex.nodes.%s.index' % i) == path:
|
|
||||||
return
|
|
||||||
|
|
||||||
if mediatype == "photos":
|
|
||||||
path = "plugin://plugin.video.plexkodiconnect?mode=browseplex&key=/library/sections/%s&id=%s" % (viewid, viewid)
|
|
||||||
|
|
||||||
utils.window('Plex.nodes.%s.index' % indexnumber, value=path)
|
|
||||||
|
|
||||||
# Root
|
|
||||||
if not mediatype == "photos":
|
|
||||||
if viewtype == "mixed":
|
|
||||||
specialtag = "%s-%s" % (tagname, mediatype)
|
|
||||||
root = self.commonRoot(order=0,
|
|
||||||
label=specialtag,
|
|
||||||
tagname=tagname,
|
|
||||||
roottype=0)
|
|
||||||
else:
|
|
||||||
root = self.commonRoot(order=0,
|
|
||||||
label=tagname,
|
|
||||||
tagname=tagname,
|
|
||||||
roottype=0)
|
|
||||||
utils.indent(root)
|
|
||||||
etree.ElementTree(root).write(nodeXML, encoding="UTF-8")
|
|
||||||
|
|
||||||
nodetypes = {
|
|
||||||
'1': "all",
|
|
||||||
'2': "recent",
|
|
||||||
'3': "recentepisodes",
|
|
||||||
'4': "inprogress",
|
|
||||||
'5': "inprogressepisodes",
|
|
||||||
'6': "unwatched",
|
|
||||||
'7': "nextepisodes",
|
|
||||||
'8': "sets",
|
|
||||||
'9': "genres",
|
|
||||||
'10': "random",
|
|
||||||
'11': "recommended",
|
|
||||||
'12': "ondeck",
|
|
||||||
'13': 'browsefiles',
|
|
||||||
}
|
|
||||||
mediatypes = {
|
|
||||||
# label according to nodetype per mediatype
|
|
||||||
'movies':
|
|
||||||
{
|
|
||||||
'1': tagname,
|
|
||||||
'2': 30174,
|
|
||||||
# '4': 30177,
|
|
||||||
# '6': 30189,
|
|
||||||
'8': 39501,
|
|
||||||
'9': 135,
|
|
||||||
'10': 30227,
|
|
||||||
'11': 30230,
|
|
||||||
'12': 39500,
|
|
||||||
'13': 39702,
|
|
||||||
},
|
|
||||||
|
|
||||||
'tvshows':
|
|
||||||
{
|
|
||||||
'1': tagname,
|
|
||||||
# '2': 30170,
|
|
||||||
'3': 30174,
|
|
||||||
# '4': 30171,
|
|
||||||
# '5': 30178,
|
|
||||||
# '7': 30179,
|
|
||||||
'9': 135,
|
|
||||||
'10': 30227,
|
|
||||||
# '11': 30230,
|
|
||||||
'12': 39500,
|
|
||||||
'13': 39702,
|
|
||||||
},
|
|
||||||
|
|
||||||
'homevideos':
|
|
||||||
{
|
|
||||||
'1': tagname,
|
|
||||||
'2': 30251,
|
|
||||||
'11': 30253,
|
|
||||||
'13': 39702,
|
|
||||||
'14': 136
|
|
||||||
},
|
|
||||||
|
|
||||||
'photos':
|
|
||||||
{
|
|
||||||
'1': tagname,
|
|
||||||
'2': 30252,
|
|
||||||
'8': 30255,
|
|
||||||
'11': 30254,
|
|
||||||
'13': 39702
|
|
||||||
},
|
|
||||||
|
|
||||||
'musicvideos':
|
|
||||||
{
|
|
||||||
'1': tagname,
|
|
||||||
'2': 30256,
|
|
||||||
'4': 30257,
|
|
||||||
'6': 30258,
|
|
||||||
'13': 39702
|
|
||||||
},
|
|
||||||
|
|
||||||
'albums':
|
|
||||||
{
|
|
||||||
'1': tagname,
|
|
||||||
'2': 517, # Recently played albums
|
|
||||||
'2': 359, # Recently added albums
|
|
||||||
'13': 39702, # browse by folder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Key: nodetypes, value: sort order in Kodi
|
|
||||||
sortorder = {
|
|
||||||
'1': '3', # "all",
|
|
||||||
'2': '2', # "recent",
|
|
||||||
'3': '2', # "recentepisodes",
|
|
||||||
# '4': # "inprogress",
|
|
||||||
# '5': # "inprogressepisodes",
|
|
||||||
# '6': # "unwatched",
|
|
||||||
# '7': # "nextepisodes",
|
|
||||||
'8': '7', # "sets",
|
|
||||||
'9': '6', # "genres",
|
|
||||||
'10': '8', # "random",
|
|
||||||
'11': '5', # "recommended",
|
|
||||||
'12': '1', # "ondeck"
|
|
||||||
'13': '9', # browse by folder
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes = mediatypes[mediatype]
|
|
||||||
for node in nodes:
|
|
||||||
|
|
||||||
nodetype = nodetypes[node]
|
|
||||||
nodeXML = "%s%s_%s.xml" % (nodepath, viewid, nodetype)
|
|
||||||
# Get label
|
|
||||||
stringid = nodes[node]
|
|
||||||
if node != "1":
|
|
||||||
label = utils.lang(stringid)
|
|
||||||
else:
|
|
||||||
label = stringid
|
|
||||||
|
|
||||||
# Set window properties
|
|
||||||
if ((mediatype == "homevideos" or mediatype == "photos") and
|
|
||||||
nodetype == "all"):
|
|
||||||
# Custom query
|
|
||||||
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browseplex&type=%s"
|
|
||||||
% (viewid, mediatype))
|
|
||||||
elif (mediatype == "homevideos" or mediatype == "photos"):
|
|
||||||
# Custom query
|
|
||||||
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=browseplex&type=%s&folderid=%s"
|
|
||||||
% (viewid, mediatype, nodetype))
|
|
||||||
elif nodetype == "nextepisodes":
|
|
||||||
# Custom query
|
|
||||||
path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=nextup&limit=%s" % (tagname, limit)
|
|
||||||
# elif v.KODIVERSION == 14 and nodetype == "recentepisodes":
|
|
||||||
elif nodetype == "recentepisodes":
|
|
||||||
# Custom query
|
|
||||||
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=recentepisodes&type=%s&tagname=%s&limit=%s"
|
|
||||||
% (viewid, mediatype, tagname, limit))
|
|
||||||
elif v.KODIVERSION == 14 and nodetype == "inprogressepisodes":
|
|
||||||
# Custom query
|
|
||||||
path = "plugin://plugin.video.plexkodiconnect/?id=%s&mode=inprogressepisodes&limit=%s" % (tagname, limit)
|
|
||||||
elif nodetype == 'ondeck':
|
|
||||||
# PLEX custom query
|
|
||||||
if mediatype == "tvshows":
|
|
||||||
path = ("plugin://plugin.video.plexkodiconnect/?id=%s&mode=ondeck&type=%s&tagname=%s&limit=%s"
|
|
||||||
% (viewid, mediatype, tagname, limit))
|
|
||||||
elif mediatype =="movies":
|
|
||||||
# Reset nodetype; we got the label
|
|
||||||
nodetype = 'inprogress'
|
|
||||||
elif nodetype == 'browsefiles':
|
|
||||||
path = 'plugin://plugin.video.plexkodiconnect?mode=browseplex&key=/library/sections/%s/folder' % viewid
|
|
||||||
else:
|
|
||||||
path = "library://video/Plex-%s/%s_%s.xml" % (dirname, viewid, nodetype)
|
|
||||||
|
|
||||||
if mediatype == "photos":
|
|
||||||
windowpath = "ActivateWindow(Pictures,%s,return)" % path
|
|
||||||
else:
|
|
||||||
if v.KODIVERSION >= 17:
|
|
||||||
# Krypton
|
|
||||||
windowpath = "ActivateWindow(Videos,%s,return)" % path
|
|
||||||
else:
|
|
||||||
windowpath = "ActivateWindow(Video,%s,return)" % path
|
|
||||||
|
|
||||||
if nodetype == "all":
|
|
||||||
|
|
||||||
if viewtype == "mixed":
|
|
||||||
templabel = "%s-%s" % (tagname, mediatype)
|
|
||||||
else:
|
|
||||||
templabel = label
|
|
||||||
|
|
||||||
embynode = "Plex.nodes.%s" % indexnumber
|
|
||||||
utils.window('%s.title' % embynode, value=templabel)
|
|
||||||
utils.window('%s.path' % embynode, value=windowpath)
|
|
||||||
utils.window('%s.content' % embynode, value=path)
|
|
||||||
utils.window('%s.type' % embynode, value=mediatype)
|
|
||||||
else:
|
|
||||||
embynode = "Plex.nodes.%s.%s" % (indexnumber, nodetype)
|
|
||||||
utils.window('%s.title' % embynode, value=label)
|
|
||||||
utils.window('%s.path' % embynode, value=windowpath)
|
|
||||||
utils.window('%s.content' % embynode, value=path)
|
|
||||||
|
|
||||||
if mediatype == "photos":
|
|
||||||
# For photos, we do not create a node in videos but we do want
|
|
||||||
# the window props to be created. To do: add our photos nodes to
|
|
||||||
# kodi picture sources somehow
|
|
||||||
continue
|
|
||||||
|
|
||||||
if path_ops.exists(nodeXML):
|
|
||||||
# Don't recreate xml if already exists
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Create the root
|
|
||||||
if (nodetype in ("nextepisodes",
|
|
||||||
"ondeck",
|
|
||||||
'recentepisodes',
|
|
||||||
'browsefiles') or mediatype == "homevideos"):
|
|
||||||
# Folder type with plugin path
|
|
||||||
root = self.commonRoot(order=sortorder[node],
|
|
||||||
label=label,
|
|
||||||
tagname=tagname,
|
|
||||||
roottype=2)
|
|
||||||
else:
|
|
||||||
root = self.commonRoot(order=sortorder[node],
|
|
||||||
label=label,
|
|
||||||
tagname=tagname)
|
|
||||||
# Set the content type
|
|
||||||
if mediatype == 'tvshows' and nodetype != 'all':
|
|
||||||
etree.SubElement(root, 'content').text = 'episodes'
|
|
||||||
else:
|
|
||||||
etree.SubElement(root, 'content').text = mediatype
|
|
||||||
# Now fill the view
|
|
||||||
if (nodetype in ("nextepisodes",
|
|
||||||
"ondeck",
|
|
||||||
'recentepisodes',
|
|
||||||
'browsefiles') or mediatype == "homevideos"):
|
|
||||||
etree.SubElement(root, 'path').text = path
|
|
||||||
else:
|
|
||||||
# Elements per nodetype
|
|
||||||
if nodetype == "all":
|
|
||||||
etree.SubElement(root,
|
|
||||||
'order',
|
|
||||||
{'direction': "ascending"}).text = "sorttitle"
|
|
||||||
elif nodetype == "recent":
|
|
||||||
etree.SubElement(root,
|
|
||||||
'order',
|
|
||||||
{'direction': "descending"}).text = "dateadded"
|
|
||||||
etree.SubElement(root, 'limit').text = limit
|
|
||||||
if utils.settings('MovieShowWatched') == 'false':
|
|
||||||
rule = etree.SubElement(root,
|
|
||||||
'rule',
|
|
||||||
{'field': "playcount",
|
|
||||||
'operator': "is"})
|
|
||||||
etree.SubElement(rule, 'value').text = "0"
|
|
||||||
elif nodetype == "inprogress":
|
|
||||||
etree.SubElement(root,
|
|
||||||
'rule',
|
|
||||||
{'field': "inprogress", 'operator': "true"})
|
|
||||||
etree.SubElement(root, 'limit').text = limit
|
|
||||||
etree.SubElement(
|
|
||||||
root,
|
|
||||||
'order',
|
|
||||||
{'direction': 'descending'}
|
|
||||||
).text = 'lastplayed'
|
|
||||||
|
|
||||||
elif nodetype == "genres":
|
|
||||||
etree.SubElement(root,
|
|
||||||
'order',
|
|
||||||
{'direction': "ascending"}).text = "sorttitle"
|
|
||||||
etree.SubElement(root, 'group').text = "genres"
|
|
||||||
elif nodetype == "unwatched":
|
|
||||||
etree.SubElement(root,
|
|
||||||
'order',
|
|
||||||
{'direction': "ascending"}).text = "sorttitle"
|
|
||||||
rule = etree.SubElement(root,
|
|
||||||
"rule",
|
|
||||||
{'field': "playcount", 'operator': "is"})
|
|
||||||
etree.SubElement(rule, 'value').text = "0"
|
|
||||||
elif nodetype == "sets":
|
|
||||||
etree.SubElement(root,
|
|
||||||
'order',
|
|
||||||
{'direction': "ascending"}).text = "sorttitle"
|
|
||||||
etree.SubElement(root, 'group').text = "tags"
|
|
||||||
elif nodetype == "random":
|
|
||||||
etree.SubElement(root,
|
|
||||||
'order',
|
|
||||||
{'direction': "ascending"}).text = "random"
|
|
||||||
etree.SubElement(root, 'limit').text = limit
|
|
||||||
elif nodetype == "recommended":
|
|
||||||
etree.SubElement(root,
|
|
||||||
'order',
|
|
||||||
{'direction': "descending"}).text = "rating"
|
|
||||||
etree.SubElement(root, 'limit').text = limit
|
|
||||||
rule = etree.SubElement(root,
|
|
||||||
'rule',
|
|
||||||
{'field': "playcount", 'operator': "is"})
|
|
||||||
etree.SubElement(rule, 'value').text = "0"
|
|
||||||
rule2 = etree.SubElement(root,
|
|
||||||
'rule',
|
|
||||||
{'field': "rating", 'operator': "greaterthan"})
|
|
||||||
etree.SubElement(rule2, 'value').text = "7"
|
|
||||||
elif nodetype == "recentepisodes":
|
|
||||||
# Kodi Isengard, Jarvis
|
|
||||||
etree.SubElement(root,
|
|
||||||
'order',
|
|
||||||
{'direction': "descending"}).text = "dateadded"
|
|
||||||
etree.SubElement(root, 'limit').text = limit
|
|
||||||
rule = etree.SubElement(root,
|
|
||||||
'rule',
|
|
||||||
{'field': "playcount", 'operator': "is"})
|
|
||||||
etree.SubElement(rule, 'value').text = "0"
|
|
||||||
elif nodetype == "inprogressepisodes":
|
|
||||||
# Kodi Isengard, Jarvis
|
|
||||||
etree.SubElement(root, 'limit').text = limit
|
|
||||||
rule = etree.SubElement(root,
|
|
||||||
'rule',
|
|
||||||
{'field': "inprogress", 'operator':"true"})
|
|
||||||
utils.indent(root)
|
|
||||||
etree.ElementTree(root).write(path_ops.encode_path(nodeXML),
|
|
||||||
encoding="UTF-8")
|
|
||||||
|
|
||||||
def singleNode(self, indexnumber, tagname, mediatype, itemtype):
|
|
||||||
cleantagname = utils.normalize_nodes(tagname)
|
|
||||||
nodepath = path_ops.translate_path('special://profile/library/video/')
|
|
||||||
nodeXML = "%splex_%s.xml" % (nodepath, cleantagname)
|
|
||||||
path = "library://video/plex_%s.xml" % cleantagname
|
|
||||||
if v.KODIVERSION >= 17:
|
|
||||||
# Krypton
|
|
||||||
windowpath = "ActivateWindow(Videos,%s,return)" % path
|
|
||||||
else:
|
|
||||||
windowpath = "ActivateWindow(Video,%s,return)" % path
|
|
||||||
|
|
||||||
# Create the video node directory
|
|
||||||
if not path_ops.exists(nodepath):
|
|
||||||
# We need to copy over the default items
|
|
||||||
path_ops.copy_tree(
|
|
||||||
src=path_ops.translate_path(
|
|
||||||
'special://xbmc/system/library/video'),
|
|
||||||
dst=path_ops.translate_path('special://profile/library/video'),
|
|
||||||
preserve_mode=0) # do not copy permission bits!
|
|
||||||
|
|
||||||
labels = {
|
|
||||||
'Favorite movies': 30180,
|
|
||||||
'Favorite tvshows': 30181,
|
|
||||||
'channels': 30173
|
|
||||||
}
|
|
||||||
label = utils.lang(labels[tagname])
|
|
||||||
embynode = "Plex.nodes.%s" % indexnumber
|
|
||||||
utils.window('%s.title' % embynode, value=label)
|
|
||||||
utils.window('%s.path' % embynode, value=windowpath)
|
|
||||||
utils.window('%s.content' % embynode, value=path)
|
|
||||||
utils.window('%s.type' % embynode, value=itemtype)
|
|
||||||
|
|
||||||
if path_ops.exists(nodeXML):
|
|
||||||
# Don't recreate xml if already exists
|
|
||||||
return
|
|
||||||
|
|
||||||
if itemtype == "channels":
|
|
||||||
root = self.commonRoot(order=1,
|
|
||||||
label=label,
|
|
||||||
tagname=tagname,
|
|
||||||
roottype=2)
|
|
||||||
etree.SubElement(root,
|
|
||||||
'path').text = "plugin://plugin.video.plexkodiconnect/?id=0&mode=channels"
|
|
||||||
else:
|
|
||||||
root = self.commonRoot(order=1, label=label, tagname=tagname)
|
|
||||||
etree.SubElement(root,
|
|
||||||
'order',
|
|
||||||
{'direction': "ascending"}).text = "sorttitle"
|
|
||||||
|
|
||||||
etree.SubElement(root, 'content').text = mediatype
|
|
||||||
|
|
||||||
utils.indent(root)
|
|
||||||
etree.ElementTree(root).write(nodeXML, encoding="UTF-8")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def clearProperties():
|
|
||||||
|
|
||||||
LOG.info("Clearing nodes properties.")
|
|
||||||
plexprops = utils.window('Plex.nodes.total')
|
|
||||||
propnames = [
|
|
||||||
"index","path","title","content",
|
|
||||||
"inprogress.content","inprogress.title",
|
|
||||||
"inprogress.content","inprogress.path",
|
|
||||||
"nextepisodes.title","nextepisodes.content",
|
|
||||||
"nextepisodes.path","unwatched.title",
|
|
||||||
"unwatched.content","unwatched.path",
|
|
||||||
"recent.title","recent.content","recent.path",
|
|
||||||
"recentepisodes.title","recentepisodes.content",
|
|
||||||
"recentepisodes.path", "inprogressepisodes.title",
|
|
||||||
"inprogressepisodes.content","inprogressepisodes.path"
|
|
||||||
]
|
|
||||||
|
|
||||||
if plexprops:
|
|
||||||
totalnodes = int(plexprops)
|
|
||||||
for i in range(totalnodes):
|
|
||||||
for prop in propnames:
|
|
||||||
utils.window('Plex.nodes.%s.%s' % (str(i), prop),
|
|
||||||
clear=True)
|
|
|
@ -3,8 +3,8 @@
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from . import utils
|
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
|
from . import utils
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -12,7 +12,7 @@ LOG = getLogger('PLEX.music')
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
def excludefromscan_music_folders(xml):
|
def excludefromscan_music_folders(sections):
|
||||||
"""
|
"""
|
||||||
Gets a complete list of paths for music libraries from the PMS. Sets them
|
Gets a complete list of paths for music libraries from the PMS. Sets them
|
||||||
to be excluded in the advancedsettings.xml from being scanned by Kodi.
|
to be excluded in the advancedsettings.xml from being scanned by Kodi.
|
||||||
|
@ -24,16 +24,17 @@ def excludefromscan_music_folders(xml):
|
||||||
paths = []
|
paths = []
|
||||||
reboot = False
|
reboot = False
|
||||||
api = API(item=None)
|
api = API(item=None)
|
||||||
for library in xml:
|
for section in sections:
|
||||||
if library.attrib['type'] != v.PLEX_TYPE_ARTIST:
|
if section.plex_type != v.PLEX_TYPE_ARTIST:
|
||||||
# Only look at music libraries
|
# Only look at music libraries
|
||||||
continue
|
continue
|
||||||
for location in library:
|
if not section.sync_to_kodi:
|
||||||
if location.tag == 'Location':
|
continue
|
||||||
|
for location in section.xml.findall('Location'):
|
||||||
path = api.validate_playurl(location.attrib['path'],
|
path = api.validate_playurl(location.attrib['path'],
|
||||||
typus=v.PLEX_TYPE_ARTIST,
|
typus=v.PLEX_TYPE_ARTIST,
|
||||||
omit_check=True)
|
omit_check=True)
|
||||||
paths.append(__turn_to_regex(path))
|
paths.append(_turn_to_regex(path))
|
||||||
try:
|
try:
|
||||||
with utils.XmlKodiSetting(
|
with utils.XmlKodiSetting(
|
||||||
'advancedsettings.xml',
|
'advancedsettings.xml',
|
||||||
|
@ -73,7 +74,7 @@ def excludefromscan_music_folders(xml):
|
||||||
utils.reboot_kodi(utils.lang(39711))
|
utils.reboot_kodi(utils.lang(39711))
|
||||||
|
|
||||||
|
|
||||||
def __turn_to_regex(path):
|
def _turn_to_regex(path):
|
||||||
"""
|
"""
|
||||||
Turns a path into regex expression to be fed to Kodi's advancedsettings.xml
|
Turns a path into regex expression to be fed to Kodi's advancedsettings.xml
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -59,7 +59,10 @@ def translate_path(path):
|
||||||
|
|
||||||
|
|
||||||
def exists(path):
|
def exists(path):
|
||||||
"""Returns True if the path [unicode] exists"""
|
"""
|
||||||
|
Returns True if the path [unicode] exists. Folders NEED a trailing slash or
|
||||||
|
backslash!!
|
||||||
|
"""
|
||||||
return xbmcvfs.exists(path.encode(KODI_ENCODING, 'strict')) == 1
|
return xbmcvfs.exists(path.encode(KODI_ENCODING, 'strict')) == 1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ from logging import getLogger
|
||||||
from re import sub
|
from re import sub
|
||||||
from urllib import urlencode, unquote, quote
|
from urllib import urlencode, unquote, quote
|
||||||
from urlparse import parse_qsl
|
from urlparse import parse_qsl
|
||||||
|
|
||||||
from xbmcgui import ListItem
|
from xbmcgui import ListItem
|
||||||
|
|
||||||
from .plex_db import PlexDB
|
from .plex_db import PlexDB
|
||||||
|
@ -149,6 +150,30 @@ class API(object):
|
||||||
omit_check=True)
|
omit_check=True)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
def directory_path(self, section_id=None, plex_type=None, old_key=None,
|
||||||
|
synched=True):
|
||||||
|
key = self.item.get('fastKey')
|
||||||
|
if not key:
|
||||||
|
key = self.item.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.item.get('prompt'):
|
||||||
|
# User input needed, e.g. search for a movie or episode
|
||||||
|
params['prompt'] = self.item.get('prompt')
|
||||||
|
if section_id:
|
||||||
|
params['id'] = section_id
|
||||||
|
return 'plugin://%s/?%s' % (v.ADDON_ID, urlencode(params))
|
||||||
|
|
||||||
def path_and_plex_id(self):
|
def path_and_plex_id(self):
|
||||||
"""
|
"""
|
||||||
Returns the Plex key such as '/library/metadata/246922' or None
|
Returns the Plex key such as '/library/metadata/246922' or None
|
||||||
|
@ -335,6 +360,26 @@ class API(object):
|
||||||
'UserRating': userrating
|
'UserRating': userrating
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def leave_count(self):
|
||||||
|
"""
|
||||||
|
Returns the following dict or None
|
||||||
|
{
|
||||||
|
'totalepisodes': unicode('leafCount'),
|
||||||
|
'watchedepisodes': unicode('viewedLeafCount'),
|
||||||
|
'unwatchedepisodes': unicode(totalepisodes - watchedepisodes)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
total = int(self.item.attrib['leafCount'])
|
||||||
|
watched = int(self.item.attrib['viewedLeafCount'])
|
||||||
|
return {
|
||||||
|
'totalepisodes': unicode(total),
|
||||||
|
'watchedepisodes': unicode(watched),
|
||||||
|
'unwatchedepisodes': unicode(total - watched)
|
||||||
|
}
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
def collection_list(self):
|
def collection_list(self):
|
||||||
"""
|
"""
|
||||||
Returns a list of tuples of the collection id and tags or an empty list
|
Returns a list of tuples of the collection id and tags or an empty list
|
||||||
|
@ -522,7 +567,7 @@ class API(object):
|
||||||
|
|
||||||
def resume_point(self):
|
def resume_point(self):
|
||||||
"""
|
"""
|
||||||
Returns the resume point of time in seconds as int. 0 if not found
|
Returns the resume point of time in seconds as float. 0.0 if not found
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
resume = float(self.item.attrib['viewOffset'])
|
resume = float(self.item.attrib['viewOffset'])
|
||||||
|
@ -588,7 +633,7 @@ class API(object):
|
||||||
|
|
||||||
def premiere_date(self):
|
def premiere_date(self):
|
||||||
"""
|
"""
|
||||||
Returns the "originallyAvailableAt" or None
|
Returns the "originallyAvailableAt", e.g. "2018-11-16" or None
|
||||||
"""
|
"""
|
||||||
return self.item.get('originallyAvailableAt')
|
return self.item.get('originallyAvailableAt')
|
||||||
|
|
||||||
|
@ -859,7 +904,20 @@ class API(object):
|
||||||
'subtitle': subtitlelanguages
|
'subtitle': subtitlelanguages
|
||||||
}
|
}
|
||||||
|
|
||||||
def one_artwork(self, art_kind):
|
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
|
||||||
artwork = self.item.get(art_kind)
|
artwork = self.item.get(art_kind)
|
||||||
if artwork and not artwork.startswith('http'):
|
if artwork and not artwork.startswith('http'):
|
||||||
if '/composite/' in artwork:
|
if '/composite/' in artwork:
|
||||||
|
@ -870,27 +928,48 @@ class API(object):
|
||||||
args = dict(parse_qsl(args))
|
args = dict(parse_qsl(args))
|
||||||
width = int(args.get('width', 400))
|
width = int(args.get('width', 400))
|
||||||
height = int(args.get('height', 400))
|
height = int(args.get('height', 400))
|
||||||
# Adjust to 4k resolution 3,840x2,160
|
# Adjust to 4k resolution 1920x1080
|
||||||
scaling = 3840.0 / float(max(width, height))
|
scaling = 1920.0 / float(max(width, height))
|
||||||
width = int(scaling * width)
|
width = int(scaling * width)
|
||||||
height = int(scaling * height)
|
height = int(scaling * height)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# e.g. playlists
|
# e.g. playlists
|
||||||
width = 3840
|
pass
|
||||||
height = 3840
|
|
||||||
artwork = '%s?width=%s&height=%s' % (artwork, width, height)
|
artwork = '%s?width=%s&height=%s' % (artwork, width, height)
|
||||||
artwork = ('%s/photo/:/transcode?width=3840&height=3840&'
|
artwork = ('%s/photo/:/transcode?width=1920&height=1920&'
|
||||||
'minSize=1&upscale=0&url=%s'
|
'minSize=1&upscale=0&url=%s'
|
||||||
% (app.CONN.server, quote(artwork)))
|
% (app.CONN.server, quote(artwork)))
|
||||||
artwork = self.attach_plex_token_to_url(artwork)
|
artwork = self.attach_plex_token_to_url(artwork)
|
||||||
return 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):
|
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
|
Gets the URLs to the Plex artwork. Dict keys will be missing if there
|
||||||
is no corresponding artwork.
|
is no corresponding artwork.
|
||||||
Pass kodi_id and kodi_type to grab the artwork saved in the Kodi DB
|
Pass kodi_id and kodi_type to grab the artwork saved in the Kodi DB
|
||||||
(thus potentially more artwork, e.g. clearart, discart)
|
(thus potentially more artwork, e.g. clearart, discart).
|
||||||
|
|
||||||
Output ('max' version)
|
Output ('max' version)
|
||||||
{
|
{
|
||||||
|
@ -905,44 +984,9 @@ class API(object):
|
||||||
Passing full_artwork=True returns ALL the artwork for the item, so not
|
Passing full_artwork=True returns ALL the artwork for the item, so not
|
||||||
just 'thumb' for episodes, but also season and show artwork
|
just 'thumb' for episodes, but also season and show artwork
|
||||||
"""
|
"""
|
||||||
artworks = {}
|
|
||||||
if self.plex_type() == v.PLEX_TYPE_EPISODE:
|
if self.plex_type() == v.PLEX_TYPE_EPISODE:
|
||||||
# Artwork lookup for episodes is broken for addon paths
|
return self.artwork_episode(full_artwork)
|
||||||
# Episodes is a bit special, only get the thumb, because all
|
artworks = {}
|
||||||
# the other artwork will be saved under season and show
|
|
||||||
# EXCEPT if you're constructing a listitem
|
|
||||||
if not full_artwork:
|
|
||||||
art = self.one_artwork('thumb')
|
|
||||||
if art:
|
|
||||||
artworks['thumb'] = art
|
|
||||||
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
|
|
||||||
if not full_artwork:
|
|
||||||
return artworks
|
|
||||||
with PlexDB(lock=False) as plexdb:
|
|
||||||
db_item = plexdb.item_by_id(self.plex_id(),
|
|
||||||
v.PLEX_TYPE_EPISODE)
|
|
||||||
if db_item:
|
|
||||||
season_id = db_item['parent_id']
|
|
||||||
show_id = db_item['grandparent_id']
|
|
||||||
else:
|
|
||||||
return artworks
|
|
||||||
# Grab artwork from the season
|
|
||||||
with KodiVideoDB(lock=False) as kodidb:
|
|
||||||
season_art = kodidb.get_art(season_id, v.KODI_TYPE_SEASON)
|
|
||||||
for kodi_art in season_art:
|
|
||||||
artworks['season.%s' % kodi_art] = season_art[kodi_art]
|
|
||||||
# Grab more artwork from the show
|
|
||||||
with KodiVideoDB(lock=False) as kodidb:
|
|
||||||
show_art = kodidb.get_art(show_id, v.KODI_TYPE_SHOW)
|
|
||||||
for kodi_art in show_art:
|
|
||||||
artworks['tvshow.%s' % kodi_art] = show_art[kodi_art]
|
|
||||||
return artworks
|
|
||||||
|
|
||||||
if kodi_id:
|
if kodi_id:
|
||||||
# in Kodi database, potentially with additional e.g. clearart
|
# in Kodi database, potentially with additional e.g. clearart
|
||||||
if self.plex_type() in v.PLEX_VIDEOTYPES:
|
if self.plex_type() in v.PLEX_VIDEOTYPES:
|
||||||
|
@ -952,9 +996,6 @@ class API(object):
|
||||||
with KodiMusicDB(lock=False) as kodidb:
|
with KodiMusicDB(lock=False) as kodidb:
|
||||||
return kodidb.get_art(kodi_id, kodi_type)
|
return kodidb.get_art(kodi_id, kodi_type)
|
||||||
|
|
||||||
# Grab artwork from Plex
|
|
||||||
# if self.plex_type() == v.PLEX_TYPE_EPISODE:
|
|
||||||
|
|
||||||
for kodi_artwork, plex_artwork in v.KODI_TO_PLEX_ARTWORK.iteritems():
|
for kodi_artwork, plex_artwork in v.KODI_TO_PLEX_ARTWORK.iteritems():
|
||||||
art = self.one_artwork(plex_artwork)
|
art = self.one_artwork(plex_artwork)
|
||||||
if art:
|
if art:
|
||||||
|
@ -976,6 +1017,10 @@ class API(object):
|
||||||
art = self.one_artwork('thumb')
|
art = self.one_artwork('thumb')
|
||||||
if art:
|
if art:
|
||||||
artworks['thumb'] = art
|
artworks['thumb'] = art
|
||||||
|
if self.plex_type() == v.PLEX_TYPE_PLAYLIST:
|
||||||
|
art = self.one_artwork('composite')
|
||||||
|
if art:
|
||||||
|
artworks['thumb'] = art
|
||||||
return artworks
|
return artworks
|
||||||
|
|
||||||
def fanart_artwork(self, artworks):
|
def fanart_artwork(self, artworks):
|
||||||
|
@ -1649,6 +1694,96 @@ class API(object):
|
||||||
pass
|
pass
|
||||||
return listitem
|
return listitem
|
||||||
|
|
||||||
|
def _create_folder_listitem(self, listitem=None):
|
||||||
|
"""
|
||||||
|
Use for video items only
|
||||||
|
Call on a child level of PMS xml response (e.g. in a for loop)
|
||||||
|
|
||||||
|
listitem : existing xbmcgui.ListItem to work with
|
||||||
|
otherwise, a new one is created
|
||||||
|
append_show_title : True to append TV show title to episode title
|
||||||
|
append_sxxexx : True to append SxxExx to episode title
|
||||||
|
|
||||||
|
Returns XBMC listitem for this PMS library item
|
||||||
|
"""
|
||||||
|
title = self.title()
|
||||||
|
typus = self.plex_type()
|
||||||
|
|
||||||
|
if listitem is None:
|
||||||
|
listitem = ListItem(title)
|
||||||
|
else:
|
||||||
|
listitem.setLabel(title)
|
||||||
|
# Necessary; Kodi won't start video otherwise!
|
||||||
|
listitem.setProperty('IsPlayable', 'true')
|
||||||
|
# Video items, e.g. movies and episodes or clips
|
||||||
|
people = self.people()
|
||||||
|
userdata = self.userdata()
|
||||||
|
metadata = {
|
||||||
|
'genre': self.genre_list(),
|
||||||
|
'country': self.country_list(),
|
||||||
|
'year': self.year(),
|
||||||
|
'rating': self.audience_rating(),
|
||||||
|
'playcount': userdata['PlayCount'],
|
||||||
|
'cast': people['Cast'],
|
||||||
|
'director': people['Director'],
|
||||||
|
'plot': self.plot(),
|
||||||
|
'sorttitle': self.sorttitle(),
|
||||||
|
'duration': userdata['Runtime'],
|
||||||
|
'studio': self.music_studio_list(),
|
||||||
|
'tagline': self.tagline(),
|
||||||
|
'writer': people.get('Writer'),
|
||||||
|
'premiered': self.premiere_date(),
|
||||||
|
'dateadded': self.date_created(),
|
||||||
|
'lastplayed': userdata['LastPlayedDate'],
|
||||||
|
'mpaa': self.content_rating(),
|
||||||
|
'aired': self.premiere_date(),
|
||||||
|
}
|
||||||
|
# Do NOT set resumetime - otherwise Kodi always resumes at that time
|
||||||
|
# even if the user chose to start element from the beginning
|
||||||
|
# listitem.setProperty('resumetime', str(userdata['Resume']))
|
||||||
|
listitem.setProperty('totaltime', str(userdata['Runtime']))
|
||||||
|
|
||||||
|
if typus == v.PLEX_TYPE_EPISODE:
|
||||||
|
metadata['mediatype'] = 'episode'
|
||||||
|
_, _, show, season, episode = self.episode_data()
|
||||||
|
season = -1 if season is None else int(season)
|
||||||
|
episode = -1 if episode is None else int(episode)
|
||||||
|
metadata['episode'] = episode
|
||||||
|
metadata['sortepisode'] = episode
|
||||||
|
metadata['season'] = season
|
||||||
|
metadata['sortseason'] = season
|
||||||
|
metadata['tvshowtitle'] = show
|
||||||
|
if season and episode:
|
||||||
|
if append_sxxexx is True:
|
||||||
|
title = "S%.2dE%.2d - %s" % (season, episode, title)
|
||||||
|
if append_show_title is True:
|
||||||
|
title = "%s - %s " % (show, title)
|
||||||
|
if append_show_title or append_sxxexx:
|
||||||
|
listitem.setLabel(title)
|
||||||
|
elif typus == v.PLEX_TYPE_MOVIE:
|
||||||
|
metadata['mediatype'] = 'movie'
|
||||||
|
else:
|
||||||
|
# E.g. clips, trailers, ...
|
||||||
|
pass
|
||||||
|
|
||||||
|
plex_id = self.plex_id()
|
||||||
|
listitem.setProperty('plexid', str(plex_id))
|
||||||
|
with PlexDB() as plexdb:
|
||||||
|
db_item = plexdb.item_by_id(plex_id, self.plex_type())
|
||||||
|
if db_item:
|
||||||
|
metadata['dbid'] = db_item['kodi_id']
|
||||||
|
metadata['title'] = title
|
||||||
|
# Expensive operation
|
||||||
|
listitem.setInfo('video', infoLabels=metadata)
|
||||||
|
try:
|
||||||
|
# Add context menu entry for information screen
|
||||||
|
listitem.addContextMenuItems([(utils.lang(30032),
|
||||||
|
'XBMC.Action(Info)',)])
|
||||||
|
except TypeError:
|
||||||
|
# Kodi fuck-up
|
||||||
|
pass
|
||||||
|
return listitem
|
||||||
|
|
||||||
def disc_number(self):
|
def disc_number(self):
|
||||||
"""
|
"""
|
||||||
Returns the song's disc number as an int or None if not found
|
Returns the song's disc number as an int or None if not found
|
||||||
|
|
|
@ -74,7 +74,10 @@ class PlexDBBase(object):
|
||||||
answ = self.album(plex_id)
|
answ = self.album(plex_id)
|
||||||
elif plex_type == v.PLEX_TYPE_ARTIST:
|
elif plex_type == v.PLEX_TYPE_ARTIST:
|
||||||
answ = self.artist(plex_id)
|
answ = self.artist(plex_id)
|
||||||
else:
|
elif plex_type in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_PHOTO, v.PLEX_TYPE_PLAYLIST):
|
||||||
|
# Will never be synched to Kodi
|
||||||
|
pass
|
||||||
|
elif plex_type is None:
|
||||||
# SLOW - lookup plex_id in all our tables
|
# SLOW - lookup plex_id in all our tables
|
||||||
for kind in (v.PLEX_TYPE_MOVIE,
|
for kind in (v.PLEX_TYPE_MOVIE,
|
||||||
v.PLEX_TYPE_EPISODE,
|
v.PLEX_TYPE_EPISODE,
|
||||||
|
|
|
@ -819,7 +819,12 @@ def get_plex_sections():
|
||||||
"""
|
"""
|
||||||
Returns all Plex sections (libraries) of the PMS as an etree xml
|
Returns all Plex sections (libraries) of the PMS as an etree xml
|
||||||
"""
|
"""
|
||||||
return DU().downloadUrl('{server}/library/sections')
|
xml = DU().downloadUrl('{server}/library/sections')
|
||||||
|
try:
|
||||||
|
xml[0].attrib
|
||||||
|
except (TypeError, IndexError, AttributeError):
|
||||||
|
xml = None
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
def init_plex_playqueue(plex_id, librarySectionUUID, mediatype='movie',
|
def init_plex_playqueue(plex_id, librarySectionUUID, mediatype='movie',
|
||||||
|
|
|
@ -86,9 +86,6 @@ class Service(object):
|
||||||
for prop in WINDOW_PROPERTIES:
|
for prop in WINDOW_PROPERTIES:
|
||||||
utils.window(prop, clear=True)
|
utils.window(prop, clear=True)
|
||||||
|
|
||||||
# Load/Reset PKC entirely - important for user/Kodi profile switch
|
|
||||||
# Clear video nodes properties
|
|
||||||
library_sync.VideoNodes().clearProperties()
|
|
||||||
clientinfo.getDeviceId()
|
clientinfo.getDeviceId()
|
||||||
# Init time-offset between Kodi and Plex
|
# Init time-offset between Kodi and Plex
|
||||||
timing.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset') or 0.0)
|
timing.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset') or 0.0)
|
||||||
|
@ -212,10 +209,8 @@ class Service(object):
|
||||||
|
|
||||||
def switch_plex_user(self):
|
def switch_plex_user(self):
|
||||||
self.log_out()
|
self.log_out()
|
||||||
# First remove playlists of old user
|
# First remove playlists and video nodes of old user
|
||||||
utils.delete_playlists()
|
library_sync.delete_files()
|
||||||
# Remove video nodes
|
|
||||||
utils.delete_nodes()
|
|
||||||
app.ACCOUNT.set_unauthenticated()
|
app.ACCOUNT.set_unauthenticated()
|
||||||
# Force full sync after login
|
# Force full sync after login
|
||||||
library_sync.force_full_sync()
|
library_sync.force_full_sync()
|
||||||
|
@ -300,9 +295,7 @@ class Service(object):
|
||||||
from .library_sync import sections
|
from .library_sync import sections
|
||||||
try:
|
try:
|
||||||
# Get newest sections from the PMS
|
# Get newest sections from the PMS
|
||||||
if not sections.sync_from_pms(self):
|
if not sections.sync_from_pms(self, pick_libraries=True):
|
||||||
return
|
|
||||||
if not sections.choose_libraries():
|
|
||||||
return
|
return
|
||||||
# Force a full sync
|
# Force a full sync
|
||||||
app.SYNC.run_lib_scan = 'full'
|
app.SYNC.run_lib_scan = 'full'
|
||||||
|
@ -508,6 +501,9 @@ class Service(object):
|
||||||
# Tell all threads to terminate (e.g. several lib sync threads)
|
# Tell all threads to terminate (e.g. several lib sync threads)
|
||||||
LOG.debug('Aborting all threads')
|
LOG.debug('Aborting all threads')
|
||||||
app.APP.stop_pkc = True
|
app.APP.stop_pkc = True
|
||||||
|
# Load/Reset PKC entirely - important for user/Kodi profile switch
|
||||||
|
# Clear video nodes properties
|
||||||
|
library_sync.clear_window_vars()
|
||||||
# Will block until threads have quit
|
# Will block until threads have quit
|
||||||
app.APP.stop_threads()
|
app.APP.stop_threads()
|
||||||
utils.window('plex_service_started', clear=True)
|
utils.window('plex_service_started', clear=True)
|
||||||
|
|
|
@ -17,6 +17,7 @@ from functools import wraps
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
import gc
|
import gc
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcaddon
|
import xbmcaddon
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
|
@ -429,11 +430,10 @@ def wipe_database():
|
||||||
Will also delete all cached artwork.
|
Will also delete all cached artwork.
|
||||||
"""
|
"""
|
||||||
LOG.warn('Start wiping')
|
LOG.warn('Start wiping')
|
||||||
# Clean up the playlists
|
from .library_sync.sections import delete_files
|
||||||
delete_playlists()
|
|
||||||
# Clean up the video nodes
|
|
||||||
delete_nodes()
|
|
||||||
from . import kodi_db, plex_db
|
from . import kodi_db, plex_db
|
||||||
|
# Clean up the playlists and video nodes
|
||||||
|
delete_files()
|
||||||
# First get the paths to all synced playlists
|
# First get the paths to all synced playlists
|
||||||
playlist_paths = []
|
playlist_paths = []
|
||||||
try:
|
try:
|
||||||
|
@ -814,76 +814,6 @@ class XmlKodiSetting(object):
|
||||||
return element
|
return element
|
||||||
|
|
||||||
|
|
||||||
def playlist_xsp(mediatype, tagname, viewid, viewtype="", delete=False):
|
|
||||||
"""
|
|
||||||
Feed with tagname as unicode
|
|
||||||
"""
|
|
||||||
path = path_ops.translate_path("special://profile/playlists/video/")
|
|
||||||
if viewtype == "mixed":
|
|
||||||
plname = "%s - %s" % (tagname, mediatype)
|
|
||||||
xsppath = "%sPlex %s - %s.xsp" % (path, viewid, mediatype)
|
|
||||||
else:
|
|
||||||
plname = tagname
|
|
||||||
xsppath = "%sPlex %s.xsp" % (path, viewid)
|
|
||||||
|
|
||||||
# Create the playlist directory
|
|
||||||
if not path_ops.exists(path):
|
|
||||||
LOG.info("Creating directory: %s", path)
|
|
||||||
path_ops.makedirs(path)
|
|
||||||
|
|
||||||
# Only add the playlist if it doesn't already exists
|
|
||||||
if path_ops.exists(xsppath):
|
|
||||||
LOG.info('Path %s does exist', xsppath)
|
|
||||||
if delete:
|
|
||||||
path_ops.remove(xsppath)
|
|
||||||
LOG.info("Successfully removed playlist: %s.", tagname)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Using write process since there's no guarantee the xml declaration works
|
|
||||||
# with etree
|
|
||||||
kinds = {
|
|
||||||
'homevideos': 'movies',
|
|
||||||
'movie': 'movies',
|
|
||||||
'show': 'tvshows'
|
|
||||||
}
|
|
||||||
LOG.info("Writing playlist file to: %s", xsppath)
|
|
||||||
with open(path_ops.encode_path(xsppath), 'wb') as filer:
|
|
||||||
filer.write(try_encode(
|
|
||||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
|
|
||||||
'<smartplaylist type="%s">\n\t'
|
|
||||||
'<name>Plex %s</name>\n\t'
|
|
||||||
'<match>all</match>\n\t'
|
|
||||||
'<rule field="tag" operator="is">\n\t\t'
|
|
||||||
'<value>%s</value>\n\t'
|
|
||||||
'</rule>\n'
|
|
||||||
'</smartplaylist>\n'
|
|
||||||
% (kinds.get(mediatype, mediatype), plname, tagname)))
|
|
||||||
LOG.info("Successfully added playlist: %s", tagname)
|
|
||||||
|
|
||||||
|
|
||||||
def delete_playlists():
|
|
||||||
"""
|
|
||||||
Clean up the playlists
|
|
||||||
"""
|
|
||||||
path = path_ops.translate_path('special://profile/playlists/video/')
|
|
||||||
for root, _, files in path_ops.walk(path):
|
|
||||||
for file in files:
|
|
||||||
if file.startswith('Plex'):
|
|
||||||
path_ops.remove(path_ops.path.join(root, file))
|
|
||||||
|
|
||||||
|
|
||||||
def delete_nodes():
|
|
||||||
"""
|
|
||||||
Clean up video nodes
|
|
||||||
"""
|
|
||||||
path = path_ops.translate_path("special://profile/library/video/")
|
|
||||||
for root, dirs, _ in path_ops.walk(path):
|
|
||||||
for directory in dirs:
|
|
||||||
if directory.startswith('Plex-'):
|
|
||||||
path_ops.rmtree(path_ops.path.join(root, directory))
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def generate_file_md5(path):
|
def generate_file_md5(path):
|
||||||
"""
|
"""
|
||||||
Generates the md5 hash value for the file located at path [unicode].
|
Generates the md5 hash value for the file located at path [unicode].
|
||||||
|
|
|
@ -179,6 +179,9 @@ PLEX_TYPE_MUSICVIDEO = 'musicvideo'
|
||||||
|
|
||||||
PLEX_TYPE_PHOTO = 'photo'
|
PLEX_TYPE_PHOTO = 'photo'
|
||||||
|
|
||||||
|
PLEX_TYPE_PLAYLIST = 'playlist'
|
||||||
|
PLEX_TYPE_CHANNEL = 'channel'
|
||||||
|
|
||||||
# Used for /:/timeline XML messages
|
# Used for /:/timeline XML messages
|
||||||
PLEX_PLAYLIST_TYPE_VIDEO = 'video'
|
PLEX_PLAYLIST_TYPE_VIDEO = 'video'
|
||||||
PLEX_PLAYLIST_TYPE_AUDIO = 'music'
|
PLEX_PLAYLIST_TYPE_AUDIO = 'music'
|
||||||
|
@ -212,6 +215,8 @@ KODI_TYPE_MUSICVIDEO = 'musicvideo'
|
||||||
|
|
||||||
KODI_TYPE_PHOTO = 'photo'
|
KODI_TYPE_PHOTO = 'photo'
|
||||||
|
|
||||||
|
KODI_TYPE_PLAYLIST = 'playlist'
|
||||||
|
|
||||||
KODI_VIDEOTYPES = (
|
KODI_VIDEOTYPES = (
|
||||||
KODI_TYPE_VIDEO,
|
KODI_TYPE_VIDEO,
|
||||||
KODI_TYPE_MOVIE,
|
KODI_TYPE_MOVIE,
|
||||||
|
@ -251,7 +256,12 @@ ADDON_TYPE = {
|
||||||
PLEX_TYPE_MOVIE: 'plugin.video.plexkodiconnect.movies',
|
PLEX_TYPE_MOVIE: 'plugin.video.plexkodiconnect.movies',
|
||||||
PLEX_TYPE_CLIP: 'plugin.video.plexkodiconnect.movies',
|
PLEX_TYPE_CLIP: 'plugin.video.plexkodiconnect.movies',
|
||||||
PLEX_TYPE_EPISODE: 'plugin.video.plexkodiconnect.tvshows',
|
PLEX_TYPE_EPISODE: 'plugin.video.plexkodiconnect.tvshows',
|
||||||
PLEX_TYPE_SONG: 'plugin.video.plexkodiconnect'
|
PLEX_TYPE_SEASON: 'plugin.video.plexkodiconnect.tvshows',
|
||||||
|
PLEX_TYPE_SHOW: 'plugin.video.plexkodiconnect.tvshows',
|
||||||
|
PLEX_TYPE_SONG: 'plugin.video.plexkodiconnect',
|
||||||
|
PLEX_TYPE_ALBUM: 'plugin.video.plexkodiconnect',
|
||||||
|
PLEX_TYPE_ARTIST: 'plugin.video.plexkodiconnect',
|
||||||
|
PLEX_TYPE_PLAYLIST: 'plugin.video.plexkodiconnect'
|
||||||
}
|
}
|
||||||
|
|
||||||
ITEMTYPE_FROM_PLEXTYPE = {
|
ITEMTYPE_FROM_PLEXTYPE = {
|
||||||
|
@ -276,6 +286,7 @@ ITEMTYPE_FROM_KODITYPE = {
|
||||||
|
|
||||||
KODITYPE_FROM_PLEXTYPE = {
|
KODITYPE_FROM_PLEXTYPE = {
|
||||||
PLEX_TYPE_MOVIE: KODI_TYPE_MOVIE,
|
PLEX_TYPE_MOVIE: KODI_TYPE_MOVIE,
|
||||||
|
PLEX_TYPE_CLIP: KODI_TYPE_CLIP,
|
||||||
PLEX_TYPE_EPISODE: KODI_TYPE_EPISODE,
|
PLEX_TYPE_EPISODE: KODI_TYPE_EPISODE,
|
||||||
PLEX_TYPE_SEASON: KODI_TYPE_SEASON,
|
PLEX_TYPE_SEASON: KODI_TYPE_SEASON,
|
||||||
PLEX_TYPE_SHOW: KODI_TYPE_SHOW,
|
PLEX_TYPE_SHOW: KODI_TYPE_SHOW,
|
||||||
|
@ -284,7 +295,8 @@ KODITYPE_FROM_PLEXTYPE = {
|
||||||
PLEX_TYPE_ALBUM: KODI_TYPE_ALBUM,
|
PLEX_TYPE_ALBUM: KODI_TYPE_ALBUM,
|
||||||
PLEX_TYPE_PHOTO: KODI_TYPE_PHOTO,
|
PLEX_TYPE_PHOTO: KODI_TYPE_PHOTO,
|
||||||
'XXXXXX': 'musicvideo',
|
'XXXXXX': 'musicvideo',
|
||||||
'XXXXXXX': 'genre'
|
'XXXXXXX': 'genre',
|
||||||
|
PLEX_TYPE_PLAYLIST: KODI_TYPE_PLAYLIST
|
||||||
}
|
}
|
||||||
|
|
||||||
PLEX_TYPE_FROM_KODI_TYPE = {
|
PLEX_TYPE_FROM_KODI_TYPE = {
|
||||||
|
@ -316,7 +328,6 @@ KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = {
|
||||||
PLEX_TYPE_PHOTO: KODI_TYPE_PHOTO
|
PLEX_TYPE_PHOTO: KODI_TYPE_PHOTO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
KODI_PLAYLIST_TYPE_FROM_KODI_TYPE = {
|
KODI_PLAYLIST_TYPE_FROM_KODI_TYPE = {
|
||||||
KODI_TYPE_VIDEO: KODI_TYPE_VIDEO,
|
KODI_TYPE_VIDEO: KODI_TYPE_VIDEO,
|
||||||
KODI_TYPE_MOVIE: KODI_TYPE_VIDEO,
|
KODI_TYPE_MOVIE: KODI_TYPE_VIDEO,
|
||||||
|
@ -344,6 +355,22 @@ REMAP_TYPE_FROM_PLEXTYPE = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ICON_FROM_PLEXTYPE = {
|
||||||
|
PLEX_TYPE_VIDEO: 'DefaultAddonAlbumInfo.png',
|
||||||
|
PLEX_TYPE_MOVIE: 'DefaultMovies.png',
|
||||||
|
PLEX_TYPE_EPISODE: 'DefaultTvShows.png',
|
||||||
|
PLEX_TYPE_SEASON: 'DefaultTvShows.png',
|
||||||
|
PLEX_TYPE_SHOW: 'DefaultTvShows.png',
|
||||||
|
PLEX_TYPE_CLIP: 'DefaultAddonAlbumInfo.png',
|
||||||
|
PLEX_TYPE_ARTIST: 'DefaultArtist.png',
|
||||||
|
PLEX_TYPE_ALBUM: 'DefaultAlbumCover.png',
|
||||||
|
PLEX_TYPE_SONG: 'DefaultMusicSongs.png',
|
||||||
|
PLEX_TYPE_AUDIO: 'DefaultAddonAlbumInfo.png',
|
||||||
|
PLEX_TYPE_PHOTO: 'DefaultPicture.png',
|
||||||
|
PLEX_TYPE_PLAYLIST: 'DefaultPlaylist.png',
|
||||||
|
'mixed': 'DefaultAddonAlbumInfo.png'
|
||||||
|
}
|
||||||
|
|
||||||
TRANSLATION_FROM_PLEXTYPE = {
|
TRANSLATION_FROM_PLEXTYPE = {
|
||||||
PLEX_TYPE_MOVIE: 342,
|
PLEX_TYPE_MOVIE: 342,
|
||||||
PLEX_TYPE_EPISODE: 20360,
|
PLEX_TYPE_EPISODE: 20360,
|
||||||
|
@ -396,6 +423,21 @@ PLEX_TYPE_NUMBER_FROM_PLEX_TYPE = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# To be used with e.g. Kodi Widgets
|
||||||
|
MEDIATYPE_FROM_PLEX_TYPE = {
|
||||||
|
PLEX_TYPE_MOVIE: 'movies',
|
||||||
|
PLEX_TYPE_SHOW: 'tvshows',
|
||||||
|
PLEX_TYPE_SEASON: 'tvshows',
|
||||||
|
PLEX_TYPE_EPISODE: 'episodes',
|
||||||
|
PLEX_TYPE_ARTIST: 'artists',
|
||||||
|
PLEX_TYPE_ALBUM: 'albumbs',
|
||||||
|
PLEX_TYPE_SONG: 'songs',
|
||||||
|
PLEX_TYPE_CLIP: 'videos',
|
||||||
|
PLEX_TYPE_SET: 'movies',
|
||||||
|
PLEX_TYPE_PHOTO: 'photos',
|
||||||
|
'mixed': 'tvshows',
|
||||||
|
}
|
||||||
|
|
||||||
KODI_TO_PLEX_ARTWORK = {
|
KODI_TO_PLEX_ARTWORK = {
|
||||||
'poster': 'thumb',
|
'poster': 'thumb',
|
||||||
'banner': 'banner',
|
'banner': 'banner',
|
||||||
|
@ -403,10 +445,10 @@ KODI_TO_PLEX_ARTWORK = {
|
||||||
}
|
}
|
||||||
|
|
||||||
KODI_TO_PLEX_ARTWORK_EPISODE = {
|
KODI_TO_PLEX_ARTWORK_EPISODE = {
|
||||||
'thumb': 'thumb',
|
'season.poster': 'parentThumb',
|
||||||
'poster': 'grandparentThumb',
|
'tvshow.poster': 'grandparentThumb',
|
||||||
'banner': 'banner',
|
'tvshow.fanart': 'grandparentArt',
|
||||||
'fanart': 'art'
|
# 'tvshow.banner': 'banner' # Not included in PMS episode metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
# Might be implemented in the future: 'icon', 'landscape' (16:9)
|
# Might be implemented in the future: 'icon', 'landscape' (16:9)
|
||||||
|
|
624
resources/lib/widgets.py
Normal file
624
resources/lib/widgets.py
Normal file
|
@ -0,0 +1,624 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Code from script.module.metadatautils, kodidb.py
|
||||||
|
|
||||||
|
Loads of different functions called in SEPARATE Python instances through
|
||||||
|
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
|
||||||
|
import urllib
|
||||||
|
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')
|
||||||
|
|
||||||
|
# To easily use threadpool, we can only pass one argument
|
||||||
|
PLEX_TYPE = None
|
||||||
|
SECTION_ID = None
|
||||||
|
APPEND_SHOW_TITLE = None
|
||||||
|
APPEND_SXXEXX = None
|
||||||
|
SYNCHED = True
|
||||||
|
# Need to chain the PMS keys
|
||||||
|
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(items) 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'''
|
||||||
|
if not image:
|
||||||
|
return ""
|
||||||
|
if "music@" in image:
|
||||||
|
# fix for embedded images
|
||||||
|
thumbcache = xbmc.getCacheThumbName(image).replace(".tbn", ".jpg")
|
||||||
|
thumbcache = "special://thumbnails/%s/%s" % (thumbcache[0], thumbcache)
|
||||||
|
if not xbmcvfs.exists(thumbcache):
|
||||||
|
xbmcvfs.copy(image, thumbcache)
|
||||||
|
image = thumbcache
|
||||||
|
if image and "image://" in image:
|
||||||
|
image = image.replace("image://", "")
|
||||||
|
image = urllib.unquote(image.encode("utf-8"))
|
||||||
|
if image.endswith("/"):
|
||||||
|
image = image[:-1]
|
||||||
|
if not isinstance(image, unicode):
|
||||||
|
image = image.decode("utf8")
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def generate_item(xml_element):
|
||||||
|
"""
|
||||||
|
Meant to be consumed by metadatautils.kodidb.prepare_listitem(), and then
|
||||||
|
subsequently by metadatautils.kodidb.create_listitem()
|
||||||
|
|
||||||
|
Do NOT set resumetime - otherwise Kodi always resumes at that time
|
||||||
|
even if the user chose to start element from the beginning
|
||||||
|
listitem.setProperty('resumetime', str(userdata['Resume']))
|
||||||
|
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
return _generate_content(xml_element)
|
||||||
|
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)
|
||||||
|
utils.ERROR(notify=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_folder(xml_element):
|
||||||
|
'''Generates "folder"/"directory" items that user can further navigate'''
|
||||||
|
api = API(xml_element)
|
||||||
|
art = api.artwork()
|
||||||
|
return {
|
||||||
|
'title': api.title(),
|
||||||
|
'label': api.title(),
|
||||||
|
'file': api.directory_path(section_id=SECTION_ID,
|
||||||
|
plex_type=PLEX_TYPE,
|
||||||
|
old_key=KEY),
|
||||||
|
'icon': 'DefaultFolder.png',
|
||||||
|
'art': {
|
||||||
|
'thumb': art['thumb'] if 'thumb' in art else
|
||||||
|
(art['poster'] if 'poster' in art else
|
||||||
|
'special://home/addons/%s/icon.png' % v.ADDON_ID),
|
||||||
|
'fanart': art['fanart'] if 'fanart' in art else
|
||||||
|
'special://home/addons/%s/fanart.jpg' % v.ADDON_ID},
|
||||||
|
'isFolder': True,
|
||||||
|
'type': '',
|
||||||
|
'IsPlayable': 'false',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
# 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)
|
||||||
|
else:
|
||||||
|
people = api.people()
|
||||||
|
cast = [{
|
||||||
|
'name': x[0],
|
||||||
|
'thumbnail': x[1],
|
||||||
|
'role': x[2],
|
||||||
|
'order': x[3],
|
||||||
|
} for x in api.people_list()['actor']]
|
||||||
|
item = {
|
||||||
|
'cast': cast,
|
||||||
|
'country': api.country_list(),
|
||||||
|
'dateadded': api.date_created(), # e.g '2019-01-03 19:40:59'
|
||||||
|
'director': people['Director'], # list of [str]
|
||||||
|
'duration': userdata['Runtime'],
|
||||||
|
'episode': episode_no,
|
||||||
|
# 'file': '', # e.g. 'videodb://tvshows/titles/20'
|
||||||
|
'genre': api.genre_list(),
|
||||||
|
# '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'
|
||||||
|
'mpaa': api.content_rating(), # e.g. 'TV-MA'
|
||||||
|
'originaltitle': '', # e.g. 'Titans (2018)'
|
||||||
|
'playcount': userdata['PlayCount'], # [int]
|
||||||
|
'plot': api.plot(), # [str]
|
||||||
|
'plotoutline': api.tagline(),
|
||||||
|
'premiered': api.premiere_date(), # '2018-10-12'
|
||||||
|
'rating': api.audience_rating(), # [float]
|
||||||
|
'season': season_no,
|
||||||
|
'sorttitle': api.sorttitle(), # 'Titans (2018)'
|
||||||
|
'studio': api.music_studio_list(), # e.g. 'DC Universe'
|
||||||
|
'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,
|
||||||
|
'trailer': api.trailer(),
|
||||||
|
'tvshowtitle': tvshowtitle,
|
||||||
|
'uniqueid': {
|
||||||
|
'imdbnumber': api.provider('imdb') or '',
|
||||||
|
'tvdb_id': api.provider('tvdb') or ''
|
||||||
|
},
|
||||||
|
'votes': '0', # [str]!
|
||||||
|
'writer': people['Writer'], # list of [str]
|
||||||
|
'year': api.year(), # [int]
|
||||||
|
}
|
||||||
|
|
||||||
|
if plex_type in (v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SEASON, v.PLEX_TYPE_SHOW):
|
||||||
|
leaves = api.leave_count()
|
||||||
|
if leaves:
|
||||||
|
item['extraproperties'] = leaves
|
||||||
|
# Add all the artwork we can
|
||||||
|
item['art'] = api.artwork(full_artwork=True)
|
||||||
|
# Add all info for e.g. video and audio streams
|
||||||
|
item['streamdetails'] = api.mediastreams()
|
||||||
|
# Cleanup required due to the way metadatautils works
|
||||||
|
if not item['lastplayed']:
|
||||||
|
del item['lastplayed']
|
||||||
|
for stream in item['streamdetails']['video']:
|
||||||
|
stream['height'] = utils.cast(int, stream['height'])
|
||||||
|
stream['width'] = utils.cast(int, stream['width'])
|
||||||
|
stream['aspect'] = utils.cast(float, stream['aspect'])
|
||||||
|
item['streamdetails']['subtitle'] = [{'language': x} for x in item['streamdetails']['subtitle']]
|
||||||
|
# Resume point
|
||||||
|
resume = api.resume_point()
|
||||||
|
if resume:
|
||||||
|
item['resume'] = {
|
||||||
|
'position': resume,
|
||||||
|
'total': userdata['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 APPEND_SXXEXX is True:
|
||||||
|
item['label'] = "S%.2dE%.2d - %s" % (season_no, episode_no, item['label'])
|
||||||
|
if APPEND_SHOW_TITLE is True:
|
||||||
|
item['label'] = "%s - %s " % (tvshowtitle, item['label'])
|
||||||
|
|
||||||
|
# Determine the path for this item
|
||||||
|
key = api.path_and_plex_id()
|
||||||
|
if key.startswith('/system/services') or key.startswith('http'):
|
||||||
|
params = {
|
||||||
|
'mode': 'plex_node',
|
||||||
|
'key': key,
|
||||||
|
'offset': xml_element.attrib.get('viewOffset', '0'),
|
||||||
|
}
|
||||||
|
url = "plugin://%s?%s" % (v.ADDON_ID, urllib.urlencode(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:
|
||||||
|
# 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',
|
||||||
|
'plugin.video.plexkodiconnect')
|
||||||
|
item['file'] = url
|
||||||
|
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
|
||||||
|
listitems
|
||||||
|
|
||||||
|
Code from script.module.metadatautils, kodidb.py
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# fix values returned from json to be used as listitem values
|
||||||
|
properties = item.get("extraproperties", {})
|
||||||
|
|
||||||
|
# set type
|
||||||
|
for idvar in [
|
||||||
|
('episode', 'DefaultTVShows.png'),
|
||||||
|
('tvshow', 'DefaultTVShows.png'),
|
||||||
|
('movie', 'DefaultMovies.png'),
|
||||||
|
('song', 'DefaultAudio.png'),
|
||||||
|
('album', 'DefaultAudio.png'),
|
||||||
|
('artist', 'DefaultArtist.png'),
|
||||||
|
('musicvideo', 'DefaultMusicVideos.png'),
|
||||||
|
('recording', 'DefaultTVShows.png'),
|
||||||
|
('channel', 'DefaultAddonPVRClient.png')]:
|
||||||
|
dbid = item.get(idvar[0] + "id")
|
||||||
|
if dbid:
|
||||||
|
properties["DBID"] = str(dbid)
|
||||||
|
if not item.get("type"):
|
||||||
|
item["type"] = idvar[0]
|
||||||
|
if not item.get("icon"):
|
||||||
|
item["icon"] = idvar[1]
|
||||||
|
break
|
||||||
|
|
||||||
|
# general properties
|
||||||
|
if "genre" in item and isinstance(item['genre'], list):
|
||||||
|
item["genre"] = " / ".join(item['genre'])
|
||||||
|
if "studio" in item and isinstance(item['studio'], list):
|
||||||
|
item["studio"] = " / ".join(item['studio'])
|
||||||
|
if "writer" in item and isinstance(item['writer'], list):
|
||||||
|
item["writer"] = " / ".join(item['writer'])
|
||||||
|
if 'director' in item and isinstance(item['director'], list):
|
||||||
|
item["director"] = " / ".join(item['director'])
|
||||||
|
if 'artist' in item and not isinstance(item['artist'], list):
|
||||||
|
item["artist"] = [item['artist']]
|
||||||
|
if 'artist' not in item:
|
||||||
|
item["artist"] = []
|
||||||
|
if item['type'] == "album" and 'album' not in item and 'label' in item:
|
||||||
|
item['album'] = item['label']
|
||||||
|
if "duration" not in item and "runtime" in item:
|
||||||
|
if (item["runtime"] / 60) > 300:
|
||||||
|
item["duration"] = item["runtime"] / 60
|
||||||
|
else:
|
||||||
|
item["duration"] = item["runtime"]
|
||||||
|
if "plot" not in item and "comment" in item:
|
||||||
|
item["plot"] = item["comment"]
|
||||||
|
if "tvshowtitle" not in item and "showtitle" in item:
|
||||||
|
item["tvshowtitle"] = item["showtitle"]
|
||||||
|
if "premiered" not in item and "firstaired" in item:
|
||||||
|
item["premiered"] = item["firstaired"]
|
||||||
|
if "firstaired" in item and "aired" not in item:
|
||||||
|
item["aired"] = item["firstaired"]
|
||||||
|
if "imdbnumber" not in properties and "imdbnumber" in item:
|
||||||
|
properties["imdbnumber"] = item["imdbnumber"]
|
||||||
|
if "imdbnumber" not in properties and "uniqueid" in item:
|
||||||
|
for value in item["uniqueid"].values():
|
||||||
|
if value.startswith("tt"):
|
||||||
|
properties["imdbnumber"] = value
|
||||||
|
|
||||||
|
properties["dbtype"] = item["type"]
|
||||||
|
properties["DBTYPE"] = item["type"]
|
||||||
|
properties["type"] = item["type"]
|
||||||
|
properties["path"] = item.get("file")
|
||||||
|
|
||||||
|
# cast
|
||||||
|
list_cast = []
|
||||||
|
list_castandrole = []
|
||||||
|
item["cast_org"] = item.get("cast", [])
|
||||||
|
if "cast" in item and isinstance(item["cast"], list):
|
||||||
|
for castmember in item["cast"]:
|
||||||
|
if isinstance(castmember, dict):
|
||||||
|
list_cast.append(castmember.get("name", ""))
|
||||||
|
list_castandrole.append((castmember["name"], castmember["role"]))
|
||||||
|
else:
|
||||||
|
list_cast.append(castmember)
|
||||||
|
list_castandrole.append((castmember, ""))
|
||||||
|
|
||||||
|
item["cast"] = list_cast
|
||||||
|
item["castandrole"] = list_castandrole
|
||||||
|
|
||||||
|
if "season" in item and "episode" in item:
|
||||||
|
properties["episodeno"] = "s%se%s" % (item.get("season"), item.get("episode"))
|
||||||
|
if "resume" in item:
|
||||||
|
properties["resumetime"] = str(item['resume']['position'])
|
||||||
|
properties["totaltime"] = str(item['resume']['total'])
|
||||||
|
properties['StartOffset'] = str(item['resume']['position'])
|
||||||
|
|
||||||
|
# streamdetails
|
||||||
|
if "streamdetails" in item:
|
||||||
|
streamdetails = item["streamdetails"]
|
||||||
|
audiostreams = streamdetails.get('audio', [])
|
||||||
|
videostreams = streamdetails.get('video', [])
|
||||||
|
subtitles = streamdetails.get('subtitle', [])
|
||||||
|
if len(videostreams) > 0:
|
||||||
|
stream = videostreams[0]
|
||||||
|
height = stream.get("height", "")
|
||||||
|
width = stream.get("width", "")
|
||||||
|
if height and width:
|
||||||
|
resolution = ""
|
||||||
|
if width <= 720 and height <= 480:
|
||||||
|
resolution = "480"
|
||||||
|
elif width <= 768 and height <= 576:
|
||||||
|
resolution = "576"
|
||||||
|
elif width <= 960 and height <= 544:
|
||||||
|
resolution = "540"
|
||||||
|
elif width <= 1280 and height <= 720:
|
||||||
|
resolution = "720"
|
||||||
|
elif width <= 1920 and height <= 1080:
|
||||||
|
resolution = "1080"
|
||||||
|
elif width * height >= 6000000:
|
||||||
|
resolution = "4K"
|
||||||
|
properties["VideoResolution"] = resolution
|
||||||
|
if stream.get("codec", ""):
|
||||||
|
properties["VideoCodec"] = str(stream["codec"])
|
||||||
|
if stream.get("aspect", ""):
|
||||||
|
properties["VideoAspect"] = str(round(stream["aspect"], 2))
|
||||||
|
item["streamdetails"]["video"] = stream
|
||||||
|
|
||||||
|
# grab details of first audio stream
|
||||||
|
if len(audiostreams) > 0:
|
||||||
|
stream = audiostreams[0]
|
||||||
|
properties["AudioCodec"] = stream.get('codec', '')
|
||||||
|
properties["AudioChannels"] = str(stream.get('channels', ''))
|
||||||
|
properties["AudioLanguage"] = stream.get('language', '')
|
||||||
|
item["streamdetails"]["audio"] = stream
|
||||||
|
|
||||||
|
# grab details of first subtitle
|
||||||
|
if len(subtitles) > 0:
|
||||||
|
properties["SubtitleLanguage"] = subtitles[0].get('language', '')
|
||||||
|
item["streamdetails"]["subtitle"] = subtitles[0]
|
||||||
|
else:
|
||||||
|
item["streamdetails"] = {}
|
||||||
|
item["streamdetails"]["video"] = {'duration': item.get('duration', 0)}
|
||||||
|
|
||||||
|
# additional music properties
|
||||||
|
if 'album_description' in item:
|
||||||
|
properties["Album_Description"] = item.get('album_description')
|
||||||
|
|
||||||
|
# pvr properties
|
||||||
|
if "channellogo" in item:
|
||||||
|
properties["channellogo"] = item["channellogo"]
|
||||||
|
properties["channelicon"] = item["channellogo"]
|
||||||
|
if "episodename" in item:
|
||||||
|
properties["episodename"] = item["episodename"]
|
||||||
|
if "channel" in item:
|
||||||
|
properties["channel"] = item["channel"]
|
||||||
|
properties["channelname"] = item["channel"]
|
||||||
|
item["label2"] = item["title"]
|
||||||
|
|
||||||
|
# artwork
|
||||||
|
art = item.get("art", {})
|
||||||
|
if item["type"] in ["episode", "season"]:
|
||||||
|
if not art.get("fanart") and art.get("season.fanart"):
|
||||||
|
art["fanart"] = art["season.fanart"]
|
||||||
|
if not art.get("poster") and art.get("season.poster"):
|
||||||
|
art["poster"] = art["season.poster"]
|
||||||
|
if not art.get("landscape") and art.get("season.landscape"):
|
||||||
|
art["poster"] = art["season.landscape"]
|
||||||
|
if not art.get("fanart") and art.get("tvshow.fanart"):
|
||||||
|
art["fanart"] = art.get("tvshow.fanart")
|
||||||
|
if not art.get("poster") and art.get("tvshow.poster"):
|
||||||
|
art["poster"] = art.get("tvshow.poster")
|
||||||
|
if not art.get("clearlogo") and art.get("tvshow.clearlogo"):
|
||||||
|
art["clearlogo"] = art.get("tvshow.clearlogo")
|
||||||
|
if not art.get("banner") and art.get("tvshow.banner"):
|
||||||
|
art["banner"] = art.get("tvshow.banner")
|
||||||
|
if not art.get("landscape") and art.get("tvshow.landscape"):
|
||||||
|
art["landscape"] = art.get("tvshow.landscape")
|
||||||
|
if not art.get("fanart") and item.get('fanart'):
|
||||||
|
art["fanart"] = item.get('fanart')
|
||||||
|
if not art.get("thumb") and item.get('thumbnail'):
|
||||||
|
art["thumb"] = get_clean_image(item.get('thumbnail'))
|
||||||
|
if not art.get("thumb") and art.get('poster'):
|
||||||
|
art["thumb"] = get_clean_image(art.get('poster'))
|
||||||
|
if not art.get("thumb") and item.get('icon'):
|
||||||
|
art["thumb"] = get_clean_image(item.get('icon'))
|
||||||
|
if not item.get("thumbnail") and art.get('thumb'):
|
||||||
|
item["thumbnail"] = art["thumb"]
|
||||||
|
|
||||||
|
# clean art
|
||||||
|
for key, value in art.iteritems():
|
||||||
|
if not isinstance(value, (str, unicode)):
|
||||||
|
art[key] = ""
|
||||||
|
elif value:
|
||||||
|
art[key] = get_clean_image(value)
|
||||||
|
item["art"] = art
|
||||||
|
|
||||||
|
item["extraproperties"] = properties
|
||||||
|
|
||||||
|
# return the result
|
||||||
|
return item
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
utils.ERROR(notify=True)
|
||||||
|
LOG.error('item that caused crash: %s', item)
|
||||||
|
|
||||||
|
|
||||||
|
def create_listitem(item, as_tuple=True, offscreen=True):
|
||||||
|
"""
|
||||||
|
helper to create a kodi listitem from kodi compatible dict with mediainfo
|
||||||
|
|
||||||
|
WARNING: paths, so item['file'] for items NOT synched to the Kodi DB
|
||||||
|
shall NOT occur in the Kodi paths table!
|
||||||
|
Kodi information screen does not work otherwise
|
||||||
|
|
||||||
|
Code from script.module.metadatautils, kodidb.py
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if v.KODIVERSION > 17:
|
||||||
|
liz = xbmcgui.ListItem(
|
||||||
|
label=item.get("label", ""),
|
||||||
|
label2=item.get("label2", ""),
|
||||||
|
path=item['file'],
|
||||||
|
offscreen=offscreen)
|
||||||
|
else:
|
||||||
|
liz = xbmcgui.ListItem(
|
||||||
|
label=item.get("label", ""),
|
||||||
|
label2=item.get("label2", ""),
|
||||||
|
path=item['file'])
|
||||||
|
|
||||||
|
# only set isPlayable prop if really needed
|
||||||
|
if item.get("isFolder", False):
|
||||||
|
liz.setProperty('IsPlayable', 'false')
|
||||||
|
elif "plugin://script.skin.helper" not in item['file']:
|
||||||
|
liz.setProperty('IsPlayable', 'true')
|
||||||
|
|
||||||
|
nodetype = "Video"
|
||||||
|
if item["type"] in ["song", "album", "artist"]:
|
||||||
|
nodetype = "Music"
|
||||||
|
|
||||||
|
# extra properties
|
||||||
|
for key, value in item["extraproperties"].iteritems():
|
||||||
|
liz.setProperty(key, value)
|
||||||
|
|
||||||
|
# video infolabels
|
||||||
|
if nodetype == "Video":
|
||||||
|
infolabels = {
|
||||||
|
"title": item.get("title"),
|
||||||
|
"size": item.get("size"),
|
||||||
|
"genre": item.get("genre"),
|
||||||
|
"year": item.get("year"),
|
||||||
|
"top250": item.get("top250"),
|
||||||
|
"tracknumber": item.get("tracknumber"),
|
||||||
|
"rating": item.get("rating"),
|
||||||
|
"playcount": item.get("playcount"),
|
||||||
|
"overlay": item.get("overlay"),
|
||||||
|
"cast": item.get("cast"),
|
||||||
|
"castandrole": item.get("castandrole"),
|
||||||
|
"director": item.get("director"),
|
||||||
|
"mpaa": item.get("mpaa"),
|
||||||
|
"plot": item.get("plot"),
|
||||||
|
"plotoutline": item.get("plotoutline"),
|
||||||
|
"originaltitle": item.get("originaltitle"),
|
||||||
|
"sorttitle": item.get("sorttitle"),
|
||||||
|
"duration": item.get("duration"),
|
||||||
|
"studio": item.get("studio"),
|
||||||
|
"tagline": item.get("tagline"),
|
||||||
|
"writer": item.get("writer"),
|
||||||
|
"tvshowtitle": item.get("tvshowtitle"),
|
||||||
|
"premiered": item.get("premiered"),
|
||||||
|
"status": item.get("status"),
|
||||||
|
"code": item.get("imdbnumber"),
|
||||||
|
"imdbnumber": item.get("imdbnumber"),
|
||||||
|
"aired": item.get("aired"),
|
||||||
|
"credits": item.get("credits"),
|
||||||
|
"album": item.get("album"),
|
||||||
|
"artist": item.get("artist"),
|
||||||
|
"votes": item.get("votes"),
|
||||||
|
"trailer": item.get("trailer"),
|
||||||
|
# "progress": item.get('progresspercentage')
|
||||||
|
}
|
||||||
|
if item["type"] == "episode":
|
||||||
|
infolabels["season"] = item["season"]
|
||||||
|
infolabels["episode"] = item["episode"]
|
||||||
|
|
||||||
|
# streamdetails
|
||||||
|
if item.get("streamdetails"):
|
||||||
|
liz.addStreamInfo("video", item["streamdetails"].get("video", {}))
|
||||||
|
liz.addStreamInfo("audio", item["streamdetails"].get("audio", {}))
|
||||||
|
liz.addStreamInfo("subtitle", item["streamdetails"].get("subtitle", {}))
|
||||||
|
|
||||||
|
if "dateadded" in item:
|
||||||
|
infolabels["dateadded"] = item["dateadded"]
|
||||||
|
if "date" in item:
|
||||||
|
infolabels["date"] = item["date"]
|
||||||
|
|
||||||
|
# music infolabels
|
||||||
|
else:
|
||||||
|
infolabels = {
|
||||||
|
"title": item.get("title"),
|
||||||
|
"size": item.get("size"),
|
||||||
|
"genre": item.get("genre"),
|
||||||
|
"year": item.get("year"),
|
||||||
|
"tracknumber": item.get("track"),
|
||||||
|
"album": item.get("album"),
|
||||||
|
"artist": " / ".join(item.get('artist')),
|
||||||
|
"rating": str(item.get("rating", 0)),
|
||||||
|
"lyrics": item.get("lyrics"),
|
||||||
|
"playcount": item.get("playcount")
|
||||||
|
}
|
||||||
|
if "date" in item:
|
||||||
|
infolabels["date"] = item["date"]
|
||||||
|
if "duration" in item:
|
||||||
|
infolabels["duration"] = item["duration"]
|
||||||
|
if "lastplayed" in item:
|
||||||
|
infolabels["lastplayed"] = item["lastplayed"]
|
||||||
|
|
||||||
|
# setting the dbtype and dbid is supported from kodi krypton and up
|
||||||
|
# PKC hack: ignore empty type
|
||||||
|
if item["type"] not in ["recording", "channel", "favourite", ""]:
|
||||||
|
infolabels["mediatype"] = item["type"]
|
||||||
|
# setting the dbid on music items is not supported ?
|
||||||
|
if nodetype == "Video" and "DBID" in item["extraproperties"]:
|
||||||
|
infolabels["dbid"] = item["extraproperties"]["DBID"]
|
||||||
|
|
||||||
|
if "lastplayed" in item:
|
||||||
|
infolabels["lastplayed"] = item["lastplayed"]
|
||||||
|
|
||||||
|
# assign the infolabels
|
||||||
|
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'])
|
||||||
|
|
||||||
|
# contextmenu
|
||||||
|
if item["type"] in ["episode", "season"] and "season" in item and "tvshowid" in item:
|
||||||
|
# add series and season level to widgets
|
||||||
|
if "contextmenu" not in item:
|
||||||
|
item["contextmenu"] = []
|
||||||
|
item["contextmenu"] += [
|
||||||
|
(xbmc.getLocalizedString(20364), "ActivateWindow(Video,videodb://tvshows/titles/%s/,return)"
|
||||||
|
% (item["tvshowid"])),
|
||||||
|
(xbmc.getLocalizedString(20373), "ActivateWindow(Video,videodb://tvshows/titles/%s/%s/,return)"
|
||||||
|
% (item["tvshowid"], item["season"]))]
|
||||||
|
if "contextmenu" in item:
|
||||||
|
liz.addContextMenuItems(item["contextmenu"])
|
||||||
|
|
||||||
|
if as_tuple:
|
||||||
|
return (item["file"], liz, item.get("isFolder", False))
|
||||||
|
else:
|
||||||
|
return liz
|
||||||
|
except Exception:
|
||||||
|
utils.ERROR(notify=True)
|
||||||
|
LOG.error('item that should have been turned into a listitem: %s', item)
|
||||||
|
|
||||||
|
|
||||||
|
def create_main_entry(item):
|
||||||
|
'''helper to create a simple (directory) listitem'''
|
||||||
|
return {
|
||||||
|
'title': item[0],
|
||||||
|
'label': item[0],
|
||||||
|
'file': item[1],
|
||||||
|
'icon': item[2],
|
||||||
|
'art': {
|
||||||
|
'thumb': 'special://home/addons/%s/icon.png' % v.ADDON_ID,
|
||||||
|
'fanart': 'special://home/addons/%s/fanart.jpg' % v.ADDON_ID},
|
||||||
|
'isFolder': True,
|
||||||
|
'type': '',
|
||||||
|
'IsPlayable': 'false'
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue