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)
[![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!

View file

@ -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:

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:
- 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',
prompt=params.get('prompt'))
elif mode == 'show_section':
entrypoint.show_section(params.get('section_index'))
elif mode == '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
# 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)

View file

@ -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):
"""

View file

@ -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

View file

@ -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
"""

View file

@ -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:

View file

@ -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)

View file

@ -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),

View file

@ -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):
"""

View file

@ -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'

View file

@ -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):

View file

@ -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))

View file

@ -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 = {

View file

@ -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):