1
0
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:
spl0k 2014-07-27 20:24:35 +02:00
commit a18f670ff0
24 changed files with 980 additions and 568 deletions

View File

@ -23,8 +23,8 @@ or as a WSGI application (on Apache for instance). But first:
### Prerequisites ### Prerequisites
* Python 2.7 * Python 2.7
* [Flask](http://flask.pocoo.org/) >= 0.7 (`pip install flask`) * [Flask](http://flask.pocoo.org/) >= 0.9 (`pip install flask`)
* [SQLAlchemy](http://www.sqlalchemy.org/) (`apt-get install python-sqlalchemy`) * [Storm](https://storm.canonical.com/) (`apt-get install python-storm`)
* Python Imaging Library (`apt-get install python-imaging`) * Python Imaging Library (`apt-get install python-imaging`)
* simplejson (`apt-get install python-simplejson`) * simplejson (`apt-get install python-simplejson`)
* [requests](http://docs.python-requests.org/) >= 1.0.0 (`pip install requests`) * [requests](http://docs.python-requests.org/) >= 1.0.0 (`pip install requests`)
@ -38,8 +38,9 @@ or `KEY: VALUE` syntax.
Available settings are: Available settings are:
* Section **base**: * 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. 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 * **scanner_extensions**: space-separated list of file extensions the scanner is restricted to. If omitted, files will be scanned
regardless of their extension regardless of their extension
* Section **webapp** * 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 * 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). 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 Running the application
----------------------- -----------------------
@ -65,8 +71,8 @@ To start the server, just run the `debug_server.py` script.
python debug_server.py 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, By default, it will listen on the loopback interface (127.0.0.1) on port 5000, but you can specify another address on
for instance on all the IPv6 interfaces: the command line, for instance on all the IPv6 interfaces:
python debug_server.py :: python debug_server.py ::

View File

@ -22,6 +22,11 @@
import sys, cmd, argparse, getpass, time import sys, cmd, argparse, getpass, time
from supysonic import config 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): class CLIParser(argparse.ArgumentParser):
def error(self, message): def error(self, message):
self.print_usage(sys.stderr) self.print_usage(sys.stderr)
@ -53,7 +58,7 @@ class CLI(cmd.Cmd):
return method return method
def __init__(self): def __init__(self, store):
cmd.Cmd.__init__(self) cmd.Cmd.__init__(self)
# Generate do_* and help_* methods # Generate do_* and help_* methods
@ -69,6 +74,8 @@ class CLI(cmd.Cmd):
for action, subparser in getattr(self.__class__, command + '_subparsers').choices.iteritems(): for action, subparser in getattr(self.__class__, command + '_subparsers').choices.iteritems():
setattr(self, 'help_{} {}'.format(command, action), subparser.print_help) setattr(self, 'help_{} {}'.format(command, action), subparser.print_help)
self.__store = store
def do_EOF(self, line): def do_EOF(self, line):
return True return True
@ -105,17 +112,17 @@ class CLI(cmd.Cmd):
def folder_list(self): def folder_list(self):
print 'Name\t\tPath\n----\t\t----' 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): def folder_add(self, name, path):
ret = FolderManager.add(name, path) ret = FolderManager.add(self.__store, name, path)
if ret != FolderManager.SUCCESS: if ret != FolderManager.SUCCESS:
print FolderManager.error_str(ret) print FolderManager.error_str(ret)
else: else:
print "Folder '{}' added".format(name) print "Folder '{}' added".format(name)
def folder_delete(self, name): def folder_delete(self, name):
ret = FolderManager.delete_by_name(name) ret = FolderManager.delete_by_name(self.__store, name)
if ret != FolderManager.SUCCESS: if ret != FolderManager.SUCCESS:
print FolderManager.error_str(ret) print FolderManager.error_str(ret)
else: else:
@ -134,19 +141,19 @@ class CLI(cmd.Cmd):
print "Scanning '{0}': {1}% ({2}/{3})".format(self.__name, (scanned * 100) / total, scanned, total) print "Scanning '{0}': {1}% ({2}/{3})".format(self.__name, (scanned * 100) / total, scanned, total)
self.__last_display = time.time() self.__last_display = time.time()
s = Scanner(db.session) s = Scanner(self.__store)
if folders: 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)): if any(map(lambda f: isinstance(f, basestring), folders)):
print "No such folder(s): " + ' '.join(f for f in folders if isinstance(f, basestring)) 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): for folder in filter(lambda f: isinstance(f, Folder), folders):
FolderManager.scan(folder.id, s, TimedProgressDisplay(folder.name)) FolderManager.scan(self.__store, folder.id, s, TimedProgressDisplay(folder.name))
else: else:
for folder in db.Folder.query.filter(db.Folder.root == True): for folder in self.__store.find(Folder, Folder.root == True):
FolderManager.scan(folder.id, s, TimedProgressDisplay(folder.name)) FolderManager.scan(self.__store, folder.id, s, TimedProgressDisplay(folder.name))
added, deleted = s.stats() added, deleted = s.stats()
db.session.commit() self.__store.commit()
print "Scanning done" print "Scanning done"
print 'Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2]) 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): def user_list(self):
print 'Name\t\tAdmin\tEmail\n----\t\t-----\t-----' 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): def user_add(self, name, admin, password, email):
if not password: if not password:
@ -180,26 +187,26 @@ class CLI(cmd.Cmd):
if password != confirm: if password != confirm:
print >>sys.stderr, "Passwords don't match" print >>sys.stderr, "Passwords don't match"
return return
status = UserManager.add(name, password, email, admin) status = UserManager.add(self.__store, name, password, email, admin)
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
print >>sys.stderr, UserManager.error_str(status) print >>sys.stderr, UserManager.error_str(status)
def user_delete(self, name): 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: if not user:
print >>sys.stderr, 'No such user' print >>sys.stderr, 'No such user'
else: else:
db.session.delete(user) self.__store.remove(user)
db.session.commit() self.__store.commit()
print "User '{}' deleted".format(name) print "User '{}' deleted".format(name)
def user_setadmin(self, name, off): 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: if not user:
print >>sys.stderr, 'No such user' print >>sys.stderr, 'No such user'
else: else:
user.admin = not off user.admin = not off
db.session.commit() self.__store.commit()
print "{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name) print "{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name)
def user_changepass(self, name, password): def user_changepass(self, name, password):
@ -209,7 +216,7 @@ class CLI(cmd.Cmd):
if password != confirm: if password != confirm:
print >>sys.stderr, "Passwords don't match" print >>sys.stderr, "Passwords don't match"
return return
status = UserManager.change_password2(name, password) status = UserManager.change_password2(self.__store, name, password)
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
print >>sys.stderr, UserManager.error_str(status) print >>sys.stderr, UserManager.error_str(status)
else: else:
@ -219,15 +226,9 @@ if __name__ == "__main__":
if not config.check(): if not config.check():
sys.exit(1) sys.exit(1)
from supysonic import db cli = CLI(get_store(config.get('base', 'database_uri')))
db.init_db()
from supysonic.managers.folder import FolderManager
from supysonic.managers.user import UserManager
from supysonic.scanner import Scanner
if len(sys.argv) > 1: if len(sys.argv) > 1:
CLI().onecmd(' '.join(sys.argv[1:])) cli.onecmd(' '.join(sys.argv[1:]))
else: else:
CLI().cmdloop() cli.cmdloop()

127
schema/mysql.sql Normal file
View 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
View 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
View 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)
);

View File

@ -23,7 +23,7 @@ from xml.etree import ElementTree
import simplejson import simplejson
import uuid import uuid
from supysonic.web import app from supysonic.web import app, store
from supysonic.managers.user import UserManager from supysonic.managers.user import UserManager
@app.before_request @app.before_request
@ -58,7 +58,7 @@ def authorize():
error = request.error_formatter(40, 'Unauthorized'), 401 error = request.error_formatter(40, 'Unauthorized'), 401
if request.authorization: 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: if status == UserManager.SUCCESS:
request.username = request.authorization.username request.username = request.authorization.username
request.user = user request.user = user
@ -68,7 +68,7 @@ def authorize():
if not username or not password: if not username or not password:
return error return error
status, user = UserManager.try_auth(username, password) status, user = UserManager.try_auth(store, username, password)
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
return error return error
@ -184,7 +184,7 @@ def get_entity(req, ent, param = 'id'):
except: except:
return False, req.error_formatter(0, 'Invalid %s id' % ent.__name__) return False, req.error_formatter(0, 'Invalid %s id' % ent.__name__)
entity = ent.query.get(eid) entity = store.get(ent, eid)
if not entity: if not entity:
return False, (req.error_formatter(70, '%s not found' % ent.__name__), 404) return False, (req.error_formatter(70, '%s not found' % ent.__name__), 404)

View File

