Merge branch 'dependencies-update' into 'develop'
Dependencies update Closes #657 See merge request funkwhale/funkwhale!531
This commit is contained in:
commit
8c578fa9f5
|
@ -391,7 +391,7 @@ This is regular pytest, so you can use any arguments/options that pytest usually
|
|||
# Stop on first failure
|
||||
docker-compose -f dev.yml run --rm api pytest -x
|
||||
# Run a specific test file
|
||||
docker-compose -f dev.yml run --rm api pytest tests/test_acoustid.py
|
||||
docker-compose -f dev.yml run --rm api pytest tests/music/test_models.py
|
||||
|
||||
Writing tests
|
||||
^^^^^^^^^^^^^
|
||||
|
|
|
@ -10,11 +10,9 @@ RUN apt-get update; \
|
|||
grep -Fv "python3-dev" | \
|
||||
xargs apt-get install -y --no-install-recommends; \
|
||||
rm -rf /usr/share/doc/* /usr/share/locale/*
|
||||
RUN curl -L https://github.com/acoustid/chromaprint/releases/download/v1.4.2/chromaprint-fpcalc-1.4.2-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin --strip 1
|
||||
|
||||
COPY ./requirements/base.txt /requirements/base.txt
|
||||
RUN pip install -r /requirements/base.txt
|
||||
COPY ./requirements/production.txt /requirements/production.txt
|
||||
RUN pip install -r /requirements/production.txt
|
||||
|
||||
COPY . /app
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ from funkwhale_api.playlists import views as playlists_views
|
|||
from funkwhale_api.subsonic.views import SubsonicViewSet
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r"settings", GlobalPreferencesViewSet, base_name="settings")
|
||||
router.register(r"settings", GlobalPreferencesViewSet, basename="settings")
|
||||
router.register(r"activity", activity_views.ActivityViewSet, "activity")
|
||||
router.register(r"tags", views.TagViewSet, "tags")
|
||||
router.register(r"tracks", views.TrackViewSet, "tracks")
|
||||
|
@ -27,7 +27,7 @@ router.register(
|
|||
v1_patterns = router.urls
|
||||
|
||||
subsonic_router = routers.SimpleRouter(trailing_slash=False)
|
||||
subsonic_router.register(r"subsonic/rest", SubsonicViewSet, base_name="subsonic")
|
||||
subsonic_router.register(r"subsonic/rest", SubsonicViewSet, basename="subsonic")
|
||||
|
||||
|
||||
v1_patterns += [
|
||||
|
|
|
@ -160,7 +160,6 @@ LOCAL_APPS = (
|
|||
"funkwhale_api.radios",
|
||||
"funkwhale_api.history",
|
||||
"funkwhale_api.playlists",
|
||||
"funkwhale_api.providers.acoustid",
|
||||
"funkwhale_api.subsonic",
|
||||
)
|
||||
|
||||
|
@ -318,8 +317,6 @@ FILE_UPLOAD_PERMISSIONS = 0o644
|
|||
# ------------------------------------------------------------------------------
|
||||
ROOT_URLCONF = "config.urls"
|
||||
SPA_URLCONF = "config.spa_urls"
|
||||
# See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
ASGI_APPLICATION = "config.routing.application"
|
||||
|
||||
# This ensures that Django will be able to detect a secure connection
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
"""
|
||||
WSGI config for funkwhale_api project.
|
||||
|
||||
This module contains the WSGI application used by Django's development server
|
||||
and any production WSGI deployments. It should expose a module-level variable
|
||||
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
|
||||
this application via the ``WSGI_APPLICATION`` setting.
|
||||
|
||||
Usually you will have the standard Django WSGI application here, but it also
|
||||
might make sense to replace the whole Django WSGI application with a custom one
|
||||
that later delegates to the Django one. For example, you could introduce WSGI
|
||||
middleware here, or combine a Django application with an application of another
|
||||
framework.
|
||||
|
||||
"""
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
from whitenoise.django import DjangoWhiteNoise
|
||||
|
||||
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
|
||||
# if running multiple sites in the same mod_wsgi process. To fix this, use
|
||||
# mod_wsgi daemon mode with each site in its own daemon process, or use
|
||||
# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production"
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
|
||||
|
||||
# This application object is used by any WSGI server configured to use this
|
||||
# file. This includes Django's development server, if the WSGI_APPLICATION
|
||||
# setting points here.
|
||||
application = get_wsgi_application()
|
||||
|
||||
# Use Whitenoise to serve static files
|
||||
# See: https://whitenoise.readthedocs.org/
|
||||
application = DjangoWhiteNoise(application)
|
||||
|
||||
|
||||
# Apply WSGI middleware here.
|
||||
# from helloworld.wsgi import HelloWorldApplication
|
||||
# application = HelloWorldApplication(application)
|
|
@ -11,8 +11,6 @@ RUN apt-get update; \
|
|||
xargs apt-get install -y --no-install-recommends; \
|
||||
rm -rf /usr/share/doc/* /usr/share/locale/*
|
||||
|
||||
RUN curl -L https://github.com/acoustid/chromaprint/releases/download/v1.4.2/chromaprint-fpcalc-1.4.2-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin --strip 1
|
||||
|
||||
RUN mkdir /requirements
|
||||
COPY ./requirements/base.txt /requirements/base.txt
|
||||
RUN pip install -r /requirements/base.txt
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from rest_framework import response
|
||||
from rest_framework.decorators import list_route
|
||||
from rest_framework import decorators
|
||||
|
||||
|
||||
def action_route(serializer_class):
|
||||
@list_route(methods=["post"])
|
||||
@decorators.action(methods=["post"], detail=False)
|
||||
def action(self, request, *args, **kwargs):
|
||||
queryset = self.get_queryset()
|
||||
serializer = serializer_class(request.data, queryset=queryset)
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
from .downloader import download
|
||||
|
||||
__all__ = ["download"]
|
|
@ -1,19 +0,0 @@
|
|||
import os
|
||||
|
||||
import youtube_dl
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def download(
|
||||
url, target_directory=settings.MEDIA_ROOT, name="%(id)s.%(ext)s", bitrate=192
|
||||
):
|
||||
target_path = os.path.join(target_directory, name)
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"outtmpl": target_path,
|
||||
"postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "vorbis"}],
|
||||
}
|
||||
_downloader = youtube_dl.YoutubeDL(ydl_opts)
|
||||
info = _downloader.extract_info(url)
|
||||
info["audio_file_path"] = target_path % {"id": info["id"], "ext": "ogg"}
|
||||
return info
|
|
@ -1,5 +1,5 @@
|
|||
from rest_framework import mixins, status, viewsets
|
||||
from rest_framework.decorators import list_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
@ -20,7 +20,7 @@ class TrackFavoriteViewSet(
|
|||
viewsets.GenericViewSet,
|
||||
):
|
||||
|
||||
filter_class = filters.TrackFavoriteFilter
|
||||
filterset_class = filters.TrackFavoriteFilter
|
||||
serializer_class = serializers.UserTrackFavoriteSerializer
|
||||
queryset = models.TrackFavorite.objects.all().select_related("user")
|
||||
permission_classes = [
|
||||
|
@ -62,7 +62,7 @@ class TrackFavoriteViewSet(
|
|||
favorite = models.TrackFavorite.add(track=track, user=self.request.user)
|
||||
return favorite
|
||||
|
||||
@list_route(methods=["delete", "post"])
|
||||
@action(methods=["delete", "post"], detail=False)
|
||||
def remove(self, request, *args, **kwargs):
|
||||
try:
|
||||
pk = int(request.data["track"])
|
||||
|
@ -72,7 +72,7 @@ class TrackFavoriteViewSet(
|
|||
favorite.delete()
|
||||
return Response([], status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@list_route(methods=["get"])
|
||||
@action(methods=["get"], detail=False)
|
||||
def all(self, request, *args, **kwargs):
|
||||
"""
|
||||
Return all the favorites of the current user, with only limited data
|
||||
|
|
|
@ -43,7 +43,7 @@ class LibraryFollowViewSet(
|
|||
)
|
||||
serializer_class = api_serializers.LibraryFollowSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_class = filters.LibraryFollowFilter
|
||||
filterset_class = filters.LibraryFollowFilter
|
||||
ordering_fields = ("creation_date",)
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -66,7 +66,7 @@ class LibraryFollowViewSet(
|
|||
context["actor"] = self.request.user.actor
|
||||
return context
|
||||
|
||||
@decorators.detail_route(methods=["post"])
|
||||
@decorators.action(methods=["post"], detail=True)
|
||||
def accept(self, request, *args, **kwargs):
|
||||
try:
|
||||
follow = self.queryset.get(
|
||||
|
@ -77,7 +77,7 @@ class LibraryFollowViewSet(
|
|||
update_follow(follow, approved=True)
|
||||
return response.Response(status=204)
|
||||
|
||||
@decorators.detail_route(methods=["post"])
|
||||
@decorators.action(methods=["post"], detail=True)
|
||||
def reject(self, request, *args, **kwargs):
|
||||
try:
|
||||
follow = self.queryset.get(
|
||||
|
@ -105,7 +105,7 @@ class LibraryViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|||
qs = super().get_queryset()
|
||||
return qs.viewable_by(actor=self.request.user.actor)
|
||||
|
||||
@decorators.detail_route(methods=["post"])
|
||||
@decorators.action(methods=["post"], detail=True)
|
||||
def scan(self, request, *args, **kwargs):
|
||||
library = self.get_object()
|
||||
if library.actor.get_user():
|
||||
|
@ -122,7 +122,7 @@ class LibraryViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|||
)
|
||||
return response.Response({"status": "skipped"}, 200)
|
||||
|
||||
@decorators.list_route(methods=["post"])
|
||||
@decorators.action(methods=["post"], detail=False)
|
||||
def fetch(self, request, *args, **kwargs):
|
||||
try:
|
||||
fid = request.data["fid"]
|
||||
|
@ -168,14 +168,14 @@ class InboxItemViewSet(
|
|||
)
|
||||
serializer_class = api_serializers.InboxItemSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_class = filters.InboxItemFilter
|
||||
filterset_class = filters.InboxItemFilter
|
||||
ordering_fields = ("activity__creation_date",)
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
return qs.filter(actor=self.request.user.actor)
|
||||
|
||||
@decorators.list_route(methods=["post"])
|
||||
@decorators.action(methods=["post"], detail=False)
|
||||
def action(self, request, *args, **kwargs):
|
||||
queryset = self.get_queryset()
|
||||
serializer = api_serializers.InboxItemActionSerializer(
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.core import paginator
|
|||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from rest_framework import exceptions, mixins, response, viewsets
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from funkwhale_api.common import preferences
|
||||
from funkwhale_api.music import models as music_models
|
||||
|
@ -23,7 +23,7 @@ class SharedViewSet(FederationMixin, viewsets.GenericViewSet):
|
|||
authentication_classes = [authentication.SignatureAuthentication]
|
||||
renderer_classes = [renderers.ActivityPubRenderer]
|
||||
|
||||
@list_route(methods=["post"])
|
||||
@action(methods=["post"], detail=False)
|
||||
def inbox(self, request, *args, **kwargs):
|
||||
if request.method.lower() == "post" and request.actor is None:
|
||||
raise exceptions.AuthenticationFailed(
|
||||
|
@ -42,7 +42,7 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV
|
|||
queryset = models.Actor.objects.local().select_related("user")
|
||||
serializer_class = serializers.ActorSerializer
|
||||
|
||||
@detail_route(methods=["get", "post"])
|
||||
@action(methods=["get", "post"], detail=True)
|
||||
def inbox(self, request, *args, **kwargs):
|
||||
if request.method.lower() == "post" and request.actor is None:
|
||||
raise exceptions.AuthenticationFailed(
|
||||
|
@ -52,17 +52,17 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV
|
|||
activity.receive(activity=request.data, on_behalf_of=request.actor)
|
||||
return response.Response({}, status=200)
|
||||
|
||||
@detail_route(methods=["get", "post"])
|
||||
@action(methods=["get", "post"], detail=True)
|
||||
def outbox(self, request, *args, **kwargs):
|
||||
return response.Response({}, status=200)
|
||||
|
||||
@detail_route(methods=["get"])
|
||||
@action(methods=["get"], detail=True)
|
||||
def followers(self, request, *args, **kwargs):
|
||||
self.get_object()
|
||||
# XXX to implement
|
||||
return response.Response({})
|
||||
|
||||
@detail_route(methods=["get"])
|
||||
@action(methods=["get"], detail=True)
|
||||
def following(self, request, *args, **kwargs):
|
||||
self.get_object()
|
||||
# XXX to implement
|
||||
|
@ -74,7 +74,7 @@ class WellKnownViewSet(viewsets.GenericViewSet):
|
|||
permission_classes = []
|
||||
renderer_classes = [renderers.JSONRenderer, renderers.WebfingerRenderer]
|
||||
|
||||
@list_route(methods=["get"])
|
||||
@action(methods=["get"], detail=False)
|
||||
def nodeinfo(self, request, *args, **kwargs):
|
||||
if not preferences.get("instance__nodeinfo_enabled"):
|
||||
return HttpResponse(status=404)
|
||||
|
@ -88,7 +88,7 @@ class WellKnownViewSet(viewsets.GenericViewSet):
|
|||
}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(methods=["get"])
|
||||
@action(methods=["get"], detail=False)
|
||||
def webfinger(self, request, *args, **kwargs):
|
||||
if not preferences.get("federation__enabled"):
|
||||
return HttpResponse(status=405)
|
||||
|
@ -180,7 +180,7 @@ class MusicLibraryViewSet(
|
|||
|
||||
return response.Response(data)
|
||||
|
||||
@detail_route(methods=["get"])
|
||||
@action(methods=["get"], detail=True)
|
||||
def followers(self, request, *args, **kwargs):
|
||||
self.get_object()
|
||||
# XXX Implement this
|
||||
|
|
|
@ -49,7 +49,7 @@ class ManageActorFilterSet(filters.FilterSet):
|
|||
},
|
||||
)
|
||||
)
|
||||
local = filters.BooleanFilter(name="_", method="filter_local")
|
||||
local = filters.BooleanFilter(field_name="_", method="filter_local")
|
||||
|
||||
class Meta:
|
||||
model = federation_models.Actor
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from rest_framework import mixins, response, viewsets
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
from rest_framework import decorators as rest_decorators
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from funkwhale_api.common import preferences, decorators
|
||||
|
@ -22,7 +22,7 @@ class ManageUploadViewSet(
|
|||
.order_by("-id")
|
||||
)
|
||||
serializer_class = serializers.ManageUploadSerializer
|
||||
filter_class = filters.ManageUploadFilterSet
|
||||
filterset_class = filters.ManageUploadFilterSet
|
||||
permission_classes = (HasUserPermission,)
|
||||
required_permissions = ["library"]
|
||||
ordering_fields = [
|
||||
|
@ -35,7 +35,7 @@ class ManageUploadViewSet(
|
|||
"duration",
|
||||
]
|
||||
|
||||
@list_route(methods=["post"])
|
||||
@rest_decorators.action(methods=["post"], detail=False)
|
||||
def action(self, request, *args, **kwargs):
|
||||
queryset = self.get_queryset()
|
||||
serializer = serializers.ManageUploadActionSerializer(
|
||||
|
@ -54,7 +54,7 @@ class ManageUserViewSet(
|
|||
):
|
||||
queryset = users_models.User.objects.all().order_by("-id")
|
||||
serializer_class = serializers.ManageUserSerializer
|
||||
filter_class = filters.ManageUserFilterSet
|
||||
filterset_class = filters.ManageUserFilterSet
|
||||
permission_classes = (HasUserPermission,)
|
||||
required_permissions = ["settings"]
|
||||
ordering_fields = ["date_joined", "last_activity", "username"]
|
||||
|
@ -79,7 +79,7 @@ class ManageInvitationViewSet(
|
|||
.select_related("owner")
|
||||
)
|
||||
serializer_class = serializers.ManageInvitationSerializer
|
||||
filter_class = filters.ManageInvitationFilterSet
|
||||
filterset_class = filters.ManageInvitationFilterSet
|
||||
permission_classes = (HasUserPermission,)
|
||||
required_permissions = ["settings"]
|
||||
ordering_fields = ["creation_date", "expiration_date"]
|
||||
|
@ -87,7 +87,7 @@ class ManageInvitationViewSet(
|
|||
def perform_create(self, serializer):
|
||||
serializer.save(owner=self.request.user)
|
||||
|
||||
@list_route(methods=["post"])
|
||||
@rest_decorators.action(methods=["post"], detail=False)
|
||||
def action(self, request, *args, **kwargs):
|
||||
queryset = self.get_queryset()
|
||||
serializer = serializers.ManageInvitationActionSerializer(
|
||||
|
@ -113,7 +113,7 @@ class ManageDomainViewSet(
|
|||
.order_by("name")
|
||||
)
|
||||
serializer_class = serializers.ManageDomainSerializer
|
||||
filter_class = filters.ManageDomainFilterSet
|
||||
filterset_class = filters.ManageDomainFilterSet
|
||||
permission_classes = (HasUserPermission,)
|
||||
required_permissions = ["moderation"]
|
||||
ordering_fields = [
|
||||
|
@ -125,14 +125,14 @@ class ManageDomainViewSet(
|
|||
"instance_policy",
|
||||
]
|
||||
|
||||
@detail_route(methods=["get"])
|
||||
@rest_decorators.action(methods=["get"], detail=True)
|
||||
def nodeinfo(self, request, *args, **kwargs):
|
||||
domain = self.get_object()
|
||||
federation_tasks.update_domain_nodeinfo(domain_name=domain.name)
|
||||
domain.refresh_from_db()
|
||||
return response.Response(domain.nodeinfo, status=200)
|
||||
|
||||
@detail_route(methods=["get"])
|
||||
@rest_decorators.action(methods=["get"], detail=True)
|
||||
def stats(self, request, *args, **kwargs):
|
||||
domain = self.get_object()
|
||||
return response.Response(domain.get_stats(), status=200)
|
||||
|
@ -152,7 +152,7 @@ class ManageActorViewSet(
|
|||
.prefetch_related("instance_policy")
|
||||
)
|
||||
serializer_class = serializers.ManageActorSerializer
|
||||
filter_class = filters.ManageActorFilterSet
|
||||
filterset_class = filters.ManageActorFilterSet
|
||||
permission_classes = (HasUserPermission,)
|
||||
required_permissions = ["moderation"]
|
||||
ordering_fields = [
|
||||
|
@ -176,7 +176,7 @@ class ManageActorViewSet(
|
|||
|
||||
return obj
|
||||
|
||||
@detail_route(methods=["get"])
|
||||
@rest_decorators.action(methods=["get"], detail=True)
|
||||
def stats(self, request, *args, **kwargs):
|
||||
domain = self.get_object()
|
||||
return response.Response(domain.get_stats(), status=200)
|
||||
|
@ -198,7 +198,7 @@ class ManageInstancePolicyViewSet(
|
|||
.select_related()
|
||||
)
|
||||
serializer_class = serializers.ManageInstancePolicySerializer
|
||||
filter_class = filters.ManageInstancePolicyFilterSet
|
||||
filterset_class = filters.ManageInstancePolicyFilterSet
|
||||
permission_classes = (HasUserPermission,)
|
||||
required_permissions = ["moderation"]
|
||||
ordering_fields = ["id", "creation_date"]
|
||||
|
|
|
@ -9,7 +9,7 @@ from . import utils
|
|||
|
||||
class ArtistFilter(filters.FilterSet):
|
||||
q = fields.SearchFilter(search_fields=["name"])
|
||||
playable = filters.BooleanFilter(name="_", method="filter_playable")
|
||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||
|
||||
class Meta:
|
||||
model = models.Artist
|
||||
|
@ -25,7 +25,7 @@ class ArtistFilter(filters.FilterSet):
|
|||
|
||||
class TrackFilter(filters.FilterSet):
|
||||
q = fields.SearchFilter(search_fields=["title", "album__title", "artist__name"])
|
||||
playable = filters.BooleanFilter(name="_", method="filter_playable")
|
||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||
|
||||
class Meta:
|
||||
model = models.Track
|
||||
|
@ -48,7 +48,7 @@ class UploadFilter(filters.FilterSet):
|
|||
track_artist = filters.UUIDFilter("track__artist__uuid")
|
||||
album_artist = filters.UUIDFilter("track__album__artist__uuid")
|
||||
library = filters.UUIDFilter("library__uuid")
|
||||
playable = filters.BooleanFilter(name="_", method="filter_playable")
|
||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||
q = fields.SmartSearchFilter(
|
||||
config=search.SearchConfig(
|
||||
search_fields={
|
||||
|
@ -86,7 +86,7 @@ class UploadFilter(filters.FilterSet):
|
|||
|
||||
|
||||
class AlbumFilter(filters.FilterSet):
|
||||
playable = filters.BooleanFilter(name="_", method="filter_playable")
|
||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||
q = fields.SearchFilter(search_fields=["title", "artist__name" "source"])
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -11,7 +11,7 @@ from rest_framework import mixins
|
|||
from rest_framework import permissions
|
||||
from rest_framework import settings as rest_settings
|
||||
from rest_framework import views, viewsets
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from taggit.models import Tag
|
||||
|
||||
|
@ -28,25 +28,25 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def get_libraries(filter_uploads):
|
||||
def view(self, request, *args, **kwargs):
|
||||
def libraries(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
actor = utils.get_actor_from_request(request)
|
||||
uploads = models.Upload.objects.all()
|
||||
uploads = filter_uploads(obj, uploads)
|
||||
uploads = uploads.playable_by(actor)
|
||||
libraries = models.Library.objects.filter(
|
||||
qs = models.Library.objects.filter(
|
||||
pk__in=uploads.values_list("library", flat=True)
|
||||
).annotate(_uploads_count=Count("uploads"))
|
||||
libraries = libraries.select_related("actor")
|
||||
page = self.paginate_queryset(libraries)
|
||||
qs = qs.select_related("actor")
|
||||
page = self.paginate_queryset(qs)
|
||||
if page is not None:
|
||||
serializer = federation_api_serializers.LibrarySerializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = federation_api_serializers.LibrarySerializer(libraries, many=True)
|
||||
serializer = federation_api_serializers.LibrarySerializer(qs, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
return view
|
||||
return libraries
|
||||
|
||||
|
||||
class TagViewSetMixin(object):
|
||||
|
@ -62,7 +62,7 @@ class ArtistViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
queryset = models.Artist.objects.all()
|
||||
serializer_class = serializers.ArtistWithAlbumsSerializer
|
||||
permission_classes = [common_permissions.ConditionalAuthentication]
|
||||
filter_class = filters.ArtistFilter
|
||||
filterset_class = filters.ArtistFilter
|
||||
ordering_fields = ("id", "name", "creation_date")
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -73,7 +73,7 @@ class ArtistViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
)
|
||||
return queryset.prefetch_related(Prefetch("albums", queryset=albums))
|
||||
|
||||
libraries = detail_route(methods=["get"])(
|
||||
libraries = action(methods=["get"], detail=True)(
|
||||
get_libraries(
|
||||
filter_uploads=lambda o, uploads: uploads.filter(
|
||||
Q(track__artist=o) | Q(track__album__artist=o)
|
||||
|
@ -89,7 +89,7 @@ class AlbumViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
serializer_class = serializers.AlbumSerializer
|
||||
permission_classes = [common_permissions.ConditionalAuthentication]
|
||||
ordering_fields = ("creation_date", "release_date", "title")
|
||||
filter_class = filters.AlbumFilter
|
||||
filterset_class = filters.AlbumFilter
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
@ -101,7 +101,7 @@ class AlbumViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
qs = queryset.prefetch_related(Prefetch("tracks", queryset=tracks))
|
||||
return qs
|
||||
|
||||
libraries = detail_route(methods=["get"])(
|
||||
libraries = action(methods=["get"], detail=True)(
|
||||
get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track__album=o))
|
||||
)
|
||||
|
||||
|
@ -144,7 +144,9 @@ class LibraryViewSet(
|
|||
)
|
||||
instance.delete()
|
||||
|
||||
@detail_route(methods=["get"])
|
||||
follows = action
|
||||
|
||||
@action(methods=["get"], detail=True)
|
||||
@transaction.non_atomic_requests
|
||||
def follows(self, request, *args, **kwargs):
|
||||
library = self.get_object()
|
||||
|
@ -172,7 +174,7 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
|||
queryset = models.Track.objects.all().for_nested_serialization()
|
||||
serializer_class = serializers.TrackSerializer
|
||||
permission_classes = [common_permissions.ConditionalAuthentication]
|
||||
filter_class = filters.TrackFilter
|
||||
filterset_class = filters.TrackFilter
|
||||
ordering_fields = (
|
||||
"creation_date",
|
||||
"title",
|
||||
|
@ -193,7 +195,7 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
|||
)
|
||||
return queryset
|
||||
|
||||
@detail_route(methods=["get"])
|
||||
@action(methods=["get"], detail=True)
|
||||
@transaction.non_atomic_requests
|
||||
def lyrics(self, request, *args, **kwargs):
|
||||
try:
|
||||
|
@ -218,7 +220,7 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
|||
serializer = serializers.LyricsSerializer(lyrics)
|
||||
return Response(serializer.data)
|
||||
|
||||
libraries = detail_route(methods=["get"])(
|
||||
libraries = action(methods=["get"], detail=True)(
|
||||
get_libraries(filter_uploads=lambda o, uploads: uploads.filter(track=o))
|
||||
)
|
||||
|
||||
|
@ -375,7 +377,7 @@ class UploadViewSet(
|
|||
]
|
||||
owner_field = "library.actor.user"
|
||||
owner_checks = ["read", "write"]
|
||||
filter_class = filters.UploadFilter
|
||||
filterset_class = filters.UploadFilter
|
||||
ordering_fields = (
|
||||
"creation_date",
|
||||
"import_date",
|
||||
|
@ -388,7 +390,7 @@ class UploadViewSet(
|
|||
qs = super().get_queryset()
|
||||
return qs.filter(library__actor=self.request.user.actor)
|
||||
|
||||
@list_route(methods=["post"])
|
||||
@action(methods=["post"], detail=False)
|
||||
def action(self, request, *args, **kwargs):
|
||||
queryset = self.get_queryset()
|
||||
serializer = serializers.UploadActionSerializer(request.data, queryset=queryset)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import list_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
@ -47,19 +47,19 @@ class ReleaseBrowse(APIView):
|
|||
class SearchViewSet(viewsets.ViewSet):
|
||||
permission_classes = [ConditionalAuthentication]
|
||||
|
||||
@list_route(methods=["get"])
|
||||
@action(methods=["get"], detail=False)
|
||||
def recordings(self, request, *args, **kwargs):
|
||||
query = request.GET["query"]
|
||||
results = api.recordings.search(query)
|
||||
return Response(results)
|
||||
|
||||
@list_route(methods=["get"])
|
||||
@action(methods=["get"], detail=False)
|
||||
def releases(self, request, *args, **kwargs):
|
||||
query = request.GET["query"]
|
||||
results = api.releases.search(query)
|
||||
return Response(results)
|
||||
|
||||
@list_route(methods=["get"])
|
||||
@action(methods=["get"], detail=False)
|
||||
def artists(self, request, *args, **kwargs):
|
||||
query = request.GET["query"]
|
||||
results = api.artists.search(query)
|
||||
|
|
|
@ -7,8 +7,8 @@ from . import models
|
|||
|
||||
|
||||
class PlaylistFilter(filters.FilterSet):
|
||||
q = filters.CharFilter(name="_", method="filter_q")
|
||||
playable = filters.BooleanFilter(name="_", method="filter_playable")
|
||||
q = filters.CharFilter(field_name="_", method="filter_q")
|
||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||
|
||||
class Meta:
|
||||
model = models.Playlist
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from rest_framework import exceptions, mixins, viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
@ -33,10 +33,10 @@ class PlaylistViewSet(
|
|||
IsAuthenticatedOrReadOnly,
|
||||
]
|
||||
owner_checks = ["write"]
|
||||
filter_class = filters.PlaylistFilter
|
||||
filterset_class = filters.PlaylistFilter
|
||||
ordering_fields = ("id", "name", "creation_date", "modification_date")
|
||||
|
||||
@detail_route(methods=["get"])
|
||||
@action(methods=["get"], detail=True)
|
||||
def tracks(self, request, *args, **kwargs):
|
||||
playlist = self.get_object()
|
||||
plts = playlist.playlist_tracks.all().for_nested_serialization(
|
||||
|
@ -46,7 +46,7 @@ class PlaylistViewSet(
|
|||
data = {"count": len(plts), "results": serializer.data}
|
||||
return Response(data, status=200)
|
||||
|
||||
@detail_route(methods=["post"])
|
||||
@action(methods=["post"], detail=True)
|
||||
@transaction.atomic
|
||||
def add(self, request, *args, **kwargs):
|
||||
playlist = self.get_object()
|
||||
|
@ -67,7 +67,7 @@ class PlaylistViewSet(
|
|||
data = {"count": len(plts), "results": serializer.data}
|
||||
return Response(data, status=201)
|
||||
|
||||
@detail_route(methods=["delete"])
|
||||
@action(methods=["delete"], detail=True)
|
||||
@transaction.atomic
|
||||
def clear(self, request, *args, **kwargs):
|
||||
playlist = self.get_object()
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import acoustid
|
||||
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, api_key):
|
||||
self.api_key = api_key
|
||||
|
||||
def match(self, file_path):
|
||||
return acoustid.match(self.api_key, file_path, parse=False)
|
||||
|
||||
def get_best_match(self, file_path):
|
||||
results = self.match(file_path=file_path)
|
||||
MIN_SCORE_FOR_MATCH = 0.8
|
||||
try:
|
||||
rows = results["results"]
|
||||
except KeyError:
|
||||
return
|
||||
for row in rows:
|
||||
if row["score"] >= MIN_SCORE_FOR_MATCH:
|
||||
return row
|
||||
|
||||
|
||||
def get_acoustid_client():
|
||||
manager = global_preferences_registry.manager()
|
||||
return Client(api_key=manager["providers_acoustid__api_key"])
|
|
@ -1,16 +0,0 @@
|
|||
from django import forms
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
from dynamic_preferences.types import Section, StringPreference
|
||||
|
||||
acoustid = Section("providers_acoustid")
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class APIKey(StringPreference):
|
||||
section = acoustid
|
||||
name = "api_key"
|
||||
default = ""
|
||||
verbose_name = "Acoustid API key"
|
||||
help_text = "The API key used to query AcoustID. Get one at https://acoustid.org/new-application."
|
||||
widget = forms.PasswordInput
|
||||
field_kwargs = {"required": False}
|
|
@ -1,6 +1,6 @@
|
|||
from django.db.models import Q
|
||||
from rest_framework import mixins, permissions, status, viewsets
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from funkwhale_api.common import permissions as common_permissions
|
||||
|
@ -23,7 +23,7 @@ class RadioViewSet(
|
|||
permissions.IsAuthenticated,
|
||||
common_permissions.OwnerPermission,
|
||||
]
|
||||
filter_class = filtersets.RadioFilter
|
||||
filterset_class = filtersets.RadioFilter
|
||||
owner_field = "user"
|
||||
owner_checks = ["write"]
|
||||
|
||||
|
@ -40,7 +40,7 @@ class RadioViewSet(
|
|||
def perform_update(self, serializer):
|
||||
return serializer.save(user=self.request.user)
|
||||
|
||||
@detail_route(methods=["get"])
|
||||
@action(methods=["get"], detail=True)
|
||||
def tracks(self, request, *args, **kwargs):
|
||||
radio = self.get_object()
|
||||
tracks = radio.get_candidates().for_nested_serialization()
|
||||
|
@ -50,14 +50,14 @@ class RadioViewSet(
|
|||
serializer = TrackSerializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
@list_route(methods=["get"])
|
||||
@action(methods=["get"], detail=False)
|
||||
def filters(self, request, *args, **kwargs):
|
||||
serializer = serializers.FilterSerializer(
|
||||
filters.registry.exposed_filters, many=True
|
||||
)
|
||||
return Response(serializer.data)
|
||||
|
||||
@list_route(methods=["post"])
|
||||
@action(methods=["post"], detail=False)
|
||||
def validate(self, request, *args, **kwargs):
|
||||
try:
|
||||
f_list = request.data["filters"]
|
||||
|
|
|
@ -4,7 +4,7 @@ from funkwhale_api.music import models as music_models
|
|||
|
||||
|
||||
class AlbumList2FilterSet(filters.FilterSet):
|
||||
type = filters.CharFilter(name="_", method="filter_type")
|
||||
type = filters.CharFilter(field_name="_", method="filter_type")
|
||||
|
||||
class Meta:
|
||||
model = music_models.Album
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import datetime
|
||||
import functools
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from rest_framework import exceptions
|
||||
from rest_framework import permissions as rest_permissions
|
||||
from rest_framework import renderers, response, viewsets
|
||||
from rest_framework.decorators import list_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
import funkwhale_api
|
||||
|
@ -25,6 +26,7 @@ def find_object(
|
|||
queryset, model_field="pk", field="id", cast=int, filter_playable=False
|
||||
):
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def inner(self, request, *args, **kwargs):
|
||||
data = request.GET or request.POST
|
||||
try:
|
||||
|
@ -110,12 +112,13 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
@list_route(methods=["get", "post"], permission_classes=[])
|
||||
@action(detail=False, methods=["get", "post"], permission_classes=[])
|
||||
def ping(self, request, *args, **kwargs):
|
||||
data = {"status": "ok", "version": "1.16.0"}
|
||||
return response.Response(data, status=200)
|
||||
|
||||
@list_route(
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_license",
|
||||
permissions_classes=[],
|
||||
|
@ -136,7 +139,12 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
}
|
||||
return response.Response(data, status=200)
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="get_artists", url_path="getArtists")
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_artists",
|
||||
url_path="getArtists",
|
||||
)
|
||||
def get_artists(self, request, *args, **kwargs):
|
||||
artists = music_models.Artist.objects.all().playable_by(
|
||||
utils.get_actor_from_request(request)
|
||||
|
@ -146,7 +154,12 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="get_indexes", url_path="getIndexes")
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_indexes",
|
||||
url_path="getIndexes",
|
||||
)
|
||||
def get_indexes(self, request, *args, **kwargs):
|
||||
artists = music_models.Artist.objects.all().playable_by(
|
||||
utils.get_actor_from_request(request)
|
||||
|
@ -156,7 +169,12 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="get_artist", url_path="getArtist")
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_artist",
|
||||
url_path="getArtist",
|
||||
)
|
||||
@find_object(music_models.Artist.objects.all(), filter_playable=True)
|
||||
def get_artist(self, request, *args, **kwargs):
|
||||
artist = kwargs.pop("obj")
|
||||
|
@ -165,7 +183,9 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="get_song", url_path="getSong")
|
||||
@action(
|
||||
detail=False, methods=["get", "post"], url_name="get_song", url_path="getSong"
|
||||
)
|
||||
@find_object(music_models.Track.objects.all(), filter_playable=True)
|
||||
def get_song(self, request, *args, **kwargs):
|
||||
track = kwargs.pop("obj")
|
||||
|
@ -174,8 +194,11 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
@list_route(
|
||||
methods=["get", "post"], url_name="get_artist_info2", url_path="getArtistInfo2"
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_artist_info2",
|
||||
url_path="getArtistInfo2",
|
||||
)
|
||||
@find_object(music_models.Artist.objects.all(), filter_playable=True)
|
||||
def get_artist_info2(self, request, *args, **kwargs):
|
||||
|
@ -183,7 +206,9 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="get_album", url_path="getAlbum")
|
||||
@action(
|
||||
detail=False, methods=["get", "post"], url_name="get_album", url_path="getAlbum"
|
||||
)
|
||||
@find_object(
|
||||
music_models.Album.objects.select_related("artist"), filter_playable=True
|
||||
)
|
||||
|
@ -193,7 +218,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
payload = {"album": data}
|
||||
return response.Response(payload, status=200)
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="stream", url_path="stream")
|
||||
@action(detail=False, methods=["get", "post"], url_name="stream", url_path="stream")
|
||||
@find_object(music_models.Track.objects.all(), filter_playable=True)
|
||||
def stream(self, request, *args, **kwargs):
|
||||
data = request.GET or request.POST
|
||||
|
@ -208,30 +233,36 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
format = None
|
||||
return music_views.handle_serve(upload=upload, user=request.user, format=format)
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="star", url_path="star")
|
||||
@action(detail=False, methods=["get", "post"], url_name="star", url_path="star")
|
||||
@find_object(music_models.Track.objects.all())
|
||||
def star(self, request, *args, **kwargs):
|
||||
track = kwargs.pop("obj")
|
||||
TrackFavorite.add(user=request.user, track=track)
|
||||
return response.Response({"status": "ok"})
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="unstar", url_path="unstar")
|
||||
@action(detail=False, methods=["get", "post"], url_name="unstar", url_path="unstar")
|
||||
@find_object(music_models.Track.objects.all())
|
||||
def unstar(self, request, *args, **kwargs):
|
||||
track = kwargs.pop("obj")
|
||||
request.user.track_favorites.filter(track=track).delete()
|
||||
return response.Response({"status": "ok"})
|
||||
|
||||
@list_route(
|
||||
methods=["get", "post"], url_name="get_starred2", url_path="getStarred2"
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_starred2",
|
||||
url_path="getStarred2",
|
||||
)
|
||||
def get_starred2(self, request, *args, **kwargs):
|
||||
favorites = request.user.track_favorites.all()
|
||||
data = {"starred2": {"song": serializers.get_starred_tracks_data(favorites)}}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(
|
||||
methods=["get", "post"], url_name="get_random_songs", url_path="getRandomSongs"
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_random_songs",
|
||||
url_path="getRandomSongs",
|
||||
)
|
||||
def get_random_songs(self, request, *args, **kwargs):
|
||||
data = request.GET or request.POST
|
||||
|
@ -253,14 +284,22 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="get_starred", url_path="getStarred")
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_starred",
|
||||
url_path="getStarred",
|
||||
)
|
||||
def get_starred(self, request, *args, **kwargs):
|
||||
favorites = request.user.track_favorites.all()
|
||||
data = {"starred": {"song": serializers.get_starred_tracks_data(favorites)}}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(
|
||||
methods=["get", "post"], url_name="get_album_list2", url_path="getAlbumList2"
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_album_list2",
|
||||
url_path="getAlbumList2",
|
||||
)
|
||||
def get_album_list2(self, request, *args, **kwargs):
|
||||
queryset = music_models.Album.objects.with_tracks_count().order_by(
|
||||
|
@ -287,7 +326,9 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
data = {"albumList2": {"album": serializers.get_album_list2_data(queryset)}}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="search3", url_path="search3")
|
||||
@action(
|
||||
detail=False, methods=["get", "post"], url_name="search3", url_path="search3"
|
||||
)
|
||||
def search3(self, request, *args, **kwargs):
|
||||
data = request.GET or request.POST
|
||||
query = str(data.get("query", "")).replace("*", "")
|
||||
|
@ -350,8 +391,11 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
payload["searchResult3"][c["subsonic"]] = c["serializer"](queryset)
|
||||
return response.Response(payload)
|
||||
|
||||
@list_route(
|
||||
methods=["get", "post"], url_name="get_playlists", url_path="getPlaylists"
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_playlists",
|
||||
url_path="getPlaylists",
|
||||
)
|
||||
def get_playlists(self, request, *args, **kwargs):
|
||||
playlists = request.user.playlists.with_tracks_count().select_related("user")
|
||||
|
@ -362,8 +406,11 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(
|
||||
methods=["get", "post"], url_name="get_playlist", url_path="getPlaylist"
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_playlist",
|
||||
url_path="getPlaylist",
|
||||
)
|
||||
@find_object(playlists_models.Playlist.objects.with_tracks_count())
|
||||
def get_playlist(self, request, *args, **kwargs):
|
||||
|
@ -371,8 +418,11 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
data = {"playlist": serializers.get_playlist_detail_data(playlist)}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(
|
||||
methods=["get", "post"], url_name="update_playlist", url_path="updatePlaylist"
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="update_playlist",
|
||||
url_path="updatePlaylist",
|
||||
)
|
||||
@find_object(lambda request: request.user.playlists.all(), field="playlistId")
|
||||
def update_playlist(self, request, *args, **kwargs):
|
||||
|
@ -413,8 +463,11 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
data = {"status": "ok"}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(
|
||||
methods=["get", "post"], url_name="delete_playlist", url_path="deletePlaylist"
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="delete_playlist",
|
||||
url_path="deletePlaylist",
|
||||
)
|
||||
@find_object(lambda request: request.user.playlists.all())
|
||||
def delete_playlist(self, request, *args, **kwargs):
|
||||
|
@ -423,8 +476,11 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
data = {"status": "ok"}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(
|
||||
methods=["get", "post"], url_name="create_playlist", url_path="createPlaylist"
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="create_playlist",
|
||||
url_path="createPlaylist",
|
||||
)
|
||||
def create_playlist(self, request, *args, **kwargs):
|
||||
data = request.GET or request.POST
|
||||
|
@ -462,7 +518,12 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
data = {"playlist": serializers.get_playlist_detail_data(playlist)}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="get_avatar", url_path="getAvatar")
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_avatar",
|
||||
url_path="getAvatar",
|
||||
)
|
||||
@find_object(
|
||||
queryset=users_models.User.objects.exclude(avatar=None).exclude(avatar=""),
|
||||
model_field="username__iexact",
|
||||
|
@ -479,7 +540,9 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
r[file_header] = path
|
||||
return r
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="get_user", url_path="getUser")
|
||||
@action(
|
||||
detail=False, methods=["get", "post"], url_name="get_user", url_path="getUser"
|
||||
)
|
||||
@find_object(
|
||||
queryset=lambda request: users_models.User.objects.filter(pk=request.user.pk),
|
||||
model_field="username__iexact",
|
||||
|
@ -490,7 +553,8 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
data = {"user": serializers.get_user_detail_data(request.user)}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_music_folders",
|
||||
url_path="getMusicFolders",
|
||||
|
@ -499,8 +563,11 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
data = {"musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]}}
|
||||
return response.Response(data)
|
||||
|
||||
@list_route(
|
||||
methods=["get", "post"], url_name="get_cover_art", url_path="getCoverArt"
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get", "post"],
|
||||
url_name="get_cover_art",
|
||||
url_path="getCoverArt",
|
||||
)
|
||||
def get_cover_art(self, request, *args, **kwargs):
|
||||
data = request.GET or request.POST
|
||||
|
@ -536,7 +603,9 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
r[file_header] = path
|
||||
return r
|
||||
|
||||
@list_route(methods=["get", "post"], url_name="scrobble", url_path="scrobble")
|
||||
@action(
|
||||
detail=False, methods=["get", "post"], url_name="scrobble", url_path="scrobble"
|
||||
)
|
||||
def scrobble(self, request, *args, **kwargs):
|
||||
data = request.GET or request.POST
|
||||
serializer = serializers.ScrobbleSerializer(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from allauth.account.adapter import get_adapter
|
||||
from rest_auth.registration.views import RegisterView as BaseRegisterView
|
||||
from rest_framework import mixins, viewsets
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from funkwhale_api.common import preferences
|
||||
|
@ -28,13 +28,13 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
|
|||
serializer_class = serializers.UserWriteSerializer
|
||||
lookup_field = "username"
|
||||
|
||||
@list_route(methods=["get"])
|
||||
@action(methods=["get"], detail=False)
|
||||
def me(self, request, *args, **kwargs):
|
||||
"""Return information about the current user"""
|
||||
serializer = serializers.MeSerializer(request.user)
|
||||
return Response(serializer.data)
|
||||
|
||||
@detail_route(methods=["get", "post", "delete"], url_path="subsonic-token")
|
||||
@action(methods=["get", "post", "delete"], url_path="subsonic-token", detail=True)
|
||||
def subsonic_token(self, request, *args, **kwargs):
|
||||
if not self.request.user.username == kwargs.get("username"):
|
||||
return Response(status=403)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# This file is here because many Platforms as a Service look for
|
||||
# requirements.txt in the root directory of a project.
|
||||
-r requirements/base.txt
|
||||
-r requirements/production.txt
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
# Bleeding edge Django
|
||||
django>=2.0,<2.1
|
||||
django>=2.1,<2.2
|
||||
|
||||
# Configuration
|
||||
django-environ>=0.4,<0.5
|
||||
whitenoise>=3.3,<3.4
|
||||
|
||||
# Images
|
||||
Pillow>=4.3,<4.4
|
||||
Pillow>=5.4,<5.5
|
||||
|
||||
# For user registration, either via email or social
|
||||
# Well-built with regular release cycles!
|
||||
|
@ -17,42 +16,36 @@ django-allauth>=0.36,<0.37
|
|||
psycopg2-binary>=2.7,<=2.8
|
||||
|
||||
# Time zones support
|
||||
pytz==2017.3
|
||||
pytz==2018.9
|
||||
|
||||
# Redis support
|
||||
django-redis>=4.5,<4.6
|
||||
redis>=2.10,<2.11
|
||||
django-redis>=4.10,<4.11
|
||||
redis>=3.0,<3.1
|
||||
|
||||
|
||||
celery>=4.1,<4.2
|
||||
celery>=4.2,<4.3
|
||||
|
||||
|
||||
# Your custom requirements go here
|
||||
django-cors-headers>=2.1,<2.2
|
||||
musicbrainzngs==0.6
|
||||
youtube_dl>=2017.12.14
|
||||
djangorestframework>=3.7,<3.8
|
||||
djangorestframework>=3.9,<3.10
|
||||
djangorestframework-jwt>=1.11,<1.12
|
||||
oauth2client<4
|
||||
google-api-python-client>=1.6,<1.7
|
||||
pendulum>=2,<3
|
||||
persisting-theory>=0.2,<0.3
|
||||
django-versatileimagefield>=1.9,<1.10
|
||||
django-filter>=1.1,<1.2
|
||||
django-filter>=2.0,<2.1
|
||||
django-rest-auth>=0.9,<0.10
|
||||
beautifulsoup4>=4.6,<4.7
|
||||
Markdown>=2.6,<2.7
|
||||
ipython>=6,<7
|
||||
mutagen>=1.39,<1.40
|
||||
mutagen>=1.42,<1.43
|
||||
|
||||
|
||||
# Until this is merged
|
||||
django-taggit>=0.22,<0.23
|
||||
# Until this is merged
|
||||
pymemoize==1.0.3
|
||||
|
||||
django-dynamic-preferences>=1.7,<1.8
|
||||
pyacoustid>=1.1.5,<1.2
|
||||
raven>=6.5,<7
|
||||
python-magic==0.4.15
|
||||
ffmpeg-python==0.1.10
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# Local development dependencies go here
|
||||
|
||||
coverage>=4.4,<4.5
|
||||
django_coverage_plugin>=1.5,<1.6
|
||||
factory_boy>=2.8.1
|
||||
coverage>=4.5,<4.6
|
||||
django_coverage_plugin>=1.6,<1.7
|
||||
factory_boy>=2.11.1
|
||||
|
||||
# django-debug-toolbar that works with Django 1.5+
|
||||
django-debug-toolbar>=1.9,<1.10
|
||||
django-debug-toolbar>=1.11,<1.12
|
||||
|
||||
# improved REPL
|
||||
ipdb==0.8.1
|
||||
ipdb==0.11
|
||||
black
|
||||
profiling
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
# Pro-tip: Try not to put anything here. There should be no dependency in
|
||||
# production that isn't in development.
|
||||
|
||||
# WSGI Handler
|
||||
# ------------------------------------------------
|
|
@ -23,3 +23,4 @@ env =
|
|||
DEBUG=False
|
||||
WEAK_PASSWORDS=True
|
||||
CREATE_IMAGE_THUMBNAILS=False
|
||||
FORCE_HTTPS_URLS=False
|
||||
|
|
|
@ -45,7 +45,7 @@ def test_exception_wrong_credentials(f, db, api_client):
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_exception_missing_credentials(f, db, api_client):
|
||||
url = reverse("api:subsonic-get-artists")
|
||||
url = reverse("api:subsonic-get_artists")
|
||||
response = api_client.get(url)
|
||||
|
||||
expected = {
|
||||
|
@ -66,7 +66,7 @@ def test_disabled_subsonic(preferences, api_client):
|
|||
|
||||
@pytest.mark.parametrize("f", ["xml", "json"])
|
||||
def test_get_license(f, db, logged_in_api_client, mocker):
|
||||
url = reverse("api:subsonic-get-license")
|
||||
url = reverse("api:subsonic-get_license")
|
||||
assert url.endswith("getLicense") is True
|
||||
now = timezone.now()
|
||||
mocker.patch("django.utils.timezone.now", return_value=now)
|
||||
|
@ -100,7 +100,7 @@ def test_ping(f, db, api_client):
|
|||
def test_get_artists(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
||||
):
|
||||
url = reverse("api:subsonic-get-artists")
|
||||
url = reverse("api:subsonic-get_artists")
|
||||
assert url.endswith("getArtists") is True
|
||||
factories["music.Artist"].create_batch(size=3, playable=True)
|
||||
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
|
||||
|
@ -120,7 +120,7 @@ def test_get_artists(
|
|||
def test_get_artist(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
||||
):
|
||||
url = reverse("api:subsonic-get-artist")
|
||||
url = reverse("api:subsonic-get_artist")
|
||||
assert url.endswith("getArtist") is True
|
||||
artist = factories["music.Artist"](playable=True)
|
||||
factories["music.Album"].create_batch(size=3, artist=artist, playable=True)
|
||||
|
@ -136,7 +136,7 @@ def test_get_artist(
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_invalid_artist(f, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic-get-artist")
|
||||
url = reverse("api:subsonic-get_artist")
|
||||
assert url.endswith("getArtist") is True
|
||||
expected = {"error": {"code": 0, "message": 'For input string "asdf"'}}
|
||||
response = logged_in_api_client.get(url, {"id": "asdf"})
|
||||
|
@ -149,7 +149,7 @@ def test_get_invalid_artist(f, db, logged_in_api_client, factories):
|
|||
def test_get_artist_info2(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
||||
):
|
||||
url = reverse("api:subsonic-get-artist-info2")
|
||||
url = reverse("api:subsonic-get_artist_info2")
|
||||
assert url.endswith("getArtistInfo2") is True
|
||||
artist = factories["music.Artist"](playable=True)
|
||||
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
|
||||
|
@ -167,7 +167,7 @@ def test_get_artist_info2(
|
|||
def test_get_album(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
||||
):
|
||||
url = reverse("api:subsonic-get-album")
|
||||
url = reverse("api:subsonic-get_album")
|
||||
assert url.endswith("getAlbum") is True
|
||||
artist = factories["music.Artist"]()
|
||||
album = factories["music.Album"](artist=artist)
|
||||
|
@ -188,7 +188,7 @@ def test_get_album(
|
|||
def test_get_song(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
||||
):
|
||||
url = reverse("api:subsonic-get-song")
|
||||
url = reverse("api:subsonic-get_song")
|
||||
assert url.endswith("getSong") is True
|
||||
artist = factories["music.Artist"]()
|
||||
album = factories["music.Album"](artist=artist)
|
||||
|
@ -264,7 +264,7 @@ def test_unstar(f, db, logged_in_api_client, factories):
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_starred2(f, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic-get-starred2")
|
||||
url = reverse("api:subsonic-get_starred2")
|
||||
assert url.endswith("getStarred2") is True
|
||||
track = factories["music.Track"]()
|
||||
favorite = factories["favorites.TrackFavorite"](
|
||||
|
@ -280,7 +280,7 @@ def test_get_starred2(f, db, logged_in_api_client, factories):
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_random_songs(f, db, logged_in_api_client, factories, mocker):
|
||||
url = reverse("api:subsonic-get-random-songs")
|
||||
url = reverse("api:subsonic-get_random_songs")
|
||||
assert url.endswith("getRandomSongs") is True
|
||||
track1 = factories["music.Track"]()
|
||||
track2 = factories["music.Track"]()
|
||||
|
@ -303,7 +303,7 @@ def test_get_random_songs(f, db, logged_in_api_client, factories, mocker):
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_starred(f, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic-get-starred")
|
||||
url = reverse("api:subsonic-get_starred")
|
||||
assert url.endswith("getStarred") is True
|
||||
track = factories["music.Track"]()
|
||||
favorite = factories["favorites.TrackFavorite"](
|
||||
|
@ -321,7 +321,7 @@ def test_get_starred(f, db, logged_in_api_client, factories):
|
|||
def test_get_album_list2(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
||||
):
|
||||
url = reverse("api:subsonic-get-album-list2")
|
||||
url = reverse("api:subsonic-get_album_list2")
|
||||
assert url.endswith("getAlbumList2") is True
|
||||
album1 = factories["music.Album"](playable=True)
|
||||
album2 = factories["music.Album"](playable=True)
|
||||
|
@ -338,7 +338,7 @@ def test_get_album_list2(
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_album_list2_pagination(f, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic-get-album-list2")
|
||||
url = reverse("api:subsonic-get_album_list2")
|
||||
assert url.endswith("getAlbumList2") is True
|
||||
album1 = factories["music.Album"](playable=True)
|
||||
factories["music.Album"](playable=True)
|
||||
|
@ -385,7 +385,7 @@ def test_search3(f, db, logged_in_api_client, factories):
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_playlists(f, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic-get-playlists")
|
||||
url = reverse("api:subsonic-get_playlists")
|
||||
assert url.endswith("getPlaylists") is True
|
||||
playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
|
||||
response = logged_in_api_client.get(url, {"f": f})
|
||||
|
@ -399,7 +399,7 @@ def test_get_playlists(f, db, logged_in_api_client, factories):
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_playlist(f, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic-get-playlist")
|
||||
url = reverse("api:subsonic-get_playlist")
|
||||
assert url.endswith("getPlaylist") is True
|
||||
playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
|
||||
response = logged_in_api_client.get(url, {"f": f, "id": playlist.pk})
|
||||
|
@ -413,7 +413,7 @@ def test_get_playlist(f, db, logged_in_api_client, factories):
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_update_playlist(f, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic-update-playlist")
|
||||
url = reverse("api:subsonic-update_playlist")
|
||||
assert url.endswith("updatePlaylist") is True
|
||||
playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
|
||||
factories["playlists.PlaylistTrack"](index=0, playlist=playlist)
|
||||
|
@ -437,7 +437,7 @@ def test_update_playlist(f, db, logged_in_api_client, factories):
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_delete_playlist(f, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic-delete-playlist")
|
||||
url = reverse("api:subsonic-delete_playlist")
|
||||
assert url.endswith("deletePlaylist") is True
|
||||
playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
|
||||
response = logged_in_api_client.get(url, {"f": f, "id": playlist.pk})
|
||||
|
@ -448,7 +448,7 @@ def test_delete_playlist(f, db, logged_in_api_client, factories):
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_create_playlist(f, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic-create-playlist")
|
||||
url = reverse("api:subsonic-create_playlist")
|
||||
assert url.endswith("createPlaylist") is True
|
||||
track1 = factories["music.Track"]()
|
||||
track2 = factories["music.Track"]()
|
||||
|
@ -470,7 +470,7 @@ def test_create_playlist(f, db, logged_in_api_client, factories):
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_music_folders(f, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic-get-music-folders")
|
||||
url = reverse("api:subsonic-get_music_folders")
|
||||
assert url.endswith("getMusicFolders") is True
|
||||
response = logged_in_api_client.get(url, {"f": f})
|
||||
assert response.status_code == 200
|
||||
|
@ -483,7 +483,7 @@ def test_get_music_folders(f, db, logged_in_api_client, factories):
|
|||
def test_get_indexes(
|
||||
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
|
||||
):
|
||||
url = reverse("api:subsonic-get-indexes")
|
||||
url = reverse("api:subsonic-get_indexes")
|
||||
assert url.endswith("getIndexes") is True
|
||||
factories["music.Artist"].create_batch(size=3, playable=True)
|
||||
expected = {
|
||||
|
@ -501,7 +501,7 @@ def test_get_indexes(
|
|||
|
||||
|
||||
def test_get_cover_art_album(factories, logged_in_api_client):
|
||||
url = reverse("api:subsonic-get-cover-art")
|
||||
url = reverse("api:subsonic-get_cover_art")
|
||||
assert url.endswith("getCoverArt") is True
|
||||
album = factories["music.Album"]()
|
||||
response = logged_in_api_client.get(url, {"id": "al-{}".format(album.pk)})
|
||||
|
@ -515,7 +515,7 @@ def test_get_cover_art_album(factories, logged_in_api_client):
|
|||
|
||||
def test_get_avatar(factories, logged_in_api_client):
|
||||
user = factories["users.User"]()
|
||||
url = reverse("api:subsonic-get-avatar")
|
||||
url = reverse("api:subsonic-get_avatar")
|
||||
assert url.endswith("getAvatar") is True
|
||||
response = logged_in_api_client.get(url, {"username": user.username})
|
||||
|
||||
|
@ -541,7 +541,7 @@ def test_scrobble(factories, logged_in_api_client):
|
|||
|
||||
@pytest.mark.parametrize("f", ["json"])
|
||||
def test_get_user(f, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic-get-user")
|
||||
url = reverse("api:subsonic-get_user")
|
||||
assert url.endswith("getUser") is True
|
||||
response = logged_in_api_client.get(
|
||||
url, {"f": f, "username": logged_in_api_client.user.username}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
from funkwhale_api.providers.acoustid import get_acoustid_client
|
||||
|
||||
|
||||
def test_client_is_configured_with_correct_api_key(preferences):
|
||||
api_key = "hello world"
|
||||
preferences["providers_acoustid__api_key"] = api_key
|
||||
|
||||
client = get_acoustid_client()
|
||||
assert client.api_key == api_key
|
||||
|
||||
|
||||
def test_client_returns_raw_results(db, mocker, preferences):
|
||||
api_key = "test"
|
||||
preferences["providers_acoustid__api_key"] = api_key
|
||||
payload = {
|
||||
"results": [
|
||||
{
|
||||
"id": "e475bf79-c1ce-4441-bed7-1e33f226c0a2",
|
||||
"recordings": [
|
||||
{
|
||||
"artists": [
|
||||
{
|
||||
"id": "9c6bddde-6228-4d9f-ad0d-03f6fcb19e13",
|
||||
"name": "Binärpilot",
|
||||
}
|
||||
],
|
||||
"duration": 268,
|
||||
"id": "f269d497-1cc0-4ae4-a0c4-157ec7d73fcb",
|
||||
"title": "Bend",
|
||||
}
|
||||
],
|
||||
"score": 0.860825,
|
||||
}
|
||||
],
|
||||
"status": "ok",
|
||||
}
|
||||
|
||||
m = mocker.patch("acoustid.match", return_value=payload)
|
||||
client = get_acoustid_client()
|
||||
response = client.match("/tmp/noopfile.mp3")
|
||||
|
||||
assert response == payload
|
||||
m.assert_called_once_with("test", "/tmp/noopfile.mp3", parse=False)
|
|
@ -0,0 +1 @@
|
|||
Updated rots of dependencies (especially django 2.0->2.1), and removed unused dependencies (#657)
|
Loading…
Reference in New Issue