diff --git a/resources/lib/itemtypes/__init__.py b/resources/lib/itemtypes/__init__.py index bd99cd60..069a39e4 100644 --- a/resources/lib/itemtypes/__init__.py +++ b/resources/lib/itemtypes/__init__.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals +from .movies import Movie from .tvshows import Show, Season, Episode # Note: always use same order of URL arguments, NOT urlencode: diff --git a/resources/lib/itemtypes/movies.py b/resources/lib/itemtypes/movies.py new file mode 100644 index 00000000..4bfe8fcc --- /dev/null +++ b/resources/lib/itemtypes/movies.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals +from logging import getLogger + +from .common import ItemBase +from ..plex_api import API +from .. import state, variables as v, plex_functions as PF + +LOG = getLogger('PLEX.movies') + + +class Movie(ItemBase): + """ + Used for plex library-type movies + """ + def add_update(self, xml, section_name=None, section_id=None, + children=None): + """ + Process single movie + """ + api = API(xml) + update_item = True + plex_id = api.plex_id() + LOG.debug('Adding movie with plex_id %s', plex_id) + # Cannot parse XML, abort + if not plex_id: + LOG.error('Cannot parse XML data for movie: %s', xml.attrib) + return + movie = self.plex_db.getItem_byId(plex_id) + try: + kodi_id = movie[0] + old_kodi_fileid = movie[1] + kodi_pathid = movie[2] + except TypeError: + update_item = False + self.kodicursor.execute('SELECT COALESCE(MAX(idMovie), 0) FROM movie') + kodi_id = self.kodicursor.fetchone()[0] + 1 + else: + # Verification the item is still in Kodi + self.kodicursor.execute('SELECT idMovie FROM movie WHERE idMovie = ? LIMIT 1', + (kodi_id, )) + try: + self.kodicursor.fetchone()[0] + except TypeError: + # item is not found, let's recreate it. + update_item = False + LOG.info("kodi_id: %s missing from Kodi, repairing the entry.", + kodi_id) + + userdata = api.userdata() + playcount = userdata['PlayCount'] + dateplayed = userdata['LastPlayedDate'] + resume = userdata['Resume'] + runtime = userdata['Runtime'] + rating = userdata['Rating'] + + title = api.title() + people = api.people() + genres = api.genre_list() + collections = api.collection_list() + countries = api.country_list() + studios = api.music_studio_list() + + # GET THE FILE AND PATH ##### + do_indirect = not state.DIRECT_PATHS + if state.DIRECT_PATHS: + # Direct paths is set the Kodi way + playurl = api.file_path(force_first_media=True) + if playurl is None: + # Something went wrong, trying to use non-direct paths + do_indirect = True + else: + playurl = api.validate_playurl(playurl, api.plex_type()) + if playurl is None: + return False + if '\\' in playurl: + # Local path + filename = playurl.rsplit("\\", 1)[1] + else: + # Network share + filename = playurl.rsplit("/", 1)[1] + path = playurl.replace(filename, "") + kodi_pathid = self.kodi_db.add_video_path(path, + content='movies', + scraper='metadata.local') + if do_indirect: + # Set plugin path and media flags using real filename + filename = api.file_name(force_first_media=True) + path = 'plugin://%s.movies/' % v.ADDON_ID + filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' + % (path, plex_id, v.PLEX_TYPE_MOVIE, filename)) + playurl = filename + kodi_pathid = self.kodi_db.get_path(path) + + file_id = self.kodi_db.add_file(filename, + kodi_pathid, + api.date_created()) + + if update_item: + LOG.info('UPDATE movie plex_id: %s - Title: %s', + plex_id, api.title()) + if file_id != old_kodi_fileid: + self.kodi_db.remove_file(old_kodi_fileid) + rating_id = self.kodi_db.get_ratingid(kodi_id, + v.KODI_TYPE_MOVIE) + self.kodi_db.update_ratings(kodi_id, + v.KODI_TYPE_MOVIE, + "default", + rating, + api.votecount(), + rating_id) + # update new uniqueid Kodi 17 + if api.provider('imdb') is not None: + uniqueid = self.kodi_db.get_uniqueid(kodi_id, + v.KODI_TYPE_MOVIE) + self.kodi_db.update_uniqueid(kodi_id, + v.KODI_TYPE_MOVIE, + api.provider('imdb'), + "imdb", + uniqueid) + else: + self.kodi_db.remove_uniqueid(kodi_id, v.KODI_TYPE_MOVIE) + uniqueid = -1 + else: + LOG.info("ADD movie plex_id: %s - Title: %s", plex_id, title) + rating_id = self.kodi_db.get_ratingid(kodi_id, + v.KODI_TYPE_MOVIE) + self.kodi_db.add_ratings(rating_id, + kodi_id, + v.KODI_TYPE_MOVIE, + "default", + rating, + api.votecount()) + if api.provider('imdb') is not None: + uniqueid = self.kodi_db.get_uniqueid(kodi_id, + v.KODI_TYPE_MOVIE) + self.kodi_db.add_uniqueid(uniqueid, + kodi_id, + v.KODI_TYPE_MOVIE, + api.provider('imdb'), + "imdb") + else: + uniqueid = -1 + + # Update Kodi's main entry + query = ''' + INSERT OR REPLACE INTO movie(idMovie, idFile, c00, c01, c02, c03, + c04, c05, c06, c07, c09, c10, c11, c12, c14, c15, c16, + c18, c19, c21, c22, c23, premiered, userrating) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, ?) + ''' + self.kodicursor.execute( + query, + (kodi_id, file_id, title, api.plot(), api.shortplot(), + api.tagline(), api.votecount(), rating_id, + api.list_to_string(people['Writer']), api.year(), + uniqueid, api.sorttitle(), runtime, api.content_rating(), + api.list_to_string(genres), api.list_to_string(people['Director']), + title, api.list_to_string(studios), api.trailer(), + api.list_to_string(countries), playurl, kodi_pathid, + api.premiere_date(), userdata['UserRating'])) + + self.kodi_db.modify_countries(kodi_id, v.KODI_TYPE_MOVIE, countries) + self.kodi_db.modify_people(kodi_id, + v.KODI_TYPE_MOVIE, + api.people_list()) + self.kodi_db.modify_genres(kodi_id, v.KODI_TYPE_MOVIE, genres) + self.artwork.modify_artwork(api.artwork(), + kodi_id, + v.KODI_TYPE_MOVIE, + self.kodicursor) + self.kodi_db.modify_streams(file_id, api.mediastreams(), runtime) + self.kodi_db.modify_studios(kodi_id, v.KODI_TYPE_MOVIE, studios) + tags = [section_name] + if collections: + collections_match = api.collections_match() + for plex_set_id, set_name in collections: + tags.append(set_name) + # Add any sets from Plex collection tags + kodi_set_id = self.kodi_db.create_collection(set_name) + self.kodi_db.assign_collection(kodi_set_id, kodi_id) + for index, plex_id in collections_match: + # Get Plex artwork for collections - a pain + if index == plex_set_id: + set_xml = PF.GetPlexMetadata(plex_id) + try: + set_xml.attrib + except AttributeError: + LOG.error('Could not get set metadata %s', plex_id) + continue + set_api = API(set_xml[0]) + self.artwork.modify_artwork(set_api.artwork(), + kodi_set_id, + v.KODI_TYPE_SET, + self.kodicursor) + break + self.kodi_db.modify_tags(kodi_id, v.KODI_TYPE_MOVIE, tags) + # Process playstate + self.kodi_db.set_resume(file_id, + resume, + runtime, + playcount, + dateplayed, + v.PLEX_TYPE_MOVIE) + self.plex_db.add_movie(plex_id=plex_id, + checksum=api.checksum(), + section_id=section_id, + kodi_id=kodi_id, + kodi_fileid=file_id, + kodi_pathid=kodi_pathid, + last_sync=self.last_sync) + + def remove(self, plex_id): + """ + Remove a movie with all references and all orphaned associated entries + from the Kodi DB + """ + movie = self.plex_db.movie(plex_id) + try: + kodi_id = movie[3] + file_id = movie[4] + kodi_type = v.KODI_TYPE_MOVIE + LOG.debug('Removing movie with plex_id %s, kodi_id: %s', + plex_id, kodi_id) + except TypeError: + LOG.error('Movie with plex_id %s not found - cannot delete', + plex_id) + return + # Remove the plex reference + self.plex_db.remove(plex_id, v.PLEX_TYPE_MOVIE) + # Remove artwork + self.artwork.delete_artwork(kodi_id, kodi_type, self.self.kodicursor) + set_id = self.kodi_db.get_set_id(kodi_id) + self.kodi_db.modify_countries(kodi_id, kodi_type) + self.kodi_db.modify_people(kodi_id, kodi_type) + self.kodi_db.modify_genres(kodi_id, kodi_type) + self.kodi_db.modify_studios(kodi_id, kodi_type) + self.kodi_db.modify_tags(kodi_id, kodi_type) + # Delete kodi movie and file + self.kodi_db.remove_file(file_id) + self.self.kodicursor.execute('DELETE FROM movie WHERE idMovie = ?', + (kodi_id,)) + if set_id: + self.kodi_db.delete_possibly_empty_set(set_id) + self.kodi_db.remove_uniqueid(kodi_id, kodi_type) + self.kodi_db.remove_ratings(kodi_id, kodi_type) + LOG.debug('Deleted movie %s from kodi database', plex_id) diff --git a/resources/lib/itemtypes/tvshows.py b/resources/lib/itemtypes/tvshows.py index 8235308f..e5554d76 100644 --- a/resources/lib/itemtypes/tvshows.py +++ b/resources/lib/itemtypes/tvshows.py @@ -203,8 +203,8 @@ class Show(ItemBase, TvShowMixin): toppathid = None kodi_pathid = self.kodi_db.add_video_path(path, - date_added=api.date_created(), - id_parent_path=toppathid) + date_added=api.date_created(), + id_parent_path=toppathid) # UPDATE THE TVSHOW ##### if update_item: LOG.info("UPDATE tvshow plex_id: %s - Title: %s", @@ -248,8 +248,6 @@ class Show(ItemBase, TvShowMixin): # Link the path query = "INSERT INTO tvshowlinkpath(idShow, idPath) values (?, ?)" self.kodicursor.execute(query, (kodi_id, kodi_pathid)) - # Create the reference in plex table - rating_id = self.kodi_db.get_ratingid(kodi_id, v.KODI_TYPE_SHOW) self.kodi_db.add_ratings(rating_id, kodi_id, @@ -293,13 +291,12 @@ class Show(ItemBase, TvShowMixin): tags = [section_name] tags.extend([i for _, i in api.collection_list()]) self.kodi_db.modify_tags(kodi_id, v.KODI_TYPE_SHOW, tags) - self.plex_db.add_reference(plex_type=v.PLEX_TYPE_SHOW, - plex_id=plex_id, - checksum=api.checksum(), - section_id=section_id, - kodi_id=kodi_id, - kodi_pathid=kodi_pathid, - last_sync=self.last_sync) + self.plex_db.add_show(plex_id=plex_id, + checksum=api.checksum(), + section_id=section_id, + kodi_id=kodi_id, + kodi_pathid=kodi_pathid, + last_sync=self.last_sync) class Season(ItemBase, TvShowMixin): @@ -328,14 +325,13 @@ class Season(ItemBase, TvShowMixin): kodi_id, v.KODI_TYPE_SEASON, self.kodicursor) - self.plex_db.add_reference(plex_type=v.PLEX_TYPE_SEASON, - plex_id=plex_id, - checksum=api.checksum(), - section_id=section_id, - show_id=show_id, - parent_id=parent_id, - kodi_id=kodi_id, - last_sync=self.last_sync) + self.plex_db.add_season(plex_id=plex_id, + checksum=api.checksum(), + section_id=section_id, + show_id=show_id, + parent_id=parent_id, + kodi_id=kodi_id, + last_sync=self.last_sync) class Episode(ItemBase, TvShowMixin): @@ -535,15 +531,14 @@ class Episode(ItemBase, TvShowMixin): userdata['PlayCount'], userdata['LastPlayedDate'], None) # Do send None - 2nd entry - self.plex_db.add_reference(plex_type=v.PLEX_TYPE_EPISODE, - plex_id=plex_id, - checksum=api.checksum(), - 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) + self.plex_db.add_episode(plex_id=plex_id, + checksum=api.checksum(), + 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) diff --git a/resources/lib/library_sync/__init__.py b/resources/lib/library_sync/__init__.py index b93054b3..9c5071d9 100644 --- a/resources/lib/library_sync/__init__.py +++ b/resources/lib/library_sync/__init__.py @@ -1 +1,4 @@ -# Dummy file to make this directory a package. +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals + +from .full_sync import start, PLAYLIST_SYNC_ENABLED diff --git a/resources/lib/library_sync/full_sync.py b/resources/lib/library_sync/full_sync.py index e6b4b71c..b2b9ddc9 100644 --- a/resources/lib/library_sync/full_sync.py +++ b/resources/lib/library_sync/full_sync.py @@ -4,21 +4,25 @@ from __future__ import absolute_import, division, unicode_literals from logging import getLogger import time -from . import common, process_metadata, sections from .get_metadata import GetMetadataTask -from .. import utils, backgroundthread, playlists, variables as v, state +from . import common, process_metadata, sections +from .. import utils, backgroundthread, variables as v, state from .. import plex_functions as PF, itemtypes +from ..plex_db import PlexDB + + +if (v.PLATFORM != 'Microsoft UWP' and + utils.settings('enablePlaylistSync') == 'true'): + # Xbox cannot use watchdog, a dependency for PKC playlist features + from .. import playlists + PLAYLIST_SYNC_ENABLED = True +else: + PLAYLIST_SYNC_ENABLED = False + LOG = getLogger('PLEX.library_sync.full_sync') -def start(repair, callback): - """ - """ - # backgroundthread.BGThreader.addTask(FullSync().setup(repair, callback)) - FullSync(repair, callback).start() - - class FullSync(backgroundthread.KillableThread, common.libsync_mixin): def __init__(self, repair, callback): """ @@ -99,6 +103,7 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin): except RuntimeError: LOG.error('Could not entirely process section %s', section) continue + self.queue.join() LOG.debug('Finished processing %ss', self.plex_type) return True @@ -110,50 +115,55 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin): (v.PLEX_TYPE_MOVIE, itemtypes.Movie, False), (v.PLEX_TYPE_SHOW, itemtypes.Show, False), (v.PLEX_TYPE_SEASON, itemtypes.Season, 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), + (v.PLEX_TYPE_EPISODE, itemtypes.Episode, False) ] + if state.ENABLE_MUSIC: + kinds.extend( + (v.PLEX_TYPE_ARTIST, itemtypes.Artist, False), + (v.PLEX_TYPE_ALBUM, itemtypes.Album, True), + (v.PLEX_TYPE_SONG, itemtypes.Song, False)) for kind in kinds: # 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 - if self.new_items_only: - # Delete movies that are not on Plex anymore - do this only once - self.process_delete() + with PlexDB() as self.plex_db: + if self.isCanceled() or not self.process_kind(): + return False + if self.new_items_only: + # Delete movies that are not on Plex anymore - do this only once + self.process_delete() return True @utils.log_time def run(self): + if self.isCanceled(): + return successful = False self.last_sync = time.time() - if self.isCanceled(): - return - LOG.info('Running fullsync for NEW PMS items with repair=%s', - self.repair) if not sections.sync_from_pms(): return - if self.isCanceled(): - return try: # Fire up our single processing thread self.queue = backgroundthread.Queue.Queue(maxsize=200) self.processing_thread = process_metadata.ProcessMetadata( self.queue, self.last_sync) self.processing_thread.start() + + # Actual syncing - do only new items first + LOG.info('Running fullsync for **NEW** items with repair=%s', + self.repair) + self.new_items_only = True # This will also update playstates and userratings! - if self.full_library_sync(new_items_only=True) is False: + if not self.full_library_sync(): return if self.isCanceled(): return # This will NOT update playstates and userratings! - LOG.info('Running fullsync for CHANGED PMS items with repair=%s', + LOG.info('Running fullsync for **CHANGED** items with repair=%s', self.repair) + self.new_items_only = False if not self.full_library_sync(): return if self.isCanceled(): @@ -174,58 +184,8 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin): LOG.info('Done full_sync') -def process_updatelist(item_class, show_sync_info=True): +def start(repair, callback): """ - Downloads all XMLs for item_class (e.g. Movies, TV-Shows). Processes - them by then calling item_classs.() - - Input: - item_class: 'Movies', 'TVShows' (itemtypes.py classes) """ - search_fanart = (item_class in ('Movies', 'TVShows') and - utils.settings('FanartTV') == 'true') - LOG.debug("Starting sync threads") - # Spawn GetMetadata threads for downloading - for _ in range(state.SYNC_THREAD_NUMBER): - thread = get_metadata.ThreadedGetMetadata(DOWNLOAD_QUEUE, - PROCESS_QUEUE) - thread.start() - THREADS.append(thread) - LOG.debug("%s download threads spawned", state.SYNC_THREAD_NUMBER) - # Spawn one more thread to process Metadata, once downloaded - thread = process_metadata.ThreadedProcessMetadata(PROCESS_QUEUE, - item_class) - thread.start() - THREADS.append(thread) - # Start one thread to show sync progress ONLY for new PMS items - if show_sync_info: - sync_info.GET_METADATA_COUNT = 0 - sync_info.PROCESS_METADATA_COUNT = 0 - sync_info.PROCESSING_VIEW_NAME = '' - thread = sync_info.ThreadedShowSyncInfo(item_number, item_class) - thread.start() - THREADS.append(thread) - # Process items we need to download - for _ in generator: - DOWNLOAD_QUEUE.put(self.updatelist.pop(0)) - if search_fanart: - pass - # Wait until finished - DOWNLOAD_QUEUE.join() - PROCESS_QUEUE.join() - # Kill threads - LOG.debug("Waiting to kill threads") - for thread in THREADS: - # Threads might already have quit by themselves (e.g. Kodi exit) - try: - thread.stop() - except AttributeError: - pass - LOG.debug("Stop sent to all threads") - # Wait till threads are indeed dead - for thread in threads: - try: - thread.join(1.0) - except AttributeError: - pass - LOG.debug("Sync threads finished") + # backgroundthread.BGThreader.addTask(FullSync().setup(repair, callback)) + FullSync(repair, callback).start() diff --git a/resources/lib/library_sync/process_metadata.py b/resources/lib/library_sync/process_metadata.py index e8f9011e..0573f660 100644 --- a/resources/lib/library_sync/process_metadata.py +++ b/resources/lib/library_sync/process_metadata.py @@ -80,9 +80,9 @@ class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin): while self.isCanceled() is False: # grabs item from queue. This will block! xml = self.queue.get() - self.queue.task_done() if xml is InitNewSection or xml is None: section = xml + self.queue.task_done() break try: context.add_update(xml[0], @@ -97,6 +97,7 @@ class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin): xml[0].get('title')) self.update_dialog() self.current += 1 + self.queue.task_done() finally: self.dialog.close() LOG.debug('Processing thread terminated') diff --git a/resources/lib/library_sync/sections.py b/resources/lib/library_sync/sections.py index e43cbcfb..17ec9078 100644 --- a/resources/lib/library_sync/sections.py +++ b/resources/lib/library_sync/sections.py @@ -95,7 +95,7 @@ def _process_section(section_xml, kodi_db, plex_db, sorted_sections, # Prevent duplicate for playlists of the same type playlists = PLAYLISTS[plex_type] # Get current media folders from plex database - section = plex_db.section_by_id(section_id) + section = plex_db.section(section_id) try: current_sectionname = section[1] current_sectiontype = section[2] @@ -137,7 +137,10 @@ def _process_section(section_xml, kodi_db, plex_db, sorted_sections, tagid = kodi_db.create_tag(section_name) # Update view with new info - plex_db.update_section(section_name, tagid, section_id) + plex_db.add_section(section_id, + section_name, + plex_type, + tagid) if plex_db.section_id_by_name(current_sectionname) is None: # The tag could be a combined view. Ensure there's @@ -210,7 +213,7 @@ def delete_sections(old_sections): video_library_update = False music_library_update = False with plexdb.PlexDB() as plex_db: - old_sections = [plex_db.section_by_id(x) for x in old_sections] + old_sections = [plex_db.section(x) for x in old_sections] LOG.info("Removing entire Plex library sections: %s", old_sections) with kodidb.GetKodiDB() as kodi_db: for section in old_sections: diff --git a/resources/lib/plex_api.py b/resources/lib/plex_api.py index 15455254..58785b33 100644 --- a/resources/lib/plex_api.py +++ b/resources/lib/plex_api.py @@ -476,33 +476,39 @@ class API(object): """ Returns the title of the element as unicode or 'Missing Title Name' """ - return utils.try_decode(self.item.get('title', 'Missing Title Name')) - - def title(self): - """ - Returns an item's name/title or "Missing Title". - """ - return self.item.get('title', 'Missing Title') + return cast(unicode, self.item.get('title', 'Missing Title Name')) def sorttitle(self): """ Returns an item's sorting name/title or the title itself if not found "Missing Title" if both are not present """ - return self.item.get('titleSort', - self.item.get('title','Missing Title')) + return cast(unicode, self.item.get('titleSort', + self.item.get('title','Missing Title'))) def plot(self): """ Returns the plot or None. """ - return self.item.get('summary') + return cast(unicode, self.item.get('summary')) + + def shortplot(self): + """ + Not yet implemented + """ + pass + + def votecount(self): + """ + Not yet implemented + """ + pass def tagline(self): """ Returns a shorter tagline or None """ - return self.item.get('tagline') + return cast(unicode, self.item.get('tagline')) def audience_rating(self): """ @@ -755,7 +761,7 @@ class API(object): answ.append(extra) return answ - def trailers(self): + def trailer(self): """ Returns the URL for a single trailer (local trailer preferred; first trailer found returned) or an add-on path to list all Plex extras diff --git a/resources/lib/plex_db/__init__.py b/resources/lib/plex_db/__init__.py index 12b9647a..0acefd7d 100644 --- a/resources/lib/plex_db/__init__.py +++ b/resources/lib/plex_db/__init__.py @@ -2,42 +2,10 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals -from .common import PlexDBBase -from .tvshows import -from .. import utils, variables as v +from .common import PlexDBBase, initialize, wipe +from .tvshows import TVShows +from .movies import Movies -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) +class PlexDB(PlexDBBase, TVShows, Movies): + pass diff --git a/resources/lib/plex_db/common.py b/resources/lib/plex_db/common.py index 7f152195..13a478a5 100644 --- a/resources/lib/plex_db/common.py +++ b/resources/lib/plex_db/common.py @@ -2,14 +2,26 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals +from . import utils -class PlexDB(object): + +class PlexDBBase(object): """ Methods used for all types of items """ - def __init__(self, cursor): + def __init__(self, cursor=None): + # Allows us to use this class with a cursor instead of context mgr self.cursor = cursor + def __enter__(self): + self.plexconn = utils.kodi_sql('plex') + self.cursor = self.plexconn.cursor() + return self + + def __exit__(self, e_typ, e_val, trcbak): + self.plexconn.commit() + self.plexconn.close() + def section_ids(self): """ Returns an iterator for section Plex ids for all sections @@ -35,14 +47,14 @@ class PlexDB(object): 'kodi_tagid': x[3], 'sync_to_kodi': x[4]} for x in self.cursor) - def section_by_id(self, section_id): + def section(self, section_id): """ - For section_id, returns tuple (or None) - (section_id, - section_name, - plex_type, - kodi_tagid, - sync_to_kodi) + For section_id, returns the tuple (or None) + section_id INTEGER PRIMARY KEY, + section_name TEXT, + plex_type TEXT, + kodi_tagid INTEGER, + sync_to_kodi INTEGER """ self.cursor.execute('SELECT * FROM sections WHERE section_id = ? LIMIT 1', (section_id, )) @@ -66,7 +78,7 @@ class PlexDB(object): sync=False: Plex library won't be synced to Kodi """ query = ''' - INSERT INTO sections( + INSERT OR REPLACE INTO sections( section_id, section_name, plex_type, kodi_tagid, sync_to_kodi) VALUES (?, ?, ?, ?, ?) ''' @@ -77,17 +89,6 @@ class PlexDB(object): 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 @@ -95,18 +96,147 @@ class PlexDB(object): self.cursor.execute('DELETE FROM sections WHERE section_id = ?', (section_id, )) - def item_by_id(self, plex_id): + def plex_id_by_last_sync(self, plex_type, last_sync): """ - For plex_id, returns the tuple - (kodi_id, kodi_fileid, kodi_pathid, parent_id, kodi_type, plex_type) + Returns an iterator for all items where the last_sync is NOT identical + """ + query = 'SELECT plex_id FROM %s WHERE last_sync <> ?' % plex_type + self.cursor.execute(query, (last_sync, )) + return (x[0] for x in self.cursor) - None if not found + def update_last_sync(self, plex_type, plex_id, last_sync): """ - 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() + Sets a new timestamp for plex_id + """ + query = 'UPDATE %s SET last_sync = ? WHERE plex_id = ?' % plex_type + self.cursor.execute(query, (last_sync, plex_id)) + + def remove(self, plex_id, plex_type): + """ + Removes the item from our Plex db + """ + query = 'DELETE FROM ? WHERE plex_id = ?' % plex_type + self.cursor.execute(query, (plex_id, )) + + +def initialize(): + """ + Run once during startup to verify that plex db exists. + """ + with PlexDBBase() as plex_db: + # Create the tables for the plex database + plex_db.cursor.execute(''' + CREATE TABLE IF NOT EXISTS sections( + section_id INTEGER PRIMARY KEY, + section_name TEXT, + plex_type TEXT, + kodi_tagid INTEGER, + sync_to_kodi INTEGER) + ''') + plex_db.cursor.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.cursor.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.cursor.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.cursor.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.cursor.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.cursor.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.cursor.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.cursor.execute(''' + CREATE TABLE IF NOT EXISTS playlists( + plex_id INTEGER PRIMARY KEY ASC, + plex_name TEXT, + plex_updatedat INTEGER, + kodi_path TEXT, + kodi_type TEXT, + kodi_hash TEXT) + ''') + # Create an index for actors to speed up sync + utils.create_actor_db_index() + + +def wipe(): + """ + Completely resets the Plex database + """ + query = "SELECT name FROM sqlite_master WHERE type = 'table'" + with PlexDBBase() as plex_db: + plex_db.cursor.execute(query) + tables = plex_db.cursor.fetchall() + tables = [i[0] for i in tables] + for table in tables: + delete_query = 'DELETE FROM %s' % table + plex_db.cursor.execute(delete_query) diff --git a/resources/lib/plex_db/movies.py b/resources/lib/plex_db/movies.py new file mode 100644 index 00000000..0a742a58 --- /dev/null +++ b/resources/lib/plex_db/movies.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals + + +class Movies(object): + def add_movie(self, plex_id=None, checksum=None, section_id=None, + kodi_id=None, kodi_fileid=None, kodi_pathid=None, + last_sync=None): + """ + Appends or replaces an entry into the plex table for movies + """ + query = ''' + INSERT OR REPLACE INTO movie( + plex_id, + checksum, + section_id, + kodi_id, + kodi_fileid, + kodi_pathid, + fanart_synced, + last_sync) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''' + self.plexcursor.execute( + query, + (plex_id, + checksum, + section_id, + kodi_id, + kodi_fileid, + kodi_pathid, + 0, + last_sync)) + + def movie(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_fileid INTEGER, + kodi_pathid INTEGER, + fanart_synced INTEGER, + last_sync INTEGER + """ + self.cursor.execute('SELECT * FROM movie WHERE plex_id = ?', + (plex_id, )) + return self.cursor.fetchone() diff --git a/resources/lib/plex_db/tvshows.py b/resources/lib/plex_db/tvshows.py index 4c650c4f..6221fd19 100644 --- a/resources/lib/plex_db/tvshows.py +++ b/resources/lib/plex_db/tvshows.py @@ -2,55 +2,59 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals -from . import common -from .. import variables as v -############################################################################### +class TVShows(object): + def add_show(self, plex_id=None, checksum=None, section_id=None, + kodi_id=None, kodi_pathid=None, last_sync=None): + """ + Appends or replaces tv show entry into the plex table + """ + 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)) - -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): + def add_season(self, plex_id=None, checksum=None, section_id=None, + show_id=None, parent_id=None, kodi_id=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 (?, ?, ?, ?, ?, ?, ?, ?) + 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)) + + def add_episode(self, 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 + """ + 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, 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)) + 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)) def show(self, plex_id): """ @@ -63,7 +67,7 @@ class PlexDB(common.PlexDB): fanart_synced INTEGER, last_sync INTEGER """ - self.cursor.execute('SELECT * FROM show WHERE plex_id = ?', + self.cursor.execute('SELECT * FROM show WHERE plex_id = ? LIMIT 1', (plex_id, )) return self.cursor.fetchone() @@ -79,7 +83,7 @@ class PlexDB(common.PlexDB): fanart_synced INTEGER, last_sync INTEGER """ - self.cursor.execute('SELECT * FROM season WHERE plex_id = ?', + self.cursor.execute('SELECT * FROM season WHERE plex_id = ? LIMIT 1', (plex_id, )) return self.cursor.fetchone() @@ -99,26 +103,6 @@ class PlexDB(common.PlexDB): fanart_synced INTEGER, last_sync INTEGER """ - self.cursor.execute('SELECT * FROM episode WHERE plex_id = ?', + self.cursor.execute('SELECT * FROM episode WHERE plex_id = ? LIMIT 1', (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 - """