diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ff1df416301d2db72c22acdcc8ff5967213138c8..c2d364856aa40f9cc70c4793b74df03ccf383894 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -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 ^^^^^^^^^^^^^ diff --git a/api/Dockerfile b/api/Dockerfile index 6acdaac56a6fa024917f60ac0629217d93813dc4..f82ab3e89f2e5df61e16f393bf5659032766d8de 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -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 diff --git a/api/config/api_urls.py b/api/config/api_urls.py index e6eeff31dd1a5c59f149f5c3a9c20d391f50929f..3cb7ec36daf2b7b9f564c7c3d33df1dd58e7303a 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -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 += [ diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 74fe79ed0115a752e2e64fffdee7bc8c5beb703e..12cb102dd812fec0a5263a96f5f1aa83281d2b3b 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -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 diff --git a/api/config/wsgi.py b/api/config/wsgi.py deleted file mode 100644 index 8e843eb4d29c4f162fe23e1a1a2ea3c0ed622fc5..0000000000000000000000000000000000000000 --- a/api/config/wsgi.py +++ /dev/null @@ -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) diff --git a/api/docker/Dockerfile.test b/api/docker/Dockerfile.test index 963e3ab20e4f114a96a176b4a9a1cb5c91f6023c..9e3202f92eb6be382efc8fcfbd2a4d55d10e61a5 100644 --- a/api/docker/Dockerfile.test +++ b/api/docker/Dockerfile.test @@ -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 diff --git a/api/funkwhale_api/common/decorators.py b/api/funkwhale_api/common/decorators.py index 5ecedc5121eb25b079fea3d1bfdec76d41a90886..71992eff3f9eacfe546df84d77a8faf4924138c2 100644 --- a/api/funkwhale_api/common/decorators.py +++ b/api/funkwhale_api/common/decorators.py @@ -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) diff --git a/api/funkwhale_api/downloader/__init__.py b/api/funkwhale_api/downloader/__init__.py deleted file mode 100644 index eca15e121d9be1167e0a72c65de662189242720c..0000000000000000000000000000000000000000 --- a/api/funkwhale_api/downloader/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .downloader import download - -__all__ = ["download"] diff --git a/api/funkwhale_api/downloader/downloader.py b/api/funkwhale_api/downloader/downloader.py deleted file mode 100644 index f2b7568cc5e992a4407edd86100cb73c15100587..0000000000000000000000000000000000000000 --- a/api/funkwhale_api/downloader/downloader.py +++ /dev/null @@ -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 diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py index e02d1a3e40f41a33cb72d843f0453f1bcc3c9e68..d54b79cea376c6598012f707cbb50784adb6d33a 100644 --- a/api/funkwhale_api/favorites/views.py +++ b/api/funkwhale_api/favorites/views.py @@ -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 diff --git a/api/funkwhale_api/federation/api_views.py b/api/funkwhale_api/federation/api_views.py index 4c5aaf92a2d76e3ef3ab87aeb24afe5a57adb31b..2c7e2658223fe80db5fc3b65b38966e74b4bbbf8 100644 --- a/api/funkwhale_api/federation/api_views.py +++ b/api/funkwhale_api/federation/api_views.py @@ -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( diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py index a12d5e5b53d2474510d9c81f4365f2a0c836e8d9..3b322e915144bec01e536640022402a1b20429bf 100644 --- a/api/funkwhale_api/federation/views.py +++ b/api/funkwhale_api/federation/views.py @@ -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 diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index b2088b5a6f076416722b1802bed7dcd744fa9c58..edae49f991bcb9f2a415f8f829df1a1e7d7c5248 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -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 diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index e42915eb5b11bfdccf12d84db2c8d00a14c5627b..99d9020315882a3c461b3e8fe44eb1310635b0f5 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -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"] diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py index 437b9222f86b4d08788c9e0f5419f0721629735c..76bc93b6776c8b4c1ae294381b448fb47efbe9b5 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -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: diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 2f670fe22e00460d73fe28c01668fb885ed2b46a..5de07ca947edb23ac3a7334c6f94c294e1b64a15 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -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) diff --git a/api/funkwhale_api/musicbrainz/views.py b/api/funkwhale_api/musicbrainz/views.py index b6f009dca7531dd932f9d5c32c6e3ae1f65cb03e..43d2857844c3807de8998acd869e8fe449cbe40f 100644 --- a/api/funkwhale_api/musicbrainz/views.py +++ b/api/funkwhale_api/musicbrainz/views.py @@ -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) diff --git a/api/funkwhale_api/playlists/filters.py b/api/funkwhale_api/playlists/filters.py index 1f12521f050cd5bd4d1236bda53df8dbb52ab330..b204df4b0a5ad7fe8cd5936e73bba10e2b928b54 100644 --- a/api/funkwhale_api/playlists/filters.py +++ b/api/funkwhale_api/playlists/filters.py @@ -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 diff --git a/api/funkwhale_api/playlists/views.py b/api/funkwhale_api/playlists/views.py index 6ff49173c9413bd9c82f7575dd5d1533b1d63489..2f536d7a712f47515dccc55a01d7ddc9fba55a47 100644 --- a/api/funkwhale_api/playlists/views.py +++ b/api/funkwhale_api/playlists/views.py @@ -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() diff --git a/api/funkwhale_api/providers/acoustid/__init__.py b/api/funkwhale_api/providers/acoustid/__init__.py deleted file mode 100644 index 558a95bb80fedc6b9c9f192f86ae9a3136d7d8c1..0000000000000000000000000000000000000000 --- a/api/funkwhale_api/providers/acoustid/__init__.py +++ /dev/null @@ -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"]) diff --git a/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py b/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py deleted file mode 100644 index 2411de86add2154645f670491fd0377996ef216c..0000000000000000000000000000000000000000 --- a/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py +++ /dev/null @@ -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} diff --git a/api/funkwhale_api/radios/views.py b/api/funkwhale_api/radios/views.py index fb2c4d855d266fbda530b974f7b725af31d2b7c4..5df0fe287a28f69513f97640f3790bb0cc031018 100644 --- a/api/funkwhale_api/radios/views.py +++ b/api/funkwhale_api/radios/views.py @@ -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"] diff --git a/api/funkwhale_api/subsonic/filters.py b/api/funkwhale_api/subsonic/filters.py index a354e23f111fd354d80bac3e4929083bf2b599b9..a3c251e6684b9df0dd20943c081474d115dc67ca 100644 --- a/api/funkwhale_api/subsonic/filters.py +++ b/api/funkwhale_api/subsonic/filters.py @@ -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 diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index 17d537dc3ba6061fafb724beeab733e0837c0153..31110dbe694f24739badd806231c73328c21f6d2 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -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( diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py index 3ca0c6b611e24013d81bc2d726f8f4aa1a510bf2..2393882e725ada35c6d1890c82e04a685f60221f 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -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) diff --git a/api/requirements.txt b/api/requirements.txt index 00be27c5356b417fc5da953c6bceab608e2f382c..647e39b8ca5f1ffc55a064c2f51fa19d935009f2 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -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 diff --git a/api/requirements/base.txt b/api/requirements/base.txt index 61b5aa8a5622ef3d9ac8db7efbe8ad317bb4b564..d004a7044f10bc71925dc614bc9e321b25d46c05 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.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 diff --git a/api/requirements/local.txt b/api/requirements/local.txt index c12f1ecb82c33cb88ae3b92e531181da5bc5540c..60724fc959a0b666d8b15cb39d333e6d61f89866 100644 --- a/api/requirements/local.txt +++ b/api/requirements/local.txt @@ -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 diff --git a/api/requirements/production.txt b/api/requirements/production.txt deleted file mode 100644 index d51ee863ad7e465c5ddcfcf9de5ac8cfcfa51e4f..0000000000000000000000000000000000000000 --- a/api/requirements/production.txt +++ /dev/null @@ -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 -# ------------------------------------------------ diff --git a/api/setup.cfg b/api/setup.cfg index 4466d626f68de8b786b4acb85bc447dd3c653318..a3c0e746864b1eb9bd92d7d9077c63490cebe2f6 100644 --- a/api/setup.cfg +++ b/api/setup.cfg @@ -23,3 +23,4 @@ env = DEBUG=False WEAK_PASSWORDS=True CREATE_IMAGE_THUMBNAILS=False + FORCE_HTTPS_URLS=False diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index 30e0f9c1d0272660a6ad61e6e4e17933d2d09684..d185524610dfdcbdd44c67d6f806e3015bd856d5 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -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} diff --git a/api/tests/test_acoustid.py b/api/tests/test_acoustid.py deleted file mode 100644 index ab3dfd1d87c9a8c47e5e6d1b357f27f01f19a3c2..0000000000000000000000000000000000000000 --- a/api/tests/test_acoustid.py +++ /dev/null @@ -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) diff --git a/changes/changelog.d/657.enhancement b/changes/changelog.d/657.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..7620ef02bcd0efb03c9c1b7ea9e75305f12d222d --- /dev/null +++ b/changes/changelog.d/657.enhancement @@ -0,0 +1 @@ +Updated rots of dependencies (especially django 2.0->2.1), and removed unused dependencies (#657)