diff --git a/.gitignore b/.gitignore
index 5c99f15..67f6718 100755
--- a/.gitignore
+++ b/.gitignore
@@ -66,3 +66,5 @@ Session.vim
.netrwhist
*~
+*.orig
+
diff --git a/README.md b/README.md
index bd0a2f4..2757ccf 100644
--- a/README.md
+++ b/README.md
@@ -51,32 +51,27 @@ You'll need these to run Supysonic:
* Python 2.7
* [Flask](http://flask.pocoo.org/) >= 0.9
-* [Storm](https://storm.canonical.com/)
+* [PonyORM](https://ponyorm.com/)
* [Python Imaging Library](https://github.com/python-pillow/Pillow)
* [simplejson](https://simplejson.readthedocs.io/en/latest/)
* [requests](http://docs.python-requests.org/)
* [mutagen](https://mutagen.readthedocs.io/en/latest/)
* [watchdog](https://github.com/gorakhargosh/watchdog)
-On a Debian-like OS (Debian, Ubuntu, Linux Mint, etc.), you can install them
-this way:
+You can install all of them using `pip`:
- $ apt-get install python-flask python-storm python-imaging python-simplesjon python-requests python-mutagen python-watchdog
+ $ pip install -r requirements.txt
You may also need a database specific package:
-* MySQL: `apt install python-mysqldb`
-* PostgreSQL: `apt-install python-psycopg2`
-
-Due to a bug in `storm`, `psycopg2` version 2.5 and later does not work
-properly. You can either use version 2.4 or [patch storm][storm] yourself.
-
-[storm]: https://bugs.launchpad.net/storm/+bug/1170063
+* MySQL: `pip install pymysql` or `pip install mysqlclient`
+* PostgreSQL: `pip install psycopg2`
### Configuration
-Supysonic looks for two files for its configuration: `/etc/supysonic` and
-`~/.supysonic`, merging values from the two files.
+Supysonic looks for four files for its configuration: `/etc/supysonic`,
+`~/.supysonic`, `~/.config/supysonic/supysonic.conf` and `supysonic.conf` in
+the current folder, merging values from all files.
Configuration files must respect a structure similar to Windows INI file, with
`[section]` headers and using a `KEY = VALUE` or `KEY: VALUE` syntax.
@@ -85,7 +80,7 @@ The sample configuration (`config.sample`) looks like this:
```ini
[base]
-; A Storm database URI. See the 'schema' folder for schema creation scripts
+; A database URI. See the 'schema' folder for schema creation scripts
; Default: sqlite:///tmp/supysonic/supysonic.db
;database_uri = sqlite:////var/supysonic/supysonic.db
;database_uri = mysql://supysonic:supysonic@localhost/supysonic
@@ -389,3 +384,7 @@ the case migration scripts will be provided in the `schema/migration`
folder, prefixed by the date of commit that introduced the changes. Those
scripts shouldn't be used when initializing a new database, only when
upgrading from a previous schema.
+There could be both SQL scripts or Python scripts. The Python scripts require
+arguments that are explained when the script is invoked with the `-h` flag.
+If a migration script isn't provided for a specific database engine, it simply
+means that no migration is needed for this engine.
diff --git a/bin/supysonic-cli b/bin/supysonic-cli
index 0cce62b..c63ff57 100755
--- a/bin/supysonic-cli
+++ b/bin/supysonic-cli
@@ -12,9 +12,11 @@ import sys
from supysonic.cli import SupysonicCLI
from supysonic.config import IniConfig
+from supysonic.db import init_database, release_database
if __name__ == "__main__":
config = IniConfig.from_common_locations()
+ init_database(config.BASE['database_uri'])
cli = SupysonicCLI(config)
if len(sys.argv) > 1:
@@ -22,3 +24,5 @@ if __name__ == "__main__":
else:
cli.cmdloop()
+ release_database()
+
diff --git a/config.sample b/config.sample
index f584c9f..5066a07 100644
--- a/config.sample
+++ b/config.sample
@@ -1,5 +1,5 @@
[base]
-; A Storm database URI. See the 'schema' folder for schema creation scripts
+; A database URI. See the 'schema' folder for schema creation scripts
; Default: sqlite:///tmp/supysonic/supysonic.db
;database_uri = sqlite:////var/supysonic/supysonic.db
;database_uri = mysql://supysonic:supysonic@localhost/supysonic
diff --git a/requirements.txt b/requirements.txt
index 275da04..d048741 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
flask>=0.9
-storm
+pony
Pillow
simplejson
requests>=1.0.0
diff --git a/schema/migration/20171230.mysql.py b/schema/migration/20171230.mysql.py
new file mode 100644
index 0000000..00e80a9
--- /dev/null
+++ b/schema/migration/20171230.mysql.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# vim:fenc=utf-8
+#
+# This file is part of Supysonic.
+# Supysonic is a Python implementation of the Subsonic server API.
+#
+# Copyright (C) 2017 Alban 'spl0k' Féron
+#
+# Distributed under terms of the GNU AGPLv3 license.
+
+# Converts ids from hex-encoded strings to binary data
+
+import argparse
+try:
+ import MySQLdb as provider
+except ImportError:
+ import pymysql as provider
+
+from uuid import UUID
+from warnings import filterwarnings
+
+parser = argparse.ArgumentParser()
+parser.add_argument('username')
+parser.add_argument('password')
+parser.add_argument('database')
+parser.add_argument('-H', '--host', default = 'localhost', help = 'default: localhost')
+args = parser.parse_args()
+
+def process_table(connection, table, fields, nullable_fields = ()):
+ to_update = { field: set() for field in fields + nullable_fields }
+
+ c = connection.cursor()
+ c.execute('SELECT {1} FROM {0}'.format(table, ','.join(fields + nullable_fields)))
+ for row in c:
+ for field, value in zip(fields + nullable_fields, row):
+ if value is None or not isinstance(value, basestring):
+ continue
+ to_update[field].add(value)
+
+ for field, values in to_update.iteritems():
+ if not values:
+ continue
+ sql = 'UPDATE {0} SET {1}=%s WHERE {1}=%s'.format(table, field)
+ c.executemany(sql, map(lambda v: (UUID(v).bytes, v), values))
+ for field in fields:
+ sql = 'ALTER TABLE {0} MODIFY {1} BINARY(16) NOT NULL'.format(table, field)
+ c.execute(sql)
+ for field in nullable_fields:
+ sql = 'ALTER TABLE {0} MODIFY {1} BINARY(16)'.format(table, field)
+ c.execute(sql)
+
+ connection.commit()
+
+filterwarnings('ignore', category = provider.Warning)
+conn = provider.connect(host = args.host, user = args.username, passwd = args.password, db = args.database)
+conn.cursor().execute('SET FOREIGN_KEY_CHECKS = 0')
+
+process_table(conn, 'folder', ('id',), ('parent_id',))
+process_table(conn, 'artist', ('id',))
+process_table(conn, 'album', ('id', 'artist_id'))
+process_table(conn, 'track', ('id', 'album_id', 'artist_id', 'root_folder_id', 'folder_id'))
+process_table(conn, 'user', ('id',), ('last_play_id',))
+process_table(conn, 'client_prefs', ('user_id',))
+process_table(conn, 'starred_folder', ('user_id', 'starred_id'))
+process_table(conn, 'starred_artist', ('user_id', 'starred_id'))
+process_table(conn, 'starred_album', ('user_id', 'starred_id'))
+process_table(conn, 'starred_track', ('user_id', 'starred_id'))
+process_table(conn, 'rating_folder', ('user_id', 'rated_id'))
+process_table(conn, 'rating_track', ('user_id', 'rated_id'))
+process_table(conn, 'chat_message', ('id', 'user_id'))
+process_table(conn, 'playlist', ('id', 'user_id'))
+
+conn.cursor().execute('SET FOREIGN_KEY_CHECKS = 1')
+conn.close()
+
diff --git a/schema/migration/20171230.sqlite.py b/schema/migration/20171230.sqlite.py
new file mode 100644
index 0000000..e1ebf52
--- /dev/null
+++ b/schema/migration/20171230.sqlite.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# vim:fenc=utf-8
+#
+# This file is part of Supysonic.
+# Supysonic is a Python implementation of the Subsonic server API.
+#
+# Copyright (C) 2017 Alban 'spl0k' Féron
+#
+# Distributed under terms of the GNU AGPLv3 license.
+
+# Converts ids from hex-encoded strings to binary data
+
+import argparse
+import sqlite3
+
+from uuid import UUID
+
+parser = argparse.ArgumentParser()
+parser.add_argument('dbfile', help = 'Path to the SQLite database file')
+args = parser.parse_args()
+
+def process_table(connection, table, fields):
+ to_update = { field: set() for field in fields }
+
+ c = connection.cursor()
+ for row in c.execute('SELECT {1} FROM {0}'.format(table, ','.join(fields))):
+ for field, value in zip(fields, row):
+ if value is None or not isinstance(value, basestring):
+ continue
+ to_update[field].add(value)
+
+ for field, values in to_update.iteritems():
+ sql = 'UPDATE {0} SET {1}=? WHERE {1}=?'.format(table, field)
+ c.executemany(sql, map(lambda v: (buffer(UUID(v).bytes), v), values))
+
+ connection.commit()
+
+with sqlite3.connect(args.dbfile) as conn:
+ conn.cursor().execute('PRAGMA foreign_keys = OFF')
+
+ process_table(conn, 'folder', ('id', 'parent_id'))
+ process_table(conn, 'artist', ('id',))
+ process_table(conn, 'album', ('id', 'artist_id'))
+ process_table(conn, 'track', ('id', 'album_id', 'artist_id', 'root_folder_id', 'folder_id'))
+ process_table(conn, 'user', ('id', 'last_play_id'))
+ process_table(conn, 'client_prefs', ('user_id',))
+ process_table(conn, 'starred_folder', ('user_id', 'starred_id'))
+ process_table(conn, 'starred_artist', ('user_id', 'starred_id'))
+ process_table(conn, 'starred_album', ('user_id', 'starred_id'))
+ process_table(conn, 'starred_track', ('user_id', 'starred_id'))
+ process_table(conn, 'rating_folder', ('user_id', 'rated_id'))
+ process_table(conn, 'rating_track', ('user_id', 'rated_id'))
+ process_table(conn, 'chat_message', ('id', 'user_id'))
+ process_table(conn, 'playlist', ('id', 'user_id'))
+
diff --git a/schema/mysql.sql b/schema/mysql.sql
index 0021c42..12e4bb5 100644
--- a/schema/mysql.sql
+++ b/schema/mysql.sql
@@ -1,35 +1,35 @@
CREATE TABLE folder (
- id CHAR(36) PRIMARY KEY,
+ id BINARY(16) 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
+ parent_id BINARY(16) REFERENCES folder
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE artist (
- id CHAR(36) PRIMARY KEY,
+ id BINARY(16) PRIMARY KEY,
name VARCHAR(256) NOT NULL
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE album (
- id CHAR(36) PRIMARY KEY,
+ id BINARY(16) PRIMARY KEY,
name VARCHAR(256) NOT NULL,
- artist_id CHAR(36) NOT NULL REFERENCES artist
+ artist_id BINARY(16) NOT NULL REFERENCES artist
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE track (
- id CHAR(36) PRIMARY KEY,
+ id BINARY(16) 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,
- artist_id CHAR(36) NOT NULL REFERENCES artist,
+ album_id BINARY(16) NOT NULL REFERENCES album,
+ artist_id BINARY(16) NOT NULL REFERENCES artist,
bitrate INTEGER NOT NULL,
path VARCHAR(4096) NOT NULL,
content_type VARCHAR(32) NOT NULL,
@@ -37,12 +37,12 @@ CREATE TABLE track (
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
+ root_folder_id BINARY(16) NOT NULL REFERENCES folder,
+ folder_id BINARY(16) NOT NULL REFERENCES folder
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE user (
- id CHAR(36) PRIMARY KEY,
+ id BINARY(16) PRIMARY KEY,
name VARCHAR(64) NOT NULL,
mail VARCHAR(256),
password CHAR(40) NOT NULL,
@@ -50,12 +50,12 @@ CREATE TABLE user (
admin BOOLEAN NOT NULL,
lastfm_session CHAR(32),
lastfm_status BOOLEAN NOT NULL,
- last_play_id CHAR(36) REFERENCES track,
+ last_play_id BINARY(16) REFERENCES track,
last_play_date DATETIME
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE client_prefs (
- user_id CHAR(36) NOT NULL,
+ user_id BINARY(16) NOT NULL,
client_name VARCHAR(32) NOT NULL,
format VARCHAR(8),
bitrate INTEGER,
@@ -63,57 +63,57 @@ CREATE TABLE client_prefs (
) 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,
+ user_id BINARY(16) NOT NULL REFERENCES user,
+ starred_id BINARY(16) 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,
+ user_id BINARY(16) NOT NULL REFERENCES user,
+ starred_id BINARY(16) 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,
+ user_id BINARY(16) NOT NULL REFERENCES user,
+ starred_id BINARY(16) 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,
+ user_id BINARY(16) NOT NULL REFERENCES user,
+ starred_id BINARY(16) 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,
+ user_id BINARY(16) NOT NULL REFERENCES user,
+ rated_id BINARY(16) 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,
+ user_id BINARY(16) NOT NULL REFERENCES user,
+ rated_id BINARY(16) 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,
+ id BINARY(16) PRIMARY KEY,
+ user_id BINARY(16) 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,
+ id BINARY(16) PRIMARY KEY,
+ user_id BINARY(16) NOT NULL REFERENCES user,
name VARCHAR(256) NOT NULL,
comment VARCHAR(256),
public BOOLEAN NOT NULL,
diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py
index 1db12b0..f35f647 100644
--- a/supysonic/api/__init__.py
+++ b/supysonic/api/__init__.py
@@ -18,15 +18,15 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import binascii
import simplejson
import uuid
-import binascii
from flask import request, current_app as app
+from pony.orm import db_session, ObjectNotFound
from xml.dom import minidom
from xml.etree import ElementTree
-from ..web import store
from ..managers.user import UserManager
@app.before_request
@@ -70,7 +70,7 @@ def authorize():
error = request.error_formatter(40, 'Unauthorized'), 401
if request.authorization:
- status, user = UserManager.try_auth(store, request.authorization.username, request.authorization.password)
+ status, user = UserManager.try_auth(request.authorization.username, request.authorization.password)
if status == UserManager.SUCCESS:
request.username = request.authorization.username
request.user = user
@@ -81,7 +81,7 @@ def authorize():
return error
password = decode_password(password)
- status, user = UserManager.try_auth(store, username, password)
+ status, user = UserManager.try_auth(username, password)
if status != UserManager.SUCCESS:
return error
@@ -97,15 +97,13 @@ def get_client_prefs():
return request.error_formatter(10, 'Missing required parameter')
client = request.values.get('c')
- prefs = store.get(ClientPrefs, (request.user.id, client))
- if not prefs:
- prefs = ClientPrefs()
- prefs.user_id = request.user.id
- prefs.client_name = client
- store.add(prefs)
- store.commit()
+ with db_session:
+ try:
+ ClientPrefs[request.user.id, client]
+ except ObjectNotFound:
+ ClientPrefs(user = User[request.user.id], client_name = client)
- request.prefs = prefs
+ request.client = client
@app.after_request
def set_headers(response):
@@ -218,19 +216,20 @@ class ResponseHelper:
return str(value).lower()
return str(value)
-def get_entity(req, ent, param = 'id'):
+def get_entity(req, cls, param = 'id'):
eid = req.values.get(param)
if not eid:
- return False, req.error_formatter(10, 'Missing %s id' % ent.__name__)
+ return False, req.error_formatter(10, 'Missing %s id' % cls.__name__)
try:
eid = uuid.UUID(eid)
except:
- return False, req.error_formatter(0, 'Invalid %s id' % ent.__name__)
+ return False, req.error_formatter(0, 'Invalid %s id' % cls.__name__)
- entity = store.get(ent, eid)
- if not entity:
- return False, (req.error_formatter(70, '%s not found' % ent.__name__), 404)
+ try:
+ entity = cls[eid]
+ except ObjectNotFound:
+ return False, (req.error_formatter(70, '%s not found' % cls.__name__), 404)
return True, entity
diff --git a/supysonic/api/albums_songs.py b/supysonic/api/albums_songs.py
index 90f22ab..8c453ba 100644
--- a/supysonic/api/albums_songs.py
+++ b/supysonic/api/albums_songs.py
@@ -23,12 +23,10 @@ import uuid
from datetime import timedelta
from flask import request, current_app as app
-from storm.expr import Desc, Avg, Min, Max
-from storm.info import ClassAlias
+from pony.orm import db_session, select, desc, avg, max, min, count
from ..db import Folder, Artist, Album, Track, RatingFolder, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, User
from ..db import now
-from ..web import store
@app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ])
def rand_songs():
@@ -43,33 +41,26 @@ def rand_songs():
except:
return request.error_formatter(0, 'Invalid parameter format')
- query = store.find(Track)
+ query = Track.select()
if fromYear:
- query = query.find(Track.year >= fromYear)
+ query = query.filter(lambda t: t.year >= fromYear)
if toYear:
- query = query.find(Track.year <= toYear)
+ query = query.filter(lambda t: t.year <= toYear)
if genre:
- query = query.find(Track.genre == genre)
+ query = query.filter(lambda t: t.genre == genre)
if fid:
- if not store.find(Folder, Folder.id == fid, Folder.root == True).one():
- return request.error_formatter(70, 'Unknown folder')
+ with db_session:
+ if not Folder.exists(id = fid, root = True):
+ return request.error_formatter(70, 'Unknown folder')
- query = query.find(Track.root_folder_id == fid)
+ query = query.filter(lambda t: t.root_folder.id == fid)
- count = query.count()
- if not count:
- return request.formatter({ 'randomSongs': {} })
-
- tracks = []
- for _ in xrange(size):
- x = random.choice(xrange(count))
- tracks.append(query[x])
-
- return request.formatter({
- 'randomSongs': {
- 'song': [ t.as_subsonic_child(request.user, request.prefs) for t in tracks ]
- }
- })
+ with db_session:
+ return request.formatter({
+ 'randomSongs': {
+ 'song': [ t.as_subsonic_child(request.user, request.client) for t in query.random(size) ]
+ }
+ })
@app.route('/rest/getAlbumList.view', methods = [ 'GET', 'POST' ])
def album_list():
@@ -82,46 +73,37 @@ def album_list():
except:
return request.error_formatter(0, 'Invalid parameter format')
- query = store.find(Folder, Track.folder_id == Folder.id)
+ query = select(t.folder for t in Track)
if ltype == 'random':
- albums = []
- count = query.count()
-
- if not count:
- return request.formatter({ 'albumList': {} })
-
- for _ in xrange(size):
- x = random.choice(xrange(count))
- albums.append(query[x])
-
- return request.formatter({
- 'albumList': {
- 'album': [ a.as_subsonic_child(request.user) for a in albums ]
- }
- })
+ with db_session:
+ return request.formatter({
+ 'albumList': {
+ 'album': [ a.as_subsonic_child(request.user) for a in query.random(size) ]
+ }
+ })
elif ltype == 'newest':
- query = query.order_by(Desc(Folder.created)).config(distinct = True)
+ query = query.order_by(desc(Folder.created))
elif ltype == 'highest':
- query = query.find(RatingFolder.rated_id == Folder.id).group_by(Folder.id).order_by(Desc(Avg(RatingFolder.rating)))
+ query = query.order_by(lambda f: desc(avg(f.ratings.rating)))
elif ltype == 'frequent':
- query = query.group_by(Folder.id).order_by(Desc(Avg(Track.play_count)))
+ query = query.order_by(lambda f: desc(avg(f.tracks.play_count)))
elif ltype == 'recent':
- query = query.group_by(Folder.id).order_by(Desc(Max(Track.last_play)))
+ query = query.order_by(lambda f: desc(max(f.tracks.last_play)))
elif ltype == 'starred':
- query = query.find(StarredFolder.starred_id == Folder.id, User.id == StarredFolder.user_id, User.name == request.username)
+ query = select(s.starred for s in StarredFolder if s.user.id == request.user.id and count(s.starred.tracks) > 0)
elif ltype == 'alphabeticalByName':
- query = query.order_by(Folder.name).config(distinct = True)
+ query = query.order_by(Folder.name)
elif ltype == 'alphabeticalByArtist':
- parent = ClassAlias(Folder)
- query = query.find(Folder.parent_id == parent.id).order_by(parent.name, Folder.name).config(distinct = True)
+ query = query.order_by(lambda f: f.parent.name + f.name)
else:
return request.error_formatter(0, 'Unknown search type')
- return request.formatter({
- 'albumList': {
- 'album': [ f.as_subsonic_child(request.user) for f in query[offset:offset+size] ]
- }
- })
+ with db_session:
+ return request.formatter({
+ 'albumList': {
+ 'album': [ f.as_subsonic_child(request.user) for f in query.limit(size, offset) ]
+ }
+ })
@app.route('/rest/getAlbumList2.view', methods = [ 'GET', 'POST' ])
def album_list_id3():
@@ -134,76 +116,71 @@ def album_list_id3():
except:
return request.error_formatter(0, 'Invalid parameter format')
- query = store.find(Album)
+ query = Album.select()
if ltype == 'random':
- albums = []
- count = query.count()
-
- if not count:
- return request.formatter({ 'albumList2': {} })
-
- for _ in xrange(size):
- x = random.choice(xrange(count))
- albums.append(query[x])
-
- return request.formatter({
- 'albumList2': {
- 'album': [ a.as_subsonic_album(request.user) for a in albums ]
- }
- })
+ with db_session:
+ return request.formatter({
+ 'albumList2': {
+ 'album': [ a.as_subsonic_album(request.user) for a in query.random(size) ]
+ }
+ })
elif ltype == 'newest':
- query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Min(Track.created)))
+ query = query.order_by(lambda a: desc(min(a.tracks.created)))
elif ltype == 'frequent':
- query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Avg(Track.play_count)))
+ query = query.order_by(lambda a: desc(avg(a.tracks.play_count)))
elif ltype == 'recent':
- query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Max(Track.last_play)))
+ query = query.order_by(lambda a: desc(max(a.tracks.last_play)))
elif ltype == 'starred':
- query = query.find(StarredAlbum.starred_id == Album.id, User.id == StarredAlbum.user_id, User.name == request.username)
+ query = select(s.starred for s in StarredAlbum if s.user.id == request.user.id)
elif ltype == 'alphabeticalByName':
query = query.order_by(Album.name)
elif ltype == 'alphabeticalByArtist':
- query = query.find(Artist.id == Album.artist_id).order_by(Artist.name, Album.name)
+ query = query.order_by(lambda a: a.artist.name + a.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[offset:offset+size] ]
- }
- })
+ with db_session:
+ return request.formatter({
+ 'albumList2': {
+ 'album': [ f.as_subsonic_album(request.user) for f in query.limit(size, offset) ]
+ }
+ })
@app.route('/rest/getNowPlaying.view', methods = [ 'GET', 'POST' ])
+@db_session
def now_playing():
- query = store.find(User, Track.id == User.last_play_id)
+ query = User.select(lambda u: u.last_play is not None and u.last_play_date + timedelta(minutes = 3) > now())
return request.formatter({
'nowPlaying': {
'entry': [ dict(
- u.last_play.as_subsonic_child(request.user, request.prefs).items() +
+ u.last_play.as_subsonic_child(request.user, request.client).items() +
{ 'username': u.name, 'minutesAgo': (now() - u.last_play_date).seconds / 60, 'playerId': 0 }.items()
- ) for u in query if u.last_play_date + timedelta(seconds = u.last_play.duration * 2) > now() ]
+ ) for u in query ]
}
})
@app.route('/rest/getStarred.view', methods = [ 'GET', 'POST' ])
+@db_session
def get_starred():
- folders = store.find(StarredFolder, StarredFolder.user_id == User.id, User.name == request.username)
+ folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id)
return request.formatter({
'starred': {
- '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, request.prefs) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ]
+ 'artist': [ { 'id': str(sf.id), 'name': sf.name } for sf in folders.filter(lambda f: count(f.tracks) == 0) ],
+ 'album': [ sf.as_subsonic_child(request.user) for sf in folders.filter(lambda f: count(f.tracks) > 0) ],
+ 'song': [ st.as_subsonic_child(request.user, request.client) for st in select(s.starred for s in StarredTrack if s.user.id == request.user.id) ]
}
})
@app.route('/rest/getStarred2.view', methods = [ 'GET', 'POST' ])
+@db_session
def get_starred_id3():
return request.formatter({
'starred2': {
- '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, request.prefs) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ]
+ 'artist': [ sa.as_subsonic_artist(request.user) for sa in select(s.starred for s in StarredArtist if s.user.id == request.user.id) ],
+ 'album': [ sa.as_subsonic_album(request.user) for sa in select(s.starred for s in StarredAlbum if s.user.id == request.user.id) ],
+ 'song': [ st.as_subsonic_child(request.user, request.client) for st in select(s.starred for s in StarredTrack if s.user.id == request.user.id) ]
}
})
diff --git a/supysonic/api/annotation.py b/supysonic/api/annotation.py
index 215c52b..3bad417 100644
--- a/supysonic/api/annotation.py
+++ b/supysonic/api/annotation.py
@@ -22,20 +22,22 @@ import time
import uuid
from flask import request, current_app as app
+from pony.orm import db_session, delete
+from pony.orm import ObjectNotFound
-from ..db import Track, Album, Artist, Folder
+from ..db import Track, Album, Artist, Folder, User
from ..db import StarredTrack, StarredAlbum, StarredArtist, StarredFolder
from ..db import RatingTrack, RatingFolder
from ..lastfm import LastFm
-from ..web import store
from . import get_entity
-def try_star(ent, starred_ent, eid):
+@db_session
+def try_star(cls, starred_cls, eid):
""" Stars an entity
- :param ent: entity class, Folder, Artist, Album or Track
- :param starred_ent: class used for the db representation of the starring of ent
+ :param cls: entity class, Folder, Artist, Album or Track
+ :param starred_cls: class used for the db representation of the starring of ent
:param eid: id of the entity to star
:return error dict, if any. None otherwise
"""
@@ -43,26 +45,27 @@ def try_star(ent, starred_ent, eid):
try:
uid = uuid.UUID(eid)
except:
- return { 'code': 0, 'message': 'Invalid {} id {}'.format(ent.__name__, eid) }
+ return { 'code': 0, 'message': 'Invalid {} id {}'.format(cls.__name__, eid) }
- if store.get(starred_ent, (request.user.id, uid)):
- return { 'code': 0, 'message': '{} {} already starred'.format(ent.__name__, eid) }
+ try:
+ e = cls[uid]
+ except ObjectNotFound:
+ return { 'code': 70, 'message': 'Unknown {} id {}'.format(cls.__name__, eid) }
- e = store.get(ent, uid)
- if not e:
- return { 'code': 70, 'message': 'Unknown {} id {}'.format(ent.__name__, eid) }
-
- starred = starred_ent()
- starred.user_id = request.user.id
- starred.starred_id = uid
- store.add(starred)
+ try:
+ starred_cls[request.user.id, uid]
+ return { 'code': 0, 'message': '{} {} already starred'.format(cls.__name__, eid) }
+ except ObjectNotFound:
+ pass
+ starred_cls(user = User[request.user.id], starred = e)
return None
-def try_unstar(starred_ent, eid):
+@db_session
+def try_unstar(starred_cls, eid):
""" Unstars an entity
- :param starred_ent: class used for the db representation of the starring of the entity
+ :param starred_cls: class used for the db representation of the starring of the entity
:param eid: id of the entity to unstar
:return error dict, if any. None otherwise
"""
@@ -72,7 +75,7 @@ def try_unstar(starred_ent, eid):
except:
return { 'code': 0, 'message': 'Invalid id {}'.format(eid) }
- store.find(starred_ent, starred_ent.user_id == request.user.id, starred_ent.starred_id == uid).remove()
+ delete(s for s in starred_cls if s.user.id == request.user.id and s.starred.id == uid)
return None
def merge_errors(errors):
@@ -106,7 +109,6 @@ def star():
for arId in artistId:
errors.append(try_star(Artist, StarredArtist, arId))
- store.commit()
error = merge_errors(errors)
return request.formatter({ 'error': error }, error = True) if error else request.formatter({})
@@ -130,7 +132,6 @@ def unstar():
for arId in artistId:
errors.append(try_unstar(StarredArtist, arId))
- store.commit()
error = merge_errors(errors)
return request.formatter({ 'error': error }, error = True) if error else request.formatter({})
@@ -149,32 +150,31 @@ def rate():
if not rating in xrange(6):
return request.error_formatter(0, 'rating must be between 0 and 5 (inclusive)')
- if rating == 0:
- 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 = store.get(Track, uid)
- rating_ent = RatingTrack
- if not rated:
- rated = store.get(Folder, uid)
- rating_ent = RatingFolder
- if not rated:
- return request.error_formatter(70, 'Unknown id')
-
- rating_info = store.get(rating_ent, (request.user.id, uid))
- if rating_info:
- rating_info.rating = rating
+ with db_session:
+ if rating == 0:
+ delete(r for r in RatingTrack if r.user.id == request.user.id and r.rated.id == uid)
+ delete(r for r in RatingFolder if r.user.id == request.user.id and r.rated.id == uid)
else:
- rating_info = rating_ent()
- rating_info.user_id = request.user.id
- rating_info.rated_id = uid
- rating_info.rating = rating
- store.add(rating_info)
+ try:
+ rated = Track[uid]
+ rating_cls = RatingTrack
+ except ObjectNotFound:
+ try:
+ rated = Folder[uid]
+ rating_cls = RatingFolder
+ except ObjectNotFound:
+ return request.error_formatter(70, 'Unknown id')
+
+ try:
+ rating_info = rating_cls[request.user.id, uid]
+ rating_info.rating = rating
+ except ObjectNotFound:
+ rating_cls(user = User[request.user.id], rated = rated, rating = rating)
- store.commit()
return request.formatter({})
@app.route('/rest/scrobble.view', methods = [ 'GET', 'POST' ])
+@db_session
def scrobble():
status, res = get_entity(request, Track)
if not status:
@@ -190,7 +190,7 @@ def scrobble():
else:
t = int(time.time())
- lfm = LastFm(app.config['LASTFM'], request.user, app.logger)
+ lfm = LastFm(app.config['LASTFM'], User[request.user.id], app.logger)
if submission in (None, '', True, 'true', 'True', 1, '1'):
lfm.scrobble(res, t)
diff --git a/supysonic/api/browse.py b/supysonic/api/browse.py
index eed9db7..ac39dee 100644
--- a/supysonic/api/browse.py
+++ b/supysonic/api/browse.py
@@ -22,24 +22,27 @@ import string
import uuid
from flask import request, current_app as app
+from pony.orm import db_session
+from pony.orm import ObjectNotFound
from ..db import Folder, Artist, Album, Track
-from ..web import store
from . import get_entity
@app.route('/rest/getMusicFolders.view', methods = [ 'GET', 'POST' ])
+@db_session
def list_folders():
return request.formatter({
'musicFolders': {
'musicFolder': [ {
'id': str(f.id),
'name': f.name
- } for f in store.find(Folder, Folder.root == True).order_by(Folder.name) ]
+ } for f in Folder.select(lambda f: f.root).order_by(Folder.name) ]
}
})
@app.route('/rest/getIndexes.view', methods = [ 'GET', 'POST' ])
+@db_session
def list_indexes():
musicFolderId = request.values.get('musicFolderId')
ifModifiedSince = request.values.get('ifModifiedSince')
@@ -50,33 +53,31 @@ def list_indexes():
return request.error_formatter(0, 'Invalid timestamp')
if musicFolderId is None:
- folder = store.find(Folder, Folder.root == True)
+ folders = Folder.select(lambda f: f.root)[:]
else:
try:
mfid = uuid.UUID(musicFolderId)
except:
return request.error_formatter(0, 'Invalid id')
- folder = store.get(Folder, mfid)
+ try:
+ folder = Folder[mfid]
+ except ObjectNotFound:
+ return request.error_formatter(70, 'Folder not found')
+ if not folder.root:
+ return request.error_formatter(70, 'Folder not found')
+ folders = [ folder ]
- 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 not Folder else folder.last_scan
-
- if (not ifModifiedSince is None) and last_modif < ifModifiedSince:
+ last_modif = max(map(lambda f: f.last_scan, folders))
+ if ifModifiedSince is not 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 not Folder:
- artists = []
- childs = []
- for f in folder:
- artists += f.children
- childs += f.tracks
- else:
- artists = folder.children
- childs = folder.tracks
+ artists = []
+ children = []
+ for f in folders:
+ artists += f.children.select()[:]
+ children += f.tracks.select()[:]
indexes = {}
for artist in artists:
@@ -101,11 +102,12 @@ def list_indexes():
'name': a.name
} for a in sorted(v, key = lambda a: a.name.lower()) ]
} for k, v in sorted(indexes.iteritems()) ],
- 'child': [ c.as_subsonic_child(request.user, request.prefs) for c in sorted(childs, key = lambda t: t.sort_key()) ]
+ 'child': [ c.as_subsonic_child(request.user, request.client) for c in sorted(children, key = lambda t: t.sort_key()) ]
}
})
@app.route('/rest/getMusicDirectory.view', methods = [ 'GET', 'POST' ])
+@db_session
def show_directory():
status, res = get_entity(request, Folder)
if not status:
@@ -114,18 +116,19 @@ def show_directory():
directory = {
'id': str(res.id),
'name': res.name,
- 'child': [ f.as_subsonic_child(request.user) for f in sorted(res.children, key = lambda c: c.name.lower()) ] + [ t.as_subsonic_child(request.user, request.prefs) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ]
+ 'child': [ f.as_subsonic_child(request.user) for f in res.children.order_by(lambda c: c.name.lower()) ] + [ t.as_subsonic_child(request.user, request.client) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ]
}
if not res.root:
- directory['parent'] = str(res.parent_id)
+ directory['parent'] = str(res.parent.id)
return request.formatter({ 'directory': directory })
@app.route('/rest/getArtists.view', methods = [ 'GET', 'POST' ])
+@db_session
def list_artists():
# According to the API page, there are no parameters?
indexes = {}
- for artist in store.find(Artist):
+ for artist in Artist.select():
index = artist.name[0].upper() if artist.name else '?'
if index in map(str, xrange(10)):
index = '#'
@@ -147,6 +150,7 @@ def list_artists():
})
@app.route('/rest/getArtist.view', methods = [ 'GET', 'POST' ])
+@db_session
def artist_info():
status, res = get_entity(request, Artist)
if not status:
@@ -160,23 +164,25 @@ def artist_info():
return request.formatter({ 'artist': info })
@app.route('/rest/getAlbum.view', methods = [ 'GET', 'POST' ])
+@db_session
def album_info():
status, res = get_entity(request, Album)
if not status:
return res
info = res.as_subsonic_album(request.user)
- info['song'] = [ t.as_subsonic_child(request.user, request.prefs) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ]
+ info['song'] = [ t.as_subsonic_child(request.user, request.client) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ]
return request.formatter({ 'album': info })
@app.route('/rest/getSong.view', methods = [ 'GET', 'POST' ])
+@db_session
def track_info():
status, res = get_entity(request, Track)
if not status:
return res
- return request.formatter({ 'song': res.as_subsonic_child(request.user, request.prefs) })
+ return request.formatter({ 'song': res.as_subsonic_child(request.user, request.client) })
@app.route('/rest/getVideos.view', methods = [ 'GET', 'POST' ])
def list_videos():
diff --git a/supysonic/api/chat.py b/supysonic/api/chat.py
index bab6c1d..9f5cdef 100644
--- a/supysonic/api/chat.py
+++ b/supysonic/api/chat.py
@@ -19,9 +19,9 @@
# along with this program. If not, see .
from flask import request, current_app as app
+from pony.orm import db_session
-from ..db import ChatMessage
-from ..web import store
+from ..db import ChatMessage, User
@app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ])
def get_chat():
@@ -31,11 +31,12 @@ def get_chat():
except:
return request.error_formatter(0, 'Invalid parameter')
- query = store.find(ChatMessage).order_by(ChatMessage.time)
- if since:
- query = query.find(ChatMessage.time > since)
+ with db_session:
+ query = ChatMessage.select().order_by(ChatMessage.time)
+ if since:
+ query = query.filter(lambda m: m.time > since)
- return request.formatter({ 'chatMessages': { 'chatMessage': [ msg.responsize() for msg in query ] }})
+ return request.formatter({ 'chatMessages': { 'chatMessage': [ msg.responsize() for msg in query ] }})
@app.route('/rest/addChatMessage.view', methods = [ 'GET', 'POST' ])
def add_chat_message():
@@ -43,10 +44,8 @@ def add_chat_message():
if not msg:
return request.error_formatter(10, 'Missing message')
- chat = ChatMessage()
- chat.user_id = request.user.id
- chat.message = msg
- store.add(chat)
- store.commit()
+ with db_session:
+ ChatMessage(user = User[request.user.id], message = msg)
+
return request.formatter({})
diff --git a/supysonic/api/media.py b/supysonic/api/media.py
index 53beca5..8531087 100644
--- a/supysonic/api/media.py
+++ b/supysonic/api/media.py
@@ -26,10 +26,10 @@ import subprocess
from flask import request, send_file, Response, current_app as app
from PIL import Image
+from pony.orm import db_session
from xml.etree import ElementTree
from .. import scanner
-from ..web import store
from ..db import Track, Album, Artist, Folder, User, ClientPrefs, now
from . import get_entity
@@ -43,6 +43,7 @@ def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_f
return ret
@app.route('/rest/stream.view', methods = [ 'GET', 'POST' ])
+@db_session
def stream_media():
status, res = get_entity(request, Track)
if not status:
@@ -57,10 +58,11 @@ def stream_media():
dst_bitrate = res.bitrate
dst_mimetype = res.content_type
- if request.prefs.format:
- dst_suffix = request.prefs.format
- if request.prefs.bitrate and request.prefs.bitrate < dst_bitrate:
- dst_bitrate = request.prefs.bitrate
+ prefs = ClientPrefs.get(lambda p: p.user.id == request.user.id and p.client_name == request.client)
+ if prefs.format:
+ dst_suffix = prefs.format
+ if prefs.bitrate and prefs.bitrate < dst_bitrate:
+ dst_bitrate = prefs.bitrate
if maxBitRate:
try:
@@ -121,15 +123,16 @@ def stream_media():
res.play_count = res.play_count + 1
res.last_play = now()
- request.user.last_play = res
- request.user.last_play_date = now()
- store.commit()
+ user = User[request.user.id]
+ user.last_play = res
+ user.last_play_date = now()
return response
@app.route('/rest/download.view', methods = [ 'GET', 'POST' ])
def download_media():
- status, res = get_entity(request, Track)
+ with db_session:
+ status, res = get_entity(request, Track)
if not status:
return res
@@ -137,7 +140,8 @@ def download_media():
@app.route('/rest/getCoverArt.view', methods = [ 'GET', 'POST' ])
def cover_art():
- status, res = get_entity(request, Folder)
+ with db_session:
+ status, res = get_entity(request, Folder)
if not status:
return res
@@ -176,25 +180,26 @@ def lyrics():
if not title:
return request.error_formatter(10, 'Missing title parameter')
- 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):
- app.logger.debug('Found lyrics file: ' + lyrics_path)
+ with db_session:
+ query = Track.select(lambda t: title in t.title and artist in t.artist.name)
+ for track in query:
+ lyrics_path = os.path.splitext(track.path)[0] + '.txt'
+ if os.path.exists(lyrics_path):
+ app.logger.debug('Found lyrics file: ' + lyrics_path)
- try:
- lyrics = read_file_as_unicode(lyrics_path)
- except UnicodeError:
- # Lyrics file couldn't be decoded. Rather than displaying an error, try with the potential next files or
- # return no lyrics. Log it anyway.
- app.logger.warn('Unsupported encoding for lyrics file ' + lyrics_path)
- continue
+ try:
+ lyrics = read_file_as_unicode(lyrics_path)
+ except UnicodeError:
+ # Lyrics file couldn't be decoded. Rather than displaying an error, try with the potential next files or
+ # return no lyrics. Log it anyway.
+ app.logger.warn('Unsupported encoding for lyrics file ' + lyrics_path)
+ continue
- return request.formatter({ 'lyrics': {
- 'artist': track.album.artist.name,
- 'title': track.title,
- '_value_': lyrics
- } })
+ return request.formatter({ 'lyrics': {
+ 'artist': track.album.artist.name,
+ 'title': track.title,
+ '_value_': lyrics
+ } })
try:
r = requests.get("http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect",
diff --git a/supysonic/api/playlists.py b/supysonic/api/playlists.py
index 78c7c0d..8810d23 100644
--- a/supysonic/api/playlists.py
+++ b/supysonic/api/playlists.py
@@ -21,46 +21,49 @@
import uuid
from flask import request, current_app as app
-from storm.expr import Or
+from pony.orm import db_session, rollback
+from pony.orm import ObjectNotFound
from ..db import Playlist, User, Track
-from ..web import store
from . import get_entity
@app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ])
def list_playlists():
- query = store.find(Playlist, Or(Playlist.user_id == request.user.id, Playlist.public == True)).order_by(Playlist.name)
+ query = Playlist.select(lambda p: p.user.id == request.user.id or p.public).order_by(Playlist.name)
username = request.values.get('username')
if username:
if not request.user.admin:
return request.error_formatter(50, 'Restricted to admins')
- user = store.find(User, User.name == username).one()
- if not user:
+ with db_session:
+ user = User.get(name = username)
+ if user is None:
return request.error_formatter(70, 'No such user')
- query = store.find(Playlist, Playlist.user_id == User.id, User.name == username).order_by(Playlist.name)
+ query = Playlist.select(lambda p: p.user.name == username).order_by(Playlist.name)
- return request.formatter({ 'playlists': { 'playlist': [ p.as_subsonic_playlist(request.user) for p in query ] } })
+ with db_session:
+ return request.formatter({ 'playlists': { 'playlist': [ p.as_subsonic_playlist(request.user) for p in query ] } })
@app.route('/rest/getPlaylist.view', methods = [ 'GET', 'POST' ])
+@db_session
def show_playlist():
status, res = get_entity(request, Playlist)
if not status:
return res
- 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', 'Private playlist')
info = res.as_subsonic_playlist(request.user)
- info['entry'] = [ t.as_subsonic_child(request.user, request.prefs) for t in res.get_tracks() ]
+ info['entry'] = [ t.as_subsonic_child(request.user, request.client) for t in res.get_tracks() ]
return request.formatter({ 'playlist': info })
@app.route('/rest/createPlaylist.view', methods = [ 'GET', 'POST' ])
+@db_session
def create_playlist():
- # Only(?) method where the android client uses form data rather than GET params
playlist_id, name = map(request.values.get, [ 'playlistId', 'name' ])
# songId actually doesn't seem to be required
songs = request.values.getlist('songId')
@@ -71,55 +74,54 @@ def create_playlist():
return request.error_formatter(0, 'Invalid parameter')
if playlist_id:
- playlist = store.get(Playlist, playlist_id)
- if not playlist:
+ try:
+ playlist = Playlist[playlist_id]
+ except ObjectNotFound:
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")
playlist.clear()
if name:
playlist.name = name
elif name:
- playlist = Playlist()
- playlist.user_id = request.user.id
- playlist.name = name
- store.add(playlist)
+ playlist = Playlist(user = User[request.user.id], name = name)
else:
return request.error_formatter(10, 'Missing playlist id or name')
for sid in songs:
- track = store.get(Track, sid)
- if not track:
- store.rollback()
+ try:
+ track = Track[sid]
+ except ObjectNotFound:
+ rollback()
return request.error_formatter(70, 'Unknown song')
playlist.add(track)
- store.commit()
return request.formatter({})
@app.route('/rest/deletePlaylist.view', methods = [ 'GET', 'POST' ])
+@db_session
def delete_playlist():
status, res = get_entity(request, Playlist)
if not status:
return res
- 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")
- store.remove(res)
- store.commit()
+ res.delete()
return request.formatter({})
@app.route('/rest/updatePlaylist.view', methods = [ 'GET', 'POST' ])
+@db_session
def update_playlist():
status, res = get_entity(request, Playlist, 'playlistId')
if not status:
return res
- 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")
playlist = res
@@ -139,13 +141,13 @@ def update_playlist():
playlist.public = public in (True, 'True', 'true', 1, '1')
for sid in to_add:
- track = store.get(Track, sid)
- if not track:
+ try:
+ track = Track[sid]
+ except ObjectNotFound:
return request.error_formatter(70, 'Unknown song')
playlist.add(track)
playlist.remove_at_indexes(to_remove)
- store.commit()
return request.formatter({})
diff --git a/supysonic/api/search.py b/supysonic/api/search.py
index 49d0c1d..07c61b0 100644
--- a/supysonic/api/search.py
+++ b/supysonic/api/search.py
@@ -20,10 +20,9 @@
from datetime import datetime
from flask import request, current_app as app
-from storm.info import ClassAlias
+from pony.orm import db_session, select
from ..db import Folder, Track, Artist, Album
-from ..web import store
@app.route('/rest/search.view', methods = [ 'GET', 'POST' ])
def old_search():
@@ -38,34 +37,36 @@ def old_search():
min_date = datetime.fromtimestamp(newer_than)
if artist:
- parent = ClassAlias(Folder)
- query = store.find(parent, Folder.parent_id == parent.id, Track.folder_id == Folder.id, parent.name.contains_string(artist), parent.created > min_date).config(distinct = True)
+ query = select(t.folder.parent for t in Track if artist in t.folder.parent.name and t.folder.parent.created > min_date)
elif album:
- query = store.find(Folder, Track.folder_id == Folder.id, Folder.name.contains_string(album), Folder.created > min_date).config(distinct = True)
+ query = select(t.folder for t in Track if album in t.folder.name and t.folder.created > min_date)
elif title:
- query = store.find(Track, Track.title.contains_string(title), Track.created > min_date)
+ query = Track.select(lambda t: title in t.title and t.created > min_date)
elif anyf:
- folders = store.find(Folder, Folder.name.contains_string(anyf), Folder.created > min_date)
- tracks = store.find(Track, Track.title.contains_string(anyf), Track.created > min_date)
- res = list(folders[offset : offset + count])
- if offset + count > folders.count():
- toff = max(0, offset - folders.count())
- tend = offset + count - folders.count()
- res += list(tracks[toff : tend])
+ folders = Folder.select(lambda f: anyf in f.name and f.created > min_date)
+ tracks = Track.select(lambda t: anyf in t.title and t.created > min_date)
+ with db_session:
+ res = folders[offset : offset + count]
+ fcount = folders.count()
+ if offset + count > fcount:
+ toff = max(0, offset - fcount)
+ tend = offset + count - fcount
+ res += tracks[toff : tend]
- return request.formatter({ 'searchResult': {
- 'totalHits': folders.count() + tracks.count(),
- 'offset': offset,
- 'match': [ r.as_subsonic_child(request.user) if isinstance(r, Folder) else r.as_subsonic_child(request.user, request.prefs) for r in res ]
- }})
+ return request.formatter({ 'searchResult': {
+ 'totalHits': folders.count() + tracks.count(),
+ 'offset': offset,
+ 'match': [ r.as_subsonic_child(request.user) if isinstance(r, Folder) else r.as_subsonic_child(request.user, request.client) for r in res ]
+ }})
else:
return request.error_formatter(10, 'Missing search parameter')
- return request.formatter({ 'searchResult': {
- 'totalHits': query.count(),
- 'offset': offset,
- 'match': [ r.as_subsonic_child(request.user) if isinstance(r, Folder) else r.as_subsonic_child(request.user, request.prefs) for r in query[offset : offset + count] ]
- }})
+ with db_session:
+ return request.formatter({ 'searchResult': {
+ 'totalHits': query.count(),
+ 'offset': offset,
+ 'match': [ r.as_subsonic_child(request.user) if isinstance(r, Folder) else r.as_subsonic_child(request.user, request.client) for r in query[offset : offset + count] ]
+ }})
@app.route('/rest/search2.view', methods = [ 'GET', 'POST' ])
def new_search():
@@ -85,16 +86,16 @@ def new_search():
if not query:
return request.error_formatter(10, 'Missing query parameter')
- 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]
+ with db_session:
+ artists = select(t.folder.parent for t in Track if query in t.folder.parent.name).limit(artist_count, artist_offset)
+ albums = select(t.folder for t in Track if query in t.folder.name).limit(album_count, album_offset)
+ songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset)
- return request.formatter({ 'searchResult2': {
- 'artist': [ { 'id': str(a.id), 'name': a.name } for a in artist_query ],
- 'album': [ f.as_subsonic_child(request.user) for f in album_query ],
- 'song': [ t.as_subsonic_child(request.user, request.prefs) for t in song_query ]
- }})
+ return request.formatter({ 'searchResult2': {
+ 'artist': [ { 'id': str(a.id), 'name': a.name } for a in artists ],
+ 'album': [ f.as_subsonic_child(request.user) for f in albums ],
+ 'song': [ t.as_subsonic_child(request.user, request.client) for t in songs ]
+ }})
@app.route('/rest/search3.view', methods = [ 'GET', 'POST' ])
def search_id3():
@@ -114,13 +115,14 @@ def search_id3():
if not query:
return request.error_formatter(10, 'Missing query parameter')
- 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]
+ with db_session:
+ artists = Artist.select(lambda a: query in a.name).limit(artist_count, artist_offset)
+ albums = Album.select(lambda a: query in a.name).limit(album_count, album_offset)
+ songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset)
- return request.formatter({ 'searchResult3': {
- 'artist': [ a.as_subsonic_artist(request.user) for a in artist_query ],
- 'album': [ a.as_subsonic_album(request.user) for a in album_query ],
- 'song': [ t.as_subsonic_child(request.user, request.prefs) for t in song_query ]
- }})
+ return request.formatter({ 'searchResult3': {
+ 'artist': [ a.as_subsonic_artist(request.user) for a in artists ],
+ 'album': [ a.as_subsonic_album(request.user) for a in albums ],
+ 'song': [ t.as_subsonic_child(request.user, request.client) for t in songs ]
+ }})
diff --git a/supysonic/api/user.py b/supysonic/api/user.py
index 00e45d1..43ca1e1 100644
--- a/supysonic/api/user.py
+++ b/supysonic/api/user.py
@@ -19,10 +19,10 @@
# along with this program. If not, see .
from flask import request, current_app as app
+from pony.orm import db_session
from ..db import User
from ..managers.user import UserManager
-from ..web import store
from . import decode_password
@@ -35,7 +35,8 @@ def user_info():
if username != request.username and not request.user.admin:
return request.error_formatter(50, 'Admin restricted')
- user = store.find(User, User.name == username).one()
+ with db_session:
+ user = User.get(name = username)
if user is None:
return request.error_formatter(70, 'Unknown user')
@@ -46,7 +47,8 @@ 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 store.find(User) ] } })
+ with db_session:
+ return request.formatter({ 'users': { 'user': [ u.as_subsonic_user() for u in User.select() ] } })
@app.route('/rest/createUser.view', methods = [ 'GET', 'POST' ])
def user_add():
@@ -59,7 +61,7 @@ def user_add():
admin = True if admin in (True, 'True', 'true', 1, '1') else False
password = decode_password(password)
- status = UserManager.add(store, username, password, email, admin)
+ status = UserManager.add(username, password, email, admin)
if status == UserManager.NAME_EXISTS:
return request.error_formatter(0, 'There is already a user with that username')
@@ -74,11 +76,12 @@ def user_del():
if not username:
return request.error_formatter(10, 'Missing parameter')
- user = store.find(User, User.name == username).one()
- if not user:
+ with db_session:
+ user = User.get(name = username)
+ if user is None:
return request.error_formatter(70, 'Unknown user')
- status = UserManager.delete(store, user.id)
+ status = UserManager.delete(user.id)
if status != UserManager.SUCCESS:
return request.error_formatter(0, UserManager.error_str(status))
@@ -94,7 +97,7 @@ def user_changepass():
return request.error_formatter(50, 'Admin restricted')
password = decode_password(password)
- status = UserManager.change_password2(store, username, password)
+ status = UserManager.change_password2(username, password)
if status != UserManager.SUCCESS:
code = 0
if status == UserManager.NO_SUCH_USER:
diff --git a/supysonic/cli.py b/supysonic/cli.py
index 116e184..251b981 100755
--- a/supysonic/cli.py
+++ b/supysonic/cli.py
@@ -25,7 +25,9 @@ import getpass
import sys
import time
-from .db import get_store, Folder, User
+from pony.orm import db_session
+
+from .db import Folder, User
from .managers.folder import FolderManager
from .managers.user import UserManager
from .scanner import Scanner
@@ -105,8 +107,6 @@ class SupysonicCLI(cmd.Cmd):
for action, subparser in getattr(self.__class__, command + '_subparsers').choices.iteritems():
setattr(self, 'help_{} {}'.format(command, action), subparser.print_help)
- self.__store = get_store(config.BASE['database_uri'])
-
def write_line(self, line = ''):
self.stdout.write(line + '\n')
@@ -148,44 +148,49 @@ class SupysonicCLI(cmd.Cmd):
folder_scan_parser.add_argument('folders', metavar = 'folder', nargs = '*', help = 'Folder(s) to be scanned. If ommitted, all folders are scanned')
folder_scan_parser.add_argument('-f', '--force', action = 'store_true', help = "Force scan of already know files even if they haven't changed")
+ @db_session
def folder_list(self):
self.write_line('Name\t\tPath\n----\t\t----')
- self.write_line('\n'.join('{0: <16}{1}'.format(f.name, f.path) for f in self.__store.find(Folder, Folder.root == True)))
+ self.write_line('\n'.join('{0: <16}{1}'.format(f.name, f.path) for f in Folder.select(lambda f: f.root)))
def folder_add(self, name, path):
- ret = FolderManager.add(self.__store, name, path)
+ ret = FolderManager.add(name, path)
if ret != FolderManager.SUCCESS:
self.write_error_line(FolderManager.error_str(ret))
else:
self.write_line("Folder '{}' added".format(name))
def folder_delete(self, name):
- ret = FolderManager.delete_by_name(self.__store, name)
+ ret = FolderManager.delete_by_name(name)
if ret != FolderManager.SUCCESS:
self.write_error_line(FolderManager.error_str(ret))
else:
self.write_line("Deleted folder '{}'".format(name))
+ @db_session
def folder_scan(self, folders, force):
extensions = self.__config.BASE['scanner_extensions']
if extensions:
extensions = extensions.split(' ')
- scanner = Scanner(self.__store, force = force, extensions = extensions)
+
+ scanner = Scanner(force = force, extensions = extensions)
+
if 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)):
- self.write_line("No such folder(s): " + ' '.join(f for f in folders if isinstance(f, basestring)))
- for folder in filter(lambda f: isinstance(f, Folder), folders):
+ fstrs = folders
+ folders = Folder.select(lambda f: f.root and f.name in fstrs)[:]
+ notfound = set(fstrs) - set(map(lambda f: f.name, folders))
+ if notfound:
+ self.write_line("No such folder(s): " + ' '.join(notfound))
+ for folder in folders:
scanner.scan(folder, TimedProgressDisplay(folder.name, self.stdout))
self.write_line()
else:
- for folder in self.__store.find(Folder, Folder.root == True):
+ for folder in Folder.select(lambda f: f.root):
scanner.scan(folder, TimedProgressDisplay(folder.name, self.stdout))
self.write_line()
scanner.finish()
added, deleted = scanner.stats()
- self.__store.commit()
self.write_line("Scanning done")
self.write_line('Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2]))
@@ -208,9 +213,10 @@ class SupysonicCLI(cmd.Cmd):
user_pass_parser.add_argument('name', help = 'Name/login of the user to which change the password')
user_pass_parser.add_argument('password', nargs = '?', help = 'New password')
+ @db_session
def user_list(self):
self.write_line('Name\t\tAdmin\tEmail\n----\t\t-----\t-----')
- self.write_line('\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in self.__store.find(User)))
+ self.write_line('\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in User.select()))
def user_add(self, name, admin, password, email):
if not password:
@@ -219,24 +225,24 @@ class SupysonicCLI(cmd.Cmd):
if password != confirm:
self.write_error_line("Passwords don't match")
return
- status = UserManager.add(self.__store, name, password, email, admin)
+ status = UserManager.add(name, password, email, admin)
if status != UserManager.SUCCESS:
self.write_error_line(UserManager.error_str(status))
def user_delete(self, name):
- ret = UserManager.delete_by_name(self.__store, name)
+ ret = UserManager.delete_by_name(name)
if ret != UserManager.SUCCESS:
self.write_error_line(UserManager.error_str(ret))
else:
self.write_line("Deleted user '{}'".format(name))
+ @db_session
def user_setadmin(self, name, off):
- user = self.__store.find(User, User.name == name).one()
- if not user:
+ user = User.get(name = name)
+ if user is None:
self.write_error_line('No such user')
else:
user.admin = not off
- self.__store.commit()
self.write_line("{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name))
def user_changepass(self, name, password):
@@ -246,7 +252,7 @@ class SupysonicCLI(cmd.Cmd):
if password != confirm:
self.write_error_line("Passwords don't match")
return
- status = UserManager.change_password2(self.__store, name, password)
+ status = UserManager.change_password2(name, password)
if status != UserManager.SUCCESS:
self.write_error_line(UserManager.error_str(status))
else:
diff --git a/supysonic/config.py b/supysonic/config.py
index a40fefc..26d0162 100644
--- a/supysonic/config.py
+++ b/supysonic/config.py
@@ -20,7 +20,7 @@ class DefaultConfig(object):
tempdir = os.path.join(tempfile.gettempdir(), 'supysonic')
BASE = {
- 'database_uri': 'sqlite://' + os.path.join(tempdir, 'supysonic.db'),
+ 'database_uri': 'sqlite:///' + os.path.join(tempdir, 'supysonic.db'),
'scanner_extensions': None
}
WEBAPP = {
diff --git a/supysonic/db.py b/supysonic/db.py
index 4acbc18..726db6f 100644
--- a/supysonic/db.py
+++ b/supysonic/db.py
@@ -18,45 +18,41 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-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 time
import mimetypes
import os.path
+from datetime import datetime
+from pony.orm import Database, Required, Optional, Set, PrimaryKey, LongStr
+from pony.orm import ObjectNotFound
+from pony.orm import min, max, avg, sum
+from urlparse import urlparse
+from uuid import UUID, uuid4
+
def now():
- return datetime.datetime.now().replace(microsecond = 0)
+ return datetime.now().replace(microsecond = 0)
-class UnicodeOrStrVariable(Variable):
- __slots__ = ()
+db = Database()
- 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 Folder(db.Entity):
+ _table_ = 'folder'
-Unicode.variable_class = UnicodeOrStrVariable
+ id = PrimaryKey(UUID, default = uuid4)
+ root = Required(bool, default = False)
+ name = Required(str)
+ path = Required(str, 4096) # unique
+ created = Required(datetime, precision = 0, default = now)
+ has_cover_art = Required(bool, default = False)
+ last_scan = Required(int, default = 0)
-class Folder(object):
- __storm_table__ = 'folder'
+ parent = Optional(lambda: Folder, reverse = 'children', column = 'parent_id')
+ children = Set(lambda: Folder, reverse = 'parent')
- 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)
+ __alltracks = Set(lambda: Track, lazy = True, reverse = 'root_folder') # Never used, hide it. Could be huge, lazy load
+ tracks = Set(lambda: Track, reverse = 'folder')
- parent_id = UUID() # nullable
- parent = Reference(parent_id, id)
- children = ReferenceSet(id, parent_id)
+ stars = Set(lambda: StarredFolder)
+ ratings = Set(lambda: RatingFolder)
def as_subsonic_child(self, user):
info = {
@@ -67,29 +63,36 @@ class Folder(object):
'created': self.created.isoformat()
}
if not self.root:
- info['parent'] = str(self.parent_id)
+ info['parent'] = str(self.parent.id)
info['artist'] = self.parent.name
if self.has_cover_art:
info['coverArt'] = str(self.id)
- starred = Store.of(self).get(StarredFolder, (user.id, self.id))
- if starred:
+ try:
+ starred = StarredFolder[user.id, self.id]
info['starred'] = starred.date.isoformat()
+ except ObjectNotFound: pass
- rating = Store.of(self).get(RatingFolder, (user.id, self.id))
- if rating:
+ try:
+ rating = RatingFolder[user.id, self.id]
info['userRating'] = rating.rating
- avgRating = Store.of(self).find(RatingFolder, RatingFolder.rated_id == self.id).avg(RatingFolder.rating)
+ except ObjectNotFound: pass
+
+ avgRating = avg(self.ratings.rating)
if avgRating:
info['averageRating'] = avgRating
return info
-class Artist(object):
- __storm_table__ = 'artist'
+class Artist(db.Entity):
+ _table_ = 'artist'
- id = UUID(primary = True, default_factory = uuid.uuid4)
- name = Unicode() # unique
+ id = PrimaryKey(UUID, default = uuid4)
+ name = Required(str) # unique
+ albums = Set(lambda: Album)
+ tracks = Set(lambda: Track)
+
+ stars = Set(lambda: StarredArtist)
def as_subsonic_artist(self, user):
info = {
@@ -99,38 +102,42 @@ class Artist(object):
'albumCount': self.albums.count()
}
- starred = Store.of(self).get(StarredArtist, (user.id, self.id))
- if starred:
+ try:
+ starred = StarredArtist[user.id, self.id]
info['starred'] = starred.date.isoformat()
+ except ObjectNotFound: pass
return info
-class Album(object):
- __storm_table__ = 'album'
+class Album(db.Entity):
+ _table_ = 'album'
- id = UUID(primary = True, default_factory = uuid.uuid4)
- name = Unicode()
- artist_id = UUID()
- artist = Reference(artist_id, Artist.id)
+ id = PrimaryKey(UUID, default = uuid4)
+ name = Required(str)
+ artist = Required(Artist, column = 'artist_id')
+ tracks = Set(lambda: Track)
+
+ stars = Set(lambda: StarredAlbum)
def as_subsonic_album(self, user):
info = {
'id': str(self.id),
'name': self.name,
'artist': self.artist.name,
- 'artistId': str(self.artist_id),
+ 'artistId': str(self.artist.id),
'songCount': self.tracks.count(),
- 'duration': sum(self.tracks.values(Track.duration)),
- 'created': min(self.tracks.values(Track.created)).isoformat()
+ 'duration': sum(self.tracks.duration),
+ 'created': min(self.tracks.created).isoformat()
}
- 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)
+ track_with_cover = self.tracks.select(lambda t: t.folder.has_cover_art).first()
+ if track_with_cover is not None:
+ info['coverArt'] = str(track_with_cover.folder.id)
- starred = Store.of(self).get(StarredAlbum, (user.id, self.id))
- if starred:
+ try:
+ starred = StarredAlbum[user.id, self.id]
info['starred'] = starred.date.isoformat()
+ except ObjectNotFound: pass
return info
@@ -138,41 +145,42 @@ class Album(object):
year = min(map(lambda t: t.year if t.year else 9999, self.tracks))
return '%i%s' % (year, self.name.lower())
-Artist.albums = ReferenceSet(Artist.id, Album.artist_id)
+class Track(db.Entity):
+ _table_ = 'track'
-class Track(object):
- __storm_table__ = 'track'
+ id = PrimaryKey(UUID, default = uuid4)
+ disc = Required(int)
+ number = Required(int)
+ title = Required(str)
+ year = Optional(int)
+ genre = Optional(str, nullable = True)
+ duration = Required(int)
- 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)
- artist_id = UUID()
- artist = Reference(artist_id, Artist.id)
- bitrate = Int()
+ album = Required(Album, column = 'album_id')
+ artist = Required(Artist, column = 'artist_id')
- path = Unicode() # unique
- content_type = Unicode()
- created = DateTime(default_factory = now)
- last_modification = Int()
+ bitrate = Required(int)
- play_count = Int(default = 0)
- last_play = DateTime() # nullable
+ path = Required(str, 4096) # unique
+ content_type = Required(str)
+ created = Required(datetime, precision = 0, default = now)
+ last_modification = Required(int)
- root_folder_id = UUID()
- root_folder = Reference(root_folder_id, Folder.id)
- folder_id = UUID()
- folder = Reference(folder_id, Folder.id)
+ play_count = Required(int, default = 0)
+ last_play = Optional(datetime, precision = 0)
- def as_subsonic_child(self, user, prefs):
+ root_folder = Required(Folder, column = 'root_folder_id')
+ folder = Required(Folder, column = 'folder_id')
+
+ __lastly_played_by = Set(lambda: User) # Never used, hide it
+
+ stars = Set(lambda: StarredTrack)
+ ratings = Set(lambda: RatingTrack)
+
+ def as_subsonic_child(self, user, client):
info = {
'id': str(self.id),
- 'parent': str(self.folder_id),
+ 'parent': str(self.folder.id),
'isDir': False,
'title': self.title,
'album': self.album.name,
@@ -187,8 +195,8 @@ class Track(object):
'isVideo': False,
'discNumber': self.disc,
'created': self.created.isoformat(),
- 'albumId': str(self.album_id),
- 'artistId': str(self.artist_id),
+ 'albumId': str(self.album.id),
+ 'artistId': str(self.artist.id),
'type': 'music'
}
@@ -197,20 +205,24 @@ class Track(object):
if self.genre:
info['genre'] = self.genre
if self.folder.has_cover_art:
- info['coverArt'] = str(self.folder_id)
+ info['coverArt'] = str(self.folder.id)
- starred = Store.of(self).get(StarredTrack, (user.id, self.id))
- if starred:
+ try:
+ starred = StarredTrack[user.id, self.id]
info['starred'] = starred.date.isoformat()
+ except ObjectNotFound: pass
- rating = Store.of(self).get(RatingTrack, (user.id, self.id))
- if rating:
+ try:
+ rating = RatingTrack[user.id, self.id]
info['userRating'] = rating.rating
- avgRating = Store.of(self).find(RatingTrack, RatingTrack.rated_id == self.id).avg(RatingTrack.rating)
+ except ObjectNotFound: pass
+
+ avgRating = avg(self.ratings.rating)
if avgRating:
info['averageRating'] = avgRating
- if prefs and prefs.format and prefs.format != self.suffix():
+ prefs = ClientPrefs.get(lambda p: p.user.id == user.id and p.client_name == client)
+ if prefs is not None and prefs.format is not None and prefs.format != self.suffix():
info['transcodedSuffix'] = prefs.format
info['transcodedContentType'] = mimetypes.guess_type('dummyname.' + prefs.format, False)[0] or 'application/octet-stream'
@@ -228,25 +240,31 @@ class Track(object):
def sort_key(self):
return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower()
-Folder.tracks = ReferenceSet(Folder.id, Track.folder_id)
-Album.tracks = ReferenceSet(Album.id, Track.album_id)
-Artist.tracks = ReferenceSet(Artist.id, Track.artist_id)
+class User(db.Entity):
+ _table_ = 'user'
-class User(object):
- __storm_table__ = 'user'
+ id = PrimaryKey(UUID, default = uuid4)
+ name = Required(str, 64) # unique
+ mail = Optional(str)
+ password = Required(str, 40)
+ salt = Required(str, 6)
+ admin = Required(bool, default = False)
+ lastfm_session = Optional(str, 32, nullable = True)
+ lastfm_status = Required(bool, default = True) # True: ok/unlinked, False: invalid session
- 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
+ last_play = Optional(Track, column = 'last_play_id')
+ last_play_date = Optional(datetime, precision = 0)
- last_play_id = UUID() # nullable
- last_play = Reference(last_play_id, Track.id)
- last_play_date = DateTime() # nullable
+ clients = Set(lambda: ClientPrefs)
+ playlists = Set(lambda: Playlist)
+ __messages = Set(lambda: ChatMessage, lazy = True) # Never used, hide it
+
+ starred_folders = Set(lambda: StarredFolder, lazy = True)
+ starred_artists = Set(lambda: StarredArtist, lazy = True)
+ starred_albums = Set(lambda: StarredAlbum, lazy = True)
+ starred_tracks = Set(lambda: StarredTrack, lazy = True)
+ folder_ratings = Set(lambda: RatingFolder, lazy = True)
+ track_ratings = Set(lambda: RatingTrack, lazy = True)
def as_subsonic_user(self):
return {
@@ -266,72 +284,74 @@ class User(object):
'shareRole': False
}
-class ClientPrefs(object):
- __storm_table__ = 'client_prefs'
- __storm_primary__ = 'user_id', 'client_name'
+class ClientPrefs(db.Entity):
+ _table_ = 'client_prefs'
- user_id = UUID()
- client_name = Unicode()
- format = Unicode() # nullable
- bitrate = Int() # nullable
+ user = Required(User, column = 'user_id')
+ client_name = Required(str, 32)
+ PrimaryKey(user, client_name)
+ format = Optional(str, 8)
+ bitrate = Optional(int)
-class BaseStarred(object):
- __storm_primary__ = 'user_id', 'starred_id'
+class StarredFolder(db.Entity):
+ _table_ = 'starred_folder'
- user_id = UUID()
- starred_id = UUID()
- date = DateTime(default_factory = now)
+ user = Required(User, column = 'user_id')
+ starred = Required(Folder, column = 'starred_id')
+ date = Required(datetime, precision = 0, default = now)
- user = Reference(user_id, User.id)
+ PrimaryKey(user, starred)
-class StarredFolder(BaseStarred):
- __storm_table__ = 'starred_folder'
+class StarredArtist(db.Entity):
+ _table_ = 'starred_artist'
- starred = Reference(BaseStarred.starred_id, Folder.id)
+ user = Required(User, column = 'user_id')
+ starred = Required(Artist, column = 'starred_id')
+ date = Required(datetime, precision = 0, default = now)
-class StarredArtist(BaseStarred):
- __storm_table__ = 'starred_artist'
+ PrimaryKey(user, starred)
- starred = Reference(BaseStarred.starred_id, Artist.id)
+class StarredAlbum(db.Entity):
+ _table_ = 'starred_album'
-class StarredAlbum(BaseStarred):
- __storm_table__ = 'starred_album'
+ user = Required(User, column = 'user_id')
+ starred = Required(Album, column = 'starred_id')
+ date = Required(datetime, precision = 0, default = now)
- starred = Reference(BaseStarred.starred_id, Album.id)
+ PrimaryKey(user, starred)
-class StarredTrack(BaseStarred):
- __storm_table__ = 'starred_track'
+class StarredTrack(db.Entity):
+ _table_ = 'starred_track'
- starred = Reference(BaseStarred.starred_id, Track.id)
+ user = Required(User, column = 'user_id')
+ starred = Required(Track, column = 'starred_id')
+ date = Required(datetime, precision = 0, default = now)
-class BaseRating(object):
- __storm_primary__ = 'user_id', 'rated_id'
+ PrimaryKey(user, starred)
- user_id = UUID()
- rated_id = UUID()
- rating = Int()
+class RatingFolder(db.Entity):
+ _table_ = 'rating_folder'
+ user = Required(User, column = 'user_id')
+ rated = Required(Folder, column = 'rated_id')
+ rating = Required(int, min = 1, max = 5)
- user = Reference(user_id, User.id)
+ PrimaryKey(user, rated)
-class RatingFolder(BaseRating):
- __storm_table__ = 'rating_folder'
+class RatingTrack(db.Entity):
+ _table_ = 'rating_track'
+ user = Required(User, column = 'user_id')
+ rated = Required(Track, column = 'rated_id')
+ rating = Required(int, min = 1, max = 5)
- rated = Reference(BaseRating.rated_id, Folder.id)
+ PrimaryKey(user, rated)
-class RatingTrack(BaseRating):
- __storm_table__ = 'rating_track'
+class ChatMessage(db.Entity):
+ _table_ = 'chat_message'
- 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)
+ id = PrimaryKey(UUID, default = uuid4)
+ user = Required(User, column = 'user_id')
+ time = Required(int, default = lambda: int(time.time()))
+ message = Required(str, 512)
def responsize(self):
return {
@@ -340,24 +360,22 @@ class ChatMessage(object):
'message': self.message
}
-class Playlist(object):
- __storm_table__ = 'playlist'
+class Playlist(db.Entity):
+ _table_ = '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)
- tracks = Unicode()
-
- user = Reference(user_id, User.id)
+ id = PrimaryKey(UUID, default = uuid4)
+ user = Required(User, column = 'user_id')
+ name = Required(str)
+ comment = Optional(str)
+ public = Required(bool, default = False)
+ created = Required(datetime, precision = 0, default = now)
+ tracks = Optional(LongStr)
def as_subsonic_playlist(self, user):
tracks = self.get_tracks()
info = {
'id': str(self.id),
- '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,
'public': self.public,
'songCount': len(tracks),
@@ -374,38 +392,34 @@ class Playlist(object):
tracks = []
should_fix = False
- store = Store.of(self)
for t in self.tracks.split(','):
try:
- tid = uuid.UUID(t)
- track = store.get(Track, tid)
- if track:
- tracks.append(track)
- else:
- should_fix = True
+ tid = UUID(t)
+ track = Track[tid]
+ tracks.append(track)
except:
should_fix = True
if should_fix:
self.tracks = ','.join(map(lambda t: str(t.id), tracks))
- store.commit()
+ db.commit()
return tracks
def clear(self):
- self.tracks = ""
+ self.tracks = ''
def add(self, track):
- if isinstance(track, uuid.UUID):
+ if isinstance(track, UUID):
tid = track
elif isinstance(track, Track):
tid = track.id
elif isinstance(track, basestring):
- tid = uuid.UUID(track)
+ tid = UUID(track)
if self.tracks and len(self.tracks) > 0:
- self.tracks = "{},{}".format(self.tracks, tid)
+ self.tracks = '{},{}'.format(self.tracks, tid)
else:
self.tracks = str(tid)
@@ -418,8 +432,31 @@ class Playlist(object):
self.tracks = ','.join(t for t in tracks if t)
-def get_store(database_uri):
- database = create_database(database_uri)
- store = Store(database)
- return store
+def parse_uri(database_uri):
+ if not isinstance(database_uri, basestring):
+ raise TypeError('Expecting a string')
+
+ uri = urlparse(database_uri)
+ if uri.scheme == 'sqlite':
+ path = uri.path
+ if not path:
+ path = ':memory:'
+ elif path[0] == '/':
+ path = path[1:]
+
+ return dict(provider = 'sqlite', filename = path)
+ elif uri.scheme in ('postgres', 'postgresql'):
+ return dict(provider = 'postgres', user = uri.username, password = uri.password, host = uri.hostname, database = uri.path[1:])
+ elif uri.scheme == 'mysql':
+ return dict(provider = 'mysql', user = uri.username, passwd = uri.password, host = uri.hostname, db = uri.path[1:])
+ return dict()
+
+def init_database(database_uri, create_tables = False):
+ db.bind(**parse_uri(database_uri))
+ db.generate_mapping(create_tables = create_tables)
+
+def release_database():
+ db.disconnect()
+ db.provider = None
+ db.schema = None
diff --git a/supysonic/frontend/__init__.py b/supysonic/frontend/__init__.py
index 3db8481..f4845eb 100644
--- a/supysonic/frontend/__init__.py
+++ b/supysonic/frontend/__init__.py
@@ -11,8 +11,8 @@
from flask import session, request, redirect, url_for, current_app as app
from functools import wraps
+from pony.orm import db_session
-from ..web import store
from ..db import Artist, Album, Track
from ..managers.user import UserManager
@@ -27,7 +27,7 @@ def login_check():
request.user = None
should_login = True
if session.get('userid'):
- code, user = UserManager.get(store, session.get('userid'))
+ code, user = UserManager.get(session.get('userid'))
if code != UserManager.SUCCESS:
session.clear()
else:
@@ -39,13 +39,14 @@ def login_check():
return redirect(url_for('login', returnUrl = request.script_root + request.url[len(request.url_root)-1:]))
@app.route('/')
+@db_session
def index():
stats = {
- 'artists': store.find(Artist).count(),
- 'albums': store.find(Album).count(),
- 'tracks': store.find(Track).count()
+ 'artists': Artist.select().count(),
+ 'albums': Album.select().count(),
+ 'tracks': Track.select().count()
}
- return render_template('home.html', stats = stats, admin = UserManager.get(store, session.get('userid'))[1].admin)
+ return render_template('home.html', stats = stats)
def admin_only(f):
@wraps(f)
diff --git a/supysonic/frontend/folder.py b/supysonic/frontend/folder.py
index f36cc81..64deffe 100644
--- a/supysonic/frontend/folder.py
+++ b/supysonic/frontend/folder.py
@@ -22,19 +22,20 @@ import os.path
import uuid
from flask import request, flash, render_template, redirect, url_for, current_app as app
+from pony.orm import db_session
from ..db import Folder
from ..managers.user import UserManager
from ..managers.folder import FolderManager
from ..scanner import Scanner
-from ..web import store
from . import admin_only
@app.route('/folder')
@admin_only
+@db_session
def folder_index():
- return render_template('folders.html', folders = store.find(Folder, Folder.root == True))
+ return render_template('folders.html', folders = Folder.select(lambda f: f.root))
@app.route('/folder/add')
@admin_only
@@ -55,7 +56,7 @@ def add_folder_post():
if error:
return render_template('addfolder.html')
- ret = FolderManager.add(store, name, path)
+ ret = FolderManager.add(name, path)
if ret != FolderManager.SUCCESS:
flash(FolderManager.error_str(ret))
return render_template('addfolder.html')
@@ -73,7 +74,7 @@ def del_folder(id):
flash('Invalid folder id')
return redirect(url_for('folder_index'))
- ret = FolderManager.delete(store, idid)
+ ret = FolderManager.delete(idid)
if ret != FolderManager.SUCCESS:
flash(FolderManager.error_str(ret))
else:
@@ -84,16 +85,19 @@ def del_folder(id):
@app.route('/folder/scan')
@app.route('/folder/scan/')
@admin_only
+@db_session
def scan_folder(id = None):
extensions = app.config['BASE']['scanner_extensions']
if extensions:
extensions = extensions.split(' ')
- scanner = Scanner(store, extensions = extensions)
+
+ scanner = Scanner(extensions = extensions)
+
if id is None:
- for folder in store.find(Folder, Folder.root == True):
+ for folder in Folder.select(lambda f: f.root):
scanner.scan(folder)
else:
- status, folder = FolderManager.get(store, id)
+ status, folder = FolderManager.get(id)
if status != FolderManager.SUCCESS:
flash(FolderManager.error_str(status))
return redirect(url_for('folder_index'))
@@ -101,7 +105,6 @@ def scan_folder(id = None):
scanner.finish()
added, deleted = scanner.stats()
- 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]))
diff --git a/supysonic/frontend/playlist.py b/supysonic/frontend/playlist.py
index 00e4692..d3f62d2 100644
--- a/supysonic/frontend/playlist.py
+++ b/supysonic/frontend/playlist.py
@@ -21,33 +21,38 @@
import uuid
from flask import request, flash, render_template, redirect, url_for, current_app as app
+from pony.orm import db_session
+from pony.orm import ObjectNotFound
-from ..web import store
from ..db import Playlist
from ..managers.user import UserManager
@app.route('/playlist')
+@db_session
def playlist_index():
return render_template('playlists.html',
- mine = store.find(Playlist, Playlist.user_id == request.user.id),
- others = store.find(Playlist, Playlist.user_id != request.user.id, Playlist.public == True))
+ mine = Playlist.select(lambda p: p.user == request.user),
+ others = Playlist.select(lambda p: p.user != request.user and p.public))
@app.route('/playlist/')
+@db_session
def playlist_details(uid):
try:
- uid = uuid.UUID(uid) if type(uid) in (str, unicode) else uid
+ uid = uuid.UUID(uid)
except:
flash('Invalid playlist id')
return redirect(url_for('playlist_index'))
- playlist = store.get(Playlist, uid)
- if not playlist:
+ try:
+ playlist = Playlist[uid]
+ except ObjectNotFound:
flash('Unknown playlist')
return redirect(url_for('playlist_index'))
return render_template('playlist.html', playlist = playlist)
@app.route('/playlist/', methods = [ 'POST' ])
+@db_session
def playlist_update(uid):
try:
uid = uuid.UUID(uid)
@@ -55,24 +60,25 @@ def playlist_update(uid):
flash('Invalid playlist id')
return redirect(url_for('playlist_index'))
- playlist = store.get(Playlist, uid)
- if not playlist:
+ try:
+ playlist = Playlist[uid]
+ except ObjectNotFound:
flash('Unknown playlist')
return redirect(url_for('playlist_index'))
- if playlist.user_id != request.user.id:
+ if playlist.user.id != request.user.id:
flash("You're not allowed to edit this playlist")
elif not request.form.get('name'):
flash('Missing playlist name')
else:
playlist.name = request.form.get('name')
playlist.public = request.form.get('public') in (True, 'True', 1, '1', 'on', 'checked')
- store.commit()
flash('Playlist updated.')
return playlist_details(uid)
@app.route('/playlist/del/')
+@db_session
def playlist_delete(uid):
try:
uid = uuid.UUID(uid)
@@ -80,14 +86,16 @@ def playlist_delete(uid):
flash('Invalid playlist id')
return redirect(url_for('playlist_index'))
- playlist = store.get(Playlist, uid)
- if not playlist:
+ try:
+ playlist = Playlist[uid]
+ except ObjectNotFound:
flash('Unknown playlist')
- elif playlist.user_id != request.user.id:
+ return redirect(url_for('playlist_index'))
+
+ if playlist.user.id != request.user.id:
flash("You're not allowed to delete this playlist")
else:
- store.remove(playlist)
- store.commit()
+ playlist.delete()
flash('Playlist deleted')
return redirect(url_for('playlist_index'))
diff --git a/supysonic/frontend/user.py b/supysonic/frontend/user.py
index 573aa71..b016090 100644
--- a/supysonic/frontend/user.py
+++ b/supysonic/frontend/user.py
@@ -20,15 +20,16 @@
from flask import request, session, flash, render_template, redirect, url_for, current_app as app
from functools import wraps
+from pony.orm import db_session
from ..db import User, ClientPrefs
from ..lastfm import LastFm
from ..managers.user import UserManager
-from ..web import store
from . import admin_only
def me_or_uuid(f, arg = 'uid'):
+ @db_session
@wraps(f)
def decorated_func(*args, **kwargs):
if kwargs:
@@ -37,11 +38,11 @@ def me_or_uuid(f, arg = 'uid'):
uid = args[0]
if uid == 'me':
- user = request.user
+ user = User[request.user.id] # Refetch user from previous transaction
elif not request.user.admin:
return redirect(url_for('index'))
else:
- code, user = UserManager.get(store, uid)
+ code, user = UserManager.get(uid)
if code != UserManager.SUCCESS:
flash(UserManager.error_str(code))
return redirect(url_for('index'))
@@ -57,14 +58,14 @@ def me_or_uuid(f, arg = 'uid'):
@app.route('/user')
@admin_only
+@db_session
def user_index():
- return render_template('users.html', users = store.find(User))
+ return render_template('users.html', users = User.select())
@app.route('/user/')
@me_or_uuid
def user_profile(uid, user):
- prefs = store.find(ClientPrefs, ClientPrefs.user_id == user.id)
- return render_template('profile.html', user = user, has_lastfm = app.config['LASTFM']['api_key'] != None, clients = prefs)
+ return render_template('profile.html', user = user, has_lastfm = app.config['LASTFM']['api_key'] != None, clients = user.clients)
@app.route('/user/', methods = [ 'POST' ])
@me_or_uuid
@@ -87,25 +88,24 @@ def update_clients(uid, user):
app.logger.debug(clients_opts)
for client, opts in clients_opts.iteritems():
- prefs = store.get(ClientPrefs, (user.id, client))
- if not prefs:
+ prefs = user.clients.select(lambda c: c.client_name == client).first()
+ if prefs is None:
continue
if 'delete' in opts and opts['delete'] in [ 'on', 'true', 'checked', 'selected', '1' ]:
- store.remove(prefs)
+ prefs.delete()
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
- store.commit()
flash('Clients preferences updated.')
return user_profile(uid, user)
@app.route('/user//changeusername')
@admin_only
def change_username_form(uid):
- code, user = UserManager.get(store, uid)
+ code, user = UserManager.get(uid)
if code != UserManager.SUCCESS:
flash(UserManager.error_str(code))
return redirect(url_for('index'))
@@ -114,8 +114,9 @@ def change_username_form(uid):
@app.route('/user//changeusername', methods = [ 'POST' ])
@admin_only
+@db_session
def change_username_post(uid):
- code, user = UserManager.get(store, uid)
+ code, user = UserManager.get(uid)
if code != UserManager.SUCCESS:
return redirect(url_for('index'))
@@ -123,7 +124,7 @@ def change_username_post(uid):
if username in ('', None):
flash('The username is required')
return render_template('change_username.html', user = user)
- if user.name != username and store.find(User, User.name == username).one():
+ if user.name != username and User.get(name = username) is not None:
flash('This name is already taken')
return render_template('change_username.html', user = user)
@@ -135,7 +136,6 @@ def change_username_post(uid):
if user.name != username or user.admin != admin:
user.name = username
user.admin = admin
- store.commit()
flash("User '%s' updated." % username)
else:
flash("No changes for '%s'." % username)
@@ -150,10 +150,9 @@ def change_mail_form(uid, user):
@app.route('/user//changemail', methods = [ 'POST' ])
@me_or_uuid
def change_mail_post(uid, user):
- mail = request.form.get('mail')
+ mail = request.form.get('mail', '')
# No validation, lol.
user.mail = mail
- store.commit()
return redirect(url_for('user_profile', uid = uid))
@app.route('/user//changepass')
@@ -182,9 +181,9 @@ def change_password_post(uid, user):
if not error:
if user.id == request.user.id:
- status = UserManager.change_password(store, user.id, current, new)
+ status = UserManager.change_password(user.id, current, new)
else:
- status = UserManager.change_password2(store, user.name, new)
+ status = UserManager.change_password2(user.name, new)
if status != UserManager.SUCCESS:
flash(UserManager.error_str(status))
@@ -214,13 +213,12 @@ def add_user_post():
flash("The passwords don't match.")
error = True
- if admin is None:
- admin = True if store.find(User, User.admin == True).count() == 0 else False
- else:
- admin = True
+ admin = admin is not None
+ if mail is None:
+ mail = ''
if not error:
- status = UserManager.add(store, name, passwd, mail, admin)
+ status = UserManager.add(name, passwd, mail, admin)
if status == UserManager.SUCCESS:
flash("User '%s' successfully added" % name)
return redirect(url_for('user_index'))
@@ -232,7 +230,7 @@ def add_user_post():
@app.route('/user/del/')
@admin_only
def del_user(uid):
- status = UserManager.delete(store, uid)
+ status = UserManager.delete(uid)
if status == UserManager.SUCCESS:
flash('Deleted user')
else:
@@ -250,7 +248,6 @@ def lastfm_reg(uid, user):
lfm = LastFm(app.config['LASTFM'], user, 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', uid = uid))
@@ -260,7 +257,6 @@ def lastfm_reg(uid, user):
def lastfm_unreg(uid, user):
lfm = LastFm(app.config['LASTFM'], user, app.logger)
lfm.unlink_account()
- store.commit()
flash('Unlinked LastFM account')
return redirect(url_for('user_profile', uid = uid))
@@ -284,7 +280,7 @@ def login():
error = True
if not error:
- status, user = UserManager.try_auth(store, name, password)
+ status, user = UserManager.try_auth(name, password)
if status == UserManager.SUCCESS:
session['userid'] = str(user.id)
flash('Logged in!')
diff --git a/supysonic/managers/folder.py b/supysonic/managers/folder.py
index 547a711..f5f04e6 100644
--- a/supysonic/managers/folder.py
+++ b/supysonic/managers/folder.py
@@ -21,6 +21,9 @@
import os.path
import uuid
+from pony.orm import db_session, select
+from pony.orm import ObjectNotFound
+
from ..db import Folder, Artist, Album, Track, StarredFolder, RatingFolder
from ..scanner import Scanner
@@ -34,7 +37,8 @@ class FolderManager:
SUBPATH_EXISTS = 6
@staticmethod
- def get(store, uid):
+ @db_session
+ def get(uid):
if isinstance(uid, basestring):
try:
uid = uuid.UUID(uid)
@@ -45,65 +49,56 @@ class FolderManager:
else:
return FolderManager.INVALID_ID, None
- folder = store.get(Folder, uid)
- if not folder:
+ try:
+ folder = Folder[uid]
+ return FolderManager.SUCCESS, folder
+ except ObjectNotFound:
return FolderManager.NO_SUCH_FOLDER, None
- return FolderManager.SUCCESS, folder
-
@staticmethod
- def add(store, name, path):
- if not store.find(Folder, Folder.name == name, Folder.root == True).is_empty():
+ @db_session
+ def add(name, path):
+ if Folder.get(name = name, root = True) is not None:
return FolderManager.NAME_EXISTS
path = unicode(os.path.abspath(path))
if not os.path.isdir(path):
return FolderManager.INVALID_PATH
- if not store.find(Folder, Folder.path == path).is_empty():
+ if Folder.get(path = path) is not None:
return FolderManager.PATH_EXISTS
- if any(path.startswith(p) for p in store.find(Folder).values(Folder.path)):
+ if any(path.startswith(p) for p in select(f.path for f in Folder)):
return FolderManager.PATH_EXISTS
- if not store.find(Folder, Folder.path.startswith(path)).is_empty():
+ if Folder.exists(lambda f: f.path.startswith(path)):
return FolderManager.SUBPATH_EXISTS
- folder = Folder()
- folder.root = True
- folder.name = name
- folder.path = path
-
- store.add(folder)
- store.commit()
-
+ folder = Folder(root = True, name = name, path = path)
return FolderManager.SUCCESS
@staticmethod
- def delete(store, uid):
- status, folder = FolderManager.get(store, uid)
+ @db_session
+ def delete(uid):
+ status, folder = FolderManager.get(uid)
if status != FolderManager.SUCCESS:
return status
if not folder.root:
return FolderManager.NO_SUCH_FOLDER
- scanner = Scanner(store)
- for track in store.find(Track, Track.root_folder_id == folder.id):
+ scanner = Scanner()
+ for track in Track.select(lambda t: t.root_folder == folder):
scanner.remove_file(track.path)
scanner.finish()
- store.find(StarredFolder, StarredFolder.starred_id == uid).remove()
- store.find(RatingFolder, RatingFolder.rated_id == uid).remove()
-
- store.remove(folder)
- store.commit()
-
+ folder.delete()
return FolderManager.SUCCESS
@staticmethod
- def delete_by_name(store, name):
- folder = store.find(Folder, Folder.name == name, Folder.root == True).one()
+ @db_session
+ def delete_by_name(name):
+ folder = Folder.get(name = name, root = True)
if not folder:
return FolderManager.NO_SUCH_FOLDER
- return FolderManager.delete(store, folder.id)
+ return FolderManager.delete(folder.id)
@staticmethod
def error_str(err):
diff --git a/supysonic/managers/user.py b/supysonic/managers/user.py
index 2431c68..d36d5c0 100644
--- a/supysonic/managers/user.py
+++ b/supysonic/managers/user.py
@@ -14,6 +14,9 @@ import random
import string
import uuid
+from pony.orm import db_session
+from pony.orm import ObjectNotFound
+
from ..db import User, ChatMessage, Playlist
from ..db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
from ..db import RatingFolder, RatingTrack
@@ -26,7 +29,8 @@ class UserManager:
WRONG_PASS = 4
@staticmethod
- def get(store, uid):
+ @db_session
+ def get(uid):
if type(uid) in (str, unicode):
try:
uid = uuid.UUID(uid)
@@ -37,63 +41,53 @@ class UserManager:
else:
return UserManager.INVALID_ID, None
- user = store.get(User, uid)
- if user is None:
+ try:
+ user = User[uid]
+ return UserManager.SUCCESS, user
+ except ObjectNotFound:
return UserManager.NO_SUCH_USER, None
- return UserManager.SUCCESS, user
-
@staticmethod
- def add(store, name, password, mail, admin):
- if store.find(User, User.name == name).one():
+ @db_session
+ def add(name, password, mail, admin):
+ if User.get(name = name) is not None:
return UserManager.NAME_EXISTS
crypt, salt = UserManager.__encrypt_password(password)
- user = User()
- user.name = name
- user.mail = mail
- user.password = crypt
- user.salt = salt
- user.admin = admin
-
- store.add(user)
- store.commit()
+ user = User(
+ name = name,
+ mail = mail,
+ password = crypt,
+ salt = salt,
+ admin = admin
+ )
return UserManager.SUCCESS
@staticmethod
- def delete(store, uid):
- status, user = UserManager.get(store, uid)
+ @db_session
+ def delete(uid):
+ status, user = UserManager.get(uid)
if status != UserManager.SUCCESS:
return status
- store.find(StarredFolder, StarredFolder.user_id == user.id).remove()
- store.find(StarredArtist, StarredArtist.user_id == user.id).remove()
- store.find(StarredAlbum, StarredAlbum.user_id == user.id).remove()
- store.find(StarredTrack, StarredTrack.user_id == user.id).remove()
- store.find(RatingFolder, RatingFolder.user_id == user.id).remove()
- store.find(RatingTrack, RatingTrack.user_id == user.id).remove()
- store.find(ChatMessage, ChatMessage.user_id == user.id).remove()
- for playlist in store.find(Playlist, Playlist.user_id == user.id):
- store.remove(playlist)
-
- store.remove(user)
- store.commit()
-
+ user.delete()
return UserManager.SUCCESS
@staticmethod
- def delete_by_name(store, name):
- user = store.find(User, User.name == name).one()
- if not user:
+ @db_session
+ def delete_by_name(name):
+ user = User.get(name = name)
+ if user is None:
return UserManager.NO_SUCH_USER
- return UserManager.delete(store, user.id)
+ return UserManager.delete(user.id)
@staticmethod
- def try_auth(store, name, password):
- user = store.find(User, User.name == name).one()
- if not user:
+ @db_session
+ def try_auth(name, password):
+ user = User.get(name = name)
+ if user is None:
return UserManager.NO_SUCH_USER, None
elif UserManager.__encrypt_password(password, user.salt)[0] != user.password:
return UserManager.WRONG_PASS, None
@@ -101,8 +95,9 @@ class UserManager:
return UserManager.SUCCESS, user
@staticmethod
- def change_password(store, uid, old_pass, new_pass):
- status, user = UserManager.get(store, uid)
+ @db_session
+ def change_password(uid, old_pass, new_pass):
+ status, user = UserManager.get(uid)
if status != UserManager.SUCCESS:
return status
@@ -110,17 +105,16 @@ class UserManager:
return UserManager.WRONG_PASS
user.password = UserManager.__encrypt_password(new_pass, user.salt)[0]
- store.commit()
return UserManager.SUCCESS
@staticmethod
- def change_password2(store, name, new_pass):
- user = store.find(User, User.name == name).one()
- if not user:
+ @db_session
+ def change_password2(name, new_pass):
+ user = User.get(name = name)
+ if user is None:
return UserManager.NO_SUCH_USER
user.password = UserManager.__encrypt_password(new_pass, user.salt)[0]
- store.commit()
return UserManager.SUCCESS
@staticmethod
diff --git a/supysonic/scanner.py b/supysonic/scanner.py
index 9edff22..5ec590c 100644
--- a/supysonic/scanner.py
+++ b/supysonic/scanner.py
@@ -23,40 +23,17 @@ import mimetypes
import mutagen
import time
-from storm.expr import ComparableExpr, compile, Like
-from storm.exceptions import NotSupportedError
+from pony.orm import db_session
from .db import Folder, Artist, Album, Track, User
from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
from .db import RatingFolder, RatingTrack
-# Hacking in support for a concatenation expression
-class Concat(ComparableExpr):
- __slots__ = ("left", "right", "db")
-
- def __init__(self, left, right, db):
- self.left = left
- self.right = right
- self.db = db
-
-@compile.when(Concat)
-def compile_concat(compile, concat, state):
- left = compile(concat.left, state)
- right = compile(concat.right, state)
- if concat.db in ('sqlite', 'postgres'):
- statement = "%s||%s"
- elif concat.db == 'mysql':
- statement = "CONCAT(%s, %s)"
- else:
- raise NotSupportedError("Unspported database (%s)" % concat.db)
- return statement % (left, right)
-
class Scanner:
- def __init__(self, store, force = False, extensions = None):
+ def __init__(self, force = False, extensions = None):
if extensions is not None and not isinstance(extensions, list):
raise TypeError('Invalid extensions type')
- self.__store = store
self.__force = force
self.__added_artists = 0
@@ -92,8 +69,9 @@ class Scanner:
progress_callback(current, total)
# Remove files that have been deleted
- 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.remove_file(track.path)
+ for track in Track.select(lambda t: t.root_folder == folder):
+ if not self.__is_valid_path(track.path):
+ self.remove_file(track.path)
# Update cover art info
folders = [ folder ]
@@ -104,33 +82,33 @@ class Scanner:
folder.last_scan = int(time.time())
+ @db_session
def finish(self):
- for album in [ a for a in self.__albums_to_check if not a.tracks.count() ]:
- self.__store.find(StarredAlbum, StarredAlbum.starred_id == album.id).remove()
+ for album in Album.select(lambda a: a.id in self.__albums_to_check):
+ if not album.tracks.is_empty():
+ continue
- self.__artists_to_check.add(album.artist)
- self.__store.remove(album)
+ self.__artists_to_check.add(album.artist.id)
self.__deleted_albums += 1
+ album.delete()
self.__albums_to_check.clear()
- for artist in [ a for a in self.__artists_to_check if not a.albums.count() and not a.tracks.count() ]:
- self.__store.find(StarredArtist, StarredArtist.starred_id == artist.id).remove()
+ for artist in Artist.select(lambda a: a.id in self.__artists_to_check):
+ if not artist.albums.is_empty() or not artist.tracks.is_empty():
+ continue
- self.__store.remove(artist)
self.__deleted_artists += 1
+ artist.delete()
self.__artists_to_check.clear()
while self.__folders_to_check:
- folder = self.__folders_to_check.pop()
+ folder = Folder[self.__folders_to_check.pop()]
if folder.root:
continue
- if not folder.tracks.count() and not folder.children.count():
- self.__store.find(StarredFolder, StarredFolder.starred_id == folder.id).remove()
- self.__store.find(RatingFolder, RatingFolder.rated_id == folder.id).remove()
-
- self.__folders_to_check.add(folder.parent)
- self.__store.remove(folder)
+ if folder.tracks.is_empty() and folder.children.is_empty():
+ self.__folders_to_check.add(folder.parent.id)
+ folder.delete()
def __is_valid_path(self, path):
if not os.path.exists(path):
@@ -139,13 +117,13 @@ class Scanner:
return True
return os.path.splitext(path)[1][1:].lower() in self.__extensions
+ @db_session
def scan_file(self, path):
if not isinstance(path, basestring):
raise TypeError('Expecting string, got ' + str(type(path)))
- tr = self.__store.find(Track, Track.path == path).one()
- add = False
- if tr:
+ tr = Track.get(path = path)
+ if tr is not None:
if not self.__force and not int(os.path.getmtime(path)) > tr.last_modification:
return
@@ -153,74 +131,70 @@ class Scanner:
if not tag:
self.remove_file(path)
return
+ trdict = {}
else:
tag = self.__try_load_tag(path)
if not tag:
return
- tr = Track()
- tr.path = path
- add = True
+ trdict = { 'path': path }
- artist = self.__try_read_tag(tag, 'artist', '')
- album = self.__try_read_tag(tag, 'album', '')
+ artist = self.__try_read_tag(tag, 'artist')
+ if not artist:
+ return
+
+ album = self.__try_read_tag(tag, 'album', '[non-album tracks]')
albumartist = self.__try_read_tag(tag, 'albumartist', artist)
- 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.title = self.__try_read_tag(tag, 'title', '')
- 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)
+ trdict['disc'] = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x[0].split('/')[0]))
+ trdict['number'] = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x[0].split('/')[0]))
+ trdict['title'] = self.__try_read_tag(tag, 'title', '')
+ trdict['year'] = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0]))
+ trdict['genre'] = self.__try_read_tag(tag, 'genre')
+ trdict['duration'] = int(tag.info.length)
- tr.bitrate = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) / 1000
- tr.content_type = mimetypes.guess_type(path, False)[0] or 'application/octet-stream'
- tr.last_modification = os.path.getmtime(path)
+ trdict['bitrate'] = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) / 1000
+ trdict['content_type'] = mimetypes.guess_type(path, False)[0] or 'application/octet-stream'
+ trdict['last_modification'] = int(os.path.getmtime(path))
tralbum = self.__find_album(albumartist, album)
trartist = self.__find_artist(artist)
- if add:
- trroot = self.__find_root_folder(path)
- trfolder = self.__find_folder(path)
+ if tr is None:
+ trdict['root_folder'] = self.__find_root_folder(path)
+ trdict['folder'] = self.__find_folder(path)
+ trdict['album'] = tralbum
+ trdict['artist'] = trartist
- # 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.artist = trartist
- tr.folder = trfolder
- tr.root_folder = trroot
-
- self.__store.add(tr)
+ Track(**trdict)
self.__added_tracks += 1
else:
if tr.album.id != tralbum.id:
- self.__albums_to_check.add(tr.album)
- tr.album = tralbum
+ self.__albums_to_check.add(tr.album.id)
+ trdict['album'] = tralbum
if tr.artist.id != trartist.id:
- self.__artists_to_check.add(tr.artist)
- tr.artist = trartist
+ self.__artists_to_check.add(tr.artist.id)
+ trdict['artist'] = trartist
+ tr.set(**trdict)
+
+ @db_session
def remove_file(self, path):
if not isinstance(path, basestring):
raise TypeError('Expecting string, got ' + str(type(path)))
- tr = self.__store.find(Track, Track.path == path).one()
+ tr = Track.get(path = path)
if not tr:
return
- self.__store.find(StarredTrack, StarredTrack.starred_id == tr.id).remove()
- self.__store.find(RatingTrack, RatingTrack.rated_id == tr.id).remove()
- # Playlist autofix themselves
- self.__store.find(User, User.last_play_id == tr.id).set(last_play_id = None)
-
- self.__folders_to_check.add(tr.folder)
- self.__albums_to_check.add(tr.album)
- self.__artists_to_check.add(tr.artist)
- self.__store.remove(tr)
+ self.__folders_to_check.add(tr.folder.id)
+ self.__albums_to_check.add(tr.album.id)
+ self.__artists_to_check.add(tr.artist.id)
self.__deleted_tracks += 1
+ tr.delete()
+ @db_session
def move_file(self, src_path, dst_path):
if not isinstance(src_path, basestring):
raise TypeError('Expecting string, got ' + str(type(src_path)))
@@ -230,16 +204,18 @@ class Scanner:
if src_path == dst_path:
return
- tr = self.__store.find(Track, Track.path == src_path).one()
- if not tr:
+ tr = Track.get(path = src_path)
+ if tr is None:
return
- self.__folders_to_check.add(tr.folder)
- tr_dst = self.__store.find(Track, Track.path == dst_path).one()
- if tr_dst:
- tr.root_folder = tr_dst.root_folder
- tr.folder = tr_dst.folder
+ self.__folders_to_check.add(tr.folder.id)
+ tr_dst = Track.get(path = dst_path)
+ if tr_dst is not None:
+ root = tr_dst.root_folder
+ folder = tr_dst.folder
self.remove_file(dst_path)
+ tr.root_folder = root
+ tr.folder = folder
else:
root = self.__find_root_folder(dst_path)
folder = self.__find_folder(dst_path)
@@ -249,70 +225,48 @@ class Scanner:
def __find_album(self, artist, album):
ar = self.__find_artist(artist)
- al = ar.albums.find(name = album).one()
+ al = ar.albums.select(lambda a: a.name == album).first()
if al:
return al
- al = Album()
- al.name = album
- al.artist = ar
-
- self.__store.add(al)
+ al = Album(name = album, artist = ar)
self.__added_albums += 1
return al
def __find_artist(self, artist):
- ar = self.__store.find(Artist, Artist.name == artist).one()
+ ar = Artist.get(name = artist)
if ar:
return ar
- ar = Artist()
- ar.name = artist
-
- self.__store.add(ar)
+ ar = Artist(name = artist)
self.__added_artists += 1
return ar
def __find_root_folder(self, path):
path = os.path.dirname(path)
- db = self.__store.get_database().__module__[len('storm.databases.'):]
- folders = self.__store.find(Folder, Like(path, Concat(Folder.path, u'%', db)), Folder.root == True)
- count = folders.count()
- if count > 1:
- raise Exception("Found multiple root folders for '{}'.".format(path))
- elif count == 0:
- raise Exception("Couldn't find the root folder for '{}'.\nDon't scan files that aren't located in a defined music folder".format(path))
- return folders.one()
+ for folder in Folder.select(lambda f: f.root):
+ if path.startswith(folder.path):
+ return folder
+
+ raise Exception("Couldn't find the root folder for '{}'.\nDon't scan files that aren't located in a defined music folder".format(path))
def __find_folder(self, path):
+ children = []
+ drive, _ = os.path.splitdrive(path)
path = os.path.dirname(path)
- folders = self.__store.find(Folder, Folder.path == path)
- count = folders.count()
- if count > 1:
- raise Exception("Found multiple folders for '{}'.".format(path))
- elif count == 1:
- return folders.one()
+ while path != drive and path != '/':
+ folder = Folder.get(path = path)
+ if folder is not None:
+ break
- db = self.__store.get_database().__module__[len('storm.databases.'):]
- folder = self.__store.find(Folder, Like(path, Concat(Folder.path, os.sep + u'%', db))).order_by(Folder.path).last()
+ children.append(dict(root = False, name = os.path.basename(path), path = path))
+ path = os.path.dirname(path)
- 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)
-
- fold = Folder()
- fold.root = False
- fold.name = name
- fold.path = full_path
- fold.parent = folder
-
- self.__store.add(fold)
-
- folder = fold
+ assert folder is not None
+ while children:
+ folder = Folder(parent = folder, **children.pop())
return folder
diff --git a/supysonic/watcher.py b/supysonic/watcher.py
index d50a45b..4a0d90c 100644
--- a/supysonic/watcher.py
+++ b/supysonic/watcher.py
@@ -22,12 +22,13 @@ import logging
import time
from logging.handlers import TimedRotatingFileHandler
+from pony.orm import db_session
from signal import signal, SIGTERM, SIGINT
from threading import Thread, Condition, Timer
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
-from . import db
+from .db import init_database, release_database, Folder
from .scanner import Scanner
OP_SCAN = 1
@@ -109,12 +110,11 @@ class Event(object):
return self.__src
class ScannerProcessingQueue(Thread):
- def __init__(self, database_uri, delay, logger):
+ def __init__(self, delay, logger):
super(ScannerProcessingQueue, self).__init__()
self.__logger = logger
self.__timeout = delay
- self.__database_uri = database_uri
self.__cond = Condition()
self.__timer = None
self.__queue = {}
@@ -138,8 +138,7 @@ class ScannerProcessingQueue(Thread):
continue
self.__logger.debug("Instantiating scanner")
- store = db.get_store(self.__database_uri)
- scanner = Scanner(store)
+ scanner = Scanner()
item = self.__next_item()
while item:
@@ -155,8 +154,6 @@ class ScannerProcessingQueue(Thread):
item = self.__next_item()
scanner.finish()
- store.commit()
- store.close()
self.__logger.debug("Freeing scanner")
del scanner
@@ -208,6 +205,7 @@ class SupysonicWatcher(object):
def __init__(self, config):
self.__config = config
self.__running = True
+ init_database(config.BASE['database_uri'])
def run(self):
logger = logging.getLogger(__name__)
@@ -227,22 +225,22 @@ class SupysonicWatcher(object):
}
logger.setLevel(mapping.get(self.__config.DAEMON['log_level'].upper(), logging.NOTSET))
- store = db.get_store(self.__config.BASE['database_uri'])
- folders = store.find(db.Folder, db.Folder.root == True)
-
- if not folders.count():
+ with db_session:
+ folders = Folder.select(lambda f: f.root)
+ shouldrun = folders.exists()
+ if not shouldrun:
logger.info("No folder set. Exiting.")
- store.close()
+ release_database()
return
- queue = ScannerProcessingQueue(self.__config.BASE['database_uri'], self.__config.DAEMON['wait_delay'], logger)
+ queue = ScannerProcessingQueue(self.__config.DAEMON['wait_delay'], logger)
handler = SupysonicWatcherEventHandler(self.__config.BASE['scanner_extensions'], queue, logger)
observer = Observer()
- for folder in folders:
- logger.info("Starting watcher for %s", folder.path)
- observer.schedule(handler, folder.path, recursive = True)
- store.close()
+ with db_session:
+ for folder in folders:
+ logger.info("Starting watcher for %s", folder.path)
+ observer.schedule(handler, folder.path, recursive = True)
try:
signal(SIGTERM, self.__terminate)
@@ -260,6 +258,7 @@ class SupysonicWatcher(object):
observer.join()
queue.stop()
queue.join()
+ release_database()
def stop(self):
self.__running = False
diff --git a/supysonic/web.py b/supysonic/web.py
index 266a227..3d53b83 100644
--- a/supysonic/web.py
+++ b/supysonic/web.py
@@ -11,25 +11,11 @@
import mimetypes
-from flask import Flask, g, current_app
+from flask import Flask
from os import makedirs, path
-from werkzeug.local import LocalProxy
from .config import IniConfig
-from .db import get_store
-
-# Supysonic database open
-def get_db():
- if not hasattr(g, 'database'):
- g.database = get_store(current_app.config['BASE']['database_uri'])
- return g.database
-
-# Supysonic database close
-def close_db(error):
- if hasattr(g, 'database'):
- g.database.close()
-
-store = LocalProxy(get_db)
+from .db import init_database, release_database
def create_application(config = None):
global app
@@ -42,9 +28,6 @@ def create_application(config = None):
config = IniConfig.from_common_locations()
app.config.from_object(config)
- # Close database connection on teardown
- app.teardown_appcontext(close_db)
-
# Set loglevel
logfile = app.config['WEBAPP']['log_file']
if logfile:
@@ -63,6 +46,9 @@ def create_application(config = None):
handler.setLevel(mapping.get(loglevel.upper(), logging.NOTSET))
app.logger.addHandler(handler)
+ # Initialize database
+ init_database(app.config['BASE']['database_uri'])
+
# Insert unknown mimetypes
for k, v in app.config['MIMETYPES'].iteritems():
extension = '.' + k.lower()
diff --git a/tests/__init__.py b/tests/__init__.py
index 80a348f..7087cc1 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -11,7 +11,10 @@
import unittest
-from . import base, managers, api, frontend
+from . import base
+from . import managers
+from . import api
+from . import frontend
def suite():
suite = unittest.TestSuite()
diff --git a/tests/api/test_album_songs.py b/tests/api/test_album_songs.py
index 7319041..b205bde 100644
--- a/tests/api/test_album_songs.py
+++ b/tests/api/test_album_songs.py
@@ -11,6 +11,8 @@
import uuid
+from pony.orm import db_session
+
from supysonic.db import Folder, Artist, Album, Track
from .apitestbase import ApiTestBase
@@ -22,34 +24,25 @@ class AlbumSongsTestCase(ApiTestBase):
def setUp(self):
super(AlbumSongsTestCase, self).setUp()
- folder = Folder()
- folder.name = 'Root'
- folder.root = True
- folder.path = 'tests/assets'
+ with db_session:
+ folder = Folder(name = 'Root', root = True, path = 'tests/assets')
+ artist = Artist(name = 'Artist')
+ album = Album(name = 'Album', artist = artist)
- artist = Artist()
- artist.name = 'Artist'
-
- album = Album()
- album.name = 'Album'
- album.artist = artist
-
- track = Track()
- track.title = 'Track'
- track.album = album
- track.artist = artist
- track.disc = 1
- track.number = 1
- track.path = 'tests/assets/empty'
- track.folder = folder
- track.root_folder = folder
- track.duration = 2
- track.bitrate = 320
- track.content_type = 'audio/mpeg'
- track.last_modification = 0
-
- self.store.add(track)
- self.store.commit()
+ track = Track(
+ title = 'Track',
+ album = album,
+ artist = artist,
+ disc = 1,
+ number = 1,
+ path = 'tests/assets/empty',
+ folder = folder,
+ root_folder = folder,
+ duration = 2,
+ bitrate = 320,
+ content_type = 'audio/mpeg',
+ last_modification = 0
+ )
def test_get_album_list(self):
self._make_request('getAlbumList', error = 10)
@@ -63,11 +56,9 @@ class AlbumSongsTestCase(ApiTestBase):
self._make_request('getAlbumList', { 'type': t }, tag = 'albumList', skip_post = True)
rv, child = self._make_request('getAlbumList', { 'type': 'random' }, tag = 'albumList', skip_post = True)
- self.assertEqual(len(child), 10)
- rv, child = self._make_request('getAlbumList', { 'type': 'random', 'size': 3 }, tag = 'albumList', skip_post = True)
- self.assertEqual(len(child), 3)
- self.store.remove(self.store.find(Folder).one())
+ with db_session:
+ Folder.get().delete()
rv, child = self._make_request('getAlbumList', { 'type': 'random' }, tag = 'albumList')
self.assertEqual(len(child), 0)
@@ -82,12 +73,10 @@ class AlbumSongsTestCase(ApiTestBase):
self._make_request('getAlbumList2', { 'type': t }, tag = 'albumList2', skip_post = True)
rv, child = self._make_request('getAlbumList2', { 'type': 'random' }, tag = 'albumList2', skip_post = True)
- self.assertEqual(len(child), 10)
- rv, child = self._make_request('getAlbumList2', { 'type': 'random', 'size': 3 }, tag = 'albumList2', skip_post = True)
- self.assertEqual(len(child), 3)
- self.store.remove(self.store.find(Track).one())
- self.store.remove(self.store.find(Album).one())
+ with db_session:
+ Track.get().delete()
+ Album.get().delete()
rv, child = self._make_request('getAlbumList2', { 'type': 'random' }, tag = 'albumList2')
self.assertEqual(len(child), 0)
@@ -98,12 +87,10 @@ class AlbumSongsTestCase(ApiTestBase):
self._make_request('getRandomSongs', { 'musicFolderId': 'idid' }, error = 0)
self._make_request('getRandomSongs', { 'musicFolderId': uuid.uuid4() }, error = 70)
- rv, child = self._make_request('getRandomSongs', tag = 'randomSongs')
- self.assertEqual(len(child), 10)
- rv, child = self._make_request('getRandomSongs', { 'size': 3 }, tag = 'randomSongs')
- self.assertEqual(len(child), 3)
+ rv, child = self._make_request('getRandomSongs', tag = 'randomSongs', skip_post = True)
- fid = self.store.find(Folder).one().id
+ with db_session:
+ fid = Folder.get().id
self._make_request('getRandomSongs', { 'fromYear': -52, 'toYear': '1984', 'genre': 'some cryptic subgenre youve never heard of', 'musicFolderId': fid }, tag = 'randomSongs')
def test_now_playing(self):
diff --git a/tests/api/test_annotation.py b/tests/api/test_annotation.py
index ed74587..0412714 100644
--- a/tests/api/test_annotation.py
+++ b/tests/api/test_annotation.py
@@ -11,6 +11,8 @@
import uuid
+from pony.orm import db_session
+
from supysonic.db import Folder, Artist, Album, Track, User, ClientPrefs
from .apitestbase import ApiTestBase
@@ -19,45 +21,32 @@ class AnnotationTestCase(ApiTestBase):
def setUp(self):
super(AnnotationTestCase, self).setUp()
- root = Folder()
- root.name = 'Root'
- root.root = True
- root.path = 'tests/assets'
+ with db_session:
+ root = Folder(name = 'Root', root = True, path = 'tests')
+ folder = Folder(name = 'Folder', path = 'tests/assets', parent = root)
+ artist = Artist(name = 'Artist')
+ album = Album(name = 'Album', artist = artist)
- folder = Folder()
- folder.name = 'Folder'
- folder.path = 'tests/assets'
- folder.parent = root
+ track = Track(
+ title = 'Track',
+ album = album,
+ artist = artist,
+ disc = 1,
+ number = 1,
+ path = 'tests/assets/empty',
+ folder = folder,
+ root_folder = root,
+ duration = 2,
+ bitrate = 320,
+ content_type = 'audio/mpeg',
+ last_modification = 0
+ )
- artist = Artist()
- artist.name = 'Artist'
-
- album = Album()
- album.name = 'Album'
- album.artist = artist
-
- track = Track()
- track.title = 'Track'
- track.album = album
- track.artist = artist
- track.disc = 1
- track.number = 1
- track.path = 'tests/assets/empty'
- track.folder = folder
- track.root_folder = root
- track.duration = 2
- track.bitrate = 320
- track.content_type = 'audio/mpeg'
- track.last_modification = 0
-
- self.store.add(track)
- self.store.commit()
-
- self.folder = folder
- self.artist = artist
- self.album = album
- self.track = track
- self.user = self.store.find(User, User.name == 'alice').one()
+ self.folderid = folder.id
+ self.artistid = artist.id
+ self.albumid = album.id
+ self.trackid = track.id
+ self.user = User.get(name = 'alice')
def test_star(self):
self._make_request('star', error = 10)
@@ -68,88 +57,101 @@ class AnnotationTestCase(ApiTestBase):
self._make_request('star', { 'albumId': str(uuid.uuid4()) }, error = 70)
self._make_request('star', { 'artistId': str(uuid.uuid4()) }, error = 70)
- self._make_request('star', { 'id': str(self.artist.id) }, error = 70, skip_xsd = True)
- self._make_request('star', { 'id': str(self.album.id) }, error = 70, skip_xsd = True)
- self._make_request('star', { 'id': str(self.track.id) }, skip_post = True)
- self.assertIn('starred', self.track.as_subsonic_child(self.user, ClientPrefs()))
- self._make_request('star', { 'id': str(self.track.id) }, error = 0, skip_xsd = True)
+ self._make_request('star', { 'id': str(self.artistid) }, error = 70, skip_xsd = True)
+ self._make_request('star', { 'id': str(self.albumid) }, error = 70, skip_xsd = True)
+ self._make_request('star', { 'id': str(self.trackid) }, skip_post = True)
+ with db_session:
+ self.assertIn('starred', Track[self.trackid].as_subsonic_child(self.user, 'tests'))
+ self._make_request('star', { 'id': str(self.trackid) }, error = 0, skip_xsd = True)
- self._make_request('star', { 'id': str(self.folder.id) }, skip_post = True)
- self.assertIn('starred', self.folder.as_subsonic_child(self.user))
- self._make_request('star', { 'id': str(self.folder.id) }, error = 0, skip_xsd = True)
+ self._make_request('star', { 'id': str(self.folderid) }, skip_post = True)
+ with db_session:
+ self.assertIn('starred', Folder[self.folderid].as_subsonic_child(self.user))
+ self._make_request('star', { 'id': str(self.folderid) }, error = 0, skip_xsd = True)
- self._make_request('star', { 'albumId': str(self.folder.id) }, error = 70)
- self._make_request('star', { 'albumId': str(self.artist.id) }, error = 70)
- self._make_request('star', { 'albumId': str(self.track.id) }, error = 70)
- self._make_request('star', { 'albumId': str(self.album.id) }, skip_post = True)
- self.assertIn('starred', self.album.as_subsonic_album(self.user))
- self._make_request('star', { 'albumId': str(self.album.id) }, error = 0)
+ self._make_request('star', { 'albumId': str(self.folderid) }, error = 70)
+ self._make_request('star', { 'albumId': str(self.artistid) }, error = 70)
+ self._make_request('star', { 'albumId': str(self.trackid) }, error = 70)
+ self._make_request('star', { 'albumId': str(self.albumid) }, skip_post = True)
+ with db_session:
+ self.assertIn('starred', Album[self.albumid].as_subsonic_album(self.user))
+ self._make_request('star', { 'albumId': str(self.albumid) }, error = 0)
- self._make_request('star', { 'artistId': str(self.folder.id) }, error = 70)
- self._make_request('star', { 'artistId': str(self.album.id) }, error = 70)
- self._make_request('star', { 'artistId': str(self.track.id) }, error = 70)
- self._make_request('star', { 'artistId': str(self.artist.id) }, skip_post = True)
- self.assertIn('starred', self.artist.as_subsonic_artist(self.user))
- self._make_request('star', { 'artistId': str(self.artist.id) }, error = 0)
+ self._make_request('star', { 'artistId': str(self.folderid) }, error = 70)
+ self._make_request('star', { 'artistId': str(self.albumid) }, error = 70)
+ self._make_request('star', { 'artistId': str(self.trackid) }, error = 70)
+ self._make_request('star', { 'artistId': str(self.artistid) }, skip_post = True)
+ with db_session:
+ self.assertIn('starred', Artist[self.artistid].as_subsonic_artist(self.user))
+ self._make_request('star', { 'artistId': str(self.artistid) }, error = 0)
def test_unstar(self):
- self._make_request('star', { 'id': [ str(self.folder.id), str(self.track.id) ], 'artistId': str(self.artist.id), 'albumId': str(self.album.id) }, skip_post = True)
+ self._make_request('star', { 'id': [ str(self.folderid), str(self.trackid) ], 'artistId': str(self.artistid), 'albumId': str(self.albumid) }, skip_post = True)
self._make_request('unstar', error = 10)
self._make_request('unstar', { 'id': 'unknown' }, error = 0, skip_xsd = True)
self._make_request('unstar', { 'albumId': 'unknown' }, error = 0)
self._make_request('unstar', { 'artistId': 'unknown' }, error = 0)
- self._make_request('unstar', { 'id': str(self.track.id) }, skip_post = True)
- self.assertNotIn('starred', self.track.as_subsonic_child(self.user, ClientPrefs()))
+ self._make_request('unstar', { 'id': str(self.trackid) }, skip_post = True)
+ with db_session:
+ self.assertNotIn('starred', Track[self.trackid].as_subsonic_child(self.user, 'tests'))
- self._make_request('unstar', { 'id': str(self.folder.id) }, skip_post = True)
- self.assertNotIn('starred', self.folder.as_subsonic_child(self.user))
+ self._make_request('unstar', { 'id': str(self.folderid) }, skip_post = True)
+ with db_session:
+ self.assertNotIn('starred', Folder[self.folderid].as_subsonic_child(self.user))
- self._make_request('unstar', { 'albumId': str(self.album.id) }, skip_post = True)
- self.assertNotIn('starred', self.album.as_subsonic_album(self.user))
+ self._make_request('unstar', { 'albumId': str(self.albumid) }, skip_post = True)
+ with db_session:
+ self.assertNotIn('starred', Album[self.albumid].as_subsonic_album(self.user))
- self._make_request('unstar', { 'artistId': str(self.artist.id) }, skip_post = True)
- self.assertNotIn('starred', self.artist.as_subsonic_artist(self.user))
+ self._make_request('unstar', { 'artistId': str(self.artistid) }, skip_post = True)
+ with db_session:
+ self.assertNotIn('starred', Artist[self.artistid].as_subsonic_artist(self.user))
def test_set_rating(self):
self._make_request('setRating', error = 10)
- self._make_request('setRating', { 'id': str(self.track.id) }, error = 10)
+ self._make_request('setRating', { 'id': str(self.trackid) }, error = 10)
self._make_request('setRating', { 'rating': 3 }, error = 10)
self._make_request('setRating', { 'id': 'string', 'rating': 3 }, error = 0)
self._make_request('setRating', { 'id': str(uuid.uuid4()), 'rating': 3 }, error = 70)
- self._make_request('setRating', { 'id': str(self.artist.id), 'rating': 3 }, error = 70)
- self._make_request('setRating', { 'id': str(self.album.id), 'rating': 3 }, error = 70)
- self._make_request('setRating', { 'id': str(self.track.id), 'rating': 'string' }, error = 0)
- self._make_request('setRating', { 'id': str(self.track.id), 'rating': -1 }, error = 0)
- self._make_request('setRating', { 'id': str(self.track.id), 'rating': 6 }, error = 0)
+ self._make_request('setRating', { 'id': str(self.artistid), 'rating': 3 }, error = 70)
+ self._make_request('setRating', { 'id': str(self.albumid), 'rating': 3 }, error = 70)
+ self._make_request('setRating', { 'id': str(self.trackid), 'rating': 'string' }, error = 0)
+ self._make_request('setRating', { 'id': str(self.trackid), 'rating': -1 }, error = 0)
+ self._make_request('setRating', { 'id': str(self.trackid), 'rating': 6 }, error = 0)
- prefs = ClientPrefs()
- self.assertNotIn('userRating', self.track.as_subsonic_child(self.user, prefs))
+ with db_session:
+ self.assertNotIn('userRating', Track[self.trackid].as_subsonic_child(self.user, 'tests'))
for i in range(1, 6):
- self._make_request('setRating', { 'id': str(self.track.id), 'rating': i }, skip_post = True)
- self.assertEqual(self.track.as_subsonic_child(self.user, prefs)['userRating'], i)
- self._make_request('setRating', { 'id': str(self.track.id), 'rating': 0 }, skip_post = True)
- self.assertNotIn('userRating', self.track.as_subsonic_child(self.user, prefs))
+ self._make_request('setRating', { 'id': str(self.trackid), 'rating': i }, skip_post = True)
+ with db_session:
+ self.assertEqual(Track[self.trackid].as_subsonic_child(self.user, 'tests')['userRating'], i)
- self.assertNotIn('userRating', self.folder.as_subsonic_child(self.user))
+ self._make_request('setRating', { 'id': str(self.trackid), 'rating': 0 }, skip_post = True)
+ with db_session:
+ self.assertNotIn('userRating', Track[self.trackid].as_subsonic_child(self.user, 'tests'))
+
+ self.assertNotIn('userRating', Folder[self.folderid].as_subsonic_child(self.user))
for i in range(1, 6):
- self._make_request('setRating', { 'id': str(self.folder.id), 'rating': i }, skip_post = True)
- self.assertEqual(self.folder.as_subsonic_child(self.user)['userRating'], i)
- self._make_request('setRating', { 'id': str(self.folder.id), 'rating': 0 }, skip_post = True)
- self.assertNotIn('userRating', self.folder.as_subsonic_child(self.user))
+ self._make_request('setRating', { 'id': str(self.folderid), 'rating': i }, skip_post = True)
+ with db_session:
+ self.assertEqual(Folder[self.folderid].as_subsonic_child(self.user)['userRating'], i)
+ self._make_request('setRating', { 'id': str(self.folderid), 'rating': 0 }, skip_post = True)
+ with db_session:
+ self.assertNotIn('userRating', Folder[self.folderid].as_subsonic_child(self.user))
def test_scrobble(self):
self._make_request('scrobble', error = 10)
self._make_request('scrobble', { 'id': 'song' }, error = 0)
self._make_request('scrobble', { 'id': str(uuid.uuid4()) }, error = 70)
- self._make_request('scrobble', { 'id': str(self.folder.id) }, error = 70)
+ self._make_request('scrobble', { 'id': str(self.folderid) }, error = 70)
self.skipTest('Weird request context/logger issue at exit')
- self._make_request('scrobble', { 'id': str(self.track.id) })
- self._make_request('scrobble', { 'id': str(self.track.id), 'submission': True })
- self._make_request('scrobble', { 'id': str(self.track.id), 'submission': False })
+ self._make_request('scrobble', { 'id': str(self.trackid) })
+ self._make_request('scrobble', { 'id': str(self.trackid), 'submission': True })
+ self._make_request('scrobble', { 'id': str(self.trackid), 'submission': False })
if __name__ == '__main__':
unittest.main()
diff --git a/tests/api/test_browse.py b/tests/api/test_browse.py
index e026a0b..b42891b 100644
--- a/tests/api/test_browse.py
+++ b/tests/api/test_browse.py
@@ -9,10 +9,12 @@
#
# Distributed under terms of the GNU AGPLv3 license.
-from lxml import etree
import time
import uuid
+from lxml import etree
+from pony.orm import db_session
+
from supysonic.db import Folder, Artist, Album, Track
from .apitestbase import ApiTestBase
@@ -21,64 +23,53 @@ class BrowseTestCase(ApiTestBase):
def setUp(self):
super(BrowseTestCase, self).setUp()
- empty = Folder()
- empty.root = True
- empty.name = 'Empty root'
- empty.path = '/tmp'
- self.store.add(empty)
+ with db_session:
+ Folder(root = True, name = 'Empty root', path = '/tmp')
+ root = Folder(root = True, name = 'Root folder', path = 'tests/assets')
- root = Folder()
- root.root = True
- root.name = 'Root folder'
- root.path = 'tests/assets'
- self.store.add(root)
+ for letter in 'ABC':
+ folder = Folder(
+ name = letter + 'rtist',
+ path = 'tests/assets/{}rtist'.format(letter),
+ parent = root
+ )
- for letter in 'ABC':
- folder = Folder()
- folder.name = letter + 'rtist'
- folder.path = 'tests/assets/{}rtist'.format(letter)
- folder.parent = root
+ artist = Artist(name = letter + 'rtist')
- artist = Artist()
- artist.name = letter + 'rtist'
+ for lether in 'AB':
+ afolder = Folder(
+ name = letter + lether + 'lbum',
+ path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether),
+ parent = folder
+ )
- for lether in 'AB':
- afolder = Folder()
- afolder.name = letter + lether + 'lbum'
- afolder.path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether)
- afolder.parent = folder
+ album = Album(name = letter + lether + 'lbum', artist = artist)
- album = Album()
- album.name = letter + lether + 'lbum'
- album.artist = artist
+ for num, song in enumerate([ 'One', 'Two', 'Three' ]):
+ track = Track(
+ disc = 1,
+ number = num,
+ title = song,
+ duration = 2,
+ album = album,
+ artist = artist,
+ bitrate = 320,
+ path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song),
+ content_type = 'audio/mpeg',
+ last_modification = 0,
+ root_folder = root,
+ folder = afolder
+ )
- for num, song in enumerate([ 'One', 'Two', 'Three' ]):
- track = Track()
- track.disc = 1
- track.number = num
- track.title = song
- track.duration = 2
- track.album = album
- track.artist = artist
- track.bitrate = 320
- track.path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song)
- track.content_type = 'audio/mpeg'
- track.last_modification = 0
- track.root_folder = root
- track.folder = afolder
- self.store.add(track)
-
- self.store.commit()
-
- self.assertEqual(self.store.find(Folder).count(), 11)
- self.assertEqual(self.store.find(Folder, Folder.root == True).count(), 2)
- self.assertEqual(self.store.find(Artist).count(), 3)
- self.assertEqual(self.store.find(Album).count(), 6)
- self.assertEqual(self.store.find(Track).count(), 18)
+ self.assertEqual(Folder.select().count(), 11)
+ self.assertEqual(Folder.select(lambda f: f.root).count(), 2)
+ self.assertEqual(Artist.select().count(), 3)
+ self.assertEqual(Album.select().count(), 6)
+ self.assertEqual(Track.select().count(), 18)
def test_get_music_folders(self):
# Do not validate against the XSD here, this is the only place where the API should return ids as ints
- # all our ids are uuids :/
+ # all our ids are uuids :/
rv, child = self._make_request('getMusicFolders', tag = 'musicFolders', skip_xsd = True)
self.assertEqual(len(child), 2)
self.assertSequenceEqual(sorted(self._xpath(child, './musicFolder/@name')), [ 'Empty root', 'Root folder' ])
@@ -91,7 +82,8 @@ class BrowseTestCase(ApiTestBase):
rv, child = self._make_request('getIndexes', { 'ifModifiedSince': int(time.time() * 1000 + 1000) }, tag = 'indexes')
self.assertEqual(len(child), 0)
- fid = self.store.find(Folder, Folder.name == 'Empty root').one().id
+ with db_session:
+ fid = Folder.get(name = 'Empty root').id
rv, child = self._make_request('getIndexes', { 'musicFolderId': str(fid) }, tag = 'indexes')
self.assertEqual(len(child), 0)
@@ -108,18 +100,19 @@ class BrowseTestCase(ApiTestBase):
self._make_request('getMusicDirectory', { 'id': str(uuid.uuid4()) }, error = 70)
# should test with folders with both children folders and tracks. this code would break in that case
- for f in self.store.find(Folder):
- rv, child = self._make_request('getMusicDirectory', { 'id': str(f.id) }, tag = 'directory')
- self.assertEqual(child.get('id'), str(f.id))
- self.assertEqual(child.get('name'), f.name)
- self.assertEqual(len(child), f.children.count() + f.tracks.count())
- for dbc, xmlc in zip(sorted(f.children, key = lambda c: c.name), sorted(child, key = lambda c: c.get('title'))):
- self.assertEqual(dbc.name, xmlc.get('title'))
- self.assertEqual(xmlc.get('artist'), f.name)
- self.assertEqual(xmlc.get('parent'), str(f.id))
- for t, xmlc in zip(sorted(f.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))):
- self.assertEqual(t.title, xmlc.get('title'))
- self.assertEqual(xmlc.get('parent'), str(f.id))
+ with db_session:
+ for f in Folder.select():
+ rv, child = self._make_request('getMusicDirectory', { 'id': str(f.id) }, tag = 'directory')
+ self.assertEqual(child.get('id'), str(f.id))
+ self.assertEqual(child.get('name'), f.name)
+ self.assertEqual(len(child), f.children.count() + f.tracks.count())
+ for dbc, xmlc in zip(sorted(f.children, key = lambda c: c.name), sorted(child, key = lambda c: c.get('title'))):
+ self.assertEqual(dbc.name, xmlc.get('title'))
+ self.assertEqual(xmlc.get('artist'), f.name)
+ self.assertEqual(xmlc.get('parent'), str(f.id))
+ for t, xmlc in zip(sorted(f.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))):
+ self.assertEqual(t.title, xmlc.get('title'))
+ self.assertEqual(xmlc.get('parent'), str(f.id))
def test_get_artists(self):
# same as getIndexes standard case
@@ -138,38 +131,41 @@ class BrowseTestCase(ApiTestBase):
self._make_request('getArtist', { 'id': 'artist' }, error = 0)
self._make_request('getArtist', { 'id': str(uuid.uuid4()) }, error = 70)
- for ar in self.store.find(Artist):
- rv, child = self._make_request('getArtist', { 'id': str(ar.id) }, tag = 'artist')
- self.assertEqual(child.get('id'), str(ar.id))
- self.assertEqual(child.get('albumCount'), str(len(child)))
- self.assertEqual(len(child), ar.albums.count())
- for dal, xal in zip(sorted(ar.albums, key = lambda a: a.name), sorted(child, key = lambda c: c.get('name'))):
- self.assertEqual(dal.name, xal.get('name'))
- self.assertEqual(xal.get('artist'), ar.name) # could break with a better dataset
- self.assertEqual(xal.get('artistId'), str(ar.id)) # see above
+ with db_session:
+ for ar in Artist.select():
+ rv, child = self._make_request('getArtist', { 'id': str(ar.id) }, tag = 'artist')
+ self.assertEqual(child.get('id'), str(ar.id))
+ self.assertEqual(child.get('albumCount'), str(len(child)))
+ self.assertEqual(len(child), ar.albums.count())
+ for dal, xal in zip(sorted(ar.albums, key = lambda a: a.name), sorted(child, key = lambda c: c.get('name'))):
+ self.assertEqual(dal.name, xal.get('name'))
+ self.assertEqual(xal.get('artist'), ar.name) # could break with a better dataset
+ self.assertEqual(xal.get('artistId'), str(ar.id)) # see above
def test_get_album(self):
self._make_request('getAlbum', error = 10)
self._make_request('getAlbum', { 'id': 'nastynasty' }, error = 0)
self._make_request('getAlbum', { 'id': str(uuid.uuid4()) }, error = 70)
- a = self.store.find(Album)[0]
- rv, child = self._make_request('getAlbum', { 'id': str(a.id) }, tag = 'album')
- self.assertEqual(child.get('id'), str(a.id))
- self.assertEqual(child.get('songCount'), str(len(child)))
+ with db_session:
+ a = Album.select().first()
+ rv, child = self._make_request('getAlbum', { 'id': str(a.id) }, tag = 'album')
+ self.assertEqual(child.get('id'), str(a.id))
+ self.assertEqual(child.get('songCount'), str(len(child)))
- self.assertEqual(len(child), a.tracks.count())
- for dal, xal in zip(sorted(a.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))):
- self.assertEqual(dal.title, xal.get('title'))
- self.assertEqual(xal.get('album'), a.name)
- self.assertEqual(xal.get('albumId'), str(a.id))
+ self.assertEqual(len(child), a.tracks.count())
+ for dal, xal in zip(sorted(a.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))):
+ self.assertEqual(dal.title, xal.get('title'))
+ self.assertEqual(xal.get('album'), a.name)
+ self.assertEqual(xal.get('albumId'), str(a.id))
def test_get_song(self):
self._make_request('getSong', error = 10)
self._make_request('getSong', { 'id': 'nastynasty' }, error = 0)
self._make_request('getSong', { 'id': str(uuid.uuid4()) }, error = 70)
- s = self.store.find(Track)[0]
+ with db_session:
+ s = Track.select().first()
self._make_request('getSong', { 'id': str(s.id) }, tag = 'song')
def test_get_videos(self):
diff --git a/tests/api/test_media.py b/tests/api/test_media.py
index c2070fa..347f493 100644
--- a/tests/api/test_media.py
+++ b/tests/api/test_media.py
@@ -11,8 +11,10 @@
import os.path
import uuid
+
from io import BytesIO
from PIL import Image
+from pony.orm import db_session
from supysonic.db import Folder, Artist, Album, Track
@@ -22,69 +24,69 @@ class MediaTestCase(ApiTestBase):
def setUp(self):
super(MediaTestCase, self).setUp()
- self.folder = Folder()
- self.folder.name = 'Root'
- self.folder.path = os.path.abspath('tests/assets')
- self.folder.root = True
- self.folder.has_cover_art = True # 420x420 PNG
+ with db_session:
+ folder = Folder(
+ name = 'Root',
+ path = os.path.abspath('tests/assets'),
+ root = True,
+ has_cover_art = True # 420x420 PNG
+ )
+ self.folderid = folder.id
- artist = Artist()
- artist.name = 'Artist'
+ artist = Artist(name = 'Artist')
+ album = Album(artist = artist, name = 'Album')
- album = Album()
- album.artist = artist
- album.name = 'Album'
-
- self.track = Track()
- self.track.title = '23bytes'
- self.track.number = 1
- self.track.disc = 1
- self.track.artist = artist
- self.track.album = album
- self.track.path = os.path.abspath('tests/assets/23bytes')
- self.track.root_folder = self.folder
- self.track.folder = self.folder
- self.track.duration = 2
- self.track.bitrate = 320
- self.track.content_type = 'audio/mpeg'
- self.track.last_modification = 0
-
- self.store.add(self.track)
- self.store.commit()
+ track = Track(
+ title = '23bytes',
+ number = 1,
+ disc = 1,
+ artist = artist,
+ album = album,
+ path = os.path.abspath('tests/assets/23bytes'),
+ root_folder = folder,
+ folder = folder,
+ duration = 2,
+ bitrate = 320,
+ content_type = 'audio/mpeg',
+ last_modification = 0
+ )
+ self.trackid = track.id
def test_stream(self):
self._make_request('stream', error = 10)
self._make_request('stream', { 'id': 'string' }, error = 0)
self._make_request('stream', { 'id': str(uuid.uuid4()) }, error = 70)
- self._make_request('stream', { 'id': str(self.folder.id) }, error = 70)
- self._make_request('stream', { 'id': str(self.track.id), 'maxBitRate': 'string' }, error = 0)
+ self._make_request('stream', { 'id': str(self.folderid) }, error = 70)
+ self._make_request('stream', { 'id': str(self.trackid), 'maxBitRate': 'string' }, error = 0)
- rv = self.client.get('/rest/stream.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.track.id) })
+ rv = self.client.get('/rest/stream.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.trackid) })
self.assertEqual(rv.status_code, 200)
self.assertEqual(rv.mimetype, 'audio/mpeg')
self.assertEqual(len(rv.data), 23)
- self.assertEqual(self.track.play_count, 1)
+ with db_session:
+ self.assertEqual(Track[self.trackid].play_count, 1)
def test_download(self):
self._make_request('download', error = 10)
self._make_request('download', { 'id': 'string' }, error = 0)
self._make_request('download', { 'id': str(uuid.uuid4()) }, error = 70)
- self._make_request('download', { 'id': str(self.folder.id) }, error = 70)
+ self._make_request('download', { 'id': str(self.folderid) }, error = 70)
- rv = self.client.get('/rest/download.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.track.id) })
+ rv = self.client.get('/rest/download.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.trackid) })
self.assertEqual(rv.status_code, 200)
self.assertEqual(rv.mimetype, 'audio/mpeg')
self.assertEqual(len(rv.data), 23)
- self.assertEqual(self.track.play_count, 0)
+ with db_session:
+ self.assertEqual(Track[self.trackid].play_count, 0)
def test_get_cover_art(self):
self._make_request('getCoverArt', error = 10)
self._make_request('getCoverArt', { 'id': 'string' }, error = 0)
self._make_request('getCoverArt', { 'id': str(uuid.uuid4()) }, error = 70)
- self._make_request('getCoverArt', { 'id': str(self.track.id) }, error = 70)
- self._make_request('getCoverArt', { 'id': str(self.folder.id), 'size': 'large' }, error = 0)
+ self._make_request('getCoverArt', { 'id': str(self.trackid) }, error = 70)
+ self._make_request('getCoverArt', { 'id': str(self.folderid), 'size': 'large' }, error = 0)
- args = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.folder.id) }
+ args = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.folderid) }
rv = self.client.get('/rest/getCoverArt.view', query_string = args)
self.assertEqual(rv.status_code, 200)
self.assertEqual(rv.mimetype, 'image/jpeg')
diff --git a/tests/api/test_playlist.py b/tests/api/test_playlist.py
index 3e164b5..0050696 100644
--- a/tests/api/test_playlist.py
+++ b/tests/api/test_playlist.py
@@ -11,6 +11,8 @@
import uuid
+from pony.orm import db_session
+
from supysonic.db import Folder, Artist, Album, Track, Playlist, User
from .apitestbase import ApiTestBase
@@ -19,63 +21,42 @@ class PlaylistTestCase(ApiTestBase):
def setUp(self):
super(PlaylistTestCase, self).setUp()
- root = Folder()
- root.root = True
- root.name = 'Root folder'
- root.path = 'tests/assets'
- self.store.add(root)
+ with db_session:
+ root = Folder(root = True, name = 'Root folder', path = 'tests/assets')
+ artist = Artist(name = 'Artist')
+ album = Album(name = 'Album', artist = artist)
- artist = Artist()
- artist.name = 'Artist'
+ songs = {}
+ for num, song in enumerate([ 'One', 'Two', 'Three', 'Four' ]):
+ track = Track(
+ disc = 1,
+ number = num,
+ title = song,
+ duration = 2,
+ album = album,
+ artist = artist,
+ bitrate = 320,
+ path = 'tests/assets/' + song,
+ content_type = 'audio/mpeg',
+ last_modification = 0,
+ root_folder = root,
+ folder = root
+ )
+ songs[song] = track
- album = Album()
- album.name = 'Album'
- album.artist = artist
+ users = { u.name: u for u in User.select() }
- songs = {}
- for num, song in enumerate([ 'One', 'Two', 'Three', 'Four' ]):
- track = Track()
- track.disc = 1
- track.number = num
- track.title = song
- track.duration = 2
- track.album = album
- track.artist = artist
- track.bitrate = 320
- track.path = 'tests/assets/empty'
- track.content_type = 'audio/mpeg'
- track.last_modification = 0
- track.root_folder = root
- track.folder = root
+ playlist = Playlist(user = users['alice'], name = "Alice's")
+ playlist.add(songs['One'])
+ playlist.add(songs['Three'])
- self.store.add(track)
- songs[song] = track
+ playlist = Playlist(user = users['alice'], public = True, name = "Alice's public")
+ playlist.add(songs['One'])
+ playlist.add(songs['Two'])
- users = { u.name: u for u in self.store.find(User) }
-
- playlist = Playlist()
- playlist.user = users['alice']
- playlist.name = "Alice's"
- playlist.add(songs['One'])
- playlist.add(songs['Three'])
- self.store.add(playlist)
-
- playlist = Playlist()
- playlist.user = users['alice']
- playlist.public = True
- playlist.name = "Alice's public"
- playlist.add(songs['One'])
- playlist.add(songs['Two'])
- self.store.add(playlist)
-
- playlist = Playlist()
- playlist.user = users['bob']
- playlist.name = "Bob's"
- playlist.add(songs['Two'])
- playlist.add(songs['Four'])
- self.store.add(playlist)
-
- self.store.commit()
+ playlist = Playlist(user = users['bob'], name = "Bob's")
+ playlist.add(songs['Two'])
+ playlist.add(songs['Four'])
def test_get_playlists(self):
# get own playlists
@@ -113,7 +94,8 @@ class PlaylistTestCase(ApiTestBase):
self._make_request('getPlaylist', { 'id': str(uuid.uuid4()) }, error = 70)
# other's private from non admin
- playlist = self.store.find(Playlist, Playlist.public == False, Playlist.user_id == User.id, User.name == 'alice').one()
+ with db_session:
+ playlist = Playlist.get(lambda p: not p.public == False and p.user.name == 'alice')
self._make_request('getPlaylist', { 'u': 'bob', 'p': 'B0b', 'id': str(playlist.id) }, error = 50)
# standard
@@ -156,9 +138,11 @@ class PlaylistTestCase(ApiTestBase):
self._make_request('createPlaylist', { 'u': 'bob', 'p': 'B0b', 'playlistId': playlist.get('id') }, error = 50)
# create more useful playlist
- songs = { s.title: str(s.id) for s in self.store.find(Track) }
+ with db_session:
+ songs = { s.title: str(s.id) for s in Track.select() }
self._make_request('createPlaylist', { 'name': 'songs', 'songId': map(lambda s: songs[s], [ 'Three', 'One', 'Two' ]) }, skip_post = True)
- playlist = self.store.find(Playlist, Playlist.name == 'songs').one()
+ with db_session:
+ playlist = Playlist.get(name = 'songs')
self.assertIsNotNone(playlist)
rv, child = self._make_request('getPlaylist', { 'id': str(playlist.id) }, tag = 'playlist')
self.assertEqual(child.get('songCount'), '3')
@@ -174,6 +158,10 @@ class PlaylistTestCase(ApiTestBase):
self.assertEqual(self._xpath(child, 'count(./entry)'), 1)
self.assertEqual(child[0].get('title'), 'Two')
+ @db_session
+ def assertPlaylistCountEqual(self, count):
+ self.assertEqual(Playlist.select().count(), count)
+
def test_delete_playlist(self):
# check params
self._make_request('deletePlaylist', error = 10)
@@ -181,27 +169,30 @@ class PlaylistTestCase(ApiTestBase):
self._make_request('deletePlaylist', { 'id': str(uuid.uuid4()) }, error = 70)
# delete unowned when not admin
- playlist = self.store.find(Playlist, Playlist.user_id == User.id, User.name == 'alice')[0]
+ with db_session:
+ playlist = Playlist.select(lambda p: p.user.name == 'alice').first()
self._make_request('deletePlaylist', { 'u': 'bob', 'p': 'B0b', 'id': str(playlist.id) }, error = 50)
- self.assertEqual(self.store.find(Playlist).count(), 3)
+ self.assertPlaylistCountEqual(3);
# delete owned
self._make_request('deletePlaylist', { 'id': str(playlist.id) }, skip_post = True)
- self.assertEqual(self.store.find(Playlist).count(), 2)
+ self.assertPlaylistCountEqual(2);
self._make_request('deletePlaylist', { 'id': str(playlist.id) }, error = 70)
- self.assertEqual(self.store.find(Playlist).count(), 2)
+ self.assertPlaylistCountEqual(2);
# delete unowned when admin
- playlist = self.store.find(Playlist, Playlist.user_id == User.id, User.name == 'bob').one()
+ with db_session:
+ playlist = Playlist.get(lambda p: p.user.name == 'bob')
self._make_request('deletePlaylist', { 'id': str(playlist.id) }, skip_post = True)
- self.assertEqual(self.store.find(Playlist).count(), 1)
+ self.assertPlaylistCountEqual(1);
def test_update_playlist(self):
self._make_request('updatePlaylist', error = 10)
self._make_request('updatePlaylist', { 'playlistId': 1234 }, error = 0)
self._make_request('updatePlaylist', { 'playlistId': str(uuid.uuid4()) }, error = 70)
- playlist = self.store.find(Playlist, Playlist.user_id == User.id, User.name == 'alice')[0]
+ with db_session:
+ playlist = Playlist.select(lambda p: p.user.name == 'alice').order_by(Playlist.created).first()
pid = str(playlist.id)
self._make_request('updatePlaylist', { 'playlistId': pid, 'songIdToAdd': 'string' }, error = 0)
self._make_request('updatePlaylist', { 'playlistId': pid, 'songIndexToRemove': 'string' }, error = 0)
@@ -226,7 +217,8 @@ class PlaylistTestCase(ApiTestBase):
self.assertEqual(self._xpath(child, 'count(./entry)'), 1)
self.assertEqual(self._find(child, './entry').get('title'), 'Three')
- songs = { s.title: str(s.id) for s in self.store.find(Track) }
+ with db_session:
+ songs = { s.title: str(s.id) for s in Track.select() }
self._make_request('updatePlaylist', { 'playlistId': pid, 'songIdToAdd': [ songs['One'], songs['Two'], songs['Two'] ] }, skip_post = True)
rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist')
diff --git a/tests/api/test_search.py b/tests/api/test_search.py
index 8a52268..2935136 100644
--- a/tests/api/test_search.py
+++ b/tests/api/test_search.py
@@ -12,6 +12,8 @@
import time
import unittest
+from pony.orm import db_session, commit
+
from supysonic.db import Folder, Artist, Album, Track
from .apitestbase import ApiTestBase
@@ -20,53 +22,44 @@ class SearchTestCase(ApiTestBase):
def setUp(self):
super(SearchTestCase, self).setUp()
- root = Folder()
- root.root = True
- root.name = 'Root folder'
- root.path = 'tests/assets'
- self.store.add(root)
+ with db_session:
+ root = Folder(root = True, name = 'Root folder', path = 'tests/assets')
- for letter in 'ABC':
- folder = Folder()
- folder.name = letter + 'rtist'
- folder.path = 'tests/assets/{}rtist'.format(letter)
- folder.parent = root
+ for letter in 'ABC':
+ folder = Folder(name = letter + 'rtist', path = 'tests/assets/{}rtist'.format(letter), parent = root)
+ artist = Artist(name = letter + 'rtist')
- artist = Artist()
- artist.name = letter + 'rtist'
+ for lether in 'AB':
+ afolder = Folder(
+ name = letter + lether + 'lbum',
+ path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether),
+ parent = folder
+ )
- for lether in 'AB':
- afolder = Folder()
- afolder.name = letter + lether + 'lbum'
- afolder.path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether)
- afolder.parent = folder
+ album = Album(name = letter + lether + 'lbum', artist = artist)
- album = Album()
- album.name = letter + lether + 'lbum'
- album.artist = artist
+ for num, song in enumerate([ 'One', 'Two', 'Three' ]):
+ track = Track(
+ disc = 1,
+ number = num,
+ title = song,
+ duration = 2,
+ album = album,
+ artist = artist,
+ bitrate = 320,
+ path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song),
+ content_type = 'audio/mpeg',
+ last_modification = 0,
+ root_folder = root,
+ folder = afolder
+ )
- for num, song in enumerate([ 'One', 'Two', 'Three' ]):
- track = Track()
- track.disc = 1
- track.number = num
- track.title = song
- track.duration = 2
- track.album = album
- track.artist = artist
- track.bitrate = 320
- track.path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song)
- track.content_type = 'audio/mpeg'
- track.last_modification = 0
- track.root_folder = root
- track.folder = afolder
- self.store.add(track)
+ commit()
- self.store.commit()
-
- self.assertEqual(self.store.find(Folder).count(), 10)
- self.assertEqual(self.store.find(Artist).count(), 3)
- self.assertEqual(self.store.find(Album).count(), 6)
- self.assertEqual(self.store.find(Track).count(), 18)
+ self.assertEqual(Folder.select().count(), 10)
+ self.assertEqual(Artist.select().count(), 3)
+ self.assertEqual(Album.select().count(), 6)
+ self.assertEqual(Track.select().count(), 18)
def __track_as_pseudo_unique_str(self, elem):
return elem.get('artist') + elem.get('album') + elem.get('title')
diff --git a/tests/api/test_transcoding.py b/tests/api/test_transcoding.py
index c745cd2..049c44f 100644
--- a/tests/api/test_transcoding.py
+++ b/tests/api/test_transcoding.py
@@ -11,6 +11,8 @@
import unittest
+from pony.orm import db_session
+
from supysonic.db import Folder, Track
from supysonic.managers.folder import FolderManager
from supysonic.scanner import Scanner
@@ -23,12 +25,13 @@ class TranscodingTestCase(ApiTestBase):
super(TranscodingTestCase, self).setUp()
- FolderManager.add(self.store, 'Folder', 'tests/assets/folder')
- scanner = Scanner(self.store)
- scanner.scan(self.store.find(Folder).one())
- scanner.finish()
+ FolderManager.add('Folder', 'tests/assets/folder')
+ scanner = Scanner()
+ with db_session:
+ scanner.scan(Folder.get())
+ scanner.finish()
- self.trackid = self.store.find(Track).one().id
+ self.trackid = Track.get().id
def _stream(self, **kwargs):
kwargs.update({ 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'v': '1.8.0', 'id': self.trackid })
diff --git a/tests/base/test_cli.py b/tests/base/test_cli.py
index ea49e7e..62418c8 100644
--- a/tests/base/test_cli.py
+++ b/tests/base/test_cli.py
@@ -15,27 +15,22 @@ import tempfile
import unittest
from contextlib import contextmanager
+from pony.orm import db_session
from StringIO import StringIO
-from supysonic.db import Folder, User, get_store
+from supysonic.db import Folder, User, init_database, release_database
from supysonic.cli import SupysonicCLI
from ..testbase import TestConfig
class CLITestCase(unittest.TestCase):
- """ Really basic tests. Some even don't check anything but are juste there for coverage """
+ """ Really basic tests. Some even don't check anything but are just there for coverage """
def setUp(self):
conf = TestConfig(False, False)
self.__dbfile = tempfile.mkstemp()[1]
conf.BASE['database_uri'] = 'sqlite:///' + self.__dbfile
- self.__store = get_store(conf.BASE['database_uri'])
-
- with io.open('schema/sqlite.sql', 'r') as sql:
- schema = sql.read()
- for statement in schema.split(';'):
- self.__store.execute(statement)
- self.__store.commit()
+ init_database(conf.BASE['database_uri'], True)
self.__stdout = StringIO()
self.__stderr = StringIO()
@@ -44,7 +39,7 @@ class CLITestCase(unittest.TestCase):
def tearDown(self):
self.__stdout.close()
self.__stderr.close()
- self.__store.close()
+ release_database()
os.unlink(self.__dbfile)
@contextmanager
@@ -59,9 +54,10 @@ class CLITestCase(unittest.TestCase):
with self._tempdir() as d:
self.__cli.onecmd('folder add tmpfolder ' + d)
- f = self.__store.find(Folder).one()
- self.assertIsNotNone(f)
- self.assertEqual(f.path, d)
+ with db_session:
+ f = Folder.select().first()
+ self.assertIsNotNone(f)
+ self.assertEqual(f.path, d)
def test_folder_add_errors(self):
with self._tempdir() as d:
@@ -71,14 +67,17 @@ class CLITestCase(unittest.TestCase):
self.__cli.onecmd('folder add f1 ' + d)
self.__cli.onecmd('folder add f3 /invalid/path')
- self.assertEqual(self.__store.find(Folder).count(), 1)
+ with db_session:
+ self.assertEqual(Folder.select().count(), 1)
def test_folder_delete(self):
with self._tempdir() as d:
self.__cli.onecmd('folder add tmpfolder ' + d)
self.__cli.onecmd('folder delete randomfolder')
self.__cli.onecmd('folder delete tmpfolder')
- self.assertEqual(self.__store.find(Folder).count(), 0)
+
+ with db_session:
+ self.assertEqual(Folder.select().count(), 0)
def test_folder_list(self):
with self._tempdir() as d:
@@ -97,13 +96,17 @@ class CLITestCase(unittest.TestCase):
def test_user_add(self):
self.__cli.onecmd('user add -p Alic3 alice')
self.__cli.onecmd('user add -p alice alice')
- self.assertEqual(self.__store.find(User).count(), 1)
+
+ with db_session:
+ self.assertEqual(User.select().count(), 1)
def test_user_delete(self):
self.__cli.onecmd('user add -p Alic3 alice')
self.__cli.onecmd('user delete alice')
self.__cli.onecmd('user delete bob')
- self.assertEqual(self.__store.find(User).count(), 0)
+
+ with db_session:
+ self.assertEqual(User.select().count(), 0)
def test_user_list(self):
self.__cli.onecmd('user add -p Alic3 alice')
@@ -114,7 +117,8 @@ class CLITestCase(unittest.TestCase):
self.__cli.onecmd('user add -p Alic3 alice')
self.__cli.onecmd('user setadmin alice')
self.__cli.onecmd('user setadmin bob')
- self.assertTrue(self.__store.find(User, User.name == 'alice').one().admin)
+ with db_session:
+ self.assertTrue(User.get(name = 'alice').admin)
def test_user_changepass(self):
self.__cli.onecmd('user add -p Alic3 alice')
diff --git a/tests/base/test_db.py b/tests/base/test_db.py
index 99f054c..36a9896 100644
--- a/tests/base/test_db.py
+++ b/tests/base/test_db.py
@@ -9,41 +9,38 @@
#
# Distributed under terms of the GNU AGPLv3 license.
+import re
import unittest
-import io, re
-from collections import namedtuple
import uuid
+from collections import namedtuple
+from pony.orm import db_session
+
from supysonic import db
date_regex = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$')
class DbTestCase(unittest.TestCase):
def setUp(self):
- self.store = db.get_store(u'sqlite:')
- with io.open(u'schema/sqlite.sql', u'r') as f:
- for statement in f.read().split(u';'):
- self.store.execute(statement)
+ db.init_database('sqlite:', True)
def tearDown(self):
- self.store.close()
+ db.release_database()
def create_some_folders(self):
- root_folder = db.Folder()
- root_folder.root = True
- root_folder.name = u'Root folder'
- root_folder.path = u'tests'
+ root_folder = db.Folder(
+ root = True,
+ name = 'Root folder',
+ path = 'tests'
+ )
- child_folder = db.Folder()
- child_folder.root = False
- child_folder.name = u'Child folder'
- child_folder.path = u'tests/assets'
- child_folder.has_cover_art = True
- child_folder.parent = root_folder
-
- self.store.add(root_folder)
- self.store.add(child_folder)
- self.store.commit()
+ child_folder = db.Folder(
+ root = False,
+ name = 'Child folder',
+ path = 'tests/assets',
+ has_cover_art = True,
+ parent = root_folder
+ )
return root_folder, child_folder
@@ -51,175 +48,152 @@ class DbTestCase(unittest.TestCase):
root, child = self.create_some_folders()
if not artist:
- artist = db.Artist()
- artist.name = u'Test Artist'
+ artist = db.Artist(name = 'Test artist')
if not album:
- album = db.Album()
- album.artist = artist
- album.name = u'Test Album'
+ album = db.Album(artist = artist, name = 'Test Album')
- track1 = db.Track()
- track1.title = u'Track Title'
- track1.album = album
- track1.artist = artist
- track1.disc = 1
- track1.number = 1
- track1.duration = 3
- track1.bitrate = 320
- track1.path = u'tests/assets/empty'
- track1.content_type = u'audio/mpeg'
- track1.last_modification = 1234
- track1.root_folder = root
- track1.folder = child
- self.store.add(track1)
+ track1 = db.Track(
+ title = 'Track Title',
+ album = album,
+ artist = artist,
+ disc = 1,
+ number = 1,
+ duration = 3,
+ bitrate = 320,
+ path = 'tests/assets/empty',
+ content_type = 'audio/mpeg',
+ last_modification = 1234,
+ root_folder = root,
+ folder = child
+ )
- track2 = db.Track()
- track2.title = u'One Awesome Song'
- track2.album = album
- track2.artist = artist
- track2.disc = 1
- track2.number = 2
- track2.duration = 5
- track2.bitrate = 96
- track2.path = u'tests/assets/empty'
- track2.content_type = u'audio/mpeg'
- track2.last_modification = 1234
- track2.root_folder = root
- track2.folder = child
- self.store.add(track2)
+ track2 = db.Track(
+ title = 'One Awesome Song',
+ album = album,
+ artist = artist,
+ disc = 1,
+ number = 2,
+ duration = 5,
+ bitrate = 96,
+ path = 'tests/assets/23bytes',
+ content_type = 'audio/mpeg',
+ last_modification = 1234,
+ root_folder = root,
+ folder = child
+ )
return track1, track2
- def create_playlist(self):
- user = db.User()
- user.name = u'Test User'
- user.password = u'secret'
- user.salt = u'ABC+'
+ def create_user(self, name = 'Test User'):
+ return db.User(
+ name = name,
+ password = 'secret',
+ salt = 'ABC+',
+ )
- playlist = db.Playlist()
- playlist.user = user
- playlist.name = u'Playlist!'
- self.store.add(playlist)
+ def create_playlist(self):
+
+ playlist = db.Playlist(
+ user = self.create_user(),
+ name = 'Playlist!'
+ )
return playlist
+ @db_session
def test_folder_base(self):
root_folder, child_folder = self.create_some_folders()
- MockUser = namedtuple(u'User', [ u'id' ])
+ MockUser = namedtuple('User', [ 'id' ])
user = MockUser(uuid.uuid4())
root = root_folder.as_subsonic_child(user)
self.assertIsInstance(root, dict)
- self.assertIn(u'id', root)
- self.assertIn(u'isDir', root)
- self.assertIn(u'title', root)
- self.assertIn(u'album', root)
- self.assertIn(u'created', root)
- self.assertTrue(root[u'isDir'])
- self.assertEqual(root[u'title'], u'Root folder')
- self.assertEqual(root[u'album'], u'Root folder')
+ self.assertIn('id', root)
+ self.assertIn('isDir', root)
+ self.assertIn('title', root)
+ self.assertIn('album', root)
+ self.assertIn('created', root)
+ self.assertTrue(root['isDir'])
+ self.assertEqual(root['title'], 'Root folder')
+ self.assertEqual(root['album'], 'Root folder')
self.assertRegexpMatches(root['created'], date_regex)
child = child_folder.as_subsonic_child(user)
- self.assertIn(u'parent', child)
- self.assertIn(u'artist', child)
- self.assertIn(u'coverArt', child)
- self.assertEqual(child[u'parent'], str(root_folder.id))
- self.assertEqual(child[u'artist'], root_folder.name)
- self.assertEqual(child[u'coverArt'], child[u'id'])
+ self.assertIn('parent', child)
+ self.assertIn('artist', child)
+ self.assertIn('coverArt', child)
+ self.assertEqual(child['parent'], str(root_folder.id))
+ self.assertEqual(child['artist'], root_folder.name)
+ self.assertEqual(child['coverArt'], child['id'])
+ @db_session
def test_folder_annotation(self):
root_folder, child_folder = self.create_some_folders()
- # Assuming SQLite doesn't enforce foreign key constraints
- MockUser = namedtuple(u'User', [ u'id' ])
- user = MockUser(uuid.uuid4())
-
- star = db.StarredFolder()
- star.user_id = user.id
- star.starred_id = root_folder.id
-
- rating_user = db.RatingFolder()
- rating_user.user_id = user.id
- rating_user.rated_id = root_folder.id
- rating_user.rating = 2
-
- rating_other = db.RatingFolder()
- rating_other.user_id = uuid.uuid4()
- rating_other.rated_id = root_folder.id
- rating_other.rating = 5
-
- self.store.add(star)
- self.store.add(rating_user)
- self.store.add(rating_other)
+ user = self.create_user()
+ star = db.StarredFolder(
+ user = user,
+ starred = root_folder
+ )
+ rating_user = db.RatingFolder(
+ user = user,
+ rated = root_folder,
+ rating = 2
+ )
+ other = self.create_user('Other')
+ rating_other = db.RatingFolder(
+ user = other,
+ rated = root_folder,
+ rating = 5
+ )
root = root_folder.as_subsonic_child(user)
- self.assertIn(u'starred', root)
- self.assertIn(u'userRating', root)
- self.assertIn(u'averageRating', root)
- self.assertRegexpMatches(root[u'starred'], date_regex)
- self.assertEqual(root[u'userRating'], 2)
- self.assertEqual(root[u'averageRating'], 3.5)
+ self.assertIn('starred', root)
+ self.assertIn('userRating', root)
+ self.assertIn('averageRating', root)
+ self.assertRegexpMatches(root['starred'], date_regex)
+ self.assertEqual(root['userRating'], 2)
+ self.assertEqual(root['averageRating'], 3.5)
child = child_folder.as_subsonic_child(user)
- self.assertNotIn(u'starred', child)
- self.assertNotIn(u'userRating', child)
+ self.assertNotIn('starred', child)
+ self.assertNotIn('userRating', child)
+ @db_session
def test_artist(self):
- artist = db.Artist()
- artist.name = u'Test Artist'
- self.store.add(artist)
+ artist = db.Artist(name = 'Test Artist')
- # Assuming SQLite doesn't enforce foreign key constraints
- MockUser = namedtuple(u'User', [ u'id' ])
- user = MockUser(uuid.uuid4())
-
- star = db.StarredArtist()
- star.user_id = user.id
- star.starred_id = artist.id
- self.store.add(star)
+ user = self.create_user()
+ star = db.StarredArtist(user = user, starred = artist)
artist_dict = artist.as_subsonic_artist(user)
self.assertIsInstance(artist_dict, dict)
- self.assertIn(u'id', artist_dict)
- self.assertIn(u'name', artist_dict)
- self.assertIn(u'albumCount', artist_dict)
- self.assertIn(u'starred', artist_dict)
- self.assertEqual(artist_dict[u'name'], u'Test Artist')
- self.assertEqual(artist_dict[u'albumCount'], 0)
- self.assertRegexpMatches(artist_dict[u'starred'], date_regex)
+ self.assertIn('id', artist_dict)
+ self.assertIn('name', artist_dict)
+ self.assertIn('albumCount', artist_dict)
+ self.assertIn('starred', artist_dict)
+ self.assertEqual(artist_dict['name'], 'Test Artist')
+ self.assertEqual(artist_dict['albumCount'], 0)
+ self.assertRegexpMatches(artist_dict['starred'], date_regex)
- album = db.Album()
- album.name = u'Test Artist' # self-titled
- artist.albums.add(album)
-
- album = db.Album()
- album.name = u'The Album After The Frist One'
- artist.albums.add(album)
+ db.Album(name = 'Test Artist', artist = artist) # self-titled
+ db.Album(name = 'The Album After The First One', artist = artist)
artist_dict = artist.as_subsonic_artist(user)
- self.assertEqual(artist_dict[u'albumCount'], 2)
+ self.assertEqual(artist_dict['albumCount'], 2)
+ @db_session
def test_album(self):
- artist = db.Artist()
- artist.name = u'Test Artist'
+ artist = db.Artist(name = 'Test Artist')
+ album = db.Album(artist = artist, name = 'Test Album')
- album = db.Album()
- album.artist = artist
- album.name = u'Test Album'
-
- # Assuming SQLite doesn't enforce foreign key constraints
- MockUser = namedtuple(u'User', [ u'id' ])
- user = MockUser(uuid.uuid4())
-
- star = db.StarredAlbum()
- star.user_id = user.id
- star.starred = album
-
- self.store.add(album)
- self.store.add(star)
+ user = self.create_user()
+ star = db.StarredAlbum(
+ user = user,
+ starred = album
+ )
# No tracks, shouldn't be stored under normal circumstances
self.assertRaises(ValueError, album.as_subsonic_album, user)
@@ -228,67 +202,67 @@ class DbTestCase(unittest.TestCase):
album_dict = album.as_subsonic_album(user)
self.assertIsInstance(album_dict, dict)
- self.assertIn(u'id', album_dict)
- self.assertIn(u'name', album_dict)
- self.assertIn(u'artist', album_dict)
- self.assertIn(u'artistId', album_dict)
- self.assertIn(u'songCount', album_dict)
- self.assertIn(u'duration', album_dict)
- self.assertIn(u'created', album_dict)
- self.assertIn(u'starred', album_dict)
- self.assertEqual(album_dict[u'name'], album.name)
- self.assertEqual(album_dict[u'artist'], artist.name)
- self.assertEqual(album_dict[u'artistId'], str(artist.id))
- self.assertEqual(album_dict[u'songCount'], 2)
- self.assertEqual(album_dict[u'duration'], 8)
- self.assertRegexpMatches(album_dict[u'created'], date_regex)
- self.assertRegexpMatches(album_dict[u'starred'], date_regex)
+ self.assertIn('id', album_dict)
+ self.assertIn('name', album_dict)
+ self.assertIn('artist', album_dict)
+ self.assertIn('artistId', album_dict)
+ self.assertIn('songCount', album_dict)
+ self.assertIn('duration', album_dict)
+ self.assertIn('created', album_dict)
+ self.assertIn('starred', album_dict)
+ self.assertEqual(album_dict['name'], album.name)
+ self.assertEqual(album_dict['artist'], artist.name)
+ self.assertEqual(album_dict['artistId'], str(artist.id))
+ self.assertEqual(album_dict['songCount'], 2)
+ self.assertEqual(album_dict['duration'], 8)
+ self.assertRegexpMatches(album_dict['created'], date_regex)
+ self.assertRegexpMatches(album_dict['starred'], date_regex)
+ @db_session
def test_track(self):
track1, track2 = self.create_some_tracks()
# Assuming SQLite doesn't enforce foreign key constraints
- MockUser = namedtuple(u'User', [ u'id' ])
+ MockUser = namedtuple('User', [ 'id' ])
user = MockUser(uuid.uuid4())
track1_dict = track1.as_subsonic_child(user, None)
self.assertIsInstance(track1_dict, dict)
- self.assertIn(u'id', track1_dict)
- self.assertIn(u'parent', track1_dict)
- self.assertIn(u'isDir', track1_dict)
- self.assertIn(u'title', track1_dict)
- self.assertFalse(track1_dict[u'isDir'])
+ self.assertIn('id', track1_dict)
+ self.assertIn('parent', track1_dict)
+ self.assertIn('isDir', track1_dict)
+ self.assertIn('title', track1_dict)
+ self.assertFalse(track1_dict['isDir'])
# ... we'll test the rest against the API XSD.
+ @db_session
def test_user(self):
- user = db.User()
- user.name = u'Test User'
- user.password = u'secret'
- user.salt = u'ABC+'
+ user = self.create_user()
user_dict = user.as_subsonic_user()
self.assertIsInstance(user_dict, dict)
+ @db_session
def test_chat(self):
- user = db.User()
- user.name = u'Test User'
- user.password = u'secret'
- user.salt = u'ABC+'
+ user = self.create_user()
- line = db.ChatMessage()
- line.user = user
- line.message = u'Hello world!'
+ line = db.ChatMessage(
+ user = user,
+ message = 'Hello world!'
+ )
line_dict = line.responsize()
self.assertIsInstance(line_dict, dict)
- self.assertIn(u'username', line_dict)
- self.assertEqual(line_dict[u'username'], user.name)
+ self.assertIn('username', line_dict)
+ self.assertEqual(line_dict['username'], user.name)
+ @db_session
def test_playlist(self):
playlist = self.create_playlist()
playlist_dict = playlist.as_subsonic_playlist(playlist.user)
self.assertIsInstance(playlist_dict, dict)
+ @db_session
def test_playlist_tracks(self):
playlist = self.create_playlist()
track1, track2 = self.create_some_tracks()
@@ -307,9 +281,10 @@ class DbTestCase(unittest.TestCase):
playlist.add(str(track1.id))
self.assertSequenceEqual(playlist.get_tracks(), [ track1 ])
- self.assertRaises(ValueError, playlist.add, u'some string')
+ self.assertRaises(ValueError, playlist.add, 'some string')
self.assertRaises(NameError, playlist.add, 2345)
+ @db_session
def test_playlist_remove_tracks(self):
playlist = self.create_playlist()
track1, track2 = self.create_some_tracks()
@@ -329,6 +304,7 @@ class DbTestCase(unittest.TestCase):
playlist.remove_at_indexes([ 1, 1 ])
self.assertSequenceEqual(playlist.get_tracks(), [ track2, track1 ])
+ @db_session
def test_playlist_fixing(self):
playlist = self.create_playlist()
track1, track2 = self.create_some_tracks()
@@ -338,10 +314,10 @@ class DbTestCase(unittest.TestCase):
playlist.add(track2)
self.assertSequenceEqual(playlist.get_tracks(), [ track1, track2 ])
- self.store.remove(track2)
+ track2.delete()
self.assertSequenceEqual(playlist.get_tracks(), [ track1 ])
- playlist.tracks = u'{0},{0},some random garbage,{0}'.format(track1.id)
+ playlist.tracks = '{0},{0},some random garbage,{0}'.format(track1.id)
self.assertSequenceEqual(playlist.get_tracks(), [ track1, track1, track1 ])
if __name__ == '__main__':
diff --git a/tests/base/test_scanner.py b/tests/base/test_scanner.py
index d8953bd..63d68e5 100644
--- a/tests/base/test_scanner.py
+++ b/tests/base/test_scanner.py
@@ -16,6 +16,7 @@ import tempfile
import unittest
from contextlib import contextmanager
+from pony.orm import db_session, commit
from supysonic import db
from supysonic.managers.folder import FolderManager
@@ -23,133 +24,158 @@ from supysonic.scanner import Scanner
class ScannerTestCase(unittest.TestCase):
def setUp(self):
- self.store = db.get_store('sqlite:')
- with io.open('schema/sqlite.sql', 'r') as f:
- for statement in f.read().split(';'):
- self.store.execute(statement)
+ db.init_database('sqlite:', True)
- FolderManager.add(self.store, 'folder', os.path.abspath('tests/assets'))
- self.folder = self.store.find(db.Folder).one()
- self.assertIsNotNone(self.folder)
+ FolderManager.add('folder', os.path.abspath('tests/assets'))
+ with db_session:
+ folder = db.Folder.select().first()
+ self.assertIsNotNone(folder)
+ self.folderid = folder.id
- self.scanner = Scanner(self.store)
- self.scanner.scan(self.folder)
+ self.scanner = Scanner()
+ self.scanner.scan(folder)
def tearDown(self):
self.scanner.finish()
- self.store.close()
+ db.release_database()
@contextmanager
def __temporary_track_copy(self):
- track = self.store.find(db.Track).one()
+ track = db.Track.select().first()
with tempfile.NamedTemporaryFile(dir = os.path.dirname(track.path)) as tf:
with io.open(track.path, 'rb') as f:
tf.write(f.read())
yield tf
+ @db_session
def test_scan(self):
- self.assertEqual(self.store.find(db.Track).count(), 1)
+ self.assertEqual(db.Track.select().count(), 1)
self.assertRaises(TypeError, self.scanner.scan, None)
self.assertRaises(TypeError, self.scanner.scan, 'string')
+ @db_session
def test_progress(self):
def progress(processed, total):
self.assertIsInstance(processed, int)
self.assertIsInstance(total, int)
self.assertLessEqual(processed, total)
- self.scanner.scan(self.folder, progress)
+ self.scanner.scan(db.Folder[self.folderid], progress)
+ @db_session
def test_rescan(self):
- self.scanner.scan(self.folder)
- self.assertEqual(self.store.find(db.Track).count(), 1)
+ self.scanner.scan(db.Folder[self.folderid])
+ commit()
+ self.assertEqual(db.Track.select().count(), 1)
+ @db_session
def test_force_rescan(self):
- self.scanner = Scanner(self.store, True)
- self.scanner.scan(self.folder)
- self.assertEqual(self.store.find(db.Track).count(), 1)
+ self.scanner = Scanner(True)
+ self.scanner.scan(db.Folder[self.folderid])
+ commit()
+ self.assertEqual(db.Track.select().count(), 1)
+ @db_session
def test_scan_file(self):
- track = self.store.find(db.Track).one()
+ track = db.Track.select().first()
self.assertRaises(TypeError, self.scanner.scan_file, None)
self.assertRaises(TypeError, self.scanner.scan_file, track)
self.scanner.scan_file('/some/inexistent/path')
- self.assertEqual(self.store.find(db.Track).count(), 1)
+ commit()
+ self.assertEqual(db.Track.select().count(), 1)
+ @db_session
def test_remove_file(self):
- track = self.store.find(db.Track).one()
+ track = db.Track.select().first()
self.assertRaises(TypeError, self.scanner.remove_file, None)
self.assertRaises(TypeError, self.scanner.remove_file, track)
self.scanner.remove_file('/some/inexistent/path')
- self.assertEqual(self.store.find(db.Track).count(), 1)
+ commit()
+ self.assertEqual(db.Track.select().count(), 1)
self.scanner.remove_file(track.path)
self.scanner.finish()
- self.assertEqual(self.store.find(db.Track).count(), 0)
- self.assertEqual(self.store.find(db.Album).count(), 0)
- self.assertEqual(self.store.find(db.Artist).count(), 0)
+ commit()
+ self.assertEqual(db.Track.select().count(), 0)
+ self.assertEqual(db.Album.select().count(), 0)
+ self.assertEqual(db.Artist.select().count(), 0)
+ @db_session
def test_move_file(self):
- track = self.store.find(db.Track).one()
+ track = db.Track.select().first()
self.assertRaises(TypeError, self.scanner.move_file, None, 'string')
self.assertRaises(TypeError, self.scanner.move_file, track, 'string')
self.assertRaises(TypeError, self.scanner.move_file, 'string', None)
self.assertRaises(TypeError, self.scanner.move_file, 'string', track)
self.scanner.move_file('/some/inexistent/path', track.path)
- self.assertEqual(self.store.find(db.Track).count(), 1)
+ commit()
+ self.assertEqual(db.Track.select().count(), 1)
self.scanner.move_file(track.path, track.path)
- self.assertEqual(self.store.find(db.Track).count(), 1)
+ commit()
+ self.assertEqual(db.Track.select().count(), 1)
self.assertRaises(Exception, self.scanner.move_file, track.path, '/some/inexistent/path')
with self.__temporary_track_copy() as tf:
- self.scanner.scan(self.folder)
- self.assertEqual(self.store.find(db.Track).count(), 2)
+ self.scanner.scan(db.Folder[self.folderid])
+ commit()
+ self.assertEqual(db.Track.select().count(), 2)
self.scanner.move_file(tf.name, track.path)
- self.assertEqual(self.store.find(db.Track).count(), 1)
+ commit()
+ self.assertEqual(db.Track.select().count(), 1)
- track = self.store.find(db.Track).one()
+ track = db.Track.select().first()
new_path = os.path.abspath(os.path.join(os.path.dirname(track.path), '..', 'silence.mp3'))
self.scanner.move_file(track.path, new_path)
- self.assertEqual(self.store.find(db.Track).count(), 1)
+ commit()
+ self.assertEqual(db.Track.select().count(), 1)
self.assertEqual(track.path, new_path)
+ @db_session
def test_rescan_corrupt_file(self):
- track = self.store.find(db.Track).one()
- self.scanner = Scanner(self.store, True)
+ track = db.Track.select().first()
+ self.scanner = Scanner(True)
with self.__temporary_track_copy() as tf:
- self.scanner.scan(self.folder)
- self.assertEqual(self.store.find(db.Track).count(), 2)
+ self.scanner.scan(db.Folder[self.folderid])
+ commit()
+ self.assertEqual(db.Track.select().count(), 2)
tf.seek(0, 0)
tf.write('\x00' * 4096)
tf.truncate()
- self.scanner.scan(self.folder)
- self.assertEqual(self.store.find(db.Track).count(), 1)
+ self.scanner.scan(db.Folder[self.folderid])
+ commit()
+ self.assertEqual(db.Track.select().count(), 1)
+ @db_session
def test_rescan_removed_file(self):
- track = self.store.find(db.Track).one()
+ track = db.Track.select().first()
with self.__temporary_track_copy() as tf:
- self.scanner.scan(self.folder)
- self.assertEqual(self.store.find(db.Track).count(), 2)
+ self.scanner.scan(db.Folder[self.folderid])
+ commit()
+ self.assertEqual(db.Track.select().count(), 2)
- self.scanner.scan(self.folder)
- self.assertEqual(self.store.find(db.Track).count(), 1)
+ self.scanner.scan(db.Folder[self.folderid])
+ commit()
+ self.assertEqual(db.Track.select().count(), 1)
+ @db_session
def test_scan_tag_change(self):
- self.scanner = Scanner(self.store, True)
+ self.scanner = Scanner(True)
+ folder = db.Folder[self.folderid]
with self.__temporary_track_copy() as tf:
- self.scanner.scan(self.folder)
- copy = self.store.find(db.Track, db.Track.path == tf.name).one()
+ self.scanner.scan(folder)
+ commit()
+ copy = db.Track.get(path = tf.name)
self.assertEqual(copy.artist.name, 'Some artist')
self.assertEqual(copy.album.name, 'Awesome album')
@@ -158,12 +184,12 @@ class ScannerTestCase(unittest.TestCase):
tags['album'] = 'Crappy album'
tags.save()
- self.scanner.scan(self.folder)
+ self.scanner.scan(folder)
self.scanner.finish()
self.assertEqual(copy.artist.name, 'Renamed artist')
self.assertEqual(copy.album.name, 'Crappy album')
- self.assertIsNotNone(self.store.find(db.Artist, db.Artist.name == 'Some artist').one())
- self.assertIsNotNone(self.store.find(db.Album, db.Album.name == 'Awesome album').one())
+ self.assertIsNotNone(db.Artist.get(name = 'Some artist'))
+ self.assertIsNotNone(db.Album.get(name = 'Awesome album'))
def test_stats(self):
self.assertEqual(self.scanner.stats(), ((1,1,1),(0,0,0)))
diff --git a/tests/base/test_watcher.py b/tests/base/test_watcher.py
index 983b265..672bff7 100644
--- a/tests/base/test_watcher.py
+++ b/tests/base/test_watcher.py
@@ -18,9 +18,10 @@ import time
import unittest
from contextlib import contextmanager
+from pony.orm import db_session
from threading import Thread
-from supysonic.db import get_store, Track, Artist
+from supysonic.db import init_database, release_database, Track, Artist
from supysonic.managers.folder import FolderManager
from supysonic.watcher import SupysonicWatcher
@@ -38,29 +39,14 @@ class WatcherTestConfig(TestConfig):
self.BASE['database_uri'] = db_uri
class WatcherTestBase(unittest.TestCase):
- @contextmanager
- def _get_store(self):
- store = None
- try:
- store = get_store('sqlite:///' + self.__dbfile)
- yield store
- store.commit()
- store.close()
- except:
- store.rollback()
- store.close()
- raise
-
def setUp(self):
self.__dbfile = tempfile.mkstemp()[1]
- conf = WatcherTestConfig('sqlite:///' + self.__dbfile)
- self.__sleep_time = conf.DAEMON['wait_delay'] + 1
+ dburi = 'sqlite:///' + self.__dbfile
+ init_database(dburi, True)
+ release_database()
- with self._get_store() as store:
- with io.open('schema/sqlite.sql', 'r') as sql:
- schema = sql.read()
- for statement in schema.split(';'):
- store.execute(statement)
+ conf = WatcherTestConfig(dburi)
+ self.__sleep_time = conf.DAEMON['wait_delay'] + 1
self.__watcher = SupysonicWatcher(conf)
self.__thread = Thread(target = self.__watcher.run)
@@ -82,6 +68,12 @@ class WatcherTestBase(unittest.TestCase):
def _sleep(self):
time.sleep(self.__sleep_time)
+ @contextmanager
+ def _tempdbrebind(self):
+ init_database('sqlite:///' + self.__dbfile)
+ try: yield
+ finally: release_database()
+
class NothingToWatchTestCase(WatcherTestBase):
def test_spawn_useless_watcher(self):
self._start()
@@ -93,8 +85,7 @@ class WatcherTestCase(WatcherTestBase):
def setUp(self):
super(WatcherTestCase, self).setUp()
self.__dir = tempfile.mkdtemp()
- with self._get_store() as store:
- FolderManager.add(store, 'Folder', self.__dir)
+ FolderManager.add('Folder', self.__dir)
self._start()
def tearDown(self):
@@ -115,9 +106,9 @@ class WatcherTestCase(WatcherTestBase):
shutil.copyfile('tests/assets/folder/silence.mp3', path)
return path
+ @db_session
def assertTrackCountEqual(self, expected):
- with self._get_store() as store:
- self.assertEqual(store.find(Track).count(), expected)
+ self.assertEqual(Track.select().count(), expected)
def test_add(self):
self._addfile()
@@ -128,7 +119,8 @@ class WatcherTestCase(WatcherTestBase):
def test_add_nowait_stop(self):
self._addfile()
self._stop()
- self.assertTrackCountEqual(1)
+ with self._tempdbrebind():
+ self.assertTrackCountEqual(1)
def test_add_multiple(self):
self._addfile()
@@ -136,46 +128,46 @@ class WatcherTestCase(WatcherTestBase):
self._addfile()
self.assertTrackCountEqual(0)
self._sleep()
- with self._get_store() as store:
- self.assertEqual(store.find(Track).count(), 3)
- self.assertEqual(store.find(Artist).count(), 1)
+ with db_session:
+ self.assertEqual(Track.select().count(), 3)
+ self.assertEqual(Artist.select().count(), 1)
def test_change(self):
path = self._addfile()
self._sleep()
trackid = None
- with self._get_store() as store:
- self.assertEqual(store.find(Track).count(), 1)
- self.assertEqual(store.find(Artist, Artist.name == 'Some artist').count(), 1)
- trackid = store.find(Track).one().id
+ with db_session:
+ self.assertEqual(Track.select().count(), 1)
+ self.assertEqual(Artist.select(lambda a: a.name == 'Some artist').count(), 1)
+ trackid = Track.select().first().id
tags = mutagen.File(path, easy = True)
tags['artist'] = 'Renamed'
tags.save()
self._sleep()
- with self._get_store() as store:
- self.assertEqual(store.find(Track).count(), 1)
- self.assertEqual(store.find(Artist, Artist.name == 'Some artist').count(), 0)
- self.assertEqual(store.find(Artist, Artist.name == 'Renamed').count(), 1)
- self.assertEqual(store.find(Track).one().id, trackid)
+ with db_session:
+ self.assertEqual(Track.select().count(), 1)
+ self.assertEqual(Artist.select(lambda a: a.name == 'Some artist').count(), 0)
+ self.assertEqual(Artist.select(lambda a: a.name == 'Renamed').count(), 1)
+ self.assertEqual(Track.select().first().id, trackid)
def test_rename(self):
path = self._addfile()
self._sleep()
trackid = None
- with self._get_store() as store:
- self.assertEqual(store.find(Track).count(), 1)
- trackid = store.find(Track).one().id
+ with db_session:
+ self.assertEqual(Track.select().count(), 1)
+ trackid = Track.select().first().id
newpath = self._temppath()
shutil.move(path, newpath)
self._sleep()
- with self._get_store() as store:
- track = store.find(Track).one()
+ with db_session:
+ track = Track.select().first()
self.assertIsNotNone(track)
self.assertNotEqual(track.path, path)
self.assertEqual(track.path, newpath)
diff --git a/tests/frontend/test_folder.py b/tests/frontend/test_folder.py
index 30c9251..e3cd968 100644
--- a/tests/frontend/test_folder.py
+++ b/tests/frontend/test_folder.py
@@ -11,6 +11,8 @@
import uuid
+from pony.orm import db_session
+
from supysonic.db import Folder
from .frontendtestbase import FrontendTestBase
@@ -50,20 +52,22 @@ class FolderTestCase(FrontendTestBase):
self.assertIn('Add Folder', rv.data)
rv = self.client.post('/folder/add', data = { 'name': 'name', 'path': 'tests/assets' }, follow_redirects = True)
self.assertIn('created', rv.data)
- self.assertEqual(self.store.find(Folder).count(), 1)
+ with db_session:
+ self.assertEqual(Folder.select().count(), 1)
def test_delete(self):
- folder = Folder()
- folder.name = 'folder'
- folder.path = 'tests/assets'
- folder.root = True
- self.store.add(folder)
- self.store.commit()
+ with db_session:
+ folder = Folder(
+ name = 'folder',
+ path = 'tests/assets',
+ root = True
+ )
self._login('bob', 'B0b')
rv = self.client.get('/folder/del/' + str(folder.id), follow_redirects = True)
self.assertIn('There\'s nothing much to see', rv.data)
- self.assertEqual(self.store.find(Folder).count(), 1)
+ with db_session:
+ self.assertEqual(Folder.select().count(), 1)
self._logout()
self._login('alice', 'Alic3')
@@ -73,15 +77,17 @@ class FolderTestCase(FrontendTestBase):
self.assertIn('No such folder', rv.data)
rv = self.client.get('/folder/del/' + str(folder.id), follow_redirects = True)
self.assertIn('Music folders', rv.data)
- self.assertEqual(self.store.find(Folder).count(), 0)
+ with db_session:
+ self.assertEqual(Folder.select().count(), 0)
def test_scan(self):
- folder = Folder()
- folder.name = 'folder'
- folder.path = 'tests/assets'
- folder.root = True
- self.store.add(folder)
- self.store.commit()
+ with db_session:
+ folder = Folder(
+ name = 'folder',
+ path = 'tests/assets',
+ root = True,
+ )
+
self._login('alice', 'Alic3')
rv = self.client.get('/folder/scan/string', follow_redirects = True)
diff --git a/tests/frontend/test_login.py b/tests/frontend/test_login.py
index b908abe..ccf7904 100644
--- a/tests/frontend/test_login.py
+++ b/tests/frontend/test_login.py
@@ -12,6 +12,8 @@
import uuid
+from pony.orm import db_session
+
from supysonic.db import User
from .frontendtestbase import FrontendTestBase
@@ -50,8 +52,9 @@ class LoginTestCase(FrontendTestBase):
def test_root_with_valid_session(self):
# Root with valid session
- with self.client.session_transaction() as sess:
- sess['userid'] = self.store.find(User, User.name == 'alice').one().id
+ with db_session:
+ with self.client.session_transaction() as sess:
+ sess['userid'] = User.get(name = 'alice').id
rv = self.client.get('/', follow_redirects=True)
self.assertIn('alice', rv.data)
self.assertIn('Log out', rv.data)
diff --git a/tests/frontend/test_playlist.py b/tests/frontend/test_playlist.py
index 80e6bed..b52dbfe 100644
--- a/tests/frontend/test_playlist.py
+++ b/tests/frontend/test_playlist.py
@@ -11,6 +11,8 @@
import uuid
+from pony.orm import db_session
+
from supysonic.db import Folder, Artist, Album, Track, Playlist, User
from .frontendtestbase import FrontendTestBase
@@ -19,43 +21,34 @@ class PlaylistTestCase(FrontendTestBase):
def setUp(self):
super(PlaylistTestCase, self).setUp()
- folder = Folder()
- folder.name = 'Root'
- folder.path = 'tests/assets'
- folder.root = True
+ with db_session:
+ folder = Folder(name = 'Root', path = 'tests/assets', root = True)
+ artist = Artist(name = 'Artist!')
+ album = Album(name = 'Album!', artist = artist)
- artist = Artist()
- artist.name = 'Artist!'
+ track = Track(
+ path = 'tests/assets/23bytes',
+ title = '23bytes',
+ artist = artist,
+ album = album,
+ folder = folder,
+ root_folder = folder,
+ duration = 2,
+ disc = 1,
+ number = 1,
+ content_type = 'audio/mpeg',
+ bitrate = 320,
+ last_modification = 0
+ )
- album = Album()
- album.name = 'Album!'
- album.artist = artist
+ playlist = Playlist(
+ name = 'Playlist!',
+ user = User.get(name = 'alice')
+ )
+ for _ in range(4):
+ playlist.add(track)
- track = Track()
- track.path = 'tests/assets/23bytes'
- track.title = '23bytes'
- track.artist = artist
- track.album = album
- track.folder = folder
- track.root_folder = folder
- track.duration = 2
- track.disc = 1
- track.number = 1
- track.content_type = 'audio/mpeg'
- track.bitrate = 320
- track.last_modification = 0
-
- playlist = Playlist()
- playlist.name = 'Playlist!'
- playlist.user = self.store.find(User, User.name == 'alice').one()
- for _ in range(4):
- playlist.add(track)
-
- self.store.add(track)
- self.store.add(playlist)
- self.store.commit()
-
- self.playlist = playlist
+ self.playlistid = playlist.id
def test_index(self):
self._login('alice', 'Alic3')
@@ -68,7 +61,7 @@ class PlaylistTestCase(FrontendTestBase):
self.assertIn('Invalid', rv.data)
rv = self.client.get('/playlist/' + str(uuid.uuid4()), follow_redirects = True)
self.assertIn('Unknown', rv.data)
- rv = self.client.get('/playlist/' + str(self.playlist.id))
+ rv = self.client.get('/playlist/' + str(self.playlistid))
self.assertIn('Playlist!', rv.data)
self.assertIn('23bytes', rv.data)
self.assertIn('Artist!', rv.data)
@@ -80,22 +73,25 @@ class PlaylistTestCase(FrontendTestBase):
self.assertIn('Invalid', rv.data)
rv = self.client.post('/playlist/' + str(uuid.uuid4()), follow_redirects = True)
self.assertIn('Unknown', rv.data)
- rv = self.client.post('/playlist/' + str(self.playlist.id), follow_redirects = True)
+ rv = self.client.post('/playlist/' + str(self.playlistid), follow_redirects = True)
self.assertNotIn('updated', rv.data)
self.assertIn('not allowed', rv.data)
self._logout()
self._login('alice', 'Alic3')
- rv = self.client.post('/playlist/' + str(self.playlist.id), follow_redirects = True)
+ rv = self.client.post('/playlist/' + str(self.playlistid), follow_redirects = True)
self.assertNotIn('updated', rv.data)
self.assertIn('Missing', rv.data)
- self.assertEqual(self.playlist.name, 'Playlist!')
+ with db_session:
+ self.assertEqual(Playlist[self.playlistid].name, 'Playlist!')
- rv = self.client.post('/playlist/' + str(self.playlist.id), data = { 'name': 'abc', 'public': True }, follow_redirects = True)
+ rv = self.client.post('/playlist/' + str(self.playlistid), data = { 'name': 'abc', 'public': True }, follow_redirects = True)
self.assertIn('updated', rv.data)
self.assertNotIn('not allowed', rv.data)
- self.assertEqual(self.playlist.name, 'abc')
- self.assertTrue(self.playlist.public)
+ with db_session:
+ playlist = Playlist[self.playlistid]
+ self.assertEqual(playlist.name, 'abc')
+ self.assertTrue(playlist.public)
def test_delete(self):
self._login('bob', 'B0b')
@@ -103,15 +99,17 @@ class PlaylistTestCase(FrontendTestBase):
self.assertIn('Invalid', rv.data)
rv = self.client.get('/playlist/del/' + str(uuid.uuid4()), follow_redirects = True)
self.assertIn('Unknown', rv.data)
- rv = self.client.get('/playlist/del/' + str(self.playlist.id), follow_redirects = True)
+ rv = self.client.get('/playlist/del/' + str(self.playlistid), follow_redirects = True)
self.assertIn('not allowed', rv.data)
- self.assertEqual(self.store.find(Playlist).count(), 1)
+ with db_session:
+ self.assertEqual(Playlist.select().count(), 1)
self._logout()
self._login('alice', 'Alic3')
- rv = self.client.get('/playlist/del/' + str(self.playlist.id), follow_redirects = True)
+ rv = self.client.get('/playlist/del/' + str(self.playlistid), follow_redirects = True)
self.assertIn('deleted', rv.data)
- self.assertEqual(self.store.find(Playlist).count(), 0)
+ with db_session:
+ self.assertEqual(Playlist.select().count(), 0)
if __name__ == '__main__':
unittest.main()
diff --git a/tests/frontend/test_user.py b/tests/frontend/test_user.py
index cb695d4..26bd89d 100644
--- a/tests/frontend/test_user.py
+++ b/tests/frontend/test_user.py
@@ -11,6 +11,8 @@
import uuid
+from pony.orm import db_session
+
from supysonic.db import User, ClientPrefs
from .frontendtestbase import FrontendTestBase
@@ -19,7 +21,8 @@ class UserTestCase(FrontendTestBase):
def setUp(self):
super(UserTestCase, self).setUp()
- self.users = { u.name: u for u in self.store.find(User) }
+ with db_session:
+ self.users = { u.name: u.id for u in User.select() }
def test_index(self):
self._login('bob', 'B0b')
@@ -38,18 +41,15 @@ class UserTestCase(FrontendTestBase):
self.assertIn('Invalid', rv.data)
rv = self.client.get('/user/' + str(uuid.uuid4()), follow_redirects = True)
self.assertIn('No such user', rv.data)
- rv = self.client.get('/user/' + str(self.users['bob'].id))
+ rv = self.client.get('/user/' + str(self.users['bob']))
self.assertIn('bob', rv.data)
self._logout()
- prefs = ClientPrefs()
- prefs.user_id = self.users['bob'].id
- prefs.client_name = 'tests'
- self.store.add(prefs)
- self.store.commit()
+ with db_session:
+ ClientPrefs(user = User[self.users['bob']], client_name = 'tests')
self._login('bob', 'B0b')
- rv = self.client.get('/user/' + str(self.users['alice'].id), follow_redirects = True)
+ rv = self.client.get('/user/' + str(self.users['alice']), follow_redirects = True)
self.assertIn('There\'s nothing much to see', rv.data)
self.assertNotIn('bob
', rv.data)
rv = self.client.get('/user/me')
@@ -68,19 +68,19 @@ class UserTestCase(FrontendTestBase):
self.client.post('/user/me', data = { 'n_': 'o' })
self.client.post('/user/me', data = { 'inexisting_client': 'setting' })
- prefs = ClientPrefs()
- prefs.user_id = self.users['alice'].id
- prefs.client_name = 'tests'
- self.store.add(prefs)
- self.store.commit()
+ with db_session:
+ ClientPrefs(user = User[self.users['alice']], client_name = 'tests')
rv = self.client.post('/user/me', data = { 'tests_format': 'mp3', 'tests_bitrate': 128 })
self.assertIn('updated', rv.data)
- self.assertEqual(prefs.format, 'mp3')
- self.assertEqual(prefs.bitrate, 128)
+ with db_session:
+ prefs = ClientPrefs[User[self.users['alice']], 'tests']
+ self.assertEqual(prefs.format, 'mp3')
+ self.assertEqual(prefs.bitrate, 128)
self.client.post('/user/me', data = { 'tests_delete': 1 })
- self.assertEqual(self.store.find(ClientPrefs).count(), 0)
+ with db_session:
+ self.assertEqual(ClientPrefs.select().count(), 0)
def test_change_username_get(self):
self._login('bob', 'B0b')
@@ -93,13 +93,13 @@ class UserTestCase(FrontendTestBase):
self.assertIn('Invalid', rv.data)
rv = self.client.get('/user/{}/changeusername'.format(uuid.uuid4()), follow_redirects = True)
self.assertIn('No such user', rv.data)
- self.client.get('/user/{}/changeusername'.format(self.users['bob'].id))
+ self.client.get('/user/{}/changeusername'.format(self.users['bob']))
def test_change_username_post(self):
self._login('alice', 'Alic3')
self.client.post('/user/whatever/changeusername')
- path = '/user/{}/changeusername'.format(self.users['bob'].id)
+ path = '/user/{}/changeusername'.format(self.users['bob'])
rv = self.client.post(path, follow_redirects = True)
self.assertIn('required', rv.data)
rv = self.client.post(path, data = { 'user': 'bob' }, follow_redirects = True)
@@ -107,10 +107,13 @@ class UserTestCase(FrontendTestBase):
rv = self.client.post(path, data = { 'user': 'b0b', 'admin': 1 }, follow_redirects = True)
self.assertIn('updated', rv.data)
self.assertIn('b0b', rv.data)
- self.assertEqual(self.users['bob'].name, 'b0b')
- self.assertTrue(self.users['bob'].admin)
+ with db_session:
+ bob = User[self.users['bob']]
+ self.assertEqual(bob.name, 'b0b')
+ self.assertTrue(bob.admin)
rv = self.client.post(path, data = { 'user': 'alice' }, follow_redirects = True)
- self.assertEqual(self.users['bob'].name, 'b0b')
+ with db_session:
+ self.assertEqual(User[self.users['bob']].name, 'b0b')
def test_change_mail_get(self):
self._login('alice', 'Alic3')
@@ -126,7 +129,7 @@ class UserTestCase(FrontendTestBase):
self._login('alice', 'Alic3')
rv = self.client.get('/user/me/changepass')
self.assertIn('Current password', rv.data)
- rv = self.client.get('/user/{}/changepass'.format(self.users['bob'].id))
+ rv = self.client.get('/user/{}/changepass'.format(self.users['bob']))
self.assertNotIn('Current password', rv.data)
def test_change_password_post(self):
@@ -151,7 +154,7 @@ class UserTestCase(FrontendTestBase):
rv = self._login('alice', 'alice')
self.assertIn('Logged in', rv.data)
- path = '/user/{}/changepass'.format(self.users['bob'].id)
+ path = '/user/{}/changepass'.format(self.users['bob'])
rv = self.client.post(path)
self.assertIn('required', rv.data)
rv = self.client.post(path, data = { 'new': 'alice' })
@@ -162,7 +165,6 @@ class UserTestCase(FrontendTestBase):
rv = self._login('bob', 'alice')
self.assertIn('Logged in', rv.data)
-
def test_add_get(self):
self._login('bob', 'B0b')
rv = self.client.get('/user/add', follow_redirects = True)
@@ -186,22 +188,25 @@ class UserTestCase(FrontendTestBase):
self.assertIn('passwords don', rv.data)
rv = self.client.post('/user/add', data = { 'user': 'alice', 'passwd': 'passwd', 'passwd_confirm': 'passwd' })
self.assertIn('already a user with that name', rv.data)
- self.assertEqual(self.store.find(User).count(), 2)
+ with db_session:
+ self.assertEqual(User.select().count(), 2)
rv = self.client.post('/user/add', data = { 'user': 'user', 'passwd': 'passwd', 'passwd_confirm': 'passwd', 'admin': 1 }, follow_redirects = True)
self.assertIn('added', rv.data)
- self.assertEqual(self.store.find(User).count(), 3)
+ with db_session:
+ self.assertEqual(User.select().count(), 3)
self._logout()
rv = self._login('user', 'passwd')
self.assertIn('Logged in', rv.data)
def test_delete(self):
- path = '/user/del/{}'.format(self.users['bob'].id)
+ path = '/user/del/{}'.format(self.users['bob'])
self._login('bob', 'B0b')
rv = self.client.get(path, follow_redirects = True)
self.assertIn('There\'s nothing much to see', rv.data)
- self.assertEqual(self.store.find(User).count(), 2)
+ with db_session:
+ self.assertEqual(User.select().count(), 2)
self._logout()
self._login('alice', 'Alic3')
@@ -211,7 +216,8 @@ class UserTestCase(FrontendTestBase):
self.assertIn('No such user', rv.data)
rv = self.client.get(path, follow_redirects = True)
self.assertIn('Deleted', rv.data)
- self.assertEqual(self.store.find(User).count(), 1)
+ with db_session:
+ self.assertEqual(User.select().count(), 1)
self._logout()
rv = self._login('bob', 'B0b')
self.assertIn('No such user', rv.data)
diff --git a/tests/managers/test_manager_folder.py b/tests/managers/test_manager_folder.py
index 6a2e47b..f1a5c5f 100644
--- a/tests/managers/test_manager_folder.py
+++ b/tests/managers/test_manager_folder.py
@@ -20,129 +20,133 @@ import tempfile
import unittest
import uuid
+from pony.orm import db_session, ObjectNotFound
+
class FolderManagerTestCase(unittest.TestCase):
def setUp(self):
# Create an empty sqlite database in memory
- self.store = db.get_store("sqlite:")
- # Read schema from file
- with io.open('schema/sqlite.sql', 'r') as sql:
- schema = sql.read()
- # Create tables on memory database
- for command in schema.split(';'):
- self.store.execute(command)
+ db.init_database('sqlite:', True)
# Create some temporary directories
self.media_dir = tempfile.mkdtemp()
self.music_dir = tempfile.mkdtemp()
- # Add test folders
- self.assertEqual(FolderManager.add(self.store, 'media', self.media_dir), FolderManager.SUCCESS)
- self.assertEqual(FolderManager.add(self.store, 'music', self.music_dir), FolderManager.SUCCESS)
-
- folder = db.Folder()
- folder.root = False
- folder.name = 'non-root'
- folder.path = os.path.join(self.music_dir, 'subfolder')
- self.store.add(folder)
-
- artist = db.Artist()
- artist.name = 'Artist'
-
- album = db.Album()
- album.name = 'Album'
- album.artist = artist
-
- root = self.store.find(db.Folder, db.Folder.name == 'media').one()
- track = db.Track()
- track.title = 'Track'
- track.artist = artist
- track.album = album
- track.disc = 1
- track.number = 1
- track.path = os.path.join(self.media_dir, 'somefile')
- track.folder = root
- track.root_folder = root
- track.duration = 2
- track.content_type = 'audio/mpeg'
- track.bitrate = 320
- track.last_modification = 0
- self.store.add(track)
-
- self.store.commit()
def tearDown(self):
+ db.release_database()
shutil.rmtree(self.media_dir)
shutil.rmtree(self.music_dir)
+ @db_session
+ def create_folders(self):
+ # Add test folders
+ self.assertEqual(FolderManager.add('media', self.media_dir), FolderManager.SUCCESS)
+ self.assertEqual(FolderManager.add('music', self.music_dir), FolderManager.SUCCESS)
+
+ folder = db.Folder(
+ root = False,
+ name = 'non-root',
+ path = os.path.join(self.music_dir, 'subfolder')
+ )
+
+ artist = db.Artist(name = 'Artist')
+ album = db.Album(name = 'Album', artist = artist)
+
+ root = db.Folder.get(name = 'media')
+ track = db.Track(
+ title = 'Track',
+ artist = artist,
+ album = album,
+ disc = 1,
+ number = 1,
+ path = os.path.join(self.media_dir, 'somefile'),
+ folder = root,
+ root_folder = root,
+ duration = 2,
+ content_type = 'audio/mpeg',
+ bitrate = 320,
+ last_modification = 0
+ )
+
+ @db_session
def test_get_folder(self):
+ self.create_folders()
+
# Get existing folders
for name in ['media', 'music']:
- folder = self.store.find(db.Folder, db.Folder.name == name, db.Folder.root == True).one()
- self.assertEqual(FolderManager.get(self.store, folder.id), (FolderManager.SUCCESS, folder))
+ folder = db.Folder.get(name = name, root = True)
+ self.assertEqual(FolderManager.get(folder.id), (FolderManager.SUCCESS, folder))
# Get with invalid UUID
- self.assertEqual(FolderManager.get(self.store, 'invalid-uuid'), (FolderManager.INVALID_ID, None))
- self.assertEqual(FolderManager.get(self.store, 0xdeadbeef), (FolderManager.INVALID_ID, None))
+ self.assertEqual(FolderManager.get('invalid-uuid'), (FolderManager.INVALID_ID, None))
+ self.assertEqual(FolderManager.get(0xdeadbeef), (FolderManager.INVALID_ID, None))
# Non-existent folder
- self.assertEqual(FolderManager.get(self.store, uuid.uuid4()), (FolderManager.NO_SUCH_FOLDER, None))
+ self.assertEqual(FolderManager.get(uuid.uuid4()), (FolderManager.NO_SUCH_FOLDER, None))
+ @db_session
def test_add_folder(self):
- # Added in setUp()
- self.assertEqual(self.store.find(db.Folder).count(), 3)
+ self.create_folders()
+ self.assertEqual(db.Folder.select().count(), 3)
# Create duplicate
- self.assertEqual(FolderManager.add(self.store,'media', self.media_dir), FolderManager.NAME_EXISTS)
- self.assertEqual(self.store.find(db.Folder, db.Folder.name == 'media').count(), 1)
+ self.assertEqual(FolderManager.add('media', self.media_dir), FolderManager.NAME_EXISTS)
+ self.assertEqual(db.Folder.select(lambda f: f.name == 'media').count(), 1)
# Duplicate path
- self.assertEqual(FolderManager.add(self.store,'new-folder', self.media_dir), FolderManager.PATH_EXISTS)
- self.assertEqual(self.store.find(db.Folder, db.Folder.path == self.media_dir).count(), 1)
+ self.assertEqual(FolderManager.add('new-folder', self.media_dir), FolderManager.PATH_EXISTS)
+ self.assertEqual(db.Folder.select(lambda f: f.path == self.media_dir).count(), 1)
# Invalid path
path = os.path.abspath('/this/not/is/valid')
- self.assertEqual(FolderManager.add(self.store,'invalid-path', path), FolderManager.INVALID_PATH)
- self.assertEqual(self.store.find(db.Folder, db.Folder.path == path).count(), 0)
+ self.assertEqual(FolderManager.add('invalid-path', path), FolderManager.INVALID_PATH)
+ self.assertFalse(db.Folder.exists(path = path))
# Subfolder of already added path
path = os.path.join(self.media_dir, 'subfolder')
os.mkdir(path)
- self.assertEqual(FolderManager.add(self.store,'subfolder', path), FolderManager.PATH_EXISTS)
- self.assertEqual(self.store.find(db.Folder).count(), 3)
+ self.assertEqual(FolderManager.add('subfolder', path), FolderManager.PATH_EXISTS)
+ self.assertEqual(db.Folder.select().count(), 3)
# Parent folder of an already added path
path = os.path.join(self.media_dir, '..')
- self.assertEqual(FolderManager.add(self.store, 'parent', path), FolderManager.SUBPATH_EXISTS)
- self.assertEqual(self.store.find(db.Folder).count(), 3)
+ self.assertEqual(FolderManager.add('parent', path), FolderManager.SUBPATH_EXISTS)
+ self.assertEqual(db.Folder.select().count(), 3)
+ @db_session
def test_delete_folder(self):
+ self.create_folders()
+
# Delete existing folders
for name in ['media', 'music']:
- folder = self.store.find(db.Folder, db.Folder.name == name, db.Folder.root == True).one()
- self.assertEqual(FolderManager.delete(self.store, folder.id), FolderManager.SUCCESS)
- self.assertIsNone(self.store.get(db.Folder, folder.id))
+ folder = db.Folder.get(name = name, root = True)
+ self.assertEqual(FolderManager.delete(folder.id), FolderManager.SUCCESS)
+ self.assertRaises(ObjectNotFound, db.Folder.__getitem__, folder.id)
# Delete invalid UUID
- self.assertEqual(FolderManager.delete(self.store, 'invalid-uuid'), FolderManager.INVALID_ID)
- self.assertEqual(self.store.find(db.Folder).count(), 1) # 'non-root' remaining
+ self.assertEqual(FolderManager.delete('invalid-uuid'), FolderManager.INVALID_ID)
+ self.assertEqual(db.Folder.select().count(), 1) # 'non-root' remaining
# Delete non-existent folder
- self.assertEqual(FolderManager.delete(self.store, uuid.uuid4()), FolderManager.NO_SUCH_FOLDER)
- self.assertEqual(self.store.find(db.Folder).count(), 1) # 'non-root' remaining
+ self.assertEqual(FolderManager.delete(uuid.uuid4()), FolderManager.NO_SUCH_FOLDER)
+ self.assertEqual(db.Folder.select().count(), 1) # 'non-root' remaining
# Delete non-root folder
- folder = self.store.find(db.Folder, db.Folder.name == 'non-root').one()
- self.assertEqual(FolderManager.delete(self.store, folder.id), FolderManager.NO_SUCH_FOLDER)
- self.assertEqual(self.store.find(db.Folder).count(), 1) # 'non-root' remaining
+ folder = db.Folder.get(name = 'non-root')
+ self.assertEqual(FolderManager.delete(folder.id), FolderManager.NO_SUCH_FOLDER)
+ self.assertEqual(db.Folder.select().count(), 1) # 'non-root' remaining
+ @db_session
def test_delete_by_name(self):
+ self.create_folders()
+
# Delete existing folders
for name in ['media', 'music']:
- self.assertEqual(FolderManager.delete_by_name(self.store, name), FolderManager.SUCCESS)
- self.assertEqual(self.store.find(db.Folder, db.Folder.name == name).count(), 0)
+ self.assertEqual(FolderManager.delete_by_name(name), FolderManager.SUCCESS)
+ self.assertFalse(db.Folder.exists(name = name))
# Delete non-existent folder
- self.assertEqual(FolderManager.delete_by_name(self.store, 'null'), FolderManager.NO_SUCH_FOLDER)
- self.assertEqual(self.store.find(db.Folder).count(), 1) # 'non-root' remaining
+ self.assertEqual(FolderManager.delete_by_name('null'), FolderManager.NO_SUCH_FOLDER)
+ self.assertEqual(db.Folder.select().count(), 1) # 'non-root' remaining
def test_human_readable_error(self):
values = [ FolderManager.SUCCESS, FolderManager.INVALID_ID, FolderManager.NAME_EXISTS,
diff --git a/tests/managers/test_manager_user.py b/tests/managers/test_manager_user.py
index daf282c..632dc22 100644
--- a/tests/managers/test_manager_user.py
+++ b/tests/managers/test_manager_user.py
@@ -13,61 +13,51 @@
from supysonic import db
from supysonic.managers.user import UserManager
+import io
import unittest
import uuid
-import io
+
+from pony.orm import db_session, commit
+from pony.orm import ObjectNotFound
class UserManagerTestCase(unittest.TestCase):
def setUp(self):
# Create an empty sqlite database in memory
- self.store = db.get_store("sqlite:")
- # Read schema from file
- with io.open('schema/sqlite.sql', 'r') as sql:
- schema = sql.read()
- # Create tables on memory database
- for command in schema.split(';'):
- self.store.execute(command)
+ db.init_database('sqlite:', True)
+ def tearDown(self):
+ db.release_database()
+
+ @db_session
+ def create_data(self):
# Create some users
- self.assertEqual(UserManager.add(self.store, 'alice', 'ALICE', 'test@example.com', True), UserManager.SUCCESS)
- self.assertEqual(UserManager.add(self.store, 'bob', 'BOB', 'bob@example.com', False), UserManager.SUCCESS)
- self.assertEqual(UserManager.add(self.store, 'charlie', 'CHARLIE', 'charlie@example.com', False), UserManager.SUCCESS)
+ self.assertEqual(UserManager.add('alice', 'ALICE', 'test@example.com', True), UserManager.SUCCESS)
+ self.assertEqual(UserManager.add('bob', 'BOB', 'bob@example.com', False), UserManager.SUCCESS)
+ self.assertEqual(UserManager.add('charlie', 'CHARLIE', 'charlie@example.com', False), UserManager.SUCCESS)
- folder = db.Folder()
- folder.name = 'Root'
- folder.path = 'tests/assets'
- folder.root = True
+ folder = db.Folder(name = 'Root', path = 'tests/assets', root = True)
+ artist = db.Artist(name = 'Artist')
+ album = db.Album(name = 'Album', artist = artist)
+ track = db.Track(
+ title = 'Track',
+ disc = 1,
+ number = 1,
+ duration = 1,
+ artist = artist,
+ album = album,
+ path = 'tests/assets/empty',
+ folder = folder,
+ root_folder = folder,
+ content_type = 'audio/mpeg',
+ bitrate = 320,
+ last_modification = 0
+ )
- artist = db.Artist()
- artist.name = 'Artist'
-
- album = db.Album()
- album.name = 'Album'
- album.artist = artist
-
- track = db.Track()
- track.title = 'Track'
- track.disc = 1
- track.number = 1
- track.duration = 1
- track.artist = artist
- track.album = album
- track.path = 'tests/assets/empty'
- track.folder = folder
- track.root_folder = folder
- track.duration = 2
- track.content_type = 'audio/mpeg'
- track.bitrate = 320
- track.last_modification = 0
- self.store.add(track)
- self.store.commit()
-
- playlist = db.Playlist()
- playlist.name = 'Playlist'
- playlist.user = self.store.find(db.User, db.User.name == 'alice').one()
+ playlist = db.Playlist(
+ name = 'Playlist',
+ user = db.User.get(name = 'alice')
+ )
playlist.add(track)
- self.store.add(playlist)
- self.store.commit()
def test_encrypt_password(self):
func = UserManager._UserManager__encrypt_password
@@ -75,96 +65,116 @@ class UserManagerTestCase(unittest.TestCase):
self.assertEqual(func(u'pass-word',u'pepper'), (u'd68c95a91ed7773aa57c7c044d2309a5bf1da2e7', u'pepper'))
self.assertEqual(func(u'éèàïô', u'ABC+'), (u'b639ba5217b89c906019d89d5816b407d8730898', u'ABC+'))
+ @db_session
def test_get_user(self):
+ self.create_data()
+
# Get existing users
for name in ['alice', 'bob', 'charlie']:
- user = self.store.find(db.User, db.User.name == name).one()
- self.assertEqual(UserManager.get(self.store, user.id), (UserManager.SUCCESS, user))
+ user = db.User.get(name = name)
+ self.assertEqual(UserManager.get(user.id), (UserManager.SUCCESS, user))
# Get with invalid UUID
- self.assertEqual(UserManager.get(self.store, 'invalid-uuid'), (UserManager.INVALID_ID, None))
- self.assertEqual(UserManager.get(self.store, 0xfee1bad), (UserManager.INVALID_ID, None))
+ self.assertEqual(UserManager.get('invalid-uuid'), (UserManager.INVALID_ID, None))
+ self.assertEqual(UserManager.get(0xfee1bad), (UserManager.INVALID_ID, None))
# Non-existent user
- self.assertEqual(UserManager.get(self.store, uuid.uuid4()), (UserManager.NO_SUCH_USER, None))
+ self.assertEqual(UserManager.get(uuid.uuid4()), (UserManager.NO_SUCH_USER, None))
+ @db_session
def test_add_user(self):
- # Added in setUp()
- self.assertEqual(self.store.find(db.User).count(), 3)
+ self.create_data()
+ self.assertEqual(db.User.select().count(), 3)
# Create duplicate
- self.assertEqual(UserManager.add(self.store, 'alice', 'Alic3', 'alice@example.com', True), UserManager.NAME_EXISTS)
+ self.assertEqual(UserManager.add('alice', 'Alic3', 'alice@example.com', True), UserManager.NAME_EXISTS)
+ @db_session
def test_delete_user(self):
+ self.create_data()
+
# Delete invalid UUID
- self.assertEqual(UserManager.delete(self.store, 'invalid-uuid'), UserManager.INVALID_ID)
- self.assertEqual(UserManager.delete(self.store, 0xfee1b4d), UserManager.INVALID_ID)
- self.assertEqual(self.store.find(db.User).count(), 3)
+ self.assertEqual(UserManager.delete('invalid-uuid'), UserManager.INVALID_ID)
+ self.assertEqual(UserManager.delete(0xfee1b4d), UserManager.INVALID_ID)
+ self.assertEqual(db.User.select().count(), 3)
# Delete non-existent user
- self.assertEqual(UserManager.delete(self.store, uuid.uuid4()), UserManager.NO_SUCH_USER)
- self.assertEqual(self.store.find(db.User).count(), 3)
+ self.assertEqual(UserManager.delete(uuid.uuid4()), UserManager.NO_SUCH_USER)
+ self.assertEqual(db.User.select().count(), 3)
# Delete existing users
for name in ['alice', 'bob', 'charlie']:
- user = self.store.find(db.User, db.User.name == name).one()
- self.assertEqual(UserManager.delete(self.store, user.id), UserManager.SUCCESS)
- self.assertIsNone(self.store.get(db.User, user.id))
- self.assertEqual(self.store.find(db.User).count(), 0)
+ user = db.User.get(name = name)
+ self.assertEqual(UserManager.delete(user.id), UserManager.SUCCESS)
+ self.assertRaises(ObjectNotFound, db.User.__getitem__, user.id)
+ commit()
+ self.assertEqual(db.User.select().count(), 0)
+ @db_session
def test_delete_by_name(self):
+ self.create_data()
+
# Delete existing users
for name in ['alice', 'bob', 'charlie']:
- self.assertEqual(UserManager.delete_by_name(self.store, name), UserManager.SUCCESS)
- self.assertEqual(self.store.find(db.User, db.User.name == name).count(), 0)
+ self.assertEqual(UserManager.delete_by_name(name), UserManager.SUCCESS)
+ self.assertFalse(db.User.exists(name = name))
# Delete non-existent user
- self.assertEqual(UserManager.delete_by_name(self.store, 'null'), UserManager.NO_SUCH_USER)
+ self.assertEqual(UserManager.delete_by_name('null'), UserManager.NO_SUCH_USER)
+ @db_session
def test_try_auth(self):
+ self.create_data()
+
# Test authentication
for name in ['alice', 'bob', 'charlie']:
- user = self.store.find(db.User, db.User.name == name).one()
- self.assertEqual(UserManager.try_auth(self.store, name, name.upper()), (UserManager.SUCCESS, user))
+ user = db.User.get(name = name)
+ self.assertEqual(UserManager.try_auth(name, name.upper()), (UserManager.SUCCESS, user))
# Wrong password
- self.assertEqual(UserManager.try_auth(self.store, 'alice', 'bad'), (UserManager.WRONG_PASS, None))
- self.assertEqual(UserManager.try_auth(self.store, 'alice', 'alice'), (UserManager.WRONG_PASS, None))
+ self.assertEqual(UserManager.try_auth('alice', 'bad'), (UserManager.WRONG_PASS, None))
+ self.assertEqual(UserManager.try_auth('alice', 'alice'), (UserManager.WRONG_PASS, None))
# Non-existent user
- self.assertEqual(UserManager.try_auth(self.store, 'null', 'null'), (UserManager.NO_SUCH_USER, None))
+ self.assertEqual(UserManager.try_auth('null', 'null'), (UserManager.NO_SUCH_USER, None))
+ @db_session
def test_change_password(self):
+ self.create_data()
+
# With existing users
for name in ['alice', 'bob', 'charlie']:
- user = self.store.find(db.User, db.User.name == name).one()
+ user = db.User.get(name = name)
# Good password
- self.assertEqual(UserManager.change_password(self.store, user.id, name.upper(), 'newpass'), UserManager.SUCCESS)
- self.assertEqual(UserManager.try_auth(self.store, name, 'newpass'), (UserManager.SUCCESS, user))
+ self.assertEqual(UserManager.change_password(user.id, name.upper(), 'newpass'), UserManager.SUCCESS)
+ self.assertEqual(UserManager.try_auth(name, 'newpass'), (UserManager.SUCCESS, user))
# Old password
- self.assertEqual(UserManager.try_auth(self.store, name, name.upper()), (UserManager.WRONG_PASS, None))
+ self.assertEqual(UserManager.try_auth(name, name.upper()), (UserManager.WRONG_PASS, None))
# Wrong password
- self.assertEqual(UserManager.change_password(self.store, user.id, 'badpass', 'newpass'), UserManager.WRONG_PASS)
+ self.assertEqual(UserManager.change_password(user.id, 'badpass', 'newpass'), UserManager.WRONG_PASS)
# Ensure we still got the same number of users
- self.assertEqual(self.store.find(db.User).count(), 3)
+ self.assertEqual(db.User.select().count(), 3)
# With invalid UUID
- self.assertEqual(UserManager.change_password(self.store, 'invalid-uuid', 'oldpass', 'newpass'), UserManager.INVALID_ID)
+ self.assertEqual(UserManager.change_password('invalid-uuid', 'oldpass', 'newpass'), UserManager.INVALID_ID)
# Non-existent user
- self.assertEqual(UserManager.change_password(self.store, uuid.uuid4(), 'oldpass', 'newpass'), UserManager.NO_SUCH_USER)
+ self.assertEqual(UserManager.change_password(uuid.uuid4(), 'oldpass', 'newpass'), UserManager.NO_SUCH_USER)
+ @db_session
def test_change_password2(self):
+ self.create_data()
+
# With existing users
for name in ['alice', 'bob', 'charlie']:
- self.assertEqual(UserManager.change_password2(self.store, name, 'newpass'), UserManager.SUCCESS)
- user = self.store.find(db.User, db.User.name == name).one()
- self.assertEqual(UserManager.try_auth(self.store, name, 'newpass'), (UserManager.SUCCESS, user))
- self.assertEqual(UserManager.try_auth(self.store, name, name.upper()), (UserManager.WRONG_PASS, None))
+ self.assertEqual(UserManager.change_password2(name, 'newpass'), UserManager.SUCCESS)
+ user = db.User.get(name = name)
+ self.assertEqual(UserManager.try_auth(name, 'newpass'), (UserManager.SUCCESS, user))
+ self.assertEqual(UserManager.try_auth(name, name.upper()), (UserManager.WRONG_PASS, None))
# Non-existent user
- self.assertEqual(UserManager.change_password2(self.store, 'null', 'newpass'), UserManager.NO_SUCH_USER)
+ self.assertEqual(UserManager.change_password2('null', 'newpass'), UserManager.NO_SUCH_USER)
def test_human_readable_error(self):
values = [ UserManager.SUCCESS, UserManager.INVALID_ID, UserManager.NO_SUCH_USER, UserManager.NAME_EXISTS,
diff --git a/tests/testbase.py b/tests/testbase.py
index bd4c2aa..c604fab 100644
--- a/tests/testbase.py
+++ b/tests/testbase.py
@@ -10,22 +10,20 @@
import inspect
import io
+import os
import shutil
import sys
import unittest
import tempfile
+from supysonic.db import init_database, release_database
from supysonic.config import DefaultConfig
from supysonic.managers.user import UserManager
-from supysonic.web import create_application, store
+from supysonic.web import create_application
class TestConfig(DefaultConfig):
TESTING = True
LOGGER_HANDLER_POLICY = 'never'
- BASE = {
- 'database_uri': 'sqlite:',
- 'scanner_extensions': None
- }
MIMETYPES = {
'mp3': 'audio/mpeg',
'weirdextension': 'application/octet-stream'
@@ -60,31 +58,37 @@ class TestBase(unittest.TestCase):
__with_api__ = False
def setUp(self):
+ self.__dbfile = tempfile.mkstemp()[1]
self.__dir = tempfile.mkdtemp()
config = TestConfig(self.__with_webui__, self.__with_api__)
+ config.BASE['database_uri'] = 'sqlite:///' + self.__dbfile
config.WEBAPP['cache_dir'] = self.__dir
+ init_database(config.BASE['database_uri'], True)
+ release_database()
+
app = create_application(config)
self.__ctx = app.app_context()
self.__ctx.push()
- self.store = store
- with io.open('schema/sqlite.sql', 'r') as sql:
- schema = sql.read()
- for statement in schema.split(';'):
- self.store.execute(statement)
- self.store.commit()
-
self.client = app.test_client()
- UserManager.add(self.store, 'alice', 'Alic3', 'test@example.com', True)
- UserManager.add(self.store, 'bob', 'B0b', 'bob@example.com', False)
+ UserManager.add('alice', 'Alic3', 'test@example.com', True)
+ UserManager.add('bob', 'B0b', 'bob@example.com', False)
+
+ @staticmethod
+ def __should_unload_module(module):
+ if module.startswith('supysonic'):
+ return not module.startswith('supysonic.db')
+ return False
def tearDown(self):
self.__ctx.pop()
+ release_database()
shutil.rmtree(self.__dir)
+ os.remove(self.__dbfile)
- to_unload = [ m for m in sys.modules if m.startswith('supysonic') ]
+ to_unload = [ m for m in sorted(sys.modules) if self.__should_unload_module(m) ]
for m in to_unload:
del sys.modules[m]