Rewire llibrary sync, part 3

This commit is contained in:
croneter 2018-10-21 16:56:13 +02:00
parent 3f4c43e373
commit e935b7c97b
12 changed files with 564 additions and 155 deletions

View file

@ -48,7 +48,7 @@ class ItemBase(object):
self.last_sync = last_sync self.last_sync = last_sync
self.artwork = artwork.Artwork() self.artwork = artwork.Artwork()
self.plexconn = None self.plexconn = None
self.plexcursor = plex_db.plexcursor if plex_db else None self.plexcursor = plex_db.cursor if plex_db else None
self.kodiconn = None self.kodiconn = None
self.kodicursor = kodi_db.cursor if kodi_db else None self.kodicursor = kodi_db.cursor if kodi_db else None
self.plex_db = plex_db self.plex_db = plex_db

View file

@ -11,13 +11,23 @@ LOG = getLogger('PLEX.tvshows')
class TvShowMixin(object): class TvShowMixin(object):
def remove(self, plex_id): def remove(self, plex_id, plex_type=None):
""" """
Remove the entire TV shows object (show, season or episode) including Remove the entire TV shows object (show, season or episode) including
all associated entries from the Kodi DB. all associated entries from the Kodi DB.
""" """
entry = self.plex_db.getItem_byId(plex_id) if plex_type is None:
if entry is None: entry = self.plex_db.episode(plex_id)
kodi_type = v.KODI_TYPE_EPISODE
if not entry:
entry = self.plex_db.season(plex_id)
kodi_type = v.KODI_TYPE_SEASON
if not entry:
entry = self.plex_db.show(plex_id)
kodi_type = v.KODI_TYPE_SHOW
else:
pass
if not entry:
LOG.debug('Cannot delete plex_id %s - not found in DB', plex_id) LOG.debug('Cannot delete plex_id %s - not found in DB', plex_id)
return return
kodi_id = entry[0] kodi_id = entry[0]
@ -144,13 +154,12 @@ class Show(ItemBase, TvShowMixin):
plex_id = api.plex_id() plex_id = api.plex_id()
LOG.debug('Adding show with plex_id %s', plex_id) LOG.debug('Adding show with plex_id %s', plex_id)
if not plex_id: if not plex_id:
LOG.error("Cannot parse XML data for TV show") LOG.error("Cannot parse XML data for TV show: %s", xml.attrib)
return return
update_item = True show = self.plex_db.show(plex_id)
entry = self.plex_db.getItem_byId(plex_id)
try: try:
kodi_id = entry[0] kodi_id = show[3]
path_id = entry[2] kodi_pathid = show[4]
except TypeError: except TypeError:
update_item = False update_item = False
query = 'SELECT COALESCE(MAX(idShow), 0) FROM tvshow' query = 'SELECT COALESCE(MAX(idShow), 0) FROM tvshow'
@ -158,8 +167,8 @@ class Show(ItemBase, TvShowMixin):
kodi_id = self.kodicursor.fetchone()[0] + 1 kodi_id = self.kodicursor.fetchone()[0] + 1
else: else:
# Verification the item is still in Kodi # Verification the item is still in Kodi
query = 'SELECT * FROM tvshow WHERE idShow = ?' self.kodicursor.execute('SELECT * FROM tvshow WHERE idShow = ?',
self.kodicursor.execute(query, (kodi_id,)) (kodi_id,))
try: try:
self.kodicursor.fetchone()[0] self.kodicursor.fetchone()[0]
except TypeError: except TypeError:
@ -193,7 +202,7 @@ class Show(ItemBase, TvShowMixin):
# Do NOT set a parent id because addon-path cannot be "stacked" # Do NOT set a parent id because addon-path cannot be "stacked"
toppathid = None toppathid = None
path_id = self.kodi_db.add_video_path(path, kodi_pathid = self.kodi_db.add_video_path(path,
date_added=api.date_created(), date_added=api.date_created(),
id_parent_path=toppathid) id_parent_path=toppathid)
# UPDATE THE TVSHOW ##### # UPDATE THE TVSHOW #####
@ -238,7 +247,7 @@ class Show(ItemBase, TvShowMixin):
plex_id, api.title()) plex_id, api.title())
# Link the path # Link the path
query = "INSERT INTO tvshowlinkpath(idShow, idPath) values (?, ?)" query = "INSERT INTO tvshowlinkpath(idShow, idPath) values (?, ?)"
self.kodicursor.execute(query, (kodi_id, path_id)) self.kodicursor.execute(query, (kodi_id, kodi_pathid))
# Create the reference in plex table # Create the reference in plex table
rating_id = self.kodi_db.get_ratingid(kodi_id, v.KODI_TYPE_SHOW) rating_id = self.kodi_db.get_ratingid(kodi_id, v.KODI_TYPE_SHOW)
@ -284,13 +293,12 @@ class Show(ItemBase, TvShowMixin):
tags = [section_name] tags = [section_name]
tags.extend([i for _, i in api.collection_list()]) tags.extend([i for _, i in api.collection_list()])
self.kodi_db.modify_tags(kodi_id, v.KODI_TYPE_SHOW, tags) self.kodi_db.modify_tags(kodi_id, v.KODI_TYPE_SHOW, tags)
self.plex_db.addReference(plex_id, self.plex_db.add_reference(plex_type=v.PLEX_TYPE_SHOW,
v.PLEX_TYPE_SHOW, plex_id=plex_id,
kodi_id,
v.KODI_TYPE_SHOW,
kodi_pathid=path_id,
checksum=api.checksum(), checksum=api.checksum(),
view_id=section_id, section_id=section_id,
kodi_id=kodi_id,
kodi_pathid=kodi_pathid,
last_sync=self.last_sync) last_sync=self.last_sync)
@ -304,33 +312,29 @@ class Season(ItemBase, TvShowMixin):
plex_id = api.plex_id() plex_id = api.plex_id()
LOG.debug('Adding season with plex_id %s', plex_id) LOG.debug('Adding season with plex_id %s', plex_id)
if not plex_id: if not plex_id:
LOG.error('Error getting plex_id for season, skipping') LOG.error('Error getting plex_id for season, skipping: %s',
xml.attrib)
return return
entry = self.plex_db.getItem_byId(api.parent_plex_id()) show_id = api.parent_plex_id()
show = self.plex_db.show(show_id)
try: try:
show_id = entry[0] parent_id = show[3]
except TypeError: except TypeError:
LOG.error('Could not find parent tv show for season %s. ' LOG.error('Could not find parent tv show for season %s. '
'Skipping season for now.', plex_id) 'Skipping season for now.', plex_id)
return return
kodi_id = self.kodi_db.add_season(show_id, api.season_number()) kodi_id = self.kodi_db.add_season(parent_id, api.season_number())
# Check whether Season already exists
entry = self.plex_db.getItem_byId(plex_id)
update_item = False if entry is None else True
self.artwork.modify_artwork(api.artwork(), self.artwork.modify_artwork(api.artwork(),
kodi_id, kodi_id,
v.KODI_TYPE_SEASON, v.KODI_TYPE_SEASON,
self.kodicursor) self.kodicursor)
if update_item: self.plex_db.add_reference(plex_type=v.PLEX_TYPE_SEASON,
self.plex_db.updateReference(plex_id, api.checksum()) plex_id=plex_id,
else:
self.plex_db.addReference(plex_id,
v.PLEX_TYPE_SEASON,
kodi_id,
v.KODI_TYPE_SEASON,
parent_id=show_id,
view_id=section_id,
checksum=api.checksum(), checksum=api.checksum(),
section_id=section_id,
show_id=show_id,
parent_id=parent_id,
kodi_id=kodi_id,
last_sync=self.last_sync) last_sync=self.last_sync)
@ -345,13 +349,14 @@ class Episode(ItemBase, TvShowMixin):
plex_id = api.plex_id() plex_id = api.plex_id()
LOG.debug('Adding episode with plex_id %s', plex_id) LOG.debug('Adding episode with plex_id %s', plex_id)
if not plex_id: if not plex_id:
LOG.error('Error getting plex_id for episode, skipping') LOG.error('Error getting plex_id for episode, skipping: %s',
xml.attrib)
return return
entry = self.plex_db.getItem_byId(plex_id) entry = self.plex_db.item_by_id(plex_id)
try: try:
kodi_id = entry[0] kodi_id = entry[0]
old_file_id = entry[1] old_kodi_fileid = entry[1]
path_id = entry[2] kodi_pathid = entry[2]
except TypeError: except TypeError:
update_item = False update_item = False
query = 'SELECT COALESCE(MAX(idEpisode), 0) FROM episode' query = 'SELECT COALESCE(MAX(idEpisode), 0) FROM episode'
@ -359,7 +364,7 @@ class Episode(ItemBase, TvShowMixin):
kodi_id = self.kodicursor.fetchone()[0] + 1 kodi_id = self.kodicursor.fetchone()[0] + 1
else: else:
# Verification the item is still in Kodi # Verification the item is still in Kodi
query = 'SELECT * FROM episode WHERE idEpisode = ?' query = 'SELECT * FROM episode WHERE idEpisode = ? LIMIT 1'
self.kodicursor.execute(query, (kodi_id, )) self.kodicursor.execute(query, (kodi_id, ))
try: try:
self.kodicursor.fetchone()[0] self.kodicursor.fetchone()[0]
@ -373,23 +378,22 @@ class Episode(ItemBase, TvShowMixin):
director = api.list_to_string(peoples['Director']) director = api.list_to_string(peoples['Director'])
writer = api.list_to_string(peoples['Writer']) writer = api.list_to_string(peoples['Writer'])
userdata = api.userdata() userdata = api.userdata()
series_id, _, season, episode = api.episode_data() show_id, season_id, _, season_no, episode_no = api.episode_data()
if season is None: if season_no is None:
season = -1 season_no = -1
if episode is None: if episode_no is None:
episode = -1 episode_no = -1
airs_before_season = "-1" airs_before_season = "-1"
airs_before_episode = "-1" airs_before_episode = "-1"
# Get season id show = self.plex_db.show(show_id)
show = self.plex_db.getItem_byId(series_id)
try: try:
show_id = show[0] grandparent_id = show[3]
except TypeError: except TypeError:
LOG.error("Parent tvshow now found, skip item") LOG.error("Parent tvshow now found, skip item")
return False return False
season_id = self.kodi_db.add_season(show_id, season) parent_id = self.kodi_db.add_season(grandparent_id, season_no)
# GET THE FILE AND PATH ##### # GET THE FILE AND PATH #####
do_indirect = not state.DIRECT_PATHS do_indirect = not state.DIRECT_PATHS
@ -407,29 +411,31 @@ class Episode(ItemBase, TvShowMixin):
filename = playurl.rsplit("/", 1)[1] filename = playurl.rsplit("/", 1)[1]
path = playurl.replace(filename, "") path = playurl.replace(filename, "")
parent_path_id = self.kodi_db.parent_path_id(path) parent_path_id = self.kodi_db.parent_path_id(path)
path_id = self.kodi_db.add_video_path( kodi_pathid = self.kodi_db.add_video_path(
path, id_parent_path=parent_path_id) path, id_parent_path=parent_path_id)
if do_indirect: if do_indirect:
# Set plugin path - do NOT use "intermediate" paths for the show # Set plugin path - do NOT use "intermediate" paths for the show
# as with direct paths! # as with direct paths!
filename = api.file_name(force_first_media=True) filename = api.file_name(force_first_media=True)
path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, series_id) path = 'plugin://%s.tvshows/%s/' % (v.ADDON_ID, show_id)
filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s'
% (path, plex_id, v.PLEX_TYPE_EPISODE, filename)) % (path, plex_id, v.PLEX_TYPE_EPISODE, filename))
playurl = filename playurl = filename
# Root path tvshows/ already saved in Kodi DB # Root path tvshows/ already saved in Kodi DB
path_id = self.kodi_db.add_video_path(path) kodi_pathid = self.kodi_db.add_video_path(path)
# add/retrieve path_id and fileid # add/retrieve kodi_pathid and fileid
# if the path or file already exists, the calls return current value # if the path or file already exists, the calls return current value
file_id = self.kodi_db.add_file(filename, path_id, api.date_created()) kodi_fileid = self.kodi_db.add_file(filename,
kodi_pathid,
api.date_created())
# UPDATE THE EPISODE ##### # UPDATE THE EPISODE #####
if update_item: if update_item:
LOG.info("UPDATE episode plex_id: %s, Title: %s", LOG.info("UPDATE episode plex_id: %s, Title: %s",
plex_id, api.title()) plex_id, api.title())
if file_id != old_file_id: if kodi_fileid != old_kodi_fileid:
self.kodi_db.remove_file(old_file_id) self.kodi_db.remove_file(old_kodi_fileid)
ratingid = self.kodi_db.get_ratingid(kodi_id, ratingid = self.kodi_db.get_ratingid(kodi_id,
v.KODI_TYPE_EPISODE) v.KODI_TYPE_EPISODE)
self.kodi_db.update_ratings(kodi_id, self.kodi_db.update_ratings(kodi_id,
@ -456,12 +462,10 @@ class Episode(ItemBase, TvShowMixin):
''' '''
self.kodicursor.execute( self.kodicursor.execute(
query, (api.title(), api.plot(), ratingid, writer, query, (api.title(), api.plot(), ratingid, writer,
api.premiere_date(), api.runtime(), director, season, api.premiere_date(), api.runtime(), director, season_no,
episode, api.title(), airs_before_season, episode_no, api.title(), airs_before_season,
airs_before_episode, playurl, path_id, file_id, airs_before_episode, playurl, kodi_pathid, kodi_fileid,
season_id, userdata['UserRating'], kodi_id)) parent_id, userdata['UserRating'], kodi_id))
# Update parentid reference
self.plex_db.updateParentId(plex_id, season_id)
# OR ADD THE EPISODE ##### # OR ADD THE EPISODE #####
else: else:
@ -492,11 +496,11 @@ class Episode(ItemBase, TvShowMixin):
?, ?) ?, ?)
''' '''
self.kodicursor.execute( self.kodicursor.execute(
query, (kodi_id, file_id, api.title(), api.plot(), rating_id, query, (kodi_id, kodi_fileid, api.title(), api.plot(), rating_id,
writer, api.premiere_date(), api.runtime(), director, writer, api.premiere_date(), api.runtime(), director,
season, episode, api.title(), show_id, season_no, episode_no, api.title(), grandparent_id,
airs_before_season, airs_before_episode, playurl, airs_before_season, airs_before_episode, playurl,
path_id, season_id, userdata['UserRating'])) kodi_pathid, parent_id, userdata['UserRating']))
self.kodi_db.modify_people(kodi_id, self.kodi_db.modify_people(kodi_id,
v.KODI_TYPE_EPISODE, v.KODI_TYPE_EPISODE,
@ -506,8 +510,8 @@ class Episode(ItemBase, TvShowMixin):
v.KODI_TYPE_EPISODE, v.KODI_TYPE_EPISODE,
self.kodicursor) self.kodicursor)
streams = api.mediastreams() streams = api.mediastreams()
self.kodi_db.modify_streams(file_id, streams, api.runtime()) self.kodi_db.modify_streams(kodi_fileid, streams, api.runtime())
self.kodi_db.set_resume(file_id, self.kodi_db.set_resume(kodi_fileid,
api.resume_point(), api.resume_point(),
api.runtime(), api.runtime(),
userdata['PlayCount'], userdata['PlayCount'],
@ -519,25 +523,27 @@ class Episode(ItemBase, TvShowMixin):
path = 'plugin://%s.tvshows/' % v.ADDON_ID path = 'plugin://%s.tvshows/' % v.ADDON_ID
# Filename is exactly the same, WITH plex show id! # Filename is exactly the same, WITH plex show id!
filename = ('%s%s/?plex_id=%s&plex_type=%s&mode=play&filename=%s' filename = ('%s%s/?plex_id=%s&plex_type=%s&mode=play&filename=%s'
% (path, series_id, plex_id, v.PLEX_TYPE_EPISODE, % (path, show_id, plex_id, v.PLEX_TYPE_EPISODE,
filename)) filename))
path_id = self.kodi_db.add_video_path(path) kodi_pathid = self.kodi_db.add_video_path(path)
file_id = self.kodi_db.add_file(filename, kodi_fileid = self.kodi_db.add_file(filename,
path_id, kodi_pathid,
api.date_created()) api.date_created())
self.kodi_db.set_resume(file_id, self.kodi_db.set_resume(kodi_fileid,
api.resume_point(), api.resume_point(),
api.runtime(), api.runtime(),
userdata['PlayCount'], userdata['PlayCount'],
userdata['LastPlayedDate'], userdata['LastPlayedDate'],
None) # Do send None - 2nd entry None) # Do send None - 2nd entry
self.plex_db.addReference(plex_id, self.plex_db.add_reference(plex_type=v.PLEX_TYPE_EPISODE,
v.PLEX_TYPE_EPISODE, plex_id=plex_id,
kodi_id,
v.KODI_TYPE_EPISODE,
kodi_file_id=file_id,
kodi_pathid=path_id,
parent_id=season_id,
checksum=api.checksum(), checksum=api.checksum(),
view_id=section_id, section_id=section_id,
show_id=show_id,
grandparent_id=grandparent_id,
season_id=season_id,
parent_id=parent_id,
kodi_id=kodi_id,
kodi_fileid=kodi_fileid,
kodi_pathid=kodi_pathid,
last_sync=self.last_sync) last_sync=self.last_sync)

View file

@ -30,9 +30,10 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
self.process_thread = None self.process_thread = None
self.last_sync = None self.last_sync = None
self.plex_db = None self.plex_db = None
self.plex_type = None
super(FullSync, self).__init__() super(FullSync, self).__init__()
def process_item(self, xml_item, get_children): def process_item(self, xml_item):
""" """
Processes a single library item Processes a single library item
""" """
@ -42,7 +43,7 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
backgroundthread.BGThreader.addTask( backgroundthread.BGThreader.addTask(
GetMetadataTask().setup(self.queue, GetMetadataTask().setup(self.queue,
plex_id, plex_id,
get_children)) self.get_children))
else: else:
if self.plex_db.check_checksum( if self.plex_db.check_checksum(
int('%s%s' % (xml_item['ratingKey'], int('%s%s' % (xml_item['ratingKey'],
@ -50,20 +51,29 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
backgroundthread.BGThreader.addTask( backgroundthread.BGThreader.addTask(
GetMetadataTask().setup(self.queue, GetMetadataTask().setup(self.queue,
plex_id, plex_id,
get_children)) self.get_children))
else: else:
self.plex_db.update_last_sync(plex_id, self.last_sync) self.plex_db.update_last_sync(plex_id, self.last_sync)
def process_delete(self):
"""
Removes all the items that have NOT been updated (last_sync timestamp)
is different
"""
with self.context() as c:
for plex_id in self.plex_db.plex_id_by_last_sync(self.plex_type,
self.last_sync):
if self.isCanceled():
return
c.remove(plex_id, plex_type=self.plex_type)
@utils.log_time @utils.log_time
def process_kind(self, kind): def process_kind(self):
""" """
kind is a tuple: (<name as unicode>,
kodi_type,
<itemtype class>,
get_children)
""" """
LOG.debug('Start processing %s', kind[0]) LOG.debug('Start processing %ss', self.plex_type)
sections = (x for x in sections.SECTIONS if x['kodi_type'] == kind[1]) sections = (x for x in sections.SECTIONS
if x['plex_type'] == self.plex_type)
for section in sections: for section in sections:
LOG.debug('Processing library section %s', section) LOG.debug('Processing library section %s', section)
if self.isCanceled(): if self.isCanceled():
@ -71,10 +81,12 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
if not self.install_sync_done: if not self.install_sync_done:
state.PATH_VERIFIED = False state.PATH_VERIFIED = False
try: try:
iterator = PF.PlexSectionItems(section['id']) iterator = PF.SectionItems(
section['id'],
{'type': v.PLEX_TYPE_NUMBER_FROM_PLEX_TYPE[self.plex_type]})
# Tell the processing thread about this new section # Tell the processing thread about this new section
queue_info = process_metadata.InitNewSection( queue_info = process_metadata.InitNewSection(
kind[2], self.context,
utils.cast(int, iterator.get('totalSize', 0)), utils.cast(int, iterator.get('totalSize', 0)),
utils.cast(unicode, iterator.get('librarySectionTitle')), utils.cast(unicode, iterator.get('librarySectionTitle')),
section['id']) section['id'])
@ -82,32 +94,42 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
for xml_item in iterator: for xml_item in iterator:
if self.isCanceled(): if self.isCanceled():
return False return False
self.process_item(xml_item, kind[3]) self.process_item(xml_item)
except RuntimeError: except RuntimeError:
LOG.error('Could not entirely process section %s', section) LOG.error('Could not entirely process section %s', section)
continue continue
LOG.debug('Finished processing %s', kind[0])
LOG.debug('Finished processing %ss', self.plex_type)
return True return True
def full_library_sync(self, new_items_only=False): def full_library_sync(self):
""" """
""" """
process = [self.plex_movies, self.plex_tv_show]
if state.ENABLE_MUSIC:
process.append(self.plex_music)
self.queue = backgroundthread.Queue.Queue(maxsize=200) self.queue = backgroundthread.Queue.Queue(maxsize=200)
t = process_metadata.ProcessMetadata(self.queue, self.last_sync) t = process_metadata.ProcessMetadata(self.queue, self.last_sync)
t.start() t.start()
kinds = [ kinds = [
('movies', v.KODI_TYPE_MOVIE, itemtypes.Movie, False), (v.PLEX_TYPE_MOVIE, itemtypes.Movie, False),
('tv shows', v.KODI_TYPE_SHOW, itemtypes.Show, False), (v.PLEX_TYPE_SHOW, itemtypes.Show, False),
('tv seasons', v.KODI_TYPE_SEASON, itemtypes.Season, False), (v.PLEX_TYPE_SEASON, itemtypes.Season, False),
('tv shows', v.KODI_TYPE_SHOW, itemtypes.Show, False), (v.PLEX_TYPE_EPISODE, itemtypes.Episode, False),
(v.PLEX_TYPE_ARTIST, itemtypes.Artist, False),
(v.PLEX_TYPE_ALBUM, itemtypes.Album, True),
(v.PLEX_TYPE_SONG, itemtypes.Song, False),
] ]
try:
for kind in kinds: for kind in kinds:
if self.isCanceled() or not self.process_kind(kind): # Setup our variables
self.plex_type = kind[0]
self.context = kind[1]
self.get_children = kind[2]
# Now do the heavy lifting
if self.isCanceled() or not self.process_kind():
return False return False
if not self.new_items_only:
# Delete movies that are not on Plex anymore - do this only once
self.process_delete()
# Let kodi update the views in any case, since we're doing a full sync # Let kodi update the views in any case, since we're doing a full sync
common.update_kodi_library(video=True, music=state.ENABLE_MUSIC) common.update_kodi_library(video=True, music=state.ENABLE_MUSIC)

View file

@ -54,10 +54,10 @@ def sync_from_pms():
VNODES.clearProperties() VNODES.clearProperties()
with plexdb.Get_Plex_DB() as plex_db: with plexdb.PlexDB() as plex_db:
# 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 = plex_db.sections() old_sections = [plex_db.section_ids()]
with kodidb.GetKodiDB('video') as kodi_db: with kodidb.GetKodiDB('video') as kodi_db:
for section in sections: for section in sections:
_process_section(section, _process_section(section,
@ -71,8 +71,8 @@ def sync_from_pms():
# 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.Get_Plex_DB() as plex_db: with plexdb.PlexDB() as plex_db:
SECTIONS = plex_db.list_section_info() SECTIONS = [plex_db.section_infos()]
utils.window('Plex.nodes.total', str(totalnodes)) utils.window('Plex.nodes.total', str(totalnodes))
LOG.info("Finished processing library sections: %s", SECTIONS) LOG.info("Finished processing library sections: %s", SECTIONS)
return True return True
@ -209,7 +209,7 @@ def delete_sections(old_sections):
sound=False) sound=False)
video_library_update = False video_library_update = False
music_library_update = False music_library_update = False
with plexdb.Get_Plex_DB() as plex_db: with plexdb.PlexDB() as plex_db:
old_sections = [plex_db.section_by_id(x) for x in old_sections] old_sections = [plex_db.section_by_id(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)
with kodidb.GetKodiDB() as kodi_db: with kodidb.GetKodiDB() as kodi_db:

View file

@ -214,20 +214,6 @@ class LibrarySync(Thread):
""" """
with plexdb.Get_Plex_DB() as plex_db: with plexdb.Get_Plex_DB() as plex_db:
# Create the tables for the plex database # Create the tables for the plex database
plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS plex(
plex_id INTEGER PRIMARY KEY ASC,
section_id INTEGER,
plex_type TEXT,
kodi_type TEXT,
kodi_id INTEGER,
kodi_fileid INTEGER,
kodi_pathid INTEGER,
parent_id INTEGER,
checksum INTEGER UNIQUE,
fanart_synced INTEGER,
last_sync INTEGER)
''')
plex_db.plexcursor.execute(''' plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS sections( CREATE TABLE IF NOT EXISTS sections(
section_id INTEGER PRIMARY KEY, section_id INTEGER PRIMARY KEY,
@ -236,6 +222,88 @@ class LibrarySync(Thread):
kodi_tagid INTEGER, kodi_tagid INTEGER,
sync_to_kodi INTEGER) sync_to_kodi INTEGER)
''') ''')
plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS movie(
plex_id INTEGER PRIMARY KEY ASC,
checksum INTEGER UNIQUE,
section_id INTEGER,
kodi_id INTEGER,
kodi_fileid INTEGER,
kodi_pathid INTEGER,
fanart_synced INTEGER,
last_sync INTEGER)
''')
plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS show(
plex_id INTEGER PRIMARY KEY ASC,
checksum INTEGER UNIQUE,
section_id INTEGER,
kodi_id INTEGER,
kodi_pathid INTEGER,
fanart_synced INTEGER,
last_sync INTEGER)
''')
plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS season(
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
show_id INTEGER, # plex_id of the parent show
parent_id INTEGER, # kodi_id of the parent show
kodi_id INTEGER,
fanart_synced INTEGER,
last_sync INTEGER)
''')
plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS episode(
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
show_id INTEGER, # plex_id of the parent show
grandparent_id INTEGER, # kodi_id of the parent show
season_id INTEGER, # plex_id of the parent season
parent_id INTEGER, # kodi_id of the parent season
kodi_id INTEGER,
kodi_fileid INTEGER,
kodi_pathid INTEGER,
fanart_synced INTEGER,
last_sync INTEGER)
''')
plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS artist(
plex_id INTEGER PRIMARY KEY ASC,
checksum INTEGER UNIQUE,
section_id INTEGER,
kodi_id INTEGER,
fanart_synced INTEGER,
last_sync INTEGER)
''')
plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS album(
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
artist_id INTEGER, # plex_id of the parent artist
parent_id INTEGER, # kodi_id of the parent artist
kodi_id INTEGER,
fanart_synced INTEGER,
last_sync INTEGER)
''')
plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS track(
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
artist_id INTEGER, # plex_id of the parent artist
grandparent_id INTEGER, # kodi_id of the parent artist
album_id INTEGER, # plex_id of the parent album
parent_id INTEGER, # kodi_id of the parent album
kodi_id INTEGER,
kodi_fileid INTEGER,
kodi_pathid INTEGER,
fanart_synced INTEGER,
last_sync INTEGER)
''')
plex_db.plexcursor.execute(''' plex_db.plexcursor.execute('''
CREATE TABLE IF NOT EXISTS playlists( CREATE TABLE IF NOT EXISTS playlists(
plex_id INTEGER PRIMARY KEY ASC, plex_id INTEGER PRIMARY KEY ASC,

View file

@ -95,17 +95,17 @@ def _write_playlist_to_file(playlist, xml):
api = API(element) api = API(element)
append_season_episode = False append_season_episode = False
if api.plex_type() == v.PLEX_TYPE_EPISODE: if api.plex_type() == v.PLEX_TYPE_EPISODE:
_, show, season_id, episode_id = api.episode_data() _, _, show, season_no, episode_no = api.episode_data()
try: try:
season_id = int(season_id) season_no = int(season_no)
episode_id = int(episode_id) episode_no = int(episode_no)
except ValueError: except ValueError:
pass pass
else: else:
append_season_episode = True append_season_episode = True
if append_season_episode: if append_season_episode:
text += ('#EXTINF:%s,%s S%.2dE%.2d - %s\n%s\n' text += ('#EXTINF:%s,%s S%.2dE%.2d - %s\n%s\n'
% (api.runtime(), show, season_id, episode_id, % (api.runtime(), show, season_no, episode_no,
api.title(), api.path())) api.title(), api.path()))
else: else:
# Only append the TV show name # Only append the TV show name

View file

@ -260,9 +260,9 @@ class API(object):
def season_number(self): def season_number(self):
""" """
Returns the 'index' of an PMS XML reply. Depicts e.g. season number. Returns the 'index' of an XML reply as int. Depicts e.g. season number.
""" """
return self.item.get('index') return cast(int, self.item.get('index'))
def date_created(self): def date_created(self):
""" """
@ -639,14 +639,14 @@ class API(object):
""" """
Returns the 'parentRatingKey' as a string or None Returns the 'parentRatingKey' as a string or None
""" """
return self.item.get('parentRatingKey') return cast(int, self.item.get('parentRatingKey'))
def grandparent_id(self): def grandparent_id(self):
""" """
Returns the ratingKey for the corresponding grandparent, e.g. a TV show Returns the ratingKey for the corresponding grandparent, e.g. a TV show
for episodes, or None for episodes, or None
""" """
return self.item.get('grandparentRatingKey') return cast(int, self.item.get('grandparentRatingKey'))
def grandparent_title(self): def grandparent_title(self):
""" """
@ -661,16 +661,18 @@ class API(object):
Output: for the corresponding the TV show and season: Output: for the corresponding the TV show and season:
[ [
TV show key, Plex: 'grandparentRatingKey' TV show ID, Plex: 'grandparentRatingKey'
TV season ID, Plex: 'grandparentRatingKey'
TV show title, Plex: 'grandparentTitle' TV show title, Plex: 'grandparentTitle'
TV show season, Plex: 'parentIndex' TV show season, Plex: 'parentIndex'
Episode number, Plex: 'index' Episode number, Plex: 'index'
] ]
""" """
return (self.item.get('grandparentRatingKey'), return (cast(int, self.item.get('grandparentRatingKey')),
self.item.get('grandparentTitle'), cast(int, self.item.get('parentRatingKey')),
self.item.get('parentIndex'), cast(unicode, self.item.get('grandparentTitle')),
self.item.get('index')) cast(int, self.item.get('parentIndex')),
cast(int, self.item.get('index')))
@staticmethod @staticmethod
def attach_plex_token_to_url(url): def attach_plex_token_to_url(url):
@ -1609,7 +1611,7 @@ class API(object):
if typus == v.PLEX_TYPE_EPISODE: if typus == v.PLEX_TYPE_EPISODE:
metadata['mediatype'] = 'episode' metadata['mediatype'] = 'episode'
_, show, season, episode = self.episode_data() _, _, show, season, episode = self.episode_data()
season = -1 if season is None else int(season) season = -1 if season is None else int(season)
episode = -1 if episode is None else int(episode) episode = -1 if episode is None else int(episode)
metadata['episode'] = episode metadata['episode'] = episode

View file

@ -0,0 +1,43 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from .common import PlexDBBase
from .tvshows import
from .. import utils, variables as v
class PlexDB(object):
"""
Usage: with PlexDB() as plex_db:
plex_db.do_something()
On exiting "with" (no matter what), commits get automatically committed
and the db gets closed
"""
def __init__(self, kind=None):
pass
def __enter__(self):
self.plexconn = utils.kodi_sql('plex')
if kind is None:
func = PlexDBBase
return func(self.plexconn.cursor())
def __exit__(self, type, value, traceback):
self.plexconn.commit()
self.plexconn.close()
def wipe_dbs():
"""
Completely resets the Plex database
"""
query = "SELECT name FROM sqlite_master WHERE type = 'table'"
with PlexDB() as plex_db:
plex_db.plexcursor.execute(query)
tables = plex_db.plexcursor.fetchall()
tables = [i[0] for i in tables]
for table in tables:
delete_query = 'DELETE FROM %s' % table
plex_db.plexcursor.execute(delete_query)

View file

@ -0,0 +1,112 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
class PlexDB(object):
"""
Methods used for all types of items
"""
def __init__(self, cursor):
self.cursor = cursor
def section_ids(self):
"""
Returns an iterator for section Plex ids 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')
return ({'section_id': x[0],
'section_name': x[1],
'plex_type': x[2],
'kodi_tagid': x[3],
'sync_to_kodi': x[4]} for x in self.cursor)
def section_by_id(self, section_id):
"""
For section_id, returns tuple (or None)
(section_id,
section_name,
plex_type,
kodi_tagid,
sync_to_kodi)
"""
self.cursor.execute('SELECT * FROM sections WHERE section_id = ? LIMIT 1',
(section_id, ))
return self.cursor.fetchone()
def section_id_by_name(self, section_name):
"""
Returns the section_id for section_name (or None)
"""
self.cursor.execute('SELECT section_id FROM sections WHERE section_name = ? LIMIT 1,'
(section_name, ))
try:
return self.cursor.fetchone()[0]
except TypeError:
pass
def add_section(self, section_id, section_name, plex_type, kodi_tagid,
sync_to_kodi=True):
"""
Appends a Plex section to the Plex sections table
sync=False: Plex library won't be synced to Kodi
"""
query = '''
INSERT INTO sections(
section_id, section_name, plex_type, kodi_tagid, sync_to_kodi)
VALUES (?, ?, ?, ?, ?)
'''
self.cursor.execute(query,
(section_id,
section_name,
plex_type,
kodi_tagid,
sync_to_kodi))
def update_section(self, section_name, kodi_tagid, section_id):
"""
Updates the section_id with section_name and kodi_tagid
"""
query = '''
UPDATE sections
SET section_name = ?, kodi_tagid = ?
WHERE section_id = ?
'''
self.cursor.execute(query, (section_name, kodi_tagid, section_id))
def remove_section(self, section_id):
"""
Removes the Plex db entry for the section with section_id
"""
self.cursor.execute('DELETE FROM sections WHERE section_id = ?',
(section_id, ))
def item_by_id(self, plex_id):
"""
For plex_id, returns the tuple
(kodi_id, kodi_fileid, kodi_pathid, parent_id, kodi_type, plex_type)
None if not found
"""
query = '''
SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, kodi_type,
plex_type
FROM plex WHERE plex_id = ?
LIMIT 1
'''
self.cursor.execute(query, (plex_id,))
return self.cursor.fetchone()

View file

@ -0,0 +1,124 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from . import common
from .. import variables as v
###############################################################################
class PlexDB(common.PlexDB):
def add_reference(self, plex_type=None, plex_id=None, checksum=None,
section_id=None, show_id=None, grandparent_id=None,
season_id=None, parent_id=None, kodi_id=None,
kodi_fileid=None, kodi_pathid=None, last_sync=None):
"""
Appends or replaces an entry into the plex table
"""
if plex_type == v.PLEX_TYPE_EPISODE:
query = '''
INSERT OR REPLACE INTO episode(
plex_id, checksum, section_id, show_id, grandparent_id,
season_id, parent_id, kodi_id, kodi_fileid, kodi_pathid,
fanart_synced, last_sync)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
'''
self.plexcursor.execute(
query,
(plex_id, checksum, section_id, show_id, grandparent_id,
season_id, parent_id, kodi_id, kodi_fileid, kodi_pathid,
0, last_sync))
elif plex_type == v.PLEX_TYPE_SEASON:
query = '''
INSERT OR REPLACE INTO season(
plex_id, checksum, section_id, show_id, parent_id,
kodi_id, fanart_synced, last_sync)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
'''
self.plexcursor.execute(
query,
(plex_id, checksum, section_id, show_id, parent_id,
kodi_id, 0, last_sync))
elif plex_type == v.PLEX_TYPE_SHOW:
query = '''
INSERT OR REPLACE INTO show(
plex_id, checksum, section_id, kodi_id, kodi_pathid,
fanart_synced, last_sync)
VALUES (?, ?, ?, ?, ?, ?, ?)
'''
self.plexcursor.execute(
query,
(plex_id, checksum, section_id, kodi_id, kodi_pathid, 0,
last_sync))
def show(self, plex_id):
"""
Returns the show info as a tuple for the TV show with plex_id:
plex_id INTEGER PRIMARY KEY ASC,
checksum INTEGER UNIQUE,
section_id INTEGER,
kodi_id INTEGER,
kodi_pathid INTEGER,
fanart_synced INTEGER,
last_sync INTEGER
"""
self.cursor.execute('SELECT * FROM show WHERE plex_id = ?',
(plex_id, ))
return self.cursor.fetchone()
def season(self, plex_id):
"""
Returns the show info as a tuple for the TV show with plex_id:
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
show_id INTEGER, # plex_id of the parent show
parent_id INTEGER, # kodi_id of the parent show
kodi_id INTEGER,
fanart_synced INTEGER,
last_sync INTEGER
"""
self.cursor.execute('SELECT * FROM season WHERE plex_id = ?',
(plex_id, ))
return self.cursor.fetchone()
def episode(self, plex_id):
"""
Returns the show info as a tuple for the TV show with plex_id:
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
show_id INTEGER, # plex_id of the parent show
grandparent_id INTEGER, # kodi_id of the parent show
season_id INTEGER, # plex_id of the parent season
parent_id INTEGER, # kodi_id of the parent season
kodi_id INTEGER,
kodi_fileid INTEGER,
kodi_pathid INTEGER,
fanart_synced INTEGER,
last_sync INTEGER
"""
self.cursor.execute('SELECT * FROM episode WHERE plex_id = ?',
(plex_id, ))
return self.cursor.fetchone()
def plex_id_by_last_sync(self, plex_type, last_sync):
"""
Returns an iterator for all items where the last_sync is NOT identical
"""
self.cursor.execute('SELECT plex_id FROM ? WHERE last_sync <> ?',
(plex_type, last_sync, ))
return (x[0] for x in self.cursor)
def shows_plex_id_section_id(self):
"""
Iterator for tuples (plex_id, section_id) of all our TV shows
"""
self.cursor.execute('SELECT plex_id, section_id FROM show')
return self.cursor
def update_last_sync(self, plex_type, plex_id, last_sync):
"""
Sets a new timestamp for plex_id
"""

View file

@ -534,7 +534,8 @@ class DownloadGen(object):
Yields XML etree children or raises RuntimeError Yields XML etree children or raises RuntimeError
""" """
def __init__(self, url): def __init__(self, url, args=None):
self._args = args or {}
self._url = url self._url = url
self._pos = 0 self._pos = 0
self._exhausted = False self._exhausted = False
@ -542,17 +543,18 @@ class DownloadGen(object):
self.attrib = deepcopy(self.xml.attrib) self.attrib = deepcopy(self.xml.attrib)
def _download_chunk(self): def _download_chunk(self):
args = { self._args.update({
'X-Plex-Container-Size': CONTAINERSIZE, 'X-Plex-Container-Size': CONTAINERSIZE,
'X-Plex-Container-Start': self._pos, 'X-Plex-Container-Start': self._pos,
'sort': 'id' 'sort': 'id', # Entries are sorted by plex_id
} 'excludeAllLeaves': 1 # PMS wont attach a first summary child
self.xml = DU().downloadUrl(self._url, parameters=args) })
self.xml = DU().downloadUrl(self._url, parameters=self._args)
try: try:
self.xml.attrib self.xml.attrib
except AttributeError: except AttributeError:
LOG.error('Error while downloading chunks: %s, args: %s', LOG.error('Error while downloading chunks: %s, args: %s',
self._url, args) self._url, self._args)
raise RuntimeError('Error while downloading chunks for %s' raise RuntimeError('Error while downloading chunks for %s'
% self._url) % self._url)
@ -582,13 +584,31 @@ class DownloadGen(object):
return self.attrib.get(key, default) return self.attrib.get(key, default)
class PlexSectionItems(DownloadGen): class SectionItems(DownloadGen):
"""
Iterator object to get all items of a Plex library section
"""
def __init__(self, section_id, args):
super(SectionItems, self).__init__(
'{server}/library/sections/%s/all' % section_id, args)
class Children(DownloadGen):
"""
Iterator object to get all items of a Plex library section
"""
def __init__(self, plex_id):
super(Children, self).__init__(
'{server}/library/metadata/%s/children' % plex_id)
class Leaves(DownloadGen):
""" """
Iterator object to get all items of a Plex library section Iterator object to get all items of a Plex library section
""" """
def __init__(self, section_id): def __init__(self, section_id):
super(PlexSectionItems, self).__init__( super(Leaves, self).__init__(
'{server}/library/sections/%s/all' % section_id) '{server}/library/sections/%s/allLeaves' % section_id)
def DownloadChunks(url): def DownloadChunks(url):

View file

@ -355,6 +355,18 @@ PLEX_TYPE_FROM_WEBSOCKET = {
15: 'playlist' 15: 'playlist'
} }
PLEX_TYPE_NUMBER_FROM_PLEX_TYPE = {
PLEX_TYPE_MOVIE: 1,
PLEX_TYPE_SHOW: 2,
PLEX_TYPE_SEASON: 3,
PLEX_TYPE_EPISODE: 4,
PLEX_TYPE_ARTIST: 8,
PLEX_TYPE_ALBUM: 9,
PLEX_TYPE_SONG: 10,
PLEX_TYPE_CLIP: 12,
'playlist': 15
}
KODI_TO_PLEX_ARTWORK = { KODI_TO_PLEX_ARTWORK = {
'poster': 'thumb', 'poster': 'thumb',