Look for missing trailers using TMDB (backport)
This commit is contained in:
parent
07ed0d1105
commit
5ca9e78bff
14 changed files with 387 additions and 188 deletions
|
@ -56,6 +56,7 @@ Some people argue that PKC is 'hacky' because of the way it directly accesses th
|
||||||
- [Skip intros](https://support.plex.tv/articles/skip-content/)
|
- [Skip intros](https://support.plex.tv/articles/skip-content/)
|
||||||
- [Amazon Alexa voice recognition](https://www.plex.tv/apps/streaming-devices/amazon-alexa)
|
- [Amazon Alexa voice recognition](https://www.plex.tv/apps/streaming-devices/amazon-alexa)
|
||||||
- [Cinema Trailers & Extras](https://support.plex.tv/articles/202934883-cinema-trailers-extras/)
|
- [Cinema Trailers & Extras](https://support.plex.tv/articles/202934883-cinema-trailers-extras/)
|
||||||
|
- If Plex did not provide a trailer, automatically get one using the Kodi add-on [The Movie Database](https://kodi.wiki/view/Add-on:The_Movie_Database)
|
||||||
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)
|
- [Plex Watch Later / Plex It!](https://support.plex.tv/hc/en-us/sections/200211783-Plex-It-)
|
||||||
- [Plex Companion](https://support.plex.tv/hc/en-us/sections/200276908-Plex-Companion): fling Plex media (or anything else) from other Plex devices to PlexKodiConnect
|
- [Plex Companion](https://support.plex.tv/hc/en-us/sections/200276908-Plex-Companion): fling Plex media (or anything else) from other Plex devices to PlexKodiConnect
|
||||||
- Automatically sync Plex playlists to Kodi playlists and vice-versa
|
- Automatically sync Plex playlists to Kodi playlists and vice-versa
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<import addon="script.module.defusedxml" version="0.5.0"/>
|
<import addon="script.module.defusedxml" version="0.5.0"/>
|
||||||
<import addon="plugin.video.plexkodiconnect.movies" version="2.1.3" />
|
<import addon="plugin.video.plexkodiconnect.movies" version="2.1.3" />
|
||||||
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.1.3" />
|
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.1.3" />
|
||||||
|
<import addon="metadata.themoviedb.org.python" version="1.3.1" />
|
||||||
</requires>
|
</requires>
|
||||||
<extension point="xbmc.python.pluginsource" library="default.py">
|
<extension point="xbmc.python.pluginsource" library="default.py">
|
||||||
<provides>video audio image</provides>
|
<provides>video audio image</provides>
|
||||||
|
|
|
@ -47,8 +47,8 @@ class App(object):
|
||||||
self.monitor = None
|
self.monitor = None
|
||||||
# xbmc.Player() instance
|
# xbmc.Player() instance
|
||||||
self.player = None
|
self.player = None
|
||||||
# Instance of FanartThread()
|
# Instance of MetadataThread()
|
||||||
self.fanart_thread = None
|
self.metadata_thread = None
|
||||||
# Instance of ImageCachingThread()
|
# Instance of ImageCachingThread()
|
||||||
self.caching_thread = None
|
self.caching_thread = None
|
||||||
# Dialog to skip intro
|
# Dialog to skip intro
|
||||||
|
@ -62,24 +62,24 @@ class App(object):
|
||||||
def is_playing_video(self):
|
def is_playing_video(self):
|
||||||
return self.player.isPlayingVideo() == 1
|
return self.player.isPlayingVideo() == 1
|
||||||
|
|
||||||
def register_fanart_thread(self, thread):
|
def register_metadata_thread(self, thread):
|
||||||
self.fanart_thread = thread
|
self.metadata_thread = thread
|
||||||
self.threads.append(thread)
|
self.threads.append(thread)
|
||||||
|
|
||||||
def deregister_fanart_thread(self, thread):
|
def deregister_metadata_thread(self, thread):
|
||||||
self.fanart_thread.unblock_callers()
|
self.metadata_thread.unblock_callers()
|
||||||
self.fanart_thread = None
|
self.metadata_thread = None
|
||||||
self.threads.remove(thread)
|
self.threads.remove(thread)
|
||||||
|
|
||||||
def suspend_fanart_thread(self, block=True):
|
def suspend_metadata_thread(self, block=True):
|
||||||
try:
|
try:
|
||||||
self.fanart_thread.suspend(block=block)
|
self.metadata_thread.suspend(block=block)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def resume_fanart_thread(self):
|
def resume_metadata_thread(self):
|
||||||
try:
|
try:
|
||||||
self.fanart_thread.resume()
|
self.metadata_thread.resume()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -133,6 +133,7 @@ class Movie(ItemBase):
|
||||||
kodi_id=kodi_id,
|
kodi_id=kodi_id,
|
||||||
kodi_fileid=file_id,
|
kodi_fileid=file_id,
|
||||||
kodi_pathid=kodi_pathid,
|
kodi_pathid=kodi_pathid,
|
||||||
|
trailer_synced=bool(api.trailer()),
|
||||||
last_sync=self.last_sync)
|
last_sync=self.last_sync)
|
||||||
|
|
||||||
def remove(self, plex_id, plex_type=None):
|
def remove(self, plex_id, plex_type=None):
|
||||||
|
|
35
resources/lib/itemtypes/movies_tmdb.py
Normal file
35
resources/lib/itemtypes/movies_tmdb.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import xbmcvfs
|
||||||
|
import xbmcaddon
|
||||||
|
|
||||||
|
# Import the existing Kodi add-on metadata.themoviedb.org.python
|
||||||
|
__ADDON__ = xbmcaddon.Addon(id='metadata.themoviedb.org.python')
|
||||||
|
__TEMP_PATH__ = os.path.join(__ADDON__.getAddonInfo('path'), 'python', 'lib')
|
||||||
|
__BASE__ = xbmcvfs.translatePath(__TEMP_PATH__)
|
||||||
|
sys.path.append(__BASE__)
|
||||||
|
import tmdbscraper.tmdb as tmdb
|
||||||
|
|
||||||
|
logger = logging.getLogger('PLEX.movies_tmdb')
|
||||||
|
|
||||||
|
|
||||||
|
def get_tmdb_scraper(settings):
|
||||||
|
language = settings.getSettingString('language')
|
||||||
|
certcountry = settings.getSettingString('tmdbcertcountry')
|
||||||
|
return tmdb.TMDBMovieScraper(__ADDON__, language, certcountry)
|
||||||
|
|
||||||
|
|
||||||
|
# Instantiate once in order to prevent having to re-read the add-on settings
|
||||||
|
# for every single movie
|
||||||
|
__SCRAPER__ = get_tmdb_scraper(__ADDON__)
|
||||||
|
|
||||||
|
def get_tmdb_details(unique_ids):
|
||||||
|
details = __SCRAPER__.get_details(unique_ids)
|
||||||
|
if 'error' in details:
|
||||||
|
logger.debug('Could not get tmdb details for %s. Error: %s',
|
||||||
|
unique_ids, details)
|
||||||
|
return details
|
|
@ -479,6 +479,31 @@ class KodiVideoDB(common.KodiDBBase):
|
||||||
(kodi_id, kodi_type))
|
(kodi_id, kodi_type))
|
||||||
return dict(self.cursor.fetchall())
|
return dict(self.cursor.fetchall())
|
||||||
|
|
||||||
|
def get_trailer(self, kodi_id, kodi_type):
|
||||||
|
"""
|
||||||
|
Returns the trailer's URL for kodi_type from the Kodi database or None
|
||||||
|
"""
|
||||||
|
if kodi_type == v.KODI_TYPE_MOVIE:
|
||||||
|
self.cursor.execute('SELECT c19 FROM movie WHERE idMovie=?',
|
||||||
|
(kodi_id, ))
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f'trailers for {kodi_type} not implemented')
|
||||||
|
try:
|
||||||
|
return self.cursor.fetchone()[0]
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@db.catch_operationalerrors
|
||||||
|
def set_trailer(self, kodi_id, kodi_type, url):
|
||||||
|
"""
|
||||||
|
Writes the trailer's url to the Kodi DB
|
||||||
|
"""
|
||||||
|
if kodi_type == v.KODI_TYPE_MOVIE:
|
||||||
|
self.cursor.execute('UPDATE movie SET c19=? WHERE idMovie=?',
|
||||||
|
(url, kodi_id))
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f'trailers for {kodi_type} not implemented')
|
||||||
|
|
||||||
@db.catch_operationalerrors
|
@db.catch_operationalerrors
|
||||||
def modify_streams(self, fileid, streamdetails=None, runtime=None):
|
def modify_streams(self, fileid, streamdetails=None, runtime=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -5,5 +5,5 @@ from .full_sync import start
|
||||||
from .websocket import store_websocket_message, process_websocket_messages, \
|
from .websocket import store_websocket_message, process_websocket_messages, \
|
||||||
WEBSOCKET_MESSAGES, PLAYSTATE_SESSIONS
|
WEBSOCKET_MESSAGES, PLAYSTATE_SESSIONS
|
||||||
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
||||||
from .fanart import FanartThread, FanartTask
|
from .additional_metadata import MetadataThread, ProcessMetadataTask
|
||||||
from .sections import force_full_sync, delete_files, clear_window_vars
|
from .sections import force_full_sync, delete_files, clear_window_vars
|
||||||
|
|
117
resources/lib/library_sync/additional_metadata.py
Normal file
117
resources/lib/library_sync/additional_metadata.py
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from . import additional_metadata_tmdb
|
||||||
|
from ..plex_db import PlexDB
|
||||||
|
from .. import backgroundthread, utils
|
||||||
|
from .. import variables as v, app
|
||||||
|
|
||||||
|
|
||||||
|
logger = getLogger('PLEX.sync.metadata')
|
||||||
|
|
||||||
|
BATCH_SIZE = 500
|
||||||
|
|
||||||
|
SUPPORTED_METADATA = {
|
||||||
|
v.PLEX_TYPE_MOVIE: (
|
||||||
|
('missing_trailers', additional_metadata_tmdb.process_trailers),
|
||||||
|
('missing_fanart', additional_metadata_tmdb.process_fanart),
|
||||||
|
),
|
||||||
|
v.PLEX_TYPE_SHOW: (
|
||||||
|
('missing_fanart', additional_metadata_tmdb.process_fanart),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessingNotDone(Exception):
|
||||||
|
"""Exception to detect whether we've completed our sync and did not have to
|
||||||
|
abort or suspend."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def processing_is_activated(item_getter):
|
||||||
|
"""Checks the PKC settings whether processing is even activated."""
|
||||||
|
if item_getter == 'missing_fanart':
|
||||||
|
return utils.settings('FanartTV') == 'true'
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataThread(backgroundthread.KillableThread):
|
||||||
|
"""This will potentially take hours!"""
|
||||||
|
def __init__(self, callback, refresh=False):
|
||||||
|
self.callback = callback
|
||||||
|
self.refresh = refresh
|
||||||
|
super(MetadataThread, self).__init__()
|
||||||
|
|
||||||
|
def should_suspend(self):
|
||||||
|
return self._suspended or app.APP.is_playing_video
|
||||||
|
|
||||||
|
def _process_in_batches(self, item_getter, processor, plex_type):
|
||||||
|
offset = 0
|
||||||
|
while True:
|
||||||
|
with PlexDB() as plexdb:
|
||||||
|
# Keep DB connection open only for a short period of time!
|
||||||
|
if self.refresh:
|
||||||
|
# Simply grab every single item if we want to refresh
|
||||||
|
func = plexdb.every_plex_id
|
||||||
|
else:
|
||||||
|
func = getattr(plexdb, item_getter)
|
||||||
|
batch = list(func(plex_type, offset, BATCH_SIZE))
|
||||||
|
for plex_id in batch:
|
||||||
|
# Do the actual, time-consuming processing
|
||||||
|
if self.should_suspend() or self.should_cancel():
|
||||||
|
raise ProcessingNotDone()
|
||||||
|
processor(plex_id, plex_type, self.refresh)
|
||||||
|
if len(batch) < BATCH_SIZE:
|
||||||
|
break
|
||||||
|
offset += BATCH_SIZE
|
||||||
|
|
||||||
|
def _loop(self):
|
||||||
|
for plex_type in SUPPORTED_METADATA:
|
||||||
|
for item_getter, processor in SUPPORTED_METADATA[plex_type]:
|
||||||
|
if not processing_is_activated(item_getter):
|
||||||
|
continue
|
||||||
|
self._process_in_batches(item_getter, processor, plex_type)
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
finished = False
|
||||||
|
while not finished:
|
||||||
|
try:
|
||||||
|
self._loop()
|
||||||
|
except ProcessingNotDone:
|
||||||
|
finished = False
|
||||||
|
else:
|
||||||
|
finished = True
|
||||||
|
if self.wait_while_suspended():
|
||||||
|
break
|
||||||
|
logger.info('MetadataThread finished completely: %s', finished)
|
||||||
|
self.callback(finished)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
logger.info('Starting MetadataThread')
|
||||||
|
app.APP.register_metadata_thread(self)
|
||||||
|
try:
|
||||||
|
self._run()
|
||||||
|
except Exception:
|
||||||
|
utils.ERROR(notify=True)
|
||||||
|
finally:
|
||||||
|
app.APP.deregister_metadata_thread(self)
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessMetadataTask(backgroundthread.Task):
|
||||||
|
"""This task will also be executed while library sync is suspended!"""
|
||||||
|
def setup(self, plex_id, plex_type, refresh=False):
|
||||||
|
self.plex_id = plex_id
|
||||||
|
self.plex_type = plex_type
|
||||||
|
self.refresh = refresh
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if self.plex_type not in SUPPORTED_METADATA:
|
||||||
|
return
|
||||||
|
for item_getter, processor in SUPPORTED_METADATA[self.plex_type]:
|
||||||
|
if self.should_cancel():
|
||||||
|
# Just don't process this item at all. Next full sync will
|
||||||
|
# take care of it
|
||||||
|
return
|
||||||
|
if not processing_is_activated(item_getter):
|
||||||
|
continue
|
||||||
|
processor(self.plex_id, self.plex_type, self.refresh)
|
153
resources/lib/library_sync/additional_metadata_tmdb.py
Normal file
153
resources/lib/library_sync/additional_metadata_tmdb.py
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import xbmcvfs
|
||||||
|
import xbmcaddon
|
||||||
|
|
||||||
|
from ..plex_api import API
|
||||||
|
from ..kodi_db import KodiVideoDB
|
||||||
|
from ..plex_db import PlexDB
|
||||||
|
from .. import itemtypes, plex_functions as PF, utils, variables as v
|
||||||
|
|
||||||
|
# Import the existing Kodi add-on metadata.themoviedb.org.python
|
||||||
|
__ADDON__ = xbmcaddon.Addon(id='metadata.themoviedb.org.python')
|
||||||
|
__TEMP_PATH__ = os.path.join(__ADDON__.getAddonInfo('path'), 'python', 'lib')
|
||||||
|
__BASE__ = xbmcvfs.translatePath(__TEMP_PATH__)
|
||||||
|
sys.path.append(__BASE__)
|
||||||
|
import tmdbscraper.tmdb as tmdb
|
||||||
|
|
||||||
|
logger = logging.getLogger('PLEX.metadata_movies')
|
||||||
|
PREFER_KODI_COLLECTION_ART = utils.settings('PreferKodiCollectionArt') == 'false'
|
||||||
|
TMDB_SUPPORTED_IDS = ('tmdb', 'imdb')
|
||||||
|
|
||||||
|
|
||||||
|
def get_tmdb_scraper(settings):
|
||||||
|
language = settings.getSettingString('language')
|
||||||
|
certcountry = settings.getSettingString('tmdbcertcountry')
|
||||||
|
return tmdb.TMDBMovieScraper(settings, language, certcountry)
|
||||||
|
|
||||||
|
|
||||||
|
def get_tmdb_details(unique_ids):
|
||||||
|
settings = xbmcaddon.Addon(id='metadata.themoviedb.org.python')
|
||||||
|
details = get_tmdb_scraper(settings).get_details(unique_ids)
|
||||||
|
if 'error' in details:
|
||||||
|
logger.debug('Could not get tmdb details for %s. Error: %s',
|
||||||
|
unique_ids, details)
|
||||||
|
return details
|
||||||
|
|
||||||
|
|
||||||
|
def process_trailers(plex_id, plex_type, refresh=False):
|
||||||
|
done = True
|
||||||
|
try:
|
||||||
|
with PlexDB() as plexdb:
|
||||||
|
db_item = plexdb.item_by_id(plex_id, plex_type)
|
||||||
|
if not db_item:
|
||||||
|
logger.error('Could not get Kodi id for %s %s', plex_type, plex_id)
|
||||||
|
done = False
|
||||||
|
return
|
||||||
|
with KodiVideoDB() as kodidb:
|
||||||
|
trailer = kodidb.get_trailer(db_item['kodi_id'],
|
||||||
|
db_item['kodi_type'])
|
||||||
|
if trailer and (trailer.startswith(f'plugin://{v.ADDON_ID}') or
|
||||||
|
not refresh):
|
||||||
|
# No need to get a trailer
|
||||||
|
return
|
||||||
|
logger.debug('Processing trailer for %s %s', plex_type, plex_id)
|
||||||
|
xml = PF.GetPlexMetadata(plex_id)
|
||||||
|
try:
|
||||||
|
xml[0].attrib
|
||||||
|
except (TypeError, IndexError, AttributeError):
|
||||||
|
logger.warn('Could not get metadata for %s. Skipping that %s '
|
||||||
|
'for now', plex_id, plex_type)
|
||||||
|
done = False
|
||||||
|
return
|
||||||
|
api = API(xml[0])
|
||||||
|
if (not api.guids or
|
||||||
|
not [x for x in api.guids if x in TMDB_SUPPORTED_IDS]):
|
||||||
|
logger.debug('No unique ids found for %s %s, cannot get a trailer',
|
||||||
|
plex_type, api.title())
|
||||||
|
return
|
||||||
|
trailer = get_tmdb_details(api.guids)
|
||||||
|
trailer = trailer.get('info', {}).get('trailer')
|
||||||
|
if trailer:
|
||||||
|
with KodiVideoDB() as kodidb:
|
||||||
|
kodidb.set_trailer(db_item['kodi_id'],
|
||||||
|
db_item['kodi_type'],
|
||||||
|
trailer)
|
||||||
|
logger.debug('Found a new trailer for %s %s: %s',
|
||||||
|
plex_type, api.title(), trailer)
|
||||||
|
else:
|
||||||
|
logger.debug('No trailer found for %s %s', plex_type, api.title())
|
||||||
|
finally:
|
||||||
|
if done is True:
|
||||||
|
with PlexDB() as plexdb:
|
||||||
|
plexdb.set_trailer_synced(plex_id, plex_type)
|
||||||
|
|
||||||
|
|
||||||
|
def process_fanart(plex_id, plex_type, refresh=False):
|
||||||
|
"""
|
||||||
|
Will look for additional fanart for the plex_type item with plex_id.
|
||||||
|
Will check if we already got all artwork and only look if some are indeed
|
||||||
|
missing.
|
||||||
|
Will set the fanart_synced flag in the Plex DB if successful.
|
||||||
|
"""
|
||||||
|
done = True
|
||||||
|
try:
|
||||||
|
artworks = None
|
||||||
|
with PlexDB() as plexdb:
|
||||||
|
db_item = plexdb.item_by_id(plex_id, plex_type)
|
||||||
|
if not db_item:
|
||||||
|
logger.error('Could not get Kodi id for %s %s', plex_type, plex_id)
|
||||||
|
done = False
|
||||||
|
return
|
||||||
|
if not refresh:
|
||||||
|
with KodiVideoDB() as kodidb:
|
||||||
|
artworks = kodidb.get_art(db_item['kodi_id'],
|
||||||
|
db_item['kodi_type'])
|
||||||
|
# Check if we even need to get additional art
|
||||||
|
for key in v.ALL_KODI_ARTWORK:
|
||||||
|
if key not in artworks:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
xml = PF.GetPlexMetadata(plex_id)
|
||||||
|
try:
|
||||||
|
xml[0].attrib
|
||||||
|
except (TypeError, IndexError, AttributeError):
|
||||||
|
logger.debug('Could not get metadata for %s %s. Skipping that '
|
||||||
|
'item for now', plex_type, plex_id)
|
||||||
|
done = False
|
||||||
|
return
|
||||||
|
api = API(xml[0])
|
||||||
|
if artworks is None:
|
||||||
|
artworks = api.artwork()
|
||||||
|
# Get additional missing artwork from fanart artwork sites
|
||||||
|
artworks = api.fanart_artwork(artworks)
|
||||||
|
with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](None) as context:
|
||||||
|
context.set_fanart(artworks,
|
||||||
|
db_item['kodi_id'],
|
||||||
|
db_item['kodi_type'])
|
||||||
|
# Additional fanart for sets/collections
|
||||||
|
if plex_type == v.PLEX_TYPE_MOVIE:
|
||||||
|
for _, setname in api.collections():
|
||||||
|
logger.debug('Getting artwork for movie set %s', setname)
|
||||||
|
with KodiVideoDB() as kodidb:
|
||||||
|
setid = kodidb.create_collection(setname)
|
||||||
|
external_set_artwork = api.set_artwork()
|
||||||
|
if external_set_artwork and PREFER_KODI_COLLECTION_ART:
|
||||||
|
kodi_artwork = api.artwork(kodi_id=setid,
|
||||||
|
kodi_type=v.KODI_TYPE_SET)
|
||||||
|
for art in kodi_artwork:
|
||||||
|
if art in external_set_artwork:
|
||||||
|
del external_set_artwork[art]
|
||||||
|
with itemtypes.Movie(None) as movie:
|
||||||
|
movie.kodidb.modify_artwork(external_set_artwork,
|
||||||
|
setid,
|
||||||
|
v.KODI_TYPE_SET)
|
||||||
|
finally:
|
||||||
|
if done is True:
|
||||||
|
with PlexDB() as plexdb:
|
||||||
|
plexdb.set_fanart_synced(plex_id, plex_type)
|
|
@ -1,154 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
|
||||||
from logging import getLogger
|
|
||||||
|
|
||||||
from ..plex_api import API
|
|
||||||
from ..plex_db import PlexDB
|
|
||||||
from ..kodi_db import KodiVideoDB
|
|
||||||
from .. import backgroundthread, utils
|
|
||||||
from .. import itemtypes, plex_functions as PF, variables as v, app
|
|
||||||
|
|
||||||
|
|
||||||
LOG = getLogger('PLEX.sync.fanart')
|
|
||||||
|
|
||||||
SUPPORTED_TYPES = (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)
|
|
||||||
SYNC_FANART = (utils.settings('FanartTV') == 'true' and
|
|
||||||
utils.settings('usePlexArtwork') == 'true')
|
|
||||||
PREFER_KODI_COLLECTION_ART = utils.settings('PreferKodiCollectionArt') == 'false'
|
|
||||||
BATCH_SIZE = 500
|
|
||||||
|
|
||||||
|
|
||||||
class FanartThread(backgroundthread.KillableThread):
|
|
||||||
"""
|
|
||||||
This will potentially take hours!
|
|
||||||
"""
|
|
||||||
def __init__(self, callback, refresh=False):
|
|
||||||
self.callback = callback
|
|
||||||
self.refresh = refresh
|
|
||||||
super(FanartThread, self).__init__()
|
|
||||||
|
|
||||||
def should_suspend(self):
|
|
||||||
return self._suspended or app.APP.is_playing_video
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
LOG.info('Starting FanartThread')
|
|
||||||
app.APP.register_fanart_thread(self)
|
|
||||||
try:
|
|
||||||
self._run()
|
|
||||||
except Exception:
|
|
||||||
utils.ERROR(notify=True)
|
|
||||||
finally:
|
|
||||||
app.APP.deregister_fanart_thread(self)
|
|
||||||
|
|
||||||
def _loop(self):
|
|
||||||
for typus in SUPPORTED_TYPES:
|
|
||||||
offset = 0
|
|
||||||
while True:
|
|
||||||
with PlexDB() as plexdb:
|
|
||||||
# Keep DB connection open only for a short period of time!
|
|
||||||
if self.refresh:
|
|
||||||
batch = list(plexdb.every_plex_id(typus,
|
|
||||||
offset,
|
|
||||||
BATCH_SIZE))
|
|
||||||
else:
|
|
||||||
batch = list(plexdb.missing_fanart(typus,
|
|
||||||
offset,
|
|
||||||
BATCH_SIZE))
|
|
||||||
for plex_id in batch:
|
|
||||||
# Do the actual, time-consuming processing
|
|
||||||
if self.should_suspend() or self.should_cancel():
|
|
||||||
return False
|
|
||||||
process_fanart(plex_id, typus, self.refresh)
|
|
||||||
if len(batch) < BATCH_SIZE:
|
|
||||||
break
|
|
||||||
offset += BATCH_SIZE
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _run(self):
|
|
||||||
finished = False
|
|
||||||
while not finished:
|
|
||||||
finished = self._loop()
|
|
||||||
if self.wait_while_suspended():
|
|
||||||
break
|
|
||||||
LOG.info('FanartThread finished: %s', finished)
|
|
||||||
self.callback(finished)
|
|
||||||
|
|
||||||
|
|
||||||
class FanartTask(backgroundthread.Task):
|
|
||||||
"""
|
|
||||||
This task will also be executed while library sync is suspended!
|
|
||||||
"""
|
|
||||||
def setup(self, plex_id, plex_type, refresh=False):
|
|
||||||
self.plex_id = plex_id
|
|
||||||
self.plex_type = plex_type
|
|
||||||
self.refresh = refresh
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
process_fanart(self.plex_id, self.plex_type, self.refresh)
|
|
||||||
|
|
||||||
|
|
||||||
def process_fanart(plex_id, plex_type, refresh=False):
|
|
||||||
"""
|
|
||||||
Will look for additional fanart for the plex_type item with plex_id.
|
|
||||||
Will check if we already got all artwork and only look if some are indeed
|
|
||||||
missing.
|
|
||||||
Will set the fanart_synced flag in the Plex DB if successful.
|
|
||||||
"""
|
|
||||||
done = False
|
|
||||||
try:
|
|
||||||
artworks = None
|
|
||||||
with PlexDB() as plexdb:
|
|
||||||
db_item = plexdb.item_by_id(plex_id,
|
|
||||||
plex_type)
|
|
||||||
if not db_item:
|
|
||||||
LOG.error('Could not get Kodi id for plex id %s', plex_id)
|
|
||||||
return
|
|
||||||
if not refresh:
|
|
||||||
with KodiVideoDB() as kodidb:
|
|
||||||
artworks = kodidb.get_art(db_item['kodi_id'],
|
|
||||||
db_item['kodi_type'])
|
|
||||||
# Check if we even need to get additional art
|
|
||||||
for key in v.ALL_KODI_ARTWORK:
|
|
||||||
if key not in artworks:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
done = True
|
|
||||||
return
|
|
||||||
xml = PF.GetPlexMetadata(plex_id)
|
|
||||||
try:
|
|
||||||
xml[0].attrib
|
|
||||||
except (TypeError, IndexError, AttributeError):
|
|
||||||
LOG.warn('Could not get metadata for %s. Skipping that item '
|
|
||||||
'for now', plex_id)
|
|
||||||
return
|
|
||||||
api = API(xml[0])
|
|
||||||
if artworks is None:
|
|
||||||
artworks = api.artwork()
|
|
||||||
# Get additional missing artwork from fanart artwork sites
|
|
||||||
artworks = api.fanart_artwork(artworks)
|
|
||||||
with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](None) as context:
|
|
||||||
context.set_fanart(artworks,
|
|
||||||
db_item['kodi_id'],
|
|
||||||
db_item['kodi_type'])
|
|
||||||
# Additional fanart for sets/collections
|
|
||||||
if plex_type == v.PLEX_TYPE_MOVIE:
|
|
||||||
for _, setname in api.collections():
|
|
||||||
LOG.debug('Getting artwork for movie set %s', setname)
|
|
||||||
with KodiVideoDB() as kodidb:
|
|
||||||
setid = kodidb.create_collection(setname)
|
|
||||||
external_set_artwork = api.set_artwork()
|
|
||||||
if external_set_artwork and PREFER_KODI_COLLECTION_ART:
|
|
||||||
kodi_artwork = api.artwork(kodi_id=setid,
|
|
||||||
kodi_type=v.KODI_TYPE_SET)
|
|
||||||
for art in kodi_artwork:
|
|
||||||
if art in external_set_artwork:
|
|
||||||
del external_set_artwork[art]
|
|
||||||
with itemtypes.Movie(None) as movie:
|
|
||||||
movie.kodidb.modify_artwork(external_set_artwork,
|
|
||||||
setid,
|
|
||||||
v.KODI_TYPE_SET)
|
|
||||||
done = True
|
|
||||||
finally:
|
|
||||||
if done is True:
|
|
||||||
with PlexDB() as plexdb:
|
|
||||||
plexdb.set_fanart_synced(plex_id, plex_type)
|
|
|
@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
|
||||||
from .fanart import SYNC_FANART, FanartTask
|
from .additional_metadata import ProcessMetadataTask
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from ..plex_db import PlexDB
|
from ..plex_db import PlexDB
|
||||||
from .. import kodi_db
|
from .. import kodi_db
|
||||||
|
@ -85,9 +85,8 @@ def process_websocket_messages():
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
successful, video, music = process_new_item_message(message)
|
successful, video, music = process_new_item_message(message)
|
||||||
if (successful and SYNC_FANART and
|
if successful:
|
||||||
message['plex_type'] in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)):
|
task = ProcessMetadataTask()
|
||||||
task = FanartTask()
|
|
||||||
task.setup(message['plex_id'],
|
task.setup(message['plex_id'],
|
||||||
message['plex_type'],
|
message['plex_type'],
|
||||||
refresh=False)
|
refresh=False)
|
||||||
|
|
|
@ -160,6 +160,19 @@ class PlexDBBase(object):
|
||||||
''' % (plex_type, limit, offset)
|
''' % (plex_type, limit, offset)
|
||||||
return (x[0] for x in self.cursor.execute(query))
|
return (x[0] for x in self.cursor.execute(query))
|
||||||
|
|
||||||
|
def missing_trailers(self, plex_type, offset, limit):
|
||||||
|
"""
|
||||||
|
Returns an iterator for plex_type for all plex_id, where trailer_synced
|
||||||
|
has not yet been set to 1
|
||||||
|
Will start with records at DB position offset [int] and return limit
|
||||||
|
[int] number of items
|
||||||
|
"""
|
||||||
|
query = '''
|
||||||
|
SELECT plex_id FROM %s WHERE trailer_synced = 0
|
||||||
|
LIMIT %s OFFSET %s
|
||||||
|
''' % (plex_type, limit, offset)
|
||||||
|
return (x[0] for x in self.cursor.execute(query))
|
||||||
|
|
||||||
def set_fanart_synced(self, plex_id, plex_type):
|
def set_fanart_synced(self, plex_id, plex_type):
|
||||||
"""
|
"""
|
||||||
Toggles fanart_synced to 1 for plex_id
|
Toggles fanart_synced to 1 for plex_id
|
||||||
|
@ -167,6 +180,13 @@ class PlexDBBase(object):
|
||||||
self.cursor.execute('UPDATE %s SET fanart_synced = 1 WHERE plex_id = ?' % plex_type,
|
self.cursor.execute('UPDATE %s SET fanart_synced = 1 WHERE plex_id = ?' % plex_type,
|
||||||
(plex_id, ))
|
(plex_id, ))
|
||||||
|
|
||||||
|
def set_trailer_synced(self, plex_id, plex_type):
|
||||||
|
"""
|
||||||
|
Toggles fanart_synced to 1 for plex_id
|
||||||
|
"""
|
||||||
|
self.cursor.execute('UPDATE %s SET trailer_synced = 1 WHERE plex_id = ?' % plex_type,
|
||||||
|
(plex_id, ))
|
||||||
|
|
||||||
def plexid_by_sectionid(self, section_id, plex_type, limit):
|
def plexid_by_sectionid(self, section_id, plex_type, limit):
|
||||||
query = '''
|
query = '''
|
||||||
SELECT plex_id FROM %s WHERE section_id = ? LIMIT %s
|
SELECT plex_id FROM %s WHERE section_id = ? LIMIT %s
|
||||||
|
@ -210,6 +230,7 @@ def initialize():
|
||||||
kodi_fileid INTEGER,
|
kodi_fileid INTEGER,
|
||||||
kodi_pathid INTEGER,
|
kodi_pathid INTEGER,
|
||||||
fanart_synced INTEGER,
|
fanart_synced INTEGER,
|
||||||
|
trailer_synced BOOLEAN,
|
||||||
last_sync INTEGER)
|
last_sync INTEGER)
|
||||||
''')
|
''')
|
||||||
plexdb.cursor.execute('''
|
plexdb.cursor.execute('''
|
||||||
|
|
|
@ -6,7 +6,7 @@ from .. import variables as v
|
||||||
|
|
||||||
class Movies(object):
|
class Movies(object):
|
||||||
def add_movie(self, plex_id, checksum, section_id, kodi_id, kodi_fileid,
|
def add_movie(self, plex_id, checksum, section_id, kodi_id, kodi_fileid,
|
||||||
kodi_pathid, last_sync):
|
kodi_pathid, trailer_synced, last_sync):
|
||||||
"""
|
"""
|
||||||
Appends or replaces an entry into the plex table for movies
|
Appends or replaces an entry into the plex table for movies
|
||||||
"""
|
"""
|
||||||
|
@ -19,8 +19,9 @@ class Movies(object):
|
||||||
kodi_fileid,
|
kodi_fileid,
|
||||||
kodi_pathid,
|
kodi_pathid,
|
||||||
fanart_synced,
|
fanart_synced,
|
||||||
|
trailer_synced,
|
||||||
last_sync)
|
last_sync)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
'''
|
'''
|
||||||
self.cursor.execute(
|
self.cursor.execute(
|
||||||
query,
|
query,
|
||||||
|
@ -31,6 +32,7 @@ class Movies(object):
|
||||||
kodi_fileid,
|
kodi_fileid,
|
||||||
kodi_pathid,
|
kodi_pathid,
|
||||||
0,
|
0,
|
||||||
|
trailer_synced,
|
||||||
last_sync))
|
last_sync))
|
||||||
|
|
||||||
def movie(self, plex_id):
|
def movie(self, plex_id):
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.sync_successful = False
|
self.sync_successful = False
|
||||||
self.last_full_sync = 0
|
self.last_full_sync = 0
|
||||||
self.fanart_thread = None
|
self.metadata_thread = None
|
||||||
self.image_cache_thread = None
|
self.image_cache_thread = None
|
||||||
# Lock used to wait on a full sync, e.g. on initial sync
|
# Lock used to wait on a full sync, e.g. on initial sync
|
||||||
# self.lock = backgroundthread.threading.Lock()
|
# self.lock = backgroundthread.threading.Lock()
|
||||||
|
@ -52,7 +52,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
utils.lang(39223),
|
utils.lang(39223),
|
||||||
utils.lang(39224), # refresh all
|
utils.lang(39224), # refresh all
|
||||||
utils.lang(39225)) == 0
|
utils.lang(39225)) == 0
|
||||||
if not self.start_fanart_download(refresh=refresh):
|
if not self.start_additional_metadata(refresh=refresh):
|
||||||
# Fanart download already running
|
# Fanart download already running
|
||||||
utils.dialog('notification',
|
utils.dialog('notification',
|
||||||
heading='{plex}',
|
heading='{plex}',
|
||||||
|
@ -76,33 +76,31 @@ class Sync(backgroundthread.KillableThread):
|
||||||
self.last_full_sync = timing.unix_timestamp()
|
self.last_full_sync = timing.unix_timestamp()
|
||||||
if not successful:
|
if not successful:
|
||||||
LOG.warn('Could not finish scheduled full sync')
|
LOG.warn('Could not finish scheduled full sync')
|
||||||
app.APP.resume_fanart_thread()
|
app.APP.resume_metadata_thread()
|
||||||
app.APP.resume_caching_thread()
|
app.APP.resume_caching_thread()
|
||||||
|
|
||||||
def start_library_sync(self, show_dialog=None, repair=False, block=False):
|
def start_library_sync(self, show_dialog=None, repair=False, block=False):
|
||||||
app.APP.suspend_fanart_thread(block=True)
|
app.APP.suspend_metadata_thread(block=True)
|
||||||
app.APP.suspend_caching_thread(block=True)
|
app.APP.suspend_caching_thread(block=True)
|
||||||
show_dialog = show_dialog if show_dialog is not None else app.SYNC.sync_dialog
|
show_dialog = show_dialog if show_dialog is not None else app.SYNC.sync_dialog
|
||||||
library_sync.start(show_dialog, repair, self.on_library_scan_finished)
|
library_sync.start(show_dialog, repair, self.on_library_scan_finished)
|
||||||
|
|
||||||
def start_fanart_download(self, refresh):
|
def start_additional_metadata(self, refresh):
|
||||||
if not utils.settings('FanartTV') == 'true':
|
|
||||||
LOG.info('Additional fanart download is deactivated')
|
|
||||||
return False
|
|
||||||
if not app.SYNC.artwork:
|
if not app.SYNC.artwork:
|
||||||
LOG.info('Not synching Plex PMS artwork, not getting artwork')
|
LOG.info('Not synching Plex PMS artwork, not getting artwork')
|
||||||
return False
|
return False
|
||||||
elif self.fanart_thread is None or not self.fanart_thread.is_alive():
|
elif self.metadata_thread is None or not self.metadata_thread.is_alive():
|
||||||
LOG.info('Start downloading additional fanart with refresh %s',
|
LOG.info('Start downloading additional metadata with refresh %s',
|
||||||
refresh)
|
refresh)
|
||||||
self.fanart_thread = library_sync.FanartThread(self.on_fanart_download_finished, refresh)
|
self.metadata_thread = library_sync.MetadataThread(self.on_metadata_finished, refresh)
|
||||||
self.fanart_thread.start()
|
self.metadata_thread.start()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
LOG.info('Still downloading fanart')
|
LOG.info('Still downloading metadata')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_fanart_download_finished(self, successful):
|
@staticmethod
|
||||||
|
def on_metadata_finished(successful):
|
||||||
# FanartTV lookup completed
|
# FanartTV lookup completed
|
||||||
if successful:
|
if successful:
|
||||||
# Toggled to "Yes"
|
# Toggled to "Yes"
|
||||||
|
@ -189,7 +187,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
xbmc.executebuiltin('ReloadSkin()')
|
xbmc.executebuiltin('ReloadSkin()')
|
||||||
if library_sync.PLAYLIST_SYNC_ENABLED:
|
if library_sync.PLAYLIST_SYNC_ENABLED:
|
||||||
playlist_monitor = playlists.kodi_playlist_monitor()
|
playlist_monitor = playlists.kodi_playlist_monitor()
|
||||||
self.start_fanart_download(refresh=False)
|
self.start_additional_metadata(refresh=False)
|
||||||
self.start_image_cache_thread()
|
self.start_image_cache_thread()
|
||||||
else:
|
else:
|
||||||
LOG.error('Initial start-up full sync unsuccessful')
|
LOG.error('Initial start-up full sync unsuccessful')
|
||||||
|
@ -206,7 +204,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
LOG.info('Done initial sync on Kodi startup')
|
LOG.info('Done initial sync on Kodi startup')
|
||||||
if library_sync.PLAYLIST_SYNC_ENABLED:
|
if library_sync.PLAYLIST_SYNC_ENABLED:
|
||||||
playlist_monitor = playlists.kodi_playlist_monitor()
|
playlist_monitor = playlists.kodi_playlist_monitor()
|
||||||
self.start_fanart_download(refresh=False)
|
self.start_additional_metadata(refresh=False)
|
||||||
self.start_image_cache_thread()
|
self.start_image_cache_thread()
|
||||||
else:
|
else:
|
||||||
LOG.info('Startup sync has not yet been successful')
|
LOG.info('Startup sync has not yet been successful')
|
||||||
|
|
Loading…
Reference in a new issue