@ -19,13 +19,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import request from flask import request
from sqlalchemy import desc, func from storm.expr import Desc, Avg, Min, Max
from sqlalchemy.orm import aliased from storm.info import ClassAlias
from datetime import timedelta
import random import random
import uuid import uuid
from supysonic.web import app from supysonic.web import app, store
from supysonic.db import * 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' ]) @app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ])
def rand_songs(): def rand_songs():
@ -40,15 +42,15 @@ def rand_songs():
except: except:
return request.error_formatter(0, 'Invalid parameter format') return request.error_formatter(0, 'Invalid parameter format')
query = Track.query query = store.find(Track)
if fromYear: if fromYear:
query = query.filter(Track.year >= fromYear) query = query.find(Track.year >= fromYear)
if toYear: if toYear:
query = query.filter(Track.year <= toYear) query = query.find(Track.year <= toYear)
if genre: if genre:
query = query.filter(Track.genre == genre) query = query.find(Track.genre == genre)
if fid: if fid:
query = query.filter(Track.root_folder_id == fid) query = query.find(Track.root_folder_id == fid)
count = query.count() count = query.count()
if not count: if not count:
@ -57,7 +59,7 @@ def rand_songs():
tracks = [] tracks = []
for _ in xrange(size): for _ in xrange(size):
x = random.choice(xrange(count)) x = random.choice(xrange(count))
tracks.append(query.offset(x).limit(1).one()) tracks.append(query[x])
return request.formatter({ return request.formatter({
'randomSongs': { 'randomSongs': {
@ -74,7 +76,7 @@ def album_list():
except: except:
return request.error_formatter(0, 'Invalid parameter format') 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': if ltype == 'random':
albums = [] albums = []
count = query.count() count = query.count()
@ -84,7 +86,7 @@ def album_list():
for _ in xrange(size): for _ in xrange(size):
x = random.choice(xrange(count)) x = random.choice(xrange(count))
albums.append(query.offset(x).limit(1).one()) albums.append(query[x])
return request.formatter({ return request.formatter({
'albumList': { 'albumList': {
@ -92,26 +94,26 @@ def album_list():
} }
}) })
elif ltype == 'newest': elif ltype == 'newest':
query = query.order_by(desc(Folder.created)) query = query.order_by(Desc(Folder.created)).config(distinct = True)
elif ltype == 'highest': 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': 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': 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': 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': elif ltype == 'alphabeticalByName':
query = query.order_by(Folder.name) query = query.order_by(Folder.name).config(distinct = True)
elif ltype == 'alphabeticalByArtist': elif ltype == 'alphabeticalByArtist':
parent = aliased(Folder) parent = ClassAlias(Folder)
query = query.join(parent, Folder.parent).order_by(parent.name).order_by(Folder.name) query = query.find(Folder.parent_id == parent.id).order_by(parent.name, Folder.name).config(distinct = True)
else: else:
return request.error_formatter(0, 'Unknown search type') return request.error_formatter(0, 'Unknown search type')
return request.formatter({ return request.formatter({
'albumList': { '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: except:
return request.error_formatter(0, 'Invalid parameter format') return request.error_formatter(0, 'Invalid parameter format')
query = Album.query query = store.find(Album)
if ltype == 'random': if ltype == 'random':
albums = [] albums = []
count = query.count() count = query.count()
@ -134,7 +136,7 @@ def album_list_id3():
for _ in xrange(size): for _ in xrange(size):
x = random.choice(xrange(count)) x = random.choice(xrange(count))
albums.append(query.offset(x).limit(1).one()) albums.append(query[x])
return request.formatter({ return request.formatter({
'albumList2': { 'albumList2': {
@ -142,51 +144,48 @@ def album_list_id3():
} }
}) })
elif ltype == 'newest': 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': 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': 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': 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': elif ltype == 'alphabeticalByName':
query = query.order_by(Album.name) query = query.order_by(Album.name)
elif ltype == 'alphabeticalByArtist': 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: else:
return request.error_formatter(0, 'Unknown search type') return request.error_formatter(0, 'Unknown search type')
return request.formatter({ return request.formatter({
'albumList2': { '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' ]) @app.route('/rest/getNowPlaying.view', methods = [ 'GET', 'POST' ])
def now_playing(): def now_playing():
if engine.name == 'sqlite': query = store.find(User, Track.id == User.last_play_id)
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)
return request.formatter({ return request.formatter({
'nowPlaying': { 'nowPlaying': {
'entry': [ dict( 'entry': [ dict(
u.last_play.as_subsonic_child(request.user).items() + u.last_play.as_subsonic_child(request.user).items() +
{ 'username': u.name, 'minutesAgo': (now() - u.last_play_date).seconds / 60, 'playerId': 0 }.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' ]) @app.route('/rest/getStarred.view', methods = [ 'GET', 'POST' ])
def get_starred(): def get_starred():
folders = store.find(StarredFolder, StarredFolder.user_id == User.id, User.name == request.username)
return request.formatter({ return request.formatter({
'starred': { '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()) ], '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 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 folders.find(Track.folder_id == StarredFolder.starred_id).config(distinct = True) ],
'song': [ st.starred.as_subsonic_child(request.user) for st in StarredTrack.query.join(User).filter(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) ]
} }
}) })
@ -194,9 +193,9 @@ def get_starred():
def get_starred_id3(): def get_starred_id3():
return request.formatter({ return request.formatter({
'starred2': { 'starred2': {
'artist': [ sa.starred.as_subsonic_artist(request.user) for sa in StarredArtist.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 StarredAlbum.query.join(User).filter(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 StarredTrack.query.join(User).filter(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) ]
} }
}) })

View File

@ -21,10 +21,12 @@
import time import time
import uuid import uuid
from flask import request from flask import request
from supysonic.web import app from supysonic.web import app, store
from . import get_entity from . import get_entity
from supysonic.lastfm import LastFm 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' ]) @app.route('/rest/star.view', methods = [ 'GET', 'POST' ])
def star(): def star():
@ -36,11 +38,14 @@ def star():
except: except:
return 2, request.error_formatter(0, 'Invalid %s id' % ent.__name__) 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__) return 2, request.error_formatter(0, '%s already starred' % ent.__name__)
e = ent.query.get(uid) e = store.get(ent, uid)
if e: 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: else:
return 1, request.error_formatter(70, 'Unknown %s id' % ent.__name__) return 1, request.error_formatter(70, 'Unknown %s id' % ent.__name__)
@ -65,7 +70,7 @@ def star():
if err: if err:
return ferror return ferror
session.commit() store.commit()
return request.formatter({}) return request.formatter({})
@app.route('/rest/unstar.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/unstar.view', methods = [ 'GET', 'POST' ])
@ -78,7 +83,7 @@ def unstar():
except: except:
return request.error_formatter(0, 'Invalid id') 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 return None
for eid in id: for eid in id:
@ -99,7 +104,7 @@ def unstar():
if err: if err:
return err return err
session.commit() store.commit()
return request.formatter({}) return request.formatter({})
@app.route('/rest/setRating.view', methods = [ 'GET', 'POST' ]) @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)') return request.error_formatter(0, 'rating must be between 0 and 5 (inclusive)')
if rating == 0: if rating == 0:
RatingTrack.query.filter(RatingTrack.user_id == request.user.id).filter(RatingTrack.rated_id == uid).delete() store.find(RatingTrack, RatingTrack.user_id == request.user.id, RatingTrack.rated_id == uid).remove()
RatingFolder.query.filter(RatingFolder.user_id == request.user.id).filter(RatingFolder.rated_id == uid).delete() store.find(RatingFolder, RatingFolder.user_id == request.user.id, RatingFolder.rated_id == uid).remove()
else: else:
rated = Track.query.get(uid) rated = store.get(Track, uid)
rating_ent = RatingTrack rating_ent = RatingTrack
if not rated: if not rated:
rated = Folder.query.get(uid) rated = store.get(Folder, uid)
rating_ent = RatingFolder rating_ent = RatingFolder
if not rated: if not rated:
return request.error_formatter(70, 'Unknown id') 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: if rating_info:
rating_info.rating = rating rating_info.rating = rating
else: 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({}) return request.formatter({})
@app.route('/rest/scrobble.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/scrobble.view', methods = [ 'GET', 'POST' ])

View File

@ -19,7 +19,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import request from flask import request
from supysonic.web import app from supysonic.web import app, store
from supysonic.db import Folder, Artist, Album, Track from supysonic.db import Folder, Artist, Album, Track
from . import get_entity from . import get_entity
import uuid, string import uuid, string
@ -31,7 +31,7 @@ def list_folders():
'musicFolder': [ { 'musicFolder': [ {
'id': str(f.id), 'id': str(f.id),
'name': f.name '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') return request.error_formatter(0, 'Invalid timestamp')
if musicFolderId is None: if musicFolderId is None:
folder = Folder.query.filter(Folder.root == True).all() folder = store.find(Folder, Folder.root == True)
else: else:
try: try:
mfid = uuid.UUID(musicFolderId) mfid = uuid.UUID(musicFolderId)
except: except:
return request.error_formatter(0, 'Invalid id') 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') 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: if (not ifModifiedSince is None) and last_modif < ifModifiedSince:
return request.formatter({ 'indexes': { 'lastModified': last_modif * 1000 } }) return request.formatter({ 'indexes': { 'lastModified': last_modif * 1000 } })
# The XSD lies, we don't return artists but a directory structure # The XSD lies, we don't return artists but a directory structure
if type(folder) is list: if type(folder) is not Folder:
artists = [] artists = []
childs = [] childs = []
for f in folder: for f in folder:
@ -121,7 +121,7 @@ def show_directory():
def list_artists(): def list_artists():
# According to the API page, there are no parameters? # According to the API page, there are no parameters?
indexes = {} indexes = {}
for artist in Artist.query.all(): for artist in store.find(Artist):
index = artist.name[0].upper() if artist.name else '?' index = artist.name[0].upper() if artist.name else '?'
if index in map(str, xrange(10)): if index in map(str, xrange(10)):
index = '#' index = '#'

View File

@ -19,8 +19,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import request from flask import request
from supysonic.web import app from supysonic.web import app, store
from supysonic.db import ChatMessage, session from supysonic.db import ChatMessage
@app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ])
def get_chat(): def get_chat():
@ -30,9 +30,9 @@ def get_chat():
except: except:
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
query = ChatMessage.query.order_by(ChatMessage.time) query = store.find(ChatMessage).order_by(ChatMessage.time)
if since: 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 ] }}) return request.formatter({ 'chatMessages': { 'chatMessage': [ msg.responsize() for msg in query ] }})
@ -42,7 +42,10 @@ def add_chat_message():
if not msg: if not msg:
return request.error_formatter(10, 'Missing message') return request.error_formatter(10, 'Missing message')
session.add(ChatMessage(user = request.user, message = msg)) chat = ChatMessage()
session.commit() chat.user_id = request.user.id
chat.message = msg
store.add(chat)
store.commit()
return request.formatter({}) return request.formatter({})

View File

@ -27,12 +27,10 @@ import codecs
from xml.etree import ElementTree from xml.etree import ElementTree
from supysonic import config, scanner from supysonic import config, scanner
from supysonic.web import app from supysonic.web import app, store
from supysonic.db import Track, Album, Artist, Folder, ClientPrefs, now, session from supysonic.db import Track, Album, Artist, Folder, User, ClientPrefs, now
from . import get_entity from . import get_entity
from sqlalchemy import func
def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate): def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate):
if not base_cmdline: if not base_cmdline:
return None return None
@ -57,10 +55,12 @@ def stream_media():
dst_mimetype = res.content_type dst_mimetype = res.content_type
if client: if client:
prefs = ClientPrefs.query.get((request.user.id, client)) prefs = store.get(ClientPrefs, (request.user.id, client))
if not prefs: if not prefs:
prefs = ClientPrefs(user_id = request.user.id, client_name = client) prefs = ClientPrefs()
session.add(prefs) prefs.user_id = request.user.id
prefs.client_name = client
store.add(prefs)
if prefs.format: if prefs.format:
dst_suffix = prefs.format dst_suffix = prefs.format
@ -117,7 +117,7 @@ def stream_media():
res.last_play = now() res.last_play = now()
request.user.last_play = res request.user.last_play = res
request.user.last_play_date = now() request.user.last_play_date = now()
session.commit() store.commit()
return response return response
@ -170,7 +170,7 @@ def lyrics():
if not title: if not title:
return request.error_formatter(10, 'Missing title parameter') 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: for track in query:
lyrics_path = os.path.splitext(track.path)[0] + '.txt' lyrics_path = os.path.splitext(track.path)[0] + '.txt'
if os.path.exists(lyrics_path): if os.path.exists(lyrics_path):

View File

@ -19,22 +19,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import request from flask import request
from sqlalchemy import or_, func from storm.expr import Or
import uuid import uuid
from supysonic.web import app from supysonic.web import app, store
from supysonic.db import Playlist, User, Track, session from supysonic.db import Playlist, User, Track
from . import get_entity from . import get_entity
@app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ])
def list_playlists(): 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') username = request.args.get('username')
if username: if username:
if not request.user.admin: if not request.user.admin:
return request.error_formatter(50, 'Restricted to admins') 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 ] } }) 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') return request.error_formatter(0, 'Invalid parameter')
if playlist_id: if playlist_id:
playlist = Playlist.query.get(playlist_id) playlist = store.get(Playlist, playlist_id)
if not playlist: if not playlist:
return request.error_formatter(70, 'Unknwon playlist') return request.error_formatter(70, 'Unknwon playlist')
if playlist.user_id != request.user.id and not request.user.admin: 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") return request.error_formatter(50, "You're not allowed to modify a playlist that isn't yours")
playlist.tracks = [] playlist.tracks.clear()
if name: if name:
playlist.name = name playlist.name = name
elif name: elif name:
playlist = Playlist(user = request.user, name = name) playlist = Playlist()
session.add(playlist) playlist.user_id = request.user.id
playlist.name = name
store.add(playlist)
else: else:
return request.error_formatter(10, 'Missing playlist id or name') return request.error_formatter(10, 'Missing playlist id or name')
for sid in songs: for sid in songs:
track = Track.query.get(sid) track = store.get(Track, sid)
if not track: if not track:
return request.error_formatter(70, 'Unknown song') return request.error_formatter(70, 'Unknown song')
playlist.tracks.append(track) playlist.tracks.add(track)
session.commit() store.commit()
return request.formatter({}) return request.formatter({})
@app.route('/rest/deletePlaylist.view', methods = [ 'GET', 'POST' ]) @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: 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") return request.error_formatter(50, "You're not allowed to delete a playlist that isn't yours")
session.delete(res) store.remove(res)
session.commit() store.commit()
return request.formatter({}) return request.formatter({})
@app.route('/rest/updatePlaylist.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/updatePlaylist.view', methods = [ 'GET', 'POST' ])
@ -125,21 +127,20 @@ def update_playlist():
if public: if public:
playlist.public = public in (True, 'True', 'true', 1, '1') playlist.public = public in (True, 'True', 'true', 1, '1')
tracks = list(playlist.tracks)
for sid in to_add: for sid in to_add:
track = Track.query.get(sid) track = store.get(Track, sid)
if not track: if not track:
return request.error_formatter(70, 'Unknown song') return request.error_formatter(70, 'Unknown song')
if track not in playlist.tracks: if track not in playlist.tracks:
playlist.tracks.append(track) playlist.tracks.add(track)
offset = 0
for idx in to_remove: for idx in to_remove:
idx = idx - offset if idx < 0 or idx >= len(tracks):
if idx < 0 or idx >= len(playlist.tracks):
return request.error_formatter(0, 'Index out of range') return request.error_formatter(0, 'Index out of range')
playlist.tracks.pop(idx) playlist.tracks.remove(tracks[idx])
offset += 1
session.commit() store.commit()
return request.formatter({}) return request.formatter({})

View File

@ -19,7 +19,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import request 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 from supysonic.db import Folder, Track, Artist, Album
@app.route('/rest/search.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/search.view', methods = [ 'GET', 'POST' ])
@ -33,19 +34,20 @@ def old_search():
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
if artist: 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: 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: elif title:
query = Track.query.filter(Track.title.contains(title)) query = store.find(Track, Track.title.contains_string(title))
elif anyf: elif anyf:
folders = Folder.query.filter(Folder.name.contains(anyf)) folders = store.find(Folder, Folder.name.contains_string(anyf))
tracks = Track.query.filter(Track.title.contains(anyf)) tracks = store.find(Track, Track.title.contains_string(anyf))
res = folders.slice(offset, offset + count).all() res = list(folders[offset : offset + count])
if offset + count > folders.count(): if offset + count > folders.count():
toff = max(0, offset - folders.count()) toff = max(0, offset - folders.count())
tend = offset + count - folders.count() tend = offset + count - folders.count()
res += tracks.slice(toff, tend).all() res += list(tracks[toff : tend])
return request.formatter({ 'searchResult': { return request.formatter({ 'searchResult': {
'totalHits': folders.count() + tracks.count(), 'totalHits': folders.count() + tracks.count(),
@ -58,7 +60,7 @@ def old_search():
return request.formatter({ 'searchResult': { return request.formatter({ 'searchResult': {
'totalHits': query.count(), 'totalHits': query.count(),
'offset': offset, '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' ]) @app.route('/rest/search2.view', methods = [ 'GET', 'POST' ])
@ -67,21 +69,22 @@ def new_search():
request.args.get, [ 'query', 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ]) request.args.get, [ 'query', 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ])
try: try:
artist_count = int(artist_count) if artist_count else 20 artist_count = int(artist_count) if artist_count else 20
artist_offset = int(artist_offset) if artist_offset else 0 artist_offset = int(artist_offset) if artist_offset else 0
album_count = int(album_count) if album_count else 20 album_count = int(album_count) if album_count else 20
album_offset = int(album_offset) if album_offset else 0 album_offset = int(album_offset) if album_offset else 0
song_count = int(song_count) if song_count else 20 song_count = int(song_count) if song_count else 20
song_offset = int(song_offset) if song_offset else 0 song_offset = int(song_offset) if song_offset else 0
except: except:
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
if not query: if not query:
return request.error_formatter(10, 'Missing query parameter') 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) parent = ClassAlias(Folder)
album_query = Folder.query.filter(Folder.tracks.any(), Folder.name.contains(query)).slice(album_offset, album_offset + album_count) 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)
song_query = Track.query.filter(Track.title.contains(query)).slice(song_offset, song_offset + song_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': { return request.formatter({ 'searchResult2': {
'artist': [ { 'id': str(a.id), 'name': a.name } for a in artist_query ], 'artist': [ { 'id': str(a.id), 'name': a.name } for a in artist_query ],
@ -95,21 +98,21 @@ def search_id3():
request.args.get, [ 'query', 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ]) request.args.get, [ 'query', 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ])
try: try:
artist_count = int(artist_count) if artist_count else 20 artist_count = int(artist_count) if artist_count else 20
artist_offset = int(artist_offset) if artist_offset else 0 artist_offset = int(artist_offset) if artist_offset else 0
album_count = int(album_count) if album_count else 20 album_count = int(album_count) if album_count else 20
album_offset = int(album_offset) if album_offset else 0 album_offset = int(album_offset) if album_offset else 0
song_count = int(song_count) if song_count else 20 song_count = int(song_count) if song_count else 20
song_offset = int(song_offset) if song_offset else 0 song_offset = int(song_offset) if song_offset else 0
except: except:
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
if not query: if not query:
return request.error_formatter(10, 'Missing query parameter') return request.error_formatter(10, 'Missing query parameter')
artist_query = Artist.query.filter(Artist.name.contains(query)).slice(artist_offset, artist_offset + artist_count) artist_query = store.find(Artist, Artist.name.contains_string(query))[artist_offset : artist_offset + artist_count]
album_query = Album.query.filter(Album.name.contains(query)).slice(album_offset, album_offset + album_count) album_query = store.find(Album, Album.name.contains_string(query))[album_offset : album_offset + album_count]
song_query = Track.query.filter(Track.title.contains(query)).slice(song_offset, song_offset + song_count) song_query = store.find(Track, Track.title.contains_string(query))[song_offset : song_offset + song_count]
return request.formatter({ 'searchResult2': { return request.formatter({ 'searchResult2': {
'artist': [ a.as_subsonic_artist(request.user) for a in artist_query ], 'artist': [ a.as_subsonic_artist(request.user) for a in artist_query ],

View File

@ -19,7 +19,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import request from flask import request
from supysonic.web import app from supysonic.web import app, store
from supysonic.db import User from supysonic.db import User
from supysonic.managers.user import UserManager from supysonic.managers.user import UserManager
@ -32,7 +32,7 @@ def user_info():
if username != request.username and not request.user.admin: if username != request.username and not request.user.admin:
return request.error_formatter(50, 'Admin restricted') 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: if user is None:
return request.error_formatter(0, 'Unknown user') return request.error_formatter(0, 'Unknown user')
@ -43,7 +43,7 @@ def users_info():
if not request.user.admin: if not request.user.admin:
return request.error_formatter(50, 'Admin restricted') 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' ]) @app.route('/rest/createUser.view', methods = [ 'GET', 'POST' ])
def user_add(): def user_add():
@ -55,7 +55,7 @@ def user_add():
return request.error_formatter(10, 'Missing parameter') return request.error_formatter(10, 'Missing parameter')
admin = True if admin in (True, 'True', 'true', 1, '1') else False 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: if status == UserManager.NAME_EXISTS:
return request.error_formatter(0, 'There is already a user with that username') 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') return request.error_formatter(50, 'Admin restricted')
username = request.args.get('username') username = request.args.get('username')
user = User.query.filter(User.name == username).first() user = store.find(User, User.name == username).one()
if not user: if not user:
return request.error_formatter(70, 'Unknown user') return request.error_formatter(70, 'Unknown user')
status = UserManager.delete(user.id) status = UserManager.delete(store, user.id)
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
return request.error_formatter(0, UserManager.error_str(status)) 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: if username != request.username and not request.user.admin:
return request.error_formatter(50, 'Admin restricted') return request.error_formatter(50, 'Admin restricted')
status = UserManager.change_password2(username, password) status = UserManager.change_password2(store, username, password)
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
return request.error_formatter(0, UserManager.error_str(status)) return request.error_formatter(0, UserManager.error_str(status))

View File

@ -18,125 +18,44 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from supysonic import config from storm.properties import *
from storm.references import Reference, ReferenceSet
from sqlalchemy import create_engine, Table, Column, ForeignKey, func from storm.database import create_database
from sqlalchemy import Integer, String, Boolean, DateTime from storm.store import Store
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref from storm.variables import Variable
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.types import TypeDecorator, BINARY
from sqlalchemy.dialects.postgresql import UUID as pgUUID
import uuid, datetime, time import uuid, datetime, time
import os.path 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(): def now():
return datetime.datetime.now().replace(microsecond = 0) return datetime.datetime.now().replace(microsecond = 0)
config.check() class UnicodeOrStrVariable(Variable):
engine = create_engine(config.get('base', 'database_uri'), convert_unicode = True) __slots__ = ()
session = scoped_session(sessionmaker(autocommit = False, autoflush = False, bind = engine))
Base = declarative_base() def parse_set(self, value, from_db):
Base.query = session.query_property() 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): Unicode.variable_class = UnicodeOrStrVariable
__tablename__ = 'user'
id = UUID.gen_id_column() class Folder(object):
name = Column(String(64), unique = True) __storm_table__ = 'folder'
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
last_play_id = Column(UUID, ForeignKey('track.id'), nullable = True) id = UUID(primary = True, default_factory = uuid.uuid4)
last_play = relationship('Track') root = Bool(default = False)
last_play_date = Column(DateTime, nullable = True) 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): parent_id = UUID() # nullable
return { parent = Reference(parent_id, id)
'username': self.name, children = ReferenceSet(id, parent_id)
'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 ]))
def as_subsonic_child(self, user): def as_subsonic_child(self, user):
info = { info = {
@ -152,47 +71,46 @@ class Folder(Base):
if self.has_cover_art: if self.has_cover_art:
info['coverArt'] = str(self.id) 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: if starred:
info['starred'] = starred.date.isoformat() 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: if rating:
info['userRating'] = rating.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: if avgRating:
info['averageRating'] = avgRating info['averageRating'] = avgRating
return info return info
class Artist(Base): class Artist(object):
__tablename__ = 'artist' __storm_table__ = 'artist'
id = UUID.gen_id_column() id = UUID(primary = True, default_factory = uuid.uuid4)
name = Column(String(255), unique = True) name = Unicode() # unique
albums = relationship('Album', backref = 'artist')
def as_subsonic_artist(self, user): def as_subsonic_artist(self, user):
info = { info = {
'id': str(self.id), 'id': str(self.id),
'name': self.name, 'name': self.name,
# coverArt # 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: if starred:
info['starred'] = starred.date.isoformat() info['starred'] = starred.date.isoformat()
return info return info
class Album(Base): class Album(object):
__tablename__ = 'album' __storm_table__ = 'album'
id = UUID.gen_id_column() id = UUID(primary = True, default_factory = uuid.uuid4)
name = Column(String(255)) name = Unicode()
artist_id = Column(UUID, ForeignKey('artist.id')) artist_id = UUID()
tracks = relationship('Track', backref = 'album') artist = Reference(artist_id, Artist.id)
def as_subsonic_album(self, user): def as_subsonic_album(self, user):
info = { info = {
@ -200,14 +118,16 @@ class Album(Base):
'name': self.name, 'name': self.name,
'artist': self.artist.name, 'artist': self.artist.name,
'artistId': str(self.artist_id), 'artistId': str(self.artist_id),
'songCount': len(self.tracks), 'songCount': self.tracks.count(),
'duration': sum(map(lambda t: t.duration, self.tracks)), 'duration': sum(self.tracks.values(Track.duration)),
'created': min(map(lambda t: t.created, self.tracks)).isoformat() '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: if starred:
info['starred'] = starred.date.isoformat() 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)) year = min(map(lambda t: t.year if t.year else 9999, self.tracks))
return '%i%s' % (year, self.name.lower()) return '%i%s' % (year, self.name.lower())
class Track(Base): Artist.albums = ReferenceSet(Artist.id, Album.artist_id)
__tablename__ = 'track'
id = UUID.gen_id_column() class Track(object):
disc = Column(Integer) __storm_table__ = 'track'
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)
path = Column(String(4096)) # should be unique, but mysql don't like such large columns id = UUID(primary = True, default_factory = uuid.uuid4)
content_type = Column(String(32)) disc = Int()
created = Column(DateTime, default = now) number = Int()
last_modification = Column(Integer) 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) path = Unicode() # unique
last_play = Column(DateTime, nullable = True) content_type = Unicode()
created = DateTime(default_factory = now)
last_modification = Int()
root_folder_id = Column(UUID, ForeignKey('folder.id')) play_count = Int(default = 0)
root_folder = relationship('Folder', primaryjoin = Folder.id == root_folder_id) last_play = DateTime() # nullable
folder_id = Column(UUID, ForeignKey('folder.id'))
folder = relationship('Folder', primaryjoin = Folder.id == folder_id, backref = 'tracks') 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): def as_subsonic_child(self, user):
info = { info = {
'id': str(self.id), 'id': str(self.id),
'parent': str(self.folder.id), 'parent': str(self.folder_id),
'isDir': False, 'isDir': False,
'title': self.title, 'title': self.title,
'album': self.album.name, 'album': self.album.name,
@ -261,8 +184,8 @@ class Track(Base):
'isVideo': False, 'isVideo': False,
'discNumber': self.disc, 'discNumber': self.disc,
'created': self.created.isoformat(), 'created': self.created.isoformat(),
'albumId': str(self.album.id), 'albumId': str(self.album_id),
'artistId': str(self.album.artist.id), 'artistId': str(self.album.artist_id),
'type': 'music' 'type': 'music'
} }
@ -273,14 +196,14 @@ class Track(Base):
if self.folder.has_cover_art: if self.folder.has_cover_art:
info['coverArt'] = str(self.folder_id) 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: if starred:
info['starred'] = starred.date.isoformat() 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: if rating:
info['userRating'] = rating.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: if avgRating:
info['averageRating'] = avgRating info['averageRating'] = avgRating
@ -301,75 +224,109 @@ class Track(Base):
def sort_key(self): def sort_key(self):
return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower() return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower()
class StarredFolder(Base): Folder.tracks = ReferenceSet(Folder.id, Track.folder_id)
__tablename__ = 'starred_folder' Album.tracks = ReferenceSet(Album.id, Track.album_id)
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True) class User(object):
starred_id = Column(UUID, ForeignKey('folder.id'), primary_key = True) __storm_table__ = 'user'
date = Column(DateTime, default = now)
user = relationship('User') id = UUID(primary = True, default_factory = uuid.uuid4)
starred = relationship('Folder') 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): last_play_id = UUID() # nullable
__tablename__ = 'starred_artist' last_play = Reference(last_play_id, Track.id)
last_play_date = DateTime() # nullable
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True) def as_subsonic_user(self):
starred_id = Column(UUID, ForeignKey('artist.id'), primary_key = True) return {
date = Column(DateTime, default = now) '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') class ClientPrefs(object):
starred = relationship('Artist') __storm_table__ = 'client_prefs'
__storm_primary__ = 'user_id', 'client_name'
class StarredAlbum(Base): user_id = UUID()
__tablename__ = 'starred_album' client_name = Unicode()
format = Unicode() # nullable
bitrate = Int() # nullable
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True) class BaseStarred(object):
starred_id = Column(UUID, ForeignKey('album.id'), primary_key = True) __storm_primary__ = 'user_id', 'starred_id'
date = Column(DateTime, default = now)
user = relationship('User') user_id = UUID()
starred = relationship('Album') starred_id = UUID()
date = DateTime(default_factory = now)
class StarredTrack(Base): user = Reference(user_id, User.id)
__tablename__ = 'starred_track'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True) class StarredFolder(BaseStarred):
starred_id = Column(UUID, ForeignKey('track.id'), primary_key = True) __storm_table__ = 'starred_folder'
date = Column(DateTime, default = now)
user = relationship('User') starred = Reference(BaseStarred.starred_id, Folder.id)
starred = relationship('Track')
class RatingFolder(Base): class StarredArtist(BaseStarred):
__tablename__ = 'rating_folder' __storm_table__ = 'starred_artist'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True) starred = Reference(BaseStarred.starred_id, Artist.id)
rated_id = Column(UUID, ForeignKey('folder.id'), primary_key = True)
rating = Column(Integer)
user = relationship('User') class StarredAlbum(BaseStarred):
rated = relationship('Folder') __storm_table__ = 'starred_album'
class RatingTrack(Base): starred = Reference(BaseStarred.starred_id, Album.id)
__tablename__ = 'rating_track'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True) class StarredTrack(BaseStarred):
rated_id = Column(UUID, ForeignKey('track.id'), primary_key = True) __storm_table__ = 'starred_track'
rating = Column(Integer)
user = relationship('User') starred = Reference(BaseStarred.starred_id, Track.id)
rated = relationship('Track')
class ChatMessage(Base): class BaseRating(object):
__tablename__ = 'chat_message' __storm_primary__ = 'user_id', 'rated_id'
id = UUID.gen_id_column() user_id = UUID()
user_id = Column(UUID, ForeignKey('user.id')) rated_id = UUID()
time = Column(Integer, default = lambda: int(time.time())) rating = Int()
message = Column(String(512))
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): def responsize(self):
return { return {
@ -378,23 +335,17 @@ class ChatMessage(Base):
'message': self.message 'message': self.message
} }
playlist_track_assoc = Table('playlist_track', Base.metadata, class Playlist(object):
Column('playlist_id', UUID, ForeignKey('playlist.id')), __storm_table__ = 'playlist'
Column('track_id', UUID, ForeignKey('track.id'))
)
class Playlist(Base): id = UUID(primary = True, default_factory = uuid.uuid4)
__tablename__ = 'playlist' user_id = UUID()
name = Unicode()
comment = Unicode() # nullable
public = Bool(default = False)
created = DateTime(default_factory = now)
id = UUID.gen_id_column() user = Reference(user_id, User.id)
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)
def as_subsonic_playlist(self, user): def as_subsonic_playlist(self, user):
info = { 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), 'name': self.name if self.user_id == user.id else '[%s] %s' % (self.user.name, self.name),
'owner': self.user.name, 'owner': self.user.name,
'public': self.public, 'public': self.public,
'songCount': len(self.tracks), 'songCount': self.tracks.count(),
'duration': sum(map(lambda t: t.duration, self.tracks)), 'duration': self.tracks.find().sum(Track.duration),
'created': self.created.isoformat() 'created': self.created.isoformat()
} }
if self.comment: if self.comment:
info['comment'] = self.comment info['comment'] = self.comment
return info return info
def init_db(): class PlaylistTrack(object):
Base.metadata.create_all(bind = engine) __storm_table__ = 'playlist_track'
__storm_primary__ = 'playlist_id', 'track_id'
def recreate_db(): playlist_id = UUID()
Base.metadata.drop_all(bind = engine) track_id = UUID()
Base.metadata.create_all(bind = engine)
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

View File

@ -18,7 +18,9 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # 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 from supysonic.managers.user import UserManager
app.add_template_filter(str) app.add_template_filter(str)
@ -32,7 +34,7 @@ def login_check():
should_login = False should_login = False
if not session.get('userid'): if not session.get('userid'):
should_login = True should_login = True
elif UserManager.get(session.get('userid'))[0] != UserManager.SUCCESS: elif UserManager.get(store, session.get('userid'))[0] != UserManager.SUCCESS:
session.clear() session.clear()
should_login = True should_login = True
@ -43,11 +45,11 @@ def login_check():
@app.route('/') @app.route('/')
def index(): def index():
stats = { stats = {
'artists': db.Artist.query.count(), 'artists': store.find(Artist).count(),
'albums': db.Album.query.count(), 'albums': store.find(Album).count(),
'tracks': db.Track.query.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 .user import *
from .folder import * from .folder import *

View File

@ -18,11 +18,12 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # 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 import uuid
from supysonic.web import app from supysonic.web import app, store
from supysonic.db import session, Folder from supysonic.db import Folder
from supysonic.scanner import Scanner from supysonic.scanner import Scanner
from supysonic.managers.user import UserManager from supysonic.managers.user import UserManager
from supysonic.managers.folder import FolderManager from supysonic.managers.folder import FolderManager
@ -32,12 +33,12 @@ def check_admin():
if not request.path.startswith('/folder'): if not request.path.startswith('/folder'):
return 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')) return redirect(url_for('index'))
@app.route('/folder') @app.route('/folder')
def folder_index(): 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' ]) @app.route('/folder/add', methods = [ 'GET', 'POST' ])
def add_folder(): def add_folder():
@ -55,7 +56,7 @@ def add_folder():
if error: if error:
return render_template('addfolder.html') return render_template('addfolder.html')
ret = FolderManager.add(name, path) ret = FolderManager.add(store, name, path)
if ret != FolderManager.SUCCESS: if ret != FolderManager.SUCCESS:
flash(FolderManager.error_str(ret)) flash(FolderManager.error_str(ret))
return render_template('addfolder.html') return render_template('addfolder.html')
@ -72,7 +73,7 @@ def del_folder(id):
flash('Invalid folder id') flash('Invalid folder id')
return redirect(url_for('folder_index')) return redirect(url_for('folder_index'))
ret = FolderManager.delete(idid) ret = FolderManager.delete(store, idid)
if ret != FolderManager.SUCCESS: if ret != FolderManager.SUCCESS:
flash(FolderManager.error_str(ret)) flash(FolderManager.error_str(ret))
else: else:
@ -83,18 +84,18 @@ def del_folder(id):
@app.route('/folder/scan') @app.route('/folder/scan')
@app.route('/folder/scan/<id>') @app.route('/folder/scan/<id>')
def scan_folder(id = None): def scan_folder(id = None):
s = Scanner(session) s = Scanner(store)
if id is None: if id is None:
for folder in Folder.query.filter(Folder.root == True): for folder in store.find(Folder, Folder.root == True):
FolderManager.scan(folder.id, s) FolderManager.scan(store, folder.id, s)
else: else:
status = FolderManager.scan(id, s) status = FolderManager.scan(store, id, s)
if status != FolderManager.SUCCESS: if status != FolderManager.SUCCESS:
flash(FolderManager.error_str(status)) flash(FolderManager.error_str(status))
return redirect(url_for('folder_index')) return redirect(url_for('folder_index'))
added, deleted = s.stats() added, deleted = s.stats()
session.commit() store.commit()
flash('Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2])) 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])) flash('Deleted: %i artists, %i albums, %i tracks' % (deleted[0], deleted[1], deleted[2]))

View File

@ -20,13 +20,13 @@
from flask import request, session, flash, render_template, redirect, url_for from flask import request, session, flash, render_template, redirect, url_for
import uuid import uuid
from supysonic.web import app from supysonic.web import app, store
from supysonic import db from supysonic.db import Playlist
@app.route('/playlist') @app.route('/playlist')
def playlist_index(): def playlist_index():
return render_template('playlists.html', mine = 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 = db.Playlist.query.filter(db.Playlist.user_id != uuid.UUID(session.get('userid')))) others = store.find(Playlist, Playlist.user_id != uuid.UUID(session.get('userid'))))
@app.route('/playlist/<uid>') @app.route('/playlist/<uid>')
def playlist_details(uid): def playlist_details(uid):
@ -36,7 +36,7 @@ def playlist_details(uid):
flash('Invalid playlist id') flash('Invalid playlist id')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))
playlist = db.Playlist.query.get(uid) playlist = store.get(Playlist, uid)
if not playlist: if not playlist:
flash('Unknown playlist') flash('Unknown playlist')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))
@ -51,7 +51,7 @@ def playlist_update(uid):
flash('Invalid playlist id') flash('Invalid playlist id')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))
playlist = db.Playlist.query.get(uid) playlist = store.get(Playlist, uid)
if not playlist: if not playlist:
flash('Unknown playlist') flash('Unknown playlist')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))
@ -63,7 +63,7 @@ def playlist_update(uid):
else: else:
playlist.name = request.form.get('name') playlist.name = request.form.get('name')
playlist.public = request.form.get('public') in (True, 'True', 1, '1', 'on', 'checked') playlist.public = request.form.get('public') in (True, 'True', 1, '1', 'on', 'checked')
db.session.commit() store.commit()
flash('Playlist updated.') flash('Playlist updated.')
return playlist_details(uid) return playlist_details(uid)
@ -76,14 +76,14 @@ def playlist_delete(uid):
flash('Invalid playlist id') flash('Invalid playlist id')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))
playlist = db.Playlist.query.get(uid) playlist = store.get(Playlist, uid)
if not playlist: if not playlist:
flash('Unknown playlist') flash('Unknown playlist')
elif str(playlist.user_id) != session.get('userid'): elif str(playlist.user_id) != session.get('userid'):
flash("You're not allowed to delete this playlist") flash("You're not allowed to delete this playlist")
else: else:
db.session.delete(playlist) store.remove(playlist)
db.session.commit() store.commit()
flash('Playlist deleted') flash('Playlist deleted')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))

View File

@ -20,9 +20,9 @@
from flask import request, session, flash, render_template, redirect, url_for, make_response 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.managers.user import UserManager
from supysonic.db import User, ClientPrefs, session as db_sess from supysonic.db import User, ClientPrefs
import uuid, csv import uuid, csv
from supysonic import config from supysonic import config
from supysonic.lastfm import LastFm from supysonic.lastfm import LastFm
@ -32,46 +32,46 @@ def check_admin():
if not request.path.startswith('/user'): if not request.path.startswith('/user'):
return 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')) return redirect(url_for('index'))
@app.route('/user') @app.route('/user')
def user_index(): 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') @app.route('/user/me')
def user_profile(): def user_profile():
prefs = ClientPrefs.query.filter(ClientPrefs.user_id == uuid.UUID(session.get('userid'))) prefs = store.find(ClientPrefs, 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) 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' ]) @app.route('/user/me', methods = [ 'POST' ])
def update_clients(): def update_clients():
clients_opts = {} 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()) } 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) app.logger.debug(clients_opts)
for client, opts in clients_opts.iteritems(): 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' ]: if 'delete' in opts and opts['delete'] in [ 'on', 'true', 'checked', 'selected', '1' ]:
db_sess.delete(prefs) store.remove(prefs)
continue continue
prefs.format = opts['format'] if 'format' in opts and opts['format'] else None 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 prefs.bitrate = int(opts['bitrate']) if 'bitrate' in opts and opts['bitrate'] else None
db_sess.commit() store.commit()
flash('Clients preferences updated.') flash('Clients preferences updated.')
return user_profile() return user_profile()
@app.route('/user/changemail', methods = [ 'GET', 'POST' ]) @app.route('/user/changemail', methods = [ 'GET', 'POST' ])
def change_mail(): def change_mail():
user = UserManager.get(session.get('userid'))[1] user = UserManager.get(store, session.get('userid'))[1]
if request.method == 'POST': if request.method == 'POST':
mail = request.form.get('mail') mail = request.form.get('mail')
# No validation, lol. # No validation, lol.
user.mail = mail user.mail = mail
db_sess.commit() store.commit()
return redirect(url_for('user_profile')) return redirect(url_for('user_profile'))
return render_template('change_mail.html', user = user) return render_template('change_mail.html', user = user)
@ -92,14 +92,14 @@ def change_password():
error = True error = True
if not error: 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: if status != UserManager.SUCCESS:
flash(UserManager.error_str(status)) flash(UserManager.error_str(status))
else: else:
flash('Password changed') flash('Password changed')
return redirect(url_for('user_profile')) 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' ]) @app.route('/user/add', methods = [ 'GET', 'POST' ])
def add_user(): def add_user():
@ -119,12 +119,12 @@ def add_user():
error = True error = True
if admin is None: 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: else:
admin = True admin = True
if not error: if not error:
status = UserManager.add(name, passwd, mail, admin) status = UserManager.add(store, name, passwd, mail, admin)
if status == UserManager.SUCCESS: if status == UserManager.SUCCESS:
flash("User '%s' successfully added" % name) flash("User '%s' successfully added" % name)
return redirect(url_for('user_index')) return redirect(url_for('user_index'))
@ -136,7 +136,7 @@ def add_user():
@app.route('/user/del/<uid>') @app.route('/user/del/<uid>')
def del_user(uid): def del_user(uid):
status = UserManager.delete(uid) status = UserManager.delete(store, uid)
if status == UserManager.SUCCESS: if status == UserManager.SUCCESS:
flash('Deleted user') flash('Deleted user')
else: else:
@ -147,7 +147,7 @@ def del_user(uid):
@app.route('/user/export') @app.route('/user/export')
def export_users(): 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) 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-disposition'] = 'attachment;filename=users.csv'
resp.headers['Content-type'] = 'text/csv' resp.headers['Content-type'] = 'text/csv'
return resp return resp
@ -168,12 +168,22 @@ def do_user_import():
admin = admin == 'True' admin = admin == 'True'
lfmsess = None if lfmsess == 'None' else lfmsess lfmsess = None if lfmsess == 'None' else lfmsess
lfmstatus = lfmstatus == 'True' 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: for u in users:
db_sess.add(u) store.add(u)
db_sess.commit() store.commit()
return redirect(url_for('user_index')) return redirect(url_for('user_index'))
@ -184,16 +194,18 @@ def lastfm_reg():
flash('Missing LastFM auth token') flash('Missing LastFM auth token')
return redirect(url_for('user_profile')) 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) status, error = lfm.link_account(token)
store.commit()
flash(error if not status else 'Successfully linked LastFM account') flash(error if not status else 'Successfully linked LastFM account')
return redirect(url_for('user_profile')) return redirect(url_for('user_profile'))
@app.route('/user/lastfm/unlink') @app.route('/user/lastfm/unlink')
def lastfm_unreg(): 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() lfm.unlink_account()
store.commit()
flash('Unliked LastFM account') flash('Unliked LastFM account')
return redirect(url_for('user_profile')) return redirect(url_for('user_profile'))
@ -217,7 +229,7 @@ def login():
error = True error = True
if not error: if not error:
status, user = UserManager.try_auth(name, password) status, user = UserManager.try_auth(store, name, password)
if status == UserManager.SUCCESS: if status == UserManager.SUCCESS:
session['userid'] = str(user.id) session['userid'] = str(user.id)
session['username'] = user.name session['username'] = user.name

