2017-08-13 18:56:13 -07:00
|
|
|
import os
|
|
|
|
import json
|
|
|
|
import sqlite3
|
2017-08-13 21:13:46 -07:00
|
|
|
import logging
|
2017-08-15 20:26:03 -07:00
|
|
|
from hashlib import sha512
|
2017-08-13 18:56:13 -07:00
|
|
|
from contextlib import closing
|
|
|
|
|
|
|
|
|
2017-08-13 21:13:46 -07:00
|
|
|
logging = logging.getLogger("database")
|
2017-08-19 22:03:09 -07:00
|
|
|
keys_in_table = ["title", "album", "artist", "type", "size"]
|
2017-08-13 21:13:46 -07:00
|
|
|
|
|
|
|
|
2017-08-13 18:56:13 -07:00
|
|
|
def dict_factory(cursor, row):
|
|
|
|
d = {}
|
|
|
|
for idx, col in enumerate(cursor.description):
|
|
|
|
d[col[0]] = row[idx]
|
|
|
|
return d
|
|
|
|
|
|
|
|
|
2017-08-15 21:40:38 -07:00
|
|
|
class NotFoundError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2018-04-02 21:58:48 -07:00
|
|
|
class DuplicateRootException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def readcursor(func):
|
|
|
|
"""
|
|
|
|
Provides a cursor to the wrapped method as the first arg
|
|
|
|
"""
|
|
|
|
def wrapped(*args, **kwargs):
|
|
|
|
self = args[0]
|
|
|
|
with closing(self.db.cursor()) as cursor:
|
|
|
|
return func(*[self, cursor], *args[1:], **kwargs)
|
|
|
|
return wrapped
|
|
|
|
|
|
|
|
|
2017-08-13 18:56:13 -07:00
|
|
|
class PysonicDatabase(object):
|
|
|
|
def __init__(self, path):
|
2018-04-02 21:58:48 -07:00
|
|
|
self.sqlite_opts = dict(check_same_thread=False)
|
2017-08-13 18:56:13 -07:00
|
|
|
self.path = path
|
|
|
|
self.db = None
|
|
|
|
self.open()
|
|
|
|
self.migrate()
|
|
|
|
|
|
|
|
def open(self):
|
|
|
|
self.db = sqlite3.connect(self.path, **self.sqlite_opts)
|
|
|
|
self.db.row_factory = dict_factory
|
|
|
|
|
|
|
|
def migrate(self):
|
|
|
|
# Create db
|
2018-04-02 21:58:48 -07:00
|
|
|
queries = ["""CREATE TABLE 'libraries' (
|
|
|
|
'id' INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
'name' TEXT,
|
|
|
|
'path' TEXT UNIQUE);""",
|
|
|
|
"""CREATE TABLE 'artists' (
|
|
|
|
'id' INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
'libraryid' INTEGER,
|
|
|
|
'dir' TEXT UNIQUE,
|
|
|
|
'name' TEXT)""",
|
|
|
|
"""CREATE TABLE 'albums' (
|
|
|
|
'id' INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
'artistid' INTEGER,
|
|
|
|
'coverid' INTEGER,
|
|
|
|
'dir' TEXT,
|
|
|
|
'name' TEXT,
|
|
|
|
UNIQUE (artistid, dir));""",
|
|
|
|
"""CREATE TABLE 'songs' (
|
|
|
|
'id' INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
'albumid' BOOLEAN,
|
|
|
|
'file' TEXT UNIQUE, -- path from the library root
|
|
|
|
'size' INTEGER NOT NULL DEFAULT -1,
|
|
|
|
'title' TEXT NOT NULL,
|
|
|
|
'lastscan' INTEGER NOT NULL DEFAULT -1,
|
|
|
|
'format' TEXT,
|
|
|
|
'length' INTEGER,
|
|
|
|
'bitrate' INTEGER,
|
|
|
|
'track' INTEGER,
|
|
|
|
'year' INTEGER
|
2017-08-19 22:03:09 -07:00
|
|
|
)""",
|
2018-04-02 21:58:48 -07:00
|
|
|
"""CREATE TABLE 'covers' (
|
|
|
|
'id' INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
'type' TEXT,
|
|
|
|
'size' TEXT,
|
|
|
|
'path' TEXT UNIQUE);""",
|
2017-08-19 22:03:09 -07:00
|
|
|
"""CREATE TABLE 'users' (
|
2018-04-02 21:58:48 -07:00
|
|
|
'id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
|
|
'username' TEXT UNIQUE NOT NULL,
|
|
|
|
'password' TEXT NOT NULL,
|
|
|
|
'admin' BOOLEAN DEFAULT 0,
|
|
|
|
'email' TEXT)""",
|
2017-08-19 22:03:09 -07:00
|
|
|
"""CREATE TABLE 'stars' (
|
2018-04-02 21:58:48 -07:00
|
|
|
'userid' INTEGER,
|
|
|
|
'songid' INTEGER,
|
|
|
|
primary key ('userid', 'songid'))""",
|
|
|
|
"""CREATE TABLE 'meta' (
|
|
|
|
'key' TEXT PRIMARY KEY NOT NULL,
|
|
|
|
'value' TEXT);""",
|
|
|
|
"""INSERT INTO meta VALUES ('db_version', '1');"""]
|
2017-08-13 18:56:13 -07:00
|
|
|
|
|
|
|
with closing(self.db.cursor()) as cursor:
|
2018-04-02 21:58:48 -07:00
|
|
|
cursor.execute("SELECT * FROM sqlite_master WHERE type='table' AND name='meta'")
|
2017-08-13 18:56:13 -07:00
|
|
|
|
|
|
|
# Initialize DB
|
|
|
|
if len(cursor.fetchall()) == 0:
|
2017-08-13 22:08:40 -07:00
|
|
|
logging.warning("Initializing database")
|
2017-08-13 18:56:13 -07:00
|
|
|
for query in queries:
|
2018-04-02 21:58:48 -07:00
|
|
|
print(query)
|
2017-08-13 18:56:13 -07:00
|
|
|
cursor.execute(query)
|
2018-04-02 21:58:48 -07:00
|
|
|
cursor.execute("COMMIT")
|
2017-08-13 18:56:13 -07:00
|
|
|
else:
|
|
|
|
# Migrate if old db exists
|
2018-04-02 21:58:48 -07:00
|
|
|
# cursor.execute("""UPDATE meta SET value=? WHERE key="db_version";""", (str(version), ))
|
|
|
|
# logging.warning("db schema is version {}".format(version))
|
|
|
|
pass
|
|
|
|
|
|
|
|
def add_root(self, path, name="Library"):
|
2017-08-16 00:05:26 -07:00
|
|
|
"""
|
2018-04-02 21:58:48 -07:00
|
|
|
Add a new library root. Returns the root ID or raises on collision
|
|
|
|
:param path: normalized absolute path to add to the library
|
|
|
|
:type path: str:
|
|
|
|
:return: int
|
|
|
|
:raises: sqlite3.IntegrityError
|
2017-08-16 00:05:26 -07:00
|
|
|
"""
|
2018-04-02 21:58:48 -07:00
|
|
|
assert path.startswith("/")
|
2017-08-13 18:56:13 -07:00
|
|
|
with closing(self.db.cursor()) as cursor:
|
2017-08-16 00:05:26 -07:00
|
|
|
try:
|
2018-04-02 21:58:48 -07:00
|
|
|
cursor.execute("INSERT INTO libraries ('name', 'path') VALUES (?, ?)", (name, path, ))
|
|
|
|
cursor.execute("COMMIT")
|
|
|
|
return cursor.lastrowid
|
2017-08-16 00:05:26 -07:00
|
|
|
except sqlite3.IntegrityError:
|
2018-04-02 21:58:48 -07:00
|
|
|
raise DuplicateRootException("Root '{}' already exists".format(path))
|
|
|
|
|
|
|
|
@readcursor
|
|
|
|
def get_libraries(self, cursor):
|
|
|
|
libs = []
|
|
|
|
cursor.execute("SELECT * FROM libraries")
|
|
|
|
for row in cursor:
|
|
|
|
libs.append(row)
|
|
|
|
return libs
|
2018-04-02 22:11:02 -07:00
|
|
|
|
2018-04-03 21:04:55 -07:00
|
|
|
@readcursor
|
|
|
|
def get_artists(self, cursor, id=None, sortby=None, order=None):
|
|
|
|
assert order in ["asc", "desc", None]
|
|
|
|
artists = []
|
|
|
|
q = "SELECT * FROM artists"
|
|
|
|
params = []
|
|
|
|
if id:
|
|
|
|
q += " WHERE id = ?"
|
|
|
|
params.append(id)
|
|
|
|
if sortby:
|
|
|
|
q += " ORDER BY {} {}".format(sortby, order.upper() if order else "ASC")
|
|
|
|
cursor.execute(q, params)
|
|
|
|
for row in cursor:
|
|
|
|
artists.append(row)
|
|
|
|
return artists
|
|
|
|
|
|
|
|
@readcursor
|
|
|
|
def get_albums(self, cursor, id=None, artist=None, sortby=None, order=None):
|
|
|
|
assert order in ["asc", "desc", None]
|
|
|
|
albums = []
|
|
|
|
|
|
|
|
q = "SELECT * FROM albums"
|
|
|
|
params = []
|
|
|
|
|
|
|
|
conditions = []
|
|
|
|
if id:
|
|
|
|
conditions.append("id = ?")
|
|
|
|
params.append(id)
|
|
|
|
if artist:
|
|
|
|
conditions.append("artistid = ?")
|
|
|
|
params.append(artist)
|
|
|
|
if conditions:
|
|
|
|
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:
|
|
|
|
albums.append(row)
|
|
|
|
return albums
|
|
|
|
|
|
|
|
|
2018-04-02 22:11:02 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@readcursor
|
|
|
|
def add_user(self, cursor, username, password, is_admin=False):
|
|
|
|
cursor.execute("INSERT INTO users (username, password, admin) VALUES (?, ?, ?)",
|
|
|
|
(username, self.hashit(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=?;",
|
|
|
|
(self.hashit(password), is_admin, username))
|
|
|
|
cursor.execute("COMMIT")
|
|
|
|
|
|
|
|
@readcursor
|
|
|
|
def get_user(self, cursor, user):
|
|
|
|
try:
|
|
|
|
column = "id" if type(user) is int else "username"
|
|
|
|
return cursor.execute("SELECT * FROM users WHERE {}=?;".format(column), (user, )).fetchall()[0]
|
|
|
|
except IndexError:
|
|
|
|
raise NotFoundError("User doesn't exist")
|
|
|
|
|
|
|
|
def hashit(self, unicode_string):
|
|
|
|
return sha512(unicode_string.encode('UTF-8')).hexdigest()
|