Rewire llibrary sync, part 4

This commit is contained in:
croneter 2018-10-23 13:54:09 +02:00
parent 35a25a7f15
commit 23dada9fe5
12 changed files with 613 additions and 263 deletions

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
from .movies import Movie
from .tvshows import Show, Season, Episode from .tvshows import Show, Season, Episode
# Note: always use same order of URL arguments, NOT urlencode: # Note: always use same order of URL arguments, NOT urlencode:

View file

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

View file

@ -203,8 +203,8 @@ class Show(ItemBase, TvShowMixin):
toppathid = None toppathid = None
kodi_pathid = 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 #####
if update_item: if update_item:
LOG.info("UPDATE tvshow plex_id: %s - Title: %s", LOG.info("UPDATE tvshow plex_id: %s - Title: %s",
@ -248,8 +248,6 @@ class Show(ItemBase, TvShowMixin):
# 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, kodi_pathid)) 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) rating_id = self.kodi_db.get_ratingid(kodi_id, v.KODI_TYPE_SHOW)
self.kodi_db.add_ratings(rating_id, self.kodi_db.add_ratings(rating_id,
kodi_id, kodi_id,
@ -293,13 +291,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.add_reference(plex_type=v.PLEX_TYPE_SHOW, self.plex_db.add_show(plex_id=plex_id,
plex_id=plex_id, checksum=api.checksum(),
checksum=api.checksum(), section_id=section_id,
section_id=section_id, kodi_id=kodi_id,
kodi_id=kodi_id, kodi_pathid=kodi_pathid,
kodi_pathid=kodi_pathid, last_sync=self.last_sync)
last_sync=self.last_sync)
class Season(ItemBase, TvShowMixin): class Season(ItemBase, TvShowMixin):
@ -328,14 +325,13 @@ class Season(ItemBase, TvShowMixin):
kodi_id, kodi_id,
v.KODI_TYPE_SEASON, v.KODI_TYPE_SEASON,
self.kodicursor) self.kodicursor)
self.plex_db.add_reference(plex_type=v.PLEX_TYPE_SEASON, self.plex_db.add_season(plex_id=plex_id,
plex_id=plex_id, checksum=api.checksum(),
checksum=api.checksum(), section_id=section_id,
section_id=section_id, show_id=show_id,
show_id=show_id, parent_id=parent_id,
parent_id=parent_id, kodi_id=kodi_id,
kodi_id=kodi_id, last_sync=self.last_sync)
last_sync=self.last_sync)
class Episode(ItemBase, TvShowMixin): class Episode(ItemBase, TvShowMixin):
@ -535,15 +531,14 @@ class Episode(ItemBase, TvShowMixin):
userdata['PlayCount'], userdata['PlayCount'],
userdata['LastPlayedDate'], userdata['LastPlayedDate'],
None) # Do send None - 2nd entry None) # Do send None - 2nd entry
self.plex_db.add_reference(plex_type=v.PLEX_TYPE_EPISODE, self.plex_db.add_episode(plex_id=plex_id,
plex_id=plex_id, checksum=api.checksum(),
checksum=api.checksum(), section_id=section_id,
section_id=section_id, show_id=show_id,
show_id=show_id, grandparent_id=grandparent_id,
grandparent_id=grandparent_id, season_id=season_id,
season_id=season_id, parent_id=parent_id,
parent_id=parent_id, kodi_id=kodi_id,
kodi_id=kodi_id, kodi_fileid=kodi_fileid,
kodi_fileid=kodi_fileid, kodi_pathid=kodi_pathid,
kodi_pathid=kodi_pathid, last_sync=self.last_sync)
last_sync=self.last_sync)

View file

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

View file

@ -4,21 +4,25 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger from logging import getLogger
import time import time
from . import common, process_metadata, sections
from .get_metadata import GetMetadataTask 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 .. 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') 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): class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
def __init__(self, repair, callback): def __init__(self, repair, callback):
""" """
@ -99,6 +103,7 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
except RuntimeError: except RuntimeError:
LOG.error('Could not entirely process section %s', section) LOG.error('Could not entirely process section %s', section)
continue continue
self.queue.join()
LOG.debug('Finished processing %ss', self.plex_type) LOG.debug('Finished processing %ss', self.plex_type)
return True return True
@ -110,50 +115,55 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
(v.PLEX_TYPE_MOVIE, itemtypes.Movie, False), (v.PLEX_TYPE_MOVIE, itemtypes.Movie, False),
(v.PLEX_TYPE_SHOW, itemtypes.Show, False), (v.PLEX_TYPE_SHOW, itemtypes.Show, False),
(v.PLEX_TYPE_SEASON, itemtypes.Season, False), (v.PLEX_TYPE_SEASON, itemtypes.Season, False),
(v.PLEX_TYPE_EPISODE, itemtypes.Episode, 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),
] ]
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: for kind in kinds:
# Setup our variables # Setup our variables
self.plex_type = kind[0] self.plex_type = kind[0]
self.context = kind[1] self.context = kind[1]
self.get_children = kind[2] self.get_children = kind[2]
# Now do the heavy lifting # Now do the heavy lifting
if self.isCanceled() or not self.process_kind(): with PlexDB() as self.plex_db:
return False if self.isCanceled() or not self.process_kind():
if self.new_items_only: return False
# Delete movies that are not on Plex anymore - do this only once if self.new_items_only:
self.process_delete() # Delete movies that are not on Plex anymore - do this only once
self.process_delete()
return True return True
@utils.log_time @utils.log_time
def run(self): def run(self):
if self.isCanceled():
return
successful = False successful = False
self.last_sync = time.time() 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(): if not sections.sync_from_pms():
return return
if self.isCanceled():
return
try: try:
# Fire up our single processing thread # Fire up our single processing thread
self.queue = backgroundthread.Queue.Queue(maxsize=200) self.queue = backgroundthread.Queue.Queue(maxsize=200)
self.processing_thread = process_metadata.ProcessMetadata( self.processing_thread = process_metadata.ProcessMetadata(
self.queue, self.last_sync) self.queue, self.last_sync)
self.processing_thread.start() 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! # 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 return
if self.isCanceled(): if self.isCanceled():
return return
# This will NOT update playstates and userratings! # 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.repair)
self.new_items_only = False
if not self.full_library_sync(): if not self.full_library_sync():
return return
if self.isCanceled(): if self.isCanceled():
@ -174,58 +184,8 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
LOG.info('Done full_sync') 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.<item_class>()
Input:
item_class: 'Movies', 'TVShows' (itemtypes.py classes)
""" """
search_fanart = (item_class in ('Movies', 'TVShows') and # backgroundthread.BGThreader.addTask(FullSync().setup(repair, callback))
utils.settings('FanartTV') == 'true') FullSync(repair, callback).start()
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")

