import ast
import contextlib
import os
import sqlite3 as database
import time
import xbmcaddon
from xbmcvfs import translatePath
from lib.utils.kodi.utils import (
clear_property,
delete_file,
dialog_ok,
get_property,
notification,
set_property,
translation,
)
userdata_path = translatePath(xbmcaddon.Addon().getAddonInfo("profile"))
databases_path = os.path.join(userdata_path, "databases/")
database_path_raw = os.path.join(userdata_path, "databases")
trakt_db = translatePath(os.path.join(database_path_raw, "traktcache.db"))
lists_db = translatePath(os.path.join(database_path_raw, "lists.db"))
maincache_db = translatePath(os.path.join(database_path_raw, "maincache.db"))
paginator_db = translatePath(os.path.join(database_path_raw, "paginator.db"))
database_timeout = 20
database_locations = {
"trakt_db": trakt_db,
"lists_db": lists_db,
"maincache_db": maincache_db,
"paginator_db": paginator_db,
}
integrity_check = {
"trakt_db": ("trakt_data", "watched_status", "progress"),
"maincache_db": ("maincache",),
"lists_db": ("lists",),
}
table_creators = {
"trakt_db": (
"CREATE TABLE IF NOT EXISTS trakt_data (id text unique, data text)",
"CREATE TABLE IF NOT EXISTS watched \
(db_type text not null, media_id text not null, season integer, episode integer, last_played text, title text, unique (db_type, media_id, season, episode))",
"CREATE TABLE IF NOT EXISTS progress \
(db_type text not null, media_id text not null, season integer, episode integer, resume_point text, curr_time text, \
last_played text, resume_id integer, title text, unique (db_type, media_id, season, episode))",
"CREATE TABLE IF NOT EXISTS watched_status (db_type text not null, media_id text not null, status text, unique (db_type, media_id))",
),
"lists_db": ("CREATE TABLE IF NOT EXISTS lists (id text unique, data text, expires integer)",),
"maincache_db": (
"CREATE TABLE IF NOT EXISTS maincache (id text unique, data text, expires integer)",
),
"paginator_db": (
"CREATE TABLE IF NOT EXISTS paginated_data (id text unique, page_number integer, data text, total_pages integer)",
),
}
media_prop = "jacktook.%s"
BASE_GET = "SELECT expires, data FROM %s WHERE id = ?"
BASE_SET = "INSERT OR REPLACE INTO %s(id, data, expires) VALUES (?, ?, ?)"
BASE_DELETE = "DELETE FROM %s WHERE id = ?"
def make_database(database_name):
dbcon = database.connect(database_locations[database_name])
for command in table_creators[database_name]:
dbcon.execute(command)
dbcon.close()
def setup_databases():
if not os.path.exists(databases_path):
os.makedirs(databases_path)
for database_name, database_location in database_locations.items():
dbcon = database.connect(database_location)
for command in table_creators[database_name]:
dbcon.execute(command)
def connect_database(database_name):
dbcon = database.connect(
database_locations[database_name],
timeout=database_timeout,
isolation_level=None,
check_same_thread=False,
)
dbcon.execute("PRAGMA synchronous = OFF")
dbcon.execute("PRAGMA journal_mode = OFF")
return dbcon
def get_timestamp(offset=0):
# Offset is in HOURS multiply by 3600 to get seconds
return int(time.time()) + (offset * 3600)
def check_databases_integrity():
def _process(database_name, tables):
database_location = database_locations[database_name]
dbcon = None
try:
dbcon = database.connect(database_location)
for db_table in tables:
dbcon.execute(command_base % db_table)
except Exception:
database_errors.append(database_name)
if os.path.exists(database_location):
with contextlib.suppress(BaseException):
dbcon.close()
delete_file(database_location)
command_base = "SELECT * FROM %s LIMIT 1"
database_errors = []
for database_name, tables in integrity_check.items():
_process(database_name, tables)
setup_databases()
if database_errors:
dialog_ok(
heading=translation(90432),
line1=translation(90433) % ", ".join(database_errors),
)
else:
notification(translation(90426), time=3000)
class BaseCache:
def __init__(self, dbfile, table):
self.table = table
self.dbfile = dbfile
def get(self, string):
result = None
try:
current_time = get_timestamp()
result = self.get_memory_cache(string, current_time)
if result is None:
dbcon = connect_database(self.dbfile)
cache_data = dbcon.execute(BASE_GET % self.table, (string,)).fetchone()
if cache_data:
if cache_data[0] > current_time:
result = self._deserialize(cache_data[1])
self.set_memory_cache(result, string, cache_data[0])
else:
self.delete(string)
except Exception:
pass
return result
def set(self, string, data, expiration=720):
try:
dbcon = connect_database(self.dbfile)
expires = get_timestamp(expiration)
dbcon.execute(BASE_SET % self.table, (string, self._serialize(data), int(expires)))
self.set_memory_cache(data, string, int(expires))
except Exception:
return None
def get_memory_cache(self, string, current_time):
result = None
try:
cachedata = get_property(media_prop % string)
if cachedata:
cachedata = self._deserialize(cachedata)
if cachedata[0] > current_time:
result = cachedata[1]
except Exception:
pass
return result
def set_memory_cache(self, data, string, expires):
try:
cachedata = (expires, data)
cachedata_repr = self._serialize(cachedata)
set_property(media_prop % string, cachedata_repr)
except Exception:
pass
@staticmethod
def _serialize(data):
return repr(data)
@staticmethod
def _deserialize(data):
return ast.literal_eval(data)
def delete(self, string):
try:
dbcon = connect_database(self.dbfile)
dbcon.execute(BASE_DELETE % self.table, (string,))
self.delete_memory_cache(string)
except Exception:
pass
def delete_memory_cache(self, string):
clear_property(media_prop % string)
def manual_connect(self, dbfile):
return connect_database(dbfile)