Merge pull request #937 from croneter/beta-version

Bump master
This commit is contained in:
croneter 2019-07-21 13:07:41 +02:00 committed by GitHub
commit 1ed8de2e0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 2089 additions and 171 deletions

View file

@ -1,5 +1,5 @@
[![stable version](https://img.shields.io/badge/stable_version-2.8.7-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip) [![stable version](https://img.shields.io/badge/stable_version-2.9.0-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.8.7-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip) [![beta version](https://img.shields.io/badge/beta_version-2.9.0-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) [![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) [![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)
@ -78,6 +78,7 @@ Some people argue that PKC is 'hacky' because of the way it directly accesses th
+ Russian, thanks @UncleStark + Russian, thanks @UncleStark
+ Hungarian, thanks @savage93 + Hungarian, thanks @savage93
+ Ukrainian, thanks @uniss + Ukrainian, thanks @uniss
+ Lithuanian, thanks @egidusm
### Additional Artwork ### Additional Artwork
PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.org). Many thanks for lettings us use the API, guys! PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.org). Many thanks for lettings us use the API, guys!

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.8.7" provider-name="croneter"> <addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.9.0" provider-name="croneter">
<requires> <requires>
<import addon="xbmc.python" version="2.1.0"/> <import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.9.1" /> <import addon="script.module.requests" version="2.9.1" />
@ -77,7 +77,37 @@
<summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary> <summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary>
<description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description> <description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description>
<disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer> <disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer>
<news>version 2.8.7: <summary lang="sv_SE">Inbyggd integrering av Plex i Kodi</summary>
<description lang="sv_SE">Anslut Kodi till din Plex Media Server. Detta tillägg antar att du hanterar alla dina filmer med Plex (och ingen med Kodi). Du kan förlora data redan sparad i Kodis video och musik databaser (eftersom detta tillägg direkt ändrar dem). Använd på egen risk!</description>
<disclaimer lang="sv_SE">Använd på egen risk</disclaimer>
<summary lang="lt_LT">Natūralioji „Plex“ integracija į „Kodi“</summary>
<description lang="lt_LT">Prijunkite „Kodi“ prie „Plex Medija Serverio“. Šiame papildinyje daroma prielaida, kad valdote visus savo vaizdo įrašus naudodami „Plex“ (ir nė vieno su „Kodi“). Galite prarasti jau saugomus „Kodi“ vaizdo įrašų ir muzikos duomenų bazių duomenis (kadangi šis papildinys juos tiesiogiai pakeičia). Naudokite savo pačių rizika!</description>
<disclaimer lang="lt_LT">Naudokite savo pačių rizika</disclaimer>
<news>version 2.9.0:
WARNING: You might have to manually select your PKC widgets again
- versions 2.8.8 - 2.8.11 for everyone
- Fix AttributeError: 'NoneType' object has no attribute 'attrib' on playback startup
- Add new Lithuanian translations (thanks @egidusm)
version 2.8.11 (beta only):
- Support for the Up Next Kodi add-on
- Fix casting to PlexKodiConnect always starting the first episode
- Rename video nodes for ondeck
version 2.8.10 (beta only):
- Fix broken PKC update
version 2.8.9 (beta only):
- Fix sections that are not synced not displaying menu but entire library
- Provide more metadata for unsynced directory-like items like a tv show
- Fix 'Plex.nodes."id".path' not linking directly to entire library
version 2.8.8 (beta only):
WARNING: You might have to manually select your PKC widgets again
- Ensure correct Kodi Container.Type is set for PKC widgets
- Fix missing cast artwork if an actor also acted as director or writer for another movie. You will have to manually reset the Kodi DB.
version 2.8.7:
- Fix PKC potentially marking a video as watched on startup; don't sync time by toggling a video watch status but use PMS epoch time - Fix PKC potentially marking a video as watched on startup; don't sync time by toggling a video watch status but use PMS epoch time
version 2.8.6: version 2.8.6:

View file

@ -1,3 +1,27 @@
version 2.9.0:
WARNING: You might have to manually select your PKC widgets again
- versions 2.8.8 - 2.8.11 for everyone
- Fix AttributeError: 'NoneType' object has no attribute 'attrib' on playback startup
- Add new Lithuanian translations (thanks @egidusm)
version 2.8.11 (beta only):
- Support for the Up Next Kodi add-on
- Fix casting to PlexKodiConnect always starting the first episode
- Rename video nodes for ondeck
version 2.8.10 (beta only):
- Fix broken PKC update
version 2.8.9 (beta only):
- Fix sections that are not synced not displaying menu but entire library
- Provide more metadata for unsynced directory-like items like a tv show
- Fix 'Plex.nodes.<id>.path' not linking directly to entire library
version 2.8.8 (beta only):
WARNING: You might have to manually select your PKC widgets again
- Ensure correct Kodi Container.Type is set for PKC widgets
- Fix missing cast artwork if an actor also acted as director or writer for another movie. You will have to manually reset the Kodi DB.
version 2.8.7: version 2.8.7:
- Fix PKC potentially marking a video as watched on startup; don't sync time by toggling a video watch status but use PMS epoch time - Fix PKC potentially marking a video as watched on startup; don't sync time by toggling a video watch status but use PMS epoch time

View file

@ -52,6 +52,9 @@ class Main():
synched=params.get('synched') != 'false', synched=params.get('synched') != 'false',
prompt=params.get('prompt')) prompt=params.get('prompt'))
elif mode == 'show_section':
entrypoint.show_section(params.get('section_index'))
elif mode == 'watchlater': elif mode == 'watchlater':
entrypoint.watchlater() entrypoint.watchlater()

File diff suppressed because it is too large Load diff

View file

