mirror of
https://github.com/jellyfin/jellyfin-kodi.git
synced 2024-12-25 10:16:11 +00:00
Odd Stråbø
a6241d25db
and dialog line1 to message parameter rename. The isPassword change likely bumps minimum version up to Kodi 18. This can be worked around if desirable.
444 lines
14 KiB
Python
444 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
from __future__ import division, absolute_import, print_function, unicode_literals
|
|
#################################################################################################
|
|
|
|
import datetime
|
|
import logging
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
|
|
from kodi_six import xbmc, xbmcvfs
|
|
from six import text_type
|
|
|
|
from database import jellyfin_db
|
|
from helper import translate, settings, window, dialog
|
|
from objects import obj
|
|
from helper import LazyLogger
|
|
|
|
#################################################################################################
|
|
|
|
LOG = LazyLogger(__name__)
|
|
|
|
ADDON_DATA = xbmc.translatePath("special://profile/addon_data/plugin.video.jellyfin/")
|
|
|
|
#################################################################################################
|
|
|
|
|
|
class Database(object):
|
|
|
|
''' This should be called like a context.
|
|
i.e. with Database('jellyfin') as db:
|
|
db.cursor
|
|
db.conn.commit()
|
|
'''
|
|
timeout = 120
|
|
discovered = False
|
|
discovered_file = None
|
|
|
|
def __init__(self, file=None, commit_close=True):
|
|
|
|
''' file: jellyfin, texture, music, video, :memory: or path to file
|
|
'''
|
|
self.db_file = file or "video"
|
|
self.commit_close = commit_close
|
|
|
|
def __enter__(self):
|
|
|
|
''' Open the connection and return the Database class.
|
|
This is to allow for the cursor, conn and others to be accessible.
|
|
'''
|
|
self.path = self._sql(self.db_file)
|
|
self.conn = sqlite3.connect(self.path, timeout=self.timeout)
|
|
self.cursor = self.conn.cursor()
|
|
|
|
if self.db_file in ('video', 'music', 'texture', 'jellyfin'):
|
|
self.conn.execute("PRAGMA journal_mode=WAL") # to avoid writing conflict with kodi
|
|
|
|
LOG.debug("--->[ database: %s ] %s", self.db_file, id(self.conn))
|
|
|
|
if not window('jellyfin_db_check.bool') and self.db_file == 'jellyfin':
|
|
|
|
window('jellyfin_db_check.bool', True)
|
|
jellyfin_tables(self.cursor)
|
|
self.conn.commit()
|
|
|
|
# Migration for #162
|
|
if self.db_file == 'music':
|
|
query = self.conn.execute('SELECT * FROM path WHERE strPath LIKE "%/emby/%"')
|
|
contents = query.fetchall()
|
|
if contents:
|
|
for item in contents:
|
|
new_path = item[1].replace('/emby/', '/')
|
|
self.conn.execute('UPDATE path SET strPath = "{}" WHERE idPath = "{}"'.format(new_path, item[0]))
|
|
|
|
return self
|
|
|
|
def _get_database(self, path, silent=False):
|
|
|
|
path = xbmc.translatePath(path)
|
|
|
|
if not silent:
|
|
|
|
if not xbmcvfs.exists(path):
|
|
raise Exception("Database: %s missing" % path)
|
|
|
|
conn = sqlite3.connect(path)
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
|
tables = cursor.fetchall()
|
|
conn.close()
|
|
|
|
if not len(tables):
|
|
raise Exception("Database: %s malformed?" % path)
|
|
|
|
return path
|
|
|
|
def _discover_database(self, database):
|
|
|
|
''' Use UpdateLibrary(video) to update the date modified
|
|
on the database file used by Kodi.
|
|
'''
|
|
if database == 'video':
|
|
|
|
xbmc.executebuiltin('UpdateLibrary(video)')
|
|
xbmc.sleep(200)
|
|
|
|
databases = xbmc.translatePath("special://database/")
|
|
types = {
|
|
'video': "MyVideos",
|
|
'music': "MyMusic",
|
|
'texture': "Textures"
|
|
}
|
|
database = types[database]
|
|
dirs, files = xbmcvfs.listdir(databases)
|
|
modified = {'file': None, 'time': 0}
|
|
|
|
for file in reversed(files):
|
|
|
|
if (file.startswith(database) and not file.endswith('-wal') and not file.endswith('-shm') and not file.endswith('db-journal')):
|
|
|
|
st = xbmcvfs.Stat(databases + file)
|
|
modified_int = st.st_mtime()
|
|
LOG.debug("Database detected: %s time: %s", file, modified_int)
|
|
|
|
if modified_int > modified['time']:
|
|
|
|
modified['time'] = modified_int
|
|
modified['file'] = file
|
|
|
|
LOG.debug("Discovered database: %s", modified)
|
|
self.discovered_file = modified['file']
|
|
|
|
return xbmc.translatePath("special://database/%s" % modified['file'])
|
|
|
|
def _sql(self, file):
|
|
|
|
''' Get the database path based on the file objects/obj_map.json
|
|
Compatible check, in the event multiple db version are supported with the same Kodi version.
|
|
Discover by file as a last resort.
|
|
'''
|
|
databases = obj.Objects().objects
|
|
|
|
if file not in ('video', 'music', 'texture') or databases.get('database_set%s' % file):
|
|
return self._get_database(databases[file], True)
|
|
|
|
discovered = self._discover_database(file) if not databases.get('database_set%s' % file) else None
|
|
|
|
try:
|
|
loaded = self._get_database(databases[file]) if file in databases else file
|
|
except Exception as error:
|
|
LOG.exception(error)
|
|
|
|
for i in range(1, 10):
|
|
alt_file = "%s-%s" % (file, i)
|
|
|
|
try:
|
|
loaded = self._get_database(databases[alt_file])
|
|
|
|
break
|
|
except KeyError: # No other db options
|
|
loaded = None
|
|
|
|
break
|
|
except Exception as error:
|
|
LOG.exception(error)
|
|
|
|
if discovered and discovered != loaded:
|
|
|
|
databases[file] = discovered
|
|
self.discovered = True
|
|
else:
|
|
databases[file] = loaded
|
|
|
|
databases['database_set%s' % file] = True
|
|
LOG.info("Database locked in: %s", databases[file])
|
|
|
|
return databases[file]
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
|
|
''' Close the connection and cursor.
|
|
'''
|
|
changes = self.conn.total_changes
|
|
|
|
if exc_type is not None: # errors raised
|
|
LOG.error("type: %s value: %s", exc_type, exc_val)
|
|
|
|
if self.commit_close and changes:
|
|
|
|
LOG.debug("[%s] %s rows updated.", self.db_file, changes)
|
|
self.conn.commit()
|
|
|
|
LOG.debug("---<[ database: %s ] %s", self.db_file, id(self.conn))
|
|
self.cursor.close()
|
|
self.conn.close()
|
|
|
|
|
|
def jellyfin_tables(cursor):
|
|
|
|
''' Create the tables for the jellyfin database.
|
|
jellyfin, view, version
|
|
'''
|
|
cursor.execute(
|
|
"""CREATE TABLE IF NOT EXISTS jellyfin(
|
|
jellyfin_id TEXT UNIQUE, media_folder TEXT, jellyfin_type TEXT, media_type TEXT,
|
|
kodi_id INTEGER, kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER,
|
|
checksum INTEGER, jellyfin_parent_id TEXT)""")
|
|
cursor.execute(
|
|
"""CREATE TABLE IF NOT EXISTS view(
|
|
view_id TEXT UNIQUE, view_name TEXT, media_type TEXT)""")
|
|
cursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)")
|
|
|
|
columns = cursor.execute("SELECT * FROM jellyfin")
|
|
if 'jellyfin_parent_id' not in [description[0] for description in columns.description]:
|
|
|
|
LOG.debug("Add missing column jellyfin_parent_id")
|
|
cursor.execute("ALTER TABLE jellyfin ADD COLUMN jellyfin_parent_id 'TEXT'")
|
|
|
|
|
|
def reset():
|
|
|
|
''' Reset both the jellyfin database and the kodi database.
|
|
'''
|
|
from views import Views
|
|
views = Views()
|
|
|
|
if not dialog("yesno", "{jellyfin}", translate(33074)):
|
|
return
|
|
|
|
window('jellyfin_should_stop.bool', True)
|
|
count = 10
|
|
|
|
while window('jellyfin_sync.bool'):
|
|
|
|
LOG.info("Sync is running...")
|
|
count -= 1
|
|
|
|
if not count:
|
|
dialog("ok", "{jellyfin}", translate(33085))
|
|
|
|
return
|
|
|
|
if xbmc.Monitor().waitForAbort(1):
|
|
return
|
|
|
|
reset_kodi()
|
|
reset_jellyfin()
|
|
views.delete_playlists()
|
|
views.delete_nodes()
|
|
|
|
if dialog("yesno", "{jellyfin}", translate(33086)):
|
|
reset_artwork()
|
|
|
|
if dialog("yesno", "{jellyfin}", translate(33087)):
|
|
|
|
xbmcvfs.delete(os.path.join(ADDON_DATA, "settings.xml"))
|
|
xbmcvfs.delete(os.path.join(ADDON_DATA, "data.json"))
|
|
LOG.info("[ reset settings ]")
|
|
|
|
if xbmcvfs.exists(os.path.join(ADDON_DATA, "sync.json")):
|
|
xbmcvfs.delete(os.path.join(ADDON_DATA, "sync.json"))
|
|
|
|
settings('enableMusic.bool', False)
|
|
settings('MinimumSetup', "")
|
|
settings('MusicRescan.bool', False)
|
|
settings('SyncInstallRunDone.bool', False)
|
|
dialog("ok", "{jellyfin}", translate(33088))
|
|
xbmc.executebuiltin('RestartApp')
|
|
|
|
|
|
def reset_kodi():
|
|
|
|
with Database() as videodb:
|
|
videodb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'")
|
|
|
|
for table in videodb.cursor.fetchall():
|
|
name = table[0]
|
|
|
|
if name != 'version':
|
|
videodb.cursor.execute("DELETE FROM " + name)
|
|
|
|
if settings('enableMusic.bool') or dialog("yesno", "{jellyfin}", translate(33162)):
|
|
|
|
with Database('music') as musicdb:
|
|
musicdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'")
|
|
|
|
for table in musicdb.cursor.fetchall():
|
|
name = table[0]
|
|
|
|
if name != 'version':
|
|
musicdb.cursor.execute("DELETE FROM " + name)
|
|
|
|
LOG.info("[ reset kodi ]")
|
|
|
|
|
|
def reset_jellyfin():
|
|
|
|
with Database('jellyfin') as jellyfindb:
|
|
jellyfindb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'")
|
|
|
|
for table in jellyfindb.cursor.fetchall():
|
|
name = table[0]
|
|
|
|
if name not in ('version', 'view'):
|
|
jellyfindb.cursor.execute("DELETE FROM " + name)
|
|
|
|
jellyfindb.cursor.execute("DROP table IF EXISTS jellyfin")
|
|
jellyfindb.cursor.execute("DROP table IF EXISTS view")
|
|
jellyfindb.cursor.execute("DROP table IF EXISTS version")
|
|
|
|
LOG.info("[ reset jellyfin ]")
|
|
|
|
|
|
def reset_artwork():
|
|
|
|
''' Remove all existing texture.
|
|
'''
|
|
thumbnails = xbmc.translatePath('special://thumbnails/')
|
|
|
|
if xbmcvfs.exists(thumbnails):
|
|
dirs, ignore = xbmcvfs.listdir(thumbnails)
|
|
|
|
for directory in dirs:
|
|
ignore, thumbs = xbmcvfs.listdir(os.path.join(thumbnails, directory))
|
|
|
|
for thumb in thumbs:
|
|
LOG.debug("DELETE thumbnail %s", thumb)
|
|
xbmcvfs.delete(os.path.join(thumbnails, directory, thumb))
|
|
|
|
with Database('texture') as texdb:
|
|
texdb.cursor.execute("SELECT tbl_name FROM sqlite_master WHERE type='table'")
|
|
|
|
for table in texdb.cursor.fetchall():
|
|
name = table[0]
|
|
|
|
if name != 'version':
|
|
texdb.cursor.execute("DELETE FROM " + name)
|
|
|
|
LOG.info("[ reset artwork ]")
|
|
|
|
|
|
def get_sync():
|
|
|
|
if not xbmcvfs.exists(ADDON_DATA):
|
|
xbmcvfs.mkdirs(ADDON_DATA)
|
|
|
|
try:
|
|
with open(os.path.join(ADDON_DATA, 'sync.json'), 'rb') as infile:
|
|
sync = json.load(infile, encoding='utf-8')
|
|
except Exception:
|
|
sync = {}
|
|
|
|
sync['Libraries'] = sync.get('Libraries', [])
|
|
sync['RestorePoint'] = sync.get('RestorePoint', {})
|
|
sync['Whitelist'] = list(set(sync.get('Whitelist', [])))
|
|
sync['SortedViews'] = sync.get('SortedViews', [])
|
|
|
|
return sync
|
|
|
|
|
|
def save_sync(sync):
|
|
|
|
if not xbmcvfs.exists(ADDON_DATA):
|
|
xbmcvfs.mkdirs(ADDON_DATA)
|
|
|
|
sync['Date'] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
|
|
with open(os.path.join(ADDON_DATA, 'sync.json'), 'wb') as outfile:
|
|
data = json.dumps(sync, sort_keys=True, indent=4, ensure_ascii=False)
|
|
if isinstance(data, text_type):
|
|
data = data.encode('utf-8')
|
|
outfile.write(data)
|
|
|
|
|
|
def get_credentials():
|
|
|
|
if not xbmcvfs.exists(ADDON_DATA):
|
|
xbmcvfs.mkdirs(ADDON_DATA)
|
|
|
|
try:
|
|
with open(os.path.join(ADDON_DATA, 'data.json'), 'rb') as infile:
|
|
credentials = json.load(infile, encoding='utf8')
|
|
except Exception:
|
|
|
|
try:
|
|
with open(os.path.join(ADDON_DATA, 'data.txt'), 'rb') as infile:
|
|
credentials = json.load(infile, encoding='utf-8')
|
|
save_credentials(credentials)
|
|
|
|
xbmcvfs.delete(os.path.join(ADDON_DATA, 'data.txt'))
|
|
except Exception:
|
|
credentials = {}
|
|
|
|
credentials['Servers'] = credentials.get('Servers', [])
|
|
|
|
# Migration for #145
|
|
# TODO: CLEANUP for 1.0.0 release
|
|
for server in credentials['Servers']:
|
|
# Functionality removed in #60
|
|
if 'RemoteAddress' in server:
|
|
del server['RemoteAddress']
|
|
if 'ManualAddress' in server:
|
|
server['address'] = server['ManualAddress']
|
|
del server['ManualAddress']
|
|
# If manual is present, local should always be here, but better to be safe
|
|
if 'LocalAddress' in server:
|
|
del server['LocalAddress']
|
|
elif 'LocalAddress' in server:
|
|
server['address'] = server['LocalAddress']
|
|
del server['LocalAddress']
|
|
if 'LastConnectionMode' in server:
|
|
del server['LastConnectionMode']
|
|
|
|
return credentials
|
|
|
|
|
|
def save_credentials(credentials):
|
|
credentials = credentials or {}
|
|
|
|
if not xbmcvfs.exists(ADDON_DATA):
|
|
xbmcvfs.mkdirs(ADDON_DATA)
|
|
try:
|
|
with open(os.path.join(ADDON_DATA, 'data.json'), 'wb') as outfile:
|
|
data = json.dumps(credentials, sort_keys=True, indent=4, ensure_ascii=False)
|
|
if isinstance(data, text_type):
|
|
data = data.encode('utf-8')
|
|
outfile.write(data)
|
|
except Exception:
|
|
LOG.exception("Failed to save credentials:")
|
|
|
|
|
|
def get_item(kodi_id, media):
|
|
|
|
''' Get jellyfin item based on kodi id and media.
|
|
'''
|
|
with Database('jellyfin') as jellyfindb:
|
|
item = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_full_item_by_kodi_id(kodi_id, media)
|
|
|
|
if not item:
|
|
LOG.debug("Not an jellyfin item")
|
|
|
|
return
|
|
|
|
return item
|