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

Porting supysonic.api.albums_songs

This commit is contained in:
Alban Féron 2022-12-18 16:50:03 +01:00
parent c5246c74bb
commit 2b472b4d97
No known key found for this signature in database
GPG Key ID: 8CE0313646D16165
11 changed files with 150 additions and 143 deletions

View File

@ -11,8 +11,7 @@ import binascii
import uuid import uuid
from flask import request from flask import request
from flask import Blueprint from flask import Blueprint
from pony.orm import ObjectNotFound, TransactionIntegrityError from peewee import IntegrityError
from pony.orm import commit
from ..db import ClientPrefs, Folder from ..db import ClientPrefs, Folder
from ..managers.user import UserManager from ..managers.user import UserManager
@ -82,15 +81,12 @@ def get_client_prefs():
client = request.values["c"] client = request.values["c"]
try: try:
request.client = ClientPrefs[request.user, client] request.client = ClientPrefs[request.user, client]
except ObjectNotFound: except ClientPrefs.DoesNotExist:
try: try:
request.client = ClientPrefs(user=request.user, client_name=client) request.client = ClientPrefs.create(user=request.user, client_name=client)
commit() except IntegrityError:
except TransactionIntegrityError:
# We might have hit a race condition here, another request already created # We might have hit a race condition here, another request already created
# the ClientPrefs. Issue #220 # 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] request.client = ClientPrefs[request.user, client]

View File

