|
|
|
@ -9,9 +9,9 @@ logging = logging.getLogger("database")
|
|
|
|
|
keys_in_table = ["title", "album", "artist", "type", "size"] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def dict_factory(cursor, row): |
|
|
|
|
def dict_factory(c, row): |
|
|
|
|
d = {} |
|
|
|
|
for idx, col in enumerate(cursor.description): |
|
|
|
|
for idx, col in enumerate(c.description): |
|
|
|
|
d[col[0]] = row[idx] |
|
|
|
|
return d |
|
|
|
|
|
|
|
|
@ -28,7 +28,7 @@ def hash_password(unicode_string):
|
|
|
|
|
return sha512(unicode_string.encode('UTF-8')).hexdigest() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def readcursor(func): |
|
|
|
|
def cursor(func): |
|
|
|
|
""" |
|
|
|
|
Provides a cursor to the wrapped method as the first arg. |
|
|
|
|
""" |
|
|
|
@ -37,8 +37,8 @@ def readcursor(func):
|
|
|
|
|
if len(args) >= 2 and isinstance(args[1], sqlite3.Cursor): |
|
|
|
|
return func(*args, **kwargs) |
|
|
|
|
else: |
|
|
|
|
with closing(self.db.cursor()) as cursor: |
|
|
|
|
return func(self, cursor, *args[1:], **kwargs) |
|
|
|
|
with closing(self.db.cursor()) as c: |
|
|
|
|
return func(self, c, *args[1:], **kwargs) |
|
|
|
|
return wrapped |
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -135,31 +135,31 @@ class PysonicDatabase(object):
|
|
|
|
|
'value' TEXT);""", |
|
|
|
|
"""INSERT INTO meta VALUES ('db_version', '1');"""] |
|
|
|
|
|
|
|
|
|
with closing(self.db.cursor()) as cursor: |
|
|
|
|
cursor.execute("SELECT * FROM sqlite_master WHERE type='table' AND name='meta'") |
|
|
|
|
with closing(self.db.cursor()) as c: |
|
|
|
|
c.execute("SELECT * FROM sqlite_master WHERE type='table' AND name='meta'") |
|
|
|
|
|
|
|
|
|
# Initialize DB |
|
|
|
|
if len(cursor.fetchall()) == 0: |
|
|
|
|
if len(c.fetchall()) == 0: |
|
|
|
|
logging.warning("Initializing database") |
|
|
|
|
for query in queries: |
|
|
|
|
cursor.execute(query) |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
c.execute(query) |
|
|
|
|
c.execute("COMMIT") |
|
|
|
|
else: |
|
|
|
|
# Migrate if old db exists |
|
|
|
|
# cursor.execute("""UPDATE meta SET value=? WHERE key="db_version";""", (str(version), )) |
|
|
|
|
# c.execute("""UPDATE meta SET value=? WHERE key="db_version";""", (str(version), )) |
|
|
|
|
# logging.warning("db schema is version {}".format(version)) |
|
|
|
|
pass |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_stats(self, cursor): |
|
|
|
|
songs = cursor.execute("SELECT COUNT(*) as cnt FROM songs").fetchone()['cnt'] |
|
|
|
|
artists = cursor.execute("SELECT COUNT(*) as cnt FROM artists").fetchone()['cnt'] |
|
|
|
|
albums = cursor.execute("SELECT COUNT(*) as cnt FROM albums").fetchone()['cnt'] |
|
|
|
|
@cursor |
|
|
|
|
def get_stats(self, c): |
|
|
|
|
songs = c.execute("SELECT COUNT(*) as cnt FROM songs").fetchone()['cnt'] |
|
|
|
|
artists = c.execute("SELECT COUNT(*) as cnt FROM artists").fetchone()['cnt'] |
|
|
|
|
albums = c.execute("SELECT COUNT(*) as cnt FROM albums").fetchone()['cnt'] |
|
|
|
|
return dict(songs=songs, artists=artists, albums=albums) |
|
|
|
|
|
|
|
|
|
# Music related |
|
|
|
|
@readcursor |
|
|
|
|
def add_root(self, cursor, path, name="Library"): |
|
|
|
|
@cursor |
|
|
|
|
def add_root(self, c, path, name="Library"): |
|
|
|
|
""" |
|
|
|
|
Add a new library root. Returns the root ID or raises on collision |
|
|
|
|
:param path: normalized absolute path to add to the library |
|
|
|
@ -169,14 +169,14 @@ class PysonicDatabase(object):
|
|
|
|
|
""" |
|
|
|
|
assert path.startswith("/") |
|
|
|
|
try: |
|
|
|
|
cursor.execute("INSERT INTO libraries ('name', 'path') VALUES (?, ?)", (name, path, )) |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
return cursor.lastrowid |
|
|
|
|
c.execute("INSERT INTO libraries ('name', 'path') VALUES (?, ?)", (name, path, )) |
|
|
|
|
c.execute("COMMIT") |
|
|
|
|
return c.lastrowid |
|
|
|
|
except sqlite3.IntegrityError: |
|
|
|
|
raise DuplicateRootException("Root '{}' already exists".format(path)) |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_libraries(self, cursor, id=None): |
|
|
|
|
@cursor |
|
|
|
|
def get_libraries(self, c, id=None): |
|
|
|
|
libs = [] |
|
|
|
|
q = "SELECT * FROM libraries" |
|
|
|
|
params = [] |
|
|
|
@ -186,13 +186,13 @@ class PysonicDatabase(object):
|
|
|
|
|
params.append(id) |
|
|
|
|
if conditions: |
|
|
|
|
q += " WHERE " + " AND ".join(conditions) |
|
|
|
|
cursor.execute(q, params) |
|
|
|
|
for row in cursor: |
|
|
|
|
c.execute(q, params) |
|
|
|
|
for row in c: |
|
|
|
|
libs.append(row) |
|
|
|
|
return libs |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_artists(self, cursor, id=None, dirid=None, sortby="name", order=None, name_contains=None): |
|
|
|
|
@cursor |
|
|
|
|
def get_artists(self, c, id=None, dirid=None, sortby="name", order=None, name_contains=None): |
|
|
|
|
assert order in ["asc", "desc", None] |
|
|
|
|
artists = [] |
|
|
|
|
q = "SELECT * FROM artists" |
|
|
|
@ -211,13 +211,13 @@ class PysonicDatabase(object):
|
|
|
|
|
q += " WHERE " + " AND ".join(conditions) |
|
|
|
|
if sortby: |
|
|
|
|
q += " ORDER BY {} {}".format(sortby, order.upper() if order else "ASC") |
|
|
|
|
cursor.execute(q, params) |
|
|
|
|
for row in cursor: |
|
|
|
|
c.execute(q, params) |
|
|
|
|
for row in c: |
|
|
|
|
artists.append(row) |
|
|
|
|
return artists |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_albums(self, cursor, id=None, artist=None, sortby="name", order=None, limit=None, name_contains=None): |
|
|
|
|
@cursor |
|
|
|
|
def get_albums(self, c, id=None, artist=None, sortby="name", order=None, limit=None, name_contains=None): |
|
|
|
|
""" |
|
|
|
|
:param limit: int or tuple of int, int. translates directly to sql logic. |
|
|
|
|
""" |
|
|
|
@ -264,13 +264,13 @@ class PysonicDatabase(object):
|
|
|
|
|
q += " LIMIT {}".format(limit) if isinstance(limit, int) \ |
|
|
|
|
else " LIMIT {}, {}".format(*limit) |
|
|
|
|
|
|
|
|
|
cursor.execute(q, params) |
|
|
|
|
for row in cursor: |
|
|
|
|
c.execute(q, params) |
|
|
|
|
for row in c: |
|
|
|
|
albums.append(row) |
|
|
|
|
return albums |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_songs(self, cursor, id=None, genre=None, sortby="title", order=None, limit=None, title_contains=None): |
|
|
|
|
@cursor |
|
|
|
|
def get_songs(self, c, id=None, genre=None, sortby="title", order=None, limit=None, title_contains=None): |
|
|
|
|
# TODO make this query massively uglier by joining albums and artists so that artistid etc can be a filter |
|
|
|
|
# or maybe lookup those IDs in the library layer? |
|
|
|
|
if order: |
|
|
|
@ -326,13 +326,13 @@ class PysonicDatabase(object):
|
|
|
|
|
if limit: |
|
|
|
|
q += " LIMIT {}".format(limit) # TODO support limit pagination |
|
|
|
|
|
|
|
|
|
cursor.execute(q, params) |
|
|
|
|
for row in cursor: |
|
|
|
|
c.execute(q, params) |
|
|
|
|
for row in c: |
|
|
|
|
songs.append(row) |
|
|
|
|
return songs |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_genres(self, cursor, genre_id=None): |
|
|
|
|
@cursor |
|
|
|
|
def get_genres(self, c, genre_id=None): |
|
|
|
|
genres = [] |
|
|
|
|
q = "SELECT * FROM genres" |
|
|
|
|
params = [] |
|
|
|
@ -342,19 +342,19 @@ class PysonicDatabase(object):
|
|
|
|
|
params.append(genre_id) |
|
|
|
|
if conditions: |
|
|
|
|
q += " WHERE " + " AND ".join(conditions) |
|
|
|
|
cursor.execute(q, params) |
|
|
|
|
for row in cursor: |
|
|
|
|
c.execute(q, params) |
|
|
|
|
for row in c: |
|
|
|
|
genres.append(row) |
|
|
|
|
return genres |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_cover(self, cursor, coverid): |
|
|
|
|
@cursor |
|
|
|
|
def get_cover(self, c, coverid): |
|
|
|
|
cover = None |
|
|
|
|
for cover in cursor.execute("SELECT * FROM covers WHERE id = ?", (coverid, )): |
|
|
|
|
for cover in c.execute("SELECT * FROM covers WHERE id = ?", (coverid, )): |
|
|
|
|
return cover |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_subsonic_musicdir(self, cursor, dirid): |
|
|
|
|
@cursor |
|
|
|
|
def get_subsonic_musicdir(self, c, dirid): |
|
|
|
|
""" |
|
|
|
|
The world is a harsh place. |
|
|
|
|
Again, this bullshit exists only to serve subsonic clients. Given a directory ID it returns a dict containing: |
|
|
|
@ -367,7 +367,7 @@ class PysonicDatabase(object):
|
|
|
|
|
""" |
|
|
|
|
# find directory |
|
|
|
|
dirinfo = None |
|
|
|
|
for dirinfo in cursor.execute("SELECT * FROM dirs WHERE id = ?", (dirid, )): |
|
|
|
|
for dirinfo in c.execute("SELECT * FROM dirs WHERE id = ?", (dirid, )): |
|
|
|
|
pass |
|
|
|
|
assert dirinfo |
|
|
|
|
|
|
|
|
@ -375,7 +375,7 @@ class PysonicDatabase(object):
|
|
|
|
|
|
|
|
|
|
# see if it matches the artists or albums table |
|
|
|
|
artist = None |
|
|
|
|
for artist in cursor.execute("SELECT * FROM artists WHERE dir = ?", (dirid, )): |
|
|
|
|
for artist in c.execute("SELECT * FROM artists WHERE dir = ?", (dirid, )): |
|
|
|
|
pass |
|
|
|
|
|
|
|
|
|
# if artist: |
|
|
|
@ -383,7 +383,7 @@ class PysonicDatabase(object):
|
|
|
|
|
if artist: |
|
|
|
|
ret = ("artist", dirinfo, artist) |
|
|
|
|
children = [] |
|
|
|
|
for album in cursor.execute("SELECT * FROM albums WHERE artistid = ?", (artist["id"], )): |
|
|
|
|
for album in c.execute("SELECT * FROM albums WHERE artistid = ?", (artist["id"], )): |
|
|
|
|
children.append(("album", album)) |
|
|
|
|
ret[2]['children'] = children |
|
|
|
|
return ret |
|
|
|
@ -391,45 +391,45 @@ class PysonicDatabase(object):
|
|
|
|
|
# else if album: |
|
|
|
|
# get child tracks |
|
|
|
|
album = None |
|
|
|
|
for album in cursor.execute("SELECT * FROM albums WHERE dir = ?", (dirid, )): |
|
|
|
|
for album in c.execute("SELECT * FROM albums WHERE dir = ?", (dirid, )): |
|
|
|
|
pass |
|
|
|
|
if album: |
|
|
|
|
ret = ("album", dirinfo, album) |
|
|
|
|
|
|
|
|
|
artist_info = cursor.execute("SELECT * FROM artists WHERE id = ?", (album["artistid"], )).fetchall()[0] |
|
|
|
|
artist_info = c.execute("SELECT * FROM artists WHERE id = ?", (album["artistid"], )).fetchall()[0] |
|
|
|
|
|
|
|
|
|
children = [] |
|
|
|
|
for song in cursor.execute("SELECT * FROM songs WHERE albumid = ? ORDER BY track, title ASC;", (album["id"], )): |
|
|
|
|
for song in c.execute("SELECT * FROM songs WHERE albumid = ? ORDER BY track, title ASC;", (album["id"], )): |
|
|
|
|
song["_artist"] = artist_info |
|
|
|
|
children.append(("song", song)) |
|
|
|
|
ret[2]['children'] = children |
|
|
|
|
return ret |
|
|
|
|
|
|
|
|
|
# Playlist related |
|
|
|
|
@readcursor |
|
|
|
|
def add_playlist(self, cursor, ownerid, name, song_ids, public=False): |
|
|
|
|
@cursor |
|
|
|
|
def add_playlist(self, c, ownerid, name, song_ids, public=False): |
|
|
|
|
""" |
|
|
|
|
Create a playlist |
|
|
|
|
""" |
|
|
|
|
now = time() |
|
|
|
|
cursor.execute("INSERT INTO playlists (ownerid, name, public, created, changed) VALUES (?, ?, ?, ?, ?)", |
|
|
|
|
(ownerid, name, public, now, now)) |
|
|
|
|
plid = cursor.lastrowid |
|
|
|
|
c.execute("INSERT INTO playlists (ownerid, name, public, created, changed) VALUES (?, ?, ?, ?, ?)", |
|
|
|
|
(ownerid, name, public, now, now)) |
|
|
|
|
plid = c.lastrowid |
|
|
|
|
for song_id in song_ids: |
|
|
|
|
self.add_to_playlist(cursor, plid, song_id) |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
self.add_to_playlist(c, plid, song_id) |
|
|
|
|
c.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def add_to_playlist(self, cursor, playlist_id, song_id): |
|
|
|
|
@cursor |
|
|
|
|
def add_to_playlist(self, c, playlist_id, song_id): |
|
|
|
|
# TODO deal with order column |
|
|
|
|
cursor.execute("INSERT INTO playlist_entries (playlistid, songid) VALUES (?, ?)", (playlist_id, song_id)) |
|
|
|
|
c.execute("INSERT INTO playlist_entries (playlistid, songid) VALUES (?, ?)", (playlist_id, song_id)) |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_playlist(self, cursor, playlist_id): |
|
|
|
|
return cursor.execute("SELECT * FROM playlists WHERE id=?", (playlist_id, )).fetchone() |
|
|
|
|
@cursor |
|
|
|
|
def get_playlist(self, c, playlist_id): |
|
|
|
|
return c.execute("SELECT * FROM playlists WHERE id=?", (playlist_id, )).fetchone() |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_playlist_songs(self, cursor, playlist_id): |
|
|
|
|
@cursor |
|
|
|
|
def get_playlist_songs(self, c, playlist_id): |
|
|
|
|
songs = [] |
|
|
|
|
q = """ |
|
|
|
|
SELECT |
|
|
|
@ -451,65 +451,65 @@ class PysonicDatabase(object):
|
|
|
|
|
WHERE pe.playlistid = ? |
|
|
|
|
ORDER BY pe.'order' ASC; |
|
|
|
|
""" |
|
|
|
|
for row in cursor.execute(q, (playlist_id, )): |
|
|
|
|
for row in c.execute(q, (playlist_id, )): |
|
|
|
|
songs.append(row) |
|
|
|
|
return songs |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_playlists(self, cursor, user_id): |
|
|
|
|
@cursor |
|
|
|
|
def get_playlists(self, c, user_id): |
|
|
|
|
playlists = [] |
|
|
|
|
for row in cursor.execute("SELECT * FROM playlists WHERE ownerid=? or public=1", (user_id, )): |
|
|
|
|
for row in c.execute("SELECT * FROM playlists WHERE ownerid=? or public=1", (user_id, )): |
|
|
|
|
playlists.append(row) |
|
|
|
|
return playlists |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def remove_index_from_playlist(self, cursor, playlist_id, index): |
|
|
|
|
cursor.execute("DELETE FROM playlist_entries WHERE playlistid=? LIMIT ?, 1", (playlist_id, index, )) |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def empty_playlist(self, cursor, playlist_id): |
|
|
|
|
#TODO combine with # TODO combine with |
|
|
|
|
cursor.execute("DELETE FROM playlist_entries WHERE playlistid=?", (playlist_id, )) |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def delete_playlist(self, cursor, playlist_id): |
|
|
|
|
cursor.execute("DELETE FROM playlists WHERE id=?", (playlist_id, )) |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def update_album_played(self, cursor, album_id, last_played=None): |
|
|
|
|
cursor.execute("UPDATE albums SET played=? WHERE id=?", (last_played, album_id, )) |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def increment_album_plays(self, cursor, album_id): |
|
|
|
|
cursor.execute("UPDATE albums SET plays = plays + 1 WHERE id=?", (album_id, )) |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def increment_track_plays(self, cursor, track_id): |
|
|
|
|
cursor.execute("UPDATE songs SET plays = plays + 1 WHERE id=?", (track_id, )) |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
@cursor |
|
|
|
|
def remove_index_from_playlist(self, c, playlist_id, index): |
|
|
|
|
c.execute("DELETE FROM playlist_entries WHERE playlistid=? LIMIT ?, 1", (playlist_id, index, )) |
|
|
|
|
c.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@cursor |
|
|
|
|
def empty_playlist(self, c, playlist_id): |
|
|
|
|
#TODO combine with ?? |
|
|
|
|
c.execute("DELETE FROM playlist_entries WHERE playlistid=?", (playlist_id, )) |
|
|
|
|
c.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@cursor |
|
|
|
|
def delete_playlist(self, c, playlist_id): |
|
|
|
|
c.execute("DELETE FROM playlists WHERE id=?", (playlist_id, )) |
|
|
|
|
c.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@cursor |
|
|
|
|
def update_album_played(self, c, album_id, last_played=None): |
|
|
|
|
c.execute("UPDATE albums SET played=? WHERE id=?", (last_played, album_id, )) |
|
|
|
|
c.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@cursor |
|
|
|
|
def increment_album_plays(self, c, album_id): |
|
|
|
|
c.execute("UPDATE albums SET plays = plays + 1 WHERE id=?", (album_id, )) |
|
|
|
|
c.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@cursor |
|
|
|
|
def increment_track_plays(self, c, track_id): |
|
|
|
|
c.execute("UPDATE songs SET plays = plays + 1 WHERE id=?", (track_id, )) |
|
|
|
|
c.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
# User related |
|
|
|
|
@readcursor |
|
|
|
|
def add_user(self, cursor, username, password, is_admin=False): |
|
|
|
|
cursor.execute("INSERT INTO users (username, password, admin) VALUES (?, ?, ?)", |
|
|
|
|
(username, hash_password(password), is_admin)) |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def update_user(self, cursor, username, password, is_admin=False): |
|
|
|
|
cursor.execute("UPDATE users SET password=?, admin=? WHERE username=?;", |
|
|
|
|
(hash_password(password), is_admin, username)) |
|
|
|
|
cursor.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@readcursor |
|
|
|
|
def get_user(self, cursor, user): |
|
|
|
|
@cursor |
|
|
|
|
def add_user(self, c, username, password, is_admin=False): |
|
|
|
|
c.execute("INSERT INTO users (username, password, admin) VALUES (?, ?, ?)", |
|
|
|
|
(username, hash_password(password), is_admin)) |
|
|
|
|
c.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@cursor |
|
|
|
|
def update_user(self, c, username, password, is_admin=False): |
|
|
|
|
c.execute("UPDATE users SET password=?, admin=? WHERE username=?;", |
|
|
|
|
(hash_password(password), is_admin, username)) |
|
|
|
|
c.execute("COMMIT") |
|
|
|
|
|
|
|
|
|
@cursor |
|
|
|
|
def get_user(self, c, user): |
|
|
|
|
try: |
|
|
|
|
column = "id" if type(user) is int else "username" |
|
|
|
|
return cursor.execute("SELECT * FROM users WHERE {}=?;".format(column), (user, )).fetchall()[0] |
|
|
|
|
return c.execute("SELECT * FROM users WHERE {}=?;".format(column), (user, )).fetchall()[0] |
|
|
|
|
except IndexError: |
|
|
|
|
raise NotFoundError("User doesn't exist") |
|
|
|
|