@ -20,11 +20,13 @@ from . import plex_functions as PF
from . import variables as v from . import variables as v
# Be careful - your using app in another Python instance! # Be careful - your using app in another Python instance!
from . import app, widgets from . import app, widgets
from .library_sync.nodes import NODE_TYPES
LOG = getLogger('PLEX.entrypoint') LOG = getLogger('PLEX.entrypoint')
def guess_content_type(): def guess_video_or_audio():
""" """
Returns either 'video', 'audio' or 'image', based how the user navigated to Returns either 'video', 'audio' or 'image', based how the user navigated to
the current view. the current view.
@ -102,9 +104,9 @@ def show_main_menu(content_type=None):
""" """
Shows the main PKC menu listing with all libraries, Channel, settings, etc. Shows the main PKC menu listing with all libraries, Channel, settings, etc.
""" """
content_type = content_type or guess_content_type() content_type = content_type or guess_video_or_audio()
LOG.debug('Do main listing for content_type: %s', content_type) LOG.debug('Do main listing for %s', content_type)
xbmcplugin.setContent(int(sys.argv[1]), 'files') xbmcplugin.setContent(int(sys.argv[1]), v.CONTENT_TYPE_FILE)
# Get nodes from the window props # Get nodes from the window props
totalnodes = int(utils.window('Plex.nodes.total') or 0) totalnodes = int(utils.window('Plex.nodes.total') or 0)
for i in range(totalnodes): for i in range(totalnodes):
@ -133,14 +135,6 @@ def show_main_menu(content_type=None):
# Should only be called if the user selects widgets # Should only be called if the user selects widgets
LOG.info('Detected user selecting widgets') LOG.info('Detected user selecting widgets')
directory_item(label, path) directory_item(label, path)
if not path.startswith('library://'):
# Already using add-on paths (e.g. section not synched)
continue
# Add ANOTHER menu item that uses add-on paths instead of direct
# paths in order to let the user navigate into all submenus
addon_index = utils.window('Plex.nodes.%s.addon_index' % i)
# Append "(More...)" to the label
directory_item('%s (%s)' % (label, utils.lang(22082)), addon_index)
# Playlists # Playlists
if content_type != 'image': if content_type != 'image':
path = 'plugin://%s?mode=playlists' % v.ADDON_ID path = 'plugin://%s?mode=playlists' % v.ADDON_ID
@ -169,32 +163,64 @@ def show_main_menu(content_type=None):
xbmcplugin.endOfDirectory(int(sys.argv[1])) xbmcplugin.endOfDirectory(int(sys.argv[1]))
def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None, def show_section(section_index):
content_type=None): """
Displays menu for an entire Plex section. We're using add-on paths instead
of Kodi video library xmls to be able to use type="filter" library xmls
and thus set the "content"
Only used for synched Plex sections - otherwise, PMS xml for the section
is used directly
"""
LOG.debug('Do section listing for section index %s', section_index)
xbmcplugin.setContent(int(sys.argv[1]), v.CONTENT_TYPE_FILE)
# Get nodes from the window props
node = 'Plex.nodes.%s' % section_index
content = utils.window('%s.type' % node)
plex_type = v.PLEX_TYPE_MOVIE if content == v.CONTENT_TYPE_MOVIE \
else v.PLEX_TYPE_SHOW
for node_type, _, _, _, _ in NODE_TYPES[plex_type]:
label = utils.window('%s.%s.title' % (node, node_type))
path = utils.window('%s.%s.index' % (node, node_type))
directory_item(label, path)
xbmcplugin.endOfDirectory(int(sys.argv[1]))
def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None):
""" """
Pass synched=False if the items have not been synched to the Kodi DB Pass synched=False if the items have not been synched to the Kodi DB
Kodi content type will be set using the very first item returned by the PMS
""" """
content_type = content_type or guess_content_type()
LOG.debug('show_listing: content_type %s, section_id %s, synched %s, '
'key %s, plex_type %s', content_type, section_id, synched, key,
plex_type)
try: try:
xml[0] xml[0]
except IndexError: except IndexError:
LOG.info('xml received from the PMS is empty: %s', xml.attrib) LOG.info('xml received from the PMS is empty: %s, %s',
xbmcplugin.endOfDirectory(handle=int(sys.argv[1])) xml.tag, xml.attrib)
xbmcplugin.endOfDirectory(int(sys.argv[1]))
return return
if content_type == 'video': api = API(xml[0])
xbmcplugin.setContent(int(sys.argv[1]), 'videos') # Determine content type for Kodi's Container.content
elif content_type == 'audio': if key == '/hubs/home/continueWatching':
xbmcplugin.setContent(int(sys.argv[1]), 'artists') # Mix of movies and episodes
elif plex_type in (v.PLEX_TYPE_PLAYLIST, v.PLEX_TYPE_CHANNEL): plex_type = v.PLEX_TYPE_VIDEO
xbmcplugin.setContent(int(sys.argv[1]), 'videos') elif key == '/hubs/home/recentlyAdded?type=2':
elif plex_type: # "Recently Added TV", potentially a mix of Seasons and Episodes
xbmcplugin.setContent(int(sys.argv[1]), plex_type = v.PLEX_TYPE_VIDEO
v.MEDIATYPE_FROM_PLEX_TYPE[plex_type]) elif api.plex_type is None and api.fast_key and '?collection=' in api.fast_key:
# Collections/Kodi sets
plex_type = v.PLEX_TYPE_SET
elif api.plex_type is None and plex_type:
# e.g. browse by folder - folders will be listed first
# Retain plex_type
pass
else: else:
xbmcplugin.setContent(int(sys.argv[1]), 'files') plex_type = api.plex_type
content_type = v.CONTENT_FROM_PLEX_TYPE[plex_type]
LOG.debug('show_listing: section_id %s, synched %s, key %s, plex_type %s, '
'content type %s',
section_id, synched, key, plex_type, content_type)
xbmcplugin.setContent(int(sys.argv[1]), content_type)
# Initialization # Initialization
widgets.PLEX_TYPE = plex_type widgets.PLEX_TYPE = plex_type
widgets.SYNCHED = synched widgets.SYNCHED = synched
@ -204,10 +230,14 @@ def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None,
if plex_type == v.PLEX_TYPE_SHOW and key and 'recentlyAdded' in key: if plex_type == v.PLEX_TYPE_SHOW and key and 'recentlyAdded' in key:
widgets.APPEND_SHOW_TITLE = utils.settings('RecentTvAppendShow') == 'true' widgets.APPEND_SHOW_TITLE = utils.settings('RecentTvAppendShow') == 'true'
widgets.APPEND_SXXEXX = utils.settings('RecentTvAppendSeason') == 'true' widgets.APPEND_SXXEXX = utils.settings('RecentTvAppendSeason') == 'true'
if content_type and xml[0].tag == 'Playlist': if api.tag == 'Playlist':
# Certain views mix playlist types audio and video # Only show video playlists if navigation started for videos
# and vice-versa for audio playlists
content = guess_video_or_audio()
if content:
for entry in reversed(xml): for entry in reversed(xml):
if entry.get('playlistType') != content_type: tmp_api = API(entry)
if tmp_api.playlist_type() != content:
xml.remove(entry) xml.remove(entry)
if xml.get('librarySectionID'): if xml.get('librarySectionID'):
widgets.SECTION_ID = utils.cast(int, xml.get('librarySectionID')) widgets.SECTION_ID = utils.cast(int, xml.get('librarySectionID'))
@ -355,15 +385,14 @@ def playlists(content_type):
Lists all Plex playlists of the media type plex_playlist_type Lists all Plex playlists of the media type plex_playlist_type
content_type: 'audio', 'video' content_type: 'audio', 'video'
""" """
content_type = content_type or guess_content_type() LOG.debug('Listing Plex playlists for content type %s', content_type)
LOG.debug('Listing Plex %s playlists', content_type)
if not _wait_for_auth(): if not _wait_for_auth():
return xbmcplugin.endOfDirectory(int(sys.argv[1]), False) return xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
app.init(entrypoint=True) app.init(entrypoint=True)
from .playlists.pms import all_playlists from .playlists.pms import all_playlists
xml = all_playlists() xml = all_playlists()
if xml is None: if xml is None:
return return xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
if content_type is not None: if content_type is not None:
# This will be skipped if user selects a widget # This will be skipped if user selects a widget
# Buggy xml.remove(child) requires reversed() # Buggy xml.remove(child) requires reversed()
@ -371,7 +400,7 @@ def playlists(content_type):
api = API(entry) api = API(entry)
if not api.playlist_type() == content_type: if not api.playlist_type() == content_type:
xml.remove(entry) xml.remove(entry)
show_listing(xml, content_type=content_type) show_listing(xml)
def hub(content_type): def hub(content_type):
@ -380,7 +409,7 @@ def hub(content_type):
content_type: content_type:
audio, video, image audio, video, image
""" """
content_type = content_type or guess_content_type() content_type = content_type or guess_video_or_audio()
LOG.debug('Showing Plex Hub entries for %s', content_type) LOG.debug('Showing Plex Hub entries for %s', content_type)
if not _wait_for_auth(): if not _wait_for_auth():
return xbmcplugin.endOfDirectory(int(sys.argv[1]), False) return xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
@ -410,7 +439,7 @@ def hub(content_type):
append = True append = True
if not append: if not append:
xml.remove(entry) xml.remove(entry)
show_listing(xml, content_type=content_type) show_listing(xml)
def watchlater(): def watchlater():
@ -447,7 +476,8 @@ def browse_plex(key=None, plex_type=None, section_id=None, synched=True,
LOG.debug('Browsing to key %s, section %s, plex_type: %s, synched: %s, ' LOG.debug('Browsing to key %s, section %s, plex_type: %s, synched: %s, '
'prompt "%s"', key, section_id, plex_type, synched, prompt) 'prompt "%s"', key, section_id, plex_type, synched, prompt)
if not _wait_for_auth(): if not _wait_for_auth():
return xbmcplugin.endOfDirectory(int(sys.argv[1]), False) xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
return
app.init(entrypoint=True) app.init(entrypoint=True)
if prompt: if prompt:
prompt = utils.dialog('input', prompt) prompt = utils.dialog('input', prompt)

View file

@ -349,7 +349,13 @@ class KodiVideoDB(common.KodiDBBase):
if kind == 'actor': if kind == 'actor':
for person in people_list: for person in people_list:
# Make sure the person entry in table actor exists # Make sure the person entry in table actor exists
actor_id = self._get_actor_id(person[0], art_url=person[1]) actor_id, new = self._get_actor_id(person[0],
art_url=person[1])
if not new and person[1]:
# Person might have shown up as a director or writer first
# WITHOUT an art url from the Plex side!
# Check here if we need to set the actor's art url
self._check_actor_art(actor_id, person[1])
# Link the person with the media element # Link the person with the media element
try: try:
self.cursor.execute('INSERT INTO actor_link VALUES (?, ?, ?, ?, ?)', self.cursor.execute('INSERT INTO actor_link VALUES (?, ?, ?, ?, ?)',
@ -361,7 +367,7 @@ class KodiVideoDB(common.KodiDBBase):
else: else:
for person in people_list: for person in people_list:
# Make sure the person entry in table actor exists: # Make sure the person entry in table actor exists:
actor_id = self._get_actor_id(person[0]) actor_id, _ = self._get_actor_id(person[0])
# Link the person with the media element # Link the person with the media element
try: try:
self.cursor.execute('INSERT INTO %s_link VALUES (?, ?, ?)' % kind, self.cursor.execute('INSERT INTO %s_link VALUES (?, ?, ?)' % kind,
@ -450,8 +456,11 @@ class KodiVideoDB(common.KodiDBBase):
def _get_actor_id(self, name, art_url=None): def _get_actor_id(self, name, art_url=None):
""" """
Returns the actor_id [int] for name [unicode] in table actor (without Returns the tuple
ensuring that the name matches). (actor_id [int], new_entry [bool])
for name [unicode] in table actor (without ensuring that the name
matches)."new_entry" will be True if a new DB entry has just been
created.
If not, will create a new record with actor_id, name, art_url If not, will create a new record with actor_id, name, art_url
Uses Plex ids and thus assumes that Plex person id is unique! Uses Plex ids and thus assumes that Plex person id is unique!
@ -459,9 +468,21 @@ class KodiVideoDB(common.KodiDBBase):
self.cursor.execute('SELECT actor_id FROM actor WHERE name=? LIMIT 1', self.cursor.execute('SELECT actor_id FROM actor WHERE name=? LIMIT 1',
(name,)) (name,))
try: try:
return self.cursor.fetchone()[0] return (self.cursor.fetchone()[0], False)
except TypeError: except TypeError:
return self._new_actor_id(name, art_url) return (self._new_actor_id(name, art_url), True)
def _check_actor_art(self, actor_id, url):
"""
Sets the actor's art url [unicode] for actor_id [int]
"""
self.cursor.execute('''
SELECT EXISTS(SELECT 1 FROM art
WHERE media_id = ? AND media_type = 'actor'
LIMIT 1)''', (actor_id, ))
if not self.cursor.fetchone()[0]:
# We got a new artwork url for this actor!
self.add_art(url, actor_id, 'actor', 'thumb')
def get_art(self, kodi_id, kodi_type): def get_art(self, kodi_id, kodi_type):
""" """

View file

@ -7,10 +7,13 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger from logging import getLogger
from json import loads from json import loads
import copy import copy
import json
import binascii
import xbmc import xbmc
import xbmcgui import xbmcgui
from .plex_api import API
from .plex_db import PlexDB from .plex_db import PlexDB
from . import kodi_db from . import kodi_db
from .downloadutils import DownloadUtils as DU from .downloadutils import DownloadUtils as DU
@ -143,6 +146,8 @@ class KodiMonitor(xbmc.Monitor):
elif method == "System.OnQuit": elif method == "System.OnQuit":
LOG.info('Kodi OnQuit detected - shutting down') LOG.info('Kodi OnQuit detected - shutting down')
app.APP.stop_pkc = True app.APP.stop_pkc = True
elif method == 'Other.plugin.video.plexkodiconnect_play_action':
self._start_next_episode(data)
@staticmethod @staticmethod
def _hack_addon_paths_replay_video(): def _hack_addon_paths_replay_video():
@ -283,6 +288,18 @@ class KodiMonitor(xbmc.Monitor):
json_item.get('type'), json_item.get('type'),
json_item.get('file')) json_item.get('file'))
@staticmethod
def _start_next_episode(data):
"""
Used for the add-on Upnext to start playback of the next episode
"""
LOG.info('Upnext: Start playback of the next episode')
play_info = binascii.unhexlify(data[0])
play_info = json.loads(play_info)
app.APP.player.stop()
handle = 'RunPlugin(%s)' % play_info.get('handle')
xbmc.executebuiltin(handle.encode('utf-8'))
def PlayBackStart(self, data): def PlayBackStart(self, data):
""" """
Called whenever playback is started. Example data: Called whenever playback is started. Example data:
@ -415,6 +432,8 @@ class KodiMonitor(xbmc.Monitor):
status['playmethod'] = item.playmethod status['playmethod'] = item.playmethod
status['playcount'] = item.playcount status['playcount'] = item.playcount
LOG.debug('Set the player state: %s', status) LOG.debug('Set the player state: %s', status)
if not app.SYNC.direct_paths:
_notify_upnext(item)
def _playback_cleanup(ended=False): def _playback_cleanup(ended=False):
@ -537,6 +556,85 @@ def _clean_file_table():
LOG.debug('Done cleaning up Kodi file table') LOG.debug('Done cleaning up Kodi file table')
def _next_episode(current_api):
"""
Returns the xml for the next episode after the current one
Returns None if something went wrong or there is no next episode
"""
xml = PF.show_episodes(current_api.grandparent_id())
if xml is None:
return
for counter, episode in enumerate(xml):
api = API(episode)
if api.plex_id == current_api.plex_id:
break
else:
LOG.error('Did not find the episode with Plex id %s for show %s: %s',
current_api.plex_id, current_api.grandparent_id(),
current_api.grandparent_title())
return
try:
next_api = API(xml[counter + 1])
except IndexError:
# Was the last episode
return
return next_api
def _complete_artwork_keys(info):
"""
Make sure that the minimum set of keys is present in the info dict
"""
for key in ('tvshow.poster',
'tvshow.fanart',
'tvshow.landscape',
'tvshow.clearart',
'tvshow.clearlogo',
'thumb'):
if key not in info['art']:
info['art'][key] = ''
def _notify_upnext(item):
"""
Signals to the Kodi add-on Upnext that there is another episode after this
one.
Needed for add-on paths in order to prevent crashes when Upnext does this
by itself
"""
if not item.plex_type == v.PLEX_TYPE_EPISODE:
return
this_api = API(item.xml)
next_api = _next_episode(this_api)
if next_api is None:
return
info = {}
for key, api in (('current_episode', this_api),
('next_episode', next_api)):
info[key] = {
'episodeid': api.plex_id,
'tvshowid': api.grandparent_id(),
'title': api.title(),
'showtitle': api.grandparent_title(),
'plot': api.plot(),
'playcount': api.viewcount(),
'season': api.season_number(),
'episode': api.index(),
'firstaired': api.year(),
'rating': api.rating(),
'art': api.artwork(kodi_id=api.kodi_id,
kodi_type=api.kodi_type,
full_artwork=True)
}
_complete_artwork_keys(info[key])
info['play_info'] = {'handle': next_api.path(force_addon=True)}
sender = v.ADDON_ID.encode('utf-8')
method = 'upnext_data'.encode('utf-8')
data = binascii.hexlify(json.dumps(info))
data = '\\"[\\"{0}\\"]\\"'.format(data)
xbmc.executebuiltin('NotifyAll(%s, %s, %s)' % (sender, method, data))
class ContextMonitor(backgroundthread.KillableThread): class ContextMonitor(backgroundthread.KillableThread):
""" """
Detect the resume dialog for widgets. Could also be used to detect Detect the resume dialog for widgets. Could also be used to detect

View file

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
import urllib import urllib
import copy
from ..utils import etree from ..utils import etree
from .. import variables as v, utils from .. import variables as v, utils
@ -18,40 +19,37 @@ RECOMMENDED_SCORE_LOWER_BOUND = 7
# ) # )
NODE_TYPES = { NODE_TYPES = {
v.PLEX_TYPE_MOVIE: ( v.PLEX_TYPE_MOVIE: (
('ondeck', ('plex_ondeck',
utils.lang(39500), # "On Deck" utils.lang(39500), # "On Deck"
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/onDeck', 'key': '/library/sections/{self.section_id}/onDeck',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'movies', v.CONTENT_TYPE_MOVIE,
True), True),
('pkc_ondeck', ('ondeck',
utils.lang(39502), # "PKC On Deck (faster)" utils.lang(39502), # "PKC On Deck (faster)"
{}, {},
'movies', v.CONTENT_TYPE_MOVIE,
False), False),
('recent', ('recent',
utils.lang(30174), # "Recently Added" utils.lang(30174), # "Recently Added"
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/recentlyAdded', 'key': '/library/sections/{self.section_id}/recentlyAdded',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'movies', v.CONTENT_TYPE_MOVIE,
False), False),
('all', ('all',
'{self.name}', # We're using this section's name '{self.name}', # We're using this section's name
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/all', 'key': '/library/sections/{self.section_id}/all',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'movies', v.CONTENT_TYPE_MOVIE,
False), False),
('recommended', ('recommended',
utils.lang(30230), # "Recommended" utils.lang(30230), # "Recommended"
@ -59,30 +57,27 @@ NODE_TYPES = {
'mode': 'browseplex', 'mode': 'browseplex',
'key': ('/library/sections/{self.section_id}&%s' 'key': ('/library/sections/{self.section_id}&%s'
% urllib.urlencode({'sort': 'rating:desc'})), % urllib.urlencode({'sort': 'rating:desc'})),
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'movies', v.CONTENT_TYPE_MOVIE,
False), False),
('genres', ('genres',
utils.lang(135), # "Genres" utils.lang(135), # "Genres"
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/genre', 'key': '/library/sections/{self.section_id}/genre',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'movies', v.CONTENT_TYPE_MOVIE,
False), False),
('sets', ('sets',
utils.lang(39501), # "Collections" utils.lang(39501), # "Collections"
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/collection', 'key': '/library/sections/{self.section_id}/collection',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'movies', v.CONTENT_TYPE_MOVIE,
False), False),
('random', ('random',
utils.lang(30227), # "Random" utils.lang(30227), # "Random"
@ -90,20 +85,18 @@ NODE_TYPES = {
'mode': 'browseplex', 'mode': 'browseplex',
'key': ('/library/sections/{self.section_id}&%s' 'key': ('/library/sections/{self.section_id}&%s'
% urllib.urlencode({'sort': 'random'})), % urllib.urlencode({'sort': 'random'})),
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'movies', v.CONTENT_TYPE_MOVIE,
False), False),
('lastplayed', ('lastplayed',
utils.lang(568), # "Last played" utils.lang(568), # "Last played"
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/recentlyViewed', 'key': '/library/sections/{self.section_id}/recentlyViewed',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'movies', v.CONTENT_TYPE_MOVIE,
False), False),
('browse', ('browse',
utils.lang(39702), # "Browse by folder" utils.lang(39702), # "Browse by folder"
@ -111,19 +104,20 @@ NODE_TYPES = {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/folder', 'key': '/library/sections/{self.section_id}/folder',
'plex_type': '{self.section_type}', 'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}',
'folder': True
}, },
'movies', v.CONTENT_TYPE_MOVIE,
True), True),
('more', ('more',
utils.lang(22082), # "More..." utils.lang(22082), # "More..."
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}', 'key': '/library/sections/{self.section_id}',
'plex_type': '{self.section_type}', 'section_id': '{self.section_id}',
'section_id': '{self.section_id}' 'folder': True
}, },
'movies', v.CONTENT_TYPE_FILE,
True), True),
), ),
########################################################### ###########################################################
@ -133,30 +127,27 @@ NODE_TYPES = {
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/onDeck', 'key': '/library/sections/{self.section_id}/onDeck',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'episodes', v.CONTENT_TYPE_EPISODE,
True), True),
('recent', ('recent',
utils.lang(30174), # "Recently Added" utils.lang(30174), # "Recently Added"
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/recentlyAdded', 'key': '/library/sections/{self.section_id}/recentlyAdded',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'episodes', v.CONTENT_TYPE_EPISODE,
False), False),
('all', ('all',
'{self.name}', # We're using this section's name '{self.name}', # We're using this section's name
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/all', 'key': '/library/sections/{self.section_id}/all',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'tvshows', v.CONTENT_TYPE_SHOW,
False), False),
('recommended', ('recommended',
utils.lang(30230), # "Recommended" utils.lang(30230), # "Recommended"
@ -164,30 +155,27 @@ NODE_TYPES = {
'mode': 'browseplex', 'mode': 'browseplex',
'key': ('/library/sections/{self.section_id}&%s' 'key': ('/library/sections/{self.section_id}&%s'
% urllib.urlencode({'sort': 'rating:desc'})), % urllib.urlencode({'sort': 'rating:desc'})),
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'tvshows', v.CONTENT_TYPE_SHOW,
False), False),
('genres', ('genres',
utils.lang(135), # "Genres" utils.lang(135), # "Genres"
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/genre', 'key': '/library/sections/{self.section_id}/genre',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'tvshows', v.CONTENT_TYPE_SHOW,
False), False),
('sets', ('sets',
utils.lang(39501), # "Collections" utils.lang(39501), # "Collections"
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/collection', 'key': '/library/sections/{self.section_id}/collection',
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'tvshows', v.CONTENT_TYPE_SHOW,
True), # There are no sets/collections for shows with Kodi True), # There are no sets/collections for shows with Kodi
('random', ('random',
utils.lang(30227), # "Random" utils.lang(30227), # "Random"
@ -195,10 +183,9 @@ NODE_TYPES = {
'mode': 'browseplex', 'mode': 'browseplex',
'key': ('/library/sections/{self.section_id}&%s' 'key': ('/library/sections/{self.section_id}&%s'
% urllib.urlencode({'sort': 'random'})), % urllib.urlencode({'sort': 'random'})),
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'tvshows', v.CONTENT_TYPE_SHOW,
False), False),
('lastplayed', ('lastplayed',
utils.lang(568), # "Last played" utils.lang(568), # "Last played"
@ -206,30 +193,29 @@ NODE_TYPES = {
'mode': 'browseplex', 'mode': 'browseplex',
'key': ('/library/sections/{self.section_id}/recentlyViewed&%s' 'key': ('/library/sections/{self.section_id}/recentlyViewed&%s'
% urllib.urlencode({'type': v.PLEX_TYPE_NUMBER_FROM_PLEX_TYPE[v.PLEX_TYPE_EPISODE]})), % urllib.urlencode({'type': v.PLEX_TYPE_NUMBER_FROM_PLEX_TYPE[v.PLEX_TYPE_EPISODE]})),
'plex_type': '{self.section_type}',
'section_id': '{self.section_id}' 'section_id': '{self.section_id}'
}, },
'episodes', v.CONTENT_TYPE_EPISODE,
False), False),
('browse', ('browse',
utils.lang(39702), # "Browse by folder" utils.lang(39702), # "Browse by folder"
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}/folder', 'key': '/library/sections/{self.section_id}/folder',
'plex_type': '{self.section_type}', 'section_id': '{self.section_id}',
'section_id': '{self.section_id}' 'folder': True
}, },
'episodes', v.CONTENT_TYPE_EPISODE,
True), True),
('more', ('more',
utils.lang(22082), # "More..." utils.lang(22082), # "More..."
{ {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/{self.section_id}', 'key': '/library/sections/{self.section_id}',
'plex_type': '{self.section_type}', 'section_id': '{self.section_id}',
'section_id': '{self.section_id}' 'folder': True
}, },
'episodes', v.CONTENT_TYPE_FILE,
True), True),
), ),
} }
@ -239,9 +225,19 @@ def node_pms(section, node_name, args):
""" """
Nodes where the logic resides with the PMS - we're NOT building an 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 that filters and sorts, but point to PKC add-on path
Be sure to set args['folder'] = True if the listing is a folder and does
not contain playable elements like movies, episodes or tracks
""" """
xml = etree.Element('node', attrib={'order': unicode(section.order), if 'folder' in args:
'type': 'folder'}) args = copy.deepcopy(args)
args.pop('folder')
folder = True
else:
folder = False
xml = etree.Element('node',
attrib={'order': unicode(section.order),
'type': 'folder' if folder else 'filter'})
etree.SubElement(xml, 'label').text = node_name etree.SubElement(xml, 'label').text = node_name
etree.SubElement(xml, 'icon').text = ICON_PATH etree.SubElement(xml, 'icon').text = ICON_PATH
etree.SubElement(xml, 'content').text = section.content etree.SubElement(xml, 'content').text = section.content
@ -249,7 +245,7 @@ def node_pms(section, node_name, args):
return xml return xml
def node_pkc_ondeck(section, node_name): def node_ondeck(section, node_name):
""" """
For movies only - returns in-progress movies sorted by last played For movies only - returns in-progress movies sorted by last played
""" """

