diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py index afff6e9..29ae047 100644 --- a/supysonic/api/__init__.py +++ b/supysonic/api/__init__.py @@ -11,8 +11,7 @@ import binascii import uuid from flask import request from flask import Blueprint -from pony.orm import ObjectNotFound, TransactionIntegrityError -from pony.orm import commit +from peewee import IntegrityError from ..db import ClientPrefs, Folder from ..managers.user import UserManager @@ -82,15 +81,12 @@ def get_client_prefs(): client = request.values["c"] try: request.client = ClientPrefs[request.user, client] - except ObjectNotFound: + except ClientPrefs.DoesNotExist: try: - request.client = ClientPrefs(user=request.user, client_name=client) - commit() - except TransactionIntegrityError: + request.client = ClientPrefs.create(user=request.user, client_name=client) + except IntegrityError: # We might have hit a race condition here, another request already created # the ClientPrefs. Issue #220 - # Reload the user or Pony will complain about different transactions - request.user = UserManager.get(request.user.id) request.client = ClientPrefs[request.user, client] diff --git a/supysonic/api/albums_songs.py b/supysonic/api/albums_songs.py index 25e9ed3..434b8d9 100644 --- a/supysonic/api/albums_songs.py +++ b/supysonic/api/albums_songs.py @@ -7,19 +7,21 @@ from datetime import timedelta from flask import request -from pony.orm import select, desc, avg, max, min, count, between, distinct +from peewee import fn, JOIN from ..db import ( Folder, + Artist, Album, Track, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, + RatingFolder, User, ) -from ..db import now +from ..db import now, random from . import api_routing, get_root_folder from .exceptions import GenericError @@ -39,20 +41,20 @@ def rand_songs(): query = Track.select() if fromYear: - query = query.filter(lambda t: t.year >= fromYear) + query = query.where(Track.year >= fromYear) if toYear: - query = query.filter(lambda t: t.year <= toYear) + query = query.where(Track.year <= toYear) if genre: - query = query.filter(lambda t: t.genre == genre) + query = query.where(Track.genre == genre) if root: - query = query.filter(lambda t: t.root_folder == root) + query = query.where(Track.root_folder == root) return request.formatter( "randomSongs", { "song": [ t.as_subsonic_child(request.user, request.client) - for t in query.without_distinct().random(size) + for t in query.order_by(random()).limit(size) ] }, ) @@ -67,58 +69,52 @@ def album_list(): offset = int(offset) if offset else 0 root = get_root_folder(mfid) - query = select(t.folder for t in Track) + query = Track.select(Track.folder).join(Folder).group_by(Track.folder) if root is not None: - query = select(t.folder for t in Track if t.root_folder == root) + query = query.where(Track.root_folder == root) if ltype == "random": return request.formatter( "albumList", { "album": [ - a.as_subsonic_child(request.user) - for a in distinct(query.random(size)) + t.folder.as_subsonic_child(request.user) + for t in query.order_by(random()).limit(size) ] }, ) elif ltype == "newest": - query = query.sort_by(desc(Folder.created)).distinct() + query = query.order_by(Folder.created.desc()).distinct() elif ltype == "highest": - query = query.sort_by(lambda f: desc(avg(f.ratings.rating))) + query = query.join(RatingFolder, JOIN.LEFT_OUTER).order_by( + fn.avg(RatingFolder.rating).desc() + ) elif ltype == "frequent": - query = query.sort_by(lambda f: desc(avg(f.tracks.play_count))) + query = query.order_by(fn.avg(Track.play_count).desc()) elif ltype == "recent": - query = select( - t.folder for t in Track if max(t.folder.tracks.last_play) is not None + query = query.where(Track.last_play.is_null(False)).order_by( + fn.max(Track.last_play).desc() ) - if root is not None: - query = query.where(lambda t: t.root_folder == root) - query = query.sort_by(lambda f: desc(max(f.tracks.last_play))) elif ltype == "starred": - query = select( - s.starred - for s in StarredFolder - if s.user.id == request.user.id and count(s.starred.tracks) > 0 - ) - if root is not None: - query = query.filter(lambda f: f.path.startswith(root.path)) + query = query.join(StarredFolder).where(StarredFolder.user == request.user) elif ltype == "alphabeticalByName": - query = query.sort_by(Folder.name).distinct() + query = query.order_by(Folder.name).distinct() elif ltype == "alphabeticalByArtist": - query = query.sort_by(lambda f: f.parent.name + f.name) + parent = Folder.alias() + query = query.join(parent).order_by(parent.name, Folder.name) elif ltype == "byYear": startyear = int(request.values["fromYear"]) endyear = int(request.values["toYear"]) query = query.where( - lambda t: between(t.year, min(startyear, endyear), max(startyear, endyear)) + Track.year.between(min(startyear, endyear), max(startyear, endyear)) ) + order = fn.min(Track.year) if endyear < startyear: - query = query.sort_by(lambda f: desc(min(f.tracks.year))) - else: - query = query.sort_by(lambda f: min(f.tracks.year)) + order = order.desc() + query = query.order_by(order) elif ltype == "byGenre": genre = request.values["genre"] - query = query.where(lambda t: t.genre == genre) + query = query.where(Track.genre == genre) else: raise GenericError("Unknown search type") @@ -126,7 +122,8 @@ def album_list(): "albumList", { "album": [ - f.as_subsonic_child(request.user) for f in query.limit(size, offset) + t.folder.as_subsonic_child(request.user) + for t in query.limit(size).offset(offset) ] }, ) @@ -141,46 +138,49 @@ def album_list_id3(): offset = int(offset) if offset else 0 root = get_root_folder(mfid) - query = Album.select() + query = Album.select().join(Track).group_by(Album) if root is not None: - query = query.where(lambda a: root in a.tracks.root_folder) + query = query.where(Track.root_folder == root) if ltype == "random": return request.formatter( "albumList2", - {"album": [a.as_subsonic_album(request.user) for a in query.random(size)]}, + { + "album": [ + a.as_subsonic_album(request.user) + for a in query.order_by(random()).limit(size) + ] + }, ) elif ltype == "newest": - query = query.order_by(lambda a: desc(min(a.tracks.created))) + query = query.order_by(fn.min(Track.created).desc()) elif ltype == "frequent": - query = query.order_by(lambda a: desc(avg(a.tracks.play_count))) + query = query.order_by(fn.avg(Track.play_count).desc()) elif ltype == "recent": - query = query.where(lambda a: max(a.tracks.last_play) is not None).order_by( - lambda a: desc(max(a.tracks.last_play)) + query = query.where(Track.last_play.is_null(False)).order_by( + fn.max(Track.last_play).desc() ) elif ltype == "starred": - query = select(s.starred for s in StarredAlbum if s.user.id == request.user.id) - if root is not None: - query = query.filter(lambda a: root in a.tracks.root_folder) + query = ( + query.switch().join(StarredAlbum).where(StarredAlbum.user == request.user) + ) elif ltype == "alphabeticalByName": query = query.order_by(Album.name) elif ltype == "alphabeticalByArtist": - query = query.order_by(lambda a: a.artist.name + a.name) + query = query.switch().join(Artist).order_by(Artist.name, Album.name) elif ltype == "byYear": startyear = int(request.values["fromYear"]) endyear = int(request.values["toYear"]) - query = query.where( - lambda a: between( - min(a.tracks.year), min(startyear, endyear), max(startyear, endyear) - ) + query = query.having( + fn.min(Track.year).between(min(startyear, endyear), max(startyear, endyear)) ) + order = fn.min(Track.year) if endyear < startyear: - query = query.order_by(lambda a: desc(min(a.tracks.year))) - else: - query = query.order_by(lambda a: min(a.tracks.year)) + order = order.desc() + query = query.order_by(order) elif ltype == "byGenre": genre = request.values["genre"] - query = query.where(lambda a: genre in a.tracks.genre) + query = query.where(Track.genre == genre) else: raise GenericError("Unknown search type") @@ -188,7 +188,8 @@ def album_list_id3(): "albumList2", { "album": [ - f.as_subsonic_album(request.user) for f in query.limit(size, offset) + a.as_subsonic_album(request.user) + for a in query.limit(size).offset(offset) ] }, ) @@ -203,9 +204,9 @@ def songs_by_genre(): offset = int(offset) if offset else 0 root = get_root_folder(mfid) - query = select(t for t in Track if t.genre == genre) + query = Track.select().where(Track.genre == genre) if root is not None: - query = query.where(lambda t: t.root_folder == root) + query = query.where(Track.root_folder == root) return request.formatter( "songsByGenre", { @@ -219,9 +220,9 @@ def songs_by_genre(): @api_routing("/getNowPlaying") def now_playing(): - query = User.select( - lambda u: u.last_play is not None - and u.last_play_date + timedelta(minutes=3) > now() + query = User.select().where( + User.last_play.is_null(False), + User.last_play_date > now() - timedelta(minutes=3), ) return request.formatter( @@ -245,16 +246,26 @@ def get_starred(): mfid = request.values.get("musicFolderId") root = get_root_folder(mfid) - folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id) + folders = ( + StarredFolder.select(StarredFolder.starred) + .join(Folder) + .join(Track, on=Track.folder) + .where(StarredFolder.user == request.user) + .group_by(Folder) + ) if root is not None: - folders = folders.filter(lambda f: f.path.startswith(root.path)) + folders = folders.where(Folder.path.startswith(root.path)) - arq = folders.filter(lambda f: count(f.tracks) == 0) - alq = folders.filter(lambda f: count(f.tracks) > 0) - trq = select(s.starred for s in StarredTrack if s.user.id == request.user.id) + arq = folders.having(fn.count(Track.id) == 0) + alq = folders.having(fn.count(Track.id) > 0) + trq = ( + StarredTrack.select(StarredTrack.starred) + .join(Track) + .where(StarredTrack.user == request.user) + ) if root is not None: - trq = trq.filter(lambda t: t.root_folder == root) + trq = trq.where(Track.root_folder == root) return request.formatter( "starred", @@ -271,14 +282,26 @@ def get_starred_id3(): mfid = request.values.get("musicFolderId") root = get_root_folder(mfid) - arq = select(s.starred for s in StarredArtist if s.user.id == request.user.id) - alq = select(s.starred for s in StarredAlbum if s.user.id == request.user.id) - trq = select(s.starred for s in StarredTrack if s.user.id == request.user.id) + arq = ( + StarredArtist.select(StarredArtist.starred) + .join(Artist) + .where(StarredArtist.user == request.user) + ) + alq = ( + StarredAlbum.select(StarredAlbum.starred) + .join(Album) + .where(StarredAlbum.user == request.user) + ) + trq = ( + StarredTrack.select(StarredTrack.starred) + .join(Track) + .where(StarredTrack.user == request.user) + ) if root is not None: - arq = arq.filter(lambda a: root in a.tracks.root_folder) - alq = alq.filter(lambda a: root in a.tracks.root_folder) - trq = trq.filter(lambda t: t.root_folder == root) + arq = arq.join(Track).where(Track.root_folder == root) + alq = alq.join(Track).where(Track.root_folder == root) + trq = trq.where(Track.root_folder == root) return request.formatter( "starred2", diff --git a/supysonic/api/annotation.py b/supysonic/api/annotation.py index 4b90533..a8c8bb0 100644 --- a/supysonic/api/annotation.py +++ b/supysonic/api/annotation.py @@ -1,15 +1,13 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2013-2018 Alban 'spl0k' Féron +# Copyright (C) 2013-2022 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. import time from flask import current_app, request -from pony.orm import delete -from pony.orm import ObjectNotFound from ..db import Track, Album, Artist, Folder from ..db import StarredTrack, StarredAlbum, StarredArtist, StarredFolder diff --git a/supysonic/api/browse.py b/supysonic/api/browse.py index c25746b..f1c11c7 100644 --- a/supysonic/api/browse.py +++ b/supysonic/api/browse.py @@ -9,7 +9,6 @@ import re import string from flask import current_app, request -from pony.orm import select, count from ..db import Folder, Artist, Album, Track diff --git a/supysonic/api/errors.py b/supysonic/api/errors.py index 7050b9e..5f2e8fe 100644 --- a/supysonic/api/errors.py +++ b/supysonic/api/errors.py @@ -1,12 +1,11 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2018-2019 Alban 'spl0k' Féron +# Copyright (C) 2018-2022 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. -from pony.orm import rollback -from pony.orm import ObjectNotFound +from peewee import DoesNotExist from werkzeug.exceptions import BadRequestKeyError from . import api @@ -15,25 +14,21 @@ from .exceptions import GenericError, MissingParameter, NotFound, ServerError @api.errorhandler(ValueError) def value_error(e): - rollback() return GenericError("{0.__class__.__name__}: {0}".format(e)) @api.errorhandler(BadRequestKeyError) def key_error(e): - rollback() return MissingParameter() -@api.errorhandler(ObjectNotFound) +@api.errorhandler(DoesNotExist) def object_not_found(e): - rollback() - return NotFound(e.entity.__name__) + return NotFound(e.__class__.__name__[: -len("DoesNotExist")]) @api.errorhandler(500) def generic_error(e): # pragma: nocover - rollback() return ServerError("{0.__class__.__name__}: {0}".format(e)) diff --git a/supysonic/api/jukebox.py b/supysonic/api/jukebox.py index 4dd9972..70a53eb 100644 --- a/supysonic/api/jukebox.py +++ b/supysonic/api/jukebox.py @@ -1,14 +1,13 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2019 Alban 'spl0k' Féron +# Copyright (C) 2019-2022 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. import uuid from flask import current_app, request -from pony.orm import ObjectNotFound from ..daemon import DaemonClient from ..daemon.exceptions import DaemonUnavailableError diff --git a/supysonic/api/media.py b/supysonic/api/media.py index e816203..16702e2 100644 --- a/supysonic/api/media.py +++ b/supysonic/api/media.py @@ -1,7 +1,7 @@ # This file is part of Supysonic. # Supysonic is a Python implementation of the Subsonic server API. # -# Copyright (C) 2013-2020 Alban 'spl0k' Féron +# Copyright (C) 2013-2022 Alban 'spl0k' Féron # 2018-2019 Carey 'pR0Ps' Metcalfe # # Distributed under terms of the GNU AGPLv3 license. @@ -20,7 +20,6 @@ import zlib from flask import request, Response, send_file from flask import current_app from PIL import Image -from pony.orm import ObjectNotFound from xml.etree import ElementTree from zipstream import ZipStream diff --git a/supysonic/api/search.py b/supysonic/api/search.py index 605acda..a56072d 100644 --- a/supysonic/api/search.py +++ b/supysonic/api/search.py @@ -8,7 +8,6 @@ from collections import OrderedDict from datetime import datetime from flask import request -from pony.orm import select from ..db import Folder, Track, Artist, Album diff --git a/supysonic/db.py b/supysonic/db.py index ef99907..e23f858 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -25,7 +25,7 @@ from peewee import ( IntegerField, TextField, ) -from peewee import CompositeKey, DatabaseProxy +from peewee import CompositeKey, DatabaseProxy, MySQLDatabase from peewee import fn from playhouse.db_url import parseresult_to_dict, schemes from urllib.parse import urlparse @@ -38,11 +38,18 @@ def now(): return datetime.now().replace(microsecond=0) +def random(): + if isinstance(db.obj, MySQLDatabase): + return fn.rand() + return fn.random() + + def PrimaryKeyField(**kwargs): return BinaryUUIDField(primary_key=True, default=uuid4, **kwargs) db = DatabaseProxy() +db.Model._meta.legacy_table_names = False class Meta(db.Model): diff --git a/tests/api/test_album_songs.py b/tests/api/test_album_songs.py index 6e0302c..848f8a5 100644 --- a/tests/api/test_album_songs.py +++ b/tests/api/test_album_songs.py @@ -7,8 +7,6 @@ import unittest -from pony.orm import db_session - from supysonic.db import Folder, Artist, Album, Track from .apitestbase import ApiTestBase @@ -21,41 +19,40 @@ class AlbumSongsTestCase(ApiTestBase): def setUp(self): super().setUp() - with db_session: - folder = Folder(name="Root", root=True, path="tests/assets") - empty = Folder(name="Root", root=True, path="/tmp") - artist = Artist(name="Artist") - album = Album(name="Album", artist=artist) + folder = Folder.create(name="Root", root=True, path="tests/assets") + empty = Folder.create(name="Root", root=True, path="/tmp") + artist = Artist.create(name="Artist") + album = Album.create(name="Album", artist=artist) - Track( - title="Track 1", - album=album, - artist=artist, - disc=1, - number=1, - year=123, - path="tests/assets/folder/1", - folder=folder, - root_folder=folder, - duration=2, - bitrate=320, - last_modification=0, - ) - Track( - title="Track 2", - album=album, - artist=artist, - disc=1, - number=1, - year=124, - genre="Lampshade", - path="tests/assets/folder/2", - folder=folder, - root_folder=folder, - duration=2, - bitrate=320, - last_modification=0, - ) + Track.create( + title="Track 1", + album=album, + artist=artist, + disc=1, + number=1, + year=123, + path="tests/assets/folder/1", + folder=folder, + root_folder=folder, + duration=2, + bitrate=320, + last_modification=0, + ) + Track.create( + title="Track 2", + album=album, + artist=artist, + disc=1, + number=1, + year=124, + genre="Lampshade", + path="tests/assets/folder/2", + folder=folder, + root_folder=folder, + duration=2, + bitrate=320, + last_modification=0, + ) def test_get_album_list(self): self._make_request("getAlbumList", error=10) @@ -140,8 +137,7 @@ class AlbumSongsTestCase(ApiTestBase): ) self.assertEqual(len(child), 0) - with db_session: - Folder[1].delete() + Folder[1].delete_instance() rv, child = self._make_request( "getAlbumList", {"type": "random"}, tag="albumList" ) @@ -231,9 +227,8 @@ class AlbumSongsTestCase(ApiTestBase): ) self.assertEqual(len(child), 0) - with db_session: - Track.select().delete() - Album.get().delete() + Track.delete().execute() + Album.delete().execute() rv, child = self._make_request( "getAlbumList2", {"type": "random"}, tag="albumList2" ) diff --git a/tests/testbase.py b/tests/testbase.py index 88ebb23..b668201 100644 --- a/tests/testbase.py +++ b/tests/testbase.py @@ -13,7 +13,7 @@ import sys import tempfile import unittest -from supysonic.db import init_database, release_database +from supysonic.db import release_database from supysonic.config import DefaultConfig from supysonic.managers.user import UserManager from supysonic.web import create_application @@ -93,9 +93,6 @@ class TestBase(unittest.TestCase): self.config.BASE["database_uri"] = "sqlite:///" + self.__db[1] self.config.WEBAPP["cache_dir"] = self.__dir - init_database(self.config.BASE["database_uri"]) - release_database() - self.__app = create_application(self.config) self.client = self.__app.test_client()