commit
0900e462c0
27 changed files with 2269 additions and 1665 deletions
|
@ -1,5 +1,5 @@
|
|||
[![stable version](https://img.shields.io/badge/stable_version-2.7.4-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
|
||||
[![beta version](https://img.shields.io/badge/beta_version-2.7.4-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
|
||||
[![stable version](https://img.shields.io/badge/stable_version-2.7.6-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
|
||||
[![beta version](https://img.shields.io/badge/beta_version-2.7.6-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
|
||||
|
||||
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
|
||||
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
|
||||
|
|
12
addon.xml
12
addon.xml
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.7.4" provider-name="croneter">
|
||||
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.7.6" provider-name="croneter">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.1.0"/>
|
||||
<import addon="script.module.requests" version="2.9.1" />
|
||||
|
@ -77,7 +77,15 @@
|
|||
<summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary>
|
||||
<description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description>
|
||||
<disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer>
|
||||
<news>version 2.7.4:
|
||||
<news>version 2.7.6:
|
||||
- Make 2.7.5 available for everyone
|
||||
|
||||
version 2.7.5:
|
||||
- Giant overhaul of widgets
|
||||
- Fix some KeyErrors when playing songs
|
||||
- Fix rare cases where playlists were being created
|
||||
|
||||
version 2.7.4:
|
||||
- Fix PKC not synching new items if an older Kodi db is present
|
||||
|
||||
version 2.7.3:
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
version 2.7.6:
|
||||
- Make 2.7.5 available for everyone
|
||||
|
||||
version 2.7.5 (beta only):
|
||||
- Giant overhaul of widgets
|
||||
- Fix some KeyErrors when playing songs
|
||||
- Fix rare cases where playlists were being created
|
||||
|
||||
version 2.7.4:
|
||||
- Fix PKC not synching new items if an older Kodi db is present
|
||||
|
||||
|
|
39
default.py
39
default.py
|
@ -41,33 +41,18 @@ class Main():
|
|||
elif mode == 'plex_node':
|
||||
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':
|
||||
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':
|
||||
entrypoint.watchlater()
|
||||
|
||||
elif mode == 'channels':
|
||||
entrypoint.channels()
|
||||
entrypoint.browse_plex(key='/channels/all')
|
||||
|
||||
elif mode == 'route_to_extras':
|
||||
# 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)
|
||||
|
||||
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':
|
||||
transfer.plex_command('RESET-PKC')
|
||||
|
||||
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':
|
||||
from resources.lib.windows import direct_path_sources
|
||||
direct_path_sources.start()
|
||||
|
||||
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'):
|
||||
if mode == 'repair':
|
||||
|
@ -114,7 +102,8 @@ class Main():
|
|||
transfer.plex_command('textures-scan')
|
||||
|
||||
elif mode == 'chooseServer':
|
||||
entrypoint.choose_pms_server()
|
||||
LOG.info("Choosing PMS server requested, starting")
|
||||
transfer.plex_command('choose_pms_server')
|
||||
|
||||
elif mode == 'deviceid':
|
||||
self.deviceid()
|
||||
|
@ -139,7 +128,7 @@ class Main():
|
|||
entrypoint.playlists(params.get('content_type'))
|
||||
|
||||
elif mode == 'hub':
|
||||
entrypoint.hub(params.get('type'))
|
||||
entrypoint.hub(params.get('content_type'))
|
||||
|
||||
elif mode == 'select-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 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):
|
||||
|
@ -557,3 +576,16 @@ def settings_setsettingvalue(setting, value):
|
|||
'setting': setting,
|
||||
'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'
|
||||
}
|
|
@ -454,6 +454,9 @@ def _record_playstate(status, ended):
|
|||
if not status['plex_id']:
|
||||
LOG.debug('No Plex id found to record playstate for status %s', status)
|
||||
return
|
||||
if status['plex_type'] not in v.PLEX_VIDEOTYPES:
|
||||
LOG.debug('Not messing with non-video entries')
|
||||
return
|
||||
with PlexDB() as plexdb:
|
||||
db_item = plexdb.item_by_id(status['plex_id'], status['plex_type'])
|
||||
if not db_item:
|
||||
|
|
|
@ -7,5 +7,4 @@ from .websocket import store_websocket_message, process_websocket_messages, \
|
|||
WEBSOCKET_MESSAGES, PLAYSTATE_SESSIONS
|
||||
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
||||
from .fanart import FanartThread, FanartTask
|
||||
from .videonodes import VideoNodes
|
||||
from .sections import force_full_sync
|
||||
from .sections import force_full_sync, delete_files, clear_window_vars
|
||||
|
|
|
@ -166,14 +166,14 @@ class FullSync(common.fullsync_mixin):
|
|||
app.SYNC.path_verified = False
|
||||
try:
|
||||
# Sync new, updated and deleted items
|
||||
iterator = section['iterator']
|
||||
iterator = section.iterator
|
||||
# Tell the processing thread about this new section
|
||||
queue_info = InitNewSection(section['context'],
|
||||
queue_info = InitNewSection(section.context,
|
||||
iterator.total,
|
||||
iterator.get('librarySectionTitle',
|
||||
iterator.get('title1')),
|
||||
section['section_id'],
|
||||
section['plex_type'])
|
||||
section.section_id,
|
||||
section.plex_type)
|
||||
self.queue.put(queue_info)
|
||||
last = True
|
||||
# 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
|
||||
def playstate_per_section(self, section):
|
||||
LOG.debug('Processing %s playstates for library section %s',
|
||||
section['iterator'].total, section)
|
||||
section.iterator.total, section)
|
||||
try:
|
||||
# Sync new, updated and deleted items
|
||||
iterator = section['iterator']
|
||||
iterator = section.iterator
|
||||
# Tell the processing thread about this new section
|
||||
queue_info = InitNewSection(section['context'],
|
||||
queue_info = InitNewSection(section.context,
|
||||
iterator.total,
|
||||
section['section_name'],
|
||||
section['section_id'],
|
||||
section['plex_type'])
|
||||
section.name,
|
||||
section.section_id,
|
||||
section.plex_type)
|
||||
self.queue.put(queue_info)
|
||||
self.total = iterator.total
|
||||
self.section_name = section['section_name']
|
||||
self.section_name = section.name
|
||||
self.section_type_text = utils.lang(
|
||||
v.TRANSLATION_FROM_PLEXTYPE[section['plex_type']])
|
||||
v.TRANSLATION_FROM_PLEXTYPE[section.plex_type])
|
||||
self.current = 0
|
||||
|
||||
last = True
|
||||
loop = common.tag_last(iterator)
|
||||
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):
|
||||
if self.isCanceled():
|
||||
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
|
||||
itemtype.add_update(xml_item,
|
||||
section_name=section['section_name'],
|
||||
section_id=section['section_id'])
|
||||
section_name=section.name,
|
||||
section_id=section.section_id)
|
||||
itemtype.plexdb.update_last_sync(int(xml_item.attrib['ratingKey']),
|
||||
section['plex_type'],
|
||||
section.plex_type,
|
||||
self.current_sync)
|
||||
self.current += 1
|
||||
self.update_progressbar()
|
||||
|
@ -256,26 +256,25 @@ class FullSync(common.fullsync_mixin):
|
|||
try:
|
||||
for kind in kinds:
|
||||
for section in (x for x in sections.SECTIONS
|
||||
if x['plex_type'] == kind[1]):
|
||||
if x.section_type == kind[1]):
|
||||
if self.isCanceled():
|
||||
return
|
||||
if not section['sync_to_kodi']:
|
||||
if not section.sync_to_kodi:
|
||||
LOG.info('User chose to not sync section %s', section)
|
||||
continue
|
||||
element = copy.deepcopy(section)
|
||||
element['section_type'] = element['plex_type']
|
||||
element['plex_type'] = kind[0]
|
||||
element['element_type'] = kind[1]
|
||||
element['context'] = kind[2]
|
||||
element['get_children'] = kind[3]
|
||||
element.plex_type = kind[0]
|
||||
element.section_type = element.plex_type
|
||||
element.context = kind[2]
|
||||
element.get_children = kind[3]
|
||||
if self.repair or all_items:
|
||||
updated_at = None
|
||||
else:
|
||||
updated_at = section['last_sync'] - UPDATED_AT_SAFETY \
|
||||
if section['last_sync'] else None
|
||||
updated_at = section.last_sync - UPDATED_AT_SAFETY \
|
||||
if section.last_sync else None
|
||||
try:
|
||||
element['iterator'] = PF.SectionItems(section['section_id'],
|
||||
plex_type=kind[0],
|
||||
element.iterator = PF.SectionItems(section.section_id,
|
||||
plex_type=element.plex_type,
|
||||
updated_at=updated_at,
|
||||
last_viewed_at=None)
|
||||
except RuntimeError:
|
||||
|
@ -317,10 +316,10 @@ class FullSync(common.fullsync_mixin):
|
|||
if section is None:
|
||||
break
|
||||
# Setup our variables
|
||||
self.plex_type = section['plex_type']
|
||||
self.section_type = section['section_type']
|
||||
self.context = section['context']
|
||||
self.get_children = section['get_children']
|
||||
self.plex_type = section.plex_type
|
||||
self.section_type = section.section_type
|
||||
self.context = section.context
|
||||
self.get_children = section.get_children
|
||||
# Now do the heavy lifting
|
||||
if self.isCanceled() or not self.addupdate_section(section):
|
||||
return False
|
||||
|
@ -329,7 +328,7 @@ class FullSync(common.fullsync_mixin):
|
|||
# some items from the PMS
|
||||
with PlexDB() as plexdb:
|
||||
# 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)
|
||||
common.update_kodi_library(video=True, music=True)
|
||||
# In order to not delete all your songs again
|
||||
|
@ -361,10 +360,10 @@ class FullSync(common.fullsync_mixin):
|
|||
if section is None:
|
||||
break
|
||||
# Setup our variables
|
||||
self.plex_type = section['plex_type']
|
||||
self.section_type = section['section_type']
|
||||
self.context = section['context']
|
||||
self.get_children = section['get_children']
|
||||
self.plex_type = section.plex_type
|
||||
self.section_type = section.section_type
|
||||
self.context = section.context
|
||||
self.get_children = section.get_children
|
||||
# Now do the heavy lifting
|
||||
if self.isCanceled() or not self.playstate_per_section(section):
|
||||
return False
|
||||
|
@ -410,9 +409,6 @@ class FullSync(common.fullsync_mixin):
|
|||
@utils.log_time
|
||||
def _run(self):
|
||||
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
|
||||
if not sections.sync_from_pms(self):
|
||||
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 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
import urllib
|
||||
import copy
|
||||
|
||||
from . import videonodes
|
||||
from ..utils import cast
|
||||
from . import nodes
|
||||
from ..plex_db import PlexDB
|
||||
from ..plex_api import API
|
||||
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 ..utils import etree
|
||||
|
||||
LOG = getLogger('PLEX.sync.sections')
|
||||
|
||||
BATCH_SIZE = 500
|
||||
VNODES = videonodes.VideoNodes()
|
||||
PLAYLISTS = {}
|
||||
NODES = {}
|
||||
SECTIONS = []
|
||||
# Need a way to interrupt
|
||||
# Need a way to interrupt our synching process
|
||||
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():
|
||||
"""
|
||||
|
@ -32,193 +411,42 @@ def 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
|
||||
IS_CANCELED = parent_self.isCanceled
|
||||
try:
|
||||
return _sync_from_pms()
|
||||
finally:
|
||||
IS_CANCELED = None
|
||||
for section in sections:
|
||||
for old_section in old_sections:
|
||||
if section == old_section:
|
||||
section.last_sync = old_section.last_sync
|
||||
section.kodi_tagid = old_section.kodi_tagid
|
||||
section.sync_to_kodi = old_section.sync_to_kodi
|
||||
section.last_sync = old_section.last_sync
|
||||
|
||||
|
||||
def _sync_from_pms():
|
||||
global PLAYLISTS, NODES, SECTIONS
|
||||
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:
|
||||
def _delete_kodi_db_items(section):
|
||||
if section.section_type == v.PLEX_TYPE_MOVIE:
|
||||
kodi_context = kodi_db.KodiVideoDB
|
||||
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
|
||||
types = ((v.PLEX_TYPE_SHOW, itemtypes.Show),
|
||||
(v.PLEX_TYPE_SEASON, itemtypes.Season),
|
||||
(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
|
||||
types = ((v.PLEX_TYPE_ARTIST, itemtypes.Artist),
|
||||
(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:
|
||||
while True:
|
||||
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,
|
||||
BATCH_SIZE))
|
||||
with kodi_context(texture_db=True) as kodidb:
|
||||
|
@ -240,74 +468,175 @@ def _delete_kodi_db_items(section_id, section_type):
|
|||
return True
|
||||
|
||||
|
||||
def delete_sections(old_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():
|
||||
def _choose_libraries(sections):
|
||||
"""
|
||||
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
|
||||
sections = []
|
||||
preselect = []
|
||||
selectable_sections = []
|
||||
preselected = []
|
||||
index = 0
|
||||
for section in SECTIONS:
|
||||
if not app.SYNC.enable_music and section['plex_type'] == v.PLEX_TYPE_ARTIST:
|
||||
for section in sections:
|
||||
if not app.SYNC.enable_music and section.section_type == v.PLEX_TYPE_ARTIST:
|
||||
LOG.info('Ignoring music section: %s', section)
|
||||
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
|
||||
else:
|
||||
sections.append(section['section_name'])
|
||||
if section['sync_to_kodi']:
|
||||
preselect.append(index)
|
||||
# Offer user the new section
|
||||
selectable_sections.append(section.name)
|
||||
# Sections have been either preselected by the user or they are new
|
||||
if section.sync_to_kodi:
|
||||
preselected.append(index)
|
||||
index += 1
|
||||
# "Select Plex libraries to sync"
|
||||
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)
|
||||
# Don't ask the user again for this PMS even if user cancel the sync dialog
|
||||
utils.settings('sections_asked_for_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
|
||||
|
||||
|
||||
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
|
||||
sections = PF.get_plex_sections()
|
||||
try:
|
||||
sections.attrib
|
||||
except AttributeError:
|
||||
if not sections:
|
||||
LOG.error("Error download PMS views, abort sync_pms_time")
|
||||
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)
|
|
@ -31,4 +31,9 @@ def check_migration():
|
|||
utils.settings('ipaddress', value='')
|
||||
utils.settings('port', value='')
|
||||
|
||||
if not utils.compare_version(last_migration, '2.7.6'):
|
||||
LOG.info('Migrating to version 2.7.5')
|
||||
from .library_sync.sections import delete_files
|
||||
delete_files()
|
||||
|
||||
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from . import utils
|
||||
from .plex_api import API
|
||||
from . import utils
|
||||
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
|
||||
to be excluded in the advancedsettings.xml from being scanned by Kodi.
|
||||
|
@ -24,16 +24,17 @@ def excludefromscan_music_folders(xml):
|
|||
paths = []
|
||||
reboot = False
|
||||
api = API(item=None)
|
||||
for library in xml:
|
||||
if library.attrib['type'] != v.PLEX_TYPE_ARTIST:
|
||||
for section in sections:
|
||||
if section.plex_type != v.PLEX_TYPE_ARTIST:
|
||||
# Only look at music libraries
|
||||
continue
|
||||
for location in library:
|
||||
if location.tag == 'Location':
|
||||
if not section.sync_to_kodi:
|
||||
continue
|
||||
for location in section.xml.findall('Location'):
|
||||
path = api.validate_playurl(location.attrib['path'],
|
||||
typus=v.PLEX_TYPE_ARTIST,
|
||||
omit_check=True)
|
||||
paths.append(__turn_to_regex(path))
|
||||
paths.append(_turn_to_regex(path))
|
||||
try:
|
||||
with utils.XmlKodiSetting(
|
||||
'advancedsettings.xml',
|
||||
|
@ -73,7 +74,7 @@ def excludefromscan_music_folders(xml):
|
|||
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
|
||||
"""
|
||||
|
|
|
@ -59,7 +59,10 @@ def translate_path(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
|
||||
|
||||
|
||||
|
|
|
@ -34,11 +34,6 @@ SUPPORTED_FILETYPES = (
|
|||
# 'pls',
|
||||
# 'cue',
|
||||
)
|
||||
# Avoid endless loops. Store Plex IDs for creating, Kodi paths for deleting!
|
||||
IGNORE_KODI_PLAYLIST_CHANGE = list()
|
||||
# Used for updating Plex playlists due to Kodi changes - Plex playlist
|
||||
# will have to be deleted first. Add Plex ids!
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE = list()
|
||||
###############################################################################
|
||||
|
||||
|
||||
|
@ -99,20 +94,19 @@ def websocket(plex_id, status):
|
|||
plex_id = int(plex_id)
|
||||
with app.APP.lock_playlists:
|
||||
playlist = db.get_playlist(plex_id=plex_id)
|
||||
if plex_id in IGNORE_PLEX_PLAYLIST_CHANGE:
|
||||
if plex_id in plex_pl.IGNORE_PLEX_PLAYLIST_CHANGE:
|
||||
LOG.debug('Ignoring detected Plex playlist change for %s',
|
||||
playlist)
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE.remove(plex_id)
|
||||
plex_pl.IGNORE_PLEX_PLAYLIST_CHANGE.remove(plex_id)
|
||||
return
|
||||
if playlist and status == 9:
|
||||
# Won't be able to download metadata of the deleted playlist
|
||||
if sync_plex_playlist(playlist=playlist):
|
||||
LOG.debug('Plex deletion of playlist detected: %s', playlist)
|
||||
try:
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(plex_id)
|
||||
kodi_pl.delete(playlist)
|
||||
except PlaylistError:
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(plex_id)
|
||||
pass
|
||||
return
|
||||
xml = pms.metadata(plex_id)
|
||||
if xml is None:
|
||||
|
@ -130,7 +124,6 @@ def websocket(plex_id, status):
|
|||
else:
|
||||
LOG.debug('Change of Plex playlist detected: %s',
|
||||
playlist)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(plex_id)
|
||||
kodi_pl.delete(playlist)
|
||||
create = True
|
||||
elif not playlist and not status == 9:
|
||||
|
@ -139,10 +132,9 @@ def websocket(plex_id, status):
|
|||
create = True
|
||||
# To the actual work
|
||||
if create:
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(plex_id)
|
||||
kodi_pl.create(plex_id)
|
||||
except PlaylistError:
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(plex_id)
|
||||
pass
|
||||
|
||||
|
||||
def full_sync():
|
||||
|
@ -187,41 +179,33 @@ def _full_sync():
|
|||
if not playlist:
|
||||
LOG.debug('New Plex playlist %s discovered: %s',
|
||||
api.plex_id(), api.title())
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(api.plex_id())
|
||||
try:
|
||||
kodi_pl.create(api.plex_id())
|
||||
except PlaylistError:
|
||||
LOG.info('Skipping creation of playlist %s', api.plex_id())
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(api.plex_id())
|
||||
elif playlist.plex_updatedat != api.updated_at():
|
||||
LOG.debug('Detected changed Plex playlist %s: %s',
|
||||
api.plex_id(), api.title())
|
||||
# Since we are DELETING a playlist, we need to catch with path!
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(playlist.kodi_path)
|
||||
try:
|
||||
kodi_pl.delete(playlist)
|
||||
except PlaylistError:
|
||||
LOG.info('Skipping recreation of playlist %s', api.plex_id())
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.kodi_path)
|
||||
else:
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(api.plex_id())
|
||||
try:
|
||||
kodi_pl.create(api.plex_id())
|
||||
except PlaylistError:
|
||||
LOG.info('Could not recreate playlist %s', api.plex_id())
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(api.plex_id())
|
||||
# Get rid of old Plex playlists that were deleted on the Plex side
|
||||
for plex_id in old_plex_ids:
|
||||
if isCanceled():
|
||||
return False
|
||||
playlist = db.get_playlist(plex_id=plex_id)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(playlist.kodi_path)
|
||||
LOG.debug('Removing outdated Plex playlist from Kodi: %s', playlist)
|
||||
try:
|
||||
kodi_pl.delete(playlist)
|
||||
except PlaylistError:
|
||||
LOG.debug('Skipping deletion of playlist: %s', playlist)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.kodi_path)
|
||||
# Look at all supported Kodi playlists. Check whether they are in the DB.
|
||||
old_kodi_paths = db.kodi_playlist_paths()
|
||||
for root, _, files in path_ops.walk(v.PLAYLIST_PATH):
|
||||
|
@ -250,7 +234,6 @@ def _full_sync():
|
|||
LOG.info('Skipping Kodi playlist %s', path)
|
||||
else:
|
||||
LOG.debug('Changed Kodi playlist detected: %s', path)
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE.append(playlist.plex_id)
|
||||
plex_pl.delete(playlist)
|
||||
playlist.kodi_hash = kodi_hash
|
||||
try:
|
||||
|
@ -386,15 +369,9 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
|
|||
else event.src_path
|
||||
if not sync_kodi_playlist(path):
|
||||
return
|
||||
playlist = db.get_playlist(path=path)
|
||||
if playlist and playlist.plex_id in IGNORE_KODI_PLAYLIST_CHANGE:
|
||||
LOG.debug('Ignoring event %s for playlist %s', event, playlist)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.plex_id)
|
||||
return
|
||||
if not playlist and path in IGNORE_KODI_PLAYLIST_CHANGE:
|
||||
LOG.debug('Ignoring deletion event %s for playlist %s',
|
||||
event, playlist)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(path)
|
||||
if path in kodi_pl.IGNORE_KODI_PLAYLIST_CHANGE:
|
||||
LOG.debug('Ignoring event %s', event)
|
||||
kodi_pl.IGNORE_KODI_PLAYLIST_CHANGE.remove(path)
|
||||
return
|
||||
_method_map = {
|
||||
events.EVENT_TYPE_MODIFIED: self.on_modified,
|
||||
|
|
|
@ -14,10 +14,10 @@ from ..plex_api import API
|
|||
from .. import utils, path_ops, variables as v
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.playlists.kodi_pl')
|
||||
|
||||
###############################################################################
|
||||
|
||||
REGEX_FILE_NUMBERING = re.compile(r'''_(\d\d)\.\w+$''')
|
||||
# Avoid endless loops. Store the Kodi paths
|
||||
IGNORE_KODI_PLAYLIST_CHANGE = list()
|
||||
###############################################################################
|
||||
|
||||
|
||||
def create(plex_id):
|
||||
|
@ -54,7 +54,7 @@ def create(plex_id):
|
|||
else:
|
||||
number = int(occurance.group(1)) + 1
|
||||
if number > 3:
|
||||
LOG.error('Detected spanning tree issue, abort sync for %s',
|
||||
LOG.warn('Detected spanning tree issue, abort sync for %s',
|
||||
playlist)
|
||||
raise PlaylistError('Spanning tree warning')
|
||||
basename = re.sub(REGEX_FILE_NUMBERING, '', path)
|
||||
|
@ -65,7 +65,12 @@ def create(plex_id):
|
|||
if xml_playlist is None:
|
||||
LOG.error('Could not get Plex playlist %s', plex_id)
|
||||
raise PlaylistError('Could not get Plex playlist %s' % plex_id)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(playlist.kodi_path)
|
||||
try:
|
||||
_write_playlist_to_file(playlist, xml_playlist)
|
||||
except Exception:
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.kodi_path)
|
||||
raise
|
||||
playlist.kodi_hash = utils.generate_file_md5(path)
|
||||
db.update_playlist(playlist)
|
||||
LOG.debug('Created Kodi playlist based on Plex playlist: %s', playlist)
|
||||
|
@ -79,12 +84,14 @@ def delete(playlist):
|
|||
Returns None or raises PlaylistError
|
||||
"""
|
||||
if path_ops.exists(playlist.kodi_path):
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.append(playlist.kodi_path)
|
||||
try:
|
||||
path_ops.remove(playlist.kodi_path)
|
||||
LOG.debug('Deleted Kodi playlist: %s', playlist)
|
||||
except (OSError, IOError) as err:
|
||||
LOG.error('Could not delete Kodi playlist file %s. Error:\n%s: %s',
|
||||
playlist, err.errno, err.strerror)
|
||||
IGNORE_KODI_PLAYLIST_CHANGE.remove(playlist.kodi_path)
|
||||
raise PlaylistError('Could not delete %s' % playlist.kodi_path)
|
||||
db.update_playlist(playlist, delete=True)
|
||||
|
||||
|
|
|
@ -10,7 +10,9 @@ from .common import PlaylistError
|
|||
from . import pms, db
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.playlists.plex_pl')
|
||||
|
||||
# Used for updating Plex playlists due to Kodi changes - Plex playlist
|
||||
# will have to be deleted first. Add Plex ids!
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE = list()
|
||||
###############################################################################
|
||||
|
||||
|
||||
|
@ -28,6 +30,7 @@ def create(playlist):
|
|||
if not plex_ids:
|
||||
LOG.warning('No Plex ids found for playlist %s', playlist)
|
||||
raise PlaylistError
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE.append(playlist.plex_id)
|
||||
pms.add_items(playlist, plex_ids)
|
||||
db.update_playlist(playlist)
|
||||
LOG.debug('Done creating Plex playlist %s', playlist)
|
||||
|
@ -40,5 +43,6 @@ def delete(playlist):
|
|||
Returns None or raises PlaylistError
|
||||
"""
|
||||
LOG.debug('Deleting playlist from PMS: %s', playlist)
|
||||
IGNORE_PLEX_PLAYLIST_CHANGE.append(playlist.plex_id)
|
||||
pms.delete(playlist)
|
||||
db.update_playlist(playlist, delete=True)
|
||||
|
|
|
@ -35,6 +35,7 @@ from logging import getLogger
|
|||
from re import sub
|
||||
from urllib import urlencode, unquote, quote
|
||||
from urlparse import parse_qsl
|
||||
|
||||
from xbmcgui import ListItem
|
||||
|
||||
from .plex_db import PlexDB
|
||||
|
@ -149,6 +150,30 @@ class API(object):
|
|||
omit_check=True)
|
||||
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):
|
||||
"""
|
||||
Returns the Plex key such as '/library/metadata/246922' or None
|
||||
|
@ -335,6 +360,26 @@ class API(object):
|
|||
'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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
resume = float(self.item.attrib['viewOffset'])
|
||||
|
@ -588,7 +633,7 @@ class API(object):
|
|||
|
||||
def premiere_date(self):
|
||||
"""
|
||||
Returns the "originallyAvailableAt" or None
|
||||
Returns the "originallyAvailableAt", e.g. "2018-11-16" or None
|
||||
"""
|
||||
return self.item.get('originallyAvailableAt')
|
||||
|
||||
|
@ -859,7 +904,20 @@ class API(object):
|
|||
'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)
|
||||
if artwork and not artwork.startswith('http'):
|
||||
if '/composite/' in artwork:
|
||||
|
@ -870,27 +928,48 @@ class API(object):
|
|||
args = dict(parse_qsl(args))
|
||||
width = int(args.get('width', 400))
|
||||
height = int(args.get('height', 400))
|
||||
# Adjust to 4k resolution 3,840x2,160
|
||||
scaling = 3840.0 / float(max(width, height))
|
||||
# Adjust to 4k resolution 1920x1080
|
||||
scaling = 1920.0 / float(max(width, height))
|
||||
width = int(scaling * width)
|
||||
height = int(scaling * height)
|
||||
except ValueError:
|
||||
# e.g. playlists
|
||||
width = 3840
|
||||
height = 3840
|
||||
pass
|
||||
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'
|
||||
% (app.CONN.server, quote(artwork)))
|
||||
artwork = self.attach_plex_token_to_url(artwork)
|
||||
return artwork
|
||||
|
||||
def artwork_episode(self, full_artwork):
|
||||
"""
|
||||
Episodes are special, they only get the thumb, because all the other
|
||||
artwork will be saved under season and show EXCEPT if you're
|
||||
constructing a listitem and the item has NOT been synched to the Kodi db
|
||||
"""
|
||||
artworks = {}
|
||||
# Item is currently NOT in the Kodi DB
|
||||
art = self.one_artwork('thumb')
|
||||
if art:
|
||||
artworks['thumb'] = art
|
||||
if not full_artwork:
|
||||
# For episodes, only get the thumb. Everything else stemms from
|
||||
# either the season or the show
|
||||
return artworks
|
||||
for kodi_artwork, plex_artwork in \
|
||||
v.KODI_TO_PLEX_ARTWORK_EPISODE.iteritems():
|
||||
art = self.one_artwork(plex_artwork)
|
||||
if art:
|
||||
artworks[kodi_artwork] = art
|
||||
return artworks
|
||||
|
||||
def artwork(self, kodi_id=None, kodi_type=None, full_artwork=False):
|
||||
"""
|
||||
Gets the URLs to the Plex artwork. Dict keys will be missing if there
|
||||
is no corresponding artwork.
|
||||
Pass kodi_id and kodi_type to grab the artwork saved in the Kodi DB
|
||||
(thus potentially more artwork, e.g. clearart, discart)
|
||||
(thus potentially more artwork, e.g. clearart, discart).
|
||||
|
||||
Output ('max' version)
|
||||
{
|
||||
|
@ -905,44 +984,9 @@ class API(object):
|
|||
Passing full_artwork=True returns ALL the artwork for the item, so not
|
||||
just 'thumb' for episodes, but also season and show artwork
|
||||
"""
|
||||
artworks = {}
|
||||
if self.plex_type() == v.PLEX_TYPE_EPISODE:
|
||||
# Artwork lookup for episodes is broken for addon paths
|
||||
# Episodes is a bit special, only get the thumb, because all
|
||||
# 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
|
||||
|
||||
return self.artwork_episode(full_artwork)
|
||||
artworks = {}
|
||||
if kodi_id:
|
||||
# in Kodi database, potentially with additional e.g. clearart
|
||||
if self.plex_type() in v.PLEX_VIDEOTYPES:
|
||||
|
@ -952,9 +996,6 @@ class API(object):
|
|||
with KodiMusicDB(lock=False) as kodidb:
|
||||
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():
|
||||
art = self.one_artwork(plex_artwork)
|
||||
if art:
|
||||
|
@ -976,6 +1017,10 @@ class API(object):
|
|||
art = self.one_artwork('thumb')
|
||||
if art:
|
||||
artworks['thumb'] = art
|
||||
if self.plex_type() == v.PLEX_TYPE_PLAYLIST:
|
||||
art = self.one_artwork('composite')
|
||||
if art:
|
||||
artworks['thumb'] = art
|
||||
return artworks
|
||||
|
||||
def fanart_artwork(self, artworks):
|
||||
|
@ -1649,6 +1694,96 @@ class API(object):
|
|||
pass
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
elif plex_type == v.PLEX_TYPE_ARTIST:
|
||||
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
|
||||
for kind in (v.PLEX_TYPE_MOVIE,
|
||||
v.PLEX_TYPE_EPISODE,
|
||||
|
|
|
@ -819,7 +819,12 @@ def get_plex_sections():
|
|||
"""
|
||||
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',
|
||||
|
|
|
@ -86,9 +86,6 @@ class Service(object):
|
|||
for prop in WINDOW_PROPERTIES:
|
||||
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()
|
||||
# Init time-offset between Kodi and Plex
|
||||
timing.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset') or 0.0)
|
||||
|
@ -212,10 +209,8 @@ class Service(object):
|
|||
|
||||
def switch_plex_user(self):
|
||||
self.log_out()
|
||||
# First remove playlists of old user
|
||||
utils.delete_playlists()
|
||||
# Remove video nodes
|
||||
utils.delete_nodes()
|
||||
# First remove playlists and video nodes of old user
|
||||
library_sync.delete_files()
|
||||
app.ACCOUNT.set_unauthenticated()
|
||||
# Force full sync after login
|
||||
library_sync.force_full_sync()
|
||||
|
@ -300,9 +295,7 @@ class Service(object):
|
|||
from .library_sync import sections
|
||||
try:
|
||||
# Get newest sections from the PMS
|
||||
if not sections.sync_from_pms(self):
|
||||
return
|
||||
if not sections.choose_libraries():
|
||||
if not sections.sync_from_pms(self, pick_libraries=True):
|
||||
return
|
||||
# Force a full sync
|
||||
app.SYNC.run_lib_scan = 'full'
|
||||
|
@ -508,6 +501,9 @@ class Service(object):
|
|||
# Tell all threads to terminate (e.g. several lib sync threads)
|
||||
LOG.debug('Aborting all threads')
|
||||
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
|
||||
app.APP.stop_threads()
|
||||
utils.window('plex_service_started', clear=True)
|
||||
|
|
|
@ -17,6 +17,7 @@ from functools import wraps
|
|||
import hashlib
|
||||
import re
|
||||
import gc
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
|
@ -429,11 +430,10 @@ def wipe_database():
|
|||
Will also delete all cached artwork.
|
||||
"""
|
||||
LOG.warn('Start wiping')
|
||||
# Clean up the playlists
|
||||
delete_playlists()
|
||||
# Clean up the video nodes
|
||||
delete_nodes()
|
||||
from .library_sync.sections import delete_files
|
||||
from . import kodi_db, plex_db
|
||||
# Clean up the playlists and video nodes
|
||||
delete_files()
|
||||
# First get the paths to all synced playlists
|
||||
playlist_paths = []
|
||||
try:
|
||||
|
@ -814,76 +814,6 @@ class XmlKodiSetting(object):
|
|||
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):
|
||||
"""
|
||||
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_PLAYLIST = 'playlist'
|
||||
PLEX_TYPE_CHANNEL = 'channel'
|
||||
|
||||
# Used for /:/timeline XML messages
|
||||
PLEX_PLAYLIST_TYPE_VIDEO = 'video'
|
||||
PLEX_PLAYLIST_TYPE_AUDIO = 'music'
|
||||
|
@ -212,6 +215,8 @@ KODI_TYPE_MUSICVIDEO = 'musicvideo'
|
|||
|
||||
KODI_TYPE_PHOTO = 'photo'
|
||||
|
||||
KODI_TYPE_PLAYLIST = 'playlist'
|
||||
|
||||
KODI_VIDEOTYPES = (
|
||||
KODI_TYPE_VIDEO,
|
||||
KODI_TYPE_MOVIE,
|
||||
|
@ -251,7 +256,12 @@ ADDON_TYPE = {
|
|||
PLEX_TYPE_MOVIE: 'plugin.video.plexkodiconnect.movies',
|
||||
PLEX_TYPE_CLIP: 'plugin.video.plexkodiconnect.movies',
|
||||
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 = {
|
||||
|
@ -276,6 +286,7 @@ ITEMTYPE_FROM_KODITYPE = {
|
|||
|
||||
KODITYPE_FROM_PLEXTYPE = {
|
||||
PLEX_TYPE_MOVIE: KODI_TYPE_MOVIE,
|
||||
PLEX_TYPE_CLIP: KODI_TYPE_CLIP,
|
||||
PLEX_TYPE_EPISODE: KODI_TYPE_EPISODE,
|
||||
PLEX_TYPE_SEASON: KODI_TYPE_SEASON,
|
||||
PLEX_TYPE_SHOW: KODI_TYPE_SHOW,
|
||||
|
@ -284,7 +295,8 @@ KODITYPE_FROM_PLEXTYPE = {
|
|||
PLEX_TYPE_ALBUM: KODI_TYPE_ALBUM,
|
||||
PLEX_TYPE_PHOTO: KODI_TYPE_PHOTO,
|
||||
'XXXXXX': 'musicvideo',
|
||||
'XXXXXXX': 'genre'
|
||||
'XXXXXXX': 'genre',
|
||||
PLEX_TYPE_PLAYLIST: KODI_TYPE_PLAYLIST
|
||||
}
|
||||
|
||||
PLEX_TYPE_FROM_KODI_TYPE = {
|
||||
|
@ -316,7 +328,6 @@ KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE = {
|
|||
PLEX_TYPE_PHOTO: KODI_TYPE_PHOTO
|
||||
}
|
||||
|
||||
|
||||
KODI_PLAYLIST_TYPE_FROM_KODI_TYPE = {
|
||||
KODI_TYPE_VIDEO: 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 = {
|
||||
PLEX_TYPE_MOVIE: 342,
|
||||
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 = {
|
||||
'poster': 'thumb',
|
||||
'banner': 'banner',
|
||||
|
@ -403,10 +445,10 @@ KODI_TO_PLEX_ARTWORK = {
|
|||
}
|
||||
|
||||
KODI_TO_PLEX_ARTWORK_EPISODE = {
|
||||
'thumb': 'thumb',
|
||||
'poster': 'grandparentThumb',
|
||||
'banner': 'banner',
|
||||
'fanart': 'art'
|
||||
'season.poster': 'parentThumb',
|
||||
'tvshow.poster': 'grandparentThumb',
|
||||
'tvshow.fanart': 'grandparentArt',
|
||||
# 'tvshow.banner': 'banner' # Not included in PMS episode metadata
|
||||
}
|
||||
|
||||
# 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