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]