View file

@ -132,7 +132,7 @@ class Section(object):
@section_type.setter @section_type.setter
def section_type(self, value): def section_type(self, value):
self._section_type = value self._section_type = value
self.content = v.MEDIATYPE_FROM_PLEX_TYPE[value] self.content = v.CONTENT_FROM_PLEX_TYPE[value]
# Default values whether we sync or not based on the Plex type # Default values whether we sync or not based on the Plex type
if value == v.PLEX_TYPE_PHOTO: if value == v.PLEX_TYPE_PHOTO:
self.sync_to_kodi = False self.sync_to_kodi = False
@ -239,29 +239,41 @@ class Section(object):
raise RuntimeError('Index not initialized') raise RuntimeError('Index not initialized')
# Main list entry for this section - which will show the different # Main list entry for this section - which will show the different
# nodes as "submenus" once the user navigates into this section # nodes as "submenus" once the user navigates into this section
if self.sync_to_kodi and self.section_type in v.PLEX_VIDEOTYPES:
# Node showing a menu for this section
args = {
'mode': 'show_section',
'section_index': self.index
}
index = utils.extend_url('plugin://%s' % v.ADDON_ID, args)
# Node directly displaying all content
path = 'library://video/Plex-{0}/{0}_all.xml'
path = path.format(self.section_id)
else:
# Node showing a menu for this section
args = { args = {
'mode': 'browseplex', 'mode': 'browseplex',
'key': '/library/sections/%s' % self.section_id, 'key': '/library/sections/%s' % self.section_id,
'plex_type': self.section_type,
'section_id': unicode(self.section_id) 'section_id': unicode(self.section_id)
} }
if not self.sync_to_kodi: if not self.sync_to_kodi:
args['synched'] = 'false' args['synched'] = 'false'
addon_index = self.addon_path(args) # No library xmls to speed things up
if self.sync_to_kodi and self.section_type in v.PLEX_VIDEOTYPES: # Immediately show the PMS options for this section
path = 'library://video/Plex-{0}/{0}_all.xml' index = self.addon_path(args)
path = path.format(self.section_id) # Node directly displaying all content
index = 'library://video/Plex-%s' % self.section_id args = {
else: 'mode': 'browseplex',
# No xmls to link to - let's show the listings on the fly 'key': '/library/sections/%s/all' % self.section_id,
index = addon_index 'section_id': unicode(self.section_id)
args['key'] = '/library/sections/%s/all' % self.section_id }
if not self.sync_to_kodi:
args['synched'] = 'false'
path = self.addon_path(args) path = self.addon_path(args)
# .index will list all possible nodes for this library
utils.window('%s.index' % self.node, value=index) utils.window('%s.index' % self.node, value=index)
utils.window('%s.title' % self.node, value=self.name) utils.window('%s.title' % self.node, value=self.name)
utils.window('%s.type' % self.node, value=self.content) utils.window('%s.type' % self.node, value=self.content)
utils.window('%s.content' % self.node, value=path) utils.window('%s.content' % self.node, value=index)
# .path leads to all elements of this library # .path leads to all elements of this library
if self.section_type in v.PLEX_VIDEOTYPES: if self.section_type in v.PLEX_VIDEOTYPES:
utils.window('%s.path' % self.node, utils.window('%s.path' % self.node,
@ -274,8 +286,6 @@ class Section(object):
utils.window('%s.path' % self.node, utils.window('%s.path' % self.node,
value='ActivateWindow(pictures,%s,return)' % path) value='ActivateWindow(pictures,%s,return)' % path)
utils.window('%s.id' % self.node, value=str(self.section_id)) 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_index' % self.node, value=addon_index)
if not self.sync_to_kodi: if not self.sync_to_kodi:
self.remove_files_from_kodi() self.remove_files_from_kodi()
return return
@ -312,18 +322,22 @@ class Section(object):
def _build_node(self, node_type, node_name, args, content, pms_node): def _build_node(self, node_type, node_name, args, content, pms_node):
self.content = content self.content = content
node_name = node_name.format(self=self) node_name = node_name.format(self=self)
if pms_node:
# Do NOT write a Kodi video library xml - can't use type="filter"
# to point back to plugin://plugin.video.plexkodiconnect
xml = nodes.node_pms(self, node_name, args)
args.pop('folder', None)
path = self.addon_path(args)
else:
# Write a Kodi video library xml
xml_name = '%s_%s.xml' % (self.section_id, node_type) xml_name = '%s_%s.xml' % (self.section_id, node_type)
path = path_ops.path.join(self.path, xml_name) path = path_ops.path.join(self.path, xml_name)
if not path_ops.exists(path): 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 # Let's use Kodi's logic to sort/filter the Kodi library
xml = getattr(nodes, 'node_%s' % node_type)(self, node_name) xml = getattr(nodes, 'node_%s' % node_type)(self, node_name)
self._write_xml(xml, xml_name) self._write_xml(xml, xml_name)
self.order += 1
path = 'library://video/Plex-%s/%s' % (self.section_id, xml_name) path = 'library://video/Plex-%s/%s' % (self.section_id, xml_name)
self.order += 1
self._window_node(path, node_name, node_type, pms_node) self._window_node(path, node_name, node_type, pms_node)
def _write_xml(self, xml, xml_name): def _write_xml(self, xml, xml_name):
@ -337,7 +351,7 @@ class Section(object):
LOG.debug('Creating smart playlist for section %s: %s', LOG.debug('Creating smart playlist for section %s: %s',
self.name, self.playlist_path) self.name, self.playlist_path)
xml = etree.Element('smartplaylist', xml = etree.Element('smartplaylist',
attrib={'type': v.MEDIATYPE_FROM_PLEX_TYPE[self.section_type]}) attrib={'type': v.CONTENT_FROM_PLEX_TYPE[self.section_type]})
etree.SubElement(xml, 'name').text = self.name etree.SubElement(xml, 'name').text = self.name
etree.SubElement(xml, 'match').text = 'all' etree.SubElement(xml, 'match').text = 'all'
rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag', rule = etree.SubElement(xml, 'rule', attrib={'field': 'tag',
@ -647,7 +661,6 @@ def _clear_window_vars(index):
utils.window('%s.content' % node, clear=True) utils.window('%s.content' % node, clear=True)
utils.window('%s.path' % node, clear=True) utils.window('%s.path' % node, clear=True)
utils.window('%s.id' % node, clear=True) utils.window('%s.id' % node, clear=True)
utils.window('%s.addon_index' % node, clear=True)
# Just clear everything here, ignore the plex_type # Just clear everything here, ignore the plex_type
for typus in (x[0] for y in nodes.NODE_TYPES.values() for x in y): for typus in (x[0] for y in nodes.NODE_TYPES.values() for x in y):
for kind in WINDOW_ARGS: for kind in WINDOW_ARGS:

View file

@ -51,4 +51,10 @@ def check_migration():
plexdb.cursor.execute('DROP INDEX IF EXISTS ix_playlists_3') plexdb.cursor.execute('DROP INDEX IF EXISTS ix_playlists_3')
# Index will be automatically recreated on next PKC startup # Index will be automatically recreated on next PKC startup
if not utils.compare_version(last_migration, '2.8.9'):
LOG.info('Migrating to version 2.8.8')
from .library_sync import sections
sections.clear_window_vars()
sections.delete_videonode_files()
utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION) utils.settings('last_migrated_PKC_version', value=v.ADDON_VERSION)

View file

@ -234,13 +234,14 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
playqueue.clear() playqueue.clear()
if plex_type != v.PLEX_TYPE_CLIP: if plex_type != v.PLEX_TYPE_CLIP:
# Post to the PMS to create a playqueue - in any case due to Companion # Post to the PMS to create a playqueue - in any case due to Companion
section_uuid = xml.attrib.get('librarySectionUUID')
xml = PF.init_plex_playqueue(plex_id, xml = PF.init_plex_playqueue(plex_id,
xml.attrib.get('librarySectionUUID'), section_uuid,
mediatype=plex_type, mediatype=plex_type,
trailers=trailers) trailers=trailers)
if xml is None: if xml is None:
LOG.error('Could not get a playqueue xml for plex id %s, UUID %s', LOG.error('Could not get a playqueue xml for plex id %s, UUID %s',
plex_id, xml.attrib.get('librarySectionUUID')) plex_id, section_uuid)
# "Play error" # "Play error"
utils.dialog('notification', utils.dialog('notification',
utils.lang(29999), utils.lang(29999),

View file

@ -71,6 +71,13 @@ class Base(object):
""" """
return cast(int, self.xml.get('ratingKey')) return cast(int, self.xml.get('ratingKey'))
@property
def fast_key(self):
"""
Returns the 'fastKey' as unicode or None
"""
return self.xml.get('fastKey')
@property @property
def plex_type(self): def plex_type(self):
""" """

View file

@ -91,9 +91,10 @@ class File(object):
key = '/library/sections/%s/%s' % (section_id, key) key = '/library/sections/%s/%s' % (section_id, key)
params = { params = {
'mode': 'browseplex', 'mode': 'browseplex',
'key': key, 'key': key
'plex_type': plex_type or self.plex_type
} }
if plex_type or self.plex_type:
params['plex_type'] = plex_type or self.plex_type
if not synched: if not synched:
# No item to be found in the Kodi DB # No item to be found in the Kodi DB
params['synched'] = 'false' params['synched'] = 'false'

View file

@ -33,7 +33,8 @@ def update_playqueue_from_PMS(playqueue,
playqueue_id=None, playqueue_id=None,
repeat=None, repeat=None,
offset=None, offset=None,
transient_token=None): transient_token=None,
start_plex_id=None):
""" """
Completely updates the Kodi playqueue with the new Plex playqueue. Pass Completely updates the Kodi playqueue with the new Plex playqueue. Pass
in playqueue_id if we need to fetch a new playqueue in playqueue_id if we need to fetch a new playqueue
@ -42,7 +43,8 @@ def update_playqueue_from_PMS(playqueue,
offset = time offset in Plextime (milliseconds) offset = time offset in Plextime (milliseconds)
""" """
LOG.info('New playqueue %s received from Plex companion with offset ' LOG.info('New playqueue %s received from Plex companion with offset '
'%s, repeat %s', playqueue_id, offset, repeat) '%s, repeat %s, start_plex_id %s',
playqueue_id, offset, repeat, start_plex_id)
# Safe transient token from being deleted # Safe transient token from being deleted
if transient_token is None: if transient_token is None:
transient_token = playqueue.plex_transient_token transient_token = playqueue.plex_transient_token
@ -61,7 +63,10 @@ def update_playqueue_from_PMS(playqueue,
return return
playqueue.repeat = 0 if not repeat else int(repeat) playqueue.repeat = 0 if not repeat else int(repeat)
playqueue.plex_transient_token = transient_token playqueue.plex_transient_token = transient_token
playback.play_xml(playqueue, xml, offset) playback.play_xml(playqueue,
xml,
offset=offset,
start_plex_id=start_plex_id)
class PlexCompanion(backgroundthread.KillableThread): class PlexCompanion(backgroundthread.KillableThread):
@ -154,11 +159,15 @@ class PlexCompanion(backgroundthread.KillableThread):
api = API(xml[0]) api = API(xml[0])
playqueue = PQ.get_playqueue_from_type( playqueue = PQ.get_playqueue_from_type(
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type]) v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[api.plex_type])
key = data.get('key')
if key:
_, key, _ = PF.ParseContainerKey(key)
update_playqueue_from_PMS(playqueue, update_playqueue_from_PMS(playqueue,
playqueue_id=container_key, playqueue_id=container_key,
repeat=query.get('repeat'), repeat=query.get('repeat'),
offset=data.get('offset'), offset=data.get('offset'),
transient_token=data.get('token')) transient_token=data.get('token'),
key=key)
@staticmethod @staticmethod
def _process_streams(data): def _process_streams(data):

View file

@ -1029,3 +1029,15 @@ def GetUserArtworkURL(username):
url = user.thumb url = user.thumb
LOG.debug("Avatar url for user %s is: %s", username, url) LOG.debug("Avatar url for user %s is: %s", username, url)
return url return url
def show_episodes(plex_id):
"""
Returns all episodes for the tv show with plex_id
"""
url = "{server}/library/metadata/%s/allLeaves" % plex_id
arguments = {
'checkFiles': 0,
'skipRefresh': 1,
}
return DownloadChunks(utils.extend_url(url, arguments))

View file

@ -165,6 +165,7 @@ PLEX_TYPE_VIDEO = 'video'
PLEX_TYPE_MOVIE = 'movie' PLEX_TYPE_MOVIE = 'movie'
PLEX_TYPE_CLIP = 'clip' # e.g. trailers PLEX_TYPE_CLIP = 'clip' # e.g. trailers
PLEX_TYPE_SET = 'collection' # sets/collections PLEX_TYPE_SET = 'collection' # sets/collections
PLEX_TYPE_GENRE = 'genre'
PLEX_TYPE_MIXED = 'mixed' PLEX_TYPE_MIXED = 'mixed'
PLEX_TYPE_EPISODE = 'episode' PLEX_TYPE_EPISODE = 'episode'
@ -201,7 +202,7 @@ KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE = {
KODI_TYPE_VIDEO = 'video' KODI_TYPE_VIDEO = 'video'
KODI_TYPE_MOVIE = 'movie' KODI_TYPE_MOVIE = 'movie'
KODI_TYPE_SET = 'set' # for movie sets of several movies KODI_TYPE_SET = 'set' # for movie sets of several movies
KODI_TYPE_CLIP = 'clip' # e.g. trailers KODI_TYPE_CLIP = 'video' # e.g. trailers
KODI_TYPE_EPISODE = 'episode' KODI_TYPE_EPISODE = 'episode'
KODI_TYPE_SEASON = 'season' KODI_TYPE_SEASON = 'season'
@ -216,6 +217,24 @@ KODI_TYPE_MUSICVIDEO = 'musicvideo'
KODI_TYPE_PHOTO = 'photo' KODI_TYPE_PHOTO = 'photo'
KODI_TYPE_PLAYLIST = 'playlist' KODI_TYPE_PLAYLIST = 'playlist'
KODI_TYPE_GENRE = 'genre'
# Kodi content types, primarily used for xbmcplugin.setContent()
CONTENT_TYPE_MOVIE = 'movies'
CONTENT_TYPE_SHOW = 'tvshows'
CONTENT_TYPE_SEASON = 'seasons'
CONTENT_TYPE_EPISODE = 'episodes'
CONTENT_TYPE_ARTIST = 'artists'
CONTENT_TYPE_ALBUM = 'albums'
CONTENT_TYPE_SONG = 'songs'
CONTENT_TYPE_CLIP = 'movies'
CONTENT_TYPE_SET = 'sets'
CONTENT_TYPE_PHOTO = 'photos'
CONTENT_TYPE_GENRE = 'genres'
CONTENT_TYPE_VIDEO = 'videos'
CONTENT_TYPE_PLAYLIST = 'playlists'
CONTENT_TYPE_FILE = 'files'
KODI_VIDEOTYPES = ( KODI_VIDEOTYPES = (
KODI_TYPE_VIDEO, KODI_TYPE_VIDEO,
@ -306,7 +325,6 @@ PLEX_TYPE_FROM_KODI_TYPE = {
KODI_TYPE_EPISODE: PLEX_TYPE_EPISODE, KODI_TYPE_EPISODE: PLEX_TYPE_EPISODE,
KODI_TYPE_SEASON: PLEX_TYPE_SEASON, KODI_TYPE_SEASON: PLEX_TYPE_SEASON,
KODI_TYPE_SHOW: PLEX_TYPE_SHOW, KODI_TYPE_SHOW: PLEX_TYPE_SHOW,
KODI_TYPE_CLIP: PLEX_TYPE_CLIP,
KODI_TYPE_ARTIST: PLEX_TYPE_ARTIST, KODI_TYPE_ARTIST: PLEX_TYPE_ARTIST,
KODI_TYPE_ALBUM: PLEX_TYPE_ALBUM, KODI_TYPE_ALBUM: PLEX_TYPE_ALBUM,
KODI_TYPE_SONG: PLEX_TYPE_SONG, KODI_TYPE_SONG: PLEX_TYPE_SONG,
@ -418,24 +436,29 @@ PLEX_TYPE_NUMBER_FROM_PLEX_TYPE = {
PLEX_TYPE_ALBUM: 9, PLEX_TYPE_ALBUM: 9,
PLEX_TYPE_SONG: 10, PLEX_TYPE_SONG: 10,
PLEX_TYPE_CLIP: 12, PLEX_TYPE_CLIP: 12,
'playlist': 15, PLEX_TYPE_PLAYLIST: 15,
PLEX_TYPE_SET: 18 PLEX_TYPE_SET: 18
} }
# To be used with e.g. Kodi Widgets # To be used with e.g. Kodi Widgets
MEDIATYPE_FROM_PLEX_TYPE = { CONTENT_FROM_PLEX_TYPE = {
PLEX_TYPE_MOVIE: 'movies', PLEX_TYPE_MOVIE: CONTENT_TYPE_MOVIE,
PLEX_TYPE_SHOW: 'tvshows', PLEX_TYPE_SHOW: CONTENT_TYPE_SHOW,
PLEX_TYPE_SEASON: 'tvshows', PLEX_TYPE_SEASON: CONTENT_TYPE_SEASON,
PLEX_TYPE_EPISODE: 'episodes', PLEX_TYPE_EPISODE: CONTENT_TYPE_EPISODE,
PLEX_TYPE_ARTIST: 'artists', PLEX_TYPE_ARTIST: CONTENT_TYPE_ARTIST,
PLEX_TYPE_ALBUM: 'albumbs', PLEX_TYPE_ALBUM: CONTENT_TYPE_ALBUM,
PLEX_TYPE_SONG: 'songs', PLEX_TYPE_SONG: CONTENT_TYPE_SONG,
PLEX_TYPE_CLIP: 'videos', PLEX_TYPE_CLIP: CONTENT_TYPE_CLIP,
PLEX_TYPE_SET: 'movies', PLEX_TYPE_SET: CONTENT_TYPE_SET,
PLEX_TYPE_PHOTO: 'photos', PLEX_TYPE_PHOTO: CONTENT_TYPE_PHOTO,
'mixed': 'tvshows', PLEX_TYPE_GENRE: CONTENT_TYPE_GENRE,
PLEX_TYPE_VIDEO: CONTENT_TYPE_VIDEO,
PLEX_TYPE_PLAYLIST: CONTENT_TYPE_PLAYLIST,
PLEX_TYPE_CHANNEL: CONTENT_TYPE_FILE,
'mixed': CONTENT_TYPE_SHOW,
None: CONTENT_TYPE_FILE
} }
KODI_TO_PLEX_ARTWORK = { KODI_TO_PLEX_ARTWORK = {

View file

@ -79,6 +79,31 @@ def generate_item(api):
def _generate_folder(api): def _generate_folder(api):
'''Generates "folder"/"directory" items that user can further navigate''' '''Generates "folder"/"directory" items that user can further navigate'''
typus = ''
if api.plex_type == v.PLEX_TYPE_GENRE:
# Unfortunately, 'genre' is not yet supported by Kodi
# typus = v.KODI_TYPE_GENRE
pass
elif api.plex_type == v.PLEX_TYPE_SHOW:
typus = v.KODI_TYPE_SHOW
elif api.plex_type == v.PLEX_TYPE_SEASON:
typus = v.KODI_TYPE_SEASON
elif api.plex_type == v.PLEX_TYPE_ARTIST:
typus = v.KODI_TYPE_ARTIST
elif api.plex_type == v.PLEX_TYPE_ALBUM:
typus = v.KODI_TYPE_ALBUM
elif api.fast_key and '?collection=' in api.fast_key:
typus = v.KODI_TYPE_SET
if typus and typus != v.KODI_TYPE_SET:
content = _generate_content(api)
content['type'] = typus
content['file'] = api.directory_path(section_id=SECTION_ID,
plex_type=PLEX_TYPE,
old_key=KEY)
content['isFolder'] = True
content['IsPlayable'] = 'false'
return content
else:
art = api.artwork() art = api.artwork()
return { return {
'title': api.title(), 'title': api.title(),
@ -94,7 +119,7 @@ def _generate_folder(api):
'fanart': art['fanart'] if 'fanart' in art else 'fanart': art['fanart'] if 'fanart' in art else
'special://home/addons/%s/fanart.jpg' % v.ADDON_ID}, 'special://home/addons/%s/fanart.jpg' % v.ADDON_ID},
'isFolder': True, 'isFolder': True,
'type': '', 'type': typus,
'IsPlayable': 'false', 'IsPlayable': 'false',
} }