628 lines
22 KiB
Python
628 lines
22 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
from __future__ import absolute_import, division, unicode_literals
|
|
from logging import getLogger
|
|
|
|
from . import common
|
|
from .. import db, variables as v, app, timing
|
|
|
|
LOG = getLogger('PLEX.kodi_db.music')
|
|
|
|
|
|
class KodiMusicDB(common.KodiDBBase):
|
|
db_kind = 'music'
|
|
|
|
@db.catch_operationalerrors
|
|
def add_path(self, path):
|
|
"""
|
|
Add the path (unicode) to the music DB, if it does not exist already.
|
|
Returns the path id
|
|
"""
|
|
# SQL won't return existing paths otherwise
|
|
path = '' if path is None else path
|
|
self.cursor.execute('SELECT idPath FROM path WHERE strPath = ?',
|
|
(path,))
|
|
try:
|
|
pathid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
self.cursor.execute('INSERT INTO path(strPath, strHash) VALUES (?, ?)',
|
|
(path, '123'))
|
|
pathid = self.cursor.lastrowid
|
|
return pathid
|
|
|
|
@db.catch_operationalerrors
|
|
def setup_kodi_default_entries(self):
|
|
"""
|
|
Makes sure that we retain the Kodi standard databases. E.g. that there
|
|
is a dummy artist with ID 1
|
|
"""
|
|
self.cursor.execute('''
|
|
INSERT OR REPLACE INTO artist(
|
|
idArtist,
|
|
strArtist,
|
|
strMusicBrainzArtistID)
|
|
VALUES (?, ?, ?)
|
|
''', (1, '[Missing Tag]', 'Artist Tag Missing'))
|
|
self.cursor.execute('''
|
|
INSERT OR REPLACE INTO role(
|
|
idRole,
|
|
strRole)
|
|
VALUES (?, ?)
|
|
''', (1, 'Artist'))
|
|
if v.KODIVERSION >= 18:
|
|
self.cursor.execute('DELETE FROM versiontagscan')
|
|
self.cursor.execute('''
|
|
INSERT INTO versiontagscan(
|
|
idVersion,
|
|
iNeedsScan,
|
|
lastscanned)
|
|
VALUES (?, ?, ?)
|
|
''', (v.DB_MUSIC_VERSION,
|
|
0,
|
|
timing.kodi_now()))
|
|
|
|
@db.catch_operationalerrors
|
|
def update_path(self, path, kodi_pathid):
|
|
self.cursor.execute('''
|
|
UPDATE path
|
|
SET strPath = ?, strHash = ?
|
|
WHERE idPath = ?
|
|
''', (path, '123', kodi_pathid))
|
|
|
|
def song_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.
|
|
"""
|
|
self.cursor.execute('SELECT idPath FROM path WHERE strPath = ?',
|
|
(path, ))
|
|
path_ids = self.cursor.fetchall()
|
|
if len(path_ids) != 1:
|
|
LOG.debug('Found wrong number of path ids: %s for path %s, abort',
|
|
path_ids, path)
|
|
return
|
|
self.cursor.execute('SELECT idSong FROM song WHERE strFileName = ? AND idPath = ?',
|
|
(filename, path_ids[0][0]))
|
|
song_ids = self.cursor.fetchall()
|
|
if len(song_ids) != 1:
|
|
LOG.info('Found wrong number of songs %s, abort', song_ids)
|
|
return
|
|
return song_ids[0][0]
|
|
|
|
@db.catch_operationalerrors
|
|
def delete_song_from_song_artist(self, song_id):
|
|
"""
|
|
Deletes son from song_artist table and possibly orphaned roles
|
|
"""
|
|
self.cursor.execute('''
|
|
SELECT idArtist, idRole FROM song_artist
|
|
WHERE idSong = ? LIMIT 1
|
|
''', (song_id, ))
|
|
artist = self.cursor.fetchone()
|
|
if not artist:
|
|
# No entry to begin with
|
|
return
|
|
# Delete the entry
|
|
self.cursor.execute('DELETE FROM song_artist WHERE idSong = ?',
|
|
(song_id, ))
|
|
|
|
@db.catch_operationalerrors
|
|
def delete_song_from_song_genre(self, song_id):
|
|
"""
|
|
Deletes the one entry with id song_id from the song_genre table.
|
|
Will also delete orphaned genres from genre table
|
|
"""
|
|
self.cursor.execute('SELECT idGenre FROM song_genre WHERE idSong = ?',
|
|
(song_id, ))
|
|
genres = self.cursor.fetchall()
|
|
self.cursor.execute('DELETE FROM song_genre WHERE idSong = ?',
|
|
(song_id, ))
|
|
# Check for orphaned genres in both song_genre and album_genre tables
|
|
for genre in genres:
|
|
self.cursor.execute('SELECT idGenre FROM song_genre WHERE idGenre = ? LIMIT 1',
|
|
(genre[0], ))
|
|
if not self.cursor.fetchone():
|
|
self.cursor.execute('SELECT idGenre FROM album_genre WHERE idGenre = ? LIMIT 1',
|
|
(genre[0], ))
|
|
if not self.cursor.fetchone():
|
|
self.delete_genre(genre[0])
|
|
|
|
@db.catch_operationalerrors
|
|
def delete_genre(self, genre_id):
|
|
"""
|
|
Dedicated method in order to catch OperationalErrors correctly
|
|
"""
|
|
self.cursor.execute('DELETE FROM genre WHERE idGenre = ?',
|
|
(genre_id, ))
|
|
|
|
@db.catch_operationalerrors
|
|
def delete_album_from_album_genre(self, album_id):
|
|
"""
|
|
Deletes the one entry with id album_id from the album_genre table.
|
|
Will also delete orphaned genres from genre table
|
|
"""
|
|
self.cursor.execute('SELECT idGenre FROM album_genre WHERE idAlbum = ?',
|
|
(album_id, ))
|
|
genres = self.cursor.fetchall()
|
|
self.cursor.execute('DELETE FROM album_genre WHERE idAlbum = ?',
|
|
(album_id, ))
|
|
# Check for orphaned genres in both album_genre and song_genre tables
|
|
for genre in genres:
|
|
self.cursor.execute('SELECT idGenre FROM album_genre WHERE idGenre = ? LIMIT 1',
|
|
(genre[0], ))
|
|
if not self.cursor.fetchone():
|
|
self.cursor.execute('SELECT idGenre FROM song_genre WHERE idGenre = ? LIMIT 1',
|
|
(genre[0], ))
|
|
if not self.cursor.fetchone():
|
|
self.delete_genre(genre[0])
|
|
|
|
def new_album_id(self):
|
|
self.cursor.execute('SELECT COALESCE(MAX(idAlbum), 0) FROM album')
|
|
return self.cursor.fetchone()[0] + 1
|
|
|
|
@db.catch_operationalerrors
|
|
def add_album_17(self, *args):
|
|
"""
|
|
strReleaseType: 'album' or 'single'
|
|
"""
|
|
if app.SYNC.artwork:
|
|
self.cursor.execute('''
|
|
INSERT INTO album(
|
|
idAlbum,
|
|
strAlbum,
|
|
strMusicBrainzAlbumID,
|
|
strArtists,
|
|
strGenres,
|
|
iYear,
|
|
bCompilation,
|
|
strReview,
|
|
strImage,
|
|
strLabel,
|
|
iUserrating,
|
|
lastScraped,
|
|
strReleaseType)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (args))
|
|
else:
|
|
args = list(args)
|
|
del args[8]
|
|
self.cursor.execute('''
|
|
INSERT INTO album(
|
|
idAlbum,
|
|
strAlbum,
|
|
strMusicBrainzAlbumID,
|
|
strArtists,
|
|
strGenres,
|
|
iYear,
|
|
bCompilation,
|
|
strReview,
|
|
strLabel,
|
|
iUserrating,
|
|
lastScraped,
|
|
strReleaseType)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (args))
|
|
|
|
@db.catch_operationalerrors
|
|
def update_album_17(self, *args):
|
|
if app.SYNC.artwork:
|
|
self.cursor.execute('''
|
|
UPDATE album
|
|
SET strAlbum = ?,
|
|
strMusicBrainzAlbumID = ?,
|
|
strArtists = ?,
|
|
strGenres = ?,
|
|
iYear = ?,
|
|
bCompilation = ?,
|
|
strReview = ?,
|
|
strImage = ?,
|
|
strLabel = ?,
|
|
iUserrating = ?,
|
|
lastScraped = ?,
|
|
strReleaseType = ?
|
|
WHERE idAlbum = ?
|
|
''', (args))
|
|
else:
|
|
args = list(args)
|
|
del args[7]
|
|
self.cursor.execute('''
|
|
UPDATE album
|
|
SET strAlbum = ?,
|
|
strMusicBrainzAlbumID = ?,
|
|
strArtists = ?,
|
|
strGenres = ?,
|
|
iYear = ?,
|
|
bCompilation = ?,
|
|
strReview = ?,
|
|
strLabel = ?,
|
|
iUserrating = ?,
|
|
lastScraped = ?,
|
|
strReleaseType = ?
|
|
WHERE idAlbum = ?
|
|
''', (args))
|
|
|
|
@db.catch_operationalerrors
|
|
def add_album(self, *args):
|
|
"""
|
|
strReleaseType: 'album' or 'single'
|
|
"""
|
|
if app.SYNC.artwork:
|
|
self.cursor.execute('''
|
|
INSERT INTO album(
|
|
idAlbum,
|
|
strAlbum,
|
|
strMusicBrainzAlbumID,
|
|
strArtistDisp,
|
|
strGenres,
|
|
iYear,
|
|
bCompilation,
|
|
strReview,
|
|
strImage,
|
|
strLabel,
|
|
iUserrating,
|
|
lastScraped,
|
|
strReleaseType)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (args))
|
|
else:
|
|
args = list(args)
|
|
del args[8]
|
|
self.cursor.execute('''
|
|
INSERT INTO album(
|
|
idAlbum,
|
|
strAlbum,
|
|
strMusicBrainzAlbumID,
|
|
strArtistDisp,
|
|
strGenres,
|
|
iYear,
|
|
bCompilation,
|
|
strReview,
|
|
strLabel,
|
|
iUserrating,
|
|
lastScraped,
|
|
strReleaseType)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (args))
|
|
|
|
@db.catch_operationalerrors
|
|
def update_album(self, *args):
|
|
if app.SYNC.artwork:
|
|
self.cursor.execute('''
|
|
UPDATE album
|
|
SET strAlbum = ?,
|
|
strMusicBrainzAlbumID = ?,
|
|
strArtistDisp = ?,
|
|
strGenres = ?,
|
|
iYear = ?,
|
|
bCompilation = ?,
|
|
strReview = ?,
|
|
strImage = ?,
|
|
strLabel = ?,
|
|
iUserrating = ?,
|
|
lastScraped = ?,
|
|
strReleaseType = ?
|
|
WHERE idAlbum = ?
|
|
''', (args))
|
|
else:
|
|
args = list(args)
|
|
del args[7]
|
|
self.cursor.execute('''
|
|
UPDATE album
|
|
SET strAlbum = ?,
|
|
strMusicBrainzAlbumID = ?,
|
|
strArtistDisp = ?,
|
|
strGenres = ?,
|
|
iYear = ?,
|
|
bCompilation = ?,
|
|
strReview = ?,
|
|
strLabel = ?,
|
|
iUserrating = ?,
|
|
lastScraped = ?,
|
|
strReleaseType = ?
|
|
WHERE idAlbum = ?
|
|
''', (args))
|
|
|
|
@db.catch_operationalerrors
|
|
def add_albumartist(self, artist_id, kodi_id, artistname):
|
|
self.cursor.execute('''
|
|
INSERT OR REPLACE INTO album_artist(
|
|
idArtist,
|
|
idAlbum,
|
|
strArtist)
|
|
VALUES (?, ?, ?)
|
|
''', (artist_id, kodi_id, artistname))
|
|
|
|
@db.catch_operationalerrors
|
|
def add_music_genres(self, kodiid, genres, mediatype):
|
|
"""
|
|
Adds a list of genres (list of unicode) for a certain Kodi item
|
|
"""
|
|
if mediatype == "album":
|
|
# Delete current genres for clean slate
|
|
self.cursor.execute('DELETE FROM album_genre WHERE idAlbum = ?',
|
|
(kodiid, ))
|
|
for genre in genres:
|
|
self.cursor.execute('SELECT idGenre FROM genre WHERE strGenre = ?',
|
|
(genre, ))
|
|
try:
|
|
genreid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
# Create the genre
|
|
self.cursor.execute('INSERT INTO genre(strGenre) VALUES(?)',
|
|
(genre, ))
|
|
genreid = self.cursor.lastrowid
|
|
self.cursor.execute('''
|
|
INSERT OR REPLACE INTO album_genre(
|
|
idGenre,
|
|
idAlbum)
|
|
VALUES (?, ?)
|
|
''', (genreid, kodiid))
|
|
elif mediatype == "song":
|
|
# Delete current genres for clean slate
|
|
self.cursor.execute('DELETE FROM song_genre WHERE idSong = ?',
|
|
(kodiid, ))
|
|
for genre in genres:
|
|
self.cursor.execute('SELECT idGenre FROM genre WHERE strGenre = ?',
|
|
(genre, ))
|
|
try:
|
|
genreid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
# Create the genre
|
|
self.cursor.execute('INSERT INTO genre(strGenre) VALUES (?)',
|
|
(genre, ))
|
|
genreid = self.cursor.lastrowid
|
|
self.cursor.execute('''
|
|
INSERT OR REPLACE INTO song_genre(
|
|
idGenre,
|
|
idSong,
|
|
iOrder)
|
|
VALUES (?, ?, ?)
|
|
''', (genreid, kodiid, 0))
|
|
|
|
def add_song_id(self):
|
|
self.cursor.execute('SELECT COALESCE(MAX(idSong),0) FROM song')
|
|
return self.cursor.fetchone()[0] + 1
|
|
|
|
@db.catch_operationalerrors
|
|
def add_song(self, *args):
|
|
self.cursor.execute('''
|
|
INSERT INTO song(
|
|
idSong,
|
|
idAlbum,
|
|
idPath,
|
|
strArtistDisp,
|
|
strGenres,
|
|
strTitle,
|
|
iTrack,
|
|
iDuration,
|
|
iYear,
|
|
strFileName,
|
|
strMusicBrainzTrackID,
|
|
iTimesPlayed,
|
|
lastplayed,
|
|
rating,
|
|
iStartOffset,
|
|
iEndOffset,
|
|
mood,
|
|
dateAdded)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (args))
|
|
|
|
@db.catch_operationalerrors
|
|
def add_song_17(self, *args):
|
|
self.cursor.execute('''
|
|
INSERT INTO song(
|
|
idSong,
|
|
idAlbum,
|
|
idPath,
|
|
strArtists,
|
|
strGenres,
|
|
strTitle,
|
|
iTrack,
|
|
iDuration,
|
|
iYear,
|
|
strFileName,
|
|
strMusicBrainzTrackID,
|
|
iTimesPlayed,
|
|
lastplayed,
|
|
rating,
|
|
iStartOffset,
|
|
iEndOffset,
|
|
mood,
|
|
dateAdded)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (args))
|
|
|
|
@db.catch_operationalerrors
|
|
def update_song(self, *args):
|
|
self.cursor.execute('''
|
|
UPDATE song
|
|
SET idAlbum = ?,
|
|
strArtistDisp = ?,
|
|
strGenres = ?,
|
|
strTitle = ?,
|
|
iTrack = ?,
|
|
iDuration = ?,
|
|
iYear = ?,
|
|
strFilename = ?,
|
|
iTimesPlayed = ?,
|
|
lastplayed = ?,
|
|
rating = ?,
|
|
comment = ?,
|
|
mood = ?,
|
|
dateAdded = ?
|
|
WHERE idSong = ?
|
|
''', (args))
|
|
|
|
@db.catch_operationalerrors
|
|
def set_playcount(self, *args):
|
|
self.cursor.execute('''
|
|
UPDATE song
|
|
SET iTimesPlayed = ?,
|
|
lastplayed = ?
|
|
WHERE idSong = ?
|
|
''', (args))
|
|
|
|
@db.catch_operationalerrors
|
|
def update_song_17(self, *args):
|
|
self.cursor.execute('''
|
|
UPDATE song
|
|
SET idAlbum = ?,
|
|
strArtists = ?,
|
|
strGenres = ?,
|
|
strTitle = ?,
|
|
iTrack = ?,
|
|
iDuration = ?,
|
|
iYear = ?,
|
|
strFilename = ?,
|
|
iTimesPlayed = ?,
|
|
lastplayed = ?,
|
|
rating = ?,
|
|
comment = ?,
|
|
mood = ?,
|
|
dateAdded = ?
|
|
WHERE idSong = ?
|
|
''', (args))
|
|
|
|
def path_id_from_song(self, kodi_id):
|
|
self.cursor.execute('SELECT idPath FROM song WHERE idSong = ? LIMIT 1',
|
|
(kodi_id, ))
|
|
try:
|
|
return self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
pass
|
|
|
|
@db.catch_operationalerrors
|
|
def add_artist(self, name, musicbrainz):
|
|
"""
|
|
Adds a single artist's name to the db
|
|
"""
|
|
self.cursor.execute('''
|
|
SELECT idArtist, strArtist
|
|
FROM artist
|
|
WHERE strMusicBrainzArtistID = ?
|
|
''', (musicbrainz, ))
|
|
try:
|
|
result = self.cursor.fetchone()
|
|
artistid = result[0]
|
|
artistname = result[1]
|
|
except TypeError:
|
|
self.cursor.execute('SELECT idArtist FROM artist WHERE strArtist = ? COLLATE NOCASE',
|
|
(name, ))
|
|
try:
|
|
artistid = self.cursor.fetchone()[0]
|
|
except TypeError:
|
|
# Krypton has a dummy first entry idArtist: 1 strArtist:
|
|
# [Missing Tag] strMusicBrainzArtistID: Artist Tag Missing
|
|
self.cursor.execute('''
|
|
INSERT INTO artist(strArtist, strMusicBrainzArtistID)
|
|
VALUES (?, ?)
|
|
''', (name, musicbrainz))
|
|
artistid = self.cursor.lastrowid
|
|
else:
|
|
if artistname != name:
|
|
self.cursor.execute('UPDATE artist SET strArtist = ? WHERE idArtist = ?',
|
|
(name, artistid,))
|
|
return artistid
|
|
|
|
@db.catch_operationalerrors
|
|
def update_artist(self, *args):
|
|
if app.SYNC.artwork:
|
|
self.cursor.execute('''
|
|
UPDATE artist
|
|
SET strGenres = ?,
|
|
strBiography = ?,
|
|
strImage = ?,
|
|
strFanart = ?,
|
|
lastScraped = ?
|
|
WHERE idArtist = ?
|
|
''', (args))
|
|
else:
|
|
args = list(args)
|
|
del args[3], args[2]
|
|
self.cursor.execute('''
|
|
UPDATE artist
|
|
SET strGenres = ?,
|
|
strBiography = ?,
|
|
lastScraped = ?
|
|
WHERE idArtist = ?
|
|
''', (args))
|
|
|
|
@db.catch_operationalerrors
|
|
def remove_song(self, kodi_id):
|
|
self.cursor.execute('DELETE FROM song WHERE idSong = ?', (kodi_id, ))
|
|
|
|
@db.catch_operationalerrors
|
|
def remove_path(self, path_id):
|
|
self.cursor.execute('DELETE FROM path WHERE idPath = ?', (path_id, ))
|
|
|
|
@db.catch_operationalerrors
|
|
def add_song_artist(self, artist_id, song_id, artist_name):
|
|
self.cursor.execute('''
|
|
INSERT OR REPLACE INTO song_artist(
|
|
idArtist,
|
|
idSong,
|
|
idRole,
|
|
iOrder,
|
|
strArtist)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', (artist_id, song_id, 1, 0, artist_name))
|
|
|
|
@db.catch_operationalerrors
|
|
def add_albuminfosong(self, song_id, album_id, track_no, track_title,
|
|
runtime):
|
|
"""
|
|
Kodi 17 only
|
|
"""
|
|
self.cursor.execute('''
|
|
INSERT OR REPLACE INTO albuminfosong(
|
|
idAlbumInfoSong,
|
|
idAlbumInfo,
|
|
iTrack,
|
|
strTitle,
|
|
iDuration)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', (song_id, album_id, track_no, track_title, runtime))
|
|
|
|
@db.catch_operationalerrors
|
|
def update_userrating(self, kodi_id, kodi_type, userrating):
|
|
"""
|
|
Updates userrating for songs and albums
|
|
"""
|
|
if kodi_type == v.KODI_TYPE_SONG:
|
|
column = 'userrating'
|
|
identifier = 'idSong'
|
|
elif kodi_type == v.KODI_TYPE_ALBUM:
|
|
column = 'iUserrating'
|
|
identifier = 'idAlbum'
|
|
else:
|
|
return
|
|
self.cursor.execute('''UPDATE %s SET %s = ? WHERE ? = ?'''
|
|
% (kodi_type, column),
|
|
(userrating, identifier, kodi_id))
|
|
|
|
@db.catch_operationalerrors
|
|
def remove_albuminfosong(self, kodi_id):
|
|
"""
|
|
Kodi 17 only
|
|
"""
|
|
self.cursor.execute('DELETE FROM albuminfosong WHERE idAlbumInfoSong = ?',
|
|
(kodi_id, ))
|
|
|
|
@db.catch_operationalerrors
|
|
def remove_album(self, kodi_id):
|
|
if v.KODIVERSION < 18:
|
|
self.cursor.execute('DELETE FROM albuminfosong WHERE idAlbumInfo = ?',
|
|
(kodi_id, ))
|
|
self.cursor.execute('DELETE FROM album_artist WHERE idAlbum = ?',
|
|
(kodi_id, ))
|
|
self.cursor.execute('DELETE FROM album WHERE idAlbum = ?', (kodi_id, ))
|
|
|
|
@db.catch_operationalerrors
|
|
def remove_artist(self, kodi_id):
|
|
self.cursor.execute('DELETE FROM album_artist WHERE idArtist = ?',
|
|
(kodi_id, ))
|
|
self.cursor.execute('DELETE FROM artist WHERE idArtist = ?',
|
|
(kodi_id, ))
|
|
self.cursor.execute('DELETE FROM song_artist WHERE idArtist = ?',
|
|
(kodi_id, ))
|