diff --git a/default.py b/default.py index e52c524b..eae867f6 100644 --- a/default.py +++ b/default.py @@ -23,6 +23,7 @@ import entrypoint import loghandler from utils import window, dialog, language as lang from ga_client import GoogleAnalytics +import database ################################################################################################# @@ -80,7 +81,7 @@ class Main(object): import utils modes = { - 'reset': utils.reset, + 'reset': database.db_reset, 'resetauth': entrypoint.resetAuth, 'play': entrypoint.doPlayback, 'passwords': utils.passwordsXML, diff --git a/resources/lib/artwork.py b/resources/lib/artwork.py index 2b828a2d..441dc2de 100644 --- a/resources/lib/artwork.py +++ b/resources/lib/artwork.py @@ -13,7 +13,9 @@ import xbmcvfs import requests import image_cache_thread -from utils import window, settings, dialog, language as lang, kodiSQL, JSONRPC +from utils import window, settings, dialog, language as lang, JSONRPC +from database import DatabaseConn +from contextlib import closing ################################################################################################## @@ -164,47 +166,49 @@ class Artwork(object): def _cache_all_video_entries(self, pdialog): - conn = kodiSQL('video') - cursor = conn.cursor() - cursor.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors - result = cursor.fetchall() - total = len(result) - log.info("Image cache sync about to process %s images", total) - cursor.close() + with DatabaseConn('video') as conn: + with closing(conn.cursor()) as cursor_video: + + cursor_video.execute("SELECT url FROM art WHERE media_type != 'actor'") # dont include actors + result = cursor_video.fetchall() + total = len(result) + log.info("Image cache sync about to process %s images", total) + cursor_video.close() - count = 0 - for url in result: + count = 0 + for url in result: - if pdialog.iscanceled(): - break + if pdialog.iscanceled(): + break - percentage = int((float(count) / float(total))*100) - message = "%s of %s (%s)" % (count, total, len(self.image_cache_threads)) - pdialog.update(percentage, "%s %s" % (lang(33045), message)) - self.cache_texture(url[0]) - count += 1 + percentage = int((float(count) / float(total))*100) + message = "%s of %s (%s)" % (count, total, len(self.image_cache_threads)) + pdialog.update(percentage, "%s %s" % (lang(33045), message)) + self.cache_texture(url[0]) + count += 1 def _cache_all_music_entries(self, pdialog): - conn = kodiSQL('music') - cursor = conn.cursor() - cursor.execute("SELECT url FROM art") - result = cursor.fetchall() - total = len(result) - log.info("Image cache sync about to process %s images", total) - cursor.close() + with DatabaseConn('music') as conn: + with closing(conn.cursor()) as cursor_music: + + cursor_music.execute("SELECT url FROM art") + result = cursor_music.fetchall() + total = len(result) + + log.info("Image cache sync about to process %s images", total) - count = 0 - for url in result: + count = 0 + for url in result: - if pdialog.iscanceled(): - break + if pdialog.iscanceled(): + break - percentage = int((float(count) / float(total))*100) - message = "%s of %s" % (count, total) - pdialog.update(percentage, "%s %s" % (lang(33045), message)) - self.cache_texture(url[0]) - count += 1 + percentage = int((float(count) / float(total))*100) + message = "%s of %s" % (count, total) + pdialog.update(percentage, "%s %s" % (lang(33045), message)) + self.cache_texture(url[0]) + count += 1 @classmethod def delete_cache(cls): @@ -226,16 +230,14 @@ class Artwork(object): log.debug("deleted: %s", filename) # remove all existing data from texture DB - conn = kodiSQL('texture') - cursor = conn.cursor() - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - table_name = row[0] - if table_name != "version": - cursor.execute("DELETE FROM " + table_name) - conn.commit() - cursor.close() + with DatabaseConn('texture') as conn: + with closing(conn.cursor()) as cursor_texture: + cursor_texture.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') + rows = cursor_texture.fetchall() + for row in rows: + table_name = row[0] + if table_name != "version": + cursor_texture.execute("DELETE FROM " + table_name) def _add_worker_image_thread(self, url): @@ -428,33 +430,29 @@ class Artwork(object): @classmethod def delete_cached_artwork(cls, url): - # Only necessary to remove and apply a new backdrop or poster - conn = kodiSQL('texture') - cursor = conn.cursor() + # Only necessary to remove and apply a new backdrop or poster + with DatabaseConn('texture') as conn: + with closing(conn.cursor()) as cursor_texture: + try: + cursor_texture.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,)) + cached_url = cursor_texture.fetchone()[0] - try: - cursor.execute("SELECT cachedurl FROM texture WHERE url = ?", (url,)) - cached_url = cursor.fetchone()[0] + except TypeError: + log.info("Could not find cached url") - except TypeError: - log.info("Could not find cached url") + except OperationalError: + log.info("Database is locked. Skip deletion process.") - except OperationalError: - log.info("Database is locked. Skip deletion process.") + else: # Delete thumbnail as well as the entry + thumbnails = xbmc.translatePath("special://thumbnails/%s" % cached_url).decode('utf-8') + log.info("Deleting cached thumbnail: %s", thumbnails) + xbmcvfs.delete(thumbnails) - else: # Delete thumbnail as well as the entry - thumbnails = xbmc.translatePath("special://thumbnails/%s" % cached_url).decode('utf-8') - log.info("Deleting cached thumbnail: %s", thumbnails) - xbmcvfs.delete(thumbnails) + try: + cursor_texture.execute("DELETE FROM texture WHERE url = ?", (url,)) + except OperationalError: + log.debug("Issue deleting url from cache. Skipping.") - try: - cursor.execute("DELETE FROM texture WHERE url = ?", (url,)) - conn.commit() - except OperationalError: - log.debug("Issue deleting url from cache. Skipping.") - - finally: - cursor.close() def get_people_artwork(self, people): # append imageurl if existing diff --git a/resources/lib/context_entry.py b/resources/lib/context_entry.py index 882b0489..fa9a2d05 100644 --- a/resources/lib/context_entry.py +++ b/resources/lib/context_entry.py @@ -11,8 +11,10 @@ import api import read_embyserver as embyserver import embydb_functions as embydb import musicutils as musicutils -from utils import settings, dialog, language as lang, kodiSQL +from utils import settings, dialog, language as lang from dialogs import context +from database import DatabaseConn +from contextlib import closing ################################################################################################# @@ -87,15 +89,14 @@ class ContextMenu(object): if not item_id and kodi_id and item_type: - conn = kodiSQL('emby') - cursor = conn.cursor() - emby_db = embydb.Embydb_Functions(cursor) - item = emby_db.getItem_byKodiId(kodi_id, item_type) - cursor.close() - try: - item_id = item[0] - except TypeError: - pass + with DatabaseConn('emby') as conn: + with closing(conn.cursor()) as cursor: + emby_db = embydb.Embydb_Functions(cursor) + item = emby_db.getItem_byKodiId(kodi_id, item_type) + try: + item_id = item[0] + except TypeError: + pass return item_id @@ -164,31 +165,28 @@ class ContextMenu(object): def _rate_song(self): - conn = kodiSQL('music') - cursor = conn.cursor() - query = "SELECT rating FROM song WHERE idSong = ?" - cursor.execute(query, (self.kodi_id,)) - try: - value = cursor.fetchone()[0] - current_value = int(round(float(value), 0)) - except TypeError: - pass - else: - new_value = dialog("numeric", 0, lang(30411), str(current_value)) - if new_value > -1: + with DatabaseConn('music') as conn: + with closing(conn.cursor()) as cursor_music: + query = "SELECT rating FROM song WHERE idSong = ?" + cursor_music.execute(query, (self.kodi_id,)) + try: + value = cursor_music.fetchone()[0] + current_value = int(round(float(value), 0)) + except TypeError: + pass + else: + new_value = dialog("numeric", 0, lang(30411), str(current_value)) + if new_value > -1: - new_value = int(new_value) - if new_value > 5: - new_value = 5 + new_value = int(new_value) + if new_value > 5: + new_value = 5 - if settings('enableUpdateSongRating') == "true": - musicutils.updateRatingToFile(new_value, self.api.get_file_path()) + if settings('enableUpdateSongRating') == "true": + musicutils.updateRatingToFile(new_value, self.api.get_file_path()) - query = "UPDATE song SET rating = ? WHERE idSong = ?" - cursor.execute(query, (new_value, self.kodi_id,)) - conn.commit() - finally: - cursor.close() + query = "UPDATE song SET rating = ? WHERE idSong = ?" + cursor_music.execute(query, (new_value, self.kodi_id,)) def _delete_item(self): diff --git a/resources/lib/database.py b/resources/lib/database.py index 74eb564e..eccf2132 100644 --- a/resources/lib/database.py +++ b/resources/lib/database.py @@ -4,10 +4,17 @@ import logging import sqlite3 +from contextlib import closing +import sys +import traceback import xbmc +import xbmcaddon +import xbmcgui +import xbmcplugin +import xbmcvfs -from utils import window, should_stop +from utils import window, should_stop, settings, language, deletePlaylists, deleteNodes ################################################################################################# @@ -79,22 +86,26 @@ def kodi_commit(): class DatabaseConn(object): # To be called as context manager - i.e. with DatabaseConn() as conn: #dostuff - def __init__(self, database_file="video", commit_mode="", timeout=20): + def __init__(self, database_file="video", commit_on_close=True, timeout=120): """ database_file can be custom: emby, texture, music, video, :memory: or path to the file commit_mode set to None to autocommit (isolation_level). See python documentation. """ self.db_file = database_file - self.commit_mode = commit_mode + self.commit_on_close = commit_on_close self.timeout = timeout def __enter__(self): # Open the connection self.path = self._SQL(self.db_file) - log.info("opening database: %s", self.path) - self.conn = sqlite3.connect(self.path, - isolation_level=self.commit_mode, - timeout=self.timeout) + log.info("opening: %s", self.path) + #traceback.print_stack() + + if settings('dblock') == "true": + self.conn = sqlite3.connect(self.path, isolation_level=None, timeout=self.timeout) + else: + self.conn = sqlite3.connect(self.path, timeout=self.timeout) + return self.conn def _SQL(self, media_type): @@ -114,17 +125,102 @@ class DatabaseConn(object): if exc_type is not None: # Errors were raised in the with statement log.error("Type: %s Value: %s", exc_type, exc_val) - if "database is locked" in exc_val: - self.conn.rollback() - else: - raise - elif self.commit_mode is not None and changes: + if self.commit_on_close == True and changes: log.info("number of rows updated: %s", changes) - if self.db_file == "video" and kodi_commit(): - self.conn.commit() - else: - self.conn.commit() + if self.db_file == "video": + kodi_commit() + self.conn.commit() + log.info("commit: %s", self.path) - log.info("close: %s", self.path) + log.info("closing: %s", self.path) self.conn.close() + + +def db_reset(): + + dialog = xbmcgui.Dialog() + + if not dialog.yesno(language(29999), language(33074)): + return + + # first stop any db sync + window('emby_online', value="reset") + window('emby_shouldStop', value="true") + count = 10 + while window('emby_dbScan') == "true": + log.info("Sync is running, will retry: %s..." % count) + count -= 1 + if count == 0: + dialog.ok(language(29999), language(33085)) + return + xbmc.sleep(1000) + + # Clean up the playlists + deletePlaylists() + + # Clean up the video nodes + deleteNodes() + + # Wipe the kodi databases + log.warn("Resetting the Kodi video database.") + with DatabaseConn('video') as conn: + with closing(conn.cursor()) as cursor: + cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') + rows = cursor.fetchall() + for row in rows: + tablename = row[0] + if tablename != "version": + cursor.execute("DELETE FROM " + tablename) + + if settings('enableMusic') == "true": + log.warn("Resetting the Kodi music database.") + with DatabaseConn('music') as conn: + with closing(conn.cursor()) as cursor: + cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') + rows = cursor.fetchall() + for row in rows: + tablename = row[0] + if tablename != "version": + cursor.execute("DELETE FROM " + tablename) + + # Wipe the emby database + log.warn("Resetting the Emby database.") + with DatabaseConn('emby') as conn: + with closing(conn.cursor()) as cursor: + cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') + rows = cursor.fetchall() + for row in rows: + tablename = row[0] + if tablename != "version": + cursor.execute("DELETE FROM " + tablename) + cursor.execute('DROP table IF EXISTS emby') + cursor.execute('DROP table IF EXISTS view') + cursor.execute("DROP table IF EXISTS version") + + # Offer to wipe cached thumbnails + if dialog.yesno(language(29999), language(33086)): + log.warn("Resetting all cached artwork") + # Remove all existing textures first + import artwork + artwork.Artwork().delete_cache() + + # reset the install run flag + settings('SyncInstallRunDone', value="false") + + # Remove emby info + resp = dialog.yesno(language(29999), language(33087)) + if resp: + import connectmanager + # Delete the settings + addon = xbmcaddon.Addon() + addondir = xbmc.translatePath( + "special://profile/addon_data/plugin.video.emby/").decode('utf-8') + dataPath = "%ssettings.xml" % addondir + xbmcvfs.delete(dataPath) + connectmanager.ConnectManager().clear_data() + + dialog.ok(heading=language(29999), line1=language(33088)) + xbmc.executebuiltin('RestartApp') + return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, xbmcgui.ListItem()) + \ No newline at end of file diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index 488e76f5..a94d46e0 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -28,6 +28,8 @@ import playbackutils as pbutils import playutils import api from utils import window, settings, dialog, language as lang +from database import DatabaseConn +from contextlib import closing ################################################################################################# @@ -178,12 +180,10 @@ def emby_backup(): shutil.copy(src=xbmc.translatePath("special://database/emby.db").decode('utf-8'), dst=database) # Videos database - shutil.copy(src=utils.getKodiVideoDBPath(), - dst=database) + shutil.copy(src=DatabaseConn()._SQL('video'), dst=database) # Music database if settings('enableMusic') == "true": - shutil.copy(src=utils.getKodiMusicDBPath(), - dst=database) + shutil.copy(src=DatabaseConn()._SQL('music'), dst=database) dialog(type_="ok", heading="{emby}", @@ -234,11 +234,10 @@ def deleteItem(): log.info("Unknown type, unable to proceed.") return - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - item = emby_db.getItem_byKodiId(dbId, itemType) - embycursor.close() + with DatabaseConn('emby') as conn: + with closing(conn.cursor()) as cursor: + emby_db = embydb.Embydb_Functions(cursor) + item = emby_db.getItem_byKodiId(dbId, itemType) try: itemId = item[0] @@ -422,11 +421,10 @@ def getThemeMedia(): return # Get every user view Id - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - viewids = emby_db.getViews() - embycursor.close() + with DatabaseConn('emby') as conn: + with closing(conn.cursor()) as cursor: + emby_db = embydb.Embydb_Functions(cursor) + viewids = emby_db.getViews() # Get Ids with Theme Videos itemIds = {} diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 6dda8b38..d4645afc 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -6,7 +6,9 @@ import logging import read_embyserver as embyserver from objects import Movies, MusicVideos, TVShows, Music -from utils import settings, kodiSQL +from utils import settings +from database import DatabaseConn +from contextlib import closing ################################################################################################# @@ -58,46 +60,40 @@ class Items(object): if pdialog: pdialog.update(heading="Processing %s: %s items" % (process, total)) - for itemtype in items: + # this is going to open a music connection even if it is not needed but + # I feel that is better than trying to sort out the login yourself + with DatabaseConn('music') as conn: + with closing(conn.cursor()) as cursor_music: + + for itemtype in items: - # Safety check - if not itemtypes.get(itemtype): - # We don't process this type of item - continue + # Safety check + if not itemtypes.get(itemtype): + # We don't process this type of item + continue - itemlist = items[itemtype] - if not itemlist: - # The list to process is empty - continue + itemlist = items[itemtype] + if not itemlist: + # The list to process is empty + continue - musicconn = None + if itemtype in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): + if self.music_enabled: + items_process = itemtypes[itemtype](embycursor, cursor_music, pdialog) # see note above + else: + # Music is not enabled, do not proceed with itemtype + continue + else: + update_videolibrary = True + items_process = itemtypes[itemtype](embycursor, kodicursor, pdialog) - if itemtype in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): - if self.music_enabled: - musicconn = kodiSQL('music') - musiccursor = musicconn.cursor() - items_process = itemtypes[itemtype](embycursor, musiccursor, pdialog) - else: - # Music is not enabled, do not proceed with itemtype - continue - else: - update_videolibrary = True - items_process = itemtypes[itemtype](embycursor, kodicursor, pdialog) + if process == "added": + items_process.add_all(itemtype, itemlist) + elif process == "remove": + items_process.remove_all(itemtype, itemlist) + else: + process_items = self.emby.getFullItems(itemlist) + items_process.process_all(itemtype, process, process_items, total) - if process == "added": - items_process.add_all(itemtype, itemlist) - elif process == "remove": - items_process.remove_all(itemtype, itemlist) - else: - process_items = self.emby.getFullItems(itemlist) - items_process.process_all(itemtype, process, process_items, total) - - - if musicconn is not None: - # close connection for special types - log.info("updating music database") - musicconn.commit() - musiccursor.close() - return (True, update_videolibrary) diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index a66e790b..c0a9bd72 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -11,8 +11,10 @@ import xbmcgui import downloadutils import embydb_functions as embydb import playbackutils as pbutils -from utils import window, settings, kodiSQL +from utils import window, settings from ga_client import log_error +from database import DatabaseConn +from contextlib import closing ################################################################################################# @@ -165,11 +167,10 @@ class KodiMonitor(xbmc.Monitor): item_id = None - conn = kodiSQL('emby') - cursor = conn.cursor() - emby_db = embydb.Embydb_Functions(cursor) - db_item = emby_db.getItem_byKodiId(kodi_id, item_type) - cursor.close() + with DatabaseConn('emby') as conn: + with closing(conn.cursor()) as cursor: + emby_db = embydb.Embydb_Functions(cursor) + db_item = emby_db.getItem_byKodiId(kodi_id, item_type) try: item_id = db_item[0] diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 2704fe65..94b52997 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -12,7 +12,6 @@ import xbmcgui import xbmcvfs import api -import database import utils import clientinfo import downloadutils @@ -25,6 +24,8 @@ import videonodes from objects import Movies, MusicVideos, TVShows, Music from utils import window, settings, language as lang, should_stop from ga_client import GoogleAnalytics +from database import DatabaseConn +from contextlib import closing ################################################################################################## @@ -56,7 +57,6 @@ class LibrarySync(threading.Thread): self.monitor = xbmc.Monitor() self.clientInfo = clientinfo.ClientInfo() - self.database = database.DatabaseConn self.doUtils = downloadutils.DownloadUtils().downloadUrl self.user = userclient.UserClient() self.emby = embyserver.Read_EmbyServer() @@ -234,123 +234,110 @@ class LibrarySync(threading.Thread): # Add sources utils.sourcesXML() - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - # content sync: movies, tvshows, musicvideos, music - kodiconn = utils.kodiSQL('video') - kodicursor = kodiconn.cursor() + # use emby and video DBs + with DatabaseConn('emby') as conn_emby, DatabaseConn('video') as conn_video: + with closing(conn_emby.cursor()) as cursor_emby, closing(conn_video.cursor()) as cursor_video: + # content sync: movies, tvshows, musicvideos, music - if manualrun: - message = "Manual sync" - elif repair: - message = "Repair sync" - repair_list = [] - choices = ['all', 'movies', 'musicvideos', 'tvshows'] - if music_enabled: - choices.append('music') + if manualrun: + message = "Manual sync" + elif repair: + message = "Repair sync" + repair_list = [] + choices = ['all', 'movies', 'musicvideos', 'tvshows'] + if music_enabled: + choices.append('music') - if self.kodi_version > 15: - # Jarvis or higher - types = xbmcgui.Dialog().multiselect(lang(33094), choices) - if types is None: - pass - elif 0 in types: # all - choices.pop(0) - repair_list.extend(choices) + if self.kodi_version > 15: + # Jarvis or higher + types = xbmcgui.Dialog().multiselect(lang(33094), choices) + if types is None: + pass + elif 0 in types: # all + choices.pop(0) + repair_list.extend(choices) + else: + for index in types: + repair_list.append(choices[index]) + else: + resp = xbmcgui.Dialog().select(lang(33094), choices) + if resp == 0: # all + choices.pop(resp) + repair_list.extend(choices) + else: + repair_list.append(choices[resp]) + + log.info("Repair queued for: %s", repair_list) else: - for index in types: - repair_list.append(choices[index]) - else: - resp = xbmcgui.Dialog().select(lang(33094), choices) - if resp == 0: # all - choices.pop(resp) - repair_list.extend(choices) - else: - repair_list.append(choices[resp]) + message = "Initial sync" + window('emby_initialScan', value="true") - log.info("Repair queued for: %s", repair_list) - else: - message = "Initial sync" - window('emby_initialScan', value="true") + pDialog = self.progressDialog("%s" % message) + starttotal = datetime.now() - pDialog = self.progressDialog("%s" % message) - starttotal = datetime.now() + # Set views + self.maintainViews(cursor_emby, cursor_video) - # Set views - self.maintainViews(embycursor, kodicursor) - embyconn.commit() + # Sync video library + process = { - # Sync video library - process = { + 'movies': self.movies, + 'musicvideos': self.musicvideos, + 'tvshows': self.tvshows + } + for itemtype in process: - 'movies': self.movies, - 'musicvideos': self.musicvideos, - 'tvshows': self.tvshows - } - for itemtype in process: + if repair and itemtype not in repair_list: + continue - if repair and itemtype not in repair_list: - continue + startTime = datetime.now() + completed = process[itemtype](cursor_emby, cursor_video, pDialog) + if not completed: + xbmc.executebuiltin('InhibitIdleShutdown(false)') + utils.setScreensaver(value=screensaver) + window('emby_dbScan', clear=True) + if pDialog: + pDialog.close() - startTime = datetime.now() - completed = process[itemtype](embycursor, kodicursor, pDialog) - if not completed: - xbmc.executebuiltin('InhibitIdleShutdown(false)') - utils.setScreensaver(value=screensaver) - window('emby_dbScan', clear=True) - if pDialog: - pDialog.close() - - embycursor.close() - kodicursor.close() - return False - else: - self.dbCommit(kodiconn) - embyconn.commit() - elapsedTime = datetime.now() - startTime - log.info("SyncDatabase (finished %s in: %s)" - % (itemtype, str(elapsedTime).split('.')[0])) - else: - # Close the Kodi cursor - kodicursor.close() + return False + else: + elapsedTime = datetime.now() - startTime + log.info("SyncDatabase (finished %s in: %s)" + % (itemtype, str(elapsedTime).split('.')[0])) + # sync music - if music_enabled: - + # use emby and music + if music_enabled: if repair and 'music' not in repair_list: pass else: - musicconn = utils.kodiSQL('music') - musiccursor = musicconn.cursor() + with DatabaseConn('emby') as conn_emby, DatabaseConn('music') as conn_music: + with closing(conn_emby.cursor()) as cursor_emby, closing(conn_music.cursor()) as cursor_music: + startTime = datetime.now() + completed = self.music(cursor_emby, cursor_music, pDialog) + if not completed: + xbmc.executebuiltin('InhibitIdleShutdown(false)') + utils.setScreensaver(value=screensaver) + window('emby_dbScan', clear=True) + if pDialog: + pDialog.close() - startTime = datetime.now() - completed = self.music(embycursor, musiccursor, pDialog) - if not completed: - xbmc.executebuiltin('InhibitIdleShutdown(false)') - utils.setScreensaver(value=screensaver) - window('emby_dbScan', clear=True) - if pDialog: - pDialog.close() - - embycursor.close() - musiccursor.close() - return False - else: - musicconn.commit() - embyconn.commit() - elapsedTime = datetime.now() - startTime - log.info("SyncDatabase (finished music in: %s)" - % (str(elapsedTime).split('.')[0])) - musiccursor.close() + return False + else: + elapsedTime = datetime.now() - startTime + log.info("SyncDatabase (finished music in: %s)" + % (str(elapsedTime).split('.')[0])) if pDialog: pDialog.close() - emby_db = embydb.Embydb_Functions(embycursor) - current_version = emby_db.get_version(self.clientInfo.get_version()) + with DatabaseConn('emby') as conn_emby: + with closing(conn_emby.cursor()) as cursor_emby: + emby_db = embydb.Embydb_Functions(cursor_emby) + current_version = emby_db.get_version(self.clientInfo.get_version()) + window('emby_version', current_version) - embyconn.commit() - embycursor.close() settings('SyncInstallRunDone', value="true") @@ -374,20 +361,12 @@ class LibrarySync(threading.Thread): def refreshViews(self): + + with DatabaseConn('emby') as conn_emby, DatabaseConn('video') as conn_video: + with closing(conn_emby.cursor()) as cursor_emby, closing(conn_video.cursor()) as cursor_video: + # Compare views, assign correct tags to items + self.maintainViews(cursor_emby, cursor_video) - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - kodiconn = utils.kodiSQL('video') - kodicursor = kodiconn.cursor() - - # Compare views, assign correct tags to items - self.maintainViews(embycursor, kodicursor) - - self.dbCommit(kodiconn) - kodicursor.close() - - embyconn.commit() - embycursor.close() def maintainViews(self, embycursor, kodicursor): @@ -735,97 +714,90 @@ class LibrarySync(threading.Thread): processlist[process].extend(items) def incrementalSync(self): - - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - kodiconn = utils.kodiSQL('video') - kodicursor = kodiconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - pDialog = None - update_embydb = False - - if self.refresh_views: - # Received userconfig update - self.refresh_views = False - self.maintainViews(embycursor, kodicursor) - embycursor.commit() - self.forceLibraryUpdate = True - update_embydb = True - - incSyncIndicator = int(settings('incSyncIndicator') or 10) - totalUpdates = len(self.addedItems) + len(self.updateItems) + len(self.userdataItems) + len(self.removeItems) - if incSyncIndicator != -1 and totalUpdates > incSyncIndicator: - # Only present dialog if we are going to process items - pDialog = self.progressDialog('Incremental sync') - log.info("incSyncIndicator=" + str(incSyncIndicator) + " totalUpdates=" + str(totalUpdates)) + with DatabaseConn('emby') as conn_emby, DatabaseConn('video') as conn_video: + with closing(conn_emby.cursor()) as cursor_emby, closing(conn_video.cursor()) as cursor_video: + + emby_db = embydb.Embydb_Functions(cursor_emby) + pDialog = None + update_embydb = False - process = { + if self.refresh_views: + # Received userconfig update + self.refresh_views = False + self.maintainViews(cursor_emby, cursor_video) + self.forceLibraryUpdate = True + update_embydb = True - 'added': self.addedItems, - 'update': self.updateItems, - 'userdata': self.userdataItems, - 'remove': self.removeItems - } - for process_type in ['added', 'update', 'userdata', 'remove']: + incSyncIndicator = int(settings('incSyncIndicator') or 10) + totalUpdates = len(self.addedItems) + len(self.updateItems) + len(self.userdataItems) + len(self.removeItems) + + if incSyncIndicator != -1 and totalUpdates > incSyncIndicator: + # Only present dialog if we are going to process items + pDialog = self.progressDialog('Incremental sync') + log.info("incSyncIndicator=" + str(incSyncIndicator) + " totalUpdates=" + str(totalUpdates)) - if process[process_type] and window('emby_kodiScan') != "true": + process = { - listItems = list(process[process_type]) - del process[process_type][:] # Reset class list + 'added': self.addedItems, + 'update': self.updateItems, + 'userdata': self.userdataItems, + 'remove': self.removeItems + } + for process_type in ['added', 'update', 'userdata', 'remove']: - items_process = itemtypes.Items(embycursor, kodicursor) - update = False + if process[process_type] and window('emby_kodiScan') != "true": - # Prepare items according to process process_type - if process_type == "added": - items = self.emby.sortby_mediatype(listItems) + listItems = list(process[process_type]) + del process[process_type][:] # Reset class list - elif process_type in ("userdata", "remove"): - items = emby_db.sortby_mediaType(listItems, unsorted=False) + items_process = itemtypes.Items(cursor_emby, cursor_video) + update = False - else: - items = emby_db.sortby_mediaType(listItems) - if items.get('Unsorted'): - sorted_items = self.emby.sortby_mediatype(items['Unsorted']) - doupdate = items_process.itemsbyId(sorted_items, "added", pDialog) + # Prepare items according to process process_type + if process_type == "added": + items = self.emby.sortby_mediatype(listItems) + + elif process_type in ("userdata", "remove"): + items = emby_db.sortby_mediaType(listItems, unsorted=False) + + else: + items = emby_db.sortby_mediaType(listItems) + if items.get('Unsorted'): + sorted_items = self.emby.sortby_mediatype(items['Unsorted']) + doupdate = items_process.itemsbyId(sorted_items, "added", pDialog) + if doupdate: + embyupdate, kodiupdate_video = doupdate + if embyupdate: + update_embydb = True + if kodiupdate_video: + self.forceLibraryUpdate = True + del items['Unsorted'] + + doupdate = items_process.itemsbyId(items, process_type, pDialog) if doupdate: embyupdate, kodiupdate_video = doupdate if embyupdate: update_embydb = True if kodiupdate_video: self.forceLibraryUpdate = True - del items['Unsorted'] - doupdate = items_process.itemsbyId(items, process_type, pDialog) - if doupdate: - embyupdate, kodiupdate_video = doupdate - if embyupdate: - update_embydb = True - if kodiupdate_video: - self.forceLibraryUpdate = True + if update_embydb: + update_embydb = False + log.info("Updating emby database.") + self.saveLastSync() - if update_embydb: - update_embydb = False - log.info("Updating emby database.") - embyconn.commit() - self.saveLastSync() + if self.forceLibraryUpdate: + # Force update the Kodi library + self.forceLibraryUpdate = False - if self.forceLibraryUpdate: - # Force update the Kodi library - self.forceLibraryUpdate = False - self.dbCommit(kodiconn) - - log.info("Updating video library.") - window('emby_kodiScan', value="true") - xbmc.executebuiltin('UpdateLibrary(video)') + log.info("Updating video library.") + window('emby_kodiScan', value="true") + xbmc.executebuiltin('UpdateLibrary(video)') if pDialog: pDialog.close() - kodicursor.close() - embycursor.close() - def compareDBVersion(self, current, minimum): # It returns True is database is up to date. False otherwise. @@ -848,18 +820,18 @@ class LibrarySync(threading.Thread): def _verify_emby_database(self): # Create the tables for the emby database - with self.database('emby') as conn: - cursor = conn.cursor() - # emby, view, version - cursor.execute( - """CREATE TABLE IF NOT EXISTS emby( - emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, - kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, - checksum INTEGER)""") - cursor.execute( - """CREATE TABLE IF NOT EXISTS view( - view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""") - cursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") + with DatabaseConn('emby') as conn: + with closing(conn.cursor()) as cursor: + # emby, view, version + cursor.execute( + """CREATE TABLE IF NOT EXISTS emby( + emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, + kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, + checksum INTEGER)""") + cursor.execute( + """CREATE TABLE IF NOT EXISTS view( + view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""") + cursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)") def run(self): @@ -906,18 +878,18 @@ class LibrarySync(threading.Thread): if (window('emby_dbCheck') != "true" and settings('SyncInstallRunDone') == "true"): # Verify the validity of the database + log.info("Doing DB Version Check") + with DatabaseConn('emby') as conn: + with closing(conn.cursor()) as cursor: + + emby_db = embydb.Embydb_Functions(cursor) + currentVersion = emby_db.get_version() + ###$ Begin migration $### + if not currentVersion: + currentVersion = emby_db.get_version(settings('dbCreatedWithVersion') or self.clientInfo.get_version()) + log.info("Migration of database version completed") + ###$ End migration $### - embyconn = utils.kodiSQL('emby') - embycursor = embyconn.cursor() - emby_db = embydb.Embydb_Functions(embycursor) - currentVersion = emby_db.get_version() - ###$ Begin migration $### - if not currentVersion: - currentVersion = emby_db.get_version(settings('dbCreatedWithVersion') or self.clientInfo.get_version()) - embyconn.commit() - log.info("Migration of database version completed") - ###$ End migration $### - embycursor.close() window('emby_version', value=currentVersion) minVersion = window('emby_minDBVersion') @@ -932,7 +904,7 @@ class LibrarySync(threading.Thread): log.warn("Database version is out of date! USER IGNORED!") dialog.ok(lang(29999), lang(33023)) else: - utils.reset() + database.db_reset() break @@ -941,7 +913,7 @@ class LibrarySync(threading.Thread): if not startupComplete: # Verify the video database can be found - videoDb = utils.getKodiVideoDBPath() + videoDb = DatabaseConn()._SQL('video') if not xbmcvfs.exists(videoDb): # Database does not exists log.error( diff --git a/resources/lib/playlist.py b/resources/lib/playlist.py index 0a534c06..82137c0f 100644 --- a/resources/lib/playlist.py +++ b/resources/lib/playlist.py @@ -11,7 +11,9 @@ import playutils import playbackutils import embydb_functions as embydb import read_embyserver as embyserver -from utils import window, kodiSQL, JSONRPC +from utils import window, JSONRPC +from database import DatabaseConn +from contextlib import closing ################################################################################################# @@ -29,77 +31,76 @@ class Playlist(object): def play_all(self, item_ids, start_at): - conn = kodiSQL('emby') - cursor = conn.cursor() - emby_db = embydb.Embydb_Functions(cursor) + with DatabaseConn('emby') as conn: + with closing(conn.cursor()) as cursor: + emby_db = embydb.Embydb_Functions(cursor) - player = xbmc.Player() - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - playlist.clear() + player = xbmc.Player() + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + playlist.clear() - log.info("---*** PLAY ALL ***---") - log.info("Items: %s and start at: %s", item_ids, start_at) + log.info("---*** PLAY ALL ***---") + log.info("Items: %s and start at: %s", item_ids, start_at) - started = False - window('emby_customplaylist', value="true") + started = False + window('emby_customplaylist', value="true") - if start_at: - # Seek to the starting position - window('emby_customplaylist.seektime', str(start_at)) + if start_at: + # Seek to the starting position + window('emby_customplaylist.seektime', str(start_at)) - for item_id in item_ids: + for item_id in item_ids: - log.info("Adding %s to playlist", item_id) - item = emby_db.getItem_byId(item_id) - try: - db_id = item[0] - media_type = item[4] + log.info("Adding %s to playlist", item_id) + item = emby_db.getItem_byId(item_id) + try: + db_id = item[0] + media_type = item[4] - except TypeError: - # Item is not found in our database, add item manually - log.info("Item was not found in the database, manually adding item") - item = self.emby.getItem(item_id) - self.add_to_xbmc_playlist(playlist, item) + except TypeError: + # Item is not found in our database, add item manually + log.info("Item was not found in the database, manually adding item") + item = self.emby.getItem(item_id) + self.add_to_xbmc_playlist(playlist, item) - else: # Add to playlist - self.add_to_playlist(db_id, media_type) + else: # Add to playlist + self.add_to_playlist(db_id, media_type) - if not started: - started = True - player.play(playlist) + if not started: + started = True + player.play(playlist) - self.verify_playlist() - cursor.close() + self.verify_playlist() def modify_playlist(self, item_ids): - conn = kodiSQL('emby') - cursor = conn.cursor() - emby_db = embydb.Embydb_Functions(cursor) + with DatabaseConn('emby') as conn: + with closing(conn.cursor()) as cursor: + emby_db = embydb.Embydb_Functions(cursor) - log.info("---*** ADD TO PLAYLIST ***---") - log.info("Items: %s", item_ids) + log.info("---*** ADD TO PLAYLIST ***---") + log.info("Items: %s", item_ids) - playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) - for item_id in item_ids: + for item_id in item_ids: - log.info("Adding %s to playlist", item_id) - item = emby_db.getItem_byId(item_id) - try: - db_id = item[0] - media_type = item[4] + log.info("Adding %s to playlist", item_id) + item = emby_db.getItem_byId(item_id) + try: + db_id = item[0] + media_type = item[4] - except TypeError: - # Item is not found in our database, add item manually - item = self.emby.getItem(item_id) - self.add_to_xbmc_playlist(playlist, item) + except TypeError: + # Item is not found in our database, add item manually + item = self.emby.getItem(item_id) + self.add_to_xbmc_playlist(playlist, item) - else: # Add to playlist - self.add_to_playlist(db_id, media_type) + else: # Add to playlist + self.add_to_playlist(db_id, media_type) - self.verify_playlist() - cursor.close() + self.verify_playlist() + return playlist @classmethod diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py index 0b96db16..b201e807 100644 --- a/resources/lib/read_embyserver.py +++ b/resources/lib/read_embyserver.py @@ -8,7 +8,9 @@ import hashlib import xbmc import downloadutils -from utils import window, settings, kodiSQL +from utils import window, settings +from database import DatabaseConn +from contextlib import closing ################################################################################################# @@ -102,24 +104,23 @@ class Read_EmbyServer(): viewId = view['Id'] # Compare to view table in emby database - emby = kodiSQL('emby') - cursor_emby = emby.cursor() - query = ' '.join(( + with DatabaseConn('emby') as conn: + with closing(conn.cursor()) as cursor: + query = ' '.join(( - "SELECT view_name, media_type", - "FROM view", - "WHERE view_id = ?" - )) - cursor_emby.execute(query, (viewId,)) - result = cursor_emby.fetchone() - try: - viewName = result[0] - mediatype = result[1] - except TypeError: - viewName = None - mediatype = None + "SELECT view_name, media_type", + "FROM view", + "WHERE view_id = ?" + )) + cursor.execute(query, (viewId,)) + result = cursor.fetchone() + try: + viewName = result[0] + mediatype = result[1] + except TypeError: + viewName = None + mediatype = None - cursor_emby.close() return [viewName, viewId, mediatype] diff --git a/resources/lib/utils.py b/resources/lib/utils.py index b4145e76..9191c702 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -2,7 +2,6 @@ ################################################################################################# - import inspect import json import logging @@ -15,6 +14,7 @@ import unicodedata import xml.etree.ElementTree as etree from datetime import datetime + import xbmc import xbmcaddon import xbmcgui @@ -120,55 +120,6 @@ def should_stop(): else: # Keep going return False -def kodiSQL(media_type="video"): - - if media_type == "emby": - dbPath = xbmc.translatePath("special://database/emby.db").decode('utf-8') - elif media_type == "texture": - dbPath = xbmc.translatePath("special://database/Textures13.db").decode('utf-8') - elif media_type == "music": - dbPath = getKodiMusicDBPath() - else: - dbPath = getKodiVideoDBPath() - - if settings('dblock') == "true": - connection = sqlite3.connect(dbPath, isolation_level=None, timeout=20) - else: - connection = sqlite3.connect(dbPath, timeout=20) - return connection - -def getKodiVideoDBPath(): - - dbVersion = { - - "13": 78, # Gotham - "14": 90, # Helix - "15": 93, # Isengard - "16": 99, # Jarvis - "17": 107 # Krypton - } - - dbPath = xbmc.translatePath( - "special://database/MyVideos%s.db" - % dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")).decode('utf-8') - return dbPath - -def getKodiMusicDBPath(): - - dbVersion = { - - "13": 46, # Gotham - "14": 48, # Helix - "15": 52, # Isengard - "16": 56, # Jarvis - "17": 60 # Krypton - } - - dbPath = xbmc.translatePath( - "special://database/MyMusic%s.db" - % dbVersion.get(xbmc.getInfoLabel('System.BuildVersion')[:2], "")).decode('utf-8') - return dbPath - ################################################################################################# # Utility methods @@ -280,99 +231,6 @@ def profiling(sortby="cumulative"): ################################################################################################# # Addon utilities -def reset(): - - dialog = xbmcgui.Dialog() - - if not dialog.yesno(language(29999), language(33074)): - return - - # first stop any db sync - window('emby_online', value="reset") - window('emby_shouldStop', value="true") - count = 10 - while window('emby_dbScan') == "true": - log.info("Sync is running, will retry: %s..." % count) - count -= 1 - if count == 0: - dialog.ok(language(29999), language(33085)) - return - xbmc.sleep(1000) - - # Clean up the playlists - deletePlaylists() - - # Clean up the video nodes - deleteNodes() - - # Wipe the kodi databases - log.warn("Resetting the Kodi video database.") - connection = kodiSQL('video') - cursor = connection.cursor() - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tablename = row[0] - if tablename != "version": - cursor.execute("DELETE FROM " + tablename) - connection.commit() - cursor.close() - - if settings('enableMusic') == "true": - log.warn("Resetting the Kodi music database.") - connection = kodiSQL('music') - cursor = connection.cursor() - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tablename = row[0] - if tablename != "version": - cursor.execute("DELETE FROM " + tablename) - connection.commit() - cursor.close() - - # Wipe the emby database - log.warn("Resetting the Emby database.") - connection = kodiSQL('emby') - cursor = connection.cursor() - cursor.execute('SELECT tbl_name FROM sqlite_master WHERE type="table"') - rows = cursor.fetchall() - for row in rows: - tablename = row[0] - if tablename != "version": - cursor.execute("DELETE FROM " + tablename) - cursor.execute('DROP table IF EXISTS emby') - cursor.execute('DROP table IF EXISTS view') - cursor.execute("DROP table IF EXISTS version") - connection.commit() - cursor.close() - - # Offer to wipe cached thumbnails - if dialog.yesno(language(29999), language(33086)): - log.warn("Resetting all cached artwork") - # Remove all existing textures first - import artwork - artwork.Artwork().delete_cache() - - # reset the install run flag - settings('SyncInstallRunDone', value="false") - - # Remove emby info - resp = dialog.yesno(language(29999), language(33087)) - if resp: - import connectmanager - # Delete the settings - addon = xbmcaddon.Addon() - addondir = xbmc.translatePath( - "special://profile/addon_data/plugin.video.emby/").decode('utf-8') - dataPath = "%ssettings.xml" % addondir - xbmcvfs.delete(dataPath) - connectmanager.ConnectManager().clear_data() - - dialog.ok(heading=language(29999), line1=language(33088)) - xbmc.executebuiltin('RestartApp') - return xbmcplugin.setResolvedUrl(int(sys.argv[1]), False, xbmcgui.ListItem()) - def sourcesXML(): # To make Master lock compatible path = xbmc.translatePath("special://profile/").decode('utf-8')