Choose which Plex libraries PKC should sync
This commit is contained in:
parent
f24266fb54
commit
50d770718d
10 changed files with 242 additions and 131 deletions
|
@ -141,6 +141,10 @@ class Main():
|
||||||
elif mode == 'hub':
|
elif mode == 'hub':
|
||||||
entrypoint.hub(params.get('type'))
|
entrypoint.hub(params.get('type'))
|
||||||
|
|
||||||
|
elif mode == 'select-libraries':
|
||||||
|
LOG.info('User requested to select Plex libraries')
|
||||||
|
transfer.plex_command('select-libraries')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
entrypoint.show_main_menu(content_type=params.get('content_type'))
|
entrypoint.show_main_menu(content_type=params.get('content_type'))
|
||||||
|
|
||||||
|
|
|
@ -558,6 +558,12 @@ msgctxt "#30523"
|
||||||
msgid "Also show sync progress for playstate and user data"
|
msgid "Also show sync progress for playstate and user data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# PKC Settings - Sync Options
|
||||||
|
msgctxt "#30524"
|
||||||
|
msgid "Select Plex libraries to sync"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
# PKC Settings - Playback
|
# PKC Settings - Playback
|
||||||
msgctxt "#30527"
|
msgctxt "#30527"
|
||||||
msgid "Ignore specials in next episodes"
|
msgid "Ignore specials in next episodes"
|
||||||
|
|
|
@ -7,3 +7,5 @@ from .websocket import store_websocket_message, process_websocket_messages, \
|
||||||
WEBSOCKET_MESSAGES, PLAYSTATE_SESSIONS
|
WEBSOCKET_MESSAGES, PLAYSTATE_SESSIONS
|
||||||
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
||||||
from .fanart import FanartThread, FanartTask
|
from .fanart import FanartThread, FanartTask
|
||||||
|
from .videonodes import VideoNodes
|
||||||
|
from .sections import force_full_sync
|
||||||
|
|
|
@ -68,6 +68,7 @@ class FullSync(common.fullsync_mixin):
|
||||||
self.context = None
|
self.context = None
|
||||||
self.get_children = None
|
self.get_children = None
|
||||||
self.successful = None
|
self.successful = None
|
||||||
|
self.section_success = None
|
||||||
self.install_sync_done = utils.settings('SyncInstallRunDone') == 'true'
|
self.install_sync_done = utils.settings('SyncInstallRunDone') == 'true'
|
||||||
self.threader = backgroundthread.ThreaderManager(
|
self.threader = backgroundthread.ThreaderManager(
|
||||||
worker=backgroundthread.NonstoppingBackgroundWorker,
|
worker=backgroundthread.NonstoppingBackgroundWorker,
|
||||||
|
@ -232,8 +233,8 @@ class FullSync(common.fullsync_mixin):
|
||||||
if not itemtype.update_userdata(xml_item, section['plex_type']):
|
if not itemtype.update_userdata(xml_item, section['plex_type']):
|
||||||
# Somehow did not sync this item yet
|
# Somehow did not sync this item yet
|
||||||
itemtype.add_update(xml_item,
|
itemtype.add_update(xml_item,
|
||||||
section['section_name'],
|
section_name=section['section_name'],
|
||||||
section['section_id'])
|
section_id=section['section_id'])
|
||||||
itemtype.plexdb.update_last_sync(int(xml_item.attrib['ratingKey']),
|
itemtype.plexdb.update_last_sync(int(xml_item.attrib['ratingKey']),
|
||||||
section['plex_type'],
|
section['plex_type'],
|
||||||
self.current_sync)
|
self.current_sync)
|
||||||
|
@ -248,38 +249,39 @@ class FullSync(common.fullsync_mixin):
|
||||||
LOG.error('Could not entirely process section %s', section)
|
LOG.error('Could not entirely process section %s', section)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def threaded_get_iterators(self, kinds, queue, updated_at=None,
|
def threaded_get_iterators(self, kinds, queue, all_items=False):
|
||||||
last_viewed_at=None):
|
|
||||||
"""
|
"""
|
||||||
PF.SectionItems is costly, so let's do it asynchronous
|
PF.SectionItems is costly, so let's do it asynchronous
|
||||||
"""
|
"""
|
||||||
if self.repair:
|
|
||||||
updated_at = None
|
|
||||||
last_viewed_at = None
|
|
||||||
else:
|
|
||||||
updated_at = updated_at - UPDATED_AT_SAFETY if updated_at else None
|
|
||||||
last_viewed_at = last_viewed_at - LAST_VIEWED_AT_SAFETY \
|
|
||||||
if last_viewed_at else None
|
|
||||||
try:
|
try:
|
||||||
for kind in kinds:
|
for kind in kinds:
|
||||||
for section in (x for x in sections.SECTIONS
|
for section in (x for x in sections.SECTIONS
|
||||||
if x['plex_type'] == kind[1]):
|
if x['plex_type'] == kind[1]):
|
||||||
if self.isCanceled():
|
if self.isCanceled():
|
||||||
return
|
return
|
||||||
|
if not section['sync_to_kodi']:
|
||||||
|
LOG.info('User chose to not sync section %s', section)
|
||||||
|
continue
|
||||||
element = copy.deepcopy(section)
|
element = copy.deepcopy(section)
|
||||||
element['section_type'] = element['plex_type']
|
element['section_type'] = element['plex_type']
|
||||||
element['plex_type'] = kind[0]
|
element['plex_type'] = kind[0]
|
||||||
element['element_type'] = kind[1]
|
element['element_type'] = kind[1]
|
||||||
element['context'] = kind[2]
|
element['context'] = kind[2]
|
||||||
element['get_children'] = kind[3]
|
element['get_children'] = kind[3]
|
||||||
|
if self.repair or all_items:
|
||||||
|
updated_at = None
|
||||||
|
else:
|
||||||
|
updated_at = section['last_sync'] - UPDATED_AT_SAFETY \
|
||||||
|
if section['last_sync'] else None
|
||||||
try:
|
try:
|
||||||
element['iterator'] = PF.SectionItems(section['section_id'],
|
element['iterator'] = PF.SectionItems(section['section_id'],
|
||||||
plex_type=kind[0],
|
plex_type=kind[0],
|
||||||
updated_at=updated_at,
|
updated_at=updated_at,
|
||||||
last_viewed_at=last_viewed_at)
|
last_viewed_at=None)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
LOG.warn('Sync at least partially unsuccessful')
|
LOG.warn('Sync at least partially unsuccessful')
|
||||||
self.successful = False
|
self.successful = False
|
||||||
|
self.section_success = False
|
||||||
else:
|
else:
|
||||||
queue.put(element)
|
queue.put(element)
|
||||||
finally:
|
finally:
|
||||||
|
@ -303,14 +305,13 @@ class FullSync(common.fullsync_mixin):
|
||||||
# Already start setting up the iterators. We need to enforce
|
# Already start setting up the iterators. We need to enforce
|
||||||
# syncing e.g. show before season before episode
|
# syncing e.g. show before season before episode
|
||||||
iterator_queue = Queue.Queue()
|
iterator_queue = Queue.Queue()
|
||||||
updated_at = int(utils.settings('lastfullsync')) or None
|
|
||||||
task = backgroundthread.FunctionAsTask(self.threaded_get_iterators,
|
task = backgroundthread.FunctionAsTask(self.threaded_get_iterators,
|
||||||
None,
|
None,
|
||||||
kinds,
|
kinds,
|
||||||
iterator_queue,
|
iterator_queue)
|
||||||
updated_at=updated_at)
|
|
||||||
backgroundthread.BGThreader.addTask(task)
|
backgroundthread.BGThreader.addTask(task)
|
||||||
while True:
|
while True:
|
||||||
|
self.section_success = True
|
||||||
section = iterator_queue.get()
|
section = iterator_queue.get()
|
||||||
iterator_queue.task_done()
|
iterator_queue.task_done()
|
||||||
if section is None:
|
if section is None:
|
||||||
|
@ -323,10 +324,14 @@ class FullSync(common.fullsync_mixin):
|
||||||
# Now do the heavy lifting
|
# Now do the heavy lifting
|
||||||
if self.isCanceled() or not self.addupdate_section(section):
|
if self.isCanceled() or not self.addupdate_section(section):
|
||||||
return False
|
return False
|
||||||
|
if self.section_success:
|
||||||
|
# Need to check because a thread might have missed to get
|
||||||
|
# some items from the PMS
|
||||||
|
with PlexDB() as plexdb:
|
||||||
|
# Set the new time mark for the next delta sync
|
||||||
|
plexdb.update_section_last_sync(section['section_id'],
|
||||||
|
self.current_sync)
|
||||||
common.update_kodi_library(video=True, music=True)
|
common.update_kodi_library(video=True, music=True)
|
||||||
if self.successful:
|
|
||||||
# Set timestamp for next sync - neglecting playstates!
|
|
||||||
utils.settings('lastfullsync', value=str(int(self.current_sync)))
|
|
||||||
# In order to not delete all your songs again
|
# In order to not delete all your songs again
|
||||||
if app.SYNC.enable_music:
|
if app.SYNC.enable_music:
|
||||||
kinds.extend([
|
kinds.extend([
|
||||||
|
@ -347,7 +352,8 @@ class FullSync(common.fullsync_mixin):
|
||||||
task = backgroundthread.FunctionAsTask(self.threaded_get_iterators,
|
task = backgroundthread.FunctionAsTask(self.threaded_get_iterators,
|
||||||
None,
|
None,
|
||||||
kinds,
|
kinds,
|
||||||
iterator_queue)
|
iterator_queue,
|
||||||
|
all_items=True)
|
||||||
backgroundthread.BGThreader.addTask(task)
|
backgroundthread.BGThreader.addTask(task)
|
||||||
while True:
|
while True:
|
||||||
section = iterator_queue.get()
|
section = iterator_queue.get()
|
||||||
|
@ -364,7 +370,7 @@ class FullSync(common.fullsync_mixin):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Delete movies that are not on Plex anymore
|
# Delete movies that are not on Plex anymore
|
||||||
LOG.info('Looking for items to delete')
|
LOG.debug('Looking for items to delete')
|
||||||
kinds = [
|
kinds = [
|
||||||
(v.PLEX_TYPE_MOVIE, itemtypes.Movie),
|
(v.PLEX_TYPE_MOVIE, itemtypes.Movie),
|
||||||
(v.PLEX_TYPE_SHOW, itemtypes.Show),
|
(v.PLEX_TYPE_SHOW, itemtypes.Show),
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from . import common, videonodes
|
from . import videonodes
|
||||||
from ..utils import cast
|
from ..utils import cast
|
||||||
from ..plex_db import PlexDB
|
from ..plex_db import PlexDB
|
||||||
from .. import kodi_db
|
from .. import kodi_db
|
||||||
|
@ -22,6 +22,16 @@ SECTIONS = []
|
||||||
IS_CANCELED = None
|
IS_CANCELED = None
|
||||||
|
|
||||||
|
|
||||||
|
def force_full_sync():
|
||||||
|
"""
|
||||||
|
Resets the sync timestamp for all sections to 0, thus forcing a subsequent
|
||||||
|
full sync (not delta)
|
||||||
|
"""
|
||||||
|
LOG.info('Telling PKC to do a full sync instead of a delta sync')
|
||||||
|
with PlexDB() as plexdb:
|
||||||
|
plexdb.force_full_sync()
|
||||||
|
|
||||||
|
|
||||||
def sync_from_pms(parent_self):
|
def sync_from_pms(parent_self):
|
||||||
"""
|
"""
|
||||||
Sync the Plex library sections
|
Sync the Plex library sections
|
||||||
|
@ -35,6 +45,7 @@ def sync_from_pms(parent_self):
|
||||||
|
|
||||||
|
|
||||||
def _sync_from_pms():
|
def _sync_from_pms():
|
||||||
|
global PLAYLISTS, NODES, SECTIONS
|
||||||
sections = PF.get_plex_sections()
|
sections = PF.get_plex_sections()
|
||||||
try:
|
try:
|
||||||
sections.attrib
|
sections.attrib
|
||||||
|
@ -45,7 +56,7 @@ def _sync_from_pms():
|
||||||
# Will reboot Kodi is new library detected
|
# Will reboot Kodi is new library detected
|
||||||
music.excludefromscan_music_folders(xml=sections)
|
music.excludefromscan_music_folders(xml=sections)
|
||||||
|
|
||||||
global PLAYLISTS, NODES, SECTIONS
|
VNODES.clearProperties()
|
||||||
SECTIONS = []
|
SECTIONS = []
|
||||||
NODES = {
|
NODES = {
|
||||||
v.PLEX_TYPE_MOVIE: [],
|
v.PLEX_TYPE_MOVIE: [],
|
||||||
|
@ -54,64 +65,46 @@ def _sync_from_pms():
|
||||||
v.PLEX_TYPE_PHOTO: []
|
v.PLEX_TYPE_PHOTO: []
|
||||||
}
|
}
|
||||||
PLAYLISTS = copy.deepcopy(NODES)
|
PLAYLISTS = copy.deepcopy(NODES)
|
||||||
sorted_sections = []
|
|
||||||
|
|
||||||
for section in sections:
|
|
||||||
if (section.attrib['type'] in
|
|
||||||
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW, v.PLEX_TYPE_PHOTO,
|
|
||||||
v.PLEX_TYPE_ARTIST)):
|
|
||||||
sorted_sections.append(section.attrib['title'])
|
|
||||||
LOG.debug('Sorted sections: %s', sorted_sections)
|
|
||||||
totalnodes = len(sorted_sections)
|
|
||||||
|
|
||||||
VNODES.clearProperties()
|
|
||||||
|
|
||||||
with PlexDB() as plexdb:
|
with PlexDB() as plexdb:
|
||||||
# Backup old sections to delete them later, if needed (at the end
|
# Backup old sections to delete them later, if needed (at the end
|
||||||
# of this method, only unused sections will be left in old_sections)
|
# of this method, only unused sections will be left in old_sections)
|
||||||
old_sections = list(plexdb.section_ids())
|
old_sections = list(plexdb.all_sections())
|
||||||
with kodi_db.KodiVideoDB() as kodidb:
|
with kodi_db.KodiVideoDB() as kodidb:
|
||||||
for section in sections:
|
for index, section in enumerate(sections):
|
||||||
_process_section(section,
|
_process_section(section,
|
||||||
kodidb,
|
kodidb,
|
||||||
plexdb,
|
plexdb,
|
||||||
sorted_sections,
|
index,
|
||||||
old_sections,
|
old_sections)
|
||||||
totalnodes)
|
|
||||||
if old_sections:
|
if old_sections:
|
||||||
# Section has been deleted on the PMS
|
# Section has been deleted on the PMS
|
||||||
delete_sections(old_sections)
|
delete_sections(old_sections)
|
||||||
# update sections for all:
|
# update sections for all:
|
||||||
with PlexDB() as plexdb:
|
with PlexDB() as plexdb:
|
||||||
SECTIONS = list(plexdb.section_infos())
|
SECTIONS = list(plexdb.all_sections())
|
||||||
utils.window('Plex.nodes.total', str(totalnodes))
|
utils.window('Plex.nodes.total', str(len(sections)))
|
||||||
LOG.info("Finished processing library sections: %s", SECTIONS)
|
LOG.info("Finished processing %s library sections: %s", len(sections), SECTIONS)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _process_section(section_xml, kodidb, plexdb, sorted_sections,
|
def _process_section(section_xml, kodidb, plexdb, index, old_sections):
|
||||||
old_sections, totalnodes):
|
global PLAYLISTS, NODES
|
||||||
folder = section_xml.attrib
|
folder = section_xml.attrib
|
||||||
plex_type = folder['type']
|
plex_type = folder['type']
|
||||||
# Only process supported formats
|
# Only process supported formats
|
||||||
if plex_type not in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW,
|
if plex_type not in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW,
|
||||||
v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_PHOTO):
|
v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_PHOTO):
|
||||||
LOG.error('Unsupported Plex section type: %s', folder)
|
LOG.error('Unsupported Plex section type: %s', folder)
|
||||||
return totalnodes
|
return
|
||||||
section_id = cast(int, folder['key'])
|
section_id = cast(int, folder['key'])
|
||||||
section_name = folder['title']
|
section_name = folder['title']
|
||||||
global PLAYLISTS, NODES
|
|
||||||
# Prevent duplicate for nodes of the same type
|
# Prevent duplicate for nodes of the same type
|
||||||
nodes = NODES[plex_type]
|
nodes = NODES[plex_type]
|
||||||
# Prevent duplicate for playlists of the same type
|
# Prevent duplicate for playlists of the same type
|
||||||
playlists = PLAYLISTS[plex_type]
|
playlists = PLAYLISTS[plex_type]
|
||||||
# Get current media folders from plex database
|
# Get current media folders from plex database
|
||||||
section = plexdb.section(section_id)
|
section = plexdb.section(section_id)
|
||||||
try:
|
if not section:
|
||||||
current_sectionname = section[1]
|
|
||||||
current_sectiontype = section[2]
|
|
||||||
current_tagid = section[3]
|
|
||||||
except TypeError:
|
|
||||||
LOG.info('Creating section id: %s in Plex database.', section_id)
|
LOG.info('Creating section id: %s in Plex database.', section_id)
|
||||||
tagid = kodidb.create_tag(section_name)
|
tagid = kodidb.create_tag(section_name)
|
||||||
# Create playlist for the video library
|
# Create playlist for the video library
|
||||||
|
@ -121,28 +114,30 @@ def _process_section(section_xml, kodidb, plexdb, sorted_sections,
|
||||||
playlists.append(section_name)
|
playlists.append(section_name)
|
||||||
# Create the video node
|
# Create the video node
|
||||||
if section_name not in nodes:
|
if section_name not in nodes:
|
||||||
VNODES.viewNode(sorted_sections.index(section_name),
|
VNODES.viewNode(index,
|
||||||
section_name,
|
section_name,
|
||||||
plex_type,
|
plex_type,
|
||||||
None,
|
None,
|
||||||
section_id)
|
section_id)
|
||||||
nodes.append(section_name)
|
nodes.append(section_name)
|
||||||
totalnodes += 1
|
|
||||||
# Add view to plex database
|
# Add view to plex database
|
||||||
plexdb.add_section(section_id, section_name, plex_type, tagid)
|
plexdb.add_section(section_id,
|
||||||
|
section_name,
|
||||||
|
plex_type,
|
||||||
|
tagid,
|
||||||
|
True, # Sync this new section for now
|
||||||
|
None)
|
||||||
else:
|
else:
|
||||||
LOG.info('Found library section id %s, name %s, type %s, tagid %s',
|
LOG.info('Found library section id %s, name %s, type %s, tagid %s',
|
||||||
section_id, current_sectionname, current_sectiontype,
|
section_id, section['section_name'], section['plex_type'],
|
||||||
current_tagid)
|
section['kodi_tagid'])
|
||||||
# Remove views that are still valid to delete rest later
|
# Remove views that are still valid to delete rest later
|
||||||
try:
|
for section in old_sections:
|
||||||
old_sections.remove(section_id)
|
if section['section_id'] == section_id:
|
||||||
except ValueError:
|
old_sections.remove(section)
|
||||||
# View was just created, nothing to remove
|
break
|
||||||
pass
|
|
||||||
|
|
||||||
# View was modified, update with latest info
|
# View was modified, update with latest info
|
||||||
if current_sectionname != section_name:
|
if section['section_name'] != section_name:
|
||||||
LOG.info('section id: %s new sectionname: %s',
|
LOG.info('section id: %s new sectionname: %s',
|
||||||
section_id, section_name)
|
section_id, section_name)
|
||||||
tagid = kodidb.create_tag(section_name)
|
tagid = kodidb.create_tag(section_name)
|
||||||
|
@ -151,22 +146,24 @@ def _process_section(section_xml, kodidb, plexdb, sorted_sections,
|
||||||
plexdb.add_section(section_id,
|
plexdb.add_section(section_id,
|
||||||
section_name,
|
section_name,
|
||||||
plex_type,
|
plex_type,
|
||||||
tagid)
|
tagid,
|
||||||
|
section['sync_to_kodi'], # Use "old" setting
|
||||||
|
section['last_sync'])
|
||||||
|
|
||||||
if plexdb.section_id_by_name(current_sectionname) is None:
|
if plexdb.section_id_by_name(section['section_name']) is None:
|
||||||
# The tag could be a combined view. Ensure there's
|
# The tag could be a combined view. Ensure there's
|
||||||
# no other tags with the same name before deleting
|
# no other tags with the same name before deleting
|
||||||
# playlist.
|
# playlist.
|
||||||
utils.playlist_xsp(plex_type,
|
utils.playlist_xsp(plex_type,
|
||||||
current_sectionname,
|
section['section_name'],
|
||||||
section_id,
|
section_id,
|
||||||
current_sectiontype,
|
section['plex_type'],
|
||||||
True)
|
True)
|
||||||
# Delete video node
|
# Delete video node
|
||||||
if plex_type != "musicvideos":
|
if plex_type != "musicvideos":
|
||||||
VNODES.viewNode(
|
VNODES.viewNode(
|
||||||
indexnumber=sorted_sections.index(section_name),
|
indexnumber=index,
|
||||||
tagname=current_sectionname,
|
tagname=section['section_name'],
|
||||||
mediatype=plex_type,
|
mediatype=plex_type,
|
||||||
viewtype=None,
|
viewtype=None,
|
||||||
viewid=section_id,
|
viewid=section_id,
|
||||||
|
@ -179,17 +176,16 @@ def _process_section(section_xml, kodidb, plexdb, sorted_sections,
|
||||||
playlists.append(section_name)
|
playlists.append(section_name)
|
||||||
# Add new video node
|
# Add new video node
|
||||||
if section_name not in nodes and plex_type != "musicvideos":
|
if section_name not in nodes and plex_type != "musicvideos":
|
||||||
VNODES.viewNode(sorted_sections.index(section_name),
|
VNODES.viewNode(index,
|
||||||
section_name,
|
section_name,
|
||||||
plex_type,
|
plex_type,
|
||||||
None,
|
None,
|
||||||
section_id)
|
section_id)
|
||||||
nodes.append(section_name)
|
nodes.append(section_name)
|
||||||
totalnodes += 1
|
|
||||||
# Update items with new tag
|
# Update items with new tag
|
||||||
for kodi_id in plexdb.kodiid_by_sectionid(section_id, plex_type):
|
for kodi_id in plexdb.kodiid_by_sectionid(section_id, plex_type):
|
||||||
kodidb.update_tag(
|
kodidb.update_tag(
|
||||||
current_tagid, tagid, kodi_id, current_sectiontype)
|
section['kodi_tagid'], tagid, kodi_id, section['plex_type'])
|
||||||
else:
|
else:
|
||||||
# Validate the playlist exists or recreate it
|
# Validate the playlist exists or recreate it
|
||||||
if (section_name not in playlists and plex_type in
|
if (section_name not in playlists and plex_type in
|
||||||
|
@ -200,14 +196,12 @@ def _process_section(section_xml, kodidb, plexdb, sorted_sections,
|
||||||
playlists.append(section_name)
|
playlists.append(section_name)
|
||||||
# Create the video node if not already exists
|
# Create the video node if not already exists
|
||||||
if section_name not in nodes and plex_type != "musicvideos":
|
if section_name not in nodes and plex_type != "musicvideos":
|
||||||
VNODES.viewNode(sorted_sections.index(section_name),
|
VNODES.viewNode(index,
|
||||||
section_name,
|
section_name,
|
||||||
plex_type,
|
plex_type,
|
||||||
None,
|
None,
|
||||||
section_id)
|
section_id)
|
||||||
nodes.append(section_name)
|
nodes.append(section_name)
|
||||||
totalnodes += 1
|
|
||||||
return totalnodes
|
|
||||||
|
|
||||||
|
|
||||||
def _delete_kodi_db_items(section_id, section_type):
|
def _delete_kodi_db_items(section_id, section_type):
|
||||||
|
@ -246,25 +240,55 @@ def delete_sections(old_sections):
|
||||||
Deletes all elements for a Plex section that has been deleted. (e.g. all
|
Deletes all elements for a Plex section that has been deleted. (e.g. all
|
||||||
TV shows, Seasons and Episodes of a Show section)
|
TV shows, Seasons and Episodes of a Show section)
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
with PlexDB() as plexdb:
|
|
||||||
old_sections = [plexdb.section(x) for x in old_sections]
|
|
||||||
LOG.info("Removing entire Plex library sections: %s", old_sections)
|
LOG.info("Removing entire Plex library sections: %s", old_sections)
|
||||||
for section in old_sections:
|
for section in old_sections:
|
||||||
# "Deleting <section_name>"
|
# "Deleting <section_name>"
|
||||||
utils.dialog('notification',
|
utils.dialog('notification',
|
||||||
heading='{plex}',
|
heading='{plex}',
|
||||||
message='%s %s' % (utils.lang(30052), section[1]),
|
message='%s %s' % (utils.lang(30052), section['section_name']),
|
||||||
icon='{plex}',
|
icon='{plex}',
|
||||||
sound=False)
|
sound=False)
|
||||||
if section[2] == v.PLEX_TYPE_PHOTO:
|
if section['plex_type'] == v.PLEX_TYPE_PHOTO:
|
||||||
# not synced - just remove the link in our Plex sections table
|
# not synced - just remove the link in our Plex sections table
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if not _delete_kodi_db_items(section[0], section[2]):
|
if not _delete_kodi_db_items(section['section_id'], section['plex_type']):
|
||||||
return
|
return
|
||||||
# Only remove Plex entry if we've removed all items first
|
# Only remove Plex entry if we've removed all items first
|
||||||
with PlexDB() as plexdb:
|
with PlexDB() as plexdb:
|
||||||
plexdb.remove_section(section[0])
|
plexdb.remove_section(section['section_id'])
|
||||||
finally:
|
|
||||||
common.update_kodi_library()
|
|
||||||
|
def choose_libraries():
|
||||||
|
"""
|
||||||
|
Displays a dialog for the user to select the libraries he wants synched
|
||||||
|
|
||||||
|
Returns True if this was successful, False if not
|
||||||
|
"""
|
||||||
|
# xbmcgui.Dialog().multiselect(heading, options[, autoclose, preselect, useDetails])
|
||||||
|
# "Select Plex libraries to sync"
|
||||||
|
import xbmcgui
|
||||||
|
sections = []
|
||||||
|
preselect = []
|
||||||
|
for i, section in enumerate(SECTIONS):
|
||||||
|
sections.append(section['section_name'])
|
||||||
|
if section['plex_type'] == v.PLEX_TYPE_ARTIST:
|
||||||
|
if section['sync_to_kodi'] and app.SYNC.enable_music:
|
||||||
|
preselect.append(i)
|
||||||
|
else:
|
||||||
|
if section['sync_to_kodi']:
|
||||||
|
preselect.append(i)
|
||||||
|
selected = xbmcgui.Dialog().multiselect(utils.lang(30524),
|
||||||
|
sections,
|
||||||
|
preselect=preselect,
|
||||||
|
useDetails=False)
|
||||||
|
if selected is None:
|
||||||
|
# User canceled
|
||||||
|
return False
|
||||||
|
with PlexDB() as plexdb:
|
||||||
|
for i, section in enumerate(SECTIONS):
|
||||||
|
sync = True if i in selected else False
|
||||||
|
plexdb.update_section_sync(section['section_id'], sync)
|
||||||
|
sections = list(plexdb.all_sections())
|
||||||
|
LOG.info('Plex libraries to sync: %s', sections)
|
||||||
|
return True
|
||||||
|
|
|
@ -194,7 +194,8 @@ def initialize():
|
||||||
section_name TEXT,
|
section_name TEXT,
|
||||||
plex_type TEXT,
|
plex_type TEXT,
|
||||||
kodi_tagid INTEGER,
|
kodi_tagid INTEGER,
|
||||||
sync_to_kodi INTEGER)
|
sync_to_kodi INTEGER,
|
||||||
|
last_sync INTEGER)
|
||||||
''')
|
''')
|
||||||
plexdb.cursor.execute('''
|
plexdb.cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS movie(
|
CREATE TABLE IF NOT EXISTS movie(
|
||||||
|
|
|
@ -4,43 +4,39 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
|
|
||||||
class Sections(object):
|
class Sections(object):
|
||||||
def section_ids(self):
|
def all_sections(self):
|
||||||
"""
|
"""
|
||||||
Returns an iterator for section Plex ids for all sections
|
Returns an iterator for all sections
|
||||||
"""
|
|
||||||
self.cursor.execute('SELECT section_id FROM sections')
|
|
||||||
return (x[0] for x in self.cursor)
|
|
||||||
|
|
||||||
def section_infos(self):
|
|
||||||
"""
|
|
||||||
Returns an iterator for dicts for all Plex libraries:
|
|
||||||
{
|
|
||||||
'section_id'
|
|
||||||
'section_name'
|
|
||||||
'plex_type'
|
|
||||||
'kodi_tagid'
|
|
||||||
'sync_to_kodi'
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
self.cursor.execute('SELECT * FROM sections')
|
self.cursor.execute('SELECT * FROM sections')
|
||||||
return ({'section_id': x[0],
|
return (self.entry_to_section(x) for x in self.cursor)
|
||||||
'section_name': x[1],
|
|
||||||
'plex_type': x[2],
|
|
||||||
'kodi_tagid': x[3],
|
|
||||||
'sync_to_kodi': x[4]} for x in self.cursor)
|
|
||||||
|
|
||||||
def section(self, section_id):
|
def section(self, section_id):
|
||||||
"""
|
"""
|
||||||
For section_id, returns the tuple (or None)
|
For section_id, returns the dict
|
||||||
section_id INTEGER PRIMARY KEY,
|
section_id INTEGER PRIMARY KEY,
|
||||||
section_name TEXT,
|
section_name TEXT,
|
||||||
plex_type TEXT,
|
plex_type TEXT,
|
||||||
kodi_tagid INTEGER,
|
kodi_tagid INTEGER,
|
||||||
sync_to_kodi INTEGER
|
sync_to_kodi BOOL,
|
||||||
|
last_sync INTEGER
|
||||||
"""
|
"""
|
||||||
self.cursor.execute('SELECT * FROM sections WHERE section_id = ? LIMIT 1',
|
self.cursor.execute('SELECT * FROM sections WHERE section_id = ? LIMIT 1',
|
||||||
(section_id, ))
|
(section_id, ))
|
||||||
return self.cursor.fetchone()
|
return self.entry_to_section(self.cursor.fetchone())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def entry_to_section(entry):
|
||||||
|
if not entry:
|
||||||
|
return
|
||||||
|
return {
|
||||||
|
'section_id': entry[0],
|
||||||
|
'section_name': entry[1],
|
||||||
|
'plex_type': entry[2],
|
||||||
|
'kodi_tagid': entry[3],
|
||||||
|
'sync_to_kodi': entry[4] == 1,
|
||||||
|
'last_sync': entry[5]
|
||||||
|
}
|
||||||
|
|
||||||
def section_id_by_name(self, section_name):
|
def section_id_by_name(self, section_name):
|
||||||
"""
|
"""
|
||||||
|
@ -54,22 +50,35 @@ class Sections(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_section(self, section_id, section_name, plex_type, kodi_tagid,
|
def add_section(self, section_id, section_name, plex_type, kodi_tagid,
|
||||||
sync_to_kodi=True):
|
sync_to_kodi, last_sync):
|
||||||
"""
|
"""
|
||||||
Appends a Plex section to the Plex sections table
|
Appends a Plex section to the Plex sections table
|
||||||
sync=False: Plex library won't be synced to Kodi
|
sync=False: Plex library won't be synced to Kodi
|
||||||
"""
|
"""
|
||||||
query = '''
|
query = '''
|
||||||
INSERT OR REPLACE INTO sections(
|
INSERT OR REPLACE INTO sections(
|
||||||
section_id, section_name, plex_type, kodi_tagid, sync_to_kodi)
|
section_id,
|
||||||
VALUES (?, ?, ?, ?, ?)
|
section_name,
|
||||||
|
plex_type,
|
||||||
|
kodi_tagid,
|
||||||
|
sync_to_kodi,
|
||||||
|
last_sync)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
'''
|
'''
|
||||||
self.cursor.execute(query,
|
self.cursor.execute(query,
|
||||||
(section_id,
|
(section_id,
|
||||||
section_name,
|
section_name,
|
||||||
plex_type,
|
plex_type,
|
||||||
kodi_tagid,
|
kodi_tagid,
|
||||||
sync_to_kodi))
|
sync_to_kodi,
|
||||||
|
last_sync))
|
||||||
|
|
||||||
|
def update_section(self, section_id, section_name):
|
||||||
|
"""
|
||||||
|
Updates the section with section_id
|
||||||
|
"""
|
||||||
|
query = 'UPDATE sections SET section_name = ? WHERE section_id = ?'
|
||||||
|
self.cursor.execute(query, (section_name, section_id))
|
||||||
|
|
||||||
def remove_section(self, section_id):
|
def remove_section(self, section_id):
|
||||||
"""
|
"""
|
||||||
|
@ -77,3 +86,35 @@ class Sections(object):
|
||||||
"""
|
"""
|
||||||
self.cursor.execute('DELETE FROM sections WHERE section_id = ?',
|
self.cursor.execute('DELETE FROM sections WHERE section_id = ?',
|
||||||
(section_id, ))
|
(section_id, ))
|
||||||
|
|
||||||
|
def update_section_sync(self, section_id, sync_to_kodi):
|
||||||
|
"""
|
||||||
|
Updates whether we should sync sections_id (sync=True) or not
|
||||||
|
"""
|
||||||
|
if sync_to_kodi:
|
||||||
|
query = '''
|
||||||
|
UPDATE sections
|
||||||
|
SET sync_to_kodi = ?
|
||||||
|
WHERE section_id = ?
|
||||||
|
'''
|
||||||
|
else:
|
||||||
|
# Set last_sync = 0 in order to force a full sync if reactivated
|
||||||
|
query = '''
|
||||||
|
UPDATE sections
|
||||||
|
SET sync_to_kodi = ?, last_sync = 0
|
||||||
|
WHERE section_id = ?
|
||||||
|
'''
|
||||||
|
self.cursor.execute(query, (sync_to_kodi, section_id))
|
||||||
|
|
||||||
|
def update_section_last_sync(self, section_id, last_sync):
|
||||||
|
"""
|
||||||
|
Updates the timestamp for the section
|
||||||
|
"""
|
||||||
|
self.cursor.execute('UPDATE sections SET last_sync = ? WHERE section_id = ?',
|
||||||
|
(last_sync, section_id))
|
||||||
|
|
||||||
|
def force_full_sync(self):
|
||||||
|
"""
|
||||||
|
Sets the last_sync flag to 0 for every section
|
||||||
|
"""
|
||||||
|
self.cursor.execute('UPDATE sections SET last_sync = 0')
|
||||||
|
|
|
@ -9,7 +9,7 @@ import xbmcgui
|
||||||
from . import utils, clientinfo, timing
|
from . import utils, clientinfo, timing
|
||||||
from . import initialsetup
|
from . import initialsetup
|
||||||
from . import kodimonitor
|
from . import kodimonitor
|
||||||
from . import sync
|
from . import sync, library_sync
|
||||||
from . import websocket_client
|
from . import websocket_client
|
||||||
from . import plex_companion
|
from . import plex_companion
|
||||||
from . import plex_functions as PF, playqueue as PQ
|
from . import plex_functions as PF, playqueue as PQ
|
||||||
|
@ -97,8 +97,7 @@ class Service():
|
||||||
|
|
||||||
# Load/Reset PKC entirely - important for user/Kodi profile switch
|
# Load/Reset PKC entirely - important for user/Kodi profile switch
|
||||||
# Clear video nodes properties
|
# Clear video nodes properties
|
||||||
from .library_sync import videonodes
|
library_sync.VideoNodes().clearProperties()
|
||||||
videonodes.VideoNodes().clearProperties()
|
|
||||||
clientinfo.getDeviceId()
|
clientinfo.getDeviceId()
|
||||||
# Init time-offset between Kodi and Plex
|
# Init time-offset between Kodi and Plex
|
||||||
timing.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset') or 0.0)
|
timing.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset') or 0.0)
|
||||||
|
@ -222,7 +221,7 @@ class Service():
|
||||||
utils.delete_nodes()
|
utils.delete_nodes()
|
||||||
app.ACCOUNT.set_unauthenticated()
|
app.ACCOUNT.set_unauthenticated()
|
||||||
# Force full sync after login
|
# Force full sync after login
|
||||||
utils.settings('lastfullsync', value='0')
|
library_sync.force_full_sync()
|
||||||
app.SYNC.run_lib_scan = 'full'
|
app.SYNC.run_lib_scan = 'full'
|
||||||
# Enable the main loop to display user selection dialog
|
# Enable the main loop to display user selection dialog
|
||||||
app.APP.suspend = False
|
app.APP.suspend = False
|
||||||
|
@ -286,6 +285,33 @@ class Service():
|
||||||
LOG.info("Entering PMS address complete")
|
LOG.info("Entering PMS address complete")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def choose_plex_libraries(self):
|
||||||
|
if not app.CONN.online:
|
||||||
|
LOG.error('PMS not online to choose libraries')
|
||||||
|
# "{0} offline"
|
||||||
|
utils.dialog('notification',
|
||||||
|
utils.lang(29999),
|
||||||
|
utils.lang(39213).format(app.CONN.server_name or ''),
|
||||||
|
icon='{plex}')
|
||||||
|
return
|
||||||
|
if not app.ACCOUNT.authenticated:
|
||||||
|
LOG.error('Not yet authenticated for PMS to choose libraries')
|
||||||
|
# "Unauthorized for PMS"
|
||||||
|
utils.dialog('notification', utils.lang(29999), utils.lang(30017))
|
||||||
|
return
|
||||||
|
app.APP.suspend_threads()
|
||||||
|
from .library_sync import sections
|
||||||
|
try:
|
||||||
|
# Get newest sections from the PMS
|
||||||
|
if not sections.sync_from_pms(self):
|
||||||
|
return
|
||||||
|
if not sections.choose_libraries():
|
||||||
|
return
|
||||||
|
# Force a full sync
|
||||||
|
app.SYNC.run_lib_scan = 'full'
|
||||||
|
finally:
|
||||||
|
app.APP.resume_threads()
|
||||||
|
|
||||||
def _do_auth(self):
|
def _do_auth(self):
|
||||||
LOG.info('Authenticating user')
|
LOG.info('Authenticating user')
|
||||||
if app.ACCOUNT.plex_username and not app.ACCOUNT.force_login: # Found a user in the settings, try to authenticate
|
if app.ACCOUNT.plex_username and not app.ACCOUNT.force_login: # Found a user in the settings, try to authenticate
|
||||||
|
@ -435,6 +461,8 @@ class Service():
|
||||||
app.SYNC.run_lib_scan = 'fanart'
|
app.SYNC.run_lib_scan = 'fanart'
|
||||||
elif plex_command == 'textures-scan':
|
elif plex_command == 'textures-scan':
|
||||||
app.SYNC.run_lib_scan = 'textures'
|
app.SYNC.run_lib_scan = 'textures'
|
||||||
|
elif plex_command == 'select-libraries':
|
||||||
|
self.choose_plex_libraries()
|
||||||
elif plex_command == 'RESET-PKC':
|
elif plex_command == 'RESET-PKC':
|
||||||
utils.reset()
|
utils.reset()
|
||||||
if task:
|
if task:
|
||||||
|
|
|
@ -466,7 +466,6 @@ def wipe_database():
|
||||||
kodi_db.reset_cached_images()
|
kodi_db.reset_cached_images()
|
||||||
# reset the install run flag
|
# reset the install run flag
|
||||||
settings('SyncInstallRunDone', value="false")
|
settings('SyncInstallRunDone', value="false")
|
||||||
settings('lastfullsync', value="0")
|
|
||||||
init_dbs()
|
init_dbs()
|
||||||
LOG.info('Wiping done')
|
LOG.info('Wiping done')
|
||||||
if settings('kodi_db_has_been_wiped_clean') != 'true':
|
if settings('kodi_db_has_been_wiped_clean') != 'true':
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
</category>
|
</category>
|
||||||
|
|
||||||
<category label="30506"><!-- Sync Options -->
|
<category label="30506"><!-- Sync Options -->
|
||||||
|
<setting label="[COLOR yellow]$ADDON[plugin.video.plexkodiconnect 30524][/COLOR]" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=select-libraries)" option="close" help="Choose which Plex library you want to sync"/><!-- Select Plex libraries to sync -->
|
||||||
<setting type="lsep" label="30537" /><!-- Restart if you make changes -->
|
<setting type="lsep" label="30537" /><!-- Restart if you make changes -->
|
||||||
<setting type="sep" />
|
<setting type="sep" />
|
||||||
<setting id="fullSyncInterval" type="number" label="39053" default="60" option="int" />
|
<setting id="fullSyncInterval" type="number" label="39053" default="60" option="int" />
|
||||||
|
@ -84,7 +85,6 @@
|
||||||
<setting id="themoviedbAPIKey" type="text" default="19c90103adb9e98f2172c6a6a3d85dc4" visible="false"/>
|
<setting id="themoviedbAPIKey" type="text" default="19c90103adb9e98f2172c6a6a3d85dc4" visible="false"/>
|
||||||
<setting id="FanArtTVAPIKey" type="text" default="639191cb0774661597f28a47e7e2bad5" visible="false"/>
|
<setting id="FanArtTVAPIKey" type="text" default="639191cb0774661597f28a47e7e2bad5" visible="false"/>
|
||||||
<setting id="syncEmptyShows" type="bool" label="30508" default="false" visible="false"/>
|
<setting id="syncEmptyShows" type="bool" label="30508" default="false" visible="false"/>
|
||||||
<setting id="lastfullsync" type="number" label="Time stamp when last successful full sync was conducted" default="0" visible="false" />
|
|
||||||
<setting id="kodi_db_has_been_wiped_clean" type="bool" label="PKC needs to completely clean the Kodi DB at least once, then reboot, to avoid Kodi error messages, e.g. OperationalError" default="false" visible="false" />
|
<setting id="kodi_db_has_been_wiped_clean" type="bool" label="PKC needs to completely clean the Kodi DB at least once, then reboot, to avoid Kodi error messages, e.g. OperationalError" default="false" visible="false" />
|
||||||
</category>
|
</category>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue