1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-22 08:56: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
* Python 2.7
* [Flask](http://flask.pocoo.org/) >= 0.7 (`pip install flask`)
* [SQLAlchemy](http://www.sqlalchemy.org/) (`apt-get install python-sqlalchemy`)
* [Flask](http://flask.pocoo.org/) >= 0.9 (`pip install flask`)
* [Storm](https://storm.canonical.com/) (`apt-get install python-storm`)
* Python Imaging Library (`apt-get install python-imaging`)
* simplejson (`apt-get install python-simplejson`)
* [requests](http://docs.python-requests.org/) >= 1.0.0 (`pip install requests`)
@ -38,8 +38,9 @@ or `KEY: VALUE` syntax.
Available settings are:
* Section **base**:
* **database_uri**: required, a SQLAlchemy [database URI](http://docs.sqlalchemy.org/en/rel_0_8/core/engines.html#database-urls).
* **database_uri**: required, a Storm [database URI](https://storm.canonical.com/Manual#Databases).
I personally use SQLite (`sqlite:////var/supysonic/supysonic.db`), but it might not be the brightest idea for large libraries.
Note that to use PostgreSQL you'll need *psycopg2* version 2.4 (not 2.5!) or [patch storm](https://bugs.launchpad.net/storm/+bug/1170063).
* **scanner_extensions**: space-separated list of file extensions the scanner is restricted to. If omitted, files will be scanned
regardless of their extension
* Section **webapp**
@ -53,6 +54,11 @@ Available settings are:
* Section **mimetypes**: extension to content-type mappings. Designed to help the system guess types, to help clients relying on
the content-type. See [the list of common types](https://en.wikipedia.org/wiki/Internet_media_type#List_of_common_media_types).
### Database initialization
Supysonic does not issue the `CREATE TABLE` commands for the tables it needs. Thus the tables must be created prior to
running the application. Table creation scripts are provided in the *schema* folder for SQLite, MySQL and PostgreSQL.
Running the application
-----------------------
@ -65,8 +71,8 @@ To start the server, just run the `debug_server.py` script.
python debug_server.py
By default, it will listen on the loopback interface (127.0.0.1) on port 5000, but you can specify another address on the command line,
for instance on all the IPv6 interfaces:
By default, it will listen on the loopback interface (127.0.0.1) on port 5000, but you can specify another address on
the command line, for instance on all the IPv6 interfaces:
python debug_server.py ::

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,125 +18,44 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from supysonic import config
from sqlalchemy import create_engine, Table, Column, ForeignKey, func
from sqlalchemy import Integer, String, Boolean, DateTime
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.types import TypeDecorator, BINARY
from sqlalchemy.dialects.postgresql import UUID as pgUUID
from storm.properties import *
from storm.references import Reference, ReferenceSet
from storm.database import create_database
from storm.store import Store
from storm.variables import Variable
import uuid, datetime, time
import os.path
class UUID(TypeDecorator):
"""Platform-somewhat-independent UUID type
Uses Postgresql's UUID type, otherwise uses BINARY(16),
should be more efficient than a CHAR(32).
Mix of http://stackoverflow.com/a/812363
and http://www.sqlalchemy.org/docs/core/types.html#backend-agnostic-guid-type
"""
impl = BINARY
def load_dialect_impl(self, dialect):
if dialect.name == 'postgresql':
return dialect.type_descriptor(pgUUID())
else:
return dialect.type_descriptor(BINARY(16))
def process_bind_param(self, value, dialect):
if value and isinstance(value, uuid.UUID):
if dialect.name == 'postgresql':
return str(value)
return value.bytes
if value and not isinstance(value, uuid.UUID):
raise ValueError, 'value %s is not a valid uuid.UUID' % value
return None
def process_result_value(self, value, dialect):
if value:
if dialect.name == 'postgresql':
return uuid.UUID(value)
return uuid.UUID(bytes = value)
return None
def is_mutable(self):
return False
@staticmethod
def gen_id_column():
return Column(UUID, primary_key = True, default = uuid.uuid4)
def now():
return datetime.datetime.now().replace(microsecond = 0)
config.check()
engine = create_engine(config.get('base', 'database_uri'), convert_unicode = True)
session = scoped_session(sessionmaker(autocommit = False, autoflush = False, bind = engine))
class UnicodeOrStrVariable(Variable):
__slots__ = ()
Base = declarative_base()
Base.query = session.query_property()
def parse_set(self, value, from_db):
if isinstance(value, unicode):
return value
elif isinstance(value, str):
return unicode(value)
raise TypeError("Expected unicode, found %r: %r" % (type(value), value))
class User(Base):
__tablename__ = 'user'
Unicode.variable_class = UnicodeOrStrVariable
id = UUID.gen_id_column()
name = Column(String(64), unique = True)
mail = Column(String(255))
password = Column(String(40))
salt = Column(String(6))
admin = Column(Boolean, default = False)
lastfm_session = Column(String(32), nullable = True)
lastfm_status = Column(Boolean, default = True) # True: ok/unlinked, False: invalid session
class Folder(object):
__storm_table__ = 'folder'
last_play_id = Column(UUID, ForeignKey('track.id'), nullable = True)
last_play = relationship('Track')
last_play_date = Column(DateTime, nullable = True)
id = UUID(primary = True, default_factory = uuid.uuid4)
root = Bool(default = False)
name = Unicode()
path = Unicode() # unique
created = DateTime(default_factory = now)
has_cover_art = Bool(default = False)
last_scan = Int(default = 0)
def as_subsonic_user(self):
return {
'username': self.name,
'email': self.mail,
'scrobblingEnabled': self.lastfm_session is not None and self.lastfm_status,
'adminRole': self.admin,
'settingsRole': True,
'downloadRole': True,
'uploadRole': False,
'playlistRole': True,
'coverArtRole': False,
'commentRole': False,
'podcastRole': False,
'streamRole': True,
'jukeboxRole': False,
'shareRole': False
}
class ClientPrefs(Base):
__tablename__ = 'client_prefs'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
client_name = Column(String(32), nullable = False, primary_key = True)
format = Column(String(8), nullable = True)
bitrate = Column(Integer, nullable = True)
class Folder(Base):
__tablename__ = 'folder'
id = UUID.gen_id_column()
root = Column(Boolean, default = False)
name = Column(String(255))
path = Column(String(4096)) # should be unique, but mysql don't like such large columns
created = Column(DateTime, default = now)
has_cover_art = Column(Boolean, default = False)
last_scan = Column(Integer, default = 0)
parent_id = Column(UUID, ForeignKey('folder.id'), nullable = True)
children = relationship('Folder', backref = backref('parent', remote_side = [ id ]))
parent_id = UUID() # nullable
parent = Reference(parent_id, id)
children = ReferenceSet(id, parent_id)
def as_subsonic_child(self, user):
info = {
@ -152,47 +71,46 @@ class Folder(Base):
if self.has_cover_art:
info['coverArt'] = str(self.id)
starred = StarredFolder.query.get((user.id, self.id))
starred = Store.of(self).get(StarredFolder, (user.id, self.id))
if starred:
info['starred'] = starred.date.isoformat()
rating = RatingFolder.query.get((user.id, self.id))
rating = Store.of(self).get(RatingFolder, (user.id, self.id))
if rating:
info['userRating'] = rating.rating
avgRating = RatingFolder.query.filter(RatingFolder.rated_id == self.id).value(func.avg(RatingFolder.rating))
avgRating = Store.of(self).find(RatingFolder, RatingFolder.rated_id == self.id).avg(RatingFolder.rating)
if avgRating:
info['averageRating'] = avgRating
return info
class Artist(Base):
__tablename__ = 'artist'
class Artist(object):
__storm_table__ = 'artist'
id = UUID.gen_id_column()
name = Column(String(255), unique = True)
albums = relationship('Album', backref = 'artist')
id = UUID(primary = True, default_factory = uuid.uuid4)
name = Unicode() # unique
def as_subsonic_artist(self, user):
info = {
'id': str(self.id),
'name': self.name,
# coverArt
'albumCount': len(self.albums)
'albumCount': self.albums.count()
}
starred = StarredArtist.query.get((user.id, self.id))
starred = Store.of(self).get(StarredArtist, (user.id, self.id))
if starred:
info['starred'] = starred.date.isoformat()
return info
class Album(Base):
__tablename__ = 'album'
class Album(object):
__storm_table__ = 'album'
id = UUID.gen_id_column()
name = Column(String(255))
artist_id = Column(UUID, ForeignKey('artist.id'))
tracks = relationship('Track', backref = 'album')
id = UUID(primary = True, default_factory = uuid.uuid4)
name = Unicode()
artist_id = UUID()
artist = Reference(artist_id, Artist.id)
def as_subsonic_album(self, user):
info = {
@ -200,14 +118,16 @@ class Album(Base):
'name': self.name,
'artist': self.artist.name,
'artistId': str(self.artist_id),
'songCount': len(self.tracks),
'duration': sum(map(lambda t: t.duration, self.tracks)),
'created': min(map(lambda t: t.created, self.tracks)).isoformat()
'songCount': self.tracks.count(),
'duration': sum(self.tracks.values(Track.duration)),
'created': min(self.tracks.values(Track.created)).isoformat()
}
if self.tracks[0].folder.has_cover_art:
info['coverArt'] = str(self.tracks[0].folder_id)
starred = StarredAlbum.query.get((user.id, self.id))
track_with_cover = self.tracks.find(Track.folder_id == Folder.id, Folder.has_cover_art).any()
if track_with_cover:
info['coverArt'] = str(track_with_cover.folder_id)
starred = Store.of(self).get(StarredAlbum, (user.id, self.id))
if starred:
info['starred'] = starred.date.isoformat()
@ -217,36 +137,39 @@ class Album(Base):
year = min(map(lambda t: t.year if t.year else 9999, self.tracks))
return '%i%s' % (year, self.name.lower())
class Track(Base):
__tablename__ = 'track'
Artist.albums = ReferenceSet(Artist.id, Album.artist_id)
id = UUID.gen_id_column()
disc = Column(Integer)
number = Column(Integer)
title = Column(String(255))
year = Column(Integer, nullable = True)
genre = Column(String(255), nullable = True)
duration = Column(Integer)
album_id = Column(UUID, ForeignKey('album.id'))
bitrate = Column(Integer)
class Track(object):
__storm_table__ = 'track'
path = Column(String(4096)) # should be unique, but mysql don't like such large columns
content_type = Column(String(32))
created = Column(DateTime, default = now)
last_modification = Column(Integer)
id = UUID(primary = True, default_factory = uuid.uuid4)
disc = Int()
number = Int()
title = Unicode()
year = Int() # nullable
genre = Unicode() # nullable
duration = Int()
album_id = UUID()
album = Reference(album_id, Album.id)
bitrate = Int()
play_count = Column(Integer, default = 0)
last_play = Column(DateTime, nullable = True)
path = Unicode() # unique
content_type = Unicode()
created = DateTime(default_factory = now)
last_modification = Int()
root_folder_id = Column(UUID, ForeignKey('folder.id'))
root_folder = relationship('Folder', primaryjoin = Folder.id == root_folder_id)
folder_id = Column(UUID, ForeignKey('folder.id'))
folder = relationship('Folder', primaryjoin = Folder.id == folder_id, backref = 'tracks')
play_count = Int(default = 0)
last_play = DateTime() # nullable
root_folder_id = UUID()
root_folder = Reference(root_folder_id, Folder.id)
folder_id = UUID()
folder = Reference(folder_id, Folder.id)
def as_subsonic_child(self, user):
info = {
'id': str(self.id),
'parent': str(self.folder.id),
'parent': str(self.folder_id),
'isDir': False,
'title': self.title,
'album': self.album.name,
@ -261,8 +184,8 @@ class Track(Base):
'isVideo': False,
'discNumber': self.disc,
'created': self.created.isoformat(),
'albumId': str(self.album.id),
'artistId': str(self.album.artist.id),
'albumId': str(self.album_id),
'artistId': str(self.album.artist_id),
'type': 'music'
}
@ -273,14 +196,14 @@ class Track(Base):
if self.folder.has_cover_art:
info['coverArt'] = str(self.folder_id)
starred = StarredTrack.query.get((user.id, self.id))
starred = Store.of(self).get(StarredTrack, (user.id, self.id))
if starred:
info['starred'] = starred.date.isoformat()
rating = RatingTrack.query.get((user.id, self.id))
rating = Store.of(self).get(RatingTrack, (user.id, self.id))
if rating:
info['userRating'] = rating.rating
avgRating = RatingTrack.query.filter(RatingTrack.rated_id == self.id).value(func.avg(RatingTrack.rating))
avgRating = Store.of(self).find(RatingTrack, RatingTrack.rated_id == self.id).avg(RatingTrack.rating)
if avgRating:
info['averageRating'] = avgRating
@ -301,75 +224,109 @@ class Track(Base):
def sort_key(self):
return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower()
class StarredFolder(Base):
__tablename__ = 'starred_folder'
Folder.tracks = ReferenceSet(Folder.id, Track.folder_id)
Album.tracks = ReferenceSet(Album.id, Track.album_id)
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
starred_id = Column(UUID, ForeignKey('folder.id'), primary_key = True)
date = Column(DateTime, default = now)
class User(object):
__storm_table__ = 'user'
user = relationship('User')
starred = relationship('Folder')
id = UUID(primary = True, default_factory = uuid.uuid4)
name = Unicode() # unique
mail = Unicode()
password = Unicode()
salt = Unicode()
admin = Bool(default = False)
lastfm_session = Unicode() # nullable
lastfm_status = Bool(default = True) # True: ok/unlinked, False: invalid session
class StarredArtist(Base):
__tablename__ = 'starred_artist'
last_play_id = UUID() # nullable
last_play = Reference(last_play_id, Track.id)
last_play_date = DateTime() # nullable
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
starred_id = Column(UUID, ForeignKey('artist.id'), primary_key = True)
date = Column(DateTime, default = now)
def as_subsonic_user(self):
return {
'username': self.name,
'email': self.mail,
'scrobblingEnabled': self.lastfm_session is not None and self.lastfm_status,
'adminRole': self.admin,
'settingsRole': True,
'downloadRole': True,
'uploadRole': False,
'playlistRole': True,
'coverArtRole': False,
'commentRole': False,
'podcastRole': False,
'streamRole': True,
'jukeboxRole': False,
'shareRole': False
}
user = relationship('User')
starred = relationship('Artist')
class ClientPrefs(object):
__storm_table__ = 'client_prefs'
__storm_primary__ = 'user_id', 'client_name'
class StarredAlbum(Base):
__tablename__ = 'starred_album'
user_id = UUID()
client_name = Unicode()
format = Unicode() # nullable
bitrate = Int() # nullable
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
starred_id = Column(UUID, ForeignKey('album.id'), primary_key = True)
date = Column(DateTime, default = now)
class BaseStarred(object):
__storm_primary__ = 'user_id', 'starred_id'
user = relationship('User')
starred = relationship('Album')
user_id = UUID()
starred_id = UUID()
date = DateTime(default_factory = now)
class StarredTrack(Base):
__tablename__ = 'starred_track'
user = Reference(user_id, User.id)
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
starred_id = Column(UUID, ForeignKey('track.id'), primary_key = True)
date = Column(DateTime, default = now)
class StarredFolder(BaseStarred):
__storm_table__ = 'starred_folder'
user = relationship('User')
starred = relationship('Track')
starred = Reference(BaseStarred.starred_id, Folder.id)
class RatingFolder(Base):
__tablename__ = 'rating_folder'
class StarredArtist(BaseStarred):
__storm_table__ = 'starred_artist'
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
rated_id = Column(UUID, ForeignKey('folder.id'), primary_key = True)
rating = Column(Integer)
starred = Reference(BaseStarred.starred_id, Artist.id)
user = relationship('User')
rated = relationship('Folder')
class StarredAlbum(BaseStarred):
__storm_table__ = 'starred_album'
class RatingTrack(Base):
__tablename__ = 'rating_track'
starred = Reference(BaseStarred.starred_id, Album.id)
user_id = Column(UUID, ForeignKey('user.id'), primary_key = True)
rated_id = Column(UUID, ForeignKey('track.id'), primary_key = True)
rating = Column(Integer)
class StarredTrack(BaseStarred):
__storm_table__ = 'starred_track'
user = relationship('User')
rated = relationship('Track')
starred = Reference(BaseStarred.starred_id, Track.id)
class ChatMessage(Base):
__tablename__ = 'chat_message'
class BaseRating(object):
__storm_primary__ = 'user_id', 'rated_id'
id = UUID.gen_id_column()
user_id = Column(UUID, ForeignKey('user.id'))
time = Column(Integer, default = lambda: int(time.time()))
message = Column(String(512))
user_id = UUID()
rated_id = UUID()
rating = Int()
user = relationship('User')
user = Reference(user_id, User.id)
class RatingFolder(BaseRating):
__storm_table__ = 'rating_folder'
rated = Reference(BaseRating.rated_id, Folder.id)
class RatingTrack(BaseRating):
__storm_table__ = 'rating_track'
rated = Reference(BaseRating.rated_id, Track.id)
class ChatMessage(object):
__storm_table__ = 'chat_message'
id = UUID(primary = True, default_factory = uuid.uuid4)
user_id = UUID()
time = Int(default_factory = lambda: int(time.time()))
message = Unicode()
user = Reference(user_id, User.id)
def responsize(self):
return {
@ -378,23 +335,17 @@ class ChatMessage(Base):
'message': self.message
}
playlist_track_assoc = Table('playlist_track', Base.metadata,
Column('playlist_id', UUID, ForeignKey('playlist.id')),
Column('track_id', UUID, ForeignKey('track.id'))
)
class Playlist(object):
__storm_table__ = 'playlist'
class Playlist(Base):
__tablename__ = 'playlist'
id = UUID(primary = True, default_factory = uuid.uuid4)
user_id = UUID()
name = Unicode()
comment = Unicode() # nullable
public = Bool(default = False)
created = DateTime(default_factory = now)
id = UUID.gen_id_column()
user_id = Column(UUID, ForeignKey('user.id'))
name = Column(String(255))
comment = Column(String(255), nullable = True)
public = Column(Boolean, default = False)
created = Column(DateTime, default = now)
user = relationship('User')
tracks = relationship('Track', secondary = playlist_track_assoc)
user = Reference(user_id, User.id)
def as_subsonic_playlist(self, user):
info = {
@ -402,18 +353,25 @@ class Playlist(Base):
'name': self.name if self.user_id == user.id else '[%s] %s' % (self.user.name, self.name),
'owner': self.user.name,
'public': self.public,
'songCount': len(self.tracks),
'duration': sum(map(lambda t: t.duration, self.tracks)),
'songCount': self.tracks.count(),
'duration': self.tracks.find().sum(Track.duration),
'created': self.created.isoformat()
}
if self.comment:
info['comment'] = self.comment
return info
def init_db():
Base.metadata.create_all(bind = engine)
class PlaylistTrack(object):
__storm_table__ = 'playlist_track'
__storm_primary__ = 'playlist_id', 'track_id'
def recreate_db():
Base.metadata.drop_all(bind = engine)
Base.metadata.create_all(bind = engine)
playlist_id = UUID()
track_id = UUID()
Playlist.tracks = ReferenceSet(Playlist.id, PlaylistTrack.playlist_id, PlaylistTrack.track_id, Track.id)
def get_store(database_uri):
database = create_database(database_uri)
store = Store(database)
return store

View File

@ -18,7 +18,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from supysonic.web import app
from flask import session
from supysonic.web import app, store
from supysonic.db import Artist, Album, Track
from supysonic.managers.user import UserManager
app.add_template_filter(str)
@ -32,7 +34,7 @@ def login_check():
should_login = False
if not session.get('userid'):
should_login = True
elif UserManager.get(session.get('userid'))[0] != UserManager.SUCCESS:
elif UserManager.get(store, session.get('userid'))[0] != UserManager.SUCCESS:
session.clear()
should_login = True
@ -43,11 +45,11 @@ def login_check():
@app.route('/')
def index():
stats = {
'artists': db.Artist.query.count(),
'albums': db.Album.query.count(),
'tracks': db.Track.query.count()
'artists': store.find(Artist).count(),
'albums': store.find(Album).count(),
'tracks': store.find(Track).count()
}
return render_template('home.html', stats = stats, admin = UserManager.get(session.get('userid'))[1].admin)
return render_template('home.html', stats = stats, admin = UserManager.get(store, session.get('userid'))[1].admin)
from .user import *
from .folder import *

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,17 +21,15 @@
import os, os.path
import time, mimetypes
import mutagen
from supysonic import config, db
from supysonic import config
from supysonic.db import Folder, Artist, Album, Track
def get_mime(ext):
return mimetypes.guess_type('dummy.' + ext, False)[0] or config.get('mimetypes', ext) or 'application/octet-stream'
class Scanner:
def __init__(self, session):
self.__session = session
self.__tracks = db.Track.query.all()
self.__artists = db.Artist.query.all()
self.__folders = db.Folder.query.all()
def __init__(self, store):
self.__store = store
self.__added_artists = 0
self.__added_albums = 0
@ -56,20 +54,25 @@ class Scanner:
folder.last_scan = int(time.time())
def prune(self, folder):
for track in [ t for t in self.__tracks if t.root_folder.id == folder.id and not self.__is_valid_path(t.path) ]:
self.__remove_track(track)
self.__store.flush()
for album in [ album for artist in self.__artists for album in artist.albums if len(album.tracks) == 0 ]:
album.artist.albums.remove(album)
self.__session.delete(album)
def prune(self, folder):
for track in [ t for t in self.__store.find(Track, Track.root_folder_id == folder.id) if not self.__is_valid_path(t.path) ]:
self.__store.remove(track)
self.__deleted_tracks += 1
# TODO execute the conditional part on SQL
for album in [ a for a in self.__store.find(Album) if a.tracks.count() == 0 ]:
self.__store.remove(album)
self.__deleted_albums += 1
for artist in [ a for a in self.__artists if len(a.albums) == 0 ]:
self.__session.delete(artist)
# TODO execute the conditional part on SQL
for artist in [ a for a in self.__store.find(Artist) if a.albums.count() == 0 ]:
self.__store.remove(artist)
self.__deleted_artists += 1
self.__cleanup_folder(folder)
self.__store.flush()
def check_cover_art(self, folder):
folder.has_cover_art = os.path.isfile(os.path.join(folder.path, 'cover.jpg'))
@ -84,24 +87,25 @@ class Scanner:
return os.path.splitext(path)[1][1:].lower() in self.__extensions
def scan_file(self, path):
tr = filter(lambda t: t.path == path, self.__tracks)
tr = self.__store.find(Track, Track.path == path).one()
add = False
if tr:
tr = tr[0]
if not os.path.getmtime(path) > tr.last_modification:
if not int(os.path.getmtime(path)) > tr.last_modification:
return
tag = self.__try_load_tag(path)
if not tag:
self.__remove_track(tr)
self.__store.remove(tr)
self.__deleted_tracks += 1
return
else:
tag = self.__try_load_tag(path)
if not tag:
return
tr = db.Track(path = path, root_folder = self.__find_root_folder(path), folder = self.__find_folder(path))
self.__tracks.append(tr)
self.__added_tracks += 1
tr = Track()
tr.path = path
add = True
tr.disc = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x[0].split('/')[0]))
tr.number = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x[0].split('/')[0]))
@ -109,61 +113,88 @@ class Scanner:
tr.year = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0]))
tr.genre = self.__try_read_tag(tag, 'genre')
tr.duration = int(tag.info.length)
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.content_type = get_mime(os.path.splitext(path)[1][1:])
tr.last_modification = os.path.getmtime(path)
if add:
tralbum = self.__find_album(self.__try_read_tag(tag, 'artist', ''), self.__try_read_tag(tag, 'album', ''))
trroot = self.__find_root_folder(path)
trfolder = self.__find_folder(path)
# Set the references at the very last as searching for them will cause the added track to be flushed, even if
# it is incomplete, causing not null constraints errors.
tr.album = tralbum
tr.folder = trfolder
tr.root_folder = trroot
self.__store.add(tr)
self.__added_tracks += 1
def __find_album(self, artist, album):
ar = self.__find_artist(artist)
al = filter(lambda a: a.name == album, ar.albums)
al = ar.albums.find(name = album).one()
if al:
return al[0]
return al
al = db.Album(name = album, artist = ar)
al = Album()
al.name = album
al.artist = ar
self.__store.add(al)
self.__added_albums += 1
return al
def __find_artist(self, artist):
ar = filter(lambda a: a.name.lower() == artist.lower(), self.__artists)
ar = self.__store.find(Artist, Artist.name == artist).one()
if ar:
return ar[0]
return ar
ar = db.Artist(name = artist)
self.__artists.append(ar)
self.__session.add(ar)
ar = Artist()
ar.name = artist
self.__store.add(ar)
self.__added_artists += 1
return ar
def __find_root_folder(self, path):
path = os.path.dirname(path)
folders = filter(lambda f: path.startswith(f.path) and f.root, self.__folders)
if len(folders) > 1:
folders = self.__store.find(Folder, path.startswith(Folder.path), Folder.root == True)
if folders.count() > 1:
raise Exception("Found multiple root folders for '{}'.".format(path))
elif len(folders) == 0:
elif folders.count() == 0:
raise Exception("Couldn't find the root folder for '{}'.\nDon't scan files that aren't located in a defined music folder")
return folders[0]
return folders.one()
def __find_folder(self, path):
path = os.path.dirname(path)
folders = filter(lambda f: f.path == path, self.__folders)
if len(folders) > 1:
folders = self.__store.find(Folder, Folder.path == path)
if folders.count() > 1:
raise Exception("Found multiple folders for '{}'.".format(path))
elif len(folders) == 1:
return folders[0]
elif folders.count() == 1:
return folders.one()
folders = sorted(filter(lambda f: path.startswith(f.path), self.__folders), key = lambda f: len(f.path), reverse = True)
folder = folders[0]
folder = self.__store.find(Folder, path.startswith(Folder.path)).order_by(Folder.path).last()
full_path = folder.path
path = path[len(folder.path) + 1:]
for name in path.split(os.sep):
full_path = os.path.join(full_path, name)
folder = db.Folder(root = False, name = name, path = full_path, parent = folder)
self.__folders.append(folder)
fold = Folder()
fold.root = False
fold.name = name
fold.path = full_path
fold.parent = folder
self.__store.add(fold)
folder = fold
return folder
@ -184,22 +215,11 @@ class Scanner:
except:
return default
def __remove_track(self, track):
track.album.tracks.remove(track)
track.folder.tracks.remove(track)
# As we don't have a track -> playlists relationship, SQLAlchemy doesn't know it has to remove tracks
# from playlists as well, so let's help it
for playlist in db.Playlist.query.filter(db.Playlist.tracks.contains(track)):
playlist.tracks.remove(track)
self.__session.delete(track)
self.__deleted_tracks += 1
def __cleanup_folder(self, folder):
for f in folder.children:
self.__cleanup_folder(f)
if len(folder.children) == 0 and len(folder.tracks) == 0 and not folder.root:
folder.parent = None
self.__session.delete(folder)
if folder.children.count() == 0 and folder.tracks.count() == 0 and not folder.root:
self.__store.remove(folder)
def stats(self):
return (self.__added_artists, self.__added_albums, self.__added_tracks), (self.__deleted_artists, self.__deleted_albums, self.__deleted_tracks)

View File

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