mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-22 17:06:17 +00:00
Merge branch 'master' into scanner_daemon
Conflicts: README.md supysonic/scanner.py supysonic/web.py
This commit is contained in:
commit
a18f670ff0
16
README.md
16
README.md
@ -23,8 +23,8 @@ or as a WSGI application (on Apache for instance). But first:
|
||||
### Prerequisites
|
||||
|
||||
* Python 2.7
|
||||
* [Flask](http://flask.pocoo.org/) >= 0.7 (`pip install flask`)
|
||||
* [SQLAlchemy](http://www.sqlalchemy.org/) (`apt-get install python-sqlalchemy`)
|
||||
* [Flask](http://flask.pocoo.org/) >= 0.9 (`pip install flask`)
|
||||
* [Storm](https://storm.canonical.com/) (`apt-get install python-storm`)
|
||||
* Python Imaging Library (`apt-get install python-imaging`)
|
||||
* simplejson (`apt-get install python-simplejson`)
|
||||
* [requests](http://docs.python-requests.org/) >= 1.0.0 (`pip install requests`)
|
||||
@ -38,8 +38,9 @@ or `KEY: VALUE` syntax.
|
||||
|
||||
Available settings are:
|
||||
* Section **base**:
|
||||
* **database_uri**: required, a SQLAlchemy [database URI](http://docs.sqlalchemy.org/en/rel_0_8/core/engines.html#database-urls).
|
||||
* **database_uri**: required, a Storm [database URI](https://storm.canonical.com/Manual#Databases).
|
||||
I personally use SQLite (`sqlite:////var/supysonic/supysonic.db`), but it might not be the brightest idea for large libraries.
|
||||
Note that to use PostgreSQL you'll need *psycopg2* version 2.4 (not 2.5!) or [patch storm](https://bugs.launchpad.net/storm/+bug/1170063).
|
||||
* **scanner_extensions**: space-separated list of file extensions the scanner is restricted to. If omitted, files will be scanned
|
||||
regardless of their extension
|
||||
* Section **webapp**
|
||||
@ -53,6 +54,11 @@ Available settings are:
|
||||
* Section **mimetypes**: extension to content-type mappings. Designed to help the system guess types, to help clients relying on
|
||||
the content-type. See [the list of common types](https://en.wikipedia.org/wiki/Internet_media_type#List_of_common_media_types).
|
||||
|
||||
### Database initialization
|
||||
|
||||
Supysonic does not issue the `CREATE TABLE` commands for the tables it needs. Thus the tables must be created prior to
|
||||
running the application. Table creation scripts are provided in the *schema* folder for SQLite, MySQL and PostgreSQL.
|
||||
|
||||
Running the application
|
||||
-----------------------
|
||||
|
||||
@ -65,8 +71,8 @@ To start the server, just run the `debug_server.py` script.
|
||||
|
||||
python debug_server.py
|
||||
|
||||
By default, it will listen on the loopback interface (127.0.0.1) on port 5000, but you can specify another address on the command line,
|
||||
for instance on all the IPv6 interfaces:
|
||||
By default, it will listen on the loopback interface (127.0.0.1) on port 5000, but you can specify another address on
|
||||
the command line, for instance on all the IPv6 interfaces:
|
||||
|
||||
python debug_server.py ::
|
||||
|
||||
|
@ -22,6 +22,11 @@
|
||||
import sys, cmd, argparse, getpass, time
|
||||
from supysonic import config
|
||||
|
||||
from supysonic.db import get_store, Folder, User
|
||||
from supysonic.managers.folder import FolderManager
|
||||
from supysonic.managers.user import UserManager
|
||||
from supysonic.scanner import Scanner
|
||||
|
||||
class CLIParser(argparse.ArgumentParser):
|
||||
def error(self, message):
|
||||
self.print_usage(sys.stderr)
|
||||
@ -53,7 +58,7 @@ class CLI(cmd.Cmd):
|
||||
|
||||
return method
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, store):
|
||||
cmd.Cmd.__init__(self)
|
||||
|
||||
# Generate do_* and help_* methods
|
||||
@ -69,6 +74,8 @@ class CLI(cmd.Cmd):
|
||||
for action, subparser in getattr(self.__class__, command + '_subparsers').choices.iteritems():
|
||||
setattr(self, 'help_{} {}'.format(command, action), subparser.print_help)
|
||||
|
||||
self.__store = store
|
||||
|
||||
def do_EOF(self, line):
|
||||
return True
|
||||
|
||||
@ -105,17 +112,17 @@ class CLI(cmd.Cmd):
|
||||
|
||||
def folder_list(self):
|
||||
print 'Name\t\tPath\n----\t\t----'
|
||||
print '\n'.join('{0: <16}{1}'.format(f.name, f.path) for f in db.Folder.query.filter(db.Folder.root == True))
|
||||
print '\n'.join('{0: <16}{1}'.format(f.name, f.path) for f in self.__store.find(Folder, Folder.root == True))
|
||||
|
||||
def folder_add(self, name, path):
|
||||
ret = FolderManager.add(name, path)
|
||||
ret = FolderManager.add(self.__store, name, path)
|
||||
if ret != FolderManager.SUCCESS:
|
||||
print FolderManager.error_str(ret)
|
||||
else:
|
||||
print "Folder '{}' added".format(name)
|
||||
|
||||
def folder_delete(self, name):
|
||||
ret = FolderManager.delete_by_name(name)
|
||||
ret = FolderManager.delete_by_name(self.__store, name)
|
||||
if ret != FolderManager.SUCCESS:
|
||||
print FolderManager.error_str(ret)
|
||||
else:
|
||||
@ -134,19 +141,19 @@ class CLI(cmd.Cmd):
|
||||
print "Scanning '{0}': {1}% ({2}/{3})".format(self.__name, (scanned * 100) / total, scanned, total)
|
||||
self.__last_display = time.time()
|
||||
|
||||
s = Scanner(db.session)
|
||||
s = Scanner(self.__store)
|
||||
if folders:
|
||||
folders = map(lambda n: db.Folder.query.filter(db.Folder.name == n and db.Folder.root == True).first() or n, folders)
|
||||
folders = map(lambda n: self.__store.find(Folder, Folder.name == n, Folder.root == True).one() or n, folders)
|
||||
if any(map(lambda f: isinstance(f, basestring), folders)):
|
||||
print "No such folder(s): " + ' '.join(f for f in folders if isinstance(f, basestring))
|
||||
for folder in filter(lambda f: isinstance(f, db.Folder), folders):
|
||||
FolderManager.scan(folder.id, s, TimedProgressDisplay(folder.name))
|
||||
for folder in filter(lambda f: isinstance(f, Folder), folders):
|
||||
FolderManager.scan(self.__store, folder.id, s, TimedProgressDisplay(folder.name))
|
||||
else:
|
||||
for folder in db.Folder.query.filter(db.Folder.root == True):
|
||||
FolderManager.scan(folder.id, s, TimedProgressDisplay(folder.name))
|
||||
for folder in self.__store.find(Folder, Folder.root == True):
|
||||
FolderManager.scan(self.__store, folder.id, s, TimedProgressDisplay(folder.name))
|
||||
|
||||
added, deleted = s.stats()
|
||||
db.session.commit()
|
||||
self.__store.commit()
|
||||
|
||||
print "Scanning done"
|
||||
print 'Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2])
|
||||
@ -171,7 +178,7 @@ class CLI(cmd.Cmd):
|
||||
|
||||
def user_list(self):
|
||||
print 'Name\t\tAdmin\tEmail\n----\t\t-----\t-----'
|
||||
print '\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in db.User.query.all())
|
||||
print '\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in self.__store.find(User))
|
||||
|
||||
def user_add(self, name, admin, password, email):
|
||||
if not password:
|
||||
@ -180,26 +187,26 @@ class CLI(cmd.Cmd):
|
||||
if password != confirm:
|
||||
print >>sys.stderr, "Passwords don't match"
|
||||
return
|
||||
status = UserManager.add(name, password, email, admin)
|
||||
status = UserManager.add(self.__store, name, password, email, admin)
|
||||
if status != UserManager.SUCCESS:
|
||||
print >>sys.stderr, UserManager.error_str(status)
|
||||
|
||||
def user_delete(self, name):
|
||||
user = db.User.query.filter(db.User.name == name).first()
|
||||
user = self.__store.find(User, User.name == name).one()
|
||||
if not user:
|
||||
print >>sys.stderr, 'No such user'
|
||||
else:
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
self.__store.remove(user)
|
||||
self.__store.commit()
|
||||
print "User '{}' deleted".format(name)
|
||||
|
||||
def user_setadmin(self, name, off):
|
||||
user = db.User.query.filter(db.User.name == name).first()
|
||||
user = self.__store.find(User, User.name == name).one()
|
||||
if not user:
|
||||
print >>sys.stderr, 'No such user'
|
||||
else:
|
||||
user.admin = not off
|
||||
db.session.commit()
|
||||
self.__store.commit()
|
||||
print "{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name)
|
||||
|
||||
def user_changepass(self, name, password):
|
||||
@ -209,7 +216,7 @@ class CLI(cmd.Cmd):
|
||||
if password != confirm:
|
||||
print >>sys.stderr, "Passwords don't match"
|
||||
return
|
||||
status = UserManager.change_password2(name, password)
|
||||
status = UserManager.change_password2(self.__store, name, password)
|
||||
if status != UserManager.SUCCESS:
|
||||
print >>sys.stderr, UserManager.error_str(status)
|
||||
else:
|
||||
@ -219,15 +226,9 @@ if __name__ == "__main__":
|
||||
if not config.check():
|
||||
sys.exit(1)
|
||||
|
||||
from supysonic import db
|
||||
db.init_db()
|
||||
|
||||
from supysonic.managers.folder import FolderManager
|
||||
from supysonic.managers.user import UserManager
|
||||
from supysonic.scanner import Scanner
|
||||
|
||||
cli = CLI(get_store(config.get('base', 'database_uri')))
|
||||
if len(sys.argv) > 1:
|
||||
CLI().onecmd(' '.join(sys.argv[1:]))
|
||||
cli.onecmd(' '.join(sys.argv[1:]))
|
||||
else:
|
||||
CLI().cmdloop()
|
||||
cli.cmdloop()
|
||||
|
||||
|
127
schema/mysql.sql
Normal file
127
schema/mysql.sql
Normal file
@ -0,0 +1,127 @@
|
||||
CREATE TABLE folder (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
root BOOLEAN NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
path VARCHAR(4096) NOT NULL,
|
||||
created DATETIME NOT NULL,
|
||||
has_cover_art BOOLEAN NOT NULL,
|
||||
last_scan INTEGER NOT NULL,
|
||||
parent_id CHAR(36) REFERENCES folder
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE artist (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(256) NOT NULL
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE album (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
artist_id CHAR(36) NOT NULL REFERENCES artist
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE track (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
disc INTEGER NOT NULL,
|
||||
number INTEGER NOT NULL,
|
||||
title VARCHAR(256) NOT NULL,
|
||||
year INTEGER,
|
||||
genre VARCHAR(256),
|
||||
duration INTEGER NOT NULL,
|
||||
album_id CHAR(36) NOT NULL REFERENCES album,
|
||||
bitrate INTEGER NOT NULL,
|
||||
path VARCHAR(4096) NOT NULL,
|
||||
content_type VARCHAR(32) NOT NULL,
|
||||
created DATETIME NOT NULL,
|
||||
last_modification INTEGER NOT NULL,
|
||||
play_count INTEGER NOT NULL,
|
||||
last_play DATETIME,
|
||||
root_folder_id CHAR(36) NOT NULL REFERENCES folder,
|
||||
folder_id CHAR(36) NOT NULL REFERENCES folder
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE user (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
mail VARCHAR(256),
|
||||
password CHAR(40) NOT NULL,
|
||||
salt CHAR(6) NOT NULL,
|
||||
admin BOOLEAN NOT NULL,
|
||||
lastfm_session CHAR(32),
|
||||
lastfm_status BOOLEAN NOT NULL,
|
||||
last_play_id CHAR(36) REFERENCES track,
|
||||
last_play_date DATETIME
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE client_prefs (
|
||||
user_id CHAR(36) NOT NULL,
|
||||
client_name VARCHAR(32) NOT NULL,
|
||||
format VARCHAR(8),
|
||||
bitrate INTEGER,
|
||||
PRIMARY KEY (user_id, client_name)
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE starred_folder (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
starred_id CHAR(36) NOT NULL REFERENCES folder,
|
||||
date DATETIME NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE starred_artist (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
starred_id CHAR(36) NOT NULL REFERENCES artist,
|
||||
date DATETIME NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE starred_album (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
starred_id CHAR(36) NOT NULL REFERENCES album,
|
||||
date DATETIME NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE starred_track (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
starred_id CHAR(36) NOT NULL REFERENCES track,
|
||||
date DATETIME NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE rating_folder (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
rated_id CHAR(36) NOT NULL REFERENCES folder,
|
||||
rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5),
|
||||
PRIMARY KEY (user_id, rated_id)
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE rating_track (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
rated_id CHAR(36) NOT NULL REFERENCES track,
|
||||
rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5),
|
||||
PRIMARY KEY (user_id, rated_id)
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE chat_message (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
time INTEGER NOT NULL,
|
||||
message VARCHAR(512) NOT NULL
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE playlist (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
comment VARCHAR(256),
|
||||
public BOOLEAN NOT NULL,
|
||||
created DATETIME NOT NULL
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
||||
CREATE TABLE playlist_track (
|
||||
playlist_id CHAR(36) NOT NULL REFERENCES playlist,
|
||||
track_id CHAR(36) NOT NULL REFERENCES track,
|
||||
PRIMARY KEY(playlist_id, track_id)
|
||||
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
|
127
schema/postgresql.sql
Normal file
127
schema/postgresql.sql
Normal file
@ -0,0 +1,127 @@
|
||||
CREATE TABLE folder (
|
||||
id UUID PRIMARY KEY,
|
||||
root BOOLEAN NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
path VARCHAR(4096) NOT NULL,
|
||||
created TIMESTAMP NOT NULL,
|
||||
has_cover_art BOOLEAN NOT NULL,
|
||||
last_scan INTEGER NOT NULL,
|
||||
parent_id UUID REFERENCES folder
|
||||
);
|
||||
|
||||
CREATE TABLE artist (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(256) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE album (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
artist_id UUID NOT NULL REFERENCES artist
|
||||
);
|
||||
|
||||
CREATE TABLE track (
|
||||
id UUID PRIMARY KEY,
|
||||
disc INTEGER NOT NULL,
|
||||
number INTEGER NOT NULL,
|
||||
title VARCHAR(256) NOT NULL,
|
||||
year INTEGER,
|
||||
genre VARCHAR(256),
|
||||
duration INTEGER NOT NULL,
|
||||
album_id UUID NOT NULL REFERENCES album,
|
||||
bitrate INTEGER NOT NULL,
|
||||
path VARCHAR(4096) NOT NULL,
|
||||
content_type VARCHAR(32) NOT NULL,
|
||||
created TIMESTAMP NOT NULL,
|
||||
last_modification INTEGER NOT NULL,
|
||||
play_count INTEGER NOT NULL,
|
||||
last_play TIMESTAMP,
|
||||
root_folder_id UUID NOT NULL REFERENCES folder,
|
||||
folder_id UUID NOT NULL REFERENCES folder
|
||||
);
|
||||
|
||||
CREATE TABLE "user" (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
mail VARCHAR(256),
|
||||
password CHAR(40) NOT NULL,
|
||||
salt CHAR(6) NOT NULL,
|
||||
admin BOOLEAN NOT NULL,
|
||||
lastfm_session CHAR(32),
|
||||
lastfm_status BOOLEAN NOT NULL,
|
||||
last_play_id UUID REFERENCES track,
|
||||
last_play_date TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE client_prefs (
|
||||
user_id UUID NOT NULL,
|
||||
client_name VARCHAR(32) NOT NULL,
|
||||
format VARCHAR(8),
|
||||
bitrate INTEGER,
|
||||
PRIMARY KEY (user_id, client_name)
|
||||
);
|
||||
|
||||
CREATE TABLE starred_folder (
|
||||
user_id UUID NOT NULL REFERENCES "user",
|
||||
starred_id UUID NOT NULL REFERENCES folder,
|
||||
date TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
);
|
||||
|
||||
CREATE TABLE starred_artist (
|
||||
user_id UUID NOT NULL REFERENCES "user",
|
||||
starred_id UUID NOT NULL REFERENCES artist,
|
||||
date TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
);
|
||||
|
||||
CREATE TABLE starred_album (
|
||||
user_id UUID NOT NULL REFERENCES "user",
|
||||
starred_id UUID NOT NULL REFERENCES album,
|
||||
date TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
);
|
||||
|
||||
CREATE TABLE starred_track (
|
||||
user_id UUID NOT NULL REFERENCES "user",
|
||||
starred_id UUID NOT NULL REFERENCES track,
|
||||
date TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
);
|
||||
|
||||
CREATE TABLE rating_folder (
|
||||
user_id UUID NOT NULL REFERENCES "user",
|
||||
rated_id UUID NOT NULL REFERENCES folder,
|
||||
rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5),
|
||||
PRIMARY KEY (user_id, rated_id)
|
||||
);
|
||||
|
||||
CREATE TABLE rating_track (
|
||||
user_id UUID NOT NULL REFERENCES "user",
|
||||
rated_id UUID NOT NULL REFERENCES track,
|
||||
rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5),
|
||||
PRIMARY KEY (user_id, rated_id)
|
||||
);
|
||||
|
||||
CREATE TABLE chat_message (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES "user",
|
||||
time INTEGER NOT NULL,
|
||||
message VARCHAR(512) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE playlist (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES "user",
|
||||
name VARCHAR(256) NOT NULL,
|
||||
comment VARCHAR(256),
|
||||
public BOOLEAN NOT NULL,
|
||||
created TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE playlist_track (
|
||||
playlist_id UUID NOT NULL REFERENCES playlist,
|
||||
track_id UUID NOT NULL REFERENCES track,
|
||||
PRIMARY KEY(playlist_id, track_id)
|
||||
);
|
||||
|
127
schema/sqlite.sql
Normal file
127
schema/sqlite.sql
Normal file
@ -0,0 +1,127 @@
|
||||
CREATE TABLE folder (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
root BOOLEAN NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
path VARCHAR(4096) NOT NULL,
|
||||
created DATETIME NOT NULL,
|
||||
has_cover_art BOOLEAN NOT NULL,
|
||||
last_scan INTEGER NOT NULL,
|
||||
parent_id CHAR(36) REFERENCES folder
|
||||
);
|
||||
|
||||
CREATE TABLE artist (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(256) NOT NULL COLLATE NOCASE
|
||||
);
|
||||
|
||||
CREATE TABLE album (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(256) NOT NULL COLLATE NOCASE,
|
||||
artist_id CHAR(36) NOT NULL REFERENCES artist
|
||||
);
|
||||
|
||||
CREATE TABLE track (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
disc INTEGER NOT NULL,
|
||||
number INTEGER NOT NULL,
|
||||
title VARCHAR(256) NOT NULL,
|
||||
year INTEGER,
|
||||
genre VARCHAR(256),
|
||||
duration INTEGER NOT NULL,
|
||||
album_id CHAR(36) NOT NULL REFERENCES album,
|
||||
bitrate INTEGER NOT NULL,
|
||||
path VARCHAR(4096) NOT NULL,
|
||||
content_type VARCHAR(32) NOT NULL,
|
||||
created DATETIME NOT NULL,
|
||||
last_modification INTEGER NOT NULL,
|
||||
play_count INTEGER NOT NULL,
|
||||
last_play DATETIME,
|
||||
root_folder_id CHAR(36) NOT NULL REFERENCES folder,
|
||||
folder_id CHAR(36) NOT NULL REFERENCES folder
|
||||
);
|
||||
|
||||
CREATE TABLE user (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
mail VARCHAR(256),
|
||||
password CHAR(40) NOT NULL,
|
||||
salt CHAR(6) NOT NULL,
|
||||
admin BOOLEAN NOT NULL,
|
||||
lastfm_session CHAR(32),
|
||||
lastfm_status BOOLEAN NOT NULL,
|
||||
last_play_id CHAR(36) REFERENCES track,
|
||||
last_play_date DATETIME
|
||||
);
|
||||
|
||||
CREATE TABLE client_prefs (
|
||||
user_id CHAR(36) NOT NULL,
|
||||
client_name VARCHAR(32) NOT NULL,
|
||||
format VARCHAR(8),
|
||||
bitrate INTEGER,
|
||||
PRIMARY KEY (user_id, client_name)
|
||||
);
|
||||
|
||||
CREATE TABLE starred_folder (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
starred_id CHAR(36) NOT NULL REFERENCES folder,
|
||||
date DATETIME NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
);
|
||||
|
||||
CREATE TABLE starred_artist (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
starred_id CHAR(36) NOT NULL REFERENCES artist,
|
||||
date DATETIME NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
);
|
||||
|
||||
CREATE TABLE starred_album (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
starred_id CHAR(36) NOT NULL REFERENCES album,
|
||||
date DATETIME NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
);
|
||||
|
||||
CREATE TABLE starred_track (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
starred_id CHAR(36) NOT NULL REFERENCES track,
|
||||
date DATETIME NOT NULL,
|
||||
PRIMARY KEY (user_id, starred_id)
|
||||
);
|
||||
|
||||
CREATE TABLE rating_folder (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
rated_id CHAR(36) NOT NULL REFERENCES folder,
|
||||
rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5),
|
||||
PRIMARY KEY (user_id, rated_id)
|
||||
);
|
||||
|
||||
CREATE TABLE rating_track (
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
rated_id CHAR(36) NOT NULL REFERENCES track,
|
||||
rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5),
|
||||
PRIMARY KEY (user_id, rated_id)
|
||||
);
|
||||
|
||||
CREATE TABLE chat_message (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
time INTEGER NOT NULL,
|
||||
message VARCHAR(512) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE playlist (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
user_id CHAR(36) NOT NULL REFERENCES user,
|
||||
name VARCHAR(256) NOT NULL COLLATE NOCASE,
|
||||
comment VARCHAR(256),
|
||||
public BOOLEAN NOT NULL,
|
||||
created DATETIME NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE playlist_track (
|
||||
playlist_id CHAR(36) NOT NULL REFERENCES playlist,
|
||||
track_id CHAR(36) NOT NULL REFERENCES track,
|
||||
PRIMARY KEY(playlist_id, track_id)
|
||||
);
|
||||
|
@ -23,7 +23,7 @@ from xml.etree import ElementTree
|
||||
import simplejson
|
||||
import uuid
|
||||
|
||||
from supysonic.web import app
|
||||
from supysonic.web import app, store
|
||||
from supysonic.managers.user import UserManager
|
||||
|
||||
@app.before_request
|
||||
@ -58,7 +58,7 @@ def authorize():
|
||||
error = request.error_formatter(40, 'Unauthorized'), 401
|
||||
|
||||
if request.authorization:
|
||||
status, user = UserManager.try_auth(request.authorization.username, request.authorization.password)
|
||||
status, user = UserManager.try_auth(store, request.authorization.username, request.authorization.password)
|
||||
if status == UserManager.SUCCESS:
|
||||
request.username = request.authorization.username
|
||||
request.user = user
|
||||
@ -68,7 +68,7 @@ def authorize():
|
||||
if not username or not password:
|
||||
return error
|
||||
|
||||
status, user = UserManager.try_auth(username, password)
|
||||
status, user = UserManager.try_auth(store, username, password)
|
||||
if status != UserManager.SUCCESS:
|
||||
return error
|
||||
|
||||
@ -184,7 +184,7 @@ def get_entity(req, ent, param = 'id'):
|
||||
except:
|
||||
return False, req.error_formatter(0, 'Invalid %s id' % ent.__name__)
|
||||
|
||||
entity = ent.query.get(eid)
|
||||
entity = store.get(ent, eid)
|
||||
if not entity:
|
||||
return False, (req.error_formatter(70, '%s not found' % ent.__name__), 404)
|
||||
|
||||
|
@ -19,13 +19,15 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import request
|
||||
from sqlalchemy import desc, func
|
||||
from sqlalchemy.orm import aliased
|
||||
from storm.expr import Desc, Avg, Min, Max
|
||||
from storm.info import ClassAlias
|
||||
from datetime import timedelta
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from supysonic.web import app
|
||||
from supysonic.db import *
|
||||
from supysonic.web import app, store
|
||||
from supysonic.db import Folder, Artist, Album, Track, RatingFolder, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, User
|
||||
from supysonic.db import now
|
||||
|
||||
@app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ])
|
||||
def rand_songs():
|
||||
@ -40,15 +42,15 @@ def rand_songs():
|
||||
except:
|
||||
return request.error_formatter(0, 'Invalid parameter format')
|
||||
|
||||
query = Track.query
|
||||
query = store.find(Track)
|
||||
if fromYear:
|
||||
query = query.filter(Track.year >= fromYear)
|
||||
query = query.find(Track.year >= fromYear)
|
||||
if toYear:
|
||||
query = query.filter(Track.year <= toYear)
|
||||
query = query.find(Track.year <= toYear)
|
||||
if genre:
|
||||
query = query.filter(Track.genre == genre)
|
||||
query = query.find(Track.genre == genre)
|
||||
if fid:
|
||||
query = query.filter(Track.root_folder_id == fid)
|
||||
query = query.find(Track.root_folder_id == fid)
|
||||
count = query.count()
|
||||
|
||||
if not count:
|
||||
@ -57,7 +59,7 @@ def rand_songs():
|
||||
tracks = []
|
||||
for _ in xrange(size):
|
||||
x = random.choice(xrange(count))
|
||||
tracks.append(query.offset(x).limit(1).one())
|
||||
tracks.append(query[x])
|
||||
|
||||
return request.formatter({
|
||||
'randomSongs': {
|
||||
@ -74,7 +76,7 @@ def album_list():
|
||||
except:
|
||||
return request.error_formatter(0, 'Invalid parameter format')
|
||||
|
||||
query = Folder.query.filter(Folder.tracks.any())
|
||||
query = store.find(Folder, Track.folder_id == Folder.id)
|
||||
if ltype == 'random':
|
||||
albums = []
|
||||
count = query.count()
|
||||
@ -84,7 +86,7 @@ def album_list():
|
||||
|
||||
for _ in xrange(size):
|
||||
x = random.choice(xrange(count))
|
||||
albums.append(query.offset(x).limit(1).one())
|
||||
albums.append(query[x])
|
||||
|
||||
return request.formatter({
|
||||
'albumList': {
|
||||
@ -92,26 +94,26 @@ def album_list():
|
||||
}
|
||||
})
|
||||
elif ltype == 'newest':
|
||||
query = query.order_by(desc(Folder.created))
|
||||
query = query.order_by(Desc(Folder.created)).config(distinct = True)
|
||||
elif ltype == 'highest':
|
||||
query = query.join(RatingFolder).group_by(Folder.id).order_by(desc(func.avg(RatingFolder.rating)))
|
||||
query = query.find(RatingFolder.rated_id == Folder.id).group_by(Folder.id).order_by(Desc(Avg(RatingFolder.rating)))
|
||||
elif ltype == 'frequent':
|
||||
query = query.join(Track, Folder.tracks).group_by(Folder.id).order_by(desc(func.avg(Track.play_count)))
|
||||
query = query.group_by(Folder.id).order_by(Desc(Avg(Track.play_count)))
|
||||
elif ltype == 'recent':
|
||||
query = query.join(Track, Folder.tracks).group_by(Folder.id).order_by(desc(func.max(Track.last_play)))
|
||||
query = query.group_by(Folder.id).order_by(Desc(Max(Track.last_play)))
|
||||
elif ltype == 'starred':
|
||||
query = query.join(StarredFolder).join(User).filter(User.name == request.username)
|
||||
query = query.find(StarredFolder.starred_id == Folder.id, User.id == StarredFolder.user_id, User.name == request.username)
|
||||
elif ltype == 'alphabeticalByName':
|
||||
query = query.order_by(Folder.name)
|
||||
query = query.order_by(Folder.name).config(distinct = True)
|
||||
elif ltype == 'alphabeticalByArtist':
|
||||
parent = aliased(Folder)
|
||||
query = query.join(parent, Folder.parent).order_by(parent.name).order_by(Folder.name)
|
||||
parent = ClassAlias(Folder)
|
||||
query = query.find(Folder.parent_id == parent.id).order_by(parent.name, Folder.name).config(distinct = True)
|
||||
else:
|
||||
return request.error_formatter(0, 'Unknown search type')
|
||||
|
||||
return request.formatter({
|
||||
'albumList': {
|
||||
'album': [ f.as_subsonic_child(request.user) for f in query.limit(size).offset(offset) ]
|
||||
'album': [ f.as_subsonic_child(request.user) for f in query[offset:offset+size] ]
|
||||
}
|
||||
})
|
||||
|
||||
@ -124,7 +126,7 @@ def album_list_id3():
|
||||
except:
|
||||
return request.error_formatter(0, 'Invalid parameter format')
|
||||
|
||||
query = Album.query
|
||||
query = store.find(Album)
|
||||
if ltype == 'random':
|
||||
albums = []
|
||||
count = query.count()
|
||||
@ -134,7 +136,7 @@ def album_list_id3():
|
||||
|
||||
for _ in xrange(size):
|
||||
x = random.choice(xrange(count))
|
||||
albums.append(query.offset(x).limit(1).one())
|
||||
albums.append(query[x])
|
||||
|
||||
return request.formatter({
|
||||
'albumList2': {
|
||||
@ -142,51 +144,48 @@ def album_list_id3():
|
||||
}
|
||||
})
|
||||
elif ltype == 'newest':
|
||||
query = query.join(Track, Album.tracks).group_by(Album.id).order_by(desc(func.min(Track.created)))
|
||||
query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Min(Track.created)))
|
||||
elif ltype == 'frequent':
|
||||
query = query.join(Track, Album.tracks).group_by(Album.id).order_by(desc(func.avg(Track.play_count)))
|
||||
query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Avg(Track.play_count)))
|
||||
elif ltype == 'recent':
|
||||
query = query.join(Track, Album.tracks).group_by(Album.id).order_by(desc(func.max(Track.last_play)))
|
||||
query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Max(Track.last_play)))
|
||||
elif ltype == 'starred':
|
||||
query = query.join(StarredAlbum).join(User).filter(User.name == request.username)
|
||||
query = query.find(StarredAlbum.starred_id == Album.id, User.id == StarredAlbum.user_id, User.name == request.username)
|
||||
elif ltype == 'alphabeticalByName':
|
||||
query = query.order_by(Album.name)
|
||||
elif ltype == 'alphabeticalByArtist':
|
||||
query = query.join(Artist).order_by(Artist.name).order_by(Album.name)
|
||||
query = query.find(Artist.id == Album.artist_id).order_by(Artist.name, Album.name)
|
||||
else:
|
||||
return request.error_formatter(0, 'Unknown search type')
|
||||
|
||||
return request.formatter({
|
||||
'albumList2': {
|
||||
'album': [ f.as_subsonic_album(request.user) for f in query.limit(size).offset(offset) ]
|
||||
'album': [ f.as_subsonic_album(request.user) for f in query[offset:offset+size] ]
|
||||
}
|
||||
})
|
||||
|
||||
@app.route('/rest/getNowPlaying.view', methods = [ 'GET', 'POST' ])
|
||||
def now_playing():
|
||||
if engine.name == 'sqlite':
|
||||
query = User.query.join(Track).filter(func.strftime('%s', now()) - func.strftime('%s', User.last_play_date) < Track.duration * 2)
|
||||
elif engine.name == 'postgresql':
|
||||
query = User.query.join(Track).filter(func.date_part('epoch', func.now() - User.last_play_date) < Track.duration * 2)
|
||||
else:
|
||||
query = User.query.join(Track).filter(func.timediff(func.now(), User.last_play_date) < Track.duration * 2)
|
||||
query = store.find(User, Track.id == User.last_play_id)
|
||||
|
||||
return request.formatter({
|
||||
'nowPlaying': {
|
||||
'entry': [ dict(
|
||||
u.last_play.as_subsonic_child(request.user).items() +
|
||||
{ 'username': u.name, 'minutesAgo': (now() - u.last_play_date).seconds / 60, 'playerId': 0 }.items()
|
||||
) for u in query ]
|
||||
) for u in query if u.last_play_date + timedelta(seconds = u.last_play.duration * 2) > now() ]
|
||||
}
|
||||
})
|
||||
|
||||
@app.route('/rest/getStarred.view', methods = [ 'GET', 'POST' ])
|
||||
def get_starred():
|
||||
folders = store.find(StarredFolder, StarredFolder.user_id == User.id, User.name == request.username)
|
||||
|
||||
return request.formatter({
|
||||
'starred': {
|
||||
'artist': [ { 'id': str(sf.starred_id), 'name': sf.starred.name } for sf in StarredFolder.query.join(User).join(Folder).filter(User.name == request.username).filter(~ Folder.tracks.any()) ],
|
||||
'album': [ sf.starred.as_subsonic_child(request.user) for sf in StarredFolder.query.join(User).join(Folder).filter(User.name == request.username).filter(Folder.tracks.any()) ],
|
||||
'song': [ st.starred.as_subsonic_child(request.user) for st in StarredTrack.query.join(User).filter(User.name == request.username) ]
|
||||
'artist': [ { 'id': str(sf.starred_id), 'name': sf.starred.name } for sf in folders.find(Folder.parent_id == StarredFolder.starred_id, Track.folder_id == Folder.id).config(distinct = True) ],
|
||||
'album': [ sf.starred.as_subsonic_child(request.user) for sf in folders.find(Track.folder_id == StarredFolder.starred_id).config(distinct = True) ],
|
||||
'song': [ st.starred.as_subsonic_child(request.user) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ]
|
||||
}
|
||||
})
|
||||
|
||||
@ -194,9 +193,9 @@ def get_starred():
|
||||
def get_starred_id3():
|
||||
return request.formatter({
|
||||
'starred2': {
|
||||
'artist': [ sa.starred.as_subsonic_artist(request.user) for sa in StarredArtist.query.join(User).filter(User.name == request.username) ],
|
||||
'album': [ sa.starred.as_subsonic_album(request.user) for sa in StarredAlbum.query.join(User).filter(User.name == request.username) ],
|
||||
'song': [ st.starred.as_subsonic_child(request.user) for st in StarredTrack.query.join(User).filter(User.name == request.username) ]
|
||||
'artist': [ sa.starred.as_subsonic_artist(request.user) for sa in store.find(StarredArtist, StarredArtist.user_id == User.id, User.name == request.username) ],
|
||||
'album': [ sa.starred.as_subsonic_album(request.user) for sa in store.find(StarredAlbum, StarredAlbum.user_id == User.id, User.name == request.username) ],
|
||||
'song': [ st.starred.as_subsonic_child(request.user) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ]
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -21,10 +21,12 @@
|
||||
import time
|
||||
import uuid
|
||||
from flask import request
|
||||
from supysonic.web import app
|
||||
from supysonic.web import app, store
|
||||
from . import get_entity
|
||||
from supysonic.lastfm import LastFm
|
||||
from supysonic.db import *
|
||||
from supysonic.db import Track, Album, Artist, Folder
|
||||
from supysonic.db import StarredTrack, StarredAlbum, StarredArtist, StarredFolder
|
||||
from supysonic.db import RatingTrack, RatingFolder
|
||||
|
||||
@app.route('/rest/star.view', methods = [ 'GET', 'POST' ])
|
||||
def star():
|
||||
@ -36,11 +38,14 @@ def star():
|
||||
except:
|
||||
return 2, request.error_formatter(0, 'Invalid %s id' % ent.__name__)
|
||||
|
||||
if starred_ent.query.get((request.user.id, uid)):
|
||||
if store.get(starred_ent, (request.user.id, uid)):
|
||||
return 2, request.error_formatter(0, '%s already starred' % ent.__name__)
|
||||
e = ent.query.get(uid)
|
||||
e = store.get(ent, uid)
|
||||
if e:
|
||||
session.add(starred_ent(user = request.user, starred = e))
|
||||
starred = starred_ent()
|
||||
starred.user_id = request.user.id
|
||||
starred.starred_id = uid
|
||||
store.add(starred)
|
||||
else:
|
||||
return 1, request.error_formatter(70, 'Unknown %s id' % ent.__name__)
|
||||
|
||||
@ -65,7 +70,7 @@ def star():
|
||||
if err:
|
||||
return ferror
|
||||
|
||||
session.commit()
|
||||
store.commit()
|
||||
return request.formatter({})
|
||||
|
||||
@app.route('/rest/unstar.view', methods = [ 'GET', 'POST' ])
|
||||
@ -78,7 +83,7 @@ def unstar():
|
||||
except:
|
||||
return request.error_formatter(0, 'Invalid id')
|
||||
|
||||
ent.query.filter(ent.user_id == request.user.id).filter(ent.starred_id == uid).delete()
|
||||
store.find(ent, ent.user_id == request.user.id, ent.starred_id == uid).remove()
|
||||
return None
|
||||
|
||||
for eid in id:
|
||||
@ -99,7 +104,7 @@ def unstar():
|
||||
if err:
|
||||
return err
|
||||
|
||||
session.commit()
|
||||
store.commit()
|
||||
return request.formatter({})
|
||||
|
||||
@app.route('/rest/setRating.view', methods = [ 'GET', 'POST' ])
|
||||
@ -118,24 +123,28 @@ def rate():
|
||||
return request.error_formatter(0, 'rating must be between 0 and 5 (inclusive)')
|
||||
|
||||
if rating == 0:
|
||||
RatingTrack.query.filter(RatingTrack.user_id == request.user.id).filter(RatingTrack.rated_id == uid).delete()
|
||||
RatingFolder.query.filter(RatingFolder.user_id == request.user.id).filter(RatingFolder.rated_id == uid).delete()
|
||||
store.find(RatingTrack, RatingTrack.user_id == request.user.id, RatingTrack.rated_id == uid).remove()
|
||||
store.find(RatingFolder, RatingFolder.user_id == request.user.id, RatingFolder.rated_id == uid).remove()
|
||||
else:
|
||||
rated = Track.query.get(uid)
|
||||
rated = store.get(Track, uid)
|
||||
rating_ent = RatingTrack
|
||||
if not rated:
|
||||
rated = Folder.query.get(uid)
|
||||
rated = store.get(Folder, uid)
|
||||
rating_ent = RatingFolder
|
||||
if not rated:
|
||||
return request.error_formatter(70, 'Unknown id')
|
||||
|
||||
rating_info = rating_ent.query.get((request.user.id, uid))
|
||||
rating_info = store.get(rating_ent, (request.user.id, uid))
|
||||
if rating_info:
|
||||
rating_info.rating = rating
|
||||
else:
|
||||
session.add(rating_ent(user = request.user, rated = rated, rating = rating))
|
||||
rating_info = rating_ent()
|
||||
rating_info.user_id = request.user.id
|
||||
rating_info.rated_id = uid
|
||||
rating_info.rating = rating
|
||||
store.add(rating_info)
|
||||
|
||||
session.commit()
|
||||
store.commit()
|
||||
return request.formatter({})
|
||||
|
||||
@app.route('/rest/scrobble.view', methods = [ 'GET', 'POST' ])
|
||||
|
@ -19,7 +19,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import request
|
||||
from supysonic.web import app
|
||||
from supysonic.web import app, store
|
||||
from supysonic.db import Folder, Artist, Album, Track
|
||||
from . import get_entity
|
||||
import uuid, string
|
||||
@ -31,7 +31,7 @@ def list_folders():
|
||||
'musicFolder': [ {
|
||||
'id': str(f.id),
|
||||
'name': f.name
|
||||
} for f in Folder.query.filter(Folder.root == True).order_by(Folder.name).all() ]
|
||||
} for f in store.find(Folder, Folder.root == True).order_by(Folder.name) ]
|
||||
}
|
||||
})
|
||||
|
||||
@ -46,25 +46,25 @@ def list_indexes():
|
||||
return request.error_formatter(0, 'Invalid timestamp')
|
||||
|
||||
if musicFolderId is None:
|
||||
folder = Folder.query.filter(Folder.root == True).all()
|
||||
folder = store.find(Folder, Folder.root == True)
|
||||
else:
|
||||
try:
|
||||
mfid = uuid.UUID(musicFolderId)
|
||||
except:
|
||||
return request.error_formatter(0, 'Invalid id')
|
||||
|
||||
folder = Folder.query.get(mfid)
|
||||
folder = store.get(Folder, mfid)
|
||||
|
||||
if not folder or (type(folder) is not list and not folder.root):
|
||||
if not folder or (type(folder) is Folder and not folder.root):
|
||||
return request.error_formatter(70, 'Folder not found')
|
||||
|
||||
last_modif = max(map(lambda f: f.last_scan, folder)) if type(folder) is list else folder.last_scan
|
||||
last_modif = max(map(lambda f: f.last_scan, folder)) if type(folder) is not Folder else folder.last_scan
|
||||
|
||||
if (not ifModifiedSince is None) and last_modif < ifModifiedSince:
|
||||
return request.formatter({ 'indexes': { 'lastModified': last_modif * 1000 } })
|
||||
|
||||
# The XSD lies, we don't return artists but a directory structure
|
||||
if type(folder) is list:
|
||||
if type(folder) is not Folder:
|
||||
artists = []
|
||||
childs = []
|
||||
for f in folder:
|
||||
@ -121,7 +121,7 @@ def show_directory():
|
||||
def list_artists():
|
||||
# According to the API page, there are no parameters?
|
||||
indexes = {}
|
||||
for artist in Artist.query.all():
|
||||
for artist in store.find(Artist):
|
||||
index = artist.name[0].upper() if artist.name else '?'
|
||||
if index in map(str, xrange(10)):
|
||||
index = '#'
|
||||
|
@ -19,8 +19,8 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import request
|
||||
from supysonic.web import app
|
||||
from supysonic.db import ChatMessage, session
|
||||
from supysonic.web import app, store
|
||||
from supysonic.db import ChatMessage
|
||||
|
||||
@app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ])
|
||||
def get_chat():
|
||||
@ -30,9 +30,9 @@ def get_chat():
|
||||
except:
|
||||
return request.error_formatter(0, 'Invalid parameter')
|
||||
|
||||
query = ChatMessage.query.order_by(ChatMessage.time)
|
||||
query = store.find(ChatMessage).order_by(ChatMessage.time)
|
||||
if since:
|
||||
query = query.filter(ChatMessage.time > since)
|
||||
query = query.find(ChatMessage.time > since)
|
||||
|
||||
return request.formatter({ 'chatMessages': { 'chatMessage': [ msg.responsize() for msg in query ] }})
|
||||
|
||||
@ -42,7 +42,10 @@ def add_chat_message():
|
||||
if not msg:
|
||||
return request.error_formatter(10, 'Missing message')
|
||||
|
||||
session.add(ChatMessage(user = request.user, message = msg))
|
||||
session.commit()
|
||||
chat = ChatMessage()
|
||||
chat.user_id = request.user.id
|
||||
chat.message = msg
|
||||
store.add(chat)
|
||||
store.commit()
|
||||
return request.formatter({})
|
||||
|
||||
|
@ -27,12 +27,10 @@ import codecs
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from supysonic import config, scanner
|
||||
from supysonic.web import app
|
||||
from supysonic.db import Track, Album, Artist, Folder, ClientPrefs, now, session
|
||||
from supysonic.web import app, store
|
||||
from supysonic.db import Track, Album, Artist, Folder, User, ClientPrefs, now
|
||||
from . import get_entity
|
||||
|
||||
from sqlalchemy import func
|
||||
|
||||
def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate):
|
||||
if not base_cmdline:
|
||||
return None
|
||||
@ -57,10 +55,12 @@ def stream_media():
|
||||
dst_mimetype = res.content_type
|
||||
|
||||
if client:
|
||||
prefs = ClientPrefs.query.get((request.user.id, client))
|
||||
prefs = store.get(ClientPrefs, (request.user.id, client))
|
||||
if not prefs:
|
||||
prefs = ClientPrefs(user_id = request.user.id, client_name = client)
|
||||
session.add(prefs)
|
||||
prefs = ClientPrefs()
|
||||
prefs.user_id = request.user.id
|
||||
prefs.client_name = client
|
||||
store.add(prefs)
|
||||
|
||||
if prefs.format:
|
||||
dst_suffix = prefs.format
|
||||
@ -117,7 +117,7 @@ def stream_media():
|
||||
res.last_play = now()
|
||||
request.user.last_play = res
|
||||
request.user.last_play_date = now()
|
||||
session.commit()
|
||||
store.commit()
|
||||
|
||||
return response
|
||||
|
||||
@ -170,7 +170,7 @@ def lyrics():
|
||||
if not title:
|
||||
return request.error_formatter(10, 'Missing title parameter')
|
||||
|
||||
query = Track.query.join(Album, Artist).filter(func.lower(Track.title) == title.lower() and func.lower(Artist.name) == artist.lower())
|
||||
query = store.find(Track, Album.id == Track.album_id, Artist.id == Album.artist_id, Track.title.like(title), Artist.name.like(artist))
|
||||
for track in query:
|
||||
lyrics_path = os.path.splitext(track.path)[0] + '.txt'
|
||||
if os.path.exists(lyrics_path):
|
||||
|
@ -19,22 +19,22 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import request
|
||||
from sqlalchemy import or_, func
|
||||
from storm.expr import Or
|
||||
import uuid
|
||||
from supysonic.web import app
|
||||
from supysonic.db import Playlist, User, Track, session
|
||||
from supysonic.web import app, store
|
||||
from supysonic.db import Playlist, User, Track
|
||||
from . import get_entity
|
||||
|
||||
@app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ])
|
||||
def list_playlists():
|
||||
query = Playlist.query.filter(or_(Playlist.user_id == request.user.id, Playlist.public == True)).order_by(func.lower(Playlist.name))
|
||||
query = store.find(Playlist, Or(Playlist.user_id == request.user.id, Playlist.public == True)).order_by(Playlist.name)
|
||||
|
||||
username = request.args.get('username')
|
||||
if username:
|
||||
if not request.user.admin:
|
||||
return request.error_formatter(50, 'Restricted to admins')
|
||||
|
||||
query = Playlist.query.join(User).filter(User.name == username).order_by(func.lower(Playlist.name))
|
||||
query = store.find(Playlist, Playlist.user_id == User.id, User.name == username).order_by(Playlist.name)
|
||||
|
||||
return request.formatter({ 'playlists': { 'playlist': [ p.as_subsonic_playlist(request.user) for p in query ] } })
|
||||
|
||||
@ -61,30 +61,32 @@ def create_playlist():
|
||||
return request.error_formatter(0, 'Invalid parameter')
|
||||
|
||||
if playlist_id:
|
||||
playlist = Playlist.query.get(playlist_id)
|
||||
playlist = store.get(Playlist, playlist_id)
|
||||
if not playlist:
|
||||
return request.error_formatter(70, 'Unknwon playlist')
|
||||
|
||||
if playlist.user_id != request.user.id and not request.user.admin:
|
||||
return request.error_formatter(50, "You're not allowed to modify a playlist that isn't yours")
|
||||
|
||||
playlist.tracks = []
|
||||
playlist.tracks.clear()
|
||||
if name:
|
||||
playlist.name = name
|
||||
elif name:
|
||||
playlist = Playlist(user = request.user, name = name)
|
||||
session.add(playlist)
|
||||
playlist = Playlist()
|
||||
playlist.user_id = request.user.id
|
||||
playlist.name = name
|
||||
store.add(playlist)
|
||||
else:
|
||||
return request.error_formatter(10, 'Missing playlist id or name')
|
||||
|
||||
for sid in songs:
|
||||
track = Track.query.get(sid)
|
||||
track = store.get(Track, sid)
|
||||
if not track:
|
||||
return request.error_formatter(70, 'Unknown song')
|
||||
|
||||
playlist.tracks.append(track)
|
||||
playlist.tracks.add(track)
|
||||
|
||||
session.commit()
|
||||
store.commit()
|
||||
return request.formatter({})
|
||||
|
||||
@app.route('/rest/deletePlaylist.view', methods = [ 'GET', 'POST' ])
|
||||
@ -96,8 +98,8 @@ def delete_playlist():
|
||||
if res.user_id != request.user.id and not request.user.admin:
|
||||
return request.error_formatter(50, "You're not allowed to delete a playlist that isn't yours")
|
||||
|
||||
session.delete(res)
|
||||
session.commit()
|
||||
store.remove(res)
|
||||
store.commit()
|
||||
return request.formatter({})
|
||||
|
||||
@app.route('/rest/updatePlaylist.view', methods = [ 'GET', 'POST' ])
|
||||
@ -125,21 +127,20 @@ def update_playlist():
|
||||
if public:
|
||||
playlist.public = public in (True, 'True', 'true', 1, '1')
|
||||
|
||||
tracks = list(playlist.tracks)
|
||||
|
||||
for sid in to_add:
|
||||
track = Track.query.get(sid)
|
||||
track = store.get(Track, sid)
|
||||
if not track:
|
||||
return request.error_formatter(70, 'Unknown song')
|
||||
if track not in playlist.tracks:
|
||||
playlist.tracks.append(track)
|
||||
playlist.tracks.add(track)
|
||||
|
||||
offset = 0
|
||||
for idx in to_remove:
|
||||
idx = idx - offset
|
||||
if idx < 0 or idx >= len(playlist.tracks):
|
||||
if idx < 0 or idx >= len(tracks):
|
||||
return request.error_formatter(0, 'Index out of range')
|
||||
playlist.tracks.pop(idx)
|
||||
offset += 1
|
||||
playlist.tracks.remove(tracks[idx])
|
||||
|
||||
session.commit()
|
||||
store.commit()
|
||||
return request.formatter({})
|
||||
|
||||
|
@ -19,7 +19,8 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import request
|
||||
from supysonic.web import app
|
||||
from storm.info import ClassAlias
|
||||
from supysonic.web import app, store
|
||||
from supysonic.db import Folder, Track, Artist, Album
|
||||
|
||||
@app.route('/rest/search.view', methods = [ 'GET', 'POST' ])
|
||||
@ -33,19 +34,20 @@ def old_search():
|
||||
return request.error_formatter(0, 'Invalid parameter')
|
||||
|
||||
if artist:
|
||||
query = Folder.query.filter(~ Folder.tracks.any(), Folder.name.contains(artist))
|
||||
parent = ClassAlias(Folder)
|
||||
query = store.find(parent, Folder.parent_id == parent.id, Track.folder_id == Folder.id, parent.name.contains_string(artist)).config(distinct = True)
|
||||
elif album:
|
||||
query = Folder.query.filter(Folder.tracks.any(), Folder.name.contains(album))
|
||||
query = store.find(Folder, Track.folder_id == Folder.id, Folder.name.contains_string(album)).config(distinct = True)
|
||||
elif title:
|
||||
query = Track.query.filter(Track.title.contains(title))
|
||||
query = store.find(Track, Track.title.contains_string(title))
|
||||
elif anyf:
|
||||
folders = Folder.query.filter(Folder.name.contains(anyf))
|
||||
tracks = Track.query.filter(Track.title.contains(anyf))
|
||||
res = folders.slice(offset, offset + count).all()
|
||||
folders = store.find(Folder, Folder.name.contains_string(anyf))
|
||||
tracks = store.find(Track, Track.title.contains_string(anyf))
|
||||
res = list(folders[offset : offset + count])
|
||||
if offset + count > folders.count():
|
||||
toff = max(0, offset - folders.count())
|
||||
tend = offset + count - folders.count()
|
||||
res += tracks.slice(toff, tend).all()
|
||||
res += list(tracks[toff : tend])
|
||||
|
||||
return request.formatter({ 'searchResult': {
|
||||
'totalHits': folders.count() + tracks.count(),
|
||||
@ -58,7 +60,7 @@ def old_search():
|
||||
return request.formatter({ 'searchResult': {
|
||||
'totalHits': query.count(),
|
||||
'offset': offset,
|
||||
'match': [ r.as_subsonic_child(request.user) for r in query.slice(offset, offset + count) ]
|
||||
'match': [ r.as_subsonic_child(request.user) for r in query[offset : offset + count] ]
|
||||
}})
|
||||
|
||||
@app.route('/rest/search2.view', methods = [ 'GET', 'POST' ])
|
||||
@ -79,9 +81,10 @@ def new_search():
|
||||
if not query:
|
||||
return request.error_formatter(10, 'Missing query parameter')
|
||||
|
||||
artist_query = Folder.query.filter(~ Folder.tracks.any(), Folder.name.contains(query)).slice(artist_offset, artist_offset + artist_count)
|
||||
album_query = Folder.query.filter(Folder.tracks.any(), Folder.name.contains(query)).slice(album_offset, album_offset + album_count)
|
||||
song_query = Track.query.filter(Track.title.contains(query)).slice(song_offset, song_offset + song_count)
|
||||
parent = ClassAlias(Folder)
|
||||
artist_query = store.find(parent, Folder.parent_id == parent.id, Track.folder_id == Folder.id, parent.name.contains_string(query)).config(distinct = True, offset = artist_offset, limit = artist_count)
|
||||
album_query = store.find(Folder, Track.folder_id == Folder.id, Folder.name.contains_string(query)).config(distinct = True, offset = album_offset, limit = album_count)
|
||||
song_query = store.find(Track, Track.title.contains_string(query))[song_offset : song_offset + song_count]
|
||||
|
||||
return request.formatter({ 'searchResult2': {
|
||||
'artist': [ { 'id': str(a.id), 'name': a.name } for a in artist_query ],
|
||||
@ -107,9 +110,9 @@ def search_id3():
|
||||
if not query:
|
||||
return request.error_formatter(10, 'Missing query parameter')
|
||||
|
||||
artist_query = Artist.query.filter(Artist.name.contains(query)).slice(artist_offset, artist_offset + artist_count)
|
||||
album_query = Album.query.filter(Album.name.contains(query)).slice(album_offset, album_offset + album_count)
|
||||
song_query = Track.query.filter(Track.title.contains(query)).slice(song_offset, song_offset + song_count)
|
||||
artist_query = store.find(Artist, Artist.name.contains_string(query))[artist_offset : artist_offset + artist_count]
|
||||
album_query = store.find(Album, Album.name.contains_string(query))[album_offset : album_offset + album_count]
|
||||
song_query = store.find(Track, Track.title.contains_string(query))[song_offset : song_offset + song_count]
|
||||
|
||||
return request.formatter({ 'searchResult2': {
|
||||
'artist': [ a.as_subsonic_artist(request.user) for a in artist_query ],
|
||||
|
@ -19,7 +19,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import request
|
||||
from supysonic.web import app
|
||||
from supysonic.web import app, store
|
||||
from supysonic.db import User
|
||||
from supysonic.managers.user import UserManager
|
||||
|
||||
@ -32,7 +32,7 @@ def user_info():
|
||||
if username != request.username and not request.user.admin:
|
||||
return request.error_formatter(50, 'Admin restricted')
|
||||
|
||||
user = User.query.filter(User.name == username).first()
|
||||
user = store.find(User, User.name == username).one()
|
||||
if user is None:
|
||||
return request.error_formatter(0, 'Unknown user')
|
||||
|
||||
@ -43,7 +43,7 @@ def users_info():
|
||||
if not request.user.admin:
|
||||
return request.error_formatter(50, 'Admin restricted')
|
||||
|
||||
return request.formatter({ 'users': { 'user': [ u.as_subsonic_user() for u in User.query.all() ] } })
|
||||
return request.formatter({ 'users': { 'user': [ u.as_subsonic_user() for u in store.find(User) ] } })
|
||||
|
||||
@app.route('/rest/createUser.view', methods = [ 'GET', 'POST' ])
|
||||
def user_add():
|
||||
@ -55,7 +55,7 @@ def user_add():
|
||||
return request.error_formatter(10, 'Missing parameter')
|
||||
admin = True if admin in (True, 'True', 'true', 1, '1') else False
|
||||
|
||||
status = UserManager.add(username, password, email, admin)
|
||||
status = UserManager.add(store, username, password, email, admin)
|
||||
if status == UserManager.NAME_EXISTS:
|
||||
return request.error_formatter(0, 'There is already a user with that username')
|
||||
|
||||
@ -67,11 +67,11 @@ def user_del():
|
||||
return request.error_formatter(50, 'Admin restricted')
|
||||
|
||||
username = request.args.get('username')
|
||||
user = User.query.filter(User.name == username).first()
|
||||
user = store.find(User, User.name == username).one()
|
||||
if not user:
|
||||
return request.error_formatter(70, 'Unknown user')
|
||||
|
||||
status = UserManager.delete(user.id)
|
||||
status = UserManager.delete(store, user.id)
|
||||
if status != UserManager.SUCCESS:
|
||||
return request.error_formatter(0, UserManager.error_str(status))
|
||||
|
||||
@ -86,7 +86,7 @@ def user_changepass():
|
||||
if username != request.username and not request.user.admin:
|
||||
return request.error_formatter(50, 'Admin restricted')
|
||||
|
||||
status = UserManager.change_password2(username, password)
|
||||
status = UserManager.change_password2(store, username, password)
|
||||
if status != UserManager.SUCCESS:
|
||||
return request.error_formatter(0, UserManager.error_str(status))
|
||||
|
||||
|
412
supysonic/db.py
412
supysonic/db.py
@ -18,125 +18,44 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from supysonic import config
|
||||
|
||||
from sqlalchemy import create_engine, Table, Column, ForeignKey, func
|
||||
from sqlalchemy import Integer, String, Boolean, DateTime
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from sqlalchemy.types import TypeDecorator, BINARY
|
||||
from sqlalchemy.dialects.postgresql import UUID as pgUUID
|
||||
from storm.properties import *
|
||||
from storm.references import Reference, ReferenceSet
|
||||
from storm.database import create_database
|
||||
from storm.store import Store
|
||||
from storm.variables import Variable
|
||||
|
||||
import uuid, datetime, time
|
||||
import os.path
|
||||
|
||||
class UUID(TypeDecorator):
|
||||
"""Platform-somewhat-independent UUID type
|
||||
|
||||
Uses Postgresql's UUID type, otherwise uses BINARY(16),
|
||||
should be more efficient than a CHAR(32).
|
||||
|
||||
Mix of http://stackoverflow.com/a/812363
|
||||
and http://www.sqlalchemy.org/docs/core/types.html#backend-agnostic-guid-type
|
||||
"""
|
||||
|
||||
impl = BINARY
|
||||
|
||||
def load_dialect_impl(self, dialect):
|
||||
if dialect.name == 'postgresql':
|
||||
return dialect.type_descriptor(pgUUID())
|
||||
else:
|
||||
return dialect.type_descriptor(BINARY(16))
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value and isinstance(value, uuid.UUID):
|
||||
if dialect.name == 'postgresql':
|
||||
return str(value)
|
||||
return value.bytes
|
||||
if value and not isinstance(value, uuid.UUID):
|
||||
raise ValueError, 'value %s is not a valid uuid.UUID' % value
|
||||
return None
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value:
|
||||
if dialect.name == 'postgresql':
|
||||
return uuid.UUID(value)
|
||||
return uuid.UUID(bytes = value)
|
||||
return None
|
||||
|
||||
def is_mutable(self):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def gen_id_column():
|
||||
return Column(UUID, primary_key = True, default = uuid.uuid4)
|
||||
|
||||
def now():
|
||||
return datetime.datetime.now().replace(microsecond = 0)
|
||||
|
||||
config.check()
|
||||
engine = create_engine(config.get('base', 'database_uri'), convert_unicode = True)
|
||||
session = scoped_session(sessionmaker(autocommit = False, autoflush = False, bind = engine))
|
||||
class UnicodeOrStrVariable(Variable):
|
||||
__slots__ = ()
|
||||
|
||||
Base = declarative_base()
|
||||
Base.query = session.query_property()
|
||||
def parse_set(self, value, from_db):
|
||||
if isinstance(value, unicode):
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
return unicode(value)
|
||||
raise TypeError("Expected unicode, found %r: %r" % (type(value), value))
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'user'
|
||||
Unicode.variable_class = UnicodeOrStrVariable
|
||||
|
||||
id = UUID.gen_id_column()
|
||||
name = Column(String(64), unique = True)
|
||||
mail = Column(String(255))
|
||||
password = Column(String(40))
|
||||
salt = Column(String(6))
|
||||
admin = Column(Boolean, default = False)
|
||||
lastfm_session = Column(String(32), nullable = True)
|
||||
lastfm_status = Column(Boolean, default = True) # True: ok/unlinked, False: invalid session
|
||||
class Folder(object):
|
||||
__storm_table__ = 'folder'
|
||||
|
||||
last_play_id = Column(UUID, ForeignKey('track.id'), nullable = True)
|
||||
last_play = relationship('Track')
|
||||
last_play_date = Column(DateTime, nullable = True)
|
||||
id = UUID(primary = True, default_factory = uuid.uuid4)
|
||||
root = Bool(default = False)
|
||||
name = Unicode()
|
||||
path = Unicode() # unique
|
||||
created = DateTime(default_factory = now)
|
||||
has_cover_art = Bool(default = False)
|
||||
last_scan = Int(default = 0)
|
||||
|
||||
def as_subsonic_user(self):
|
||||
return {
|
||||
'username': self.name,
|
||||
'email': self.mail,
|
||||
'scrobblingEnabled': self.lastfm_session is not None and self.lastfm_status,
|
||||
'adminRole': self.admin,
|
||||
'settingsRole': True,
|
||||
'downloadRole': True,
|
||||
'uploadRole': False,
|
||||
'playlistRole': True,
|
||||
'coverArtRole': False,
|
||||
'commentRole': False,
|
||||
'podcastRole': False,
|
||||
'streamRole': True,
|
||||
'jukeboxRole': False,
|
||||
'shareRole': False
|
||||
}
|
||||
|
||||
class ClientPrefs(Base):
|
||||
__tablename__ = 'client_prefs'
|
||||
|
||||
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
|
||||
client_name = Column(String(32), nullable = False, primary_key = True)
|
||||
format = Column(String(8), nullable = True)
|
||||
bitrate = Column(Integer, nullable = True)
|
||||
|
||||
class Folder(Base):
|
||||
__tablename__ = 'folder'
|
||||
|
||||
id = UUID.gen_id_column()
|
||||
root = Column(Boolean, default = False)
|
||||
name = Column(String(255))
|
||||
path = Column(String(4096)) # should be unique, but mysql don't like such large columns
|
||||
created = Column(DateTime, default = now)
|
||||
has_cover_art = Column(Boolean, default = False)
|
||||
last_scan = Column(Integer, default = 0)
|
||||
|
||||
parent_id = Column(UUID, ForeignKey('folder.id'), nullable = True)
|
||||
children = relationship('Folder', backref = backref('parent', remote_side = [ id ]))
|
||||
parent_id = UUID() # nullable
|
||||
parent = Reference(parent_id, id)
|
||||
children = ReferenceSet(id, parent_id)
|
||||
|
||||
def as_subsonic_child(self, user):
|
||||
info = {
|
||||
@ -152,47 +71,46 @@ class Folder(Base):
|
||||
if self.has_cover_art:
|
||||
info['coverArt'] = str(self.id)
|
||||
|
||||
starred = StarredFolder.query.get((user.id, self.id))
|
||||
starred = Store.of(self).get(StarredFolder, (user.id, self.id))
|
||||
if starred:
|
||||
info['starred'] = starred.date.isoformat()
|
||||
|
||||
rating = RatingFolder.query.get((user.id, self.id))
|
||||
rating = Store.of(self).get(RatingFolder, (user.id, self.id))
|
||||
if rating:
|
||||
info['userRating'] = rating.rating
|
||||
avgRating = RatingFolder.query.filter(RatingFolder.rated_id == self.id).value(func.avg(RatingFolder.rating))
|
||||
avgRating = Store.of(self).find(RatingFolder, RatingFolder.rated_id == self.id).avg(RatingFolder.rating)
|
||||
if avgRating:
|
||||
info['averageRating'] = avgRating
|
||||
|
||||
return info
|
||||
|
||||
class Artist(Base):
|
||||
__tablename__ = 'artist'
|
||||
class Artist(object):
|
||||
__storm_table__ = 'artist'
|
||||
|
||||
id = UUID.gen_id_column()
|
||||
name = Column(String(255), unique = True)
|
||||
albums = relationship('Album', backref = 'artist')
|
||||
id = UUID(primary = True, default_factory = uuid.uuid4)
|
||||
name = Unicode() # unique
|
||||
|
||||
def as_subsonic_artist(self, user):
|
||||
info = {
|
||||
'id': str(self.id),
|
||||
'name': self.name,
|
||||
# coverArt
|
||||
'albumCount': len(self.albums)
|
||||
'albumCount': self.albums.count()
|
||||
}
|
||||
|
||||
starred = StarredArtist.query.get((user.id, self.id))
|
||||
starred = Store.of(self).get(StarredArtist, (user.id, self.id))
|
||||
if starred:
|
||||
info['starred'] = starred.date.isoformat()
|
||||
|
||||
return info
|
||||
|
||||
class Album(Base):
|
||||
__tablename__ = 'album'
|
||||
class Album(object):
|
||||
__storm_table__ = 'album'
|
||||
|
||||
id = UUID.gen_id_column()
|
||||
name = Column(String(255))
|
||||
artist_id = Column(UUID, ForeignKey('artist.id'))
|
||||
tracks = relationship('Track', backref = 'album')
|
||||
id = UUID(primary = True, default_factory = uuid.uuid4)
|
||||
name = Unicode()
|
||||
artist_id = UUID()
|
||||
artist = Reference(artist_id, Artist.id)
|
||||
|
||||
def as_subsonic_album(self, user):
|
||||
info = {
|
||||
@ -200,14 +118,16 @@ class Album(Base):
|
||||
'name': self.name,
|
||||
'artist': self.artist.name,
|
||||
'artistId': str(self.artist_id),
|
||||
'songCount': len(self.tracks),
|
||||
'duration': sum(map(lambda t: t.duration, self.tracks)),
|
||||
'created': min(map(lambda t: t.created, self.tracks)).isoformat()
|
||||
'songCount': self.tracks.count(),
|
||||
'duration': sum(self.tracks.values(Track.duration)),
|
||||
'created': min(self.tracks.values(Track.created)).isoformat()
|
||||
}
|
||||
if self.tracks[0].folder.has_cover_art:
|
||||
info['coverArt'] = str(self.tracks[0].folder_id)
|
||||
|
||||
starred = StarredAlbum.query.get((user.id, self.id))
|
||||
track_with_cover = self.tracks.find(Track.folder_id == Folder.id, Folder.has_cover_art).any()
|
||||
if track_with_cover:
|
||||
info['coverArt'] = str(track_with_cover.folder_id)
|
||||
|
||||
starred = Store.of(self).get(StarredAlbum, (user.id, self.id))
|
||||
if starred:
|
||||
info['starred'] = starred.date.isoformat()
|
||||
|
||||
@ -217,36 +137,39 @@ class Album(Base):
|
||||
year = min(map(lambda t: t.year if t.year else 9999, self.tracks))
|
||||
return '%i%s' % (year, self.name.lower())
|
||||
|
||||
class Track(Base):
|
||||
__tablename__ = 'track'
|
||||
Artist.albums = ReferenceSet(Artist.id, Album.artist_id)
|
||||
|
||||
id = UUID.gen_id_column()
|
||||
disc = Column(Integer)
|
||||
number = Column(Integer)
|
||||
title = Column(String(255))
|
||||
year = Column(Integer, nullable = True)
|
||||
genre = Column(String(255), nullable = True)
|
||||
duration = Column(Integer)
|
||||
album_id = Column(UUID, ForeignKey('album.id'))
|
||||
bitrate = Column(Integer)
|
||||
class Track(object):
|
||||
__storm_table__ = 'track'
|
||||
|
||||
path = Column(String(4096)) # should be unique, but mysql don't like such large columns
|
||||
content_type = Column(String(32))
|
||||
created = Column(DateTime, default = now)
|
||||
last_modification = Column(Integer)
|
||||
id = UUID(primary = True, default_factory = uuid.uuid4)
|
||||
disc = Int()
|
||||
number = Int()
|
||||
title = Unicode()
|
||||
year = Int() # nullable
|
||||
genre = Unicode() # nullable
|
||||
duration = Int()
|
||||
album_id = UUID()
|
||||
album = Reference(album_id, Album.id)
|
||||
bitrate = Int()
|
||||
|
||||
play_count = Column(Integer, default = 0)
|
||||
last_play = Column(DateTime, nullable = True)
|
||||
path = Unicode() # unique
|
||||
content_type = Unicode()
|
||||
created = DateTime(default_factory = now)
|
||||
last_modification = Int()
|
||||
|
||||
root_folder_id = Column(UUID, ForeignKey('folder.id'))
|
||||
root_folder = relationship('Folder', primaryjoin = Folder.id == root_folder_id)
|
||||
folder_id = Column(UUID, ForeignKey('folder.id'))
|
||||
folder = relationship('Folder', primaryjoin = Folder.id == folder_id, backref = 'tracks')
|
||||
play_count = Int(default = 0)
|
||||
last_play = DateTime() # nullable
|
||||
|
||||
root_folder_id = UUID()
|
||||
root_folder = Reference(root_folder_id, Folder.id)
|
||||
folder_id = UUID()
|
||||
folder = Reference(folder_id, Folder.id)
|
||||
|
||||
def as_subsonic_child(self, user):
|
||||
info = {
|
||||
'id': str(self.id),
|
||||
'parent': str(self.folder.id),
|
||||
'parent': str(self.folder_id),
|
||||
'isDir': False,
|
||||
'title': self.title,
|
||||
'album': self.album.name,
|
||||
@ -261,8 +184,8 @@ class Track(Base):
|
||||
'isVideo': False,
|
||||
'discNumber': self.disc,
|
||||
'created': self.created.isoformat(),
|
||||
'albumId': str(self.album.id),
|
||||
'artistId': str(self.album.artist.id),
|
||||
'albumId': str(self.album_id),
|
||||
'artistId': str(self.album.artist_id),
|
||||
'type': 'music'
|
||||
}
|
||||
|
||||
@ -273,14 +196,14 @@ class Track(Base):
|
||||
if self.folder.has_cover_art:
|
||||
info['coverArt'] = str(self.folder_id)
|
||||
|
||||
starred = StarredTrack.query.get((user.id, self.id))
|
||||
starred = Store.of(self).get(StarredTrack, (user.id, self.id))
|
||||
if starred:
|
||||
info['starred'] = starred.date.isoformat()
|
||||
|
||||
rating = RatingTrack.query.get((user.id, self.id))
|
||||
rating = Store.of(self).get(RatingTrack, (user.id, self.id))
|
||||
if rating:
|
||||
info['userRating'] = rating.rating
|
||||
avgRating = RatingTrack.query.filter(RatingTrack.rated_id == self.id).value(func.avg(RatingTrack.rating))
|
||||
avgRating = Store.of(self).find(RatingTrack, RatingTrack.rated_id == self.id).avg(RatingTrack.rating)
|
||||
if avgRating:
|
||||
info['averageRating'] = avgRating
|
||||
|
||||
@ -301,75 +224,109 @@ class Track(Base):
|
||||
def sort_key(self):
|
||||
return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower()
|
||||
|
||||
class StarredFolder(Base):
|
||||
__tablename__ = 'starred_folder'
|
||||
Folder.tracks = ReferenceSet(Folder.id, Track.folder_id)
|
||||
Album.tracks = ReferenceSet(Album.id, Track.album_id)
|
||||
|
||||
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
|
||||
starred_id = Column(UUID, ForeignKey('folder.id'), primary_key = True)
|
||||
date = Column(DateTime, default = now)
|
||||
class User(object):
|
||||
__storm_table__ = 'user'
|
||||
|
||||
user = relationship('User')
|
||||
starred = relationship('Folder')
|
||||
id = UUID(primary = True, default_factory = uuid.uuid4)
|
||||
name = Unicode() # unique
|
||||
mail = Unicode()
|
||||
password = Unicode()
|
||||
salt = Unicode()
|
||||
admin = Bool(default = False)
|
||||
lastfm_session = Unicode() # nullable
|
||||
lastfm_status = Bool(default = True) # True: ok/unlinked, False: invalid session
|
||||
|
||||
class StarredArtist(Base):
|
||||
__tablename__ = 'starred_artist'
|
||||
last_play_id = UUID() # nullable
|
||||
last_play = Reference(last_play_id, Track.id)
|
||||
last_play_date = DateTime() # nullable
|
||||
|
||||
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
|
||||
starred_id = Column(UUID, ForeignKey('artist.id'), primary_key = True)
|
||||
date = Column(DateTime, default = now)
|
||||
def as_subsonic_user(self):
|
||||
return {
|
||||
'username': self.name,
|
||||
'email': self.mail,
|
||||
'scrobblingEnabled': self.lastfm_session is not None and self.lastfm_status,
|
||||
'adminRole': self.admin,
|
||||
'settingsRole': True,
|
||||
'downloadRole': True,
|
||||
'uploadRole': False,
|
||||
'playlistRole': True,
|
||||
'coverArtRole': False,
|
||||
'commentRole': False,
|
||||
'podcastRole': False,
|
||||
'streamRole': True,
|
||||
'jukeboxRole': False,
|
||||
'shareRole': False
|
||||
}
|
||||
|
||||
user = relationship('User')
|
||||
starred = relationship('Artist')
|
||||
class ClientPrefs(object):
|
||||
__storm_table__ = 'client_prefs'
|
||||
__storm_primary__ = 'user_id', 'client_name'
|
||||
|
||||
class StarredAlbum(Base):
|
||||
__tablename__ = 'starred_album'
|
||||
user_id = UUID()
|
||||
client_name = Unicode()
|
||||
format = Unicode() # nullable
|
||||
bitrate = Int() # nullable
|
||||
|
||||
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
|
||||
starred_id = Column(UUID, ForeignKey('album.id'), primary_key = True)
|
||||
date = Column(DateTime, default = now)
|
||||
class BaseStarred(object):
|
||||
__storm_primary__ = 'user_id', 'starred_id'
|
||||
|
||||
user = relationship('User')
|
||||
starred = relationship('Album')
|
||||
user_id = UUID()
|
||||
starred_id = UUID()
|
||||
date = DateTime(default_factory = now)
|
||||
|
||||
class StarredTrack(Base):
|
||||
__tablename__ = 'starred_track'
|
||||
user = Reference(user_id, User.id)
|
||||
|
||||
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
|
||||
starred_id = Column(UUID, ForeignKey('track.id'), primary_key = True)
|
||||
date = Column(DateTime, default = now)
|
||||
class StarredFolder(BaseStarred):
|
||||
__storm_table__ = 'starred_folder'
|
||||
|
||||
user = relationship('User')
|
||||
starred = relationship('Track')
|
||||
starred = Reference(BaseStarred.starred_id, Folder.id)
|
||||
|
||||
class RatingFolder(Base):
|
||||
__tablename__ = 'rating_folder'
|
||||
class StarredArtist(BaseStarred):
|
||||
__storm_table__ = 'starred_artist'
|
||||
|
||||
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
|
||||
rated_id = Column(UUID, ForeignKey('folder.id'), primary_key = True)
|
||||
rating = Column(Integer)
|
||||
starred = Reference(BaseStarred.starred_id, Artist.id)
|
||||
|
||||
user = relationship('User')
|
||||
rated = relationship('Folder')
|
||||
class StarredAlbum(BaseStarred):
|
||||
__storm_table__ = 'starred_album'
|
||||
|
||||
class RatingTrack(Base):
|
||||
__tablename__ = 'rating_track'
|
||||
starred = Reference(BaseStarred.starred_id, Album.id)
|
||||
|
||||
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
|
||||
rated_id = Column(UUID, ForeignKey('track.id'), primary_key = True)
|
||||
rating = Column(Integer)
|
||||
class StarredTrack(BaseStarred):
|
||||
__storm_table__ = 'starred_track'
|
||||
|
||||
user = relationship('User')
|
||||
rated = relationship('Track')
|
||||
starred = Reference(BaseStarred.starred_id, Track.id)
|
||||
|
||||
class ChatMessage(Base):
|
||||
__tablename__ = 'chat_message'
|
||||
class BaseRating(object):
|
||||
__storm_primary__ = 'user_id', 'rated_id'
|
||||
|
||||
id = UUID.gen_id_column()
|
||||
user_id = Column(UUID, ForeignKey('user.id'))
|
||||
time = Column(Integer, default = lambda: int(time.time()))
|
||||
message = Column(String(512))
|
||||
user_id = UUID()
|
||||
rated_id = UUID()
|
||||
rating = Int()
|
||||
|
||||
user = relationship('User')
|
||||
user = Reference(user_id, User.id)
|
||||
|
||||
class RatingFolder(BaseRating):
|
||||
__storm_table__ = 'rating_folder'
|
||||
|
||||
rated = Reference(BaseRating.rated_id, Folder.id)
|
||||
|
||||
class RatingTrack(BaseRating):
|
||||
__storm_table__ = 'rating_track'
|
||||
|
||||
rated = Reference(BaseRating.rated_id, Track.id)
|
||||
|
||||
class ChatMessage(object):
|
||||
__storm_table__ = 'chat_message'
|
||||
|
||||
id = UUID(primary = True, default_factory = uuid.uuid4)
|
||||
user_id = UUID()
|
||||
time = Int(default_factory = lambda: int(time.time()))
|
||||
message = Unicode()
|
||||
|
||||
user = Reference(user_id, User.id)
|
||||
|
||||
def responsize(self):
|
||||
return {
|
||||
@ -378,23 +335,17 @@ class ChatMessage(Base):
|
||||
'message': self.message
|
||||
}
|
||||
|
||||
playlist_track_assoc = Table('playlist_track', Base.metadata,
|
||||
Column('playlist_id', UUID, ForeignKey('playlist.id')),
|
||||
Column('track_id', UUID, ForeignKey('track.id'))
|
||||
)
|
||||
class Playlist(object):
|
||||
__storm_table__ = 'playlist'
|
||||
|
||||
class Playlist(Base):
|
||||
__tablename__ = 'playlist'
|
||||
id = UUID(primary = True, default_factory = uuid.uuid4)
|
||||
user_id = UUID()
|
||||
name = Unicode()
|
||||
comment = Unicode() # nullable
|
||||
public = Bool(default = False)
|
||||
created = DateTime(default_factory = now)
|
||||
|
||||
id = UUID.gen_id_column()
|
||||
user_id = Column(UUID, ForeignKey('user.id'))
|
||||
name = Column(String(255))
|
||||
comment = Column(String(255), nullable = True)
|
||||
public = Column(Boolean, default = False)
|
||||
created = Column(DateTime, default = now)
|
||||
|
||||
user = relationship('User')
|
||||
tracks = relationship('Track', secondary = playlist_track_assoc)
|
||||
user = Reference(user_id, User.id)
|
||||
|
||||
def as_subsonic_playlist(self, user):
|
||||
info = {
|
||||
@ -402,18 +353,25 @@ class Playlist(Base):
|
||||
'name': self.name if self.user_id == user.id else '[%s] %s' % (self.user.name, self.name),
|
||||
'owner': self.user.name,
|
||||
'public': self.public,
|
||||
'songCount': len(self.tracks),
|
||||
'duration': sum(map(lambda t: t.duration, self.tracks)),
|
||||
'songCount': self.tracks.count(),
|
||||
'duration': self.tracks.find().sum(Track.duration),
|
||||
'created': self.created.isoformat()
|
||||
}
|
||||
if self.comment:
|
||||
info['comment'] = self.comment
|
||||
return info
|
||||
|
||||
def init_db():
|
||||
Base.metadata.create_all(bind = engine)
|
||||
class PlaylistTrack(object):
|
||||
__storm_table__ = 'playlist_track'
|
||||
__storm_primary__ = 'playlist_id', 'track_id'
|
||||
|
||||
def recreate_db():
|
||||
Base.metadata.drop_all(bind = engine)
|
||||
Base.metadata.create_all(bind = engine)
|
||||
playlist_id = UUID()
|
||||
track_id = UUID()
|
||||
|
||||
Playlist.tracks = ReferenceSet(Playlist.id, PlaylistTrack.playlist_id, PlaylistTrack.track_id, Track.id)
|
||||
|
||||
def get_store(database_uri):
|
||||
database = create_database(database_uri)
|
||||
store = Store(database)
|
||||
return store
|
||||
|
||||
|
@ -18,7 +18,9 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from supysonic.web import app
|
||||
from flask import session
|
||||
from supysonic.web import app, store
|
||||
from supysonic.db import Artist, Album, Track
|
||||
from supysonic.managers.user import UserManager
|
||||
|
||||
app.add_template_filter(str)
|
||||
@ -32,7 +34,7 @@ def login_check():
|
||||
should_login = False
|
||||
if not session.get('userid'):
|
||||
should_login = True
|
||||
elif UserManager.get(session.get('userid'))[0] != UserManager.SUCCESS:
|
||||
elif UserManager.get(store, session.get('userid'))[0] != UserManager.SUCCESS:
|
||||
session.clear()
|
||||
should_login = True
|
||||
|
||||
@ -43,11 +45,11 @@ def login_check():
|
||||
@app.route('/')
|
||||
def index():
|
||||
stats = {
|
||||
'artists': db.Artist.query.count(),
|
||||
'albums': db.Album.query.count(),
|
||||
'tracks': db.Track.query.count()
|
||||
'artists': store.find(Artist).count(),
|
||||
'albums': store.find(Album).count(),
|
||||
'tracks': store.find(Track).count()
|
||||
}
|
||||
return render_template('home.html', stats = stats, admin = UserManager.get(session.get('userid'))[1].admin)
|
||||
return render_template('home.html', stats = stats, admin = UserManager.get(store, session.get('userid'))[1].admin)
|
||||
|
||||
from .user import *
|
||||
from .folder import *
|
||||
|
@ -18,11 +18,12 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import request, flash, render_template, redirect, url_for, session as fl_sess
|
||||
from flask import request, flash, render_template, redirect, url_for, session
|
||||
import os.path
|
||||
import uuid
|
||||
|
||||
from supysonic.web import app
|
||||
from supysonic.db import session, Folder
|
||||
from supysonic.web import app, store
|
||||
from supysonic.db import Folder
|
||||
from supysonic.scanner import Scanner
|
||||
from supysonic.managers.user import UserManager
|
||||
from supysonic.managers.folder import FolderManager
|
||||
@ -32,12 +33,12 @@ def check_admin():
|
||||
if not request.path.startswith('/folder'):
|
||||
return
|
||||
|
||||
if not UserManager.get(fl_sess.get('userid'))[1].admin:
|
||||
if not UserManager.get(store, session.get('userid'))[1].admin:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/folder')
|
||||
def folder_index():
|
||||
return render_template('folders.html', folders = Folder.query.filter(Folder.root == True).all())
|
||||
return render_template('folders.html', folders = store.find(Folder, Folder.root == True))
|
||||
|
||||
@app.route('/folder/add', methods = [ 'GET', 'POST' ])
|
||||
def add_folder():
|
||||
@ -55,7 +56,7 @@ def add_folder():
|
||||
if error:
|
||||
return render_template('addfolder.html')
|
||||
|
||||
ret = FolderManager.add(name, path)
|
||||
ret = FolderManager.add(store, name, path)
|
||||
if ret != FolderManager.SUCCESS:
|
||||
flash(FolderManager.error_str(ret))
|
||||
return render_template('addfolder.html')
|
||||
@ -72,7 +73,7 @@ def del_folder(id):
|
||||
flash('Invalid folder id')
|
||||
return redirect(url_for('folder_index'))
|
||||
|
||||
ret = FolderManager.delete(idid)
|
||||
ret = FolderManager.delete(store, idid)
|
||||
if ret != FolderManager.SUCCESS:
|
||||
flash(FolderManager.error_str(ret))
|
||||
else:
|
||||
@ -83,18 +84,18 @@ def del_folder(id):
|
||||
@app.route('/folder/scan')
|
||||
@app.route('/folder/scan/<id>')
|
||||
def scan_folder(id = None):
|
||||
s = Scanner(session)
|
||||
s = Scanner(store)
|
||||
if id is None:
|
||||
for folder in Folder.query.filter(Folder.root == True):
|
||||
FolderManager.scan(folder.id, s)
|
||||
for folder in store.find(Folder, Folder.root == True):
|
||||
FolderManager.scan(store, folder.id, s)
|
||||
else:
|
||||
status = FolderManager.scan(id, s)
|
||||
status = FolderManager.scan(store, id, s)
|
||||
if status != FolderManager.SUCCESS:
|
||||
flash(FolderManager.error_str(status))
|
||||
return redirect(url_for('folder_index'))
|
||||
|
||||
added, deleted = s.stats()
|
||||
session.commit()
|
||||
store.commit()
|
||||
|
||||
flash('Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2]))
|
||||
flash('Deleted: %i artists, %i albums, %i tracks' % (deleted[0], deleted[1], deleted[2]))
|
||||
|
@ -20,13 +20,13 @@
|
||||
|
||||
from flask import request, session, flash, render_template, redirect, url_for
|
||||
import uuid
|
||||
from supysonic.web import app
|
||||
from supysonic import db
|
||||
from supysonic.web import app, store
|
||||
from supysonic.db import Playlist
|
||||
|
||||
@app.route('/playlist')
|
||||
def playlist_index():
|
||||
return render_template('playlists.html', mine = db.Playlist.query.filter(db.Playlist.user_id == uuid.UUID(session.get('userid'))),
|
||||
others = db.Playlist.query.filter(db.Playlist.user_id != uuid.UUID(session.get('userid'))))
|
||||
return render_template('playlists.html', mine = store.find(Playlist, Playlist.user_id == uuid.UUID(session.get('userid'))),
|
||||
others = store.find(Playlist, Playlist.user_id != uuid.UUID(session.get('userid'))))
|
||||
|
||||
@app.route('/playlist/<uid>')
|
||||
def playlist_details(uid):
|
||||
@ -36,7 +36,7 @@ def playlist_details(uid):
|
||||
flash('Invalid playlist id')
|
||||
return redirect(url_for('playlist_index'))
|
||||
|
||||
playlist = db.Playlist.query.get(uid)
|
||||
playlist = store.get(Playlist, uid)
|
||||
if not playlist:
|
||||
flash('Unknown playlist')
|
||||
return redirect(url_for('playlist_index'))
|
||||
@ -51,7 +51,7 @@ def playlist_update(uid):
|
||||
flash('Invalid playlist id')
|
||||
return redirect(url_for('playlist_index'))
|
||||
|
||||
playlist = db.Playlist.query.get(uid)
|
||||
playlist = store.get(Playlist, uid)
|
||||
if not playlist:
|
||||
flash('Unknown playlist')
|
||||
return redirect(url_for('playlist_index'))
|
||||
@ -63,7 +63,7 @@ def playlist_update(uid):
|
||||
else:
|
||||
playlist.name = request.form.get('name')
|
||||
playlist.public = request.form.get('public') in (True, 'True', 1, '1', 'on', 'checked')
|
||||
db.session.commit()
|
||||
store.commit()
|
||||
flash('Playlist updated.')
|
||||
|
||||
return playlist_details(uid)
|
||||
@ -76,14 +76,14 @@ def playlist_delete(uid):
|
||||
flash('Invalid playlist id')
|
||||
return redirect(url_for('playlist_index'))
|
||||
|
||||
playlist = db.Playlist.query.get(uid)
|
||||
playlist = store.get(Playlist, uid)
|
||||
if not playlist:
|
||||
flash('Unknown playlist')
|
||||
elif str(playlist.user_id) != session.get('userid'):
|
||||
flash("You're not allowed to delete this playlist")
|
||||
else:
|
||||
db.session.delete(playlist)
|
||||
db.session.commit()
|
||||
store.remove(playlist)
|
||||
store.commit()
|
||||
flash('Playlist deleted')
|
||||
|
||||
return redirect(url_for('playlist_index'))
|
||||
|
@ -20,9 +20,9 @@
|
||||
|
||||
from flask import request, session, flash, render_template, redirect, url_for, make_response
|
||||
|
||||
from supysonic.web import app
|
||||
from supysonic.web import app, store
|
||||
from supysonic.managers.user import UserManager
|
||||
from supysonic.db import User, ClientPrefs, session as db_sess
|
||||
from supysonic.db import User, ClientPrefs
|
||||
import uuid, csv
|
||||
from supysonic import config
|
||||
from supysonic.lastfm import LastFm
|
||||
@ -32,46 +32,46 @@ def check_admin():
|
||||
if not request.path.startswith('/user'):
|
||||
return
|
||||
|
||||
if request.endpoint in ('user_index', 'add_user', 'del_user', 'export_users', 'import_users', 'do_user_import') and not UserManager.get(session.get('userid'))[1].admin:
|
||||
if request.endpoint in ('user_index', 'add_user', 'del_user', 'export_users', 'import_users', 'do_user_import') and not UserManager.get(store, session.get('userid'))[1].admin:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/user')
|
||||
def user_index():
|
||||
return render_template('users.html', users = User.query.all())
|
||||
return render_template('users.html', users = store.find(User))
|
||||
|
||||
@app.route('/user/me')
|
||||
def user_profile():
|
||||
prefs = ClientPrefs.query.filter(ClientPrefs.user_id == uuid.UUID(session.get('userid')))
|
||||
return render_template('profile.html', user = UserManager.get(session.get('userid'))[1], api_key = config.get('lastfm', 'api_key'), clients = prefs)
|
||||
prefs = store.find(ClientPrefs, ClientPrefs.user_id == uuid.UUID(session.get('userid')))
|
||||
return render_template('profile.html', user = UserManager.get(store, session.get('userid'))[1], api_key = config.get('lastfm', 'api_key'), clients = prefs)
|
||||
|
||||
@app.route('/user/me', methods = [ 'POST' ])
|
||||
def update_clients():
|
||||
clients_opts = {}
|
||||
for client in set(map(lambda k: k.rsplit('_', 1)[0],request.form.keys())):
|
||||
for client in set(map(lambda k: k.rsplit('_', 1)[0], request.form.keys())):
|
||||
clients_opts[client] = { k.rsplit('_', 1)[1]: v for k, v in filter(lambda (k, v): k.startswith(client), request.form.iteritems()) }
|
||||
app.logger.debug(clients_opts)
|
||||
|
||||
for client, opts in clients_opts.iteritems():
|
||||
prefs = ClientPrefs.query.get((uuid.UUID(session.get('userid')), client))
|
||||
prefs = store.get(ClientPrefs, (uuid.UUID(session.get('userid')), client))
|
||||
if 'delete' in opts and opts['delete'] in [ 'on', 'true', 'checked', 'selected', '1' ]:
|
||||
db_sess.delete(prefs)
|
||||
store.remove(prefs)
|
||||
continue
|
||||
|
||||
prefs.format = opts['format'] if 'format' in opts and opts['format'] else None
|
||||
prefs.bitrate = int(opts['bitrate']) if 'bitrate' in opts and opts['bitrate'] else None
|
||||
|
||||
db_sess.commit()
|
||||
store.commit()
|
||||
flash('Clients preferences updated.')
|
||||
return user_profile()
|
||||
|
||||
@app.route('/user/changemail', methods = [ 'GET', 'POST' ])
|
||||
def change_mail():
|
||||
user = UserManager.get(session.get('userid'))[1]
|
||||
user = UserManager.get(store, session.get('userid'))[1]
|
||||
if request.method == 'POST':
|
||||
mail = request.form.get('mail')
|
||||
# No validation, lol.
|
||||
user.mail = mail
|
||||
db_sess.commit()
|
||||
store.commit()
|
||||
return redirect(url_for('user_profile'))
|
||||
|
||||
return render_template('change_mail.html', user = user)
|
||||
@ -92,14 +92,14 @@ def change_password():
|
||||
error = True
|
||||
|
||||
if not error:
|
||||
status = UserManager.change_password(session.get('userid'), current, new)
|
||||
status = UserManager.change_password(store, session.get('userid'), current, new)
|
||||
if status != UserManager.SUCCESS:
|
||||
flash(UserManager.error_str(status))
|
||||
else:
|
||||
flash('Password changed')
|
||||
return redirect(url_for('user_profile'))
|
||||
|
||||
return render_template('change_pass.html', user = UserManager.get(session.get('userid'))[1].name)
|
||||
return render_template('change_pass.html', user = UserManager.get(store, session.get('userid'))[1].name)
|
||||
|
||||
@app.route('/user/add', methods = [ 'GET', 'POST' ])
|
||||
def add_user():
|
||||
@ -119,12 +119,12 @@ def add_user():
|
||||
error = True
|
||||
|
||||
if admin is None:
|
||||
admin = True if User.query.filter(User.admin == True).count() == 0 else False
|
||||
admin = True if store.find(User, User.admin == True).count() == 0 else False
|
||||
else:
|
||||
admin = True
|
||||
|
||||
if not error:
|
||||
status = UserManager.add(name, passwd, mail, admin)
|
||||
status = UserManager.add(store, name, passwd, mail, admin)
|
||||
if status == UserManager.SUCCESS:
|
||||
flash("User '%s' successfully added" % name)
|
||||
return redirect(url_for('user_index'))
|
||||
@ -136,7 +136,7 @@ def add_user():
|
||||
|
||||
@app.route('/user/del/<uid>')
|
||||
def del_user(uid):
|
||||
status = UserManager.delete(uid)
|
||||
status = UserManager.delete(store, uid)
|
||||
if status == UserManager.SUCCESS:
|
||||
flash('Deleted user')
|
||||
else:
|
||||
@ -147,7 +147,7 @@ def del_user(uid):
|
||||
@app.route('/user/export')
|
||||
def export_users():
|
||||
resp = make_response('\n'.join([ '%s,%s,%s,%s,"%s",%s,%s,%s' % (u.id, u.name, u.mail, u.password, u.salt, u.admin, u.lastfm_session, u.lastfm_status)
|
||||
for u in User.query.all() ]))
|
||||
for u in store.find(User) ]))
|
||||
resp.headers['Content-disposition'] = 'attachment;filename=users.csv'
|
||||
resp.headers['Content-type'] = 'text/csv'
|
||||
return resp
|
||||
@ -168,12 +168,22 @@ def do_user_import():
|
||||
admin = admin == 'True'
|
||||
lfmsess = None if lfmsess == 'None' else lfmsess
|
||||
lfmstatus = lfmstatus == 'True'
|
||||
users.append(User(id = uuid.UUID(id), name = name, password = password, salt = salt, admin = admin, lastfm_session = lfmsess, lastfm_status = lfmstatus))
|
||||
|
||||
User.query.delete()
|
||||
user = User()
|
||||
user.id = uuid.UUID(id)
|
||||
user.name = name
|
||||
user.password = password
|
||||
user.salt = salt
|
||||
user.admin = admin
|
||||
user.lastfm_session = lfmsess
|
||||
user.lastfm_status = lfmstatus
|
||||
|
||||
users.append(user)
|
||||
|
||||
store.find(User).remove()
|
||||
for u in users:
|
||||
db_sess.add(u)
|
||||
db_sess.commit()
|
||||
store.add(u)
|
||||
store.commit()
|
||||
|
||||
return redirect(url_for('user_index'))
|
||||
|
||||
@ -184,16 +194,18 @@ def lastfm_reg():
|
||||
flash('Missing LastFM auth token')
|
||||
return redirect(url_for('user_profile'))
|
||||
|
||||
lfm = LastFm(UserManager.get(session.get('userid'))[1], app.logger)
|
||||
lfm = LastFm(UserManager.get(store, session.get('userid'))[1], app.logger)
|
||||
status, error = lfm.link_account(token)
|
||||
store.commit()
|
||||
flash(error if not status else 'Successfully linked LastFM account')
|
||||
|
||||
return redirect(url_for('user_profile'))
|
||||
|
||||
@app.route('/user/lastfm/unlink')
|
||||
def lastfm_unreg():
|
||||
lfm = LastFm(UserManager.get(session.get('userid'))[1], app.logger)
|
||||
lfm = LastFm(UserManager.get(store, session.get('userid'))[1], app.logger)
|
||||
lfm.unlink_account()
|
||||
store.commit()
|
||||
flash('Unliked LastFM account')
|
||||
return redirect(url_for('user_profile'))
|
||||
|
||||
@ -217,7 +229,7 @@ def login():
|
||||
error = True
|
||||
|
||||
if not error:
|
||||
status, user = UserManager.try_auth(name, password)
|
||||
status, user = UserManager.try_auth(store, name, password)
|
||||
if status == UserManager.SUCCESS:
|
||||
session['userid'] = str(user.id)
|
||||
session['username'] = user.name
|
||||
|
@ -20,7 +20,6 @@
|
||||
|
||||
import requests, hashlib
|
||||
from supysonic import config
|
||||
from supysonic.db import session
|
||||
|
||||
class LastFm:
|
||||
def __init__(self, user, logger):
|
||||
@ -42,13 +41,11 @@ class LastFm:
|
||||
else:
|
||||
self.__user.lastfm_session = res['session']['key']
|
||||
self.__user.lastfm_status = True
|
||||
session.commit()
|
||||
return True, 'OK'
|
||||
|
||||
def unlink_account(self):
|
||||
self.__user.lastfm_session = None
|
||||
self.__user.lastfm_status = True
|
||||
session.commit()
|
||||
|
||||
def now_playing(self, track):
|
||||
if not self.__enabled:
|
||||
@ -99,7 +96,6 @@ class LastFm:
|
||||
if 'error' in json:
|
||||
if json['error'] in (9, '9'):
|
||||
self.__user.lastfm_status = False
|
||||
session.commit()
|
||||
self.__logger.warn('LastFM error %i: %s' % (json['error'], json['message']))
|
||||
|
||||
return json
|
||||
|
@ -19,7 +19,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os.path, uuid
|
||||
from supysonic.db import Folder, Artist, session
|
||||
from supysonic.db import Folder, Artist, Album, Track
|
||||
|
||||
class FolderManager:
|
||||
SUCCESS = 0
|
||||
@ -30,7 +30,7 @@ class FolderManager:
|
||||
NO_SUCH_FOLDER = 5
|
||||
|
||||
@staticmethod
|
||||
def get(uid):
|
||||
def get(store, uid):
|
||||
if isinstance(uid, basestring):
|
||||
try:
|
||||
uid = uuid.UUID(uid)
|
||||
@ -41,33 +41,36 @@ class FolderManager:
|
||||
else:
|
||||
return FolderManager.INVALID_ID, None
|
||||
|
||||
folder = Folder.query.get(uid)
|
||||
folder = store.get(Folder, uid)
|
||||
if not folder:
|
||||
return FolderManager.NO_SUCH_FOLDER, None
|
||||
|
||||
return FolderManager.SUCCESS, folder
|
||||
|
||||
@staticmethod
|
||||
def add(name, path):
|
||||
if Folder.query.filter(Folder.name == name and Folder.root == True).first():
|
||||
def add(store, name, path):
|
||||
if not store.find(Folder, Folder.name == name, Folder.root == True).is_empty():
|
||||
return FolderManager.NAME_EXISTS
|
||||
|
||||
path = os.path.abspath(path)
|
||||
if not os.path.isdir(path):
|
||||
return FolderManager.INVALID_PATH
|
||||
folder = Folder.query.filter(Folder.path == path).first()
|
||||
if folder:
|
||||
if not store.find(Folder, Folder.path == path).is_empty():
|
||||
return FolderManager.PATH_EXISTS
|
||||
|
||||
folder = Folder(root = True, name = name, path = path)
|
||||
session.add(folder)
|
||||
session.commit()
|
||||
folder = Folder()
|
||||
folder.root = True
|
||||
folder.name = name
|
||||
folder.path = path
|
||||
|
||||
store.add(folder)
|
||||
store.commit()
|
||||
|
||||
return FolderManager.SUCCESS
|
||||
|
||||
@staticmethod
|
||||
def delete(uid):
|
||||
status, folder = FolderManager.get(uid)
|
||||
def delete(store, uid):
|
||||
status, folder = FolderManager.get(store, uid)
|
||||
if status != FolderManager.SUCCESS:
|
||||
return status
|
||||
|
||||
@ -75,37 +78,37 @@ class FolderManager:
|
||||
return FolderManager.NO_SUCH_FOLDER
|
||||
|
||||
# delete associated tracks and prune empty albums/artists
|
||||
for artist in Artist.query.all():
|
||||
for album in artist.albums[:]:
|
||||
for track in filter(lambda t: t.root_folder.id == folder.id, album.tracks):
|
||||
album.tracks.remove(track)
|
||||
session.delete(track)
|
||||
if len(album.tracks) == 0:
|
||||
artist.albums.remove(album)
|
||||
session.delete(album)
|
||||
if len(artist.albums) == 0:
|
||||
session.delete(artist)
|
||||
potentially_removed_albums = set()
|
||||
for track in store.find(Track, Track.root_folder_id == folder.id):
|
||||
potentially_removed_albums.add(track.album)
|
||||
store.remove(track)
|
||||
potentially_removed_artists = set()
|
||||
for album in filter(lambda album: album.tracks.count() == 0, potentially_removed_albums):
|
||||
potentially_removed_artists.add(album.artist)
|
||||
store.remove(album)
|
||||
for artist in filter(lambda artist: artist.albums.count() == 0, potentially_removed_artists):
|
||||
store.remove(artist)
|
||||
|
||||
def cleanup_folder(folder):
|
||||
for f in folder.children:
|
||||
cleanup_folder(f)
|
||||
session.delete(folder)
|
||||
store.remove(folder)
|
||||
|
||||
cleanup_folder(folder)
|
||||
session.commit()
|
||||
store.commit()
|
||||
|
||||
return FolderManager.SUCCESS
|
||||
|
||||
@staticmethod
|
||||
def delete_by_name(name):
|
||||
folder = Folder.query.filter(Folder.name == name and Folder.root == True).first()
|
||||
def delete_by_name(store, name):
|
||||
folder = store.find(Folder, Folder.name == name, Folder.root == True).one()
|
||||
if not folder:
|
||||
return FolderManager.NO_SUCH_FOLDER
|
||||
return FolderManager.delete(folder.id)
|
||||
return FolderManager.delete(store, folder.id)
|
||||
|
||||
@staticmethod
|
||||
def scan(uid, scanner, progress_callback = None):
|
||||
status, folder = FolderManager.get(uid)
|
||||
def scan(store, uid, scanner, progress_callback = None):
|
||||
status, folder = FolderManager.get(store, uid)
|
||||
if status != FolderManager.SUCCESS:
|
||||
return status
|
||||
|
||||
|
@ -21,7 +21,7 @@
|
||||
import string, random, hashlib
|
||||
import uuid
|
||||
|
||||
from supysonic.db import User, session
|
||||
from supysonic.db import User
|
||||
|
||||
class UserManager:
|
||||
SUCCESS = 0
|
||||
@ -31,7 +31,7 @@ class UserManager:
|
||||
WRONG_PASS = 4
|
||||
|
||||
@staticmethod
|
||||
def get(uid):
|
||||
def get(store, uid):
|
||||
if type(uid) in (str, unicode):
|
||||
try:
|
||||
uid = uuid.UUID(uid)
|
||||
@ -42,40 +42,47 @@ class UserManager:
|
||||
else:
|
||||
return UserManager.INVALID_ID, None
|
||||
|
||||
user = User.query.get(uid)
|
||||
user = store.get(User, uid)
|
||||
if user is None:
|
||||
return UserManager.NO_SUCH_USER, None
|
||||
|
||||
return UserManager.SUCCESS, user
|
||||
|
||||
@staticmethod
|
||||
def add(name, password, mail, admin):
|
||||
if User.query.filter(User.name == name).first():
|
||||
def add(store, name, password, mail, admin):
|
||||
if store.find(User, User.name == name).one():
|
||||
return UserManager.NAME_EXISTS
|
||||
|
||||
password = UserManager.__decode_password(password)
|
||||
crypt, salt = UserManager.__encrypt_password(password)
|
||||
user = User(name = name, mail = mail, password = crypt, salt = salt, admin = admin)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
user = User()
|
||||
user.name = name
|
||||
user.mail = mail
|
||||
user.password = crypt
|
||||
user.salt = salt
|
||||
user.admin = admin
|
||||
|
||||
store.add(user)
|
||||
store.commit()
|
||||
|
||||
return UserManager.SUCCESS
|
||||
|
||||
@staticmethod
|
||||
def delete(uid):
|
||||
status, user = UserManager.get(uid)
|
||||
def delete(store, uid):
|
||||
status, user = UserManager.get(store, uid)
|
||||
if status != UserManager.SUCCESS:
|
||||
return status
|
||||
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
store.remove(user)
|
||||
store.commit()
|
||||
|
||||
return UserManager.SUCCESS
|
||||
|
||||
@staticmethod
|
||||
def try_auth(name, password):
|
||||
def try_auth(store, name, password):
|
||||
password = UserManager.__decode_password(password)
|
||||
user = User.query.filter(User.name == name).first()
|
||||
user = store.find(User, User.name == name).one()
|
||||
if not user:
|
||||
return UserManager.NO_SUCH_USER, None
|
||||
elif UserManager.__encrypt_password(password, user.salt)[0] != user.password:
|
||||
@ -84,8 +91,8 @@ class UserManager:
|
||||
return UserManager.SUCCESS, user
|
||||
|
||||
@staticmethod
|
||||
def change_password(uid, old_pass, new_pass):
|
||||
status, user = UserManager.get(uid)
|
||||
def change_password(store, uid, old_pass, new_pass):
|
||||
status, user = UserManager.get(store, uid)
|
||||
if status != UserManager.SUCCESS:
|
||||
return status
|
||||
|
||||
@ -96,18 +103,18 @@ class UserManager:
|
||||
return UserManager.WRONG_PASS
|
||||
|
||||
user.password = UserManager.__encrypt_password(new_pass, user.salt)[0]
|
||||
session.commit()
|
||||
store.commit()
|
||||
return UserManager.SUCCESS
|
||||
|
||||
@staticmethod
|
||||
def change_password2(name, new_pass):
|
||||
user = User.query.filter(User.name == name).first()
|
||||
def change_password2(store, name, new_pass):
|
||||
user = store.find(User, User.name == name).one()
|
||||
if not user:
|
||||
return UserManager.NO_SUCH_USER
|
||||
|
||||
new_pass = UserManager.__decode_password(new_pass)
|
||||
user.password = UserManager.__encrypt_password(new_pass, user.salt)[0]
|
||||
session.commit()
|
||||
store.commit()
|
||||
return UserManager.SUCCESS
|
||||
|
||||
@staticmethod
|
||||
|
@ -21,17 +21,15 @@
|
||||
import os, os.path
|
||||
import time, mimetypes
|
||||
import mutagen
|
||||
from supysonic import config, db
|
||||
from supysonic import config
|
||||
from supysonic.db import Folder, Artist, Album, Track
|
||||
|
||||
def get_mime(ext):
|
||||
return mimetypes.guess_type('dummy.' + ext, False)[0] or config.get('mimetypes', ext) or 'application/octet-stream'
|
||||
|
||||
class Scanner:
|
||||
def __init__(self, session):
|
||||
self.__session = session
|
||||
self.__tracks = db.Track.query.all()
|
||||
self.__artists = db.Artist.query.all()
|
||||
self.__folders = db.Folder.query.all()
|
||||
def __init__(self, store):
|
||||
self.__store = store
|
||||
|
||||
self.__added_artists = 0
|
||||
self.__added_albums = 0
|
||||
@ -56,20 +54,25 @@ class Scanner:
|
||||
|
||||
folder.last_scan = int(time.time())
|
||||
|
||||
def prune(self, folder):
|
||||
for track in [ t for t in self.__tracks if t.root_folder.id == folder.id and not self.__is_valid_path(t.path) ]:
|
||||
self.__remove_track(track)
|
||||
self.__store.flush()
|
||||
|
||||
for album in [ album for artist in self.__artists for album in artist.albums if len(album.tracks) == 0 ]:
|
||||
album.artist.albums.remove(album)
|
||||
self.__session.delete(album)
|
||||
def prune(self, folder):
|
||||
for track in [ t for t in self.__store.find(Track, Track.root_folder_id == folder.id) if not self.__is_valid_path(t.path) ]:
|
||||
self.__store.remove(track)
|
||||
self.__deleted_tracks += 1
|
||||
|
||||
# TODO execute the conditional part on SQL
|
||||
for album in [ a for a in self.__store.find(Album) if a.tracks.count() == 0 ]:
|
||||
self.__store.remove(album)
|
||||
self.__deleted_albums += 1
|
||||
|
||||
for artist in [ a for a in self.__artists if len(a.albums) == 0 ]:
|
||||
self.__session.delete(artist)
|
||||
# TODO execute the conditional part on SQL
|
||||
for artist in [ a for a in self.__store.find(Artist) if a.albums.count() == 0 ]:
|
||||
self.__store.remove(artist)
|
||||
self.__deleted_artists += 1
|
||||
|
||||
self.__cleanup_folder(folder)
|
||||
self.__store.flush()
|
||||
|
||||
def check_cover_art(self, folder):
|
||||
folder.has_cover_art = os.path.isfile(os.path.join(folder.path, 'cover.jpg'))
|
||||
@ -84,24 +87,25 @@ class Scanner:
|
||||
return os.path.splitext(path)[1][1:].lower() in self.__extensions
|
||||
|
||||
def scan_file(self, path):
|
||||
tr = filter(lambda t: t.path == path, self.__tracks)
|
||||
tr = self.__store.find(Track, Track.path == path).one()
|
||||
add = False
|
||||
if tr:
|
||||
tr = tr[0]
|
||||
if not os.path.getmtime(path) > tr.last_modification:
|
||||
if not int(os.path.getmtime(path)) > tr.last_modification:
|
||||
return
|
||||
|
||||
tag = self.__try_load_tag(path)
|
||||
if not tag:
|
||||
self.__remove_track(tr)
|
||||
self.__store.remove(tr)
|
||||
self.__deleted_tracks += 1
|
||||
return
|
||||
else:
|
||||
tag = self.__try_load_tag(path)
|
||||
if not tag:
|
||||
return
|
||||
|
||||
tr = db.Track(path = path, root_folder = self.__find_root_folder(path), folder = self.__find_folder(path))
|
||||
self.__tracks.append(tr)
|
||||
self.__added_tracks += 1
|
||||
tr = Track()
|
||||
tr.path = path
|
||||
add = True
|
||||
|
||||
tr.disc = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x[0].split('/')[0]))
|
||||
tr.number = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x[0].split('/')[0]))
|
||||
@ -109,61 +113,88 @@ class Scanner:
|
||||
tr.year = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0]))
|
||||
tr.genre = self.__try_read_tag(tag, 'genre')
|
||||
tr.duration = int(tag.info.length)
|
||||
if not add:
|
||||
tr.album = self.__find_album(self.__try_read_tag(tag, 'artist', ''), self.__try_read_tag(tag, 'album', ''))
|
||||
tr.bitrate = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) / 1000
|
||||
tr.content_type = get_mime(os.path.splitext(path)[1][1:])
|
||||
tr.last_modification = os.path.getmtime(path)
|
||||
|
||||
if add:
|
||||
tralbum = self.__find_album(self.__try_read_tag(tag, 'artist', ''), self.__try_read_tag(tag, 'album', ''))
|
||||
trroot = self.__find_root_folder(path)
|
||||
trfolder = self.__find_folder(path)
|
||||
|
||||
# Set the references at the very last as searching for them will cause the added track to be flushed, even if
|
||||
# it is incomplete, causing not null constraints errors.
|
||||
tr.album = tralbum
|
||||
tr.folder = trfolder
|
||||
tr.root_folder = trroot
|
||||
|
||||
self.__store.add(tr)
|
||||
self.__added_tracks += 1
|
||||
|
||||
def __find_album(self, artist, album):
|
||||
ar = self.__find_artist(artist)
|
||||
al = filter(lambda a: a.name == album, ar.albums)
|
||||
al = ar.albums.find(name = album).one()
|
||||
if al:
|
||||
return al[0]
|
||||
return al
|
||||
|
||||
al = db.Album(name = album, artist = ar)
|
||||
al = Album()
|
||||
al.name = album
|
||||
al.artist = ar
|
||||
|
||||
self.__store.add(al)
|
||||
self.__added_albums += 1
|
||||
|
||||
return al
|
||||
|
||||
def __find_artist(self, artist):
|
||||
ar = filter(lambda a: a.name.lower() == artist.lower(), self.__artists)
|
||||
ar = self.__store.find(Artist, Artist.name == artist).one()
|
||||
if ar:
|
||||
return ar[0]
|
||||
return ar
|
||||
|
||||
ar = db.Artist(name = artist)
|
||||
self.__artists.append(ar)
|
||||
self.__session.add(ar)
|
||||
ar = Artist()
|
||||
ar.name = artist
|
||||
|
||||
self.__store.add(ar)
|
||||
self.__added_artists += 1
|
||||
|
||||
return ar
|
||||
|
||||
def __find_root_folder(self, path):
|
||||
path = os.path.dirname(path)
|
||||
folders = filter(lambda f: path.startswith(f.path) and f.root, self.__folders)
|
||||
if len(folders) > 1:
|
||||
folders = self.__store.find(Folder, path.startswith(Folder.path), Folder.root == True)
|
||||
if folders.count() > 1:
|
||||
raise Exception("Found multiple root folders for '{}'.".format(path))
|
||||
elif len(folders) == 0:
|
||||
elif folders.count() == 0:
|
||||
raise Exception("Couldn't find the root folder for '{}'.\nDon't scan files that aren't located in a defined music folder")
|
||||
return folders[0]
|
||||
return folders.one()
|
||||
|
||||
def __find_folder(self, path):
|
||||
path = os.path.dirname(path)
|
||||
folders = filter(lambda f: f.path == path, self.__folders)
|
||||
if len(folders) > 1:
|
||||
folders = self.__store.find(Folder, Folder.path == path)
|
||||
if folders.count() > 1:
|
||||
raise Exception("Found multiple folders for '{}'.".format(path))
|
||||
elif len(folders) == 1:
|
||||
return folders[0]
|
||||
elif folders.count() == 1:
|
||||
return folders.one()
|
||||
|
||||
folders = sorted(filter(lambda f: path.startswith(f.path), self.__folders), key = lambda f: len(f.path), reverse = True)
|
||||
folder = folders[0]
|
||||
folder = self.__store.find(Folder, path.startswith(Folder.path)).order_by(Folder.path).last()
|
||||
|
||||
full_path = folder.path
|
||||
path = path[len(folder.path) + 1:]
|
||||
|
||||
for name in path.split(os.sep):
|
||||
full_path = os.path.join(full_path, name)
|
||||
folder = db.Folder(root = False, name = name, path = full_path, parent = folder)
|
||||
self.__folders.append(folder)
|
||||
|
||||
fold = Folder()
|
||||
fold.root = False
|
||||
fold.name = name
|
||||
fold.path = full_path
|
||||
fold.parent = folder
|
||||
|
||||
self.__store.add(fold)
|
||||
|
||||
folder = fold
|
||||
|
||||
return folder
|
||||
|
||||
@ -184,22 +215,11 @@ class Scanner:
|
||||
except:
|
||||
return default
|
||||
|
||||
def __remove_track(self, track):
|
||||
track.album.tracks.remove(track)
|
||||
track.folder.tracks.remove(track)
|
||||
# As we don't have a track -> playlists relationship, SQLAlchemy doesn't know it has to remove tracks
|
||||
# from playlists as well, so let's help it
|
||||
for playlist in db.Playlist.query.filter(db.Playlist.tracks.contains(track)):
|
||||
playlist.tracks.remove(track)
|
||||
self.__session.delete(track)
|
||||
self.__deleted_tracks += 1
|
||||
|
||||
def __cleanup_folder(self, folder):
|
||||
for f in folder.children:
|
||||
self.__cleanup_folder(f)
|
||||
if len(folder.children) == 0 and len(folder.tracks) == 0 and not folder.root:
|
||||
folder.parent = None
|
||||
self.__session.delete(folder)
|
||||
if folder.children.count() == 0 and folder.tracks.count() == 0 and not folder.root:
|
||||
self.__store.remove(folder)
|
||||
|
||||
def stats(self):
|
||||
return (self.__added_artists, self.__added_albums, self.__added_tracks), (self.__deleted_artists, self.__deleted_albums, self.__deleted_tracks)
|
||||
|
@ -19,15 +19,28 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os.path
|
||||
from flask import Flask
|
||||
from flask import Flask, g
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from supysonic import config
|
||||
from supysonic.db import get_store
|
||||
|
||||
def teardown(exception):
|
||||
db.session.remove()
|
||||
def get_db_store():
|
||||
store = getattr(g, 'store', None)
|
||||
if store:
|
||||
return store
|
||||
g.store = get_store(config.get('base', 'database_uri'))
|
||||
return g.store
|
||||
|
||||
store = LocalProxy(get_db_store)
|
||||
|
||||
def teardown_db(exception):
|
||||
store = getattr(g, 'store', None)
|
||||
if store:
|
||||
store.close()
|
||||
|
||||
def create_application():
|
||||
global app, db, UserManager
|
||||
global app
|
||||
|
||||
if not config.check():
|
||||
return None
|
||||
@ -35,12 +48,11 @@ def create_application():
|
||||
if not os.path.exists(config.get('webapp', 'cache_dir')):
|
||||
os.makedirs(config.get('webapp', 'cache_dir'))
|
||||
|
||||
from supysonic import db
|
||||
db.init_db()
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = '?9huDM\\H'
|
||||
|
||||
app.teardown_appcontext(teardown_db)
|
||||
|
||||
if config.get('webapp', 'log_file'):
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
@ -56,8 +68,6 @@ def create_application():
|
||||
handler.setLevel(mapping.get(config.get('webapp', 'log_level').upper(), logging.NOTSET))
|
||||
app.logger.addHandler(handler)
|
||||
|
||||
app.teardown_request(teardown)
|
||||
|
||||
from supysonic import frontend
|
||||
from supysonic import api
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user