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)