#!/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, ))