1324 lines
45 KiB
Python
1324 lines
45 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
###############################################################################
|
|
from logging import getLogger
|
|
from ntpath import dirname
|
|
|
|
import artwork
|
|
from utils import kodi_sql, try_decode
|
|
import variables as v
|
|
|
|
###############################################################################
|
|
|
|
LOG = getLogger("PLEX." + __name__)
|
|
|
|
###############################################################################
|
|
|
|
|
|
class GetKodiDB(object):
|
|
"""
|
|
Usage: with GetKodiDB(db_type) as kodi_db:
|
|
do stuff with kodi_db
|
|
|
|
Parameters:
|
|
db_type: DB to open: 'video', 'music', 'plex', 'texture'
|
|
|
|
On exiting "with" (no matter what), commits get automatically committed
|
|
and the db gets closed
|
|
"""
|
|
def __init__(self, db_type):
|
|
self.kodiconn = None
|
|
self.db_type = db_type
|
|
|
|
def __enter__(self):
|
|
self.kodiconn = kodi_sql(self.db_type)
|
|
kodi_db = KodiDBMethods(self.kodiconn.cursor())
|
|
return kodi_db
|
|
|
|
def __exit__(self, typus, value, traceback):
|
|
self.kodiconn.commit()
|
|
self.kodiconn.close()
|
|
|
|
|
|
class KodiDBMethods(object):
|
|
"""
|
|
Best used indirectly with another Class GetKodiDB:
|
|
with GetKodiDB(db_type) as kodi_db:
|
|
kodi_db.method()
|
|
"""
|
|
def __init__(self, cursor):
|
|
self.cursor = cursor
|
|
self.artwork = artwork.Artwork()
|
|
|
|
def setup_path_table(self):
|
|
"""
|
|
Use with Kodi video DB
|
|
|
|
Sets strContent to e.g. 'movies' and strScraper to metadata.local
|
|
|
|
For some reason, Kodi ignores this if done via itemtypes while e.g.
|
|
adding or updating items. (addPath method does NOT work)
|
|
"""
|
|
path_id = self.getPath('plugin://%s.movies/' % v.ADDON_ID)
|
|
if path_id is None:
|
|
self.cursor.execute("select coalesce(max(idPath),0) from path")
|
|
path_id = self.cursor.fetchone()[0] + 1
|
|
query = '''
|
|
INSERT INTO path(idPath,
|
|
strPath,
|
|
strContent,
|
|
strScraper,
|
|
noUpdate,
|
|
exclude)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
'''
|
|
self.cursor.execute(query, (path_id,
|
|
'plugin://%s.movies/' % v.ADDON_ID,
|
|
'movies',
|
|
'metadata.local',
|
|
1,
|
|
0))
|
|
# And TV shows
|
|
path_id = self.getPath('plugin://%s.tvshows/' % v.ADDON_ID)
|
|
if path_id is None:
|
|
self.cursor.execute("select coalesce(max(idPath),0) from path")
|
|
path_id = self.cursor.fetchone()[0] + 1
|
|
query = '''
|
|
INSERT INTO path(idPath,
|
|
strPath,
|
|
strContent,
|
|
strScraper,
|
|
noUpdate,
|
|
exclude)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
'''
|
|
self.cursor.execute(query, (path_id,
|
|
'plugin://%s.tvshows/' % v.ADDON_ID,
|
|
'tvshows',
|
|
'metadata.local',
|
|
1,
|
|
0))
|
|
|
|
def getParentPathId(self, path):
|
|
"""
|
|
Video DB: Adds all subdirectories to SQL path while setting a "trail"
|
|
of parentPathId
|
|
"""
|
|
if "\\" in path:
|
|
# Local path
|
|
parentpath = "%s\\" % dirname(dirname(path))
|
|
else:
|
|
# Network path
|
|
parentpath = "%s/" % dirname(dirname(path))
|
|
pathid = self.getPath(parentpath)
|
|
if pathid is None:
|
|
self.cursor.execute("select coalesce(max(idPath),0) from path")
|
|
pathid = self.cursor.fetchone()[0] + 1
|
|
query = ' '.join((
|
|
"INSERT INTO path(idPath, strPath)",
|
|
"VALUES (?, ?)"
|
|
))
|
|
self.cursor.execute(query, (pathid, parentpath))
|
|
parentPathid = self.getParentPathId(parentpath)
|
|
query = ' '.join((
|
|
"UPDATE path",
|
|
"SET idParentPath = ?",
|
|
"WHERE idPath = ?"
|
|
))
|
|
self.cursor.execute(query, (parentPathid, pathid))
|
|
return pathid
|
|
|
|
def addPath(self, path, strHash=None):
|
|
# SQL won't return existing paths otherwise
|
|
if path is None:
|
|
path = ""
|
|
query = ' '.join((
|
|
|
|
"SELECT idPath",
|
|
"FROM path",
|
|
"WHERE strPath = ?"
|
|
))
|
|
self.cursor.execute(query, (path,))
|
|
try:
|
|
pathid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
self.cursor.execute("select coalesce(max(idPath),0) from path")
|
|
pathid = self.cursor.fetchone()[0] + 1
|
|
if strHash is None:
|
|
query = (
|
|
'''
|
|
INSERT INTO path(
|
|
idPath, strPath)
|
|
|
|
VALUES (?, ?)
|
|
'''
|
|
)
|
|
self.cursor.execute(query, (pathid, path))
|
|
else:
|
|
query = (
|
|
'''
|
|
INSERT INTO path(
|
|
idPath, strPath, strHash)
|
|
|
|
VALUES (?, ?, ?)
|
|
'''
|
|
)
|
|
self.cursor.execute(query, (pathid, path, strHash))
|
|
|
|
return pathid
|
|
|
|
def getPath(self, path):
|
|
|
|
query = ' '.join((
|
|
|
|
"SELECT idPath",
|
|
"FROM path",
|
|
"WHERE strPath = ?"
|
|
))
|
|
self.cursor.execute(query, (path,))
|
|
try:
|
|
pathid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
pathid = None
|
|
|
|
return pathid
|
|
|
|
def addFile(self, filename, pathid):
|
|
|
|
query = ' '.join((
|
|
|
|
"SELECT idFile",
|
|
"FROM files",
|
|
"WHERE strFilename = ?",
|
|
"AND idPath = ?"
|
|
))
|
|
self.cursor.execute(query, (filename, pathid,))
|
|
try:
|
|
fileid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
self.cursor.execute("select coalesce(max(idFile),0) from files")
|
|
fileid = self.cursor.fetchone()[0] + 1
|
|
query = (
|
|
'''
|
|
INSERT INTO files(
|
|
idFile, strFilename)
|
|
|
|
VALUES (?, ?)
|
|
'''
|
|
)
|
|
self.cursor.execute(query, (fileid, filename))
|
|
|
|
return fileid
|
|
|
|
def getFile(self, fileid):
|
|
|
|
query = ' '.join((
|
|
|
|
"SELECT strFilename",
|
|
"FROM files",
|
|
"WHERE idFile = ?"
|
|
))
|
|
self.cursor.execute(query, (fileid,))
|
|
try:
|
|
filename = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
filename = ""
|
|
|
|
return filename
|
|
|
|
def removeFile(self, path, filename):
|
|
|
|
pathid = self.getPath(path)
|
|
|
|
if pathid is not None:
|
|
query = ' '.join((
|
|
|
|
"DELETE FROM files",
|
|
"WHERE idPath = ?",
|
|
"AND strFilename = ?"
|
|
))
|
|
self.cursor.execute(query, (pathid, filename,))
|
|
|
|
def addCountries(self, kodiid, countries, mediatype):
|
|
for country in countries:
|
|
query = ' '.join((
|
|
|
|
"SELECT country_id",
|
|
"FROM country",
|
|
"WHERE name = ?",
|
|
"COLLATE NOCASE"
|
|
))
|
|
self.cursor.execute(query, (country,))
|
|
|
|
try:
|
|
country_id = self.cursor.fetchone()[0]
|
|
|
|
except TypeError:
|
|
# Country entry does not exists
|
|
self.cursor.execute("select coalesce(max(country_id),0) from country")
|
|
country_id = self.cursor.fetchone()[0] + 1
|
|
|
|
query = "INSERT INTO country(country_id, name) values(?, ?)"
|
|
self.cursor.execute(query, (country_id, country))
|
|
LOG.debug("Add country to media, processing: %s", country)
|
|
|
|
finally: # Assign country to content
|
|
query = (
|
|
'''
|
|
INSERT OR REPLACE INTO country_link(
|
|
country_id, media_id, media_type)
|
|
|
|
VALUES (?, ?, ?)
|
|
'''
|
|
)
|
|
self.cursor.execute(query, (country_id, kodiid, mediatype))
|
|
|
|
def _delete_from_link_and_table(self, kodi_id, kodi_type, link_table,
|
|
table, key):
|
|
# Get all existing links
|
|
query = ('SELECT %s FROM %s WHERE media_id = ? AND media_type = ? '
|
|
% (key, link_table))
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
key_list = self.cursor.fetchall()
|
|
# Delete all links
|
|
query = ('DELETE FROM %s WHERE media_id = ? AND media_type = ?'
|
|
% link_table)
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
# Which countries are now orphaned?
|
|
query = 'SELECT %s FROM %s WHERE %s = ?' % (key, link_table, key)
|
|
query_delete = 'DELETE FROM %s WHERE %s = ?' % (table, key)
|
|
for entry in key_list:
|
|
# country_id still in table?
|
|
self.cursor.execute(query, (entry[0],))
|
|
if self.cursor.fetchone() is None:
|
|
self.cursor.execute(query_delete, (entry[0],))
|
|
|
|
def delete_countries(self, kodi_id, kodi_type):
|
|
"""
|
|
Assuming that video kodi_id, kodi_type gets deleted, will delete any
|
|
associated country links in the table country_link and also deletes
|
|
orphaned countries in the table country
|
|
"""
|
|
self._delete_from_link_and_table(kodi_id,
|
|
kodi_type,
|
|
'country_link',
|
|
'country',
|
|
'country_id')
|
|
|
|
def _getactorid(self, name):
|
|
"""
|
|
Crucial für sync speed!
|
|
"""
|
|
query = ' '.join((
|
|
"SELECT actor_id",
|
|
"FROM actor",
|
|
"WHERE name = ?",
|
|
"LIMIT 1"
|
|
))
|
|
self.cursor.execute(query, (name,))
|
|
try:
|
|
actorid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
# Cast entry does not exists
|
|
self.cursor.execute("select coalesce(max(actor_id),0) from actor")
|
|
actorid = self.cursor.fetchone()[0] + 1
|
|
query = "INSERT INTO actor(actor_id, name) VALUES (?, ?)"
|
|
self.cursor.execute(query, (actorid, name))
|
|
return actorid
|
|
|
|
def _addPerson(self, role, person_type, actorid, kodiid, mediatype,
|
|
castorder):
|
|
if "Actor" == person_type:
|
|
query = '''
|
|
INSERT OR REPLACE INTO actor_link(
|
|
actor_id, media_id, media_type, role, cast_order)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
'''
|
|
self.cursor.execute(query, (actorid, kodiid, mediatype, role,
|
|
castorder))
|
|
castorder += 1
|
|
elif "Director" == person_type:
|
|
query = '''
|
|
INSERT OR REPLACE INTO director_link(
|
|
actor_id, media_id, media_type)
|
|
VALUES (?, ?, ?)
|
|
'''
|
|
self.cursor.execute(query, (actorid, kodiid, mediatype))
|
|
elif person_type == "Writer":
|
|
query = '''
|
|
INSERT OR REPLACE INTO writer_link(
|
|
actor_id, media_id, media_type)
|
|
VALUES (?, ?, ?)
|
|
'''
|
|
self.cursor.execute(query, (actorid, kodiid, mediatype))
|
|
elif "Artist" == person_type:
|
|
query = '''
|
|
INSERT OR REPLACE INTO actor_link(
|
|
actor_id, media_id, media_type)
|
|
VALUES (?, ?, ?)
|
|
'''
|
|
self.cursor.execute(query, (actorid, kodiid, mediatype))
|
|
return castorder
|
|
|
|
def addPeople(self, kodiid, people, mediatype):
|
|
castorder = 0
|
|
for person in people:
|
|
actorid = self._getactorid(person['Name'])
|
|
# Link person to content
|
|
castorder = self._addPerson(person.get('Role'),
|
|
person['Type'],
|
|
actorid,
|
|
kodiid,
|
|
mediatype,
|
|
castorder)
|
|
# Add person image to art table
|
|
if person['imageurl']:
|
|
self.artwork.addOrUpdateArt(person['imageurl'], actorid,
|
|
person['Type'].lower(), "thumb",
|
|
self.cursor)
|
|
|
|
def delete_people(self, kodi_id, kodi_type):
|
|
"""
|
|
Assuming that the video kodi_id, kodi_type gets deleted, will delete any
|
|
associated actor_, director_, writer_links and also deletes
|
|
orphaned actors
|
|
"""
|
|
# Actors
|
|
query = '''
|
|
SELECT actor_id FROM actor_link
|
|
WHERE media_id = ? AND media_type = ?
|
|
'''
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
actor_ids = self.cursor.fetchall()
|
|
query = 'DELETE FROM actor_link WHERE media_id = ? AND media_type = ?'
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
# Directors
|
|
query = '''
|
|
SELECT actor_id FROM director_link
|
|
WHERE media_id = ? AND media_type = ?
|
|
'''
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
actor_ids.extend(self.cursor.fetchall())
|
|
query = '''
|
|
DELETE FROM director_link WHERE media_id = ? AND media_type = ?
|
|
'''
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
# Writers
|
|
query = '''
|
|
SELECT actor_id FROM writer_link
|
|
WHERE media_id = ? AND media_type = ?
|
|
'''
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
actor_ids.extend(self.cursor.fetchall())
|
|
query = '''
|
|
DELETE FROM writer_link WHERE media_id = ? AND media_type = ?
|
|
'''
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
# Which people are now orphaned?
|
|
query_actor = 'SELECT actor_id FROM actor_link WHERE actor_id = ?'
|
|
query_director = 'SELECT actor_id FROM director_link WHERE actor_id = ?'
|
|
query_writer = 'SELECT actor_id FROM writer_link WHERE actor_id = ?'
|
|
query_delete = 'DELETE FROM actor WHERE actor_id = ?'
|
|
# Delete orphaned people
|
|
for actor_id in actor_ids:
|
|
self.cursor.execute(query_actor, (actor_id[0],))
|
|
if self.cursor.fetchone() is None:
|
|
self.cursor.execute(query_director, (actor_id[0],))
|
|
if self.cursor.fetchone() is None:
|
|
self.cursor.execute(query_writer, (actor_id[0],))
|
|
if self.cursor.fetchone() is None:
|
|
# Delete the person itself from actor table
|
|
self.cursor.execute(query_delete, (actor_id[0],))
|
|
# Delete any associated artwork
|
|
self.artwork.deleteArtwork(actor_id[0],
|
|
'actor',
|
|
self.cursor)
|
|
|
|
|
|
def existingArt(self, kodiId, mediaType, refresh=False):
|
|
"""
|
|
For kodiId, returns an artwork dict with already existing art from
|
|
the Kodi db
|
|
"""
|
|
# Only get EITHER poster OR thumb (should have same URL)
|
|
kodiToPKC = {
|
|
'banner': 'Banner',
|
|
'clearart': 'Art',
|
|
'clearlogo': 'Logo',
|
|
'discart': 'Disc',
|
|
'landscape': 'Thumb',
|
|
'thumb': 'Primary'
|
|
}
|
|
# BoxRear yet unused
|
|
result = {'BoxRear': ''}
|
|
for art in kodiToPKC:
|
|
query = ' '.join((
|
|
"SELECT url",
|
|
"FROM art",
|
|
"WHERE media_id = ?",
|
|
"AND media_type = ?",
|
|
"AND type = ?"
|
|
))
|
|
self.cursor.execute(query, (kodiId, mediaType, art,))
|
|
try:
|
|
url = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
url = ""
|
|
result[kodiToPKC[art]] = url
|
|
# There may be several fanart URLs saved
|
|
query = ' '.join((
|
|
"SELECT url",
|
|
"FROM art",
|
|
"WHERE media_id = ?",
|
|
"AND media_type = ?",
|
|
"AND type LIKE ?"
|
|
))
|
|
data = self.cursor.execute(query, (kodiId, mediaType, "fanart%",))
|
|
result['Backdrop'] = [d[0] for d in data]
|
|
return result
|
|
|
|
def addGenres(self, kodi_id, genres, kodi_type):
|
|
"""
|
|
Adds the genres (list of strings) to the Kodi DB and associates them
|
|
with the element kodi_id, kodi_type
|
|
"""
|
|
# Delete current genres for clean slate
|
|
query = 'DELETE FROM genre_link WHERE media_id = ? AND media_type = ?'
|
|
self.cursor.execute(query, (kodi_id, kodi_type,))
|
|
# Add genres
|
|
for genre in genres:
|
|
query = ' SELECT genre_id FROM genre WHERE name = ? COLLATE NOCASE'
|
|
self.cursor.execute(query, (genre,))
|
|
try:
|
|
genre_id = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
# Create genre in database
|
|
self.cursor.execute("select coalesce(max(genre_id),0) from genre")
|
|
genre_id = self.cursor.fetchone()[0] + 1
|
|
query = "INSERT INTO genre(genre_id, name) values(?, ?)"
|
|
self.cursor.execute(query, (genre_id, genre))
|
|
finally:
|
|
# Assign genre to item
|
|
query = '''
|
|
INSERT OR REPLACE INTO genre_link(
|
|
genre_id, media_id, media_type)
|
|
VALUES (?, ?, ?)
|
|
'''
|
|
self.cursor.execute(query, (genre_id, kodi_id, kodi_type))
|
|
|
|
def delete_genre(self, kodi_id, kodi_type):
|
|
"""
|
|
Removes the genre links as well as orphaned genres from the Kodi DB
|
|
"""
|
|
self._delete_from_link_and_table(kodi_id,
|
|
kodi_type,
|
|
'genre_link',
|
|
'genre',
|
|
'genre_id')
|
|
|
|
def addStudios(self, kodiid, studios, mediatype):
|
|
for studio in studios:
|
|
query = ' '.join((
|
|
|
|
"SELECT studio_id",
|
|
"FROM studio",
|
|
"WHERE name = ?",
|
|
"COLLATE NOCASE"
|
|
))
|
|
self.cursor.execute(query, (studio,))
|
|
try:
|
|
studioid = self.cursor.fetchone()[0]
|
|
|
|
except TypeError:
|
|
# Studio does not exists.
|
|
self.cursor.execute("select coalesce(max(studio_id),0) from studio")
|
|
studioid = self.cursor.fetchone()[0] + 1
|
|
|
|
query = "INSERT INTO studio(studio_id, name) values(?, ?)"
|
|
self.cursor.execute(query, (studioid, studio))
|
|
LOG.debug("Add Studios to media, processing: %s", studio)
|
|
|
|
finally: # Assign studio to item
|
|
query = (
|
|
'''
|
|
INSERT OR REPLACE INTO studio_link(
|
|
studio_id, media_id, media_type)
|
|
|
|
VALUES (?, ?, ?)
|
|
''')
|
|
self.cursor.execute(query, (studioid, kodiid, mediatype))
|
|
|
|
def delete_studios(self, kodi_id, kodi_type):
|
|
"""
|
|
Removes the studio links as well as orphaned studios from the Kodi DB
|
|
"""
|
|
self._delete_from_link_and_table(kodi_id,
|
|
kodi_type,
|
|
'studio_link',
|
|
'studio',
|
|
'studio_id')
|
|
|
|
def modify_streams(self, fileid, streamdetails=None, runtime=None):
|
|
"""
|
|
Leave streamdetails and runtime empty to delete all stream entries for
|
|
fileid
|
|
"""
|
|
# First remove any existing entries
|
|
self.cursor.execute("DELETE FROM streamdetails WHERE idFile = ?", (fileid,))
|
|
if streamdetails:
|
|
# Video details
|
|
for videotrack in streamdetails['video']:
|
|
query = (
|
|
'''
|
|
INSERT INTO streamdetails(
|
|
idFile, iStreamType, strVideoCodec, fVideoAspect,
|
|
iVideoWidth, iVideoHeight, iVideoDuration ,strStereoMode)
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
'''
|
|
)
|
|
self.cursor.execute(query, (fileid, 0, videotrack['codec'],
|
|
videotrack['aspect'], videotrack['width'], videotrack['height'],
|
|
runtime ,videotrack['video3DFormat']))
|
|
|
|
# Audio details
|
|
for audiotrack in streamdetails['audio']:
|
|
query = (
|
|
'''
|
|
INSERT INTO streamdetails(
|
|
idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage)
|
|
|
|
VALUES (?, ?, ?, ?, ?)
|
|
'''
|
|
)
|
|
self.cursor.execute(query, (fileid, 1, audiotrack['codec'],
|
|
audiotrack['channels'], audiotrack['language']))
|
|
|
|
# Subtitles details
|
|
for subtitletrack in streamdetails['subtitle']:
|
|
query = (
|
|
'''
|
|
INSERT INTO streamdetails(
|
|
idFile, iStreamType, strSubtitleLanguage)
|
|
|
|
VALUES (?, ?, ?)
|
|
'''
|
|
)
|
|
self.cursor.execute(query, (fileid, 2, subtitletrack))
|
|
|
|
def resume_points(self):
|
|
"""
|
|
VIDEOS
|
|
|
|
Returns all Kodi idFile that have a resume point set (not unwatched
|
|
ones or items that have already been completely watched)
|
|
"""
|
|
query = '''
|
|
SELECT idFile
|
|
FROM bookmark
|
|
'''
|
|
rows = self.cursor.execute(query)
|
|
ids = []
|
|
for row in rows:
|
|
ids.append(row[0])
|
|
return ids
|
|
|
|
def getUnplayedMusicItems(self):
|
|
"""
|
|
MUSIC
|
|
|
|
Returns all Kodi Item idFile that have not yet been completely played
|
|
"""
|
|
query = ' '.join((
|
|
"SELECT idSong",
|
|
"FROM song",
|
|
"WHERE iTimesPlayed = ?"
|
|
))
|
|
try:
|
|
rows = self.cursor.execute(query, (0, ))
|
|
except:
|
|
return []
|
|
ids = []
|
|
for row in rows:
|
|
ids.append(row[0])
|
|
return ids
|
|
|
|
def video_id_from_filename(self, filename, path):
|
|
"""
|
|
Returns the tuple (itemId, type) where
|
|
itemId: Kodi DB unique Id for either movie or episode
|
|
type: either 'movie' or 'episode'
|
|
|
|
Returns None if not found OR if too many entries were found
|
|
"""
|
|
query = ' '.join((
|
|
"SELECT idFile, idPath",
|
|
"FROM files",
|
|
"WHERE strFilename = ?"
|
|
))
|
|
self.cursor.execute(query, (filename,))
|
|
files = self.cursor.fetchall()
|
|
if len(files) == 0:
|
|
LOG.info('Did not find any file, abort')
|
|
return
|
|
query = ' '.join((
|
|
"SELECT strPath",
|
|
"FROM path",
|
|
"WHERE idPath = ?"
|
|
))
|
|
# result will contain a list of all idFile with matching filename and
|
|
# matching path
|
|
result = []
|
|
for file in files:
|
|
# Use idPath to get path as a string
|
|
self.cursor.execute(query, (file[1],))
|
|
try:
|
|
strPath = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
# idPath not found; skip
|
|
continue
|
|
# For whatever reason, double might have become triple
|
|
strPath = strPath.replace('///', '//')
|
|
strPath = strPath.replace('\\\\\\', '\\\\')
|
|
if strPath == path:
|
|
result.append(file[0])
|
|
if len(result) == 0:
|
|
LOG.info('Did not find matching paths, abort')
|
|
return
|
|
# Kodi seems to make ONE temporary entry; we only want the earlier,
|
|
# permanent one
|
|
if len(result) > 2:
|
|
LOG.warn('We found too many items with matching filenames and '
|
|
' paths, aborting')
|
|
return
|
|
idFile = result[0]
|
|
|
|
# Try movies first
|
|
query = ' '.join((
|
|
"SELECT idMovie",
|
|
"FROM movie",
|
|
"WHERE idFile = ?"
|
|
))
|
|
self.cursor.execute(query, (idFile,))
|
|
try:
|
|
itemId = self.cursor.fetchone()[0]
|
|
typus = v.KODI_TYPE_MOVIE
|
|
except TypeError:
|
|
# Try tv shows next
|
|
query = ' '.join((
|
|
"SELECT idEpisode",
|
|
"FROM episode",
|
|
"WHERE idFile = ?"
|
|
))
|
|
self.cursor.execute(query, (idFile,))
|
|
try:
|
|
itemId = self.cursor.fetchone()[0]
|
|
typus = v.KODI_TYPE_EPISODE
|
|
except TypeError:
|
|
LOG.warn('Unexpectantly did not find a match!')
|
|
return
|
|
return itemId, typus
|
|
|
|
def music_id_from_filename(self, filename, path):
|
|
"""
|
|
Returns the Kodi song_id from the Kodi music database or None if not
|
|
found OR something went wrong.
|
|
"""
|
|
query = '''
|
|
SELECT idPath
|
|
FROM path
|
|
WHERE strPath = ?
|
|
'''
|
|
self.cursor.execute(query, (path,))
|
|
path_id = self.cursor.fetchall()
|
|
if len(path_id) != 1:
|
|
LOG.error('Found wrong number of path ids: %s for path %s, abort',
|
|
path_id, path)
|
|
return
|
|
query = '''
|
|
SELECT idSong
|
|
FROM song
|
|
WHERE strFileName = ? AND idPath = ?
|
|
'''
|
|
self.cursor.execute(query, (filename, path_id[0]))
|
|
song_id = self.cursor.fetchall()
|
|
if len(song_id) != 1:
|
|
LOG.info('Found wrong number of songs %s, abort', song_id)
|
|
return
|
|
return song_id[0]
|
|
|
|
def getUnplayedItems(self):
|
|
"""
|
|
VIDEOS
|
|
|
|
Returns all Kodi Item idFile that have not yet been completely played
|
|
"""
|
|
query = ' '.join((
|
|
"SELECT idFile",
|
|
"FROM files",
|
|
"WHERE playCount IS NULL OR playCount = ''"
|
|
))
|
|
try:
|
|
rows = self.cursor.execute(query)
|
|
except:
|
|
return []
|
|
ids = []
|
|
for row in rows:
|
|
ids.append(row[0])
|
|
return ids
|
|
|
|
def getVideoRuntime(self, kodiid, mediatype):
|
|
if mediatype == v.KODI_TYPE_MOVIE:
|
|
query = ' '.join((
|
|
"SELECT c11",
|
|
"FROM movie",
|
|
"WHERE idMovie = ?",
|
|
))
|
|
elif mediatype == v.KODI_TYPE_EPISODE:
|
|
query = ' '.join((
|
|
"SELECT c09",
|
|
"FROM episode",
|
|
"WHERE idEpisode = ?",
|
|
))
|
|
self.cursor.execute(query, (kodiid,))
|
|
try:
|
|
runtime = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
return None
|
|
return int(runtime)
|
|
|
|
def get_resume(self, file_id):
|
|
"""
|
|
Returns the first resume point in seconds (int) if found, else None for
|
|
the Kodi file_id provided
|
|
"""
|
|
query = '''
|
|
SELECT timeInSeconds
|
|
FROM bookmark
|
|
WHERE idFile = ?
|
|
'''
|
|
self.cursor.execute(query, (file_id,))
|
|
resume = self.cursor.fetchone()
|
|
try:
|
|
resume = resume[0]
|
|
except TypeError:
|
|
resume = None
|
|
return resume
|
|
|
|
def delete_all_playstates(self):
|
|
"""
|
|
Entirely resets the table bookmark and thus all resume points
|
|
"""
|
|
self.cursor.execute("DELETE FROM bookmark")
|
|
|
|
def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount,
|
|
dateplayed):
|
|
# Delete existing resume point
|
|
query = '''
|
|
DELETE FROM bookmark
|
|
WHERE idFile = ?
|
|
'''
|
|
self.cursor.execute(query, (fileid,))
|
|
# Set watched count
|
|
query = '''
|
|
UPDATE files
|
|
SET playCount = ?, lastPlayed = ?
|
|
WHERE idFile = ?
|
|
'''
|
|
self.cursor.execute(query, (playcount, dateplayed, fileid))
|
|
# Set the resume bookmark
|
|
if resume_seconds:
|
|
self.cursor.execute(
|
|
'select coalesce(max(idBookmark),0) from bookmark')
|
|
bookmark_id = self.cursor.fetchone()[0] + 1
|
|
query = '''
|
|
INSERT INTO bookmark(
|
|
idBookmark, idFile, timeInSeconds, totalTimeInSeconds,
|
|
thumbNailImage, player, playerState, type)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
'''
|
|
self.cursor.execute(query, (bookmark_id,
|
|
fileid,
|
|
resume_seconds,
|
|
total_seconds,
|
|
'',
|
|
"VideoPlayer",
|
|
'',
|
|
1))
|
|
|
|
def delete_playstate(self, file_id):
|
|
"""
|
|
Removes all playstates/bookmarks for the file with file_id
|
|
"""
|
|
self.cursor.execute('DELETE FROM bookmark where idFile = ?', (file_id,))
|
|
|
|
def addTags(self, kodiid, tags, mediatype):
|
|
# First, delete any existing tags associated to the id
|
|
query = ' '.join((
|
|
|
|
"DELETE FROM tag_link",
|
|
"WHERE media_id = ?",
|
|
"AND media_type = ?"
|
|
))
|
|
self.cursor.execute(query, (kodiid, mediatype))
|
|
|
|
# Add tags
|
|
LOG.debug("Adding Tags: %s", tags)
|
|
for tag in tags:
|
|
self.addTag(kodiid, tag, mediatype)
|
|
|
|
def delete_tags(self, kodi_id, kodi_type):
|
|
"""
|
|
Removes the genre links as well as orphaned genres from the Kodi DB
|
|
"""
|
|
self._delete_from_link_and_table(kodi_id,
|
|
kodi_type,
|
|
'tag_link',
|
|
'tag',
|
|
'tag_id')
|
|
|
|
def addTag(self, kodiid, tag, mediatype):
|
|
query = ' '.join((
|
|
|
|
"SELECT tag_id",
|
|
"FROM tag",
|
|
"WHERE name = ?",
|
|
"COLLATE NOCASE"
|
|
))
|
|
self.cursor.execute(query, (tag,))
|
|
try:
|
|
tag_id = self.cursor.fetchone()[0]
|
|
|
|
except TypeError:
|
|
# Create the tag, because it does not exist
|
|
tag_id = self.createTag(tag)
|
|
LOG.debug("Adding tag: %s", tag)
|
|
|
|
finally:
|
|
# Assign tag to item
|
|
query = (
|
|
'''
|
|
INSERT OR REPLACE INTO tag_link(
|
|
tag_id, media_id, media_type)
|
|
|
|
VALUES (?, ?, ?)
|
|
'''
|
|
)
|
|
self.cursor.execute(query, (tag_id, kodiid, mediatype))
|
|
|
|
def createTag(self, name):
|
|
# This will create and return the tag_id
|
|
query = ' '.join((
|
|
|
|
"SELECT tag_id",
|
|
"FROM tag",
|
|
"WHERE name = ?",
|
|
"COLLATE NOCASE"
|
|
))
|
|
self.cursor.execute(query, (name,))
|
|
try:
|
|
tag_id = self.cursor.fetchone()[0]
|
|
|
|
except TypeError:
|
|
self.cursor.execute("select coalesce(max(tag_id),0) from tag")
|
|
tag_id = self.cursor.fetchone()[0] + 1
|
|
|
|
query = "INSERT INTO tag(tag_id, name) values(?, ?)"
|
|
self.cursor.execute(query, (tag_id, name))
|
|
LOG.debug("Create tag_id: %s name: %s", tag_id, name)
|
|
return tag_id
|
|
|
|
def updateTag(self, oldtag, newtag, kodiid, mediatype):
|
|
try:
|
|
query = ' '.join((
|
|
|
|
"UPDATE tag_link",
|
|
"SET tag_id = ?",
|
|
"WHERE media_id = ?",
|
|
"AND media_type = ?",
|
|
"AND tag_id = ?"
|
|
))
|
|
self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,))
|
|
except Exception as e:
|
|
# The new tag we are going to apply already exists for this item
|
|
# delete current tag instead
|
|
query = ' '.join((
|
|
|
|
"DELETE FROM tag_link",
|
|
"WHERE media_id = ?",
|
|
"AND media_type = ?",
|
|
"AND tag_id = ?"
|
|
))
|
|
self.cursor.execute(query, (kodiid, mediatype, oldtag,))
|
|
|
|
def addSets(self, movieid, collections, kodicursor):
|
|
for setname in collections:
|
|
setid = self.createBoxset(setname)
|
|
self.assignBoxset(setid, movieid)
|
|
|
|
def createBoxset(self, boxsetname):
|
|
|
|
LOG.debug("Adding boxset: %s", boxsetname)
|
|
query = ' '.join((
|
|
|
|
"SELECT idSet",
|
|
"FROM sets",
|
|
"WHERE strSet = ?",
|
|
"COLLATE NOCASE"
|
|
))
|
|
self.cursor.execute(query, (boxsetname,))
|
|
try:
|
|
setid = self.cursor.fetchone()[0]
|
|
|
|
except TypeError:
|
|
self.cursor.execute("select coalesce(max(idSet),0) from sets")
|
|
setid = self.cursor.fetchone()[0] + 1
|
|
|
|
query = "INSERT INTO sets(idSet, strSet) values(?, ?)"
|
|
self.cursor.execute(query, (setid, boxsetname))
|
|
|
|
return setid
|
|
|
|
def assignBoxset(self, setid, movieid):
|
|
|
|
query = ' '.join((
|
|
|
|
"UPDATE movie",
|
|
"SET idSet = ?",
|
|
"WHERE idMovie = ?"
|
|
))
|
|
self.cursor.execute(query, (setid, movieid,))
|
|
|
|
def removefromBoxset(self, movieid):
|
|
|
|
query = ' '.join((
|
|
|
|
"UPDATE movie",
|
|
"SET idSet = null",
|
|
"WHERE idMovie = ?"
|
|
))
|
|
self.cursor.execute(query, (movieid,))
|
|
|
|
def get_set_id(self, kodi_id):
|
|
"""
|
|
Returns the set_id for the movie with kodi_id or None
|
|
"""
|
|
query = 'SELECT idSet FROM movie WHERE idMovie = ?'
|
|
self.cursor.execute(query, (kodi_id,))
|
|
try:
|
|
answ = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
answ = None
|
|
return answ
|
|
|
|
def delete_possibly_empty_set(self, set_id):
|
|
"""
|
|
Checks whether there are other movies in the set set_id. If not,
|
|
deletes the set
|
|
"""
|
|
query = 'SELECT idSet FROM movie WHERE idSet = ?'
|
|
self.cursor.execute(query, (set_id,))
|
|
if self.cursor.fetchone() is None:
|
|
query = 'DELETE FROM sets WHERE idSet = ?'
|
|
self.cursor.execute(query, (set_id,))
|
|
|
|
def addSeason(self, showid, seasonnumber):
|
|
|
|
query = ' '.join((
|
|
|
|
"SELECT idSeason",
|
|
"FROM seasons",
|
|
"WHERE idShow = ?",
|
|
"AND season = ?"
|
|
))
|
|
self.cursor.execute(query, (showid, seasonnumber,))
|
|
try:
|
|
seasonid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
self.cursor.execute("select coalesce(max(idSeason),0) from seasons")
|
|
seasonid = self.cursor.fetchone()[0] + 1
|
|
query = "INSERT INTO seasons(idSeason, idShow, season) values(?, ?, ?)"
|
|
self.cursor.execute(query, (seasonid, showid, seasonnumber))
|
|
|
|
return seasonid
|
|
|
|
def addArtist(self, name, musicbrainz):
|
|
|
|
query = ' '.join((
|
|
|
|
"SELECT idArtist, strArtist",
|
|
"FROM artist",
|
|
"WHERE strMusicBrainzArtistID = ?"
|
|
))
|
|
self.cursor.execute(query, (musicbrainz,))
|
|
try:
|
|
result = self.cursor.fetchone()
|
|
artistid = result[0]
|
|
artistname = result[1]
|
|
|
|
except TypeError:
|
|
|
|
query = ' '.join((
|
|
|
|
"SELECT idArtist",
|
|
"FROM artist",
|
|
"WHERE strArtist = ?",
|
|
"COLLATE NOCASE"
|
|
))
|
|
self.cursor.execute(query, (name,))
|
|
try:
|
|
artistid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
# Krypton has a dummy first entry idArtist: 1 strArtist:
|
|
# [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing
|
|
if v.KODIVERSION >= 17:
|
|
self.cursor.execute(
|
|
"select coalesce(max(idArtist),1) from artist")
|
|
else:
|
|
self.cursor.execute(
|
|
"select coalesce(max(idArtist),0) from artist")
|
|
artistid = self.cursor.fetchone()[0] + 1
|
|
query = (
|
|
'''
|
|
INSERT INTO artist(idArtist, strArtist, strMusicBrainzArtistID)
|
|
|
|
VALUES (?, ?, ?)
|
|
'''
|
|
)
|
|
self.cursor.execute(query, (artistid, name, musicbrainz))
|
|
else:
|
|
if artistname != name:
|
|
query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?"
|
|
self.cursor.execute(query, (name, artistid,))
|
|
|
|
return artistid
|
|
|
|
def addAlbum(self, name, musicbrainz):
|
|
|
|
query = ' '.join((
|
|
|
|
"SELECT idAlbum",
|
|
"FROM album",
|
|
"WHERE strMusicBrainzAlbumID = ?"
|
|
))
|
|
self.cursor.execute(query, (musicbrainz,))
|
|
try:
|
|
albumid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
# Create the album
|
|
self.cursor.execute("select coalesce(max(idAlbum),0) from album")
|
|
albumid = self.cursor.fetchone()[0] + 1
|
|
query = (
|
|
'''
|
|
INSERT INTO album(idAlbum, strAlbum, strMusicBrainzAlbumID, strReleaseType)
|
|
|
|
VALUES (?, ?, ?, ?)
|
|
'''
|
|
)
|
|
self.cursor.execute(query, (albumid, name, musicbrainz, "album"))
|
|
return albumid
|
|
|
|
def addMusicGenres(self, kodiid, genres, mediatype):
|
|
|
|
if mediatype == "album":
|
|
|
|
# Delete current genres for clean slate
|
|
query = ' '.join((
|
|
|
|
"DELETE FROM album_genre",
|
|
"WHERE idAlbum = ?"
|
|
))
|
|
self.cursor.execute(query, (kodiid,))
|
|
|
|
for genre in genres:
|
|
query = ' '.join((
|
|
|
|
"SELECT idGenre",
|
|
"FROM genre",
|
|
"WHERE strGenre = ?",
|
|
"COLLATE NOCASE"
|
|
))
|
|
self.cursor.execute(query, (genre,))
|
|
try:
|
|
genreid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
# Create the genre
|
|
self.cursor.execute("select coalesce(max(idGenre),0) from genre")
|
|
genreid = self.cursor.fetchone()[0] + 1
|
|
query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)"
|
|
self.cursor.execute(query, (genreid, genre))
|
|
|
|
query = "INSERT OR REPLACE INTO album_genre(idGenre, idAlbum) values(?, ?)"
|
|
self.cursor.execute(query, (genreid, kodiid))
|
|
|
|
elif mediatype == "song":
|
|
|
|
# Delete current genres for clean slate
|
|
query = ' '.join((
|
|
|
|
"DELETE FROM song_genre",
|
|
"WHERE idSong = ?"
|
|
))
|
|
self.cursor.execute(query, (kodiid,))
|
|
|
|
for genre in genres:
|
|
query = ' '.join((
|
|
|
|
"SELECT idGenre",
|
|
"FROM genre",
|
|
"WHERE strGenre = ?",
|
|
"COLLATE NOCASE"
|
|
))
|
|
self.cursor.execute(query, (genre,))
|
|
try:
|
|
genreid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
# Create the genre
|
|
self.cursor.execute("select coalesce(max(idGenre),0) from genre")
|
|
genreid = self.cursor.fetchone()[0] + 1
|
|
query = "INSERT INTO genre(idGenre, strGenre) values(?, ?)"
|
|
self.cursor.execute(query, (genreid, genre))
|
|
|
|
query = "INSERT OR REPLACE INTO song_genre(idGenre, idSong) values(?, ?)"
|
|
self.cursor.execute(query, (genreid, kodiid))
|
|
|
|
# Krypton only stuff ##############################
|
|
|
|
def update_userrating(self, kodi_id, kodi_type, userrating):
|
|
"""
|
|
Updates userrating for >=Krypton
|
|
"""
|
|
if kodi_type == v.KODI_TYPE_MOVIE:
|
|
ID = 'idMovie'
|
|
elif kodi_type == v.KODI_TYPE_EPISODE:
|
|
ID = 'idEpisode'
|
|
elif kodi_type == v.KODI_TYPE_SONG:
|
|
ID = 'idSong'
|
|
query = '''UPDATE %s SET userrating = ? WHERE ? = ?''' % kodi_type
|
|
self.cursor.execute(query, (userrating, ID, kodi_id))
|
|
|
|
def add_uniqueid(self, *args):
|
|
"""
|
|
Feed with:
|
|
uniqueid_id: int
|
|
media_id: int
|
|
media_type: string
|
|
value: string
|
|
type: e.g. 'imdb' or 'tvdb'
|
|
"""
|
|
query = '''
|
|
INSERT INTO uniqueid(
|
|
uniqueid_id, media_id, media_type, value, type)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
'''
|
|
self.cursor.execute(query, (args))
|
|
|
|
def get_uniqueid(self, kodi_id, kodi_type):
|
|
query = '''
|
|
SELECT uniqueid_id FROM uniqueid
|
|
WHERE media_id = ? AND media_type = ?
|
|
'''
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
try:
|
|
uniqueid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
self.cursor.execute(
|
|
'SELECT COALESCE(MAX(uniqueid_id),0) FROM uniqueid')
|
|
uniqueid = self.cursor.fetchone()[0] + 1
|
|
return uniqueid
|
|
|
|
def update_uniqueid(self, *args):
|
|
"""
|
|
Pass in media_id, media_type, value, type, uniqueid_id
|
|
"""
|
|
query = '''
|
|
UPDATE uniqueid
|
|
SET media_id = ?, media_type = ?, value = ?, type = ?
|
|
WHERE uniqueid_id = ?
|
|
'''
|
|
self.cursor.execute(query, (args))
|
|
|
|
def remove_uniqueid(self, kodi_id, kodi_type):
|
|
query = '''
|
|
DELETE FROM uniqueid
|
|
WHERE media_id = ? AND media_type = ?
|
|
'''
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
|
|
def get_ratingid(self, kodi_id, kodi_type):
|
|
query = '''
|
|
SELECT rating_id FROM rating
|
|
WHERE media_id = ? AND media_type = ?
|
|
'''
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
try:
|
|
ratingid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
self.cursor.execute('SELECT COALESCE(MAX(rating_id),0) FROM rating')
|
|
ratingid = self.cursor.fetchone()[0] + 1
|
|
return ratingid
|
|
|
|
def update_ratings(self, *args):
|
|
"""
|
|
Feed with media_id, media_type, rating_type, rating, votes, rating_id
|
|
"""
|
|
query = '''
|
|
UPDATE rating
|
|
SET media_id = ?,
|
|
media_type = ?,
|
|
rating_type = ?,
|
|
rating = ?,
|
|
votes = ?
|
|
WHERE rating_id = ?
|
|
'''
|
|
self.cursor.execute(query, (args))
|
|
|
|
def add_ratings(self, *args):
|
|
"""
|
|
feed with:
|
|
rating_id, media_id, media_type, rating_type, rating, votes
|
|
|
|
rating_type = 'default'
|
|
"""
|
|
query = '''
|
|
INSERT INTO rating(
|
|
rating_id, media_id, media_type, rating_type, rating, votes)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
'''
|
|
self.cursor.execute(query, (args))
|
|
|
|
def remove_ratings(self, kodi_id, kodi_type):
|
|
query = '''
|
|
DELETE FROM rating
|
|
WHERE media_id = ? AND media_type = ?
|
|
'''
|
|
self.cursor.execute(query, (kodi_id, kodi_type))
|
|
|
|
|
|
def kodiid_from_filename(path, kodi_type):
|
|
"""
|
|
Returns kodi_id if we have an item in the Kodi video or audio database with
|
|
said path. Feed with the Kodi itemtype, e.v. 'movie', 'song'
|
|
Returns None if not possible
|
|
"""
|
|
kodi_id = None
|
|
path = try_decode(path)
|
|
try:
|
|
filename = path.rsplit('/', 1)[1]
|
|
path = path.rsplit('/', 1)[0] + '/'
|
|
except IndexError:
|
|
filename = path.rsplit('\\', 1)[1]
|
|
path = path.rsplit('\\', 1)[0] + '\\'
|
|
if kodi_type == v.KODI_TYPE_SONG:
|
|
with GetKodiDB('music') as kodi_db:
|
|
try:
|
|
kodi_id, _ = kodi_db.music_id_from_filename(filename, path)
|
|
except TypeError:
|
|
LOG.debug('No Kodi audio db element found for path %s', path)
|
|
else:
|
|
with GetKodiDB('video') as kodi_db:
|
|
try:
|
|
kodi_id, _ = kodi_db.video_id_from_filename(filename, path)
|
|
except TypeError:
|
|
LOG.debug('No kodi video db element found for path %s', path)
|
|
return kodi_id
|