From df63919634b2dc3c28f2063b71e656de1d4bfbea Mon Sep 17 00:00:00 2001 From: spl0k Date: Sat, 23 Dec 2017 22:59:04 +0100 Subject: [PATCH] Look at my pony, my pony is amazing --- supysonic/api/__init__.py | 19 ++-- supysonic/api/albums_songs.py | 152 +++++++++++++---------------- supysonic/api/annotation.py | 85 ++++++++-------- supysonic/api/browse.py | 55 ++++++----- supysonic/api/media.py | 58 ++++++----- supysonic/api/playlists.py | 56 ++++++----- supysonic/api/search.py | 8 +- supysonic/db.py | 5 +- tests/api/test_album_songs.py | 69 ++++++------- tests/api/test_annotation.py | 176 +++++++++++++++++----------------- tests/api/test_browse.py | 160 +++++++++++++++---------------- tests/api/test_media.py | 76 ++++++++------- tests/api/test_playlist.py | 118 +++++++++++------------ tests/api/test_transcoding.py | 13 ++- 14 files changed, 516 insertions(+), 534 deletions(-) diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py index d8e0c96..f35f647 100644 --- a/supysonic/api/__init__.py +++ b/supysonic/api/__init__.py @@ -99,11 +99,11 @@ def get_client_prefs(): client = request.values.get('c') with db_session: try: - prefs = ClientPrefs[request.user.id, client] + ClientPrefs[request.user.id, client] except ObjectNotFound: - prefs = ClientPrefs(user = User[request.user.id], client_name = client) + ClientPrefs(user = User[request.user.id], client_name = client) - request.prefs = prefs + request.client = client @app.after_request def set_headers(response): @@ -216,19 +216,20 @@ class ResponseHelper: return str(value).lower() return str(value) -def get_entity(req, ent, param = 'id'): +def get_entity(req, cls, param = 'id'): eid = req.values.get(param) if not eid: - return False, req.error_formatter(10, 'Missing %s id' % ent.__name__) + return False, req.error_formatter(10, 'Missing %s id' % cls.__name__) try: eid = uuid.UUID(eid) except: - return False, req.error_formatter(0, 'Invalid %s id' % ent.__name__) + return False, req.error_formatter(0, 'Invalid %s id' % cls.__name__) - entity = store.get(ent, eid) - if not entity: - return False, (req.error_formatter(70, '%s not found' % ent.__name__), 404) + try: + entity = cls[eid] + except ObjectNotFound: + return False, (req.error_formatter(70, '%s not found' % cls.__name__), 404) return True, entity diff --git a/supysonic/api/albums_songs.py b/supysonic/api/albums_songs.py index acc60c0..8c453ba 100644 --- a/supysonic/api/albums_songs.py +++ b/supysonic/api/albums_songs.py @@ -23,6 +23,7 @@ import uuid from datetime import timedelta from flask import request, current_app as app +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 @@ -40,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(): @@ -79,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(): @@ -131,76 +116,71 @@ def album_list_id3(): except: return request.error_formatter(0, 'Invalid parameter format') - query = store.find(Album) + query = Album.select() if ltype == 'random': - albums = [] - count = query.count() - - if not count: - return request.formatter({ 'albumList2': {} }) - - for _ in xrange(size): - x = random.choice(xrange(count)) - albums.append(query[x]) - - return request.formatter({ - 'albumList2': { - 'album': [ a.as_subsonic_album(request.user) for a in albums ] - } - }) + with db_session: + return request.formatter({ + 'albumList2': { + 'album': [ a.as_subsonic_album(request.user) for a in query.random(size) ] + } + }) elif ltype == 'newest': - query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Min(Track.created))) + query = query.order_by(lambda a: desc(min(a.tracks.created))) elif ltype == 'frequent': - query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Avg(Track.play_count))) + query = query.order_by(lambda a: desc(avg(a.tracks.play_count))) elif ltype == 'recent': - query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Max(Track.last_play))) + query = query.order_by(lambda a: desc(max(a.tracks.last_play))) elif ltype == 'starred': - query = query.find(StarredAlbum.starred_id == Album.id, User.id == StarredAlbum.user_id, User.name == request.username) + query = select(s.starred for s in StarredAlbum if s.user.id == request.user.id) elif ltype == 'alphabeticalByName': query = query.order_by(Album.name) elif ltype == 'alphabeticalByArtist': - query = query.find(Artist.id == Album.artist_id).order_by(Artist.name, Album.name) + query = query.order_by(lambda a: a.artist.name + a.name) else: return request.error_formatter(0, 'Unknown search type') - return request.formatter({ - 'albumList2': { - 'album': [ f.as_subsonic_album(request.user) for f in query[offset:offset+size] ] - } - }) + with db_session: + return request.formatter({ + 'albumList2': { + 'album': [ f.as_subsonic_album(request.user) for f in query.limit(size, offset) ] + } + }) @app.route('/rest/getNowPlaying.view', methods = [ 'GET', 'POST' ]) +@db_session def now_playing(): - query = store.find(User, Track.id == User.last_play_id) + query = User.select(lambda u: u.last_play is not None and u.last_play_date + timedelta(minutes = 3) > now()) return request.formatter({ 'nowPlaying': { 'entry': [ dict( - u.last_play.as_subsonic_child(request.user, request.prefs).items() + + u.last_play.as_subsonic_child(request.user, request.client).items() + { 'username': u.name, 'minutesAgo': (now() - u.last_play_date).seconds / 60, 'playerId': 0 }.items() - ) for u in query if u.last_play_date + timedelta(seconds = u.last_play.duration * 2) > now() ] + ) for u in query ] } }) @app.route('/rest/getStarred.view', methods = [ 'GET', 'POST' ]) +@db_session def get_starred(): - folders = store.find(StarredFolder, StarredFolder.user_id == User.id, User.name == request.username) + folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id) return request.formatter({ 'starred': { - 'artist': [ { 'id': str(sf.starred_id), 'name': sf.starred.name } for sf in folders.find(Folder.parent_id == StarredFolder.starred_id, Track.folder_id == Folder.id).config(distinct = True) ], - 'album': [ sf.starred.as_subsonic_child(request.user) for sf in folders.find(Track.folder_id == StarredFolder.starred_id).config(distinct = True) ], - 'song': [ st.starred.as_subsonic_child(request.user, request.prefs) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ] + 'artist': [ { 'id': str(sf.id), 'name': sf.name } for sf in folders.filter(lambda f: count(f.tracks) == 0) ], + 'album': [ sf.as_subsonic_child(request.user) for sf in folders.filter(lambda f: count(f.tracks) > 0) ], + 'song': [ st.as_subsonic_child(request.user, request.client) for st in select(s.starred for s in StarredTrack if s.user.id == request.user.id) ] } }) @app.route('/rest/getStarred2.view', methods = [ 'GET', 'POST' ]) +@db_session def get_starred_id3(): return request.formatter({ 'starred2': { - 'artist': [ sa.starred.as_subsonic_artist(request.user) for sa in store.find(StarredArtist, StarredArtist.user_id == User.id, User.name == request.username) ], - 'album': [ sa.starred.as_subsonic_album(request.user) for sa in store.find(StarredAlbum, StarredAlbum.user_id == User.id, User.name == request.username) ], - 'song': [ st.starred.as_subsonic_child(request.user, request.prefs) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ] + 'artist': [ sa.as_subsonic_artist(request.user) for sa in select(s.starred for s in StarredArtist if s.user.id == request.user.id) ], + 'album': [ sa.as_subsonic_album(request.user) for sa in select(s.starred for s in StarredAlbum if s.user.id == request.user.id) ], + 'song': [ st.as_subsonic_child(request.user, request.client) for st in select(s.starred for s in StarredTrack if s.user.id == request.user.id) ] } }) diff --git a/supysonic/api/annotation.py b/supysonic/api/annotation.py index 16e958a..3bad417 100644 --- a/supysonic/api/annotation.py +++ b/supysonic/api/annotation.py @@ -22,19 +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 . 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 """ @@ -42,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 """ @@ -71,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): @@ -105,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({}) @@ -129,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({}) @@ -148,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: @@ -189,7 +190,7 @@ def scrobble(): else: t = int(time.time()) - lfm = LastFm(app.config['LASTFM'], request.user, app.logger) + lfm = LastFm(app.config['LASTFM'], User[request.user.id], app.logger) if submission in (None, '', True, 'true', 'True', 1, '1'): lfm.scrobble(res, t) diff --git a/supysonic/api/browse.py b/supysonic/api/browse.py index 0f597f7..ac39dee 100644 --- a/supysonic/api/browse.py +++ b/supysonic/api/browse.py @@ -22,23 +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 . 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') @@ -49,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: @@ -100,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: @@ -113,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 = '#' @@ -146,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: @@ -159,23 +164,25 @@ def artist_info(): return request.formatter({ 'artist': info }) @app.route('/rest/getAlbum.view', methods = [ 'GET', 'POST' ]) +@db_session def album_info(): status, res = get_entity(request, Album) if not status: return res info = res.as_subsonic_album(request.user) - info['song'] = [ t.as_subsonic_child(request.user, request.prefs) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] + info['song'] = [ t.as_subsonic_child(request.user, request.client) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] return request.formatter({ 'album': info }) @app.route('/rest/getSong.view', methods = [ 'GET', 'POST' ]) +@db_session def track_info(): status, res = get_entity(request, Track) if not status: return res - return request.formatter({ 'song': res.as_subsonic_child(request.user, request.prefs) }) + return request.formatter({ 'song': res.as_subsonic_child(request.user, request.client) }) @app.route('/rest/getVideos.view', methods = [ 'GET', 'POST' ]) def list_videos(): diff --git a/supysonic/api/media.py b/supysonic/api/media.py index 3f91dc3..8531087 100644 --- a/supysonic/api/media.py +++ b/supysonic/api/media.py @@ -26,6 +26,7 @@ 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 @@ -42,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: @@ -56,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: @@ -120,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 @@ -136,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 @@ -175,25 +180,26 @@ def lyrics(): if not title: return request.error_formatter(10, 'Missing title parameter') - query = store.find(Track, Album.id == Track.album_id, Artist.id == Album.artist_id, Track.title.like(title), Artist.name.like(artist)) - for track in query: - lyrics_path = os.path.splitext(track.path)[0] + '.txt' - if os.path.exists(lyrics_path): - app.logger.debug('Found lyrics file: ' + lyrics_path) + with db_session: + query = Track.select(lambda t: title in t.title and artist in t.artist.name) + for track in query: + lyrics_path = os.path.splitext(track.path)[0] + '.txt' + if os.path.exists(lyrics_path): + app.logger.debug('Found lyrics file: ' + lyrics_path) - try: - lyrics = read_file_as_unicode(lyrics_path) - except UnicodeError: - # Lyrics file couldn't be decoded. Rather than displaying an error, try with the potential next files or - # return no lyrics. Log it anyway. - app.logger.warn('Unsupported encoding for lyrics file ' + lyrics_path) - continue + try: + lyrics = read_file_as_unicode(lyrics_path) + except UnicodeError: + # Lyrics file couldn't be decoded. Rather than displaying an error, try with the potential next files or + # return no lyrics. Log it anyway. + app.logger.warn('Unsupported encoding for lyrics file ' + lyrics_path) + continue - return request.formatter({ 'lyrics': { - 'artist': track.album.artist.name, - 'title': track.title, - '_value_': lyrics - } }) + return request.formatter({ 'lyrics': { + 'artist': track.album.artist.name, + 'title': track.title, + '_value_': lyrics + } }) try: r = requests.get("http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect", diff --git a/supysonic/api/playlists.py b/supysonic/api/playlists.py index 61ea77c..8810d23 100644 --- a/supysonic/api/playlists.py +++ b/supysonic/api/playlists.py @@ -21,6 +21,8 @@ import uuid from flask import request, current_app as app +from pony.orm import db_session, rollback +from pony.orm import ObjectNotFound from ..db import Playlist, User, Track @@ -28,37 +30,40 @@ 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') @@ -69,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 @@ -137,13 +141,13 @@ def update_playlist(): playlist.public = public in (True, 'True', 'true', 1, '1') for sid in to_add: - track = store.get(Track, sid) - if not track: + try: + track = Track[sid] + except ObjectNotFound: return request.error_formatter(70, 'Unknown song') playlist.add(track) playlist.remove_at_indexes(to_remove) - store.commit() return request.formatter({}) diff --git a/supysonic/api/search.py b/supysonic/api/search.py index 08d535b..07c61b0 100644 --- a/supysonic/api/search.py +++ b/supysonic/api/search.py @@ -56,7 +56,7 @@ def old_search(): 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 ] + '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') @@ -65,7 +65,7 @@ def old_search(): 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] ] + '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' ]) @@ -94,7 +94,7 @@ def new_search(): 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.prefs) for t in songs ] + 'song': [ t.as_subsonic_child(request.user, request.client) for t in songs ] }}) @app.route('/rest/search3.view', methods = [ 'GET', 'POST' ]) @@ -123,6 +123,6 @@ def search_id3(): 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.prefs) for t in songs ] + 'song': [ t.as_subsonic_child(request.user, request.client) for t in songs ] }}) diff --git a/supysonic/db.py b/supysonic/db.py index b8adfe7..9c8d0ad 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -177,7 +177,7 @@ class Track(db.Entity): stars = Set(lambda: StarredTrack) ratings = Set(lambda: RatingTrack) - def as_subsonic_child(self, user, prefs): + def as_subsonic_child(self, user, client): info = { 'id': str(self.id), 'parent': str(self.folder.id), @@ -221,7 +221,8 @@ class Track(db.Entity): 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' diff --git a/tests/api/test_album_songs.py b/tests/api/test_album_songs.py index 7319041..b205bde 100644 --- a/tests/api/test_album_songs.py +++ b/tests/api/test_album_songs.py @@ -11,6 +11,8 @@ import uuid +from pony.orm import db_session + from supysonic.db import Folder, Artist, Album, Track from .apitestbase import ApiTestBase @@ -22,34 +24,25 @@ class AlbumSongsTestCase(ApiTestBase): def setUp(self): super(AlbumSongsTestCase, self).setUp() - folder = Folder() - folder.name = 'Root' - folder.root = True - folder.path = 'tests/assets' + with db_session: + folder = Folder(name = 'Root', root = True, path = 'tests/assets') + artist = Artist(name = 'Artist') + album = Album(name = 'Album', artist = artist) - artist = Artist() - artist.name = 'Artist' - - album = Album() - album.name = 'Album' - album.artist = artist - - track = Track() - track.title = 'Track' - track.album = album - track.artist = artist - track.disc = 1 - track.number = 1 - track.path = 'tests/assets/empty' - track.folder = folder - track.root_folder = folder - track.duration = 2 - track.bitrate = 320 - track.content_type = 'audio/mpeg' - track.last_modification = 0 - - self.store.add(track) - self.store.commit() + track = Track( + title = 'Track', + album = album, + artist = artist, + disc = 1, + number = 1, + path = 'tests/assets/empty', + folder = folder, + root_folder = folder, + duration = 2, + bitrate = 320, + content_type = 'audio/mpeg', + last_modification = 0 + ) def test_get_album_list(self): self._make_request('getAlbumList', error = 10) @@ -63,11 +56,9 @@ class AlbumSongsTestCase(ApiTestBase): self._make_request('getAlbumList', { 'type': t }, tag = 'albumList', skip_post = True) rv, child = self._make_request('getAlbumList', { 'type': 'random' }, tag = 'albumList', skip_post = True) - self.assertEqual(len(child), 10) - rv, child = self._make_request('getAlbumList', { 'type': 'random', 'size': 3 }, tag = 'albumList', skip_post = True) - self.assertEqual(len(child), 3) - self.store.remove(self.store.find(Folder).one()) + with db_session: + Folder.get().delete() rv, child = self._make_request('getAlbumList', { 'type': 'random' }, tag = 'albumList') self.assertEqual(len(child), 0) @@ -82,12 +73,10 @@ class AlbumSongsTestCase(ApiTestBase): self._make_request('getAlbumList2', { 'type': t }, tag = 'albumList2', skip_post = True) rv, child = self._make_request('getAlbumList2', { 'type': 'random' }, tag = 'albumList2', skip_post = True) - self.assertEqual(len(child), 10) - rv, child = self._make_request('getAlbumList2', { 'type': 'random', 'size': 3 }, tag = 'albumList2', skip_post = True) - self.assertEqual(len(child), 3) - self.store.remove(self.store.find(Track).one()) - self.store.remove(self.store.find(Album).one()) + with db_session: + Track.get().delete() + Album.get().delete() rv, child = self._make_request('getAlbumList2', { 'type': 'random' }, tag = 'albumList2') self.assertEqual(len(child), 0) @@ -98,12 +87,10 @@ class AlbumSongsTestCase(ApiTestBase): self._make_request('getRandomSongs', { 'musicFolderId': 'idid' }, error = 0) self._make_request('getRandomSongs', { 'musicFolderId': uuid.uuid4() }, error = 70) - rv, child = self._make_request('getRandomSongs', tag = 'randomSongs') - self.assertEqual(len(child), 10) - rv, child = self._make_request('getRandomSongs', { 'size': 3 }, tag = 'randomSongs') - self.assertEqual(len(child), 3) + rv, child = self._make_request('getRandomSongs', tag = 'randomSongs', skip_post = True) - fid = self.store.find(Folder).one().id + with db_session: + fid = Folder.get().id self._make_request('getRandomSongs', { 'fromYear': -52, 'toYear': '1984', 'genre': 'some cryptic subgenre youve never heard of', 'musicFolderId': fid }, tag = 'randomSongs') def test_now_playing(self): diff --git a/tests/api/test_annotation.py b/tests/api/test_annotation.py index ed74587..0412714 100644 --- a/tests/api/test_annotation.py +++ b/tests/api/test_annotation.py @@ -11,6 +11,8 @@ import uuid +from pony.orm import db_session + from supysonic.db import Folder, Artist, Album, Track, User, ClientPrefs from .apitestbase import ApiTestBase @@ -19,45 +21,32 @@ class AnnotationTestCase(ApiTestBase): def setUp(self): super(AnnotationTestCase, self).setUp() - root = Folder() - root.name = 'Root' - root.root = True - root.path = 'tests/assets' + with db_session: + root = Folder(name = 'Root', root = True, path = 'tests') + folder = Folder(name = 'Folder', path = 'tests/assets', parent = root) + artist = Artist(name = 'Artist') + album = Album(name = 'Album', artist = artist) - folder = Folder() - folder.name = 'Folder' - folder.path = 'tests/assets' - folder.parent = root + track = Track( + title = 'Track', + album = album, + artist = artist, + disc = 1, + number = 1, + path = 'tests/assets/empty', + folder = folder, + root_folder = root, + duration = 2, + bitrate = 320, + content_type = 'audio/mpeg', + last_modification = 0 + ) - artist = Artist() - artist.name = 'Artist' - - album = Album() - album.name = 'Album' - album.artist = artist - - track = Track() - track.title = 'Track' - track.album = album - track.artist = artist - track.disc = 1 - track.number = 1 - track.path = 'tests/assets/empty' - track.folder = folder - track.root_folder = root - track.duration = 2 - track.bitrate = 320 - track.content_type = 'audio/mpeg' - track.last_modification = 0 - - self.store.add(track) - self.store.commit() - - self.folder = folder - self.artist = artist - self.album = album - self.track = track - self.user = self.store.find(User, User.name == 'alice').one() + self.folderid = folder.id + self.artistid = artist.id + self.albumid = album.id + self.trackid = track.id + self.user = User.get(name = 'alice') def test_star(self): self._make_request('star', error = 10) @@ -68,88 +57,101 @@ class AnnotationTestCase(ApiTestBase): self._make_request('star', { 'albumId': str(uuid.uuid4()) }, error = 70) self._make_request('star', { 'artistId': str(uuid.uuid4()) }, error = 70) - self._make_request('star', { 'id': str(self.artist.id) }, error = 70, skip_xsd = True) - self._make_request('star', { 'id': str(self.album.id) }, error = 70, skip_xsd = True) - self._make_request('star', { 'id': str(self.track.id) }, skip_post = True) - self.assertIn('starred', self.track.as_subsonic_child(self.user, ClientPrefs())) - self._make_request('star', { 'id': str(self.track.id) }, error = 0, skip_xsd = True) + self._make_request('star', { 'id': str(self.artistid) }, error = 70, skip_xsd = True) + self._make_request('star', { 'id': str(self.albumid) }, error = 70, skip_xsd = True) + self._make_request('star', { 'id': str(self.trackid) }, skip_post = True) + with db_session: + self.assertIn('starred', Track[self.trackid].as_subsonic_child(self.user, 'tests')) + self._make_request('star', { 'id': str(self.trackid) }, error = 0, skip_xsd = True) - self._make_request('star', { 'id': str(self.folder.id) }, skip_post = True) - self.assertIn('starred', self.folder.as_subsonic_child(self.user)) - self._make_request('star', { 'id': str(self.folder.id) }, error = 0, skip_xsd = True) + self._make_request('star', { 'id': str(self.folderid) }, skip_post = True) + with db_session: + self.assertIn('starred', Folder[self.folderid].as_subsonic_child(self.user)) + self._make_request('star', { 'id': str(self.folderid) }, error = 0, skip_xsd = True) - self._make_request('star', { 'albumId': str(self.folder.id) }, error = 70) - self._make_request('star', { 'albumId': str(self.artist.id) }, error = 70) - self._make_request('star', { 'albumId': str(self.track.id) }, error = 70) - self._make_request('star', { 'albumId': str(self.album.id) }, skip_post = True) - self.assertIn('starred', self.album.as_subsonic_album(self.user)) - self._make_request('star', { 'albumId': str(self.album.id) }, error = 0) + self._make_request('star', { 'albumId': str(self.folderid) }, error = 70) + self._make_request('star', { 'albumId': str(self.artistid) }, error = 70) + self._make_request('star', { 'albumId': str(self.trackid) }, error = 70) + self._make_request('star', { 'albumId': str(self.albumid) }, skip_post = True) + with db_session: + self.assertIn('starred', Album[self.albumid].as_subsonic_album(self.user)) + self._make_request('star', { 'albumId': str(self.albumid) }, error = 0) - self._make_request('star', { 'artistId': str(self.folder.id) }, error = 70) - self._make_request('star', { 'artistId': str(self.album.id) }, error = 70) - self._make_request('star', { 'artistId': str(self.track.id) }, error = 70) - self._make_request('star', { 'artistId': str(self.artist.id) }, skip_post = True) - self.assertIn('starred', self.artist.as_subsonic_artist(self.user)) - self._make_request('star', { 'artistId': str(self.artist.id) }, error = 0) + self._make_request('star', { 'artistId': str(self.folderid) }, error = 70) + self._make_request('star', { 'artistId': str(self.albumid) }, error = 70) + self._make_request('star', { 'artistId': str(self.trackid) }, error = 70) + self._make_request('star', { 'artistId': str(self.artistid) }, skip_post = True) + with db_session: + self.assertIn('starred', Artist[self.artistid].as_subsonic_artist(self.user)) + self._make_request('star', { 'artistId': str(self.artistid) }, error = 0) def test_unstar(self): - self._make_request('star', { 'id': [ str(self.folder.id), str(self.track.id) ], 'artistId': str(self.artist.id), 'albumId': str(self.album.id) }, skip_post = True) + self._make_request('star', { 'id': [ str(self.folderid), str(self.trackid) ], 'artistId': str(self.artistid), 'albumId': str(self.albumid) }, skip_post = True) self._make_request('unstar', error = 10) self._make_request('unstar', { 'id': 'unknown' }, error = 0, skip_xsd = True) self._make_request('unstar', { 'albumId': 'unknown' }, error = 0) self._make_request('unstar', { 'artistId': 'unknown' }, error = 0) - self._make_request('unstar', { 'id': str(self.track.id) }, skip_post = True) - self.assertNotIn('starred', self.track.as_subsonic_child(self.user, ClientPrefs())) + self._make_request('unstar', { 'id': str(self.trackid) }, skip_post = True) + with db_session: + self.assertNotIn('starred', Track[self.trackid].as_subsonic_child(self.user, 'tests')) - self._make_request('unstar', { 'id': str(self.folder.id) }, skip_post = True) - self.assertNotIn('starred', self.folder.as_subsonic_child(self.user)) + self._make_request('unstar', { 'id': str(self.folderid) }, skip_post = True) + with db_session: + self.assertNotIn('starred', Folder[self.folderid].as_subsonic_child(self.user)) - self._make_request('unstar', { 'albumId': str(self.album.id) }, skip_post = True) - self.assertNotIn('starred', self.album.as_subsonic_album(self.user)) + self._make_request('unstar', { 'albumId': str(self.albumid) }, skip_post = True) + with db_session: + self.assertNotIn('starred', Album[self.albumid].as_subsonic_album(self.user)) - self._make_request('unstar', { 'artistId': str(self.artist.id) }, skip_post = True) - self.assertNotIn('starred', self.artist.as_subsonic_artist(self.user)) + self._make_request('unstar', { 'artistId': str(self.artistid) }, skip_post = True) + with db_session: + self.assertNotIn('starred', Artist[self.artistid].as_subsonic_artist(self.user)) def test_set_rating(self): self._make_request('setRating', error = 10) - self._make_request('setRating', { 'id': str(self.track.id) }, error = 10) + self._make_request('setRating', { 'id': str(self.trackid) }, error = 10) self._make_request('setRating', { 'rating': 3 }, error = 10) self._make_request('setRating', { 'id': 'string', 'rating': 3 }, error = 0) self._make_request('setRating', { 'id': str(uuid.uuid4()), 'rating': 3 }, error = 70) - self._make_request('setRating', { 'id': str(self.artist.id), 'rating': 3 }, error = 70) - self._make_request('setRating', { 'id': str(self.album.id), 'rating': 3 }, error = 70) - self._make_request('setRating', { 'id': str(self.track.id), 'rating': 'string' }, error = 0) - self._make_request('setRating', { 'id': str(self.track.id), 'rating': -1 }, error = 0) - self._make_request('setRating', { 'id': str(self.track.id), 'rating': 6 }, error = 0) + self._make_request('setRating', { 'id': str(self.artistid), 'rating': 3 }, error = 70) + self._make_request('setRating', { 'id': str(self.albumid), 'rating': 3 }, error = 70) + self._make_request('setRating', { 'id': str(self.trackid), 'rating': 'string' }, error = 0) + self._make_request('setRating', { 'id': str(self.trackid), 'rating': -1 }, error = 0) + self._make_request('setRating', { 'id': str(self.trackid), 'rating': 6 }, error = 0) - prefs = ClientPrefs() - self.assertNotIn('userRating', self.track.as_subsonic_child(self.user, prefs)) + with db_session: + self.assertNotIn('userRating', Track[self.trackid].as_subsonic_child(self.user, 'tests')) for i in range(1, 6): - self._make_request('setRating', { 'id': str(self.track.id), 'rating': i }, skip_post = True) - self.assertEqual(self.track.as_subsonic_child(self.user, prefs)['userRating'], i) - self._make_request('setRating', { 'id': str(self.track.id), 'rating': 0 }, skip_post = True) - self.assertNotIn('userRating', self.track.as_subsonic_child(self.user, prefs)) + self._make_request('setRating', { 'id': str(self.trackid), 'rating': i }, skip_post = True) + with db_session: + self.assertEqual(Track[self.trackid].as_subsonic_child(self.user, 'tests')['userRating'], i) - self.assertNotIn('userRating', self.folder.as_subsonic_child(self.user)) + self._make_request('setRating', { 'id': str(self.trackid), 'rating': 0 }, skip_post = True) + with db_session: + self.assertNotIn('userRating', Track[self.trackid].as_subsonic_child(self.user, 'tests')) + + self.assertNotIn('userRating', Folder[self.folderid].as_subsonic_child(self.user)) for i in range(1, 6): - self._make_request('setRating', { 'id': str(self.folder.id), 'rating': i }, skip_post = True) - self.assertEqual(self.folder.as_subsonic_child(self.user)['userRating'], i) - self._make_request('setRating', { 'id': str(self.folder.id), 'rating': 0 }, skip_post = True) - self.assertNotIn('userRating', self.folder.as_subsonic_child(self.user)) + self._make_request('setRating', { 'id': str(self.folderid), 'rating': i }, skip_post = True) + with db_session: + self.assertEqual(Folder[self.folderid].as_subsonic_child(self.user)['userRating'], i) + self._make_request('setRating', { 'id': str(self.folderid), 'rating': 0 }, skip_post = True) + with db_session: + self.assertNotIn('userRating', Folder[self.folderid].as_subsonic_child(self.user)) def test_scrobble(self): self._make_request('scrobble', error = 10) self._make_request('scrobble', { 'id': 'song' }, error = 0) self._make_request('scrobble', { 'id': str(uuid.uuid4()) }, error = 70) - self._make_request('scrobble', { 'id': str(self.folder.id) }, error = 70) + self._make_request('scrobble', { 'id': str(self.folderid) }, error = 70) self.skipTest('Weird request context/logger issue at exit') - self._make_request('scrobble', { 'id': str(self.track.id) }) - self._make_request('scrobble', { 'id': str(self.track.id), 'submission': True }) - self._make_request('scrobble', { 'id': str(self.track.id), 'submission': False }) + self._make_request('scrobble', { 'id': str(self.trackid) }) + self._make_request('scrobble', { 'id': str(self.trackid), 'submission': True }) + self._make_request('scrobble', { 'id': str(self.trackid), 'submission': False }) if __name__ == '__main__': unittest.main() diff --git a/tests/api/test_browse.py b/tests/api/test_browse.py index e026a0b..b42891b 100644 --- a/tests/api/test_browse.py +++ b/tests/api/test_browse.py @@ -9,10 +9,12 @@ # # Distributed under terms of the GNU AGPLv3 license. -from lxml import etree import time import uuid +from lxml import etree +from pony.orm import db_session + from supysonic.db import Folder, Artist, Album, Track from .apitestbase import ApiTestBase @@ -21,64 +23,53 @@ class BrowseTestCase(ApiTestBase): def setUp(self): super(BrowseTestCase, self).setUp() - empty = Folder() - empty.root = True - empty.name = 'Empty root' - empty.path = '/tmp' - self.store.add(empty) + with db_session: + Folder(root = True, name = 'Empty root', path = '/tmp') + root = Folder(root = True, name = 'Root folder', path = 'tests/assets') - root = Folder() - root.root = True - root.name = 'Root folder' - root.path = 'tests/assets' - self.store.add(root) + for letter in 'ABC': + folder = Folder( + name = letter + 'rtist', + path = 'tests/assets/{}rtist'.format(letter), + parent = root + ) - for letter in 'ABC': - folder = Folder() - folder.name = letter + 'rtist' - folder.path = 'tests/assets/{}rtist'.format(letter) - folder.parent = root + artist = Artist(name = letter + 'rtist') - artist = Artist() - artist.name = letter + 'rtist' + for lether in 'AB': + afolder = Folder( + name = letter + lether + 'lbum', + path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether), + parent = folder + ) - for lether in 'AB': - afolder = Folder() - afolder.name = letter + lether + 'lbum' - afolder.path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether) - afolder.parent = folder + album = Album(name = letter + lether + 'lbum', artist = artist) - album = Album() - album.name = letter + lether + 'lbum' - album.artist = artist + for num, song in enumerate([ 'One', 'Two', 'Three' ]): + track = Track( + disc = 1, + number = num, + title = song, + duration = 2, + album = album, + artist = artist, + bitrate = 320, + path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song), + content_type = 'audio/mpeg', + last_modification = 0, + root_folder = root, + folder = afolder + ) - for num, song in enumerate([ 'One', 'Two', 'Three' ]): - track = Track() - track.disc = 1 - track.number = num - track.title = song - track.duration = 2 - track.album = album - track.artist = artist - track.bitrate = 320 - track.path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song) - track.content_type = 'audio/mpeg' - track.last_modification = 0 - track.root_folder = root - track.folder = afolder - self.store.add(track) - - self.store.commit() - - self.assertEqual(self.store.find(Folder).count(), 11) - self.assertEqual(self.store.find(Folder, Folder.root == True).count(), 2) - self.assertEqual(self.store.find(Artist).count(), 3) - self.assertEqual(self.store.find(Album).count(), 6) - self.assertEqual(self.store.find(Track).count(), 18) + self.assertEqual(Folder.select().count(), 11) + self.assertEqual(Folder.select(lambda f: f.root).count(), 2) + self.assertEqual(Artist.select().count(), 3) + self.assertEqual(Album.select().count(), 6) + self.assertEqual(Track.select().count(), 18) def test_get_music_folders(self): # Do not validate against the XSD here, this is the only place where the API should return ids as ints - # all our ids are uuids :/ + # all our ids are uuids :/ rv, child = self._make_request('getMusicFolders', tag = 'musicFolders', skip_xsd = True) self.assertEqual(len(child), 2) self.assertSequenceEqual(sorted(self._xpath(child, './musicFolder/@name')), [ 'Empty root', 'Root folder' ]) @@ -91,7 +82,8 @@ class BrowseTestCase(ApiTestBase): rv, child = self._make_request('getIndexes', { 'ifModifiedSince': int(time.time() * 1000 + 1000) }, tag = 'indexes') self.assertEqual(len(child), 0) - fid = self.store.find(Folder, Folder.name == 'Empty root').one().id + with db_session: + fid = Folder.get(name = 'Empty root').id rv, child = self._make_request('getIndexes', { 'musicFolderId': str(fid) }, tag = 'indexes') self.assertEqual(len(child), 0) @@ -108,18 +100,19 @@ class BrowseTestCase(ApiTestBase): self._make_request('getMusicDirectory', { 'id': str(uuid.uuid4()) }, error = 70) # should test with folders with both children folders and tracks. this code would break in that case - for f in self.store.find(Folder): - rv, child = self._make_request('getMusicDirectory', { 'id': str(f.id) }, tag = 'directory') - self.assertEqual(child.get('id'), str(f.id)) - self.assertEqual(child.get('name'), f.name) - self.assertEqual(len(child), f.children.count() + f.tracks.count()) - for dbc, xmlc in zip(sorted(f.children, key = lambda c: c.name), sorted(child, key = lambda c: c.get('title'))): - self.assertEqual(dbc.name, xmlc.get('title')) - self.assertEqual(xmlc.get('artist'), f.name) - self.assertEqual(xmlc.get('parent'), str(f.id)) - for t, xmlc in zip(sorted(f.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))): - self.assertEqual(t.title, xmlc.get('title')) - self.assertEqual(xmlc.get('parent'), str(f.id)) + with db_session: + for f in Folder.select(): + rv, child = self._make_request('getMusicDirectory', { 'id': str(f.id) }, tag = 'directory') + self.assertEqual(child.get('id'), str(f.id)) + self.assertEqual(child.get('name'), f.name) + self.assertEqual(len(child), f.children.count() + f.tracks.count()) + for dbc, xmlc in zip(sorted(f.children, key = lambda c: c.name), sorted(child, key = lambda c: c.get('title'))): + self.assertEqual(dbc.name, xmlc.get('title')) + self.assertEqual(xmlc.get('artist'), f.name) + self.assertEqual(xmlc.get('parent'), str(f.id)) + for t, xmlc in zip(sorted(f.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))): + self.assertEqual(t.title, xmlc.get('title')) + self.assertEqual(xmlc.get('parent'), str(f.id)) def test_get_artists(self): # same as getIndexes standard case @@ -138,38 +131,41 @@ class BrowseTestCase(ApiTestBase): self._make_request('getArtist', { 'id': 'artist' }, error = 0) self._make_request('getArtist', { 'id': str(uuid.uuid4()) }, error = 70) - for ar in self.store.find(Artist): - rv, child = self._make_request('getArtist', { 'id': str(ar.id) }, tag = 'artist') - self.assertEqual(child.get('id'), str(ar.id)) - self.assertEqual(child.get('albumCount'), str(len(child))) - self.assertEqual(len(child), ar.albums.count()) - for dal, xal in zip(sorted(ar.albums, key = lambda a: a.name), sorted(child, key = lambda c: c.get('name'))): - self.assertEqual(dal.name, xal.get('name')) - self.assertEqual(xal.get('artist'), ar.name) # could break with a better dataset - self.assertEqual(xal.get('artistId'), str(ar.id)) # see above + with db_session: + for ar in Artist.select(): + rv, child = self._make_request('getArtist', { 'id': str(ar.id) }, tag = 'artist') + self.assertEqual(child.get('id'), str(ar.id)) + self.assertEqual(child.get('albumCount'), str(len(child))) + self.assertEqual(len(child), ar.albums.count()) + for dal, xal in zip(sorted(ar.albums, key = lambda a: a.name), sorted(child, key = lambda c: c.get('name'))): + self.assertEqual(dal.name, xal.get('name')) + self.assertEqual(xal.get('artist'), ar.name) # could break with a better dataset + self.assertEqual(xal.get('artistId'), str(ar.id)) # see above def test_get_album(self): self._make_request('getAlbum', error = 10) self._make_request('getAlbum', { 'id': 'nastynasty' }, error = 0) self._make_request('getAlbum', { 'id': str(uuid.uuid4()) }, error = 70) - a = self.store.find(Album)[0] - rv, child = self._make_request('getAlbum', { 'id': str(a.id) }, tag = 'album') - self.assertEqual(child.get('id'), str(a.id)) - self.assertEqual(child.get('songCount'), str(len(child))) + with db_session: + a = Album.select().first() + rv, child = self._make_request('getAlbum', { 'id': str(a.id) }, tag = 'album') + self.assertEqual(child.get('id'), str(a.id)) + self.assertEqual(child.get('songCount'), str(len(child))) - self.assertEqual(len(child), a.tracks.count()) - for dal, xal in zip(sorted(a.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))): - self.assertEqual(dal.title, xal.get('title')) - self.assertEqual(xal.get('album'), a.name) - self.assertEqual(xal.get('albumId'), str(a.id)) + self.assertEqual(len(child), a.tracks.count()) + for dal, xal in zip(sorted(a.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))): + self.assertEqual(dal.title, xal.get('title')) + self.assertEqual(xal.get('album'), a.name) + self.assertEqual(xal.get('albumId'), str(a.id)) def test_get_song(self): self._make_request('getSong', error = 10) self._make_request('getSong', { 'id': 'nastynasty' }, error = 0) self._make_request('getSong', { 'id': str(uuid.uuid4()) }, error = 70) - s = self.store.find(Track)[0] + with db_session: + s = Track.select().first() self._make_request('getSong', { 'id': str(s.id) }, tag = 'song') def test_get_videos(self): diff --git a/tests/api/test_media.py b/tests/api/test_media.py index c2070fa..347f493 100644 --- a/tests/api/test_media.py +++ b/tests/api/test_media.py @@ -11,8 +11,10 @@ import os.path import uuid + from io import BytesIO from PIL import Image +from pony.orm import db_session from supysonic.db import Folder, Artist, Album, Track @@ -22,69 +24,69 @@ class MediaTestCase(ApiTestBase): def setUp(self): super(MediaTestCase, self).setUp() - self.folder = Folder() - self.folder.name = 'Root' - self.folder.path = os.path.abspath('tests/assets') - self.folder.root = True - self.folder.has_cover_art = True # 420x420 PNG + with db_session: + folder = Folder( + name = 'Root', + path = os.path.abspath('tests/assets'), + root = True, + has_cover_art = True # 420x420 PNG + ) + self.folderid = folder.id - artist = Artist() - artist.name = 'Artist' + artist = Artist(name = 'Artist') + album = Album(artist = artist, name = 'Album') - album = Album() - album.artist = artist - album.name = 'Album' - - self.track = Track() - self.track.title = '23bytes' - self.track.number = 1 - self.track.disc = 1 - self.track.artist = artist - self.track.album = album - self.track.path = os.path.abspath('tests/assets/23bytes') - self.track.root_folder = self.folder - self.track.folder = self.folder - self.track.duration = 2 - self.track.bitrate = 320 - self.track.content_type = 'audio/mpeg' - self.track.last_modification = 0 - - self.store.add(self.track) - self.store.commit() + track = Track( + title = '23bytes', + number = 1, + disc = 1, + artist = artist, + album = album, + path = os.path.abspath('tests/assets/23bytes'), + root_folder = folder, + folder = folder, + duration = 2, + bitrate = 320, + content_type = 'audio/mpeg', + last_modification = 0 + ) + self.trackid = track.id def test_stream(self): self._make_request('stream', error = 10) self._make_request('stream', { 'id': 'string' }, error = 0) self._make_request('stream', { 'id': str(uuid.uuid4()) }, error = 70) - self._make_request('stream', { 'id': str(self.folder.id) }, error = 70) - self._make_request('stream', { 'id': str(self.track.id), 'maxBitRate': 'string' }, error = 0) + self._make_request('stream', { 'id': str(self.folderid) }, error = 70) + self._make_request('stream', { 'id': str(self.trackid), 'maxBitRate': 'string' }, error = 0) - rv = self.client.get('/rest/stream.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.track.id) }) + rv = self.client.get('/rest/stream.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.trackid) }) self.assertEqual(rv.status_code, 200) self.assertEqual(rv.mimetype, 'audio/mpeg') self.assertEqual(len(rv.data), 23) - self.assertEqual(self.track.play_count, 1) + with db_session: + self.assertEqual(Track[self.trackid].play_count, 1) def test_download(self): self._make_request('download', error = 10) self._make_request('download', { 'id': 'string' }, error = 0) self._make_request('download', { 'id': str(uuid.uuid4()) }, error = 70) - self._make_request('download', { 'id': str(self.folder.id) }, error = 70) + self._make_request('download', { 'id': str(self.folderid) }, error = 70) - rv = self.client.get('/rest/download.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.track.id) }) + rv = self.client.get('/rest/download.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.trackid) }) self.assertEqual(rv.status_code, 200) self.assertEqual(rv.mimetype, 'audio/mpeg') self.assertEqual(len(rv.data), 23) - self.assertEqual(self.track.play_count, 0) + with db_session: + self.assertEqual(Track[self.trackid].play_count, 0) def test_get_cover_art(self): self._make_request('getCoverArt', error = 10) self._make_request('getCoverArt', { 'id': 'string' }, error = 0) self._make_request('getCoverArt', { 'id': str(uuid.uuid4()) }, error = 70) - self._make_request('getCoverArt', { 'id': str(self.track.id) }, error = 70) - self._make_request('getCoverArt', { 'id': str(self.folder.id), 'size': 'large' }, error = 0) + self._make_request('getCoverArt', { 'id': str(self.trackid) }, error = 70) + self._make_request('getCoverArt', { 'id': str(self.folderid), 'size': 'large' }, error = 0) - args = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.folder.id) } + args = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.folderid) } rv = self.client.get('/rest/getCoverArt.view', query_string = args) self.assertEqual(rv.status_code, 200) self.assertEqual(rv.mimetype, 'image/jpeg') diff --git a/tests/api/test_playlist.py b/tests/api/test_playlist.py index 3e164b5..0050696 100644 --- a/tests/api/test_playlist.py +++ b/tests/api/test_playlist.py @@ -11,6 +11,8 @@ import uuid +from pony.orm import db_session + from supysonic.db import Folder, Artist, Album, Track, Playlist, User from .apitestbase import ApiTestBase @@ -19,63 +21,42 @@ class PlaylistTestCase(ApiTestBase): def setUp(self): super(PlaylistTestCase, self).setUp() - root = Folder() - root.root = True - root.name = 'Root folder' - root.path = 'tests/assets' - self.store.add(root) + with db_session: + root = Folder(root = True, name = 'Root folder', path = 'tests/assets') + artist = Artist(name = 'Artist') + album = Album(name = 'Album', artist = artist) - artist = Artist() - artist.name = 'Artist' + songs = {} + for num, song in enumerate([ 'One', 'Two', 'Three', 'Four' ]): + track = Track( + disc = 1, + number = num, + title = song, + duration = 2, + album = album, + artist = artist, + bitrate = 320, + path = 'tests/assets/' + song, + content_type = 'audio/mpeg', + last_modification = 0, + root_folder = root, + folder = root + ) + songs[song] = track - album = Album() - album.name = 'Album' - album.artist = artist + users = { u.name: u for u in User.select() } - songs = {} - for num, song in enumerate([ 'One', 'Two', 'Three', 'Four' ]): - track = Track() - track.disc = 1 - track.number = num - track.title = song - track.duration = 2 - track.album = album - track.artist = artist - track.bitrate = 320 - track.path = 'tests/assets/empty' - track.content_type = 'audio/mpeg' - track.last_modification = 0 - track.root_folder = root - track.folder = root + playlist = Playlist(user = users['alice'], name = "Alice's") + playlist.add(songs['One']) + playlist.add(songs['Three']) - self.store.add(track) - songs[song] = track + playlist = Playlist(user = users['alice'], public = True, name = "Alice's public") + playlist.add(songs['One']) + playlist.add(songs['Two']) - users = { u.name: u for u in self.store.find(User) } - - playlist = Playlist() - playlist.user = users['alice'] - playlist.name = "Alice's" - playlist.add(songs['One']) - playlist.add(songs['Three']) - self.store.add(playlist) - - playlist = Playlist() - playlist.user = users['alice'] - playlist.public = True - playlist.name = "Alice's public" - playlist.add(songs['One']) - playlist.add(songs['Two']) - self.store.add(playlist) - - playlist = Playlist() - playlist.user = users['bob'] - playlist.name = "Bob's" - playlist.add(songs['Two']) - playlist.add(songs['Four']) - self.store.add(playlist) - - self.store.commit() + playlist = Playlist(user = users['bob'], name = "Bob's") + playlist.add(songs['Two']) + playlist.add(songs['Four']) def test_get_playlists(self): # get own playlists @@ -113,7 +94,8 @@ class PlaylistTestCase(ApiTestBase): self._make_request('getPlaylist', { 'id': str(uuid.uuid4()) }, error = 70) # other's private from non admin - playlist = self.store.find(Playlist, Playlist.public == False, Playlist.user_id == User.id, User.name == 'alice').one() + with db_session: + playlist = Playlist.get(lambda p: not p.public == False and p.user.name == 'alice') self._make_request('getPlaylist', { 'u': 'bob', 'p': 'B0b', 'id': str(playlist.id) }, error = 50) # standard @@ -156,9 +138,11 @@ class PlaylistTestCase(ApiTestBase): self._make_request('createPlaylist', { 'u': 'bob', 'p': 'B0b', 'playlistId': playlist.get('id') }, error = 50) # create more useful playlist - songs = { s.title: str(s.id) for s in self.store.find(Track) } + with db_session: + songs = { s.title: str(s.id) for s in Track.select() } self._make_request('createPlaylist', { 'name': 'songs', 'songId': map(lambda s: songs[s], [ 'Three', 'One', 'Two' ]) }, skip_post = True) - playlist = self.store.find(Playlist, Playlist.name == 'songs').one() + with db_session: + playlist = Playlist.get(name = 'songs') self.assertIsNotNone(playlist) rv, child = self._make_request('getPlaylist', { 'id': str(playlist.id) }, tag = 'playlist') self.assertEqual(child.get('songCount'), '3') @@ -174,6 +158,10 @@ class PlaylistTestCase(ApiTestBase): self.assertEqual(self._xpath(child, 'count(./entry)'), 1) self.assertEqual(child[0].get('title'), 'Two') + @db_session + def assertPlaylistCountEqual(self, count): + self.assertEqual(Playlist.select().count(), count) + def test_delete_playlist(self): # check params self._make_request('deletePlaylist', error = 10) @@ -181,27 +169,30 @@ class PlaylistTestCase(ApiTestBase): self._make_request('deletePlaylist', { 'id': str(uuid.uuid4()) }, error = 70) # delete unowned when not admin - playlist = self.store.find(Playlist, Playlist.user_id == User.id, User.name == 'alice')[0] + with db_session: + playlist = Playlist.select(lambda p: p.user.name == 'alice').first() self._make_request('deletePlaylist', { 'u': 'bob', 'p': 'B0b', 'id': str(playlist.id) }, error = 50) - self.assertEqual(self.store.find(Playlist).count(), 3) + self.assertPlaylistCountEqual(3); # delete owned self._make_request('deletePlaylist', { 'id': str(playlist.id) }, skip_post = True) - self.assertEqual(self.store.find(Playlist).count(), 2) + self.assertPlaylistCountEqual(2); self._make_request('deletePlaylist', { 'id': str(playlist.id) }, error = 70) - self.assertEqual(self.store.find(Playlist).count(), 2) + self.assertPlaylistCountEqual(2); # delete unowned when admin - playlist = self.store.find(Playlist, Playlist.user_id == User.id, User.name == 'bob').one() + with db_session: + playlist = Playlist.get(lambda p: p.user.name == 'bob') self._make_request('deletePlaylist', { 'id': str(playlist.id) }, skip_post = True) - self.assertEqual(self.store.find(Playlist).count(), 1) + self.assertPlaylistCountEqual(1); def test_update_playlist(self): self._make_request('updatePlaylist', error = 10) self._make_request('updatePlaylist', { 'playlistId': 1234 }, error = 0) self._make_request('updatePlaylist', { 'playlistId': str(uuid.uuid4()) }, error = 70) - playlist = self.store.find(Playlist, Playlist.user_id == User.id, User.name == 'alice')[0] + with db_session: + playlist = Playlist.select(lambda p: p.user.name == 'alice').order_by(Playlist.created).first() pid = str(playlist.id) self._make_request('updatePlaylist', { 'playlistId': pid, 'songIdToAdd': 'string' }, error = 0) self._make_request('updatePlaylist', { 'playlistId': pid, 'songIndexToRemove': 'string' }, error = 0) @@ -226,7 +217,8 @@ class PlaylistTestCase(ApiTestBase): self.assertEqual(self._xpath(child, 'count(./entry)'), 1) self.assertEqual(self._find(child, './entry').get('title'), 'Three') - songs = { s.title: str(s.id) for s in self.store.find(Track) } + with db_session: + songs = { s.title: str(s.id) for s in Track.select() } self._make_request('updatePlaylist', { 'playlistId': pid, 'songIdToAdd': [ songs['One'], songs['Two'], songs['Two'] ] }, skip_post = True) rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') diff --git a/tests/api/test_transcoding.py b/tests/api/test_transcoding.py index c745cd2..049c44f 100644 --- a/tests/api/test_transcoding.py +++ b/tests/api/test_transcoding.py @@ -11,6 +11,8 @@ import unittest +from pony.orm import db_session + from supysonic.db import Folder, Track from supysonic.managers.folder import FolderManager from supysonic.scanner import Scanner @@ -23,12 +25,13 @@ class TranscodingTestCase(ApiTestBase): super(TranscodingTestCase, self).setUp() - FolderManager.add(self.store, 'Folder', 'tests/assets/folder') - scanner = Scanner(self.store) - scanner.scan(self.store.find(Folder).one()) - scanner.finish() + FolderManager.add('Folder', 'tests/assets/folder') + scanner = Scanner() + with db_session: + scanner.scan(Folder.get()) + scanner.finish() - self.trackid = self.store.find(Track).one().id + self.trackid = Track.get().id def _stream(self, **kwargs): kwargs.update({ 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'v': '1.8.0', 'id': self.trackid })