1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-09-19 19:01:03 +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
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]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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