2018-11-09 07:22:16 +11:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import absolute_import, division, unicode_literals
|
2019-01-05 04:02:58 +11:00
|
|
|
from threading import Lock
|
|
|
|
from functools import wraps
|
2018-11-09 07:22:16 +11:00
|
|
|
|
2019-01-05 04:02:58 +11:00
|
|
|
from .. import utils, path_ops, app
|
|
|
|
|
|
|
|
KODIDB_LOCK = Lock()
|
2019-01-18 05:30:55 +11:00
|
|
|
DB_WRITE_ATTEMPTS = 100
|
2019-01-09 06:14:48 +11:00
|
|
|
|
2019-01-05 04:02:58 +11:00
|
|
|
|
2019-01-23 20:00:49 +11:00
|
|
|
class LockedKodiDatabase(Exception):
|
|
|
|
"""
|
|
|
|
Dedicated class to make sure we're not silently catching locked DBs.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2019-01-05 04:02:58 +11:00
|
|
|
def catch_operationalerrors(method):
|
2019-01-18 05:30:55 +11:00
|
|
|
"""
|
|
|
|
sqlite.OperationalError is raised immediately if another DB connection
|
|
|
|
is open, reading something that we're trying to change
|
|
|
|
|
|
|
|
So let's catch it and try again
|
2019-01-20 03:06:52 +11:00
|
|
|
|
|
|
|
Also see https://github.com/mattn/go-sqlite3/issues/274
|
2019-01-18 05:30:55 +11:00
|
|
|
"""
|
2019-01-05 04:02:58 +11:00
|
|
|
@wraps(method)
|
2019-01-20 03:53:35 +11:00
|
|
|
def wrapper(self, *args, **kwargs):
|
2019-01-14 00:29:35 +11:00
|
|
|
attempts = DB_WRITE_ATTEMPTS
|
2019-01-05 04:02:58 +11:00
|
|
|
while True:
|
|
|
|
try:
|
2019-01-20 03:53:35 +11:00
|
|
|
return method(self, *args, **kwargs)
|
2019-01-14 00:29:35 +11:00
|
|
|
except utils.OperationalError as err:
|
|
|
|
if 'database is locked' not in err:
|
2019-01-20 03:53:35 +11:00
|
|
|
# Not an error we want to catch, so reraise it
|
2019-01-14 00:29:35 +11:00
|
|
|
raise
|
2019-01-05 04:02:58 +11:00
|
|
|
attempts -= 1
|
|
|
|
if attempts == 0:
|
2019-01-14 00:51:48 +11:00
|
|
|
# Reraise in order to NOT catch nested OperationalErrors
|
2019-01-23 20:00:49 +11:00
|
|
|
raise LockedKodiDatabase('Kodi database locked')
|
2019-01-20 03:53:35 +11:00
|
|
|
# Need to close the transactions and begin new ones
|
|
|
|
self.kodiconn.commit()
|
|
|
|
if self.artconn:
|
|
|
|
self.artconn.commit()
|
|
|
|
if app.APP.monitor.waitForAbort(0.1):
|
|
|
|
# PKC needs to quit
|
|
|
|
return
|
|
|
|
# Start new transactions
|
|
|
|
self.kodiconn.execute('BEGIN')
|
|
|
|
if self.artconn:
|
|
|
|
self.artconn.execute('BEGIN')
|
|
|
|
return wrapper
|
2018-11-09 07:22:16 +11:00
|
|
|
|
|
|
|
|
|
|
|
class KodiDBBase(object):
|
|
|
|
"""
|
|
|
|
Kodi database methods used for all types of items
|
|
|
|
"""
|
2019-01-23 20:00:49 +11:00
|
|
|
def __init__(self, texture_db=False, kodiconn=None, artconn=None, lock=True):
|
2018-11-09 07:22:16 +11:00
|
|
|
"""
|
|
|
|
Allows direct use with a cursor instead of context mgr
|
|
|
|
"""
|
|
|
|
self._texture_db = texture_db
|
2019-01-05 04:02:58 +11:00
|
|
|
self.lock = lock
|
2019-01-23 20:00:49 +11:00
|
|
|
self.kodiconn = kodiconn
|
|
|
|
self.cursor = self.kodiconn.cursor() if self.kodiconn else None
|
|
|
|
self.artconn = artconn
|
|
|
|
self.artcursor = self.artconn.cursor() if self.artconn else None
|
2018-11-09 07:22:16 +11:00
|
|
|
|
|
|
|
def __enter__(self):
|
2019-01-05 04:02:58 +11:00
|
|
|
if self.lock:
|
|
|
|
KODIDB_LOCK.acquire()
|
2018-11-09 07:22:16 +11:00
|
|
|
self.kodiconn = utils.kodi_sql(self.db_kind)
|
|
|
|
self.cursor = self.kodiconn.cursor()
|
2019-01-23 20:00:49 +11:00
|
|
|
self.artconn = utils.kodi_sql('texture') if self._texture_db else None
|
|
|
|
self.artcursor = self.artconn.cursor() if self._texture_db else None
|
2018-11-09 07:22:16 +11:00
|
|
|
return self
|
|
|
|
|
|
|
|
def __exit__(self, e_typ, e_val, trcbak):
|
2019-01-05 04:02:58 +11:00
|
|
|
try:
|
|
|
|
if e_typ:
|
|
|
|
# re-raise any exception
|
|
|
|
return False
|
|
|
|
self.kodiconn.commit()
|
|
|
|
if self.artconn:
|
|
|
|
self.artconn.commit()
|
|
|
|
finally:
|
|
|
|
self.kodiconn.close()
|
|
|
|
if self.artconn:
|
|
|
|
self.artconn.close()
|
|
|
|
if self.lock:
|
|
|
|
KODIDB_LOCK.release()
|
2018-11-09 07:22:16 +11:00
|
|
|
|
|
|
|
def art_urls(self, kodi_id, kodi_type):
|
|
|
|
return (x[0] for x in
|
|
|
|
self.cursor.execute('SELECT url FROM art WHERE media_id = ? AND media_type = ?',
|
|
|
|
(kodi_id, kodi_type)))
|
|
|
|
|
2018-12-31 07:30:08 +11:00
|
|
|
def artwork_generator(self, kodi_type, limit, offset):
|
|
|
|
query = 'SELECT url FROM art WHERE type == ? LIMIT ? OFFSET ?'
|
2018-11-09 07:22:16 +11:00
|
|
|
return (x[0] for x in
|
2018-12-31 07:30:08 +11:00
|
|
|
self.cursor.execute(query, (kodi_type, limit, offset)))
|
2018-11-09 07:22:16 +11:00
|
|
|
|
2018-11-09 17:56:10 +11:00
|
|
|
def add_artwork(self, artworks, kodi_id, kodi_type):
|
|
|
|
"""
|
|
|
|
Pass in an artworks dict (see PlexAPI) to set an items artwork.
|
|
|
|
"""
|
|
|
|
for kodi_art, url in artworks.iteritems():
|
|
|
|
self.add_art(url, kodi_id, kodi_type, kodi_art)
|
|
|
|
|
2019-01-05 04:02:58 +11:00
|
|
|
@catch_operationalerrors
|
2018-11-09 17:56:10 +11:00
|
|
|
def add_art(self, url, kodi_id, kodi_type, kodi_art):
|
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
self.cursor.execute('''
|
|
|
|
INSERT INTO art(media_id, media_type, type, url)
|
|
|
|
VALUES (?, ?, ?, ?)
|
|
|
|
''', (kodi_id, kodi_type, kodi_art, url))
|
|
|
|
|
2018-11-09 07:22:16 +11:00
|
|
|
def modify_artwork(self, artworks, kodi_id, kodi_type):
|
|
|
|
"""
|
|
|
|
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)
|
|
|
|
|
2019-01-05 04:02:58 +11:00
|
|
|
@catch_operationalerrors
|
2018-11-09 07:22:16 +11:00
|
|
|
def modify_art(self, url, kodi_id, kodi_type, kodi_art):
|
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
self.cursor.execute('''
|
|
|
|
SELECT url FROM art
|
|
|
|
WHERE media_id = ? AND media_type = ? AND type = ?
|
|
|
|
LIMIT 1
|
|
|
|
''', (kodi_id, kodi_type, kodi_art,))
|
|
|
|
try:
|
|
|
|
# Update the artwork
|
|
|
|
old_url = self.cursor.fetchone()[0]
|
|
|
|
except TypeError:
|
|
|
|
# Add the artwork
|
|
|
|
self.cursor.execute('''
|
|
|
|
INSERT INTO art(media_id, media_type, type, url)
|
|
|
|
VALUES (?, ?, ?, ?)
|
|
|
|
''', (kodi_id, kodi_type, kodi_art, url))
|
|
|
|
else:
|
|
|
|
if url == old_url:
|
|
|
|
# Only cache artwork if it changed
|
|
|
|
return
|
|
|
|
self.delete_cached_artwork(old_url)
|
|
|
|
self.cursor.execute('''
|
|
|
|
UPDATE art SET url = ?
|
|
|
|
WHERE media_id = ? AND media_type = ? AND type = ?
|
|
|
|
''', (url, kodi_id, kodi_type, kodi_art))
|
|
|
|
|
|
|
|
def delete_artwork(self, kodi_id, kodi_type):
|
2019-01-05 04:02:58 +11:00
|
|
|
self.cursor.execute('SELECT url FROM art WHERE media_id = ? AND media_type = ?',
|
|
|
|
(kodi_id, kodi_type, ))
|
|
|
|
for row in self.cursor.fetchall():
|
2018-11-09 07:22:16 +11:00
|
|
|
self.delete_cached_artwork(row[0])
|
|
|
|
|
2019-01-05 04:02:58 +11:00
|
|
|
@catch_operationalerrors
|
2018-11-09 07:22:16 +11:00
|
|
|
def delete_cached_artwork(self, url):
|
|
|
|
try:
|
|
|
|
self.artcursor.execute("SELECT cachedurl FROM texture WHERE url = ? LIMIT 1",
|
|
|
|
(url, ))
|
|
|
|
cachedurl = self.artcursor.fetchone()[0]
|
|
|
|
except TypeError:
|
|
|
|
# Could not find cached url
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
# Delete thumbnail as well as the entry
|
|
|
|
path = path_ops.translate_path("special://thumbnails/%s"
|
|
|
|
% cachedurl)
|
|
|
|
if path_ops.exists(path):
|
|
|
|
path_ops.rmtree(path, ignore_errors=True)
|
|
|
|
self.artcursor.execute("DELETE FROM texture WHERE url = ?", (url, ))
|