commit
1ed8de2e0f
18 changed files with 2089 additions and 171 deletions
|
@ -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)
|
||||
[![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)
|
||||
[![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.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)
|
||||
[![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
|
||||
+ Hungarian, thanks @savage93
|
||||
+ Ukrainian, thanks @uniss
|
||||
+ Lithuanian, thanks @egidusm
|
||||
|
||||
### Additional Artwork
|
||||
PKC uses additional artwork for free from [TheMovieDB](https://www.themoviedb.org). Many thanks for lettings us use the API, guys!
|
||||
|
|
34
addon.xml
34
addon.xml
|
@ -1,5 +1,5 @@
|
|||
<?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>
|
||||
<import addon="xbmc.python" version="2.1.0"/>
|
||||
<import addon="script.module.requests" version="2.9.1" />
|
||||
|
@ -77,7 +77,37 @@
|
|||
<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.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
|
||||
|
||||
version 2.8.6:
|
||||
|
|
|
@ -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:
|
||||
- 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
|
||||
|
||||
|
|
|
@ -52,6 +52,9 @@ class Main():
|
|||
synched=params.get('synched') != 'false',
|
||||
prompt=params.get('prompt'))
|
||||
|
||||
elif mode == 'show_section':
|
||||
entrypoint.show_section(params.get('section_index'))
|
||||
|
||||
elif mode == 'watchlater':
|
||||
entrypoint.watchlater()
|
||||
|
||||
|
|
1618
resources/language/resource.language.lt_LT/strings.po
Normal file
1618
resources/language/resource.language.lt_LT/strings.po
Normal file
File diff suppressed because it is too large
Load diff
|
@ -20,11 +20,13 @@ from . import plex_functions as PF
|
|||
from . import variables as v
|
||||
# Be careful - your using app in another Python instance!
|
||||
from . import app, widgets
|
||||
from .library_sync.nodes import NODE_TYPES
|
||||
|
||||
|
||||
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
|
||||
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.
|
||||
"""
|
||||
content_type = content_type or guess_content_type()
|
||||
LOG.debug('Do main listing for content_type: %s', content_type)
|
||||
xbmcplugin.setContent(int(sys.argv[1]), 'files')
|
||||
content_type = content_type or guess_video_or_audio()
|
||||
LOG.debug('Do main listing for %s', content_type)
|
||||
xbmcplugin.setContent(int(sys.argv[1]), v.CONTENT_TYPE_FILE)
|
||||
# Get nodes from the window props
|
||||
totalnodes = int(utils.window('Plex.nodes.total') or 0)
|
||||
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
|
||||
LOG.info('Detected user selecting widgets')
|
||||
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
|
||||
if content_type != 'image':
|
||||
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]))
|
||||
|
||||
|
||||
def show_listing(xml, plex_type=None, section_id=None, synched=True, key=None,
|
||||
content_type=None):
|
||||
def show_section(section_index):
|
||||
"""
|
||||
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
|
||||
|
||||
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:
|
||||
xml[0]
|
||||
except IndexError:
|
||||
LOG.info('xml received from the PMS is empty: %s', xml.attrib)
|
||||
xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
||||
LOG.info('xml received from the PMS is empty: %s, %s',
|
||||
xml.tag, xml.attrib)
|
||||
xbmcplugin.endOfDirectory(int(sys.argv[1]))
|
||||
return
|
||||
if content_type == 'video':
|
||||
xbmcplugin.setContent(int(sys.argv[1]), 'videos')
|
||||
elif content_type == 'audio':
|
||||
xbmcplugin.setContent(int(sys.argv[1]), 'artists')
|
||||
elif plex_type in (v.PLEX_TYPE_PLAYLIST, v.PLEX_TYPE_CHANNEL):
|
||||
xbmcplugin.setContent(int(sys.argv[1]), 'videos')
|
||||
elif plex_type:
|
||||
xbmcplugin.setContent(int(sys.argv[1]),
|
||||
v.MEDIATYPE_FROM_PLEX_TYPE[plex_type])
|
||||
api = API(xml[0])
|
||||
# Determine content type for Kodi's Container.content
|
||||
if key == '/hubs/home/continueWatching':
|
||||
# Mix of movies and episodes
|
||||
plex_type = v.PLEX_TYPE_VIDEO
|
||||
elif key == '/hubs/home/recentlyAdded?type=2':
|
||||
# "Recently Added TV", potentially a mix of Seasons and Episodes
|
||||
plex_type = v.PLEX_TYPE_VIDEO
|
||||
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:
|
||||
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
|
||||
widgets.PLEX_TYPE = plex_type
|
||||
widgets.SYNCHED = synched
|
||||
|
@ -204,11 +230,15 @@ 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:
|
||||
widgets.APPEND_SHOW_TITLE = utils.settings('RecentTvAppendShow') == 'true'
|
||||
widgets.APPEND_SXXEXX = utils.settings('RecentTvAppendSeason') == 'true'
|
||||
if content_type and xml[0].tag == 'Playlist':
|
||||
# Certain views mix playlist types audio and video
|
||||
for entry in reversed(xml):
|
||||
if entry.get('playlistType') != content_type:
|
||||
xml.remove(entry)
|
||||
if api.tag == 'Playlist':
|
||||
# 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):
|
||||
tmp_api = API(entry)
|
||||
if tmp_api.playlist_type() != content:
|
||||
xml.remove(entry)
|
||||
if xml.get('librarySectionID'):
|
||||
widgets.SECTION_ID = utils.cast(int, xml.get('librarySectionID'))
|
||||
elif section_id:
|
||||
|
@ -355,15 +385,14 @@ def playlists(content_type):
|
|||
Lists all Plex playlists of the media type plex_playlist_type
|
||||
content_type: 'audio', 'video'
|
||||
"""
|
||||
content_type = content_type or guess_content_type()
|
||||
LOG.debug('Listing Plex %s playlists', content_type)
|
||||
LOG.debug('Listing Plex playlists for content type %s', content_type)
|
||||
if not _wait_for_auth():
|
||||
return xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
|
||||
app.init(entrypoint=True)
|
||||
from .playlists.pms import all_playlists
|
||||
xml = all_playlists()
|
||||
if xml is None:
|
||||
return
|
||||
return xbmcplugin.endOfDirectory(handle=int(sys.argv[1]))
|
||||
if content_type is not None:
|
||||
# This will be skipped if user selects a widget
|
||||
# Buggy xml.remove(child) requires reversed()
|
||||
|
@ -371,7 +400,7 @@ def playlists(content_type):
|
|||
api = API(entry)
|
||||
if not api.playlist_type() == content_type:
|
||||
xml.remove(entry)
|
||||
show_listing(xml, content_type=content_type)
|
||||
show_listing(xml)
|
||||
|
||||
|
||||
def hub(content_type):
|
||||
|
@ -380,7 +409,7 @@ def hub(content_type):
|
|||
content_type:
|
||||
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)
|
||||
if not _wait_for_auth():
|
||||
return xbmcplugin.endOfDirectory(int(sys.argv[1]), False)
|
||||
|
@ -410,7 +439,7 @@ def hub(content_type):
|
|||
append = True
|
||||
if not append:
|
||||
xml.remove(entry)
|
||||
show_listing(xml, content_type=content_type)
|
||||
show_listing(xml)
|
||||
|
||||
|
||||
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, '
|
||||
'prompt "%s"', key, section_id, plex_type, synched, prompt)
|
||||
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)
|
||||
if prompt:
|
||||
prompt = utils.dialog('input', prompt)
|
||||
|
|
|
@ -349,7 +349,13 @@ class KodiVideoDB(common.KodiDBBase):
|
|||
if kind == 'actor':
|
||||
for person in people_list:
|
||||
# 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
|
||||
try:
|
||||
self.cursor.execute('INSERT INTO actor_link VALUES (?, ?, ?, ?, ?)',
|
||||
|
@ -361,7 +367,7 @@ class KodiVideoDB(common.KodiDBBase):
|
|||
else:
|
||||
for person in people_list:
|
||||
# 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
|
||||
try:
|
||||
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):
|
||||
"""
|
||||
Returns the actor_id [int] for name [unicode] in table actor (without
|
||||
ensuring that the name matches).
|
||||
Returns the tuple
|
||||
(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
|
||||
|
||||
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',
|
||||
(name,))
|
||||
try:
|
||||
return self.cursor.fetchone()[0]
|
||||
return (self.cursor.fetchone()[0], False)
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -7,10 +7,13 @@ from __future__ import absolute_import, division, unicode_literals
|
|||
from logging import getLogger
|
||||
from json import loads
|
||||
import copy
|
||||
import json
|
||||
import binascii
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .plex_api import API
|
||||
from .plex_db import PlexDB
|
||||
from . import kodi_db
|
||||
from .downloadutils import DownloadUtils as DU
|
||||
|
@ -143,6 +146,8 @@ class KodiMonitor(xbmc.Monitor):
|
|||
elif method == "System.OnQuit":
|
||||
LOG.info('Kodi OnQuit detected - shutting down')
|
||||
app.APP.stop_pkc = True
|
||||
elif method == 'Other.plugin.video.plexkodiconnect_play_action':
|
||||
self._start_next_episode(data)
|
||||
|
||||
@staticmethod
|
||||
def _hack_addon_paths_replay_video():
|
||||
|
@ -283,6 +288,18 @@ class KodiMonitor(xbmc.Monitor):
|
|||
json_item.get('type'),
|
||||
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):
|
||||
"""
|
||||
Called whenever playback is started. Example data:
|
||||
|
@ -415,6 +432,8 @@ class KodiMonitor(xbmc.Monitor):
|
|||
status['playmethod'] = item.playmethod
|
||||
status['playcount'] = item.playcount
|
||||
LOG.debug('Set the player state: %s', status)
|
||||
if not app.SYNC.direct_paths:
|
||||
_notify_upnext(item)
|
||||
|
||||
|
||||
def _playback_cleanup(ended=False):
|
||||
|
@ -537,6 +556,85 @@ def _clean_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):
|
||||
"""
|
||||
Detect the resume dialog for widgets. Could also be used to detect
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
import urllib
|
||||
import copy
|
||||
|
||||
from ..utils import etree
|
||||
from .. import variables as v, utils
|
||||
|
@ -18,40 +19,37 @@ RECOMMENDED_SCORE_LOWER_BOUND = 7
|
|||
# )
|
||||
NODE_TYPES = {
|
||||
v.PLEX_TYPE_MOVIE: (
|
||||
('ondeck',
|
||||
('plex_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',
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
True),
|
||||
('pkc_ondeck',
|
||||
('ondeck',
|
||||
utils.lang(39502), # "PKC On Deck (faster)"
|
||||
{},
|
||||
'movies',
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
False),
|
||||
('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',
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
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',
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
False),
|
||||
('recommended',
|
||||
utils.lang(30230), # "Recommended"
|
||||
|
@ -59,30 +57,27 @@ NODE_TYPES = {
|
|||
'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',
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
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',
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
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',
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
False),
|
||||
('random',
|
||||
utils.lang(30227), # "Random"
|
||||
|
@ -90,20 +85,18 @@ NODE_TYPES = {
|
|||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}&%s'
|
||||
% urllib.urlencode({'sort': 'random'})),
|
||||
'plex_type': '{self.section_type}',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
'movies',
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
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',
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
False),
|
||||
('browse',
|
||||
utils.lang(39702), # "Browse by folder"
|
||||
|
@ -111,19 +104,20 @@ NODE_TYPES = {
|
|||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/folder',
|
||||
'plex_type': '{self.section_type}',
|
||||
'section_id': '{self.section_id}'
|
||||
'section_id': '{self.section_id}',
|
||||
'folder': True
|
||||
},
|
||||
'movies',
|
||||
v.CONTENT_TYPE_MOVIE,
|
||||
True),
|
||||
('more',
|
||||
utils.lang(22082), # "More..."
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'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),
|
||||
),
|
||||
###########################################################
|
||||
|
@ -133,30 +127,27 @@ NODE_TYPES = {
|
|||
{
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/{self.section_id}/onDeck',
|
||||
'plex_type': '{self.section_type}',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
'episodes',
|
||||
v.CONTENT_TYPE_EPISODE,
|
||||
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',
|
||||
v.CONTENT_TYPE_EPISODE,
|
||||
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',
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
False),
|
||||
('recommended',
|
||||
utils.lang(30230), # "Recommended"
|
||||
|
@ -164,30 +155,27 @@ NODE_TYPES = {
|
|||
'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',
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
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',
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
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',
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
True), # There are no sets/collections for shows with Kodi
|
||||
('random',
|
||||
utils.lang(30227), # "Random"
|
||||
|
@ -195,10 +183,9 @@ NODE_TYPES = {
|
|||
'mode': 'browseplex',
|
||||
'key': ('/library/sections/{self.section_id}&%s'
|
||||
% urllib.urlencode({'sort': 'random'})),
|
||||
'plex_type': '{self.section_type}',
|
||||
'section_id': '{self.section_id}'
|
||||
},
|
||||
'tvshows',
|
||||
v.CONTENT_TYPE_SHOW,
|
||||
False),
|
||||
('lastplayed',
|
||||
utils.lang(568), # "Last played"
|
||||
|
@ -206,30 +193,29 @@ NODE_TYPES = {
|
|||
'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',
|
||||
v.CONTENT_TYPE_EPISODE,
|
||||
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}'
|
||||
'section_id': '{self.section_id}',
|
||||
'folder': True
|
||||
},
|
||||
'episodes',
|
||||
v.CONTENT_TYPE_EPISODE,
|
||||
True),
|
||||
('more',
|
||||
utils.lang(22082), # "More..."
|
||||
{
|
||||
'mode': 'browseplex',
|
||||
'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),
|
||||
),
|
||||
}
|
||||
|
@ -239,9 +225,19 @@ 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
|
||||
|
||||
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),
|
||||
'type': 'folder'})
|
||||
if 'folder' in args:
|
||||
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, 'icon').text = ICON_PATH
|
||||
etree.SubElement(xml, 'content').text = section.content
|
||||
|
@ -249,7 +245,7 @@ def node_pms(section, node_name, args):
|
|||
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
|
||||
"""
|
||||
|
|
|
@ -132,7 +132,7 @@ class Section(object):
|
|||
@section_type.setter
|
||||
def section_type(self, 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
|
||||
if value == v.PLEX_TYPE_PHOTO:
|
||||
self.sync_to_kodi = False
|
||||
|
@ -239,29 +239,41 @@ class Section(object):
|
|||
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_index = self.addon_path(args)
|
||||
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)
|
||||
index = 'library://video/Plex-%s' % self.section_id
|
||||
else:
|
||||
# No xmls to link to - let's show the listings on the fly
|
||||
index = addon_index
|
||||
args['key'] = '/library/sections/%s/all' % self.section_id
|
||||
# Node showing a menu for this section
|
||||
args = {
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/%s' % self.section_id,
|
||||
'section_id': unicode(self.section_id)
|
||||
}
|
||||
if not self.sync_to_kodi:
|
||||
args['synched'] = 'false'
|
||||
# No library xmls to speed things up
|
||||
# Immediately show the PMS options for this section
|
||||
index = self.addon_path(args)
|
||||
# Node directly displaying all content
|
||||
args = {
|
||||
'mode': 'browseplex',
|
||||
'key': '/library/sections/%s/all' % self.section_id,
|
||||
'section_id': unicode(self.section_id)
|
||||
}
|
||||
if not self.sync_to_kodi:
|
||||
args['synched'] = 'false'
|
||||
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.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.content' % self.node, value=index)
|
||||
# .path leads to all elements of this library
|
||||
if self.section_type in v.PLEX_VIDEOTYPES:
|
||||
utils.window('%s.path' % self.node,
|
||||
|
@ -274,8 +286,6 @@ class Section(object):
|
|||
utils.window('%s.path' % self.node,
|
||||
value='ActivateWindow(pictures,%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_index' % self.node, value=addon_index)
|
||||
if not self.sync_to_kodi:
|
||||
self.remove_files_from_kodi()
|
||||
return
|
||||
|
@ -312,18 +322,22 @@ class Section(object):
|
|||
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:
|
||||
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)
|
||||
path = path_ops.path.join(self.path, xml_name)
|
||||
if not path_ops.exists(path):
|
||||
# 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._write_xml(xml, xml_name)
|
||||
path = 'library://video/Plex-%s/%s' % (self.section_id, xml_name)
|
||||
self.order += 1
|
||||
path = 'library://video/Plex-%s/%s' % (self.section_id, xml_name)
|
||||
self._window_node(path, node_name, node_type, pms_node)
|
||||
|
||||
def _write_xml(self, xml, xml_name):
|
||||
|
@ -337,7 +351,7 @@ class Section(object):
|
|||
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]})
|
||||
attrib={'type': v.CONTENT_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',
|
||||
|
@ -647,7 +661,6 @@ def _clear_window_vars(index):
|
|||
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_index' % node, clear=True)
|
||||
# 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 kind in WINDOW_ARGS:
|
||||
|
|
|
@ -51,4 +51,10 @@ def check_migration():
|
|||
plexdb.cursor.execute('DROP INDEX IF EXISTS ix_playlists_3')
|
||||
# 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)
|
||||
|
|
|
@ -234,13 +234,14 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
|||
playqueue.clear()
|
||||
if plex_type != v.PLEX_TYPE_CLIP:
|
||||
# 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.attrib.get('librarySectionUUID'),
|
||||
section_uuid,
|
||||
mediatype=plex_type,
|
||||
trailers=trailers)
|
||||
if xml is None:
|
||||
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"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
|
|
|
@ -71,6 +71,13 @@ class Base(object):
|
|||
"""
|
||||
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
|
||||
def plex_type(self):
|
||||
"""
|
||||
|
|
|
@ -91,9 +91,10 @@ class File(object):
|
|||
key = '/library/sections/%s/%s' % (section_id, key)
|
||||
params = {
|
||||
'mode': 'browseplex',
|
||||
'key': key,
|
||||
'plex_type': plex_type or self.plex_type
|
||||
'key': key
|
||||
}
|
||||
if plex_type or self.plex_type:
|
||||
params['plex_type'] = plex_type or self.plex_type
|
||||
if not synched:
|
||||
# No item to be found in the Kodi DB
|
||||
params['synched'] = 'false'
|
||||
|
|
|
@ -33,7 +33,8 @@ def update_playqueue_from_PMS(playqueue,
|
|||
playqueue_id=None,
|
||||
repeat=None,
|
||||
offset=None,
|
||||
transient_token=None):
|
||||
transient_token=None,
|
||||
start_plex_id=None):
|
||||
"""
|
||||
Completely updates the Kodi playqueue with the new Plex playqueue. Pass
|
||||
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)
|
||||
"""
|
||||
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
|
||||
if transient_token is None:
|
||||
transient_token = playqueue.plex_transient_token
|
||||
|
@ -61,7 +63,10 @@ def update_playqueue_from_PMS(playqueue,
|
|||
return
|
||||
playqueue.repeat = 0 if not repeat else int(repeat)
|
||||
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):
|
||||
|
@ -154,11 +159,15 @@ class PlexCompanion(backgroundthread.KillableThread):
|
|||
api = API(xml[0])
|
||||
playqueue = PQ.get_playqueue_from_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,
|
||||
playqueue_id=container_key,
|
||||
repeat=query.get('repeat'),
|
||||
offset=data.get('offset'),
|
||||
transient_token=data.get('token'))
|
||||
transient_token=data.get('token'),
|
||||
key=key)
|
||||
|
||||
@staticmethod
|
||||
def _process_streams(data):
|
||||
|
|
|
@ -1029,3 +1029,15 @@ def GetUserArtworkURL(username):
|
|||
url = user.thumb
|
||||
LOG.debug("Avatar url for user %s is: %s", username, 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))
|
||||
|
|
|
@ -165,6 +165,7 @@ PLEX_TYPE_VIDEO = 'video'
|
|||
PLEX_TYPE_MOVIE = 'movie'
|
||||
PLEX_TYPE_CLIP = 'clip' # e.g. trailers
|
||||
PLEX_TYPE_SET = 'collection' # sets/collections
|
||||
PLEX_TYPE_GENRE = 'genre'
|
||||
PLEX_TYPE_MIXED = 'mixed'
|
||||
|
||||
PLEX_TYPE_EPISODE = 'episode'
|
||||
|
@ -201,7 +202,7 @@ KODI_PLAYLIST_TYPE_FROM_PLEX_PLAYLIST_TYPE = {
|
|||
KODI_TYPE_VIDEO = 'video'
|
||||
KODI_TYPE_MOVIE = 'movie'
|
||||
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_SEASON = 'season'
|
||||
|
@ -216,6 +217,24 @@ KODI_TYPE_MUSICVIDEO = 'musicvideo'
|
|||
KODI_TYPE_PHOTO = 'photo'
|
||||
|
||||
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_TYPE_VIDEO,
|
||||
|
@ -306,7 +325,6 @@ PLEX_TYPE_FROM_KODI_TYPE = {
|
|||
KODI_TYPE_EPISODE: PLEX_TYPE_EPISODE,
|
||||
KODI_TYPE_SEASON: PLEX_TYPE_SEASON,
|
||||
KODI_TYPE_SHOW: PLEX_TYPE_SHOW,
|
||||
KODI_TYPE_CLIP: PLEX_TYPE_CLIP,
|
||||
KODI_TYPE_ARTIST: PLEX_TYPE_ARTIST,
|
||||
KODI_TYPE_ALBUM: PLEX_TYPE_ALBUM,
|
||||
KODI_TYPE_SONG: PLEX_TYPE_SONG,
|
||||
|
@ -418,24 +436,29 @@ PLEX_TYPE_NUMBER_FROM_PLEX_TYPE = {
|
|||
PLEX_TYPE_ALBUM: 9,
|
||||
PLEX_TYPE_SONG: 10,
|
||||
PLEX_TYPE_CLIP: 12,
|
||||
'playlist': 15,
|
||||
PLEX_TYPE_PLAYLIST: 15,
|
||||
PLEX_TYPE_SET: 18
|
||||
}
|
||||
|
||||
|
||||
# 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',
|
||||
CONTENT_FROM_PLEX_TYPE = {
|
||||
PLEX_TYPE_MOVIE: CONTENT_TYPE_MOVIE,
|
||||
PLEX_TYPE_SHOW: CONTENT_TYPE_SHOW,
|
||||
PLEX_TYPE_SEASON: CONTENT_TYPE_SEASON,
|
||||
PLEX_TYPE_EPISODE: CONTENT_TYPE_EPISODE,
|
||||
PLEX_TYPE_ARTIST: CONTENT_TYPE_ARTIST,
|
||||
PLEX_TYPE_ALBUM: CONTENT_TYPE_ALBUM,
|
||||
PLEX_TYPE_SONG: CONTENT_TYPE_SONG,
|
||||
PLEX_TYPE_CLIP: CONTENT_TYPE_CLIP,
|
||||
PLEX_TYPE_SET: CONTENT_TYPE_SET,
|
||||
PLEX_TYPE_PHOTO: CONTENT_TYPE_PHOTO,
|
||||
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 = {
|
||||
|
|
|
@ -79,24 +79,49 @@ def generate_item(api):
|
|||
|
||||
def _generate_folder(api):
|
||||
'''Generates "folder"/"directory" items that user can further navigate'''
|
||||
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',
|
||||
}
|
||||
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()
|
||||
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': typus,
|
||||
'IsPlayable': 'false',
|
||||
}
|
||||
|
||||
|
||||
def _generate_content(api):
|
||||
|
|
Loading…
Reference in a new issue