1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-22 08:56:17 +00:00

Merge branch 'pony'

This commit is contained in:
spl0k 2018-01-04 21:36:56 +01:00
commit 4b446f7121
48 changed files with 1865 additions and 1751 deletions

2
.gitignore vendored
View File

@ -66,3 +66,5 @@ Session.vim
.netrwhist
*~
*.orig

View File

@ -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.

View File

@ -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()

View File

@ -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

View File

@ -1,5 +1,5 @@
flask>=0.9
storm
pony
Pillow
simplejson
requests>=1.0.0

View File

@ -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()

View File

@ -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'))

View File

@ -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,

View File

@ -18,15 +18,15 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -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) ]
}
})

View File

@ -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)

View File

@ -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():

View File

@ -19,9 +19,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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({})

View File

@ -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",

View File

@ -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({})

View File

@ -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 ]
}})

View File

@ -19,10 +19,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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:

View File

@ -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:

View File

@ -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 = {

View File

@ -18,45 +18,41 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from 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

View File

@ -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)

View File

@ -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/<id>')
@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]))

View File

@ -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/<uid>')
@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/<uid>', 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/<uid>')
@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'))

View File

@ -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/<uid>')
@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/<uid>', 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/<uid>/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/<uid>/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/<uid>/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/<uid>/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/<uid>')
@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!')

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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):

View File

@ -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()

View File

@ -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):

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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 })

View File

@ -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')

View File

@ -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__':

View File

@ -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)))

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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('<h2>bob</h2>', 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)

View File

@ -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,

View File

@ -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,

View File

@ -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]