View File

@ -20,7 +20,6 @@
import requests, hashlib import requests, hashlib
from supysonic import config from supysonic import config
from supysonic.db import session
class LastFm: class LastFm:
def __init__(self, user, logger): def __init__(self, user, logger):
@ -42,13 +41,11 @@ class LastFm:
else: else:
self.__user.lastfm_session = res['session']['key'] self.__user.lastfm_session = res['session']['key']
self.__user.lastfm_status = True self.__user.lastfm_status = True
session.commit()
return True, 'OK' return True, 'OK'
def unlink_account(self): def unlink_account(self):
self.__user.lastfm_session = None self.__user.lastfm_session = None
self.__user.lastfm_status = True self.__user.lastfm_status = True
session.commit()
def now_playing(self, track): def now_playing(self, track):
if not self.__enabled: if not self.__enabled:
@ -99,7 +96,6 @@ class LastFm:
if 'error' in json: if 'error' in json:
if json['error'] in (9, '9'): if json['error'] in (9, '9'):
self.__user.lastfm_status = False self.__user.lastfm_status = False
session.commit()
self.__logger.warn('LastFM error %i: %s' % (json['error'], json['message'])) self.__logger.warn('LastFM error %i: %s' % (json['error'], json['message']))
return json return json

View File

@ -19,7 +19,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os.path, uuid import os.path, uuid
from supysonic.db import Folder, Artist, session from supysonic.db import Folder, Artist, Album, Track
class FolderManager: class FolderManager:
SUCCESS = 0 SUCCESS = 0
@ -30,7 +30,7 @@ class FolderManager:
NO_SUCH_FOLDER = 5 NO_SUCH_FOLDER = 5
@staticmethod @staticmethod
def get(uid): def get(store, uid):
if isinstance(uid, basestring): if isinstance(uid, basestring):
try: try:
uid = uuid.UUID(uid) uid = uuid.UUID(uid)
@ -41,33 +41,36 @@ class FolderManager:
else: else:
return FolderManager.INVALID_ID, None return FolderManager.INVALID_ID, None
folder = Folder.query.get(uid) folder = store.get(Folder, uid)
if not folder: if not folder:
return FolderManager.NO_SUCH_FOLDER, None return FolderManager.NO_SUCH_FOLDER, None
return FolderManager.SUCCESS, folder return FolderManager.SUCCESS, folder
@staticmethod @staticmethod
def add(name, path): def add(store, name, path):
if Folder.query.filter(Folder.name == name and Folder.root == True).first(): if not store.find(Folder, Folder.name == name, Folder.root == True).is_empty():
return FolderManager.NAME_EXISTS return FolderManager.NAME_EXISTS
path = os.path.abspath(path) path = os.path.abspath(path)
if not os.path.isdir(path): if not os.path.isdir(path):
return FolderManager.INVALID_PATH return FolderManager.INVALID_PATH
folder = Folder.query.filter(Folder.path == path).first() if not store.find(Folder, Folder.path == path).is_empty():
if folder:
return FolderManager.PATH_EXISTS return FolderManager.PATH_EXISTS
folder = Folder(root = True, name = name, path = path) folder = Folder()
session.add(folder) folder.root = True
session.commit() folder.name = name
folder.path = path
store.add(folder)
store.commit()
return FolderManager.SUCCESS return FolderManager.SUCCESS
@staticmethod @staticmethod
def delete(uid): def delete(store, uid):
status, folder = FolderManager.get(uid) status, folder = FolderManager.get(store, uid)
if status != FolderManager.SUCCESS: if status != FolderManager.SUCCESS:
return status return status
@ -75,37 +78,37 @@ class FolderManager:
return FolderManager.NO_SUCH_FOLDER return FolderManager.NO_SUCH_FOLDER
# delete associated tracks and prune empty albums/artists # delete associated tracks and prune empty albums/artists
for artist in Artist.query.all(): potentially_removed_albums = set()
for album in artist.albums[:]: for track in store.find(Track, Track.root_folder_id == folder.id):
for track in filter(lambda t: t.root_folder.id == folder.id, album.tracks): potentially_removed_albums.add(track.album)
album.tracks.remove(track) store.remove(track)
session.delete(track) potentially_removed_artists = set()
if len(album.tracks) == 0: for album in filter(lambda album: album.tracks.count() == 0, potentially_removed_albums):
artist.albums.remove(album) potentially_removed_artists.add(album.artist)
session.delete(album) store.remove(album)
if len(artist.albums) == 0: for artist in filter(lambda artist: artist.albums.count() == 0, potentially_removed_artists):
session.delete(artist) store.remove(artist)
def cleanup_folder(folder): def cleanup_folder(folder):
for f in folder.children: for f in folder.children:
cleanup_folder(f) cleanup_folder(f)
session.delete(folder) store.remove(folder)
cleanup_folder(folder) cleanup_folder(folder)
session.commit() store.commit()
return FolderManager.SUCCESS return FolderManager.SUCCESS
@staticmethod @staticmethod
def delete_by_name(name): def delete_by_name(store, name):
folder = Folder.query.filter(Folder.name == name and Folder.root == True).first() folder = store.find(Folder, Folder.name == name, Folder.root == True).one()
if not folder: if not folder:
return FolderManager.NO_SUCH_FOLDER return FolderManager.NO_SUCH_FOLDER
return FolderManager.delete(folder.id) return FolderManager.delete(store, folder.id)
@staticmethod @staticmethod
def scan(uid, scanner, progress_callback = None): def scan(store, uid, scanner, progress_callback = None):
status, folder = FolderManager.get(uid) status, folder = FolderManager.get(store, uid)
if status != FolderManager.SUCCESS: if status != FolderManager.SUCCESS:
return status return status

