From 3b8f7122897784f16c8c7ecd24bdb255bb7ef687 Mon Sep 17 00:00:00 2001 From: croneter Date: Thu, 14 Nov 2019 08:13:23 +0100 Subject: [PATCH] Fix database being locked in rare cases --- resources/lib/db.py | 97 +++++++++++++++++++++++++++++++ resources/lib/itemtypes/common.py | 8 +-- resources/lib/itemtypes/music.py | 8 +-- resources/lib/kodi_db/__init__.py | 62 +++++--------------- resources/lib/kodi_db/common.py | 73 +++++++---------------- resources/lib/kodi_db/music.py | 91 +++++++++++++++++++---------- resources/lib/kodi_db/texture.py | 9 +++ resources/lib/kodi_db/video.py | 86 +++++++++++++++------------ resources/lib/plex_db/common.py | 4 +- resources/lib/utils.py | 48 +-------------- 10 files changed, 265 insertions(+), 221 deletions(-) create mode 100644 resources/lib/db.py diff --git a/resources/lib/db.py b/resources/lib/db.py new file mode 100644 index 00000000..fc0363c9 --- /dev/null +++ b/resources/lib/db.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import sqlite3 +from functools import wraps + +from . import variables as v, app + +DB_WRITE_ATTEMPTS = 100 + + +class LockedDatabase(Exception): + """ + Dedicated class to make sure we're not silently catching locked DBs. + """ + pass + + +def catch_operationalerrors(method): + """ + 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 + + Also see https://github.com/mattn/go-sqlite3/issues/274 + """ + @wraps(method) + def wrapper(self, *args, **kwargs): + attempts = DB_WRITE_ATTEMPTS + while True: + try: + return method(self, *args, **kwargs) + except sqlite3.OperationalError as err: + if 'database is locked' not in err: + # Not an error we want to catch, so reraise it + raise + attempts -= 1 + if attempts == 0: + # Reraise in order to NOT catch nested OperationalErrors + raise LockedDatabase('Database is locked') + # 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 + + +def _initial_db_connection_setup(conn): + """ + Set-up DB e.g. for WAL journal mode, if that hasn't already been done + before. Also start a transaction + """ + conn.execute('PRAGMA journal_mode=WAL;') + conn.execute('PRAGMA cache_size = -8000;') + conn.execute('PRAGMA synchronous=NORMAL;') + conn.execute('BEGIN') + + +def connect(media_type=None): + """ + Open a connection to the Kodi database. + media_type: 'video' (standard if not passed), 'plex', 'music', 'texture' + """ + if media_type == "plex": + db_path = v.DB_PLEX_PATH + elif media_type == "music": + db_path = v.DB_MUSIC_PATH + elif media_type == "texture": + db_path = v.DB_TEXTURE_PATH + else: + db_path = v.DB_VIDEO_PATH + conn = sqlite3.connect(db_path, timeout=30.0) + attempts = DB_WRITE_ATTEMPTS + while True: + try: + _initial_db_connection_setup(conn) + except sqlite3.OperationalError as err: + if 'database is locked' not in err: + # Not an error we want to catch, so reraise it + raise + attempts -= 1 + if attempts == 0: + # Reraise in order to NOT catch nested OperationalErrors + raise LockedDatabase('Database is locked') + if app.APP.monitor.waitForAbort(0.05): + # PKC needs to quit + return + else: + break + return conn diff --git a/resources/lib/itemtypes/common.py b/resources/lib/itemtypes/common.py index 8c4f5d18..3188a112 100644 --- a/resources/lib/itemtypes/common.py +++ b/resources/lib/itemtypes/common.py @@ -6,7 +6,7 @@ from ntpath import dirname from ..plex_db import PlexDB, PLEXDB_LOCK from ..kodi_db import KodiVideoDB, KODIDB_LOCK -from .. import utils, timing, app +from .. import db, timing, app LOG = getLogger('PLEX.itemtypes.common') @@ -57,11 +57,11 @@ class ItemBase(object): if self.lock: PLEXDB_LOCK.acquire() KODIDB_LOCK.acquire() - self.plexconn = utils.kodi_sql('plex') + self.plexconn = db.connect('plex') self.plexcursor = self.plexconn.cursor() - self.kodiconn = utils.kodi_sql('video') + self.kodiconn = db.connect('video') self.kodicursor = self.kodiconn.cursor() - self.artconn = utils.kodi_sql('texture') + self.artconn = db.connect('texture') self.artcursor = self.artconn.cursor() self.plexdb = PlexDB(plexconn=self.plexconn, lock=False) self.kodidb = KodiVideoDB(texture_db=True, diff --git a/resources/lib/itemtypes/music.py b/resources/lib/itemtypes/music.py index f747ea84..61b16003 100644 --- a/resources/lib/itemtypes/music.py +++ b/resources/lib/itemtypes/music.py @@ -7,7 +7,7 @@ from .common import ItemBase from ..plex_api import API from ..plex_db import PlexDB, PLEXDB_LOCK from ..kodi_db import KodiMusicDB, KODIDB_LOCK -from .. import plex_functions as PF, utils, timing, app, variables as v +from .. import plex_functions as PF, db, timing, app, variables as v LOG = getLogger('PLEX.music') @@ -20,11 +20,11 @@ class MusicMixin(object): if self.lock: PLEXDB_LOCK.acquire() KODIDB_LOCK.acquire() - self.plexconn = utils.kodi_sql('plex') + self.plexconn = db.connect('plex') self.plexcursor = self.plexconn.cursor() - self.kodiconn = utils.kodi_sql('music') + self.kodiconn = db.connect('music') self.kodicursor = self.kodiconn.cursor() - self.artconn = utils.kodi_sql('texture') + self.artconn = db.connect('texture') self.artcursor = self.artconn.cursor() self.plexdb = PlexDB(plexconn=self.plexconn, lock=False) self.kodidb = KodiMusicDB(texture_db=True, diff --git a/resources/lib/kodi_db/__init__.py b/resources/lib/kodi_db/__init__.py index 052f7827..67e24cf7 100644 --- a/resources/lib/kodi_db/__init__.py +++ b/resources/lib/kodi_db/__init__.py @@ -8,7 +8,7 @@ from .video import KodiVideoDB from .music import KodiMusicDB from .texture import KodiTextureDB -from .. import path_ops, utils, timing, variables as v +from .. import path_ops, utils, variables as v LOG = getLogger('PLEX.kodi_db') @@ -56,30 +56,7 @@ def setup_kodi_default_entries(): """ if utils.settings('enableMusic') == 'true': with KodiMusicDB() as kodidb: - kodidb.cursor.execute(''' - INSERT OR REPLACE INTO artist( - idArtist, - strArtist, - strMusicBrainzArtistID) - VALUES (?, ?, ?) - ''', (1, '[Missing Tag]', 'Artist Tag Missing')) - kodidb.cursor.execute(''' - INSERT OR REPLACE INTO role( - idRole, - strRole) - VALUES (?, ?) - ''', (1, 'Artist')) - if v.KODIVERSION >= 18: - kodidb.cursor.execute('DELETE FROM versiontagscan') - kodidb.cursor.execute(''' - INSERT INTO versiontagscan( - idVersion, - iNeedsScan, - lastscanned) - VALUES (?, ?, ?) - ''', (v.DB_MUSIC_VERSION, - 0, - timing.kodi_now())) + kodidb.setup_kodi_default_entries() def reset_cached_images(): @@ -98,10 +75,7 @@ def reset_cached_images(): except OSError: pass with KodiTextureDB() as kodidb: - for row in kodidb.cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type=?', - ('table', )): - if row[0] != 'version': - kodidb.cursor.execute("DELETE FROM %s" % row[0]) + kodidb.reset_cached_images() def wipe_dbs(music=True): @@ -112,25 +86,13 @@ def wipe_dbs(music=True): DO NOT use context menu as we need to connect without WAL mode - if Kodi is still accessing the DB """ - from sqlite3 import connect LOG.warn('Wiping Kodi databases!') - kinds = [v.DB_VIDEO_PATH, v.DB_TEXTURE_PATH] + kinds = [KodiVideoDB, KodiTextureDB] if music: - kinds.append(v.DB_MUSIC_PATH) - for path in kinds: - conn = connect(path, timeout=30.0) - cursor = conn.cursor() - cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'") - tables = cursor.fetchall() - tables = [i[0] for i in tables] - if 'version' in tables: - tables.remove('version') - if 'versiontagscan' in tables: - tables.remove('versiontagscan') - for table in tables: - cursor.execute('DELETE FROM %s' % table) - conn.commit() - conn.close() + kinds.insert(1, KodiMusicDB) + for kind in kinds: + with kind() as kodidb: + kodidb.wipe() setup_kodi_default_entries() # Delete SQLITE wal files import xbmc @@ -140,6 +102,14 @@ def wipe_dbs(music=True): xbmc.executebuiltin('UpdateLibrary(music)') +def create_kodi_db_indicees(): + """ + Index the "actors" because we got a TON - speed up SELECT and WHEN + """ + with KodiVideoDB() as kodidb: + kodidb.create_kodi_db_indicees() + + KODIDB_FROM_PLEXTYPE = { v.PLEX_TYPE_MOVIE: KodiVideoDB, v.PLEX_TYPE_SHOW: KodiVideoDB, diff --git a/resources/lib/kodi_db/common.py b/resources/lib/kodi_db/common.py index 510d8559..a7ddb4d6 100644 --- a/resources/lib/kodi_db/common.py +++ b/resources/lib/kodi_db/common.py @@ -2,56 +2,12 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals from threading import Lock -from functools import wraps -from .. import utils, path_ops, app +from .. import db, path_ops KODIDB_LOCK = Lock() -DB_WRITE_ATTEMPTS = 100 - - -class LockedKodiDatabase(Exception): - """ - Dedicated class to make sure we're not silently catching locked DBs. - """ - pass - - -def catch_operationalerrors(method): - """ - 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 - - Also see https://github.com/mattn/go-sqlite3/issues/274 - """ - @wraps(method) - def wrapper(self, *args, **kwargs): - attempts = DB_WRITE_ATTEMPTS - while True: - try: - return method(self, *args, **kwargs) - except utils.OperationalError as err: - if 'database is locked' not in err: - # Not an error we want to catch, so reraise it - raise - attempts -= 1 - if attempts == 0: - # Reraise in order to NOT catch nested OperationalErrors - raise LockedKodiDatabase('Kodi database locked') - # 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 +# Names of tables we generally leave untouched and e.g. don't wipe +UNTOUCHED_TABLES = ('version', 'versiontagscan') class KodiDBBase(object): @@ -72,9 +28,9 @@ class KodiDBBase(object): def __enter__(self): if self.lock: KODIDB_LOCK.acquire() - self.kodiconn = utils.kodi_sql(self.db_kind) + self.kodiconn = db.connect(self.db_kind) self.cursor = self.kodiconn.cursor() - self.artconn = utils.kodi_sql('texture') if self._texture_db else None + self.artconn = db.connect('texture') if self._texture_db else None self.artcursor = self.artconn.cursor() if self._texture_db else None return self @@ -110,7 +66,7 @@ class KodiDBBase(object): for kodi_art, url in artworks.iteritems(): self.add_art(url, kodi_id, kodi_type, kodi_art) - @catch_operationalerrors + @db.catch_operationalerrors 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 @@ -129,7 +85,7 @@ class KodiDBBase(object): for kodi_art, url in artworks.iteritems(): self.modify_art(url, kodi_id, kodi_type, kodi_art) - @catch_operationalerrors + @db.catch_operationalerrors 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 @@ -166,7 +122,7 @@ class KodiDBBase(object): for row in self.cursor.fetchall(): self.delete_cached_artwork(row[0]) - @catch_operationalerrors + @db.catch_operationalerrors def delete_cached_artwork(self, url): try: self.artcursor.execute("SELECT cachedurl FROM texture WHERE url = ? LIMIT 1", @@ -182,3 +138,16 @@ class KodiDBBase(object): if path_ops.exists(path): path_ops.rmtree(path, ignore_errors=True) self.artcursor.execute("DELETE FROM texture WHERE url = ?", (url, )) + + @db.catch_operationalerrors + def wipe(self): + """ + Completely wipes the corresponding Kodi database + """ + self.cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'") + tables = [i[0] for i in self.cursor.fetchall()] + for table in UNTOUCHED_TABLES: + if table in tables: + tables.remove(table) + for table in tables: + self.cursor.execute('DELETE FROM %s' % table) diff --git a/resources/lib/kodi_db/music.py b/resources/lib/kodi_db/music.py index 37f5c4a9..77ede150 100644 --- a/resources/lib/kodi_db/music.py +++ b/resources/lib/kodi_db/music.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals from logging import getLogger from . import common -from .. import variables as v, app +from .. import db, variables as v, app, timing LOG = getLogger('PLEX.kodi_db.music') @@ -12,7 +12,7 @@ LOG = getLogger('PLEX.kodi_db.music') class KodiMusicDB(common.KodiDBBase): db_kind = 'music' - @common.catch_operationalerrors + @db.catch_operationalerrors def add_path(self, path): """ Add the path (unicode) to the music DB, if it does not exist already. @@ -34,7 +34,38 @@ class KodiMusicDB(common.KodiDBBase): (pathid, path, '123')) return pathid - @common.catch_operationalerrors + @db.catch_operationalerrors + def setup_kodi_default_entries(self): + """ + Makes sure that we retain the Kodi standard databases. E.g. that there + is a dummy artist with ID 1 + """ + self.cursor.execute(''' + INSERT OR REPLACE INTO artist( + idArtist, + strArtist, + strMusicBrainzArtistID) + VALUES (?, ?, ?) + ''', (1, '[Missing Tag]', 'Artist Tag Missing')) + self.cursor.execute(''' + INSERT OR REPLACE INTO role( + idRole, + strRole) + VALUES (?, ?) + ''', (1, 'Artist')) + if v.KODIVERSION >= 18: + self.cursor.execute('DELETE FROM versiontagscan') + self.cursor.execute(''' + INSERT INTO versiontagscan( + idVersion, + iNeedsScan, + lastscanned) + VALUES (?, ?, ?) + ''', (v.DB_MUSIC_VERSION, + 0, + timing.kodi_now())) + + @db.catch_operationalerrors def update_path(self, path, kodi_pathid): self.cursor.execute(''' UPDATE path @@ -62,7 +93,7 @@ class KodiMusicDB(common.KodiDBBase): return return song_ids[0][0] - @common.catch_operationalerrors + @db.catch_operationalerrors def delete_song_from_song_artist(self, song_id): """ Deletes son from song_artist table and possibly orphaned roles @@ -79,7 +110,7 @@ class KodiMusicDB(common.KodiDBBase): self.cursor.execute('DELETE FROM song_artist WHERE idSong = ?', (song_id, )) - @common.catch_operationalerrors + @db.catch_operationalerrors def delete_album_from_discography(self, album_id): """ Removes the album with id album_id from the table discography @@ -99,7 +130,7 @@ class KodiMusicDB(common.KodiDBBase): self.cursor.execute('DELETE FROM discography WHERE idArtist = ? AND strAlbum = ? AND strYear = ?', (artist[0], name, year)) - @common.catch_operationalerrors + @db.catch_operationalerrors def delete_song_from_song_genre(self, song_id): """ Deletes the one entry with id song_id from the song_genre table. @@ -120,7 +151,7 @@ class KodiMusicDB(common.KodiDBBase): if not self.cursor.fetchone(): self.delete_genre(genre[0]) - @common.catch_operationalerrors + @db.catch_operationalerrors def delete_genre(self, genre_id): """ Dedicated method in order to catch OperationalErrors correctly @@ -128,7 +159,7 @@ class KodiMusicDB(common.KodiDBBase): self.cursor.execute('DELETE FROM genre WHERE idGenre = ?', (genre_id, )) - @common.catch_operationalerrors + @db.catch_operationalerrors def delete_album_from_album_genre(self, album_id): """ Deletes the one entry with id album_id from the album_genre table. @@ -153,7 +184,7 @@ class KodiMusicDB(common.KodiDBBase): self.cursor.execute('SELECT COALESCE(MAX(idAlbum), 0) FROM album') return self.cursor.fetchone()[0] + 1 - @common.catch_operationalerrors + @db.catch_operationalerrors def add_album_17(self, *args): """ strReleaseType: 'album' or 'single' @@ -196,7 +227,7 @@ class KodiMusicDB(common.KodiDBBase): VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def update_album_17(self, *args): if app.SYNC.artwork: self.cursor.execute(''' @@ -234,7 +265,7 @@ class KodiMusicDB(common.KodiDBBase): WHERE idAlbum = ? ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_album(self, *args): """ strReleaseType: 'album' or 'single' @@ -277,7 +308,7 @@ class KodiMusicDB(common.KodiDBBase): VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def update_album(self, *args): if app.SYNC.artwork: self.cursor.execute(''' @@ -315,7 +346,7 @@ class KodiMusicDB(common.KodiDBBase): WHERE idAlbum = ? ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_albumartist(self, artist_id, kodi_id, artistname): self.cursor.execute(''' INSERT OR REPLACE INTO album_artist( @@ -325,7 +356,7 @@ class KodiMusicDB(common.KodiDBBase): VALUES (?, ?, ?) ''', (artist_id, kodi_id, artistname)) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_discography(self, artist_id, albumname, year): self.cursor.execute(''' INSERT OR REPLACE INTO discography( @@ -335,7 +366,7 @@ class KodiMusicDB(common.KodiDBBase): VALUES (?, ?, ?) ''', (artist_id, albumname, year)) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_music_genres(self, kodiid, genres, mediatype): """ Adds a list of genres (list of unicode) for a certain Kodi item @@ -388,7 +419,7 @@ class KodiMusicDB(common.KodiDBBase): self.cursor.execute('SELECT COALESCE(MAX(idSong),0) FROM song') return self.cursor.fetchone()[0] + 1 - @common.catch_operationalerrors + @db.catch_operationalerrors def add_song(self, *args): self.cursor.execute(''' INSERT INTO song( @@ -413,7 +444,7 @@ class KodiMusicDB(common.KodiDBBase): VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_song_17(self, *args): self.cursor.execute(''' INSERT INTO song( @@ -438,7 +469,7 @@ class KodiMusicDB(common.KodiDBBase): VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def update_song(self, *args): self.cursor.execute(''' UPDATE song @@ -459,7 +490,7 @@ class KodiMusicDB(common.KodiDBBase): WHERE idSong = ? ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def set_playcount(self, *args): self.cursor.execute(''' UPDATE song @@ -468,7 +499,7 @@ class KodiMusicDB(common.KodiDBBase): WHERE idSong = ? ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def update_song_17(self, *args): self.cursor.execute(''' UPDATE song @@ -497,7 +528,7 @@ class KodiMusicDB(common.KodiDBBase): except TypeError: pass - @common.catch_operationalerrors + @db.catch_operationalerrors def add_artist(self, name, musicbrainz): """ Adds a single artist's name to the db @@ -534,7 +565,7 @@ class KodiMusicDB(common.KodiDBBase): (name, artistid,)) return artistid - @common.catch_operationalerrors + @db.catch_operationalerrors def update_artist(self, *args): if app.SYNC.artwork: self.cursor.execute(''' @@ -557,15 +588,15 @@ class KodiMusicDB(common.KodiDBBase): WHERE idArtist = ? ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_song(self, kodi_id): self.cursor.execute('DELETE FROM song WHERE idSong = ?', (kodi_id, )) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_path(self, path_id): self.cursor.execute('DELETE FROM path WHERE idPath = ?', (path_id, )) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_song_artist(self, artist_id, song_id, artist_name): self.cursor.execute(''' INSERT OR REPLACE INTO song_artist( @@ -577,7 +608,7 @@ class KodiMusicDB(common.KodiDBBase): VALUES (?, ?, ?, ?, ?) ''', (artist_id, song_id, 1, 0, artist_name)) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_albuminfosong(self, song_id, album_id, track_no, track_title, runtime): """ @@ -593,7 +624,7 @@ class KodiMusicDB(common.KodiDBBase): VALUES (?, ?, ?, ?, ?) ''', (song_id, album_id, track_no, track_title, runtime)) - @common.catch_operationalerrors + @db.catch_operationalerrors def update_userrating(self, kodi_id, kodi_type, userrating): """ Updates userrating for songs and albums @@ -610,7 +641,7 @@ class KodiMusicDB(common.KodiDBBase): % (kodi_type, column), (userrating, identifier, kodi_id)) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_albuminfosong(self, kodi_id): """ Kodi 17 only @@ -618,7 +649,7 @@ class KodiMusicDB(common.KodiDBBase): self.cursor.execute('DELETE FROM albuminfosong WHERE idAlbumInfoSong = ?', (kodi_id, )) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_album(self, kodi_id): if v.KODIVERSION < 18: self.cursor.execute('DELETE FROM albuminfosong WHERE idAlbumInfo = ?', @@ -627,7 +658,7 @@ class KodiMusicDB(common.KodiDBBase): (kodi_id, )) self.cursor.execute('DELETE FROM album WHERE idAlbum = ?', (kodi_id, )) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_artist(self, kodi_id): self.cursor.execute('DELETE FROM album_artist WHERE idArtist = ?', (kodi_id, )) diff --git a/resources/lib/kodi_db/texture.py b/resources/lib/kodi_db/texture.py index 14e016f2..12600950 100644 --- a/resources/lib/kodi_db/texture.py +++ b/resources/lib/kodi_db/texture.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, unicode_literals from . import common +from .. import db class KodiTextureDB(common.KodiDBBase): @@ -15,3 +16,11 @@ class KodiTextureDB(common.KodiDBBase): self.artcursor.execute('SELECT url FROM texture WHERE url = ? LIMIT 1', (url, )) return self.artcursor.fetchone() is None + + @db.catch_operationalerrors + def reset_cached_images(self): + for row in self.cursor.execute('SELECT tbl_name ' + 'FROM sqlite_master WHERE type=?', + ('table', )): + if row[0] != 'version': + self.cursor.execute("DELETE FROM %s" % row[0]) diff --git a/resources/lib/kodi_db/video.py b/resources/lib/kodi_db/video.py index 2c4d409a..6d9e1692 100644 --- a/resources/lib/kodi_db/video.py +++ b/resources/lib/kodi_db/video.py @@ -5,7 +5,7 @@ from logging import getLogger from sqlite3 import IntegrityError from . import common -from .. import path_ops, timing, variables as v +from .. import db, path_ops, timing, variables as v LOG = getLogger('PLEX.kodi_db.video') @@ -16,7 +16,19 @@ SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID class KodiVideoDB(common.KodiDBBase): db_kind = 'video' - @common.catch_operationalerrors + @db.catch_operationalerrors + def create_kodi_db_indicees(self): + """ + Index the "actors" because we got a TON - speed up SELECT and WHEN + """ + commands = ( + 'CREATE UNIQUE INDEX IF NOT EXISTS ix_actor_2 ON actor (actor_id);', + 'CREATE UNIQUE INDEX IF NOT EXISTS ix_files_2 ON files (idFile);', + ) + for cmd in commands: + self.cursor.execute(cmd) + + @db.catch_operationalerrors def setup_path_table(self): """ Use with Kodi video DB @@ -66,7 +78,7 @@ class KodiVideoDB(common.KodiDBBase): 1, 0)) - @common.catch_operationalerrors + @db.catch_operationalerrors def parent_path_id(self, path): """ Video DB: Adds all subdirectories to path table while setting a "trail" @@ -90,7 +102,7 @@ class KodiVideoDB(common.KodiDBBase): self.update_parentpath_id(parent_id, pathid) return pathid - @common.catch_operationalerrors + @db.catch_operationalerrors def update_parentpath_id(self, parent_id, pathid): """ Dedicated method in order to catch OperationalErrors correctly @@ -98,7 +110,7 @@ class KodiVideoDB(common.KodiDBBase): self.cursor.execute('UPDATE path SET idParentPath = ? WHERE idPath = ?', (parent_id, pathid)) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_path(self, path, date_added=None, id_parent_path=None, content=None, scraper=None): """ @@ -143,7 +155,7 @@ class KodiVideoDB(common.KodiDBBase): except TypeError: pass - @common.catch_operationalerrors + @db.catch_operationalerrors def add_file(self, filename, path_id, date_added): """ Adds the filename [unicode] to the table files if not already added @@ -201,7 +213,7 @@ class KodiVideoDB(common.KodiDBBase): except TypeError: pass - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_file(self, file_id, remove_orphans=True): """ Removes the entry for file_id from the files table. Will also delete @@ -237,7 +249,7 @@ class KodiVideoDB(common.KodiDBBase): ''' self.cursor.execute(query, (path_id, MOVIE_PATH, SHOW_PATH)) - @common.catch_operationalerrors + @db.catch_operationalerrors def _modify_link_and_table(self, kodi_id, kodi_type, entries, link_table, table, key, first_id=None): first_id = first_id if first_id is not None else 1 @@ -343,7 +355,7 @@ class KodiVideoDB(common.KodiDBBase): for kind, people_list in people.iteritems(): self._add_people_kind(kodi_id, kodi_type, kind, people_list) - @common.catch_operationalerrors + @db.catch_operationalerrors def _add_people_kind(self, kodi_id, kodi_type, kind, people_list): # Save new people to Kodi DB by iterating over the remaining entries if kind == 'actor': @@ -388,7 +400,7 @@ class KodiVideoDB(common.KodiDBBase): 'writer': []}).iteritems(): self._modify_people_kind(kodi_id, kodi_type, kind, people_list) - @common.catch_operationalerrors + @db.catch_operationalerrors def _modify_people_kind(self, kodi_id, kodi_type, kind, people_list): # Get the people already saved in the DB for this specific item if kind == 'actor': @@ -443,7 +455,7 @@ class KodiVideoDB(common.KodiDBBase): # Save new people to Kodi DB by iterating over the remaining entries self._add_people_kind(kodi_id, kodi_type, kind, people_list) - @common.catch_operationalerrors + @db.catch_operationalerrors def _new_actor_id(self, name, art_url): # Not yet in actor DB, add person self.cursor.execute('SELECT COALESCE(MAX(actor_id), 0) FROM actor') @@ -503,7 +515,7 @@ class KodiVideoDB(common.KodiDBBase): (kodi_id, kodi_type)) return dict(self.cursor.fetchall()) - @common.catch_operationalerrors + @db.catch_operationalerrors def modify_streams(self, fileid, streamdetails=None, runtime=None): """ Leave streamdetails and runtime empty to delete all stream entries for @@ -622,7 +634,7 @@ class KodiVideoDB(common.KodiDBBase): except TypeError: pass - @common.catch_operationalerrors + @db.catch_operationalerrors def set_resume(self, file_id, resume_seconds, total_seconds, playcount, dateplayed): """ @@ -660,7 +672,7 @@ class KodiVideoDB(common.KodiDBBase): '', 1)) - @common.catch_operationalerrors + @db.catch_operationalerrors def create_tag(self, name): """ Will create a new tag if needed and return the tag_id @@ -676,7 +688,7 @@ class KodiVideoDB(common.KodiDBBase): (tag_id, name)) return tag_id - @common.catch_operationalerrors + @db.catch_operationalerrors def update_tag(self, oldtag, newtag, kodiid, mediatype): """ Updates the tag_id by replaying oldtag with newtag @@ -695,7 +707,7 @@ class KodiVideoDB(common.KodiDBBase): WHERE media_id = ? AND media_type = ? AND tag_id = ? ''', (kodiid, mediatype, oldtag,)) - @common.catch_operationalerrors + @db.catch_operationalerrors def create_collection(self, set_name): """ Returns the collection/set id for set_name [unicode] @@ -711,7 +723,7 @@ class KodiVideoDB(common.KodiDBBase): (setid, set_name)) return setid - @common.catch_operationalerrors + @db.catch_operationalerrors def assign_collection(self, setid, movieid): """ Assign the movie to one set/collection @@ -719,7 +731,7 @@ class KodiVideoDB(common.KodiDBBase): self.cursor.execute('UPDATE movie SET idSet = ? WHERE idMovie = ?', (setid, movieid,)) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_from_set(self, movieid): """ Remove the movie with movieid [int] from an associated movie set, movie @@ -739,7 +751,7 @@ class KodiVideoDB(common.KodiDBBase): except TypeError: pass - @common.catch_operationalerrors + @db.catch_operationalerrors def delete_possibly_empty_set(self, set_id): """ Checks whether there are other movies in the set set_id. If not, @@ -750,7 +762,7 @@ class KodiVideoDB(common.KodiDBBase): if self.cursor.fetchone() is None: self.cursor.execute('DELETE FROM sets WHERE idSet = ?', (set_id,)) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_season(self, showid, seasonnumber): """ Adds a TV show season to the Kodi video DB or simply returns the ID, @@ -764,7 +776,7 @@ class KodiVideoDB(common.KodiDBBase): ''', (seasonid, showid, seasonnumber)) return seasonid - @common.catch_operationalerrors + @db.catch_operationalerrors def add_uniqueid(self, *args): """ Feed with: @@ -799,7 +811,7 @@ class KodiVideoDB(common.KodiDBBase): except TypeError: return self.add_uniqueid_id() - @common.catch_operationalerrors + @db.catch_operationalerrors def update_uniqueid(self, *args): """ Pass in media_id, media_type, value, type, uniqueid_id @@ -810,7 +822,7 @@ class KodiVideoDB(common.KodiDBBase): WHERE uniqueid_id = ? ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_uniqueid(self, kodi_id, kodi_type): """ Deletes the entry from the uniqueid table for the item @@ -833,7 +845,7 @@ class KodiVideoDB(common.KodiDBBase): except TypeError: return self.add_ratingid() - @common.catch_operationalerrors + @db.catch_operationalerrors def update_ratings(self, *args): """ Feed with media_id, media_type, rating_type, rating, votes, rating_id @@ -848,7 +860,7 @@ class KodiVideoDB(common.KodiDBBase): WHERE rating_id = ? ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_ratings(self, *args): """ feed with: @@ -867,7 +879,7 @@ class KodiVideoDB(common.KodiDBBase): VALUES (?, ?, ?, ?, ?, ?) ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_ratings(self, kodi_id, kodi_type): """ Removes all ratings from the rating table for the item @@ -883,7 +895,7 @@ class KodiVideoDB(common.KodiDBBase): self.cursor.execute('SELECT COALESCE(MAX(idEpisode), 0) FROM episode') return self.cursor.fetchone()[0] + 1 - @common.catch_operationalerrors + @db.catch_operationalerrors def add_episode(self, *args): self.cursor.execute( ''' @@ -911,7 +923,7 @@ class KodiVideoDB(common.KodiDBBase): (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def update_episode(self, *args): self.cursor.execute( ''' @@ -936,7 +948,7 @@ class KodiVideoDB(common.KodiDBBase): WHERE idEpisode = ? ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_show(self, *args): self.cursor.execute( ''' @@ -955,7 +967,7 @@ class KodiVideoDB(common.KodiDBBase): VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def update_show(self, *args): self.cursor.execute( ''' @@ -973,21 +985,21 @@ class KodiVideoDB(common.KodiDBBase): WHERE idShow = ? ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def add_showlinkpath(self, kodi_id, kodi_pathid): self.cursor.execute('INSERT INTO tvshowlinkpath(idShow, idPath) VALUES (?, ?)', (kodi_id, kodi_pathid)) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_show(self, kodi_id): self.cursor.execute('DELETE FROM tvshow WHERE idShow = ?', (kodi_id,)) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_season(self, kodi_id): self.cursor.execute('DELETE FROM seasons WHERE idSeason = ?', (kodi_id,)) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_episode(self, kodi_id): self.cursor.execute('DELETE FROM episode WHERE idEpisode = ?', (kodi_id,)) @@ -996,7 +1008,7 @@ class KodiVideoDB(common.KodiDBBase): self.cursor.execute('SELECT COALESCE(MAX(idMovie), 0) FROM movie') return self.cursor.fetchone()[0] + 1 - @common.catch_operationalerrors + @db.catch_operationalerrors def add_movie(self, *args): self.cursor.execute( ''' @@ -1030,11 +1042,11 @@ class KodiVideoDB(common.KodiDBBase): ?, ?, ?, ?) ''', (args)) - @common.catch_operationalerrors + @db.catch_operationalerrors def remove_movie(self, kodi_id): self.cursor.execute('DELETE FROM movie WHERE idMovie = ?', (kodi_id,)) - @common.catch_operationalerrors + @db.catch_operationalerrors def update_userrating(self, kodi_id, kodi_type, userrating): """ Updates userrating diff --git a/resources/lib/plex_db/common.py b/resources/lib/plex_db/common.py index c2764cad..f150fa38 100644 --- a/resources/lib/plex_db/common.py +++ b/resources/lib/plex_db/common.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, division, unicode_literals from threading import Lock -from .. import utils, variables as v +from .. import db, variables as v PLEXDB_LOCK = Lock() @@ -31,7 +31,7 @@ class PlexDBBase(object): def __enter__(self): if self.lock: PLEXDB_LOCK.acquire() - self.plexconn = utils.kodi_sql('plex') + self.plexconn = db.connect('plex') self.cursor = self.plexconn.cursor() return self diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 555cb85a..ab2a0eaf 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -5,7 +5,7 @@ Various functions and decorators for PKC """ from __future__ import absolute_import, division, unicode_literals from logging import getLogger -from sqlite3 import connect, OperationalError +from sqlite3 import OperationalError from datetime import datetime from unicodedata import normalize from threading import Lock @@ -525,50 +525,6 @@ def delete_temporary_subtitles(): root, file, err) -def kodi_sql(media_type=None): - """ - Open a connection to the Kodi database. - media_type: 'video' (standard if not passed), 'plex', 'music', 'texture' - """ - if media_type == "plex": - db_path = v.DB_PLEX_PATH - elif media_type == "music": - db_path = v.DB_MUSIC_PATH - elif media_type == "texture": - db_path = v.DB_TEXTURE_PATH - else: - db_path = v.DB_VIDEO_PATH - conn = connect(db_path, timeout=30.0) - conn.execute('PRAGMA journal_mode=WAL;') - conn.execute('PRAGMA cache_size = -8000;') - conn.execute('PRAGMA synchronous=NORMAL;') - conn.execute('BEGIN') - # Use transactions - return conn - - -def create_kodi_db_indicees(): - """ - Index the "actors" because we got a TON - speed up SELECT and WHEN - """ - conn = kodi_sql('video') - cursor = conn.cursor() - commands = ( - 'CREATE UNIQUE INDEX IF NOT EXISTS ix_actor_2 ON actor (actor_id);', - 'CREATE UNIQUE INDEX IF NOT EXISTS ix_files_2 ON files (idFile);', - ) - for cmd in commands: - cursor.execute(cmd) - # Already used in Kodi >=17: CREATE UNIQUE INDEX ix_actor_1 ON actor (name) - # try: - # cursor.execute('CREATE UNIQUE INDEX ix_pkc_actor_index ON actor (name);') - # except OperationalError: - # # Index already exists - # pass - conn.commit() - conn.close() - - def wipe_synched_playlists(): """ Deletes all synched playlist files on the Kodi side; resets the Plex table @@ -644,7 +600,7 @@ def init_dbs(): # Ensure that Plex DB is set-up plex_db.initialize() # Hack to speed up look-ups for actors (giant table!) - create_kodi_db_indicees() + kodi_db.create_kodi_db_indicees() kodi_db.setup_kodi_default_entries() with kodi_db.KodiVideoDB() as kodidb: # Setup the paths for addon-paths (even when using direct paths)