View file

@ -80,9 +80,9 @@ class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin):
while self.isCanceled() is False: while self.isCanceled() is False:
# grabs item from queue. This will block! # grabs item from queue. This will block!
xml = self.queue.get() xml = self.queue.get()
self.queue.task_done()
if xml is InitNewSection or xml is None: if xml is InitNewSection or xml is None:
section = xml section = xml
self.queue.task_done()
break break
try: try:
context.add_update(xml[0], context.add_update(xml[0],
@ -97,6 +97,7 @@ class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin):
xml[0].get('title')) xml[0].get('title'))
self.update_dialog() self.update_dialog()
self.current += 1 self.current += 1
self.queue.task_done()
finally: finally:
self.dialog.close() self.dialog.close()
LOG.debug('Processing thread terminated') LOG.debug('Processing thread terminated')

View file

@ -95,7 +95,7 @@ def _process_section(section_xml, kodi_db, plex_db, sorted_sections,
# 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 = plex_db.section_by_id(section_id) section = plex_db.section(section_id)
try: try:
current_sectionname = section[1] current_sectionname = section[1]
current_sectiontype = section[2] 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) tagid = kodi_db.create_tag(section_name)
# Update view with new info # 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: if plex_db.section_id_by_name(current_sectionname) is None:
# The tag could be a combined view. Ensure there's # The tag could be a combined view. Ensure there's
@ -210,7 +213,7 @@ def delete_sections(old_sections):
video_library_update = False video_library_update = False
music_library_update = False music_library_update = False
with plexdb.PlexDB() 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(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:
for section in old_sections: for section in old_sections:

View file

@ -476,33 +476,39 @@ class API(object):
""" """
Returns the title of the element as unicode or 'Missing Title Name' Returns the title of the element as unicode or 'Missing Title Name'
""" """
return utils.try_decode(self.item.get('title', 'Missing Title Name')) return cast(unicode, 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')
def sorttitle(self): def sorttitle(self):
""" """
Returns an item's sorting name/title or the title itself if not found Returns an item's sorting name/title or the title itself if not found
"Missing Title" if both are not present "Missing Title" if both are not present
""" """
return self.item.get('titleSort', return cast(unicode, self.item.get('titleSort',
self.item.get('title','Missing Title')) self.item.get('title','Missing Title')))
def plot(self): def plot(self):
""" """
Returns the plot or None. 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): def tagline(self):
""" """
Returns a shorter tagline or None Returns a shorter tagline or None
""" """
return self.item.get('tagline') return cast(unicode, self.item.get('tagline'))
def audience_rating(self): def audience_rating(self):
""" """
@ -755,7 +761,7 @@ class API(object):
answ.append(extra) answ.append(extra)
return answ return answ
def trailers(self): def trailer(self):
""" """
Returns the URL for a single trailer (local trailer preferred; first Returns the URL for a single trailer (local trailer preferred; first
trailer found returned) or an add-on path to list all Plex extras trailer found returned) or an add-on path to list all Plex extras

View file

@ -2,42 +2,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
from .common import PlexDBBase from .common import PlexDBBase, initialize, wipe
from .tvshows import from .tvshows import TVShows
from .. import utils, variables as v from .movies import Movies
class PlexDB(object): class PlexDB(PlexDBBase, TVShows, Movies):
""" pass
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

@ -2,14 +2,26 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals from __future__ import absolute_import, division, unicode_literals
from . import utils
class PlexDB(object):
class PlexDBBase(object):
""" """
Methods used for all types of items 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 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): def section_ids(self):
""" """
Returns an iterator for section Plex ids for all sections Returns an iterator for section Plex ids for all sections
@ -35,14 +47,14 @@ class PlexDB(object):
'kodi_tagid': x[3], 'kodi_tagid': x[3],
'sync_to_kodi': x[4]} for x in self.cursor) '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) For section_id, returns the tuple (or None)
(section_id, section_id INTEGER PRIMARY KEY,
section_name, section_name TEXT,
plex_type, plex_type TEXT,
kodi_tagid, kodi_tagid INTEGER,
sync_to_kodi) sync_to_kodi 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, ))
@ -66,7 +78,7 @@ class PlexDB(object):
sync=False: Plex library won't be synced to Kodi sync=False: Plex library won't be synced to Kodi
""" """
query = ''' query = '''
INSERT INTO sections( INSERT OR REPLACE INTO sections(
section_id, section_name, plex_type, kodi_tagid, sync_to_kodi) section_id, section_name, plex_type, kodi_tagid, sync_to_kodi)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
''' '''
@ -77,17 +89,6 @@ class PlexDB(object):
kodi_tagid, kodi_tagid,
sync_to_kodi)) 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): def remove_section(self, section_id):
""" """
Removes the Plex db entry for the section with 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 = ?', self.cursor.execute('DELETE FROM sections WHERE section_id = ?',
(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 Returns an iterator for all items where the last_sync is NOT identical
(kodi_id, kodi_fileid, kodi_pathid, parent_id, kodi_type, plex_type) """
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 = ''' Sets a new timestamp for plex_id
SELECT kodi_id, kodi_fileid, kodi_pathid, parent_id, kodi_type, """
plex_type query = 'UPDATE %s SET last_sync = ? WHERE plex_id = ?' % plex_type
FROM plex WHERE plex_id = ? self.cursor.execute(query, (last_sync, plex_id))
LIMIT 1
''' def remove(self, plex_id, plex_type):
self.cursor.execute(query, (plex_id,)) """
return self.cursor.fetchone() 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)

View file

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

View file

@ -2,55 +2,59 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals 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))
def add_season(self, plex_id=None, checksum=None, section_id=None,
class PlexDB(common.PlexDB): show_id=None, parent_id=None, kodi_id=None, last_sync=None):
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 Appends or replaces an entry into the plex table
""" """
if plex_type == v.PLEX_TYPE_EPISODE: query = '''
query = ''' INSERT OR REPLACE INTO season(
INSERT OR REPLACE INTO episode( plex_id, checksum, section_id, show_id, parent_id,
plex_id, checksum, section_id, show_id, grandparent_id, kodi_id, fanart_synced, last_sync)
season_id, parent_id, kodi_id, kodi_fileid, kodi_pathid, VALUES (?, ?, ?, ?, ?, ?, ?, ?)
fanart_synced, last_sync) '''
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) self.plexcursor.execute(
''' query,
self.plexcursor.execute( (plex_id, checksum, section_id, show_id, parent_id,
query, kodi_id, 0, last_sync))
(plex_id, checksum, section_id, show_id, grandparent_id,
season_id, parent_id, kodi_id, kodi_fileid, kodi_pathid, def add_episode(self, plex_id=None, checksum=None, section_id=None,
0, last_sync)) show_id=None, grandparent_id=None, season_id=None,
elif plex_type == v.PLEX_TYPE_SEASON: parent_id=None, kodi_id=None, kodi_fileid=None,
query = ''' kodi_pathid=None, last_sync=None):
INSERT OR REPLACE INTO season( """
plex_id, checksum, section_id, show_id, parent_id, Appends or replaces an entry into the plex table
kodi_id, fanart_synced, last_sync) """
VALUES (?, ?, ?, ?, ?, ?, ?, ?) 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( self.plexcursor.execute(
query, query,
(plex_id, checksum, section_id, show_id, parent_id, (plex_id, checksum, section_id, show_id, grandparent_id,
kodi_id, 0, last_sync)) season_id, parent_id, kodi_id, kodi_fileid, kodi_pathid,
elif plex_type == v.PLEX_TYPE_SHOW: 0, last_sync))
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): def show(self, plex_id):
""" """
@ -63,7 +67,7 @@ class PlexDB(common.PlexDB):
fanart_synced INTEGER, fanart_synced INTEGER,
last_sync 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, )) (plex_id, ))
return self.cursor.fetchone() return self.cursor.fetchone()
@ -79,7 +83,7 @@ class PlexDB(common.PlexDB):
fanart_synced INTEGER, fanart_synced INTEGER,
last_sync 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, )) (plex_id, ))
return self.cursor.fetchone() return self.cursor.fetchone()
@ -99,26 +103,6 @@ class PlexDB(common.PlexDB):
fanart_synced INTEGER, fanart_synced INTEGER,
last_sync 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, )) (plex_id, ))
return self.cursor.fetchone() 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
"""