View File

@ -21,7 +21,7 @@
import string, random, hashlib import string, random, hashlib
import uuid import uuid
from supysonic.db import User, session from supysonic.db import User
class UserManager: class UserManager:
SUCCESS = 0 SUCCESS = 0
@ -31,7 +31,7 @@ class UserManager:
WRONG_PASS = 4 WRONG_PASS = 4
@staticmethod @staticmethod
def get(uid): def get(store, uid):
if type(uid) in (str, unicode): if type(uid) in (str, unicode):
try: try:
uid = uuid.UUID(uid) uid = uuid.UUID(uid)
@ -42,40 +42,47 @@ class UserManager:
else: else:
return UserManager.INVALID_ID, None return UserManager.INVALID_ID, None
user = User.query.get(uid) user = store.get(User, uid)
if user is None: if user is None:
return UserManager.NO_SUCH_USER, None return UserManager.NO_SUCH_USER, None
return UserManager.SUCCESS, user return UserManager.SUCCESS, user
@staticmethod @staticmethod
def add(name, password, mail, admin): def add(store, name, password, mail, admin):
if User.query.filter(User.name == name).first(): if store.find(User, User.name == name).one():
return UserManager.NAME_EXISTS return UserManager.NAME_EXISTS
password = UserManager.__decode_password(password) password = UserManager.__decode_password(password)
crypt, salt = UserManager.__encrypt_password(password) crypt, salt = UserManager.__encrypt_password(password)
user = User(name = name, mail = mail, password = crypt, salt = salt, admin = admin)
session.add(user) user = User()
session.commit() user.name = name
user.mail = mail
user.password = crypt
user.salt = salt
user.admin = admin
store.add(user)
store.commit()
return UserManager.SUCCESS return UserManager.SUCCESS
@staticmethod @staticmethod
def delete(uid): def delete(store, uid):
status, user = UserManager.get(uid) status, user = UserManager.get(store, uid)
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
return status return status
session.delete(user) store.remove(user)
session.commit() store.commit()
return UserManager.SUCCESS return UserManager.SUCCESS
@staticmethod @staticmethod
def try_auth(name, password): def try_auth(store, name, password):
password = UserManager.__decode_password(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: if not user:
return UserManager.NO_SUCH_USER, None return UserManager.NO_SUCH_USER, None
elif UserManager.__encrypt_password(password, user.salt)[0] != user.password: elif UserManager.__encrypt_password(password, user.salt)[0] != user.password:
@ -84,8 +91,8 @@ class UserManager:
return UserManager.SUCCESS, user return UserManager.SUCCESS, user
@staticmethod @staticmethod
def change_password(uid, old_pass, new_pass): def change_password(store, uid, old_pass, new_pass):
status, user = UserManager.get(uid) status, user = UserManager.get(store, uid)
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
return status return status
@ -96,18 +103,18 @@ class UserManager:
return UserManager.WRONG_PASS return UserManager.WRONG_PASS
user.password = UserManager.__encrypt_password(new_pass, user.salt)[0] user.password = UserManager.__encrypt_password(new_pass, user.salt)[0]
session.commit() store.commit()
return UserManager.SUCCESS return UserManager.SUCCESS
@staticmethod @staticmethod
def change_password2(name, new_pass): def change_password2(store, name, new_pass):
user = User.query.filter(User.name == name).first() user = store.find(User, User.name == name).one()
if not user: if not user:
return UserManager.NO_SUCH_USER return UserManager.NO_SUCH_USER
new_pass = UserManager.__decode_password(new_pass) new_pass = UserManager.__decode_password(new_pass)
user.password = UserManager.__encrypt_password(new_pass, user.salt)[0] user.password = UserManager.__encrypt_password(new_pass, user.salt)[0]
session.commit() store.commit()
return UserManager.SUCCESS return UserManager.SUCCESS
@staticmethod @staticmethod

View File

@ -21,17 +21,15 @@
import os, os.path import os, os.path
import time, mimetypes import time, mimetypes
import mutagen import mutagen
from supysonic import config, db from supysonic import config
from supysonic.db import Folder, Artist, Album, Track
def get_mime(ext): def get_mime(ext):
return mimetypes.guess_type('dummy.' + ext, False)[0] or config.get('mimetypes', ext) or 'application/octet-stream' return mimetypes.guess_type('dummy.' + ext, False)[0] or config.get('mimetypes', ext) or 'application/octet-stream'
class Scanner: class Scanner:
def __init__(self, session): def __init__(self, store):
self.__session = session self.__store = store
self.__tracks = db.Track.query.all()
self.__artists = db.Artist.query.all()
self.__folders = db.Folder.query.all()
self.__added_artists = 0 self.__added_artists = 0
self.__added_albums = 0 self.__added_albums = 0
@ -56,20 +54,25 @@ class Scanner:
folder.last_scan = int(time.time()) folder.last_scan = int(time.time())
def prune(self, folder): self.__store.flush()
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)
for album in [ album for artist in self.__artists for album in artist.albums if len(album.tracks) == 0 ]: def prune(self, folder):
album.artist.albums.remove(album) 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.__session.delete(album) 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 self.__deleted_albums += 1
for artist in [ a for a in self.__artists if len(a.albums) == 0 ]: # TODO execute the conditional part on SQL
self.__session.delete(artist) 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.__deleted_artists += 1
self.__cleanup_folder(folder) self.__cleanup_folder(folder)
self.__store.flush()
def check_cover_art(self, folder): def check_cover_art(self, folder):
folder.has_cover_art = os.path.isfile(os.path.join(folder.path, 'cover.jpg')) 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 return os.path.splitext(path)[1][1:].lower() in self.__extensions
def scan_file(self, path): 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: if tr:
tr = tr[0] if not int(os.path.getmtime(path)) > tr.last_modification:
if not os.path.getmtime(path) > tr.last_modification:
return return
tag = self.__try_load_tag(path) tag = self.__try_load_tag(path)
if not tag: if not tag:
self.__remove_track(tr) self.__store.remove(tr)
self.__deleted_tracks += 1
return return
else: else:
tag = self.__try_load_tag(path) tag = self.__try_load_tag(path)
if not tag: if not tag:
return return
tr = db.Track(path = path, root_folder = self.__find_root_folder(path), folder = self.__find_folder(path)) tr = Track()
self.__tracks.append(tr) tr.path = path
self.__added_tracks += 1 add = True
tr.disc = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x[0].split('/')[0])) 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])) 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.year = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0]))
tr.genre = self.__try_read_tag(tag, 'genre') tr.genre = self.__try_read_tag(tag, 'genre')
tr.duration = int(tag.info.length) tr.duration = int(tag.info.length)
tr.album = self.__find_album(self.__try_read_tag(tag, 'artist', ''), self.__try_read_tag(tag, 'album', '')) 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.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.content_type = get_mime(os.path.splitext(path)[1][1:])
tr.last_modification = os.path.getmtime(path) 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): def __find_album(self, artist, album):
ar = self.__find_artist(artist) ar = self.__find_artist(artist)
al = filter(lambda a: a.name == album, ar.albums) al = ar.albums.find(name = album).one()
if al: 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 self.__added_albums += 1
return al return al
def __find_artist(self, artist): 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: if ar:
return ar[0] return ar
ar = db.Artist(name = artist) ar = Artist()
self.__artists.append(ar) ar.name = artist
self.__session.add(ar)
self.__store.add(ar)
self.__added_artists += 1 self.__added_artists += 1
return ar return ar
def __find_root_folder(self, path): def __find_root_folder(self, path):
path = os.path.dirname(path) path = os.path.dirname(path)
folders = filter(lambda f: path.startswith(f.path) and f.root, self.__folders) folders = self.__store.find(Folder, path.startswith(Folder.path), Folder.root == True)
if len(folders) > 1: if folders.count() > 1:
raise Exception("Found multiple root folders for '{}'.".format(path)) 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") 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): def __find_folder(self, path):
path = os.path.dirname(path) path = os.path.dirname(path)
folders = filter(lambda f: f.path == path, self.__folders) folders = self.__store.find(Folder, Folder.path == path)
if len(folders) > 1: if folders.count() > 1:
raise Exception("Found multiple folders for '{}'.".format(path)) raise Exception("Found multiple folders for '{}'.".format(path))
elif len(folders) == 1: elif folders.count() == 1:
return folders[0] return folders.one()
folders = sorted(filter(lambda f: path.startswith(f.path), self.__folders), key = lambda f: len(f.path), reverse = True) folder = self.__store.find(Folder, path.startswith(Folder.path)).order_by(Folder.path).last()
folder = folders[0]
full_path = folder.path full_path = folder.path
path = path[len(folder.path) + 1:] path = path[len(folder.path) + 1:]
for name in path.split(os.sep): for name in path.split(os.sep):
full_path = os.path.join(full_path, name) 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 return folder
@ -184,22 +215,11 @@ class Scanner:
except: except:
return default 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): def __cleanup_folder(self, folder):
for f in folder.children: for f in folder.children:
self.__cleanup_folder(f) self.__cleanup_folder(f)
if len(folder.children) == 0 and len(folder.tracks) == 0 and not folder.root: if folder.children.count() == 0 and folder.tracks.count() == 0 and not folder.root:
folder.parent = None self.__store.remove(folder)
self.__session.delete(folder)
def stats(self): def stats(self):
return (self.__added_artists, self.__added_albums, self.__added_tracks), (self.__deleted_artists, self.__deleted_albums, self.__deleted_tracks) return (self.__added_artists, self.__added_albums, self.__added_tracks), (self.__deleted_artists, self.__deleted_albums, self.__deleted_tracks)