@ -7,19 +7,21 @@
from datetime import timedelta from datetime import timedelta
from flask import request from flask import request
from pony.orm import select, desc, avg, max, min, count, between, distinct from peewee import fn, JOIN
from ..db import ( from ..db import (
Folder, Folder,
Artist,
Album, Album,
Track, Track,
StarredFolder, StarredFolder,
StarredArtist, StarredArtist,
StarredAlbum, StarredAlbum,
StarredTrack, StarredTrack,
RatingFolder,
User, User,
) )
from ..db import now from ..db import now, random
from . import api_routing, get_root_folder from . import api_routing, get_root_folder
from .exceptions import GenericError from .exceptions import GenericError
@ -39,20 +41,20 @@ def rand_songs():
query = Track.select() query = Track.select()
if fromYear: if fromYear:
query = query.filter(lambda t: t.year >= fromYear) query = query.where(Track.year >= fromYear)
if toYear: if toYear:
query = query.filter(lambda t: t.year <= toYear) query = query.where(Track.year <= toYear)
if genre: if genre:
query = query.filter(lambda t: t.genre == genre) query = query.where(Track.genre == genre)
if root: if root:
query = query.filter(lambda t: t.root_folder == root) query = query.where(Track.root_folder == root)
return request.formatter( return request.formatter(
"randomSongs", "randomSongs",
{ {
"song": [ "song": [
t.as_subsonic_child(request.user, request.client) 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 offset = int(offset) if offset else 0
root = get_root_folder(mfid) 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: 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": if ltype == "random":
return request.formatter( return request.formatter(
"albumList", "albumList",
{ {
"album": [ "album": [
a.as_subsonic_child(request.user) t.folder.as_subsonic_child(request.user)
for a in distinct(query.random(size)) for t in query.order_by(random()).limit(size)
] ]
}, },
) )
elif ltype == "newest": elif ltype == "newest":
query = query.sort_by(desc(Folder.created)).distinct() query = query.order_by(Folder.created.desc()).distinct()
elif ltype == "highest": 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": 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": elif ltype == "recent":
query = select( query = query.where(Track.last_play.is_null(False)).order_by(
t.folder for t in Track if max(t.folder.tracks.last_play) is not None 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": elif ltype == "starred":
query = select( query = query.join(StarredFolder).where(StarredFolder.user == request.user)
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))
elif ltype == "alphabeticalByName": elif ltype == "alphabeticalByName":
query = query.sort_by(Folder.name).distinct() query = query.order_by(Folder.name).distinct()
elif ltype == "alphabeticalByArtist": 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": elif ltype == "byYear":
startyear = int(request.values["fromYear"]) startyear = int(request.values["fromYear"])
endyear = int(request.values["toYear"]) endyear = int(request.values["toYear"])
query = query.where( 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: if endyear < startyear:
query = query.sort_by(lambda f: desc(min(f.tracks.year))) order = order.desc()
else: query = query.order_by(order)
query = query.sort_by(lambda f: min(f.tracks.year))
elif ltype == "byGenre": elif ltype == "byGenre":
genre = request.values["genre"] genre = request.values["genre"]
query = query.where(lambda t: t.genre == genre) query = query.where(Track.genre == genre)
else: else:
raise GenericError("Unknown search type") raise GenericError("Unknown search type")
@ -126,7 +122,8 @@ def album_list():
"albumList", "albumList",
{ {
"album": [ "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 offset = int(offset) if offset else 0
root = get_root_folder(mfid) root = get_root_folder(mfid)
query = Album.select() query = Album.select().join(Track).group_by(Album)
if root is not None: 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": if ltype == "random":
return request.formatter( return request.formatter(
"albumList2", "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": 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": 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": elif ltype == "recent":
query = query.where(lambda a: max(a.tracks.last_play) is not None).order_by( query = query.where(Track.last_play.is_null(False)).order_by(
lambda a: desc(max(a.tracks.last_play)) fn.max(Track.last_play).desc()
) )
elif ltype == "starred": elif ltype == "starred":
query = select(s.starred for s in StarredAlbum if s.user.id == request.user.id) query = (
if root is not None: query.switch().join(StarredAlbum).where(StarredAlbum.user == request.user)
query = query.filter(lambda a: root in a.tracks.root_folder) )
elif ltype == "alphabeticalByName": elif ltype == "alphabeticalByName":
query = query.order_by(Album.name) query = query.order_by(Album.name)
elif ltype == "alphabeticalByArtist": 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": elif ltype == "byYear":
startyear = int(request.values["fromYear"]) startyear = int(request.values["fromYear"])
endyear = int(request.values["toYear"]) endyear = int(request.values["toYear"])
query = query.where( query = query.having(
lambda a: between( fn.min(Track.year).between(min(startyear, endyear), max(startyear, endyear))
min(a.tracks.year), min(startyear, endyear), max(startyear, endyear)
)
) )
order = fn.min(Track.year)
if endyear < startyear: if endyear < startyear:
query = query.order_by(lambda a: desc(min(a.tracks.year))) order = order.desc()
else: query = query.order_by(order)
query = query.order_by(lambda a: min(a.tracks.year))
elif ltype == "byGenre": elif ltype == "byGenre":
genre = request.values["genre"] genre = request.values["genre"]
query = query.where(lambda a: genre in a.tracks.genre) query = query.where(Track.genre == genre)
else: else:
raise GenericError("Unknown search type") raise GenericError("Unknown search type")
@ -188,7 +188,8 @@ def album_list_id3():
"albumList2", "albumList2",
{ {
"album": [ "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 offset = int(offset) if offset else 0
root = get_root_folder(mfid) 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: if root is not None:
query = query.where(lambda t: t.root_folder == root) query = query.where(Track.root_folder == root)
return request.formatter( return request.formatter(
"songsByGenre", "songsByGenre",
{ {
@ -219,9 +220,9 @@ def songs_by_genre():
@api_routing("/getNowPlaying") @api_routing("/getNowPlaying")
def now_playing(): def now_playing():
query = User.select( query = User.select().where(
lambda u: u.last_play is not None User.last_play.is_null(False),
and u.last_play_date + timedelta(minutes=3) > now() User.last_play_date > now() - timedelta(minutes=3),
) )
return request.formatter( return request.formatter(
@ -245,16 +246,26 @@ def get_starred():
mfid = request.values.get("musicFolderId") mfid = request.values.get("musicFolderId")
root = get_root_folder(mfid) 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: 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) arq = folders.having(fn.count(Track.id) == 0)
alq = folders.filter(lambda f: count(f.tracks) > 0) alq = folders.having(fn.count(Track.id) > 0)
trq = select(s.starred for s in StarredTrack if s.user.id == request.user.id) trq = (
StarredTrack.select(StarredTrack.starred)
.join(Track)
.where(StarredTrack.user == request.user)
)
if root is not None: if root is not None:
trq = trq.filter(lambda t: t.root_folder == root) trq = trq.where(Track.root_folder == root)
return request.formatter( return request.formatter(
"starred", "starred",
@ -271,14 +282,26 @@ def get_starred_id3():
mfid = request.values.get("musicFolderId") mfid = request.values.get("musicFolderId")
root = get_root_folder(mfid) root = get_root_folder(mfid)
arq = select(s.starred for s in StarredArtist if s.user.id == request.user.id) arq = (
alq = select(s.starred for s in StarredAlbum if s.user.id == request.user.id) StarredArtist.select(StarredArtist.starred)
trq = select(s.starred for s in StarredTrack if s.user.id == request.user.id) .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: if root is not None:
arq = arq.filter(lambda a: root in a.tracks.root_folder) arq = arq.join(Track).where(Track.root_folder == root)
alq = alq.filter(lambda a: root in a.tracks.root_folder) alq = alq.join(Track).where(Track.root_folder == root)
trq = trq.filter(lambda t: t.root_folder == root) trq = trq.where(Track.root_folder == root)
return request.formatter( return request.formatter(
"starred2", "starred2",

View File

@ -1,15 +1,13 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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. # Distributed under terms of the GNU AGPLv3 license.
import time import time
from flask import current_app, request 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 Track, Album, Artist, Folder
from ..db import StarredTrack, StarredAlbum, StarredArtist, StarredFolder from ..db import StarredTrack, StarredAlbum, StarredArtist, StarredFolder

View File

@ -9,7 +9,6 @@ import re
import string import string
from flask import current_app, request from flask import current_app, request
from pony.orm import select, count
from ..db import Folder, Artist, Album, Track from ..db import Folder, Artist, Album, Track

View File

@ -1,12 +1,11 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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. # Distributed under terms of the GNU AGPLv3 license.
from pony.orm import rollback from peewee import DoesNotExist
from pony.orm import ObjectNotFound
from werkzeug.exceptions import BadRequestKeyError from werkzeug.exceptions import BadRequestKeyError
from . import api from . import api
@ -15,25 +14,21 @@ from .exceptions import GenericError, MissingParameter, NotFound, ServerError
@api.errorhandler(ValueError) @api.errorhandler(ValueError)
def value_error(e): def value_error(e):
rollback()
return GenericError("{0.__class__.__name__}: {0}".format(e)) return GenericError("{0.__class__.__name__}: {0}".format(e))
@api.errorhandler(BadRequestKeyError) @api.errorhandler(BadRequestKeyError)
def key_error(e): def key_error(e):
rollback()
return MissingParameter() return MissingParameter()
@api.errorhandler(ObjectNotFound) @api.errorhandler(DoesNotExist)
def object_not_found(e): def object_not_found(e):
rollback() return NotFound(e.__class__.__name__[: -len("DoesNotExist")])
return NotFound(e.entity.__name__)
@api.errorhandler(500) @api.errorhandler(500)
def generic_error(e): # pragma: nocover def generic_error(e): # pragma: nocover
rollback()
return ServerError("{0.__class__.__name__}: {0}".format(e)) return ServerError("{0.__class__.__name__}: {0}".format(e))

View File

@ -1,14 +1,13 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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. # Distributed under terms of the GNU AGPLv3 license.
import uuid import uuid
from flask import current_app, request from flask import current_app, request
from pony.orm import ObjectNotFound
from ..daemon import DaemonClient from ..daemon import DaemonClient
from ..daemon.exceptions import DaemonUnavailableError from ..daemon.exceptions import DaemonUnavailableError

View File

@ -1,7 +1,7 @@
# This file is part of Supysonic. # This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API. # 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 # 2018-2019 Carey 'pR0Ps' Metcalfe
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
@ -20,7 +20,6 @@ import zlib
from flask import request, Response, send_file from flask import request, Response, send_file
from flask import current_app from flask import current_app
from PIL import Image from PIL import Image
from pony.orm import ObjectNotFound
from xml.etree import ElementTree from xml.etree import ElementTree
from zipstream import ZipStream from zipstream import ZipStream

View File

@ -8,7 +8,6 @@
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime from datetime import datetime
from flask import request from flask import request
from pony.orm import select
from ..db import Folder, Track, Artist, Album from ..db import Folder, Track, Artist, Album

View File

@ -25,7 +25,7 @@ from peewee import (
IntegerField, IntegerField,
TextField, TextField,
) )
from peewee import CompositeKey, DatabaseProxy from peewee import CompositeKey, DatabaseProxy, MySQLDatabase
from peewee import fn from peewee import fn
from playhouse.db_url import parseresult_to_dict, schemes from playhouse.db_url import parseresult_to_dict, schemes
from urllib.parse import urlparse from urllib.parse import urlparse
@ -38,11 +38,18 @@ def now():
return datetime.now().replace(microsecond=0) return datetime.now().replace(microsecond=0)
def random():
if isinstance(db.obj, MySQLDatabase):
return fn.rand()
return fn.random()
def PrimaryKeyField(**kwargs): def PrimaryKeyField(**kwargs):
return BinaryUUIDField(primary_key=True, default=uuid4, **kwargs) return BinaryUUIDField(primary_key=True, default=uuid4, **kwargs)
db = DatabaseProxy() db = DatabaseProxy()
db.Model._meta.legacy_table_names = False
class Meta(db.Model): class Meta(db.Model):

View File

@ -7,8 +7,6 @@
import unittest import unittest
from pony.orm import db_session
from supysonic.db import Folder, Artist, Album, Track from supysonic.db import Folder, Artist, Album, Track
from .apitestbase import ApiTestBase from .apitestbase import ApiTestBase
@ -21,41 +19,40 @@ class AlbumSongsTestCase(ApiTestBase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
with db_session: folder = Folder.create(name="Root", root=True, path="tests/assets")
folder = Folder(name="Root", root=True, path="tests/assets") empty = Folder.create(name="Root", root=True, path="/tmp")
empty = Folder(name="Root", root=True, path="/tmp") artist = Artist.create(name="Artist")
artist = Artist(name="Artist") album = Album.create(name="Album", artist=artist)
album = Album(name="Album", artist=artist)
Track( Track.create(
title="Track 1", title="Track 1",
album=album, album=album,
artist=artist, artist=artist,
disc=1, disc=1,
number=1, number=1,
year=123, year=123,
path="tests/assets/folder/1", path="tests/assets/folder/1",
folder=folder, folder=folder,
root_folder=folder, root_folder=folder,
duration=2, duration=2,
bitrate=320, bitrate=320,
last_modification=0, last_modification=0,
) )
Track( Track.create(
title="Track 2", title="Track 2",
album=album, album=album,
artist=artist, artist=artist,
disc=1, disc=1,
number=1, number=1,
year=124, year=124,
genre="Lampshade", genre="Lampshade",
path="tests/assets/folder/2", path="tests/assets/folder/2",
folder=folder, folder=folder,
root_folder=folder, root_folder=folder,
duration=2, duration=2,
bitrate=320, bitrate=320,
last_modification=0, last_modification=0,
) )
def test_get_album_list(self): def test_get_album_list(self):
self._make_request("getAlbumList", error=10) self._make_request("getAlbumList", error=10)
@ -140,8 +137,7 @@ class AlbumSongsTestCase(ApiTestBase):
) )
self.assertEqual(len(child), 0) self.assertEqual(len(child), 0)
with db_session: Folder[1].delete_instance()
Folder[1].delete()
rv, child = self._make_request( rv, child = self._make_request(
"getAlbumList", {"type": "random"}, tag="albumList" "getAlbumList", {"type": "random"}, tag="albumList"
) )
@ -231,9 +227,8 @@ class AlbumSongsTestCase(ApiTestBase):
) )
self.assertEqual(len(child), 0) self.assertEqual(len(child), 0)
with db_session: Track.delete().execute()
Track.select().delete() Album.delete().execute()
Album.get().delete()
rv, child = self._make_request( rv, child = self._make_request(
"getAlbumList2", {"type": "random"}, tag="albumList2" "getAlbumList2", {"type": "random"}, tag="albumList2"
) )

View File

@ -13,7 +13,7 @@ import sys
import tempfile import tempfile
import unittest import unittest
from supysonic.db import init_database, release_database from supysonic.db import release_database
from supysonic.config import DefaultConfig from supysonic.config import DefaultConfig
from supysonic.managers.user import UserManager from supysonic.managers.user import UserManager
from supysonic.web import create_application 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.BASE["database_uri"] = "sqlite:///" + self.__db[1]
self.config.WEBAPP["cache_dir"] = self.__dir self.config.WEBAPP["cache_dir"] = self.__dir
init_database(self.config.BASE["database_uri"])
release_database()
self.__app = create_application(self.config) self.__app = create_application(self.config)
self.client = self.__app.test_client() self.client = self.__app.test_client()