PlexKodiConnect/resources/lib/artwork.py

347 lines
13 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
2016-02-20 06:03:06 +11:00
###############################################################################
2017-12-09 05:43:06 +11:00
from logging import getLogger
2017-12-10 00:35:08 +11:00
from Queue import Queue, Empty
from shutil import rmtree
from urllib import quote_plus, unquote
2016-10-19 05:32:16 +11:00
from threading import Thread
from os import makedirs
2017-12-10 00:35:08 +11:00
import requests
2017-12-09 05:43:06 +11:00
from xbmc import sleep, translatePath
from xbmcvfs import exists
2018-04-30 22:16:45 +10:00
from utils import settings, language as lang, kodi_sql, try_encode, try_decode,\
thread_methods, dialog, exists_dir
import state
2016-08-30 03:39:59 +10:00
###############################################################################
2017-12-09 05:43:06 +11:00
LOG = getLogger("PLEX." + __name__)
2018-03-02 17:36:45 +11:00
# Disable annoying requests warnings
requests.packages.urllib3.disable_warnings()
ARTWORK_QUEUE = Queue()
IMAGE_CACHING_SUSPENDS = ['SUSPEND_LIBRARY_THREAD', 'DB_SCAN', 'STOP_SYNC']
if not settings('imageSyncDuringPlayback') == 'true':
IMAGE_CACHING_SUSPENDS.append('SUSPEND_SYNC')
2018-03-04 23:39:18 +11:00
###############################################################################
def double_urlencode(text):
return quote_plus(quote_plus(text))
def double_urldecode(text):
return unquote(unquote(text))
@thread_methods(add_suspends=IMAGE_CACHING_SUSPENDS)
class Image_Cache_Thread(Thread):
sleep_between = 50
2016-12-21 02:13:19 +11:00
# Potentially issues with limited number of threads
# Hence let Kodi wait till download is successful
timeout = (35.1, 35.1)
def __init__(self):
self.queue = ARTWORK_QUEUE
Thread.__init__(self)
def run(self):
LOG.info("---===### Starting Image_Cache_Thread ###===---")
2018-02-12 00:57:39 +11:00
stopped = self.stopped
suspended = self.suspended
queue = self.queue
sleep_between = self.sleep_between
counter = 0
set_zero = False
2018-02-12 00:57:39 +11:00
while not stopped():
# In the event the server goes offline
2018-02-12 00:57:39 +11:00
while suspended():
# Set in service.py
2018-02-12 00:57:39 +11:00
if stopped():
# Abort was requested while waiting. We should exit
2017-12-09 05:43:06 +11:00
LOG.info("---===### Stopped Image_Cache_Thread ###===---")
return
sleep(1000)
try:
url = queue.get(block=False)
except Empty:
if not set_zero:
# Avoid saving '0' all the time
set_zero = True
settings('caching_artwork_count', value='0')
sleep(1000)
continue
set_zero = False
if isinstance(url, ArtworkSyncMessage):
if state.IMAGE_SYNC_NOTIFICATIONS:
dialog('notification',
heading=lang(29999),
message=url.message,
icon='{plex}',
sound=False)
queue.task_done()
continue
url = double_urlencode(try_encode(url))
2017-01-21 03:32:28 +11:00
sleeptime = 0
while True:
try:
requests.head(
url="http://%s:%s/image/image://%s"
% (state.WEBSERVER_HOST,
state.WEBSERVER_PORT,
url),
auth=(state.WEBSERVER_USERNAME,
state.WEBSERVER_PASSWORD),
timeout=self.timeout)
except requests.Timeout:
# We don't need the result, only trigger Kodi to start the
# download. All is well
break
except requests.ConnectionError:
2018-02-12 00:57:39 +11:00
if stopped():
2017-01-25 06:12:46 +11:00
# Kodi terminated
break
# Server thinks its a DOS attack, ('error 10053')
# Wait before trying again
2017-01-21 03:32:28 +11:00
if sleeptime > 5:
2018-03-04 23:39:18 +11:00
LOG.error('Repeatedly got ConnectionError for url %s',
double_urldecode(url))
break
2017-12-09 05:43:06 +11:00
LOG.debug('Were trying too hard to download art, server '
'over-loaded. Sleep %s seconds before trying '
2018-03-04 23:39:18 +11:00
'again to download %s',
2**sleeptime, double_urldecode(url))
2018-04-30 22:16:45 +10:00
sleep((2**sleeptime) * 1000)
2017-01-21 03:32:28 +11:00
sleeptime += 1
continue
except Exception as err:
2018-03-04 23:39:18 +11:00
LOG.error('Unknown exception for url %s: %s'.
double_urldecode(url), err)
import traceback
2018-03-04 23:39:18 +11:00
LOG.error("Traceback:\n%s", traceback.format_exc())
break
# We did not even get a timeout
break
queue.task_done()
# Update the caching state in the PKC settings.
counter += 1
if counter > 20:
counter = 0
settings('caching_artwork_count', value=str(queue.qsize()))
# Sleep for a bit to reduce CPU strain
sleep(sleep_between)
2017-12-09 05:43:06 +11:00
LOG.info("---===### Stopped Image_Cache_Thread ###===---")
class Artwork():
enableTextureCache = settings('enableTextureCache') == "true"
if enableTextureCache:
queue = ARTWORK_QUEUE
def cache_major_artwork(self):
"""
Takes the existing Kodi library and caches posters and fanart.
Necessary because otherwise PKC caches artwork e.g. from fanart.tv
which basically blocks Kodi from getting needed artwork fast (e.g.
while browsing the library)
"""
if not self.enableTextureCache:
return
artworks = list()
# Get all posters and fanart/background for video and music
for kind in ('video', 'music'):
connection = kodi_sql(kind)
cursor = connection.cursor()
for typus in ('poster', 'fanart'):
cursor.execute('SELECT url FROM art WHERE type == ?',
(typus, ))
artworks.extend(cursor.fetchall())
connection.close()
artworks_to_cache = list()
connection = kodi_sql('texture')
cursor = connection.cursor()
for url in artworks:
query = 'SELECT url FROM texture WHERE url == ? LIMIT 1'
cursor.execute(query, (url[0], ))
if not cursor.fetchone():
artworks_to_cache.append(url)
connection.close()
if not artworks_to_cache:
LOG.info('Caching of major images to Kodi texture cache done')
# Set to "None"
settings('caching_artwork_count', value=lang(30069))
return
length = len(artworks_to_cache)
LOG.info('Caching has not been completed - caching %s major images',
length)
settings('caching_artwork_count', value=str(length))
# Caching %s Plex images
self.queue.put(ArtworkSyncMessage(lang(30006) % length))
for i, url in enumerate(artworks_to_cache):
self.queue.put(url[0])
# Plex image caching done
self.queue.put(ArtworkSyncMessage(lang(30007)))
2016-09-11 21:51:53 +10:00
def fullTextureCacheSync(self):
"""
This method will sync all Kodi artwork to textures13.db
and cache them locally. This takes diskspace!
"""
2017-01-25 05:59:38 +11:00
if not dialog('yesno', "Image Texture Cache", lang(39250)):
return
2017-12-09 05:43:06 +11:00
LOG.info("Doing Image Cache Sync")
# ask to rest all existing or not
2017-01-25 05:59:38 +11:00
if dialog('yesno', "Image Texture Cache", lang(39251)):
2017-12-09 05:43:06 +11:00
LOG.info("Resetting all cache data first")
# Remove all existing textures first
2018-02-11 22:59:04 +11:00
path = try_decode(translatePath("special://thumbnails/"))
if exists_dir(path):
rmtree(path, ignore_errors=True)
2018-03-04 23:39:18 +11:00
self.restore_cache_directories()
# remove all existing data from texture DB
2018-02-11 22:59:04 +11:00
connection = kodi_sql('texture')
cursor = connection.cursor()
query = 'SELECT tbl_name FROM sqlite_master WHERE type=?'
cursor.execute(query, ('table', ))
rows = cursor.fetchall()
for row in rows:
tableName = row[0]
if tableName != "version":
2017-05-12 21:25:46 +10:00
cursor.execute("DELETE FROM %s" % tableName)
connection.commit()
2016-09-11 21:51:53 +10:00
connection.close()
# Cache all entries in video DB
2018-02-11 22:59:04 +11:00
connection = kodi_sql('video')
cursor = connection.cursor()
2016-09-11 21:51:53 +10:00
# dont include actors
query = "SELECT url FROM art WHERE media_type != ?"
cursor.execute(query, ('actor', ))
result = cursor.fetchall()
total = len(result)
2018-03-04 23:39:18 +11:00
LOG.info("Image cache sync about to process %s video images", total)
2016-09-11 21:51:53 +10:00
connection.close()
for url in result:
2018-03-04 23:39:18 +11:00
self.cache_texture(url[0])
# Cache all entries in music DB
2018-02-11 22:59:04 +11:00
connection = kodi_sql('music')
cursor = connection.cursor()
cursor.execute("SELECT url FROM art")
result = cursor.fetchall()
total = len(result)
2018-03-04 23:39:18 +11:00
LOG.info("Image cache sync about to process %s music images", total)
2016-09-11 21:51:53 +10:00
connection.close()
for url in result:
2018-03-04 23:39:18 +11:00
self.cache_texture(url[0])
2018-03-04 23:39:18 +11:00
def cache_texture(self, url):
'''
Cache a single image url to the texture cache. url: unicode
2018-03-04 23:39:18 +11:00
'''
if url and self.enableTextureCache:
self.queue.put(url)
2018-03-04 23:39:18 +11:00
def modify_artwork(self, artworks, kodi_id, kodi_type, cursor):
"""
Pass in an artworks dict (see PlexAPI) to set an items artwork.
"""
for kodi_art, url in artworks.iteritems():
self.modify_art(url, kodi_id, kodi_type, kodi_art, cursor)
2018-03-04 23:39:18 +11:00
def modify_art(self, url, kodi_id, kodi_type, kodi_art, cursor):
"""
Adds or modifies the artwork of kind kodi_art (e.g. 'poster') in the
Kodi art table for item kodi_id/kodi_type. Will also cache everything
except actor portraits.
"""
query = '''
SELECT url FROM art
WHERE media_id = ? AND media_type = ? AND type = ?
LIMIT 1
'''
cursor.execute(query, (kodi_id, kodi_type, kodi_art,))
2016-09-11 21:51:53 +10:00
try:
# Update the artwork
2018-03-04 23:39:18 +11:00
old_url = cursor.fetchone()[0]
2016-09-11 21:51:53 +10:00
except TypeError:
# Add the artwork
2018-03-04 23:39:18 +11:00
LOG.debug('Adding Art Link for %s kodi_id %s, kodi_type %s: %s',
kodi_art, kodi_id, kodi_type, url)
query = '''
2016-09-11 21:51:53 +10:00
INSERT INTO art(media_id, media_type, type, url)
VALUES (?, ?, ?, ?)
2018-03-04 23:39:18 +11:00
'''
cursor.execute(query, (kodi_id, kodi_type, kodi_art, url))
2016-09-11 21:51:53 +10:00
else:
2018-03-04 23:39:18 +11:00
if url == old_url:
2016-09-11 21:51:53 +10:00
# Only cache artwork if it changed
return
2018-03-04 23:39:18 +11:00
self.delete_cached_artwork(old_url)
LOG.debug("Updating Art url for %s kodi_id %s, kodi_type %s to %s",
kodi_art, kodi_id, kodi_type, url)
query = '''
UPDATE art SET url = ?
WHERE media_id = ? AND media_type = ? AND type = ?
'''
cursor.execute(query, (url, kodi_id, kodi_type, kodi_art))
2016-09-11 21:51:53 +10:00
# Cache fanart and poster in Kodi texture cache
2018-03-04 23:39:18 +11:00
if kodi_type != 'actor':
self.cache_texture(url)
2018-03-04 23:39:18 +11:00
def delete_artwork(self, kodiId, mediaType, cursor):
2018-02-26 04:06:33 +11:00
query = 'SELECT url FROM art WHERE media_id = ? AND media_type = ?'
cursor.execute(query, (kodiId, mediaType,))
2018-02-26 04:06:33 +11:00
for row in cursor.fetchall():
2018-03-04 23:39:18 +11:00
self.delete_cached_artwork(row[0])
2018-03-04 23:39:18 +11:00
@staticmethod
def delete_cached_artwork(url):
"""
Deleted the cached artwork with path url (if it exists)
"""
2018-02-11 22:59:04 +11:00
connection = kodi_sql('texture')
cursor = connection.cursor()
try:
2018-03-04 23:39:18 +11:00
cursor.execute("SELECT cachedurl FROM texture WHERE url=? LIMIT 1",
2016-09-11 21:51:53 +10:00
(url,))
cachedurl = cursor.fetchone()[0]
except TypeError:
2018-03-01 03:42:21 +11:00
# Could not find cached url
pass
2016-09-11 21:51:53 +10:00
else:
# Delete thumbnail as well as the entry
path = translatePath("special://thumbnails/%s" % cachedurl)
2018-03-04 23:39:18 +11:00
LOG.debug("Deleting cached thumbnail: %s", path)
if exists(path):
2018-02-11 22:59:04 +11:00
rmtree(try_decode(path), ignore_errors=True)
cursor.execute("DELETE FROM texture WHERE url = ?", (url,))
connection.commit()
finally:
2016-09-11 21:51:53 +10:00
connection.close()
2018-01-29 03:36:36 +11:00
@staticmethod
2018-03-04 23:39:18 +11:00
def restore_cache_directories():
LOG.info("Restoring cache directories...")
2018-03-04 23:39:18 +11:00
paths = ("", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"a", "b", "c", "d", "e", "f",
"Video", "plex")
for path in paths:
makedirs(try_decode(translatePath("special://thumbnails/%s"
% path)))
class ArtworkSyncMessage(object):
"""
Put in artwork queue to display the message as a Kodi notification
"""
def __init__(self, message):
self.message = message