View File

@ -19,15 +19,28 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os.path import os.path
from flask import Flask from flask import Flask, g
from werkzeug.local import LocalProxy
from supysonic import config from supysonic import config
from supysonic.db import get_store
def teardown(exception): def get_db_store():
db.session.remove() 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(): def create_application():
global app, db, UserManager global app
if not config.check(): if not config.check():
return None return None
@ -35,12 +48,11 @@ def create_application():
if not os.path.exists(config.get('webapp', 'cache_dir')): if not os.path.exists(config.get('webapp', 'cache_dir')):
os.makedirs(config.get('webapp', 'cache_dir')) os.makedirs(config.get('webapp', 'cache_dir'))
from supysonic import db
db.init_db()
app = Flask(__name__) app = Flask(__name__)
app.secret_key = '?9huDM\\H' app.secret_key = '?9huDM\\H'
app.teardown_appcontext(teardown_db)
if config.get('webapp', 'log_file'): if config.get('webapp', 'log_file'):
import logging import logging
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
@ -56,8 +68,6 @@ def create_application():
handler.setLevel(mapping.get(config.get('webapp', 'log_level').upper(), logging.NOTSET)) handler.setLevel(mapping.get(config.get('webapp', 'log_level').upper(), logging.NOTSET))
app.logger.addHandler(handler) app.logger.addHandler(handler)
app.teardown_request(teardown)
from supysonic import frontend from supysonic import frontend
from supysonic import api from supysonic import api