diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5f65e60daa665d061cc7589f91ee3ab2755f056c..5dfbf0642691e4033bac3f82bdc6fcdeff49f878 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,13 +3,39 @@ variables:
   IMAGE: $IMAGE_NAME:$CI_COMMIT_REF_NAME
   IMAGE_LATEST: $IMAGE_NAME:latest
   PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
+  PYTHONDONTWRITEBYTECODE: "true"
 
 
 stages:
+  - lint
   - test
   - build
   - deploy
 
+black:
+  image: python:3.6
+  stage: lint
+  variables:
+    GIT_STRATEGY: fetch
+  before_script:
+    - pip install black
+  script:
+    - black --check --diff api/
+
+flake8:
+  image: python:3.6
+  stage: lint
+  variables:
+    GIT_STRATEGY: fetch
+  before_script:
+    - pip install flake8
+  script:
+    - flake8 -v api
+  cache:
+    key: "$CI_PROJECT_ID__flake8_pip_cache"
+    paths:
+      - "$PIP_CACHE_DIR"
+
 test_api:
   services:
     - postgres:9.4
@@ -108,7 +134,7 @@ pages:
   tags:
     - docker
 
-docker_develop:
+docker_release:
   stage: deploy
   before_script:
     - docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD
@@ -119,8 +145,9 @@ docker_develop:
     - docker push $IMAGE
   only:
     - develop@funkwhale/funkwhale
+    - tags@funkwhale/funkwhale
   tags:
-    - dind
+    - docker-build
 
 build_api:
   # Simply publish a zip containing api/ directory
@@ -135,19 +162,3 @@ build_api:
     - tags@funkwhale/funkwhale
     - master@funkwhale/funkwhale
     - develop@funkwhale/funkwhale
-
-
-docker_release:
-  stage: deploy
-  before_script:
-    - docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD
-    - cp -r front/dist api/frontend
-    - cd api
-  script:
-    - docker build -t $IMAGE -t $IMAGE_LATEST .
-    - docker push $IMAGE
-    - docker push $IMAGE_LATEST
-  only:
-    - tags@funkwhale/funkwhale
-  tags:
-    - dind
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
new file mode 100644
index 0000000000000000000000000000000000000000..967186030e460a9fe4cd1b9607cfb287b8470f8b
--- /dev/null
+++ b/.gitlab/issue_templates/Bug.md
@@ -0,0 +1,43 @@
+<!--
+Hi there! You are reporting a bug on this project, and we want to thank you!
+
+To ensure your bug report is as useful as possible, please try to stick
+to the following structure. You can leave the parts text between `<!- ->`
+markers untouched, they won't be displayed in your final message.
+
+Please do not edit the following line, it's used for automatic classification
+-->
+
+/label ~"Type: Bug" ~"Status: Need triage"
+
+## Steps to reproduce
+
+<!--
+Describe the steps to reproduce the issue, like:
+
+1. Visit the page at /artists/
+2. Type that
+3. Submit
+-->
+
+## What happens?
+
+<!--
+Describe what happens once the previous steps are completed.
+-->
+
+## What is expected?
+
+<!--
+Describe the expected behaviour.
+-->
+
+## Context
+
+<!--
+If relevant, share additional context here like:
+
+- Browser type and version (for front-end bugs)
+- Instance configuration (Docker/non-docker, nginx/apache as proxy, etc.)
+- Error messages, screenshots and logs
+-->
diff --git a/.gitlab/issue_templates/Feature request.md b/.gitlab/issue_templates/Feature request.md
new file mode 100644
index 0000000000000000000000000000000000000000..404f9c9defa4118bbf445ce899f08ce7066e96ce
--- /dev/null
+++ b/.gitlab/issue_templates/Feature request.md	
@@ -0,0 +1,39 @@
+<!--
+Hi there! You are about to share feature request or an idea, and we want to thank you!
+
+To ensure we can deal with your idea or request, please try to stick
+to the following structure. You can leave the parts text between `<!- ->`
+markers untouched, they won't be displayed in your final message.
+
+Please do not edit the following line, it's used for automatic classification
+-->
+
+/label ~"Type: New feature" ~"Status: Need triage"
+
+## What is the problem you are facing?
+
+<!--
+Describe the problem you'd like to solve, and why we need to add or
+improve something in the current system to solve that problem.
+
+Be as specific as possible.
+-->
+
+## What are the possible drawbacks or issues with the requested changes?
+
+<!--
+Altering the system behaviour is not always a free action, and it can impact
+user experience, performance, introduce bugs or complexity, etc..
+
+If you think about anything we should keep in mind while
+examining your request, please describe it in this section.
+-->
+
+## Context
+
+<!--
+If relevant, share additional context here like:
+
+- Links to existing implementations or examples of the requested feature
+- Screenshots
+-->
diff --git a/CHANGELOG b/CHANGELOG
index edff0877ed06f180ec16dfcc861334313637b395..ee59b7f20ebda0565beb693bcda8355d3f5644ae 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -10,6 +10,133 @@ This changelog is viewable on the web at https://docs.funkwhale.audio/changelog.
 
 .. towncrier
 
+0.14.2 (2018-06-16)
+-------------------
+
+.. warning::
+
+    This release contains a fix for a permission issue. You should upgrade
+    as soon as possible. Read the changelog below for more details.
+
+Upgrade instructions are available at
+https://docs.funkwhale.audio/upgrading.html
+
+Enhancements:
+
+- Added feedback on shuffle button (#262)
+- Added multiple warnings in the documentation that you should never run
+  makemigrations yourself (#291)
+- Album cover served in http (#264)
+- Apache2 reverse proxy now supports websockets (tested with Apache 2.4.25)
+  (!252)
+- Display file size in human format during file upload (#289)
+- Switch from BSD-3 licence to AGPL-3 licence (#280)
+
+Bugfixes:
+
+- Ensure radios can only be edited and deleted by their owners (#311)
+- Fixed admin menu not showing after login (#245)
+- Fixed broken pagination in Subsonic API (#295)
+- Fixed duplicated websocket connexion on timeline (#287)
+
+
+Documentation:
+
+- Improved documentation about in-place imports setup (#298)
+
+
+Other:
+
+- Added Black and flake8 checks in CI to ensure consistent code styling and
+  formatting (#297)
+- Added bug and feature issue templates (#299)
+
+
+Permission issues on radios
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Because of an error in the way we checked user permissions on radios,
+public radios could be deleted by any logged-in user, even if they were not
+the owner of the radio.
+
+We recommend instances owners to upgrade as fast as possible to avoid any abuse
+and data loss.
+
+
+Funkwhale is now licenced under AGPL-3
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Following the recent switch made by PixelFed
+(https://github.com/dansup/pixelfed/issues/143), we decided along with
+the community to relicence Funkwhale under the AGPL-3 licence. We did this
+switch for various reasons:
+
+- This is better aligned with other fediverse software
+- It prohibits anyone to distribute closed-source and proprietary forks of Funkwhale
+
+As end users and instance owners, this does not change anything. You can
+continue to use Funkwhale exactly as you did before :)
+
+
+Apache support for websocket
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Up until now, our Apache2 configuration was not working with websockets. This is now
+solved by adding this at the beginning of your Apache2 configuration file::
+
+    Define funkwhale-api-ws ws://localhost:5000
+
+And this, before the "/api" block::
+
+    # Activating WebSockets
+    ProxyPass "/api/v1/instance/activity" ${funkwhale-api-ws}/api/v1/instance/activity
+
+Websockets may not be supported in older versions of Apache2. Be sure to upgrade to the latest version available.
+
+
+Serving album covers in https (Apache2 proxy)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Two issues are addressed here. The first one was about Django replying with
+mixed content (http) when queried for covers. Setting up the `X-Forwarded-Proto`
+allows Django to know that the client is using https, and that the reply must
+be https as well.
+
+Second issue was a problem of permission causing Apache a denied access to
+album cover folder. It is solved by adding another block for this path in
+the Apache configuration file for funkwhale.
+
+Here is how to modify your `funkwhale.conf` apache2 configuration::
+
+  <VirtualHost *:443>
+
+    ...
+    #Add this new line
+    RequestHeader set X-Forwarded-Proto "https"
+    ...
+    # Add this new block below the other <Directory/> blocks
+    # replace /srv/funkwhale/data/media with the path to your media directory
+    # if you're not using the standard layout.
+    <Directory /srv/funkwhale/data/media/albums>
+      Options FollowSymLinks
+      AllowOverride None
+      Require all granted
+    </Directory>
+    ...
+  </VirtualHost>
+
+
+About the makemigrations warning
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+You may sometimes get the following warning while applying migrations::
+
+    "Your models have changes that are not yet reflected in a migration, and so won't be applied."
+
+This is a warning, not an error, and it can be safely ignored.
+Never run the ``makemigrations`` command yourself.
+
+
 0.14.1 (2018-06-06)
 -------------------
 
diff --git a/CONTRIBUTING b/CONTRIBUTING
index 9f4ec885038eab47d8c27cdd12b6d7ab5cb461be..f79512def15bc14d18774c9a5706bcbe4e2b5eea 100644
--- a/CONTRIBUTING
+++ b/CONTRIBUTING
@@ -61,16 +61,6 @@ If you do not want to add the ``-f dev.yml`` snippet everytime, you can run this
     export COMPOSE_FILE=dev.yml
 
 
-Building the containers
-^^^^^^^^^^^^^^^^^^^^^^^
-
-On your initial clone, or if there have been some changes in the
-app dependencies, you will have to rebuild your containers. This is done
-via the following command::
-
-    docker-compose -f dev.yml build
-
-
 Creating your env file
 ^^^^^^^^^^^^^^^^^^^^^^
 
@@ -84,6 +74,24 @@ Create it like this::
     touch .env
 
 
+Create docker network
+^^^^^^^^^^^^^^^^^^^^
+
+Create the federation network::
+
+    docker network create federation
+
+
+Building the containers
+^^^^^^^^^^^^^^^^^^^^^^^
+
+On your initial clone, or if there have been some changes in the
+app dependencies, you will have to rebuild your containers. This is done
+via the following command::
+
+    docker-compose -f dev.yml build
+
+
 Database management
 ^^^^^^^^^^^^^^^^^^^
 
@@ -124,7 +132,7 @@ Launch all services
 
 Then you can run everything with::
 
-    docker-compose -f dev.yml up
+    docker-compose -f dev.yml up front api nginx celeryworker
 
 This will launch all services, and output the logs in your current terminal window.
 If you prefer to launch them in the background instead, use the ``-d`` flag, and access the logs when you need it via ``docker-compose -f dev.yml logs --tail=50 --follow``.
@@ -194,13 +202,6 @@ Run a reverse proxy for your instances
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 
-Create docker network
-^^^^^^^^^^^^^^^^^^^^
-
-Create the federation network::
-
-    docker network create federation
-
 Launch everything
 ^^^^^^^^^^^^^^^^^
 
diff --git a/LICENSE b/LICENSE
index e30bee823beaf1cfe213133bf79ce1fd27dd3d20..dba13ed2ddf783ee8118c6a581dbf75305f816a3 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,27 +1,661 @@
-Copyright (c) 2015, Eliot Berriot
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice, this
-  list of conditions and the following disclaimer in the documentation and/or
-  other materials provided with the distribution.
-
-* Neither the name of funkwhale_api nor the names of its
-  contributors may be used to endorse or promote products derived from this
-  software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
-IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
-INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
-OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
-OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-OF THE POSSIBILITY OF SUCH DAMAGE.
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/README.rst b/README.rst
index 8646527ad63be971f9d51cf233b48232d766f381..ef3998d117949f6fb1109393a864f0c73a3ffd9c 100644
--- a/README.rst
+++ b/README.rst
@@ -7,7 +7,7 @@ Funkwhale
 
 A self-hosted tribute to Grooveshark.com.
 
-LICENSE: BSD
+LICENSE: AGPL3
 
 Getting help
 ------------
diff --git a/api/config/api_urls.py b/api/config/api_urls.py
index 98b863a93c3f108299ee67975e12df70ec9094c3..9f87a7af3eee635621c8596bf14561387aa53184 100644
--- a/api/config/api_urls.py
+++ b/api/config/api_urls.py
@@ -1,81 +1,79 @@
+from django.conf.urls import include, url
+from dynamic_preferences.api.viewsets import GlobalPreferencesViewSet
 from rest_framework import routers
 from rest_framework.urlpatterns import format_suffix_patterns
-from django.conf.urls import include, url
+from rest_framework_jwt import views as jwt_views
+
 from funkwhale_api.activity import views as activity_views
-from funkwhale_api.instance import views as instance_views
 from funkwhale_api.music import views
 from funkwhale_api.playlists import views as playlists_views
 from funkwhale_api.subsonic.views import SubsonicViewSet
-from rest_framework_jwt import views as jwt_views
-
-from dynamic_preferences.api.viewsets import GlobalPreferencesViewSet
-from dynamic_preferences.users.viewsets import UserPreferencesViewSet
 
 router = routers.SimpleRouter()
-router.register(r'settings', GlobalPreferencesViewSet, base_name='settings')
-router.register(r'activity', activity_views.ActivityViewSet, 'activity')
-router.register(r'tags', views.TagViewSet, 'tags')
-router.register(r'tracks', views.TrackViewSet, 'tracks')
-router.register(r'trackfiles', views.TrackFileViewSet, 'trackfiles')
-router.register(r'artists', views.ArtistViewSet, 'artists')
-router.register(r'albums', views.AlbumViewSet, 'albums')
-router.register(r'import-batches', views.ImportBatchViewSet, 'import-batches')
-router.register(r'import-jobs', views.ImportJobViewSet, 'import-jobs')
-router.register(r'submit', views.SubmitViewSet, 'submit')
-router.register(r'playlists', playlists_views.PlaylistViewSet, 'playlists')
+router.register(r"settings", GlobalPreferencesViewSet, base_name="settings")
+router.register(r"activity", activity_views.ActivityViewSet, "activity")
+router.register(r"tags", views.TagViewSet, "tags")
+router.register(r"tracks", views.TrackViewSet, "tracks")
+router.register(r"trackfiles", views.TrackFileViewSet, "trackfiles")
+router.register(r"artists", views.ArtistViewSet, "artists")
+router.register(r"albums", views.AlbumViewSet, "albums")
+router.register(r"import-batches", views.ImportBatchViewSet, "import-batches")
+router.register(r"import-jobs", views.ImportJobViewSet, "import-jobs")
+router.register(r"submit", views.SubmitViewSet, "submit")
+router.register(r"playlists", playlists_views.PlaylistViewSet, "playlists")
 router.register(
-    r'playlist-tracks',
-    playlists_views.PlaylistTrackViewSet,
-    'playlist-tracks')
+    r"playlist-tracks", playlists_views.PlaylistTrackViewSet, "playlist-tracks"
+)
 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, base_name="subsonic")
 
 
 v1_patterns += [
-    url(r'^instance/',
-        include(
-            ('funkwhale_api.instance.urls', 'instance'),
-            namespace='instance')),
-    url(r'^manage/',
-        include(
-            ('funkwhale_api.manage.urls', 'manage'),
-            namespace='manage')),
-    url(r'^federation/',
-        include(
-            ('funkwhale_api.federation.api_urls', 'federation'),
-            namespace='federation')),
-    url(r'^providers/',
-        include(
-            ('funkwhale_api.providers.urls', 'providers'),
-            namespace='providers')),
-    url(r'^favorites/',
-        include(
-            ('funkwhale_api.favorites.urls', 'favorites'),
-            namespace='favorites')),
-    url(r'^search$',
-        views.Search.as_view(), name='search'),
-    url(r'^radios/',
-        include(
-            ('funkwhale_api.radios.urls', 'radios'),
-            namespace='radios')),
-    url(r'^history/',
-        include(
-            ('funkwhale_api.history.urls', 'history'),
-            namespace='history')),
-    url(r'^users/',
-        include(
-            ('funkwhale_api.users.api_urls', 'users'),
-            namespace='users')),
-    url(r'^requests/',
+    url(
+        r"^instance/",
+        include(("funkwhale_api.instance.urls", "instance"), namespace="instance"),
+    ),
+    url(
+        r"^manage/",
+        include(("funkwhale_api.manage.urls", "manage"), namespace="manage"),
+    ),
+    url(
+        r"^federation/",
         include(
-            ('funkwhale_api.requests.api_urls', 'requests'),
-            namespace='requests')),
-    url(r'^token/$', jwt_views.obtain_jwt_token, name='token'),
-    url(r'^token/refresh/$', jwt_views.refresh_jwt_token, name='token_refresh'),
+            ("funkwhale_api.federation.api_urls", "federation"), namespace="federation"
+        ),
+    ),
+    url(
+        r"^providers/",
+        include(("funkwhale_api.providers.urls", "providers"), namespace="providers"),
+    ),
+    url(
+        r"^favorites/",
+        include(("funkwhale_api.favorites.urls", "favorites"), namespace="favorites"),
+    ),
+    url(r"^search$", views.Search.as_view(), name="search"),
+    url(
+        r"^radios/",
+        include(("funkwhale_api.radios.urls", "radios"), namespace="radios"),
+    ),
+    url(
+        r"^history/",
+        include(("funkwhale_api.history.urls", "history"), namespace="history"),
+    ),
+    url(
+        r"^users/",
+        include(("funkwhale_api.users.api_urls", "users"), namespace="users"),
+    ),
+    url(
+        r"^requests/",
+        include(("funkwhale_api.requests.api_urls", "requests"), namespace="requests"),
+    ),
+    url(r"^token/$", jwt_views.obtain_jwt_token, name="token"),
+    url(r"^token/refresh/$", jwt_views.refresh_jwt_token, name="token_refresh"),
 ]
 
 urlpatterns = [
-    url(r'^v1/', include((v1_patterns, 'v1'), namespace='v1'))
-] + format_suffix_patterns(subsonic_router.urls, allowed=['view'])
+    url(r"^v1/", include((v1_patterns, "v1"), namespace="v1"))
+] + format_suffix_patterns(subsonic_router.urls, allowed=["view"])
diff --git a/api/config/asgi.py b/api/config/asgi.py
index b976a02ebdde045933f4a2dd1997abf2ebdadddb..886178cc28ab9640bfdfa6efca1289402c2cbcf7 100644
--- a/api/config/asgi.py
+++ b/api/config/asgi.py
@@ -1,8 +1,9 @@
-import django
 import os
 
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
+import django
 
 django.setup()
 
-from .routing import application
+from .routing import application  # noqa
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
diff --git a/api/config/routing.py b/api/config/routing.py
index 574d5a18e2eba402b7b3c7620f195d83767b632e..fa25aad0764fc76662c2de3c0edb3b13b4a54820 100644
--- a/api/config/routing.py
+++ b/api/config/routing.py
@@ -1,18 +1,16 @@
-from django.conf.urls import url
-
-from channels.auth import AuthMiddlewareStack
 from channels.routing import ProtocolTypeRouter, URLRouter
+from django.conf.urls import url
 
 from funkwhale_api.common.auth import TokenAuthMiddleware
 from funkwhale_api.instance import consumers
 
-
-application = ProtocolTypeRouter({
-    # Empty for now (http->django views is added by default)
-    "websocket": TokenAuthMiddleware(
-        URLRouter([
-            url("^api/v1/instance/activity$",
-                consumers.InstanceActivityConsumer),
-        ])
-    ),
-})
+application = ProtocolTypeRouter(
+    {
+        # Empty for now (http->django views is added by default)
+        "websocket": TokenAuthMiddleware(
+            URLRouter(
+                [url("^api/v1/instance/activity$", consumers.InstanceActivityConsumer)]
+            )
+        )
+    }
+)
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 6ab2a8303c29ced501a84c55a032d8ac90185ce8..cb5573ed58ddf3edc8ae4df14b7ecf799b0946e4 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -10,131 +10,125 @@ https://docs.djangoproject.com/en/dev/ref/settings/
 """
 from __future__ import absolute_import, unicode_literals
 
-from urllib.parse import urlsplit
-import os
+import datetime
+from urllib.parse import urlparse, urlsplit
+
 import environ
 from celery.schedules import crontab
 
 from funkwhale_api import __version__
 
 ROOT_DIR = environ.Path(__file__) - 3  # (/a/b/myfile.py - 3 = /)
-APPS_DIR = ROOT_DIR.path('funkwhale_api')
+APPS_DIR = ROOT_DIR.path("funkwhale_api")
 
 env = environ.Env()
-
 try:
-    env.read_env(ROOT_DIR.file('.env'))
+    env.read_env(ROOT_DIR.file(".env"))
 except FileNotFoundError:
     pass
 
 FUNKWHALE_HOSTNAME = None
-FUNKWHALE_HOSTNAME_SUFFIX = env('FUNKWHALE_HOSTNAME_SUFFIX', default=None)
-FUNKWHALE_HOSTNAME_PREFIX = env('FUNKWHALE_HOSTNAME_PREFIX', default=None)
+FUNKWHALE_HOSTNAME_SUFFIX = env("FUNKWHALE_HOSTNAME_SUFFIX", default=None)
+FUNKWHALE_HOSTNAME_PREFIX = env("FUNKWHALE_HOSTNAME_PREFIX", default=None)
 if FUNKWHALE_HOSTNAME_PREFIX and FUNKWHALE_HOSTNAME_SUFFIX:
     # We're in traefik case, in development
-    FUNKWHALE_HOSTNAME = '{}.{}'.format(
-        FUNKWHALE_HOSTNAME_PREFIX, FUNKWHALE_HOSTNAME_SUFFIX)
-    FUNKWHALE_PROTOCOL = env('FUNKWHALE_PROTOCOL', default='https')
+    FUNKWHALE_HOSTNAME = "{}.{}".format(
+        FUNKWHALE_HOSTNAME_PREFIX, FUNKWHALE_HOSTNAME_SUFFIX
+    )
+    FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https")
 else:
     try:
-        FUNKWHALE_HOSTNAME = env('FUNKWHALE_HOSTNAME')
-        FUNKWHALE_PROTOCOL = env('FUNKWHALE_PROTOCOL', default='https')
+        FUNKWHALE_HOSTNAME = env("FUNKWHALE_HOSTNAME")
+        FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https")
     except Exception:
-        FUNKWHALE_URL = env('FUNKWHALE_URL')
+        FUNKWHALE_URL = env("FUNKWHALE_URL")
         _parsed = urlsplit(FUNKWHALE_URL)
         FUNKWHALE_HOSTNAME = _parsed.netloc
         FUNKWHALE_PROTOCOL = _parsed.scheme
 
-FUNKWHALE_URL = '{}://{}'.format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME)
+FUNKWHALE_URL = "{}://{}".format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME)
 
 
 # XXX: deprecated, see #186
-FEDERATION_ENABLED = env.bool('FEDERATION_ENABLED', default=True)
-FEDERATION_HOSTNAME = env('FEDERATION_HOSTNAME', default=FUNKWHALE_HOSTNAME)
+FEDERATION_ENABLED = env.bool("FEDERATION_ENABLED", default=True)
+FEDERATION_HOSTNAME = env("FEDERATION_HOSTNAME", default=FUNKWHALE_HOSTNAME)
 # XXX: deprecated, see #186
-FEDERATION_COLLECTION_PAGE_SIZE = env.int(
-    'FEDERATION_COLLECTION_PAGE_SIZE', default=50
-)
+FEDERATION_COLLECTION_PAGE_SIZE = env.int("FEDERATION_COLLECTION_PAGE_SIZE", default=50)
 # XXX: deprecated, see #186
 FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool(
-    'FEDERATION_MUSIC_NEEDS_APPROVAL', default=True
+    "FEDERATION_MUSIC_NEEDS_APPROVAL", default=True
 )
 # XXX: deprecated, see #186
-FEDERATION_ACTOR_FETCH_DELAY = env.int(
-    'FEDERATION_ACTOR_FETCH_DELAY', default=60 * 12)
-ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS')
+FEDERATION_ACTOR_FETCH_DELAY = env.int("FEDERATION_ACTOR_FETCH_DELAY", default=60 * 12)
+ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS")
 
 # APP CONFIGURATION
 # ------------------------------------------------------------------------------
 DJANGO_APPS = (
-    'channels',
+    "channels",
     # Default Django apps:
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.sites',
-    'django.contrib.messages',
-    'django.contrib.staticfiles',
-    'django.contrib.postgres',
-
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.sites",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
+    "django.contrib.postgres",
     # Useful template tags:
     # 'django.contrib.humanize',
-
     # Admin
-    'django.contrib.admin',
+    "django.contrib.admin",
 )
 THIRD_PARTY_APPS = (
     # 'crispy_forms',  # Form layouts
-    'allauth',  # registration
-    'allauth.account',  # registration
-    'allauth.socialaccount',  # registration
-    'corsheaders',
-    'rest_framework',
-    'rest_framework.authtoken',
-    'taggit',
-    'rest_auth',
-    'rest_auth.registration',
-    'dynamic_preferences',
-    'django_filters',
-    'cacheops',
-    'django_cleanup',
+    "allauth",  # registration
+    "allauth.account",  # registration
+    "allauth.socialaccount",  # registration
+    "corsheaders",
+    "rest_framework",
+    "rest_framework.authtoken",
+    "taggit",
+    "rest_auth",
+    "rest_auth.registration",
+    "dynamic_preferences",
+    "django_filters",
+    "cacheops",
+    "django_cleanup",
 )
 
 
 # Sentry
 RAVEN_ENABLED = env.bool("RAVEN_ENABLED", default=False)
-RAVEN_DSN = env("RAVEN_DSN", default='')
+RAVEN_DSN = env("RAVEN_DSN", default="")
 
 if RAVEN_ENABLED:
     RAVEN_CONFIG = {
-        'dsn': RAVEN_DSN,
+        "dsn": RAVEN_DSN,
         # If you are using git, you can also automatically configure the
         # release based on the git info.
-        'release': __version__,
+        "release": __version__,
     }
-    THIRD_PARTY_APPS += (
-        'raven.contrib.django.raven_compat',
-    )
+    THIRD_PARTY_APPS += ("raven.contrib.django.raven_compat",)
 
 
 # Apps specific for this project go here.
 LOCAL_APPS = (
-    'funkwhale_api.common',
-    'funkwhale_api.activity.apps.ActivityConfig',
-    'funkwhale_api.users',  # custom users app
+    "funkwhale_api.common",
+    "funkwhale_api.activity.apps.ActivityConfig",
+    "funkwhale_api.users",  # custom users app
     # Your stuff: custom apps go here
-    'funkwhale_api.instance',
-    'funkwhale_api.music',
-    'funkwhale_api.requests',
-    'funkwhale_api.favorites',
-    'funkwhale_api.federation',
-    'funkwhale_api.radios',
-    'funkwhale_api.history',
-    'funkwhale_api.playlists',
-    'funkwhale_api.providers.audiofile',
-    'funkwhale_api.providers.youtube',
-    'funkwhale_api.providers.acoustid',
-    'funkwhale_api.subsonic',
+    "funkwhale_api.instance",
+    "funkwhale_api.music",
+    "funkwhale_api.requests",
+    "funkwhale_api.favorites",
+    "funkwhale_api.federation",
+    "funkwhale_api.radios",
+    "funkwhale_api.history",
+    "funkwhale_api.playlists",
+    "funkwhale_api.providers.audiofile",
+    "funkwhale_api.providers.youtube",
+    "funkwhale_api.providers.acoustid",
+    "funkwhale_api.subsonic",
 )
 
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
@@ -145,20 +139,18 @@ INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
 # ------------------------------------------------------------------------------
 MIDDLEWARE = (
     # Make sure djangosecure.middleware.SecurityMiddleware is listed first
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'corsheaders.middleware.CorsMiddleware',
-    'django.middleware.common.CommonMiddleware',
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "corsheaders.middleware.CorsMiddleware",
+    "django.middleware.common.CommonMiddleware",
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
 )
 
 # MIGRATIONS CONFIGURATION
 # ------------------------------------------------------------------------------
-MIGRATION_MODULES = {
-    'sites': 'funkwhale_api.contrib.sites.migrations'
-}
+MIGRATION_MODULES = {"sites": "funkwhale_api.contrib.sites.migrations"}
 
 # DEBUG
 # ------------------------------------------------------------------------------
@@ -168,9 +160,7 @@ DEBUG = env.bool("DJANGO_DEBUG", False)
 # FIXTURE CONFIGURATION
 # ------------------------------------------------------------------------------
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS
-FIXTURE_DIRS = (
-    str(APPS_DIR.path('fixtures')),
-)
+FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),)
 
 # EMAIL CONFIGURATION
 # ------------------------------------------------------------------------------
@@ -178,16 +168,14 @@ FIXTURE_DIRS = (
 # EMAIL
 # ------------------------------------------------------------------------------
 DEFAULT_FROM_EMAIL = env(
-    'DEFAULT_FROM_EMAIL',
-    default='Funkwhale <noreply@{}>'.format(FUNKWHALE_HOSTNAME))
+    "DEFAULT_FROM_EMAIL", default="Funkwhale <noreply@{}>".format(FUNKWHALE_HOSTNAME)
+)
 
-EMAIL_SUBJECT_PREFIX = env(
-    "EMAIL_SUBJECT_PREFIX", default='[Funkwhale] ')
-SERVER_EMAIL = env('SERVER_EMAIL', default=DEFAULT_FROM_EMAIL)
+EMAIL_SUBJECT_PREFIX = env("EMAIL_SUBJECT_PREFIX", default="[Funkwhale] ")
+SERVER_EMAIL = env("SERVER_EMAIL", default=DEFAULT_FROM_EMAIL)
 
 
-EMAIL_CONFIG = env.email_url(
-    'EMAIL_CONFIG', default='consolemail://')
+EMAIL_CONFIG = env.email_url("EMAIL_CONFIG", default="consolemail://")
 
 vars().update(EMAIL_CONFIG)
 
@@ -196,9 +184,9 @@ vars().update(EMAIL_CONFIG)
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
 DATABASES = {
     # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
-    'default': env.db("DATABASE_URL"),
+    "default": env.db("DATABASE_URL")
 }
-DATABASES['default']['ATOMIC_REQUESTS'] = True
+DATABASES["default"]["ATOMIC_REQUESTS"] = True
 #
 # DATABASES = {
 #     'default': {
@@ -212,10 +200,10 @@ DATABASES['default']['ATOMIC_REQUESTS'] = True
 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
 # although not all choices may be available on all operating systems.
 # In a Windows environment this must be set to your system time zone.
-TIME_ZONE = 'UTC'
+TIME_ZONE = "UTC"
 
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
 
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id
 SITE_ID = 1
@@ -235,152 +223,142 @@ USE_TZ = True
 TEMPLATES = [
     {
         # See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
         # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
-        'DIRS': [
-            str(APPS_DIR.path('templates')),
-        ],
-        'OPTIONS': {
+        "DIRS": [str(APPS_DIR.path("templates"))],
+        "OPTIONS": {
             # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug
-            'debug': DEBUG,
+            "debug": DEBUG,
             # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
             # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types
-            'loaders': [
-                'django.template.loaders.filesystem.Loader',
-                'django.template.loaders.app_directories.Loader',
+            "loaders": [
+                "django.template.loaders.filesystem.Loader",
+                "django.template.loaders.app_directories.Loader",
             ],
             # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
-            'context_processors': [
-                'django.template.context_processors.debug',
-                'django.template.context_processors.request',
-                'django.contrib.auth.context_processors.auth',
-                'django.template.context_processors.i18n',
-                'django.template.context_processors.media',
-                'django.template.context_processors.static',
-                'django.template.context_processors.tz',
-                'django.contrib.messages.context_processors.messages',
+            "context_processors": [
+                "django.template.context_processors.debug",
+                "django.template.context_processors.request",
+                "django.contrib.auth.context_processors.auth",
+                "django.template.context_processors.i18n",
+                "django.template.context_processors.media",
+                "django.template.context_processors.static",
+                "django.template.context_processors.tz",
+                "django.contrib.messages.context_processors.messages",
                 # Your stuff: custom template context processors go here
             ],
         },
-    },
+    }
 ]
 
 # See: http://django-crispy-forms.readthedocs.org/en/latest/install.html#template-packs
-CRISPY_TEMPLATE_PACK = 'bootstrap3'
+CRISPY_TEMPLATE_PACK = "bootstrap3"
 
 # STATIC FILE CONFIGURATION
 # ------------------------------------------------------------------------------
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
-STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR('staticfiles')))
+STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR("staticfiles")))
 
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
-STATIC_URL = env("STATIC_URL", default='/staticfiles/')
-DEFAULT_FILE_STORAGE = 'funkwhale_api.common.storage.ASCIIFileSystemStorage'
+STATIC_URL = env("STATIC_URL", default="/staticfiles/")
+DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIFileSystemStorage"
 
 # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
-STATICFILES_DIRS = (
-    str(APPS_DIR.path('static')),
-)
+STATICFILES_DIRS = (str(APPS_DIR.path("static")),)
 
 # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
 STATICFILES_FINDERS = (
-    'django.contrib.staticfiles.finders.FileSystemFinder',
-    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+    "django.contrib.staticfiles.finders.FileSystemFinder",
+    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
 )
 
 # MEDIA CONFIGURATION
 # ------------------------------------------------------------------------------
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
-MEDIA_ROOT = env("MEDIA_ROOT", default=str(APPS_DIR('media')))
+MEDIA_ROOT = env("MEDIA_ROOT", default=str(APPS_DIR("media")))
 
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
-MEDIA_URL = env("MEDIA_URL", default='/media/')
+MEDIA_URL = env("MEDIA_URL", default="/media/")
 
 # URL Configuration
 # ------------------------------------------------------------------------------
-ROOT_URLCONF = 'config.urls'
+ROOT_URLCONF = "config.urls"
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
-WSGI_APPLICATION = 'config.wsgi.application'
+WSGI_APPLICATION = "config.wsgi.application"
 ASGI_APPLICATION = "config.routing.application"
 
 # This ensures that Django will be able to detect a secure connection
-SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
 
 # AUTHENTICATION CONFIGURATION
 # ------------------------------------------------------------------------------
 AUTHENTICATION_BACKENDS = (
-    'django.contrib.auth.backends.ModelBackend',
-    'allauth.account.auth_backends.AuthenticationBackend',
+    "django.contrib.auth.backends.ModelBackend",
+    "allauth.account.auth_backends.AuthenticationBackend",
 )
 SESSION_COOKIE_HTTPONLY = False
 # Some really nice defaults
-ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
+ACCOUNT_AUTHENTICATION_METHOD = "username_email"
 ACCOUNT_EMAIL_REQUIRED = True
-ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
+ACCOUNT_EMAIL_VERIFICATION = "mandatory"
 
 # Custom user app defaults
 # Select the correct user model
-AUTH_USER_MODEL = 'users.User'
-LOGIN_REDIRECT_URL = 'users:redirect'
-LOGIN_URL = 'account_login'
+AUTH_USER_MODEL = "users.User"
+LOGIN_REDIRECT_URL = "users:redirect"
+LOGIN_URL = "account_login"
 
 # SLUGLIFIER
-AUTOSLUG_SLUGIFY_FUNCTION = 'slugify.slugify'
+AUTOSLUG_SLUGIFY_FUNCTION = "slugify.slugify"
 
 CACHE_DEFAULT = "redis://127.0.0.1:6379/0"
-CACHES = {
-    "default": env.cache_url('CACHE_URL', default=CACHE_DEFAULT)
-}
+CACHES = {"default": env.cache_url("CACHE_URL", default=CACHE_DEFAULT)}
 
 CACHES["default"]["BACKEND"] = "django_redis.cache.RedisCache"
-from urllib.parse import urlparse
-cache_url = urlparse(CACHES['default']['LOCATION'])
+
+cache_url = urlparse(CACHES["default"]["LOCATION"])
 CHANNEL_LAYERS = {
     "default": {
         "BACKEND": "channels_redis.core.RedisChannelLayer",
-        "CONFIG": {
-            "hosts": [(cache_url.hostname, cache_url.port)],
-        },
-    },
+        "CONFIG": {"hosts": [(cache_url.hostname, cache_url.port)]},
+    }
 }
 
 CACHES["default"]["OPTIONS"] = {
     "CLIENT_CLASS": "django_redis.client.DefaultClient",
     "IGNORE_EXCEPTIONS": True,  # mimics memcache behavior.
-                                # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
+    # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
 }
 
 
-########## CELERY
-INSTALLED_APPS += ('funkwhale_api.taskapp.celery.CeleryConfig',)
+# CELERY
+INSTALLED_APPS += ("funkwhale_api.taskapp.celery.CeleryConfig",)
 CELERY_BROKER_URL = env(
-    "CELERY_BROKER_URL", default=env('CACHE_URL', default=CACHE_DEFAULT))
-########## END CELERY
+    "CELERY_BROKER_URL", default=env("CACHE_URL", default=CACHE_DEFAULT)
+)
+# END CELERY
 # Location of root django.contrib.admin URL, use {% url 'admin:index' %}
 
 # Your common stuff: Below this line define 3rd party library settings
 CELERY_TASK_DEFAULT_RATE_LIMIT = 1
 CELERY_TASK_TIME_LIMIT = 300
 CELERYBEAT_SCHEDULE = {
-    'federation.clean_music_cache': {
-        'task': 'funkwhale_api.federation.tasks.clean_music_cache',
-        'schedule': crontab(hour='*/2'),
-        'options': {
-            'expires': 60 * 2,
-        },
+    "federation.clean_music_cache": {
+        "task": "funkwhale_api.federation.tasks.clean_music_cache",
+        "schedule": crontab(hour="*/2"),
+        "options": {"expires": 60 * 2},
     }
 }
 
-import datetime
 JWT_AUTH = {
-    'JWT_ALLOW_REFRESH': True,
-    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
-    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=30),
-    'JWT_AUTH_HEADER_PREFIX': 'JWT',
-    'JWT_GET_USER_SECRET_KEY': lambda user: user.secret_key
+    "JWT_ALLOW_REFRESH": True,
+    "JWT_EXPIRATION_DELTA": datetime.timedelta(days=7),
+    "JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=30),
+    "JWT_AUTH_HEADER_PREFIX": "JWT",
+    "JWT_GET_USER_SECRET_KEY": lambda user: user.secret_key,
 }
 OLD_PASSWORD_FIELD_ENABLED = True
-ACCOUNT_ADAPTER = 'funkwhale_api.users.adapters.FunkwhaleAccountAdapter'
+ACCOUNT_ADAPTER = "funkwhale_api.users.adapters.FunkwhaleAccountAdapter"
 CORS_ORIGIN_ALLOW_ALL = True
 # CORS_ORIGIN_WHITELIST = (
 #     'localhost',
@@ -389,41 +367,37 @@ CORS_ORIGIN_ALLOW_ALL = True
 CORS_ALLOW_CREDENTIALS = True
 
 REST_FRAMEWORK = {
-    'DEFAULT_PERMISSION_CLASSES': (
-        'rest_framework.permissions.IsAuthenticated',
-    ),
-    'DEFAULT_PAGINATION_CLASS': 'funkwhale_api.common.pagination.FunkwhalePagination',
-    'PAGE_SIZE': 25,
-    'DEFAULT_PARSER_CLASSES': (
-        'rest_framework.parsers.JSONParser',
-        'rest_framework.parsers.FormParser',
-        'rest_framework.parsers.MultiPartParser',
-        'funkwhale_api.federation.parsers.ActivityParser',
+    "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
+    "DEFAULT_PAGINATION_CLASS": "funkwhale_api.common.pagination.FunkwhalePagination",
+    "PAGE_SIZE": 25,
+    "DEFAULT_PARSER_CLASSES": (
+        "rest_framework.parsers.JSONParser",
+        "rest_framework.parsers.FormParser",
+        "rest_framework.parsers.MultiPartParser",
+        "funkwhale_api.federation.parsers.ActivityParser",
     ),
-    'DEFAULT_AUTHENTICATION_CLASSES': (
-        'funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS',
-        'funkwhale_api.common.authentication.BearerTokenHeaderAuth',
-        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
-        'rest_framework.authentication.SessionAuthentication',
-        'rest_framework.authentication.BasicAuthentication',
+    "DEFAULT_AUTHENTICATION_CLASSES": (
+        "funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS",
+        "funkwhale_api.common.authentication.BearerTokenHeaderAuth",
+        "rest_framework_jwt.authentication.JSONWebTokenAuthentication",
+        "rest_framework.authentication.SessionAuthentication",
+        "rest_framework.authentication.BasicAuthentication",
     ),
-    'DEFAULT_FILTER_BACKENDS': (
-        'rest_framework.filters.OrderingFilter',
-        'django_filters.rest_framework.DjangoFilterBackend',
+    "DEFAULT_FILTER_BACKENDS": (
+        "rest_framework.filters.OrderingFilter",
+        "django_filters.rest_framework.DjangoFilterBackend",
     ),
-    'DEFAULT_RENDERER_CLASSES': (
-        'rest_framework.renderers.JSONRenderer',
-    )
+    "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
 }
 
-BROWSABLE_API_ENABLED = env.bool('BROWSABLE_API_ENABLED', default=False)
+BROWSABLE_API_ENABLED = env.bool("BROWSABLE_API_ENABLED", default=False)
 if BROWSABLE_API_ENABLED:
-    REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] += (
-        'rest_framework.renderers.BrowsableAPIRenderer',
+    REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] += (
+        "rest_framework.renderers.BrowsableAPIRenderer",
     )
 
 REST_AUTH_SERIALIZERS = {
-    'PASSWORD_RESET_SERIALIZER': 'funkwhale_api.users.serializers.PasswordResetSerializer'  # noqa
+    "PASSWORD_RESET_SERIALIZER": "funkwhale_api.users.serializers.PasswordResetSerializer"  # noqa
 }
 REST_SESSION_LOGIN = False
 REST_USE_JWT = True
@@ -434,60 +408,55 @@ USE_X_FORWARDED_PORT = True
 
 # Wether we should use Apache, Nginx (or other) headers when serving audio files
 # Default to Nginx
-REVERSE_PROXY_TYPE = env('REVERSE_PROXY_TYPE', default='nginx')
-assert REVERSE_PROXY_TYPE in ['apache2', 'nginx'], 'Unsupported REVERSE_PROXY_TYPE'
+REVERSE_PROXY_TYPE = env("REVERSE_PROXY_TYPE", default="nginx")
+assert REVERSE_PROXY_TYPE in ["apache2", "nginx"], "Unsupported REVERSE_PROXY_TYPE"
 
 # Which path will be used to process the internal redirection
 # **DO NOT** put a slash at the end
-PROTECT_FILES_PATH = env('PROTECT_FILES_PATH', default='/_protected')
+PROTECT_FILES_PATH = env("PROTECT_FILES_PATH", default="/_protected")
 
 
 # use this setting to tweak for how long you want to cache
 # musicbrainz results. (value is in seconds)
-MUSICBRAINZ_CACHE_DURATION = env.int(
-    'MUSICBRAINZ_CACHE_DURATION',
-    default=300
-)
-CACHEOPS_REDIS = env('CACHE_URL', default=CACHE_DEFAULT)
-CACHEOPS_ENABLED = env.bool('CACHEOPS_ENABLED', default=True)
+MUSICBRAINZ_CACHE_DURATION = env.int("MUSICBRAINZ_CACHE_DURATION", default=300)
+CACHEOPS_REDIS = env("CACHE_URL", default=CACHE_DEFAULT)
+CACHEOPS_ENABLED = env.bool("CACHEOPS_ENABLED", default=True)
 CACHEOPS = {
-    'music.artist': {'ops': 'all', 'timeout': 60 * 60},
-    'music.album': {'ops': 'all', 'timeout': 60 * 60},
-    'music.track': {'ops': 'all', 'timeout': 60 * 60},
-    'music.trackfile': {'ops': 'all', 'timeout': 60 * 60},
-    'taggit.tag': {'ops': 'all', 'timeout': 60 * 60},
+    "music.artist": {"ops": "all", "timeout": 60 * 60},
+    "music.album": {"ops": "all", "timeout": 60 * 60},
+    "music.track": {"ops": "all", "timeout": 60 * 60},
+    "music.trackfile": {"ops": "all", "timeout": 60 * 60},
+    "taggit.tag": {"ops": "all", "timeout": 60 * 60},
 }
 
 # Custom Admin URL, use {% url 'admin:index' %}
-ADMIN_URL = env('DJANGO_ADMIN_URL', default='^api/admin/')
+ADMIN_URL = env("DJANGO_ADMIN_URL", default="^api/admin/")
 CSRF_USE_SESSIONS = True
 
 # Playlist settings
 # XXX: deprecated, see #186
-PLAYLISTS_MAX_TRACKS = env.int('PLAYLISTS_MAX_TRACKS', default=250)
+PLAYLISTS_MAX_TRACKS = env.int("PLAYLISTS_MAX_TRACKS", default=250)
 
 ACCOUNT_USERNAME_BLACKLIST = [
-    'funkwhale',
-    'library',
-    'test',
-    'status',
-    'root',
-    'admin',
-    'owner',
-    'superuser',
-    'staff',
-    'service',
-] + env.list('ACCOUNT_USERNAME_BLACKLIST', default=[])
-
-EXTERNAL_REQUESTS_VERIFY_SSL = env.bool(
-    'EXTERNAL_REQUESTS_VERIFY_SSL',
-    default=True
-)
+    "funkwhale",
+    "library",
+    "test",
+    "status",
+    "root",
+    "admin",
+    "owner",
+    "superuser",
+    "staff",
+    "service",
+] + env.list("ACCOUNT_USERNAME_BLACKLIST", default=[])
+
+EXTERNAL_REQUESTS_VERIFY_SSL = env.bool("EXTERNAL_REQUESTS_VERIFY_SSL", default=True)
 # XXX: deprecated, see #186
 API_AUTHENTICATION_REQUIRED = env.bool("API_AUTHENTICATION_REQUIRED", True)
 
-MUSIC_DIRECTORY_PATH = env('MUSIC_DIRECTORY_PATH', default=None)
+MUSIC_DIRECTORY_PATH = env("MUSIC_DIRECTORY_PATH", default=None)
 # on Docker setup, the music directory may not match the host path,
 # and we need to know it for it to serve stuff properly
 MUSIC_DIRECTORY_SERVE_PATH = env(
-    'MUSIC_DIRECTORY_SERVE_PATH', default=MUSIC_DIRECTORY_PATH)
+    "MUSIC_DIRECTORY_SERVE_PATH", default=MUSIC_DIRECTORY_PATH
+)
diff --git a/api/config/settings/local.py b/api/config/settings/local.py
index df14945cc90538a7fe8ea39e66e3833aa809581c..9f0119cee54f51ec10377e662bdc8e8fa2bf32b2 100644
--- a/api/config/settings/local.py
+++ b/api/config/settings/local.py
@@ -1,79 +1,72 @@
 # -*- coding: utf-8 -*-
-'''
+"""
 Local settings
 
 - Run in Debug mode
 - Use console backend for emails
 - Add Django Debug Toolbar
 - Add django-extensions as app
-'''
+"""
 
 from .common import *  # noqa
 
+
 # DEBUG
 # ------------------------------------------------------------------------------
-DEBUG = env.bool('DJANGO_DEBUG', default=True)
-TEMPLATES[0]['OPTIONS']['debug'] = DEBUG
+DEBUG = env.bool("DJANGO_DEBUG", default=True)
+TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG
 
 # SECRET CONFIGURATION
 # ------------------------------------------------------------------------------
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
 # Note: This key only used for development and testing.
-SECRET_KEY = env("DJANGO_SECRET_KEY", default='mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0sw@r5sua*1zp4fmxc')
+SECRET_KEY = env(
+    "DJANGO_SECRET_KEY", default="mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0sw@r5sua*1zp4fmxc"
+)
 
 # Mail settings
 # ------------------------------------------------------------------------------
-EMAIL_HOST = 'localhost'
+EMAIL_HOST = "localhost"
 EMAIL_PORT = 1025
 
 # django-debug-toolbar
 # ------------------------------------------------------------------------------
-MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
+MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",)
 
 # INTERNAL_IPS = ('127.0.0.1', '10.0.2.2',)
 
 DEBUG_TOOLBAR_CONFIG = {
-    'DISABLE_PANELS': [
-        'debug_toolbar.panels.redirects.RedirectsPanel',
-    ],
-    'SHOW_TEMPLATE_CONTEXT': True,
-    'SHOW_TOOLBAR_CALLBACK': lambda request: True,
+    "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
+    "SHOW_TEMPLATE_CONTEXT": True,
+    "SHOW_TOOLBAR_CALLBACK": lambda request: True,
 }
 
 # django-extensions
 # ------------------------------------------------------------------------------
 # INSTALLED_APPS += ('django_extensions', )
-INSTALLED_APPS += ('debug_toolbar', )
+INSTALLED_APPS += ("debug_toolbar",)
 
 # TESTING
 # ------------------------------------------------------------------------------
-TEST_RUNNER = 'django.test.runner.DiscoverRunner'
+TEST_RUNNER = "django.test.runner.DiscoverRunner"
 
-########## CELERY
+# CELERY
 # In development, all tasks will be executed locally by blocking until the task returns
 CELERY_TASK_ALWAYS_EAGER = False
-########## END CELERY
+# END CELERY
 
 # Your local stuff: Below this line define 3rd party library settings
 
 LOGGING = {
-    'version': 1,
-    'handlers': {
-        'console':{
-            'level':'DEBUG',
-            'class':'logging.StreamHandler',
-        },
-    },
-    'loggers': {
-        'django.request': {
-            'handlers':['console'],
-            'propagate': True,
-            'level':'DEBUG',
-        },
-        '': {
-            'level': 'DEBUG',
-            'handlers': ['console'],
+    "version": 1,
+    "handlers": {"console": {"level": "DEBUG", "class": "logging.StreamHandler"}},
+    "loggers": {
+        "django.request": {
+            "handlers": ["console"],
+            "propagate": True,
+            "level": "DEBUG",
         },
+        "": {"level": "DEBUG", "handlers": ["console"]},
     },
 }
 CSRF_TRUSTED_ORIGINS = [o for o in ALLOWED_HOSTS]
diff --git a/api/config/settings/production.py b/api/config/settings/production.py
index 39be40dc32065dae8e8a5c3879c2aa4f9b43ae9c..72b08aa3c867efa475efb3dd8b5f502eed775abb 100644
--- a/api/config/settings/production.py
+++ b/api/config/settings/production.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-'''
+"""
 Production Configurations
 
 - Use djangosecure
@@ -8,12 +8,9 @@ Production Configurations
 - Use Redis on Heroku
 
 
-'''
+"""
 from __future__ import absolute_import, unicode_literals
 
-from django.utils import six
-
-
 from .common import *  # noqa
 
 # SECRET CONFIGURATION
@@ -58,19 +55,24 @@ CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
 # ------------------------------------------------------------------------------
 # Uploaded Media Files
 # ------------------------
-DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
+DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
 
 # Static Assets
 # ------------------------
-STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
+STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
 
 # TEMPLATE CONFIGURATION
 # ------------------------------------------------------------------------------
 # See:
 # https://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.loaders.cached.Loader
-TEMPLATES[0]['OPTIONS']['loaders'] = [
-    ('django.template.loaders.cached.Loader', [
-        'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ]),
+TEMPLATES[0]["OPTIONS"]["loaders"] = [
+    (
+        "django.template.loaders.cached.Loader",
+        [
+            "django.template.loaders.filesystem.Loader",
+            "django.template.loaders.app_directories.Loader",
+        ],
+    )
 ]
 
 # CACHING
@@ -78,7 +80,6 @@ TEMPLATES[0]['OPTIONS']['loaders'] = [
 # Heroku URL does not pass the DB number, so we parse it in
 
 
-
 # LOGGING CONFIGURATION
 # ------------------------------------------------------------------------------
 # See: https://docs.djangoproject.com/en/dev/ref/settings/#logging
@@ -88,43 +89,39 @@ TEMPLATES[0]['OPTIONS']['loaders'] = [
 # See http://docs.djangoproject.com/en/dev/topics/logging for
 # more details on how to customize your logging configuration.
 LOGGING = {
-    'version': 1,
-    'disable_existing_loggers': False,
-    'filters': {
-        'require_debug_false': {
-            '()': 'django.utils.log.RequireDebugFalse'
+    "version": 1,
+    "disable_existing_loggers": False,
+    "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
+    "formatters": {
+        "verbose": {
+            "format": "%(levelname)s %(asctime)s %(module)s "
+            "%(process)d %(thread)d %(message)s"
         }
     },
-    'formatters': {
-        'verbose': {
-            'format': '%(levelname)s %(asctime)s %(module)s '
-                      '%(process)d %(thread)d %(message)s'
+    "handlers": {
+        "mail_admins": {
+            "level": "ERROR",
+            "filters": ["require_debug_false"],
+            "class": "django.utils.log.AdminEmailHandler",
+        },
+        "console": {
+            "level": "DEBUG",
+            "class": "logging.StreamHandler",
+            "formatter": "verbose",
         },
     },
-    'handlers': {
-        'mail_admins': {
-            'level': 'ERROR',
-            'filters': ['require_debug_false'],
-            'class': 'django.utils.log.AdminEmailHandler'
+    "loggers": {
+        "django.request": {
+            "handlers": ["mail_admins"],
+            "level": "ERROR",
+            "propagate": True,
         },
-        'console': {
-            'level': 'DEBUG',
-            'class': 'logging.StreamHandler',
-            'formatter': 'verbose',
+        "django.security.DisallowedHost": {
+            "level": "ERROR",
+            "handlers": ["console", "mail_admins"],
+            "propagate": True,
         },
     },
-    'loggers': {
-        'django.request': {
-            'handlers': ['mail_admins'],
-            'level': 'ERROR',
-            'propagate': True
-        },
-        'django.security.DisallowedHost': {
-            'level': 'ERROR',
-            'handlers': ['console', 'mail_admins'],
-            'propagate': True
-        }
-    }
 }
 
 
diff --git a/api/config/urls.py b/api/config/urls.py
index 90598ea841f474e5b887fda7fe42f23975cd4c00..5ffcf211b8588c5e9ec78d21baa11f288ebf9a20 100644
--- a/api/config/urls.py
+++ b/api/config/urls.py
@@ -5,38 +5,35 @@ from django.conf import settings
 from django.conf.urls import include, url
 from django.conf.urls.static import static
 from django.contrib import admin
-from django.views.generic import TemplateView
 from django.views import defaults as default_views
 
 urlpatterns = [
     # Django Admin, use {% url 'admin:index' %}
     url(settings.ADMIN_URL, admin.site.urls),
-
-    url(r'^api/', include(("config.api_urls", 'api'), namespace="api")),
-    url(r'^', include(
-        ('funkwhale_api.federation.urls', 'federation'),
-        namespace="federation")),
-    url(r'^api/v1/auth/', include('rest_auth.urls')),
-    url(r'^api/v1/auth/registration/', include('funkwhale_api.users.rest_auth_urls')),
-    url(r'^accounts/', include('allauth.urls')),
-
+    url(r"^api/", include(("config.api_urls", "api"), namespace="api")),
+    url(
+        r"^",
+        include(
+            ("funkwhale_api.federation.urls", "federation"), namespace="federation"
+        ),
+    ),
+    url(r"^api/v1/auth/", include("rest_auth.urls")),
+    url(r"^api/v1/auth/registration/", include("funkwhale_api.users.rest_auth_urls")),
+    url(r"^accounts/", include("allauth.urls")),
     # Your stuff: custom urls includes go here
-
-
 ]
 
 if settings.DEBUG:
     # This allows the error pages to be debugged during development, just visit
     # these url in browser to see how these error pages look like.
     urlpatterns += [
-        url(r'^400/$', default_views.bad_request),
-        url(r'^403/$', default_views.permission_denied),
-        url(r'^404/$', default_views.page_not_found),
-        url(r'^500/$', default_views.server_error),
+        url(r"^400/$", default_views.bad_request),
+        url(r"^403/$", default_views.permission_denied),
+        url(r"^404/$", default_views.page_not_found),
+        url(r"^500/$", default_views.server_error),
     ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
 
-    if 'debug_toolbar' in settings.INSTALLED_APPS:
+    if "debug_toolbar" in settings.INSTALLED_APPS:
         import debug_toolbar
-        urlpatterns += [
-            url(r'^__debug__/', include(debug_toolbar.urls)),
-        ]
+
+        urlpatterns += [url(r"^__debug__/", include(debug_toolbar.urls))]
diff --git a/api/config/wsgi.py b/api/config/wsgi.py
index a53b580d76c8f5c1b9659c8cdcdb1b0382433afe..8e843eb4d29c4f162fe23e1a1a2ea3c0ed622fc5 100644
--- a/api/config/wsgi.py
+++ b/api/config/wsgi.py
@@ -15,11 +15,9 @@ 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
diff --git a/api/demo/demo-user.py b/api/demo/demo-user.py
index 4f8648fb37288ad6ff18d59be88ea3bac207fdcc..94757d2faab203b7938278e4f4f6a2289fad77f2 100644
--- a/api/demo/demo-user.py
+++ b/api/demo/demo-user.py
@@ -1,7 +1,7 @@
 from funkwhale_api.users.models import User
 
 
-u = User.objects.create(email='demo@demo.com', username='demo', is_staff=True)
-u.set_password('demo')
-u.subsonic_api_token = 'demo'
+u = User.objects.create(email="demo@demo.com", username="demo", is_staff=True)
+u.set_password("demo")
+u.subsonic_api_token = "demo"
 u.save()
diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py
index 8b5b81ad4b070cafc4de189a439c46a8195efebd..44b80d2dc5ae0236d67d26dc29b668ad321aca84 100644
--- a/api/funkwhale_api/__init__.py
+++ b/api/funkwhale_api/__init__.py
@@ -1,3 +1,8 @@
 # -*- coding: utf-8 -*-
-__version__ = '0.14.1'
-__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')])
+__version__ = "0.14.2"
+__version_info__ = tuple(
+    [
+        int(num) if num.isdigit() else num
+        for num in __version__.replace("-", ".", 1).split(".")
+    ]
+)
diff --git a/api/funkwhale_api/activity/apps.py b/api/funkwhale_api/activity/apps.py
index 0c66cbf50cf2542499ccce1fbdb262a13600324d..b70f65c57146d061bdf200110f4681e117c91a51 100644
--- a/api/funkwhale_api/activity/apps.py
+++ b/api/funkwhale_api/activity/apps.py
@@ -2,8 +2,9 @@ from django.apps import AppConfig, apps
 
 from . import record
 
+
 class ActivityConfig(AppConfig):
-    name = 'funkwhale_api.activity'
+    name = "funkwhale_api.activity"
 
     def ready(self):
         super(ActivityConfig, self).ready()
diff --git a/api/funkwhale_api/activity/record.py b/api/funkwhale_api/activity/record.py
index fa55c0e85288318acd2f3d41ab02de9805eb8632..3e34b1027f27a260a3c5085e9e0e5db99562c790 100644
--- a/api/funkwhale_api/activity/record.py
+++ b/api/funkwhale_api/activity/record.py
@@ -2,37 +2,36 @@ import persisting_theory
 
 
 class ActivityRegistry(persisting_theory.Registry):
-    look_into = 'activities'
+    look_into = "activities"
 
     def _register_for_model(self, model, attr, value):
         key = model._meta.label
-        d = self.setdefault(key, {'consumers': []})
+        d = self.setdefault(key, {"consumers": []})
         d[attr] = value
 
     def register_serializer(self, serializer_class):
         model = serializer_class.Meta.model
-        self._register_for_model(model, 'serializer', serializer_class)
+        self._register_for_model(model, "serializer", serializer_class)
         return serializer_class
 
     def register_consumer(self, label):
         def decorator(func):
-            consumers = self[label]['consumers']
+            consumers = self[label]["consumers"]
             if func not in consumers:
                 consumers.append(func)
             return func
+
         return decorator
 
 
 registry = ActivityRegistry()
 
 
-
-
 def send(obj):
     conf = registry[obj.__class__._meta.label]
-    consumers = conf['consumers']
+    consumers = conf["consumers"]
     if not consumers:
         return
-    serializer = conf['serializer'](obj)
+    serializer = conf["serializer"](obj)
     for consumer in consumers:
         consumer(data=serializer.data, obj=obj)
diff --git a/api/funkwhale_api/activity/serializers.py b/api/funkwhale_api/activity/serializers.py
index fd9b185cf9a6d3891f0356208f62b2bb54e8686f..6df3a58705591de9644176c0baafa7027b069b33 100644
--- a/api/funkwhale_api/activity/serializers.py
+++ b/api/funkwhale_api/activity/serializers.py
@@ -4,8 +4,8 @@ from funkwhale_api.activity import record
 
 
 class ModelSerializer(serializers.ModelSerializer):
-    id = serializers.CharField(source='get_activity_url')
-    local_id = serializers.IntegerField(source='id')
+    id = serializers.CharField(source="get_activity_url")
+    local_id = serializers.IntegerField(source="id")
     # url = serializers.SerializerMethodField()
 
     def get_url(self, obj):
@@ -17,8 +17,7 @@ class AutoSerializer(serializers.Serializer):
     A serializer that will automatically use registered activity serializers
     to serialize an henerogeneous list of objects (favorites, listenings, etc.)
     """
+
     def to_representation(self, instance):
-        serializer = record.registry[instance._meta.label]['serializer'](
-            instance
-        )
+        serializer = record.registry[instance._meta.label]["serializer"](instance)
         return serializer.data
diff --git a/api/funkwhale_api/activity/utils.py b/api/funkwhale_api/activity/utils.py
index 46336930ef693e29d9d5696ee3ccf12446c2bdad..236d23d88ee6d2514c858474b41038942aeda3a7 100644
--- a/api/funkwhale_api/activity/utils.py
+++ b/api/funkwhale_api/activity/utils.py
@@ -6,31 +6,25 @@ from funkwhale_api.history.models import Listening
 
 
 def combined_recent(limit, **kwargs):
-    datetime_field = kwargs.pop('datetime_field', 'creation_date')
-    source_querysets = {
-        qs.model._meta.label: qs for qs in kwargs.pop('querysets')
-    }
+    datetime_field = kwargs.pop("datetime_field", "creation_date")
+    source_querysets = {qs.model._meta.label: qs for qs in kwargs.pop("querysets")}
     querysets = {
         k: qs.annotate(
-            __type=models.Value(
-                qs.model._meta.label, output_field=models.CharField()
-            )
-        ).values('pk', datetime_field, '__type')
+            __type=models.Value(qs.model._meta.label, output_field=models.CharField())
+        ).values("pk", datetime_field, "__type")
         for k, qs in source_querysets.items()
     }
     _qs_list = list(querysets.values())
     union_qs = _qs_list[0].union(*_qs_list[1:])
     records = []
-    for row in union_qs.order_by('-{}'.format(datetime_field))[:limit]:
-        records.append({
-            'type': row['__type'],
-            'when': row[datetime_field],
-            'pk': row['pk']
-        })
+    for row in union_qs.order_by("-{}".format(datetime_field))[:limit]:
+        records.append(
+            {"type": row["__type"], "when": row[datetime_field], "pk": row["pk"]}
+        )
     # Now we bulk-load each object type in turn
     to_load = {}
     for record in records:
-        to_load.setdefault(record['type'], []).append(record['pk'])
+        to_load.setdefault(record["type"], []).append(record["pk"])
     fetched = {}
 
     for key, pks in to_load.items():
@@ -39,26 +33,19 @@ def combined_recent(limit, **kwargs):
 
     # Annotate 'records' with loaded objects
     for record in records:
-        record['object'] = fetched[(record['type'], record['pk'])]
+        record["object"] = fetched[(record["type"], record["pk"])]
     return records
 
 
 def get_activity(user, limit=20):
-    query = fields.privacy_level_query(
-        user, lookup_field='user__privacy_level')
+    query = fields.privacy_level_query(user, lookup_field="user__privacy_level")
     querysets = [
         Listening.objects.filter(query).select_related(
-            'track',
-            'user',
-            'track__artist',
-            'track__album__artist',
+            "track", "user", "track__artist", "track__album__artist"
         ),
         TrackFavorite.objects.filter(query).select_related(
-            'track',
-            'user',
-            'track__artist',
-            'track__album__artist',
+            "track", "user", "track__artist", "track__album__artist"
         ),
     ]
     records = combined_recent(limit=limit, querysets=querysets)
-    return [r['object'] for r in records]
+    return [r["object"] for r in records]
diff --git a/api/funkwhale_api/activity/views.py b/api/funkwhale_api/activity/views.py
index e66de1ccfdc94f51cd823fa5c6b104488a4aad7f..701dd04b8cfbacc1f0b5fc75ab65597c87a99b55 100644
--- a/api/funkwhale_api/activity/views.py
+++ b/api/funkwhale_api/activity/views.py
@@ -4,8 +4,7 @@ from rest_framework.response import Response
 from funkwhale_api.common.permissions import ConditionalAuthentication
 from funkwhale_api.favorites.models import TrackFavorite
 
-from . import serializers
-from . import utils
+from . import serializers, utils
 
 
 class ActivityViewSet(viewsets.GenericViewSet):
@@ -17,4 +16,4 @@ class ActivityViewSet(viewsets.GenericViewSet):
     def list(self, request, *args, **kwargs):
         activity = utils.get_activity(user=request.user)
         serializer = self.serializer_class(activity, many=True)
-        return Response({'results': serializer.data}, status=200)
+        return Response({"results": serializer.data}, status=200)
diff --git a/api/funkwhale_api/common/auth.py b/api/funkwhale_api/common/auth.py
index faf13571d6cd73208530a479ed92364989e304ad..7717c836babb4940902593185416c6a6430949ed 100644
--- a/api/funkwhale_api/common/auth.py
+++ b/api/funkwhale_api/common/auth.py
@@ -1,12 +1,7 @@
 from urllib.parse import parse_qs
 
-import jwt
-
 from django.contrib.auth.models import AnonymousUser
-from django.utils.encoding import smart_text
-
 from rest_framework import exceptions
-from rest_framework_jwt.settings import api_settings
 from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
 
 from funkwhale_api.users.models import User
@@ -16,20 +11,19 @@ class TokenHeaderAuth(BaseJSONWebTokenAuthentication):
     def get_jwt_value(self, request):
 
         try:
-            qs = request.get('query_string', b'').decode('utf-8')
+            qs = request.get("query_string", b"").decode("utf-8")
             parsed = parse_qs(qs)
-            token = parsed['token'][0]
+            token = parsed["token"][0]
         except KeyError:
-            raise exceptions.AuthenticationFailed('No token')
+            raise exceptions.AuthenticationFailed("No token")
 
         if not token:
-            raise exceptions.AuthenticationFailed('Empty token')
+            raise exceptions.AuthenticationFailed("Empty token")
 
         return token
 
 
 class TokenAuthMiddleware:
-
     def __init__(self, inner):
         # Store the ASGI application we were passed
         self.inner = inner
@@ -41,5 +35,5 @@ class TokenAuthMiddleware:
         except (User.DoesNotExist, exceptions.AuthenticationFailed):
             user = AnonymousUser()
 
-        scope['user'] = user
+        scope["user"] = user
         return self.inner(scope)
diff --git a/api/funkwhale_api/common/authentication.py b/api/funkwhale_api/common/authentication.py
index c7566eac8bd0f3d5c115a3a5d6b16dd3aec5dd9e..10bf36613f7e7cb99a20e26184dca552cc7eed44 100644
--- a/api/funkwhale_api/common/authentication.py
+++ b/api/funkwhale_api/common/authentication.py
@@ -1,39 +1,38 @@
 from django.utils.encoding import smart_text
 from django.utils.translation import ugettext as _
-
 from rest_framework import exceptions
 from rest_framework_jwt import authentication
 from rest_framework_jwt.settings import api_settings
 
 
-class JSONWebTokenAuthenticationQS(
-        authentication.BaseJSONWebTokenAuthentication):
+class JSONWebTokenAuthenticationQS(authentication.BaseJSONWebTokenAuthentication):
 
-    www_authenticate_realm = 'api'
+    www_authenticate_realm = "api"
 
     def get_jwt_value(self, request):
-        token = request.query_params.get('jwt')
-        if 'jwt' in request.query_params and not token:
-            msg = _('Invalid Authorization header. No credentials provided.')
+        token = request.query_params.get("jwt")
+        if "jwt" in request.query_params and not token:
+            msg = _("Invalid Authorization header. No credentials provided.")
             raise exceptions.AuthenticationFailed(msg)
         return token
 
     def authenticate_header(self, request):
         return '{0} realm="{1}"'.format(
-            api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm)
+            api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm
+        )
 
 
-class BearerTokenHeaderAuth(
-        authentication.BaseJSONWebTokenAuthentication):
+class BearerTokenHeaderAuth(authentication.BaseJSONWebTokenAuthentication):
     """
     For backward compatibility purpose, we used Authorization: JWT <token>
     but Authorization: Bearer <token> is probably better.
     """
-    www_authenticate_realm = 'api'
+
+    www_authenticate_realm = "api"
 
     def get_jwt_value(self, request):
         auth = authentication.get_authorization_header(request).split()
-        auth_header_prefix = 'bearer'
+        auth_header_prefix = "bearer"
 
         if not auth:
             if api_settings.JWT_AUTH_COOKIE:
@@ -44,14 +43,16 @@ class BearerTokenHeaderAuth(
             return None
 
         if len(auth) == 1:
-            msg = _('Invalid Authorization header. No credentials provided.')
+            msg = _("Invalid Authorization header. No credentials provided.")
             raise exceptions.AuthenticationFailed(msg)
         elif len(auth) > 2:
-            msg = _('Invalid Authorization header. Credentials string '
-                    'should not contain spaces.')
+            msg = _(
+                "Invalid Authorization header. Credentials string "
+                "should not contain spaces."
+            )
             raise exceptions.AuthenticationFailed(msg)
 
         return auth[1]
 
     def authenticate_header(self, request):
-        return '{0} realm="{1}"'.format('Bearer', self.www_authenticate_realm)
+        return '{0} realm="{1}"'.format("Bearer", self.www_authenticate_realm)
diff --git a/api/funkwhale_api/common/consumers.py b/api/funkwhale_api/common/consumers.py
index 300ce5e26e50068c88e91edd5dcedef5ce3de412..47a666f0540fe15a7a472478b9d75516d8706ef4 100644
--- a/api/funkwhale_api/common/consumers.py
+++ b/api/funkwhale_api/common/consumers.py
@@ -1,11 +1,12 @@
 from channels.generic.websocket import JsonWebsocketConsumer
+
 from funkwhale_api.common import channels
 
 
 class JsonAuthConsumer(JsonWebsocketConsumer):
     def connect(self):
         try:
-            assert self.scope['user'].pk is not None
+            assert self.scope["user"].pk is not None
         except (AssertionError, AttributeError, KeyError):
             return self.close()
 
diff --git a/api/funkwhale_api/common/dynamic_preferences_registry.py b/api/funkwhale_api/common/dynamic_preferences_registry.py
index 15b182671bbf386b401eeb541c3839889342e29d..d6dfed78376503387c48247426ee6925772dbdff 100644
--- a/api/funkwhale_api/common/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/common/dynamic_preferences_registry.py
@@ -3,18 +3,19 @@ from dynamic_preferences.registries import global_preferences_registry
 
 from funkwhale_api.common import preferences
 
-common = types.Section('common')
+common = types.Section("common")
 
 
 @global_preferences_registry.register
 class APIAutenticationRequired(
-        preferences.DefaultFromSettingMixin, types.BooleanPreference):
+    preferences.DefaultFromSettingMixin, types.BooleanPreference
+):
     section = common
-    name = 'api_authentication_required'
-    verbose_name = 'API Requires authentication'
-    setting = 'API_AUTHENTICATION_REQUIRED'
+    name = "api_authentication_required"
+    verbose_name = "API Requires authentication"
+    setting = "API_AUTHENTICATION_REQUIRED"
     help_text = (
-        'If disabled, anonymous users will be able to query the API'
-        'and access music data (as well as other data exposed in the API '
-        'without specific permissions).'
+        "If disabled, anonymous users will be able to query the API"
+        "and access music data (as well as other data exposed in the API "
+        "without specific permissions)."
     )
diff --git a/api/funkwhale_api/common/fields.py b/api/funkwhale_api/common/fields.py
index 98e971662758ed67959431beead7052c34b43e43..190576efa688db2d5d9748841791b76531749136 100644
--- a/api/funkwhale_api/common/fields.py
+++ b/api/funkwhale_api/common/fields.py
@@ -1,39 +1,34 @@
 import django_filters
-
 from django.db import models
 
 from funkwhale_api.music import utils
 
-
 PRIVACY_LEVEL_CHOICES = [
-    ('me', 'Only me'),
-    ('followers', 'Me and my followers'),
-    ('instance', 'Everyone on my instance, and my followers'),
-    ('everyone', 'Everyone, including people on other instances'),
+    ("me", "Only me"),
+    ("followers", "Me and my followers"),
+    ("instance", "Everyone on my instance, and my followers"),
+    ("everyone", "Everyone, including people on other instances"),
 ]
 
 
 def get_privacy_field():
     return models.CharField(
-        max_length=30, choices=PRIVACY_LEVEL_CHOICES, default='instance')
+        max_length=30, choices=PRIVACY_LEVEL_CHOICES, default="instance"
+    )
 
 
-def privacy_level_query(user, lookup_field='privacy_level'):
+def privacy_level_query(user, lookup_field="privacy_level"):
     if user.is_anonymous:
-        return models.Q(**{
-            lookup_field: 'everyone',
-        })
+        return models.Q(**{lookup_field: "everyone"})
 
-    return models.Q(**{
-        '{}__in'.format(lookup_field): [
-            'followers', 'instance', 'everyone'
-        ]
-    })
+    return models.Q(
+        **{"{}__in".format(lookup_field): ["followers", "instance", "everyone"]}
+    )
 
 
 class SearchFilter(django_filters.CharFilter):
     def __init__(self, *args, **kwargs):
-        self.search_fields = kwargs.pop('search_fields')
+        self.search_fields = kwargs.pop("search_fields")
         super().__init__(*args, **kwargs)
 
     def filter(self, qs, value):
diff --git a/api/funkwhale_api/common/management/commands/script.py b/api/funkwhale_api/common/management/commands/script.py
index 9d26a5836d46906967e24662a94bab553fa15880..b46a4327b0996a97aadd49a6b3066e99644c6e7c 100644
--- a/api/funkwhale_api/common/management/commands/script.py
+++ b/api/funkwhale_api/common/management/commands/script.py
@@ -4,17 +4,20 @@ from funkwhale_api.common import scripts
 
 
 class Command(BaseCommand):
-    help = 'Run a specific script from funkwhale_api/common/scripts/'
+    help = "Run a specific script from funkwhale_api/common/scripts/"
 
     def add_arguments(self, parser):
-        parser.add_argument('script_name', nargs='?', type=str)
+        parser.add_argument("script_name", nargs="?", type=str)
         parser.add_argument(
-            '--noinput', '--no-input', action='store_false', dest='interactive',
+            "--noinput",
+            "--no-input",
+            action="store_false",
+            dest="interactive",
             help="Do NOT prompt the user for input of any kind.",
         )
 
     def handle(self, *args, **options):
-        name = options['script_name']
+        name = options["script_name"]
         if not name:
             self.show_help()
 
@@ -23,44 +26,43 @@ class Command(BaseCommand):
             script = available_scripts[name]
         except KeyError:
             raise CommandError(
-                '{} is not a valid script. Run python manage.py script for a '
-                'list of available scripts'.format(name))
+                "{} is not a valid script. Run python manage.py script for a "
+                "list of available scripts".format(name)
+            )
 
-        self.stdout.write('')
-        if options['interactive']:
+        self.stdout.write("")
+        if options["interactive"]:
             message = (
-                'Are you sure you want to execute the script {}?\n\n'
+                "Are you sure you want to execute the script {}?\n\n"
                 "Type 'yes' to continue, or 'no' to cancel: "
             ).format(name)
-            if input(''.join(message)) != 'yes':
+            if input("".join(message)) != "yes":
                 raise CommandError("Script cancelled.")
-        script['entrypoint'](self, **options)
+        script["entrypoint"](self, **options)
 
     def show_help(self):
-        indentation = 4
-        self.stdout.write('')
-        self.stdout.write('Available scripts:')
-        self.stdout.write('Launch with: python manage.py <script_name>')
+        self.stdout.write("")
+        self.stdout.write("Available scripts:")
+        self.stdout.write("Launch with: python manage.py <script_name>")
         available_scripts = self.get_scripts()
         for name, script in sorted(available_scripts.items()):
-            self.stdout.write('')
+            self.stdout.write("")
             self.stdout.write(self.style.SUCCESS(name))
-            self.stdout.write('')
-            for line in script['help'].splitlines():
-                self.stdout.write('     {}'.format(line))
-        self.stdout.write('')
+            self.stdout.write("")
+            for line in script["help"].splitlines():
+                self.stdout.write("     {}".format(line))
+        self.stdout.write("")
 
     def get_scripts(self):
         available_scripts = [
-            k for k in sorted(scripts.__dict__.keys())
-            if not k.startswith('__')
+            k for k in sorted(scripts.__dict__.keys()) if not k.startswith("__")
         ]
         data = {}
         for name in available_scripts:
             module = getattr(scripts, name)
             data[name] = {
-                'name': name,
-                'help': module.__doc__.strip(),
-                'entrypoint': module.main
+                "name": name,
+                "help": module.__doc__.strip(),
+                "entrypoint": module.main,
             }
         return data
diff --git a/api/funkwhale_api/common/migrations/0001_initial.py b/api/funkwhale_api/common/migrations/0001_initial.py
index e95cc11e9a464eaa2a72de047262f78af0888038..a362855b8c2bb44983128d99aa211a1268f1d7dd 100644
--- a/api/funkwhale_api/common/migrations/0001_initial.py
+++ b/api/funkwhale_api/common/migrations/0001_initial.py
@@ -7,6 +7,4 @@ class Migration(migrations.Migration):
 
     dependencies = []
 
-    operations = [
-        UnaccentExtension()
-    ]
+    operations = [UnaccentExtension()]
diff --git a/api/funkwhale_api/common/pagination.py b/api/funkwhale_api/common/pagination.py
index 20efcb7413bbbb1d2413b766c0789bbcc882f313..e5068bce209da72523077a0d1dee0b7938eba422 100644
--- a/api/funkwhale_api/common/pagination.py
+++ b/api/funkwhale_api/common/pagination.py
@@ -2,5 +2,5 @@ from rest_framework.pagination import PageNumberPagination
 
 
 class FunkwhalePagination(PageNumberPagination):
-    page_size_query_param = 'page_size'
+    page_size_query_param = "page_size"
     max_page_size = 50
diff --git a/api/funkwhale_api/common/permissions.py b/api/funkwhale_api/common/permissions.py
index e9e8b8819f4b70e9ada3a0bcf71c709c9a5ef1de..8f391a70c16f825e1d0b7498ec577b69853ccc00 100644
--- a/api/funkwhale_api/common/permissions.py
+++ b/api/funkwhale_api/common/permissions.py
@@ -1,17 +1,14 @@
 import operator
 
-from django.conf import settings
 from django.http import Http404
-
 from rest_framework.permissions import BasePermission
 
 from funkwhale_api.common import preferences
 
 
 class ConditionalAuthentication(BasePermission):
-
     def has_permission(self, request, view):
-        if preferences.get('common__api_authentication_required'):
+        if preferences.get("common__api_authentication_required"):
             return request.user and request.user.is_authenticated
         return True
 
@@ -28,24 +25,25 @@ class OwnerPermission(BasePermission):
         owner_field = 'owner'
         owner_checks = ['read', 'write']
     """
+
     perms_map = {
-        'GET': 'read',
-        'OPTIONS': 'read',
-        'HEAD': 'read',
-        'POST': 'write',
-        'PUT': 'write',
-        'PATCH': 'write',
-        'DELETE': 'write',
+        "GET": "read",
+        "OPTIONS": "read",
+        "HEAD": "read",
+        "POST": "write",
+        "PUT": "write",
+        "PATCH": "write",
+        "DELETE": "write",
     }
 
     def has_object_permission(self, request, view, obj):
         method_check = self.perms_map[request.method]
-        owner_checks = getattr(view, 'owner_checks', ['read', 'write'])
+        owner_checks = getattr(view, "owner_checks", ["read", "write"])
         if method_check not in owner_checks:
             # check not enabled
             return True
 
-        owner_field = getattr(view, 'owner_field', 'user')
+        owner_field = getattr(view, "owner_field", "user")
         owner = operator.attrgetter(owner_field)(obj)
         if owner != request.user:
             raise Http404
diff --git a/api/funkwhale_api/common/preferences.py b/api/funkwhale_api/common/preferences.py
index a2d3f04b7f2414aa1e9602330c65b1abe3a9a750..acda9a90c31882a32c043edf9d3c8e44d2ea7c3e 100644
--- a/api/funkwhale_api/common/preferences.py
+++ b/api/funkwhale_api/common/preferences.py
@@ -1,8 +1,6 @@
-from django.conf import settings
 from django import forms
-
-from dynamic_preferences import serializers
-from dynamic_preferences import types
+from django.conf import settings
+from dynamic_preferences import serializers, types
 from dynamic_preferences.registries import global_preferences_registry
 
 
@@ -17,7 +15,7 @@ def get(pref):
 
 
 class StringListSerializer(serializers.BaseSerializer):
-    separator = ','
+    separator = ","
     sort = True
 
     @classmethod
@@ -27,8 +25,8 @@ class StringListSerializer(serializers.BaseSerializer):
 
         if type(value) not in [list, tuple]:
             raise cls.exception(
-                "Cannot serialize, value {} is not a list or a tuple".format(
-                    value))
+                "Cannot serialize, value {} is not a list or a tuple".format(value)
+            )
 
         if cls.sort:
             value = sorted(value)
@@ -38,7 +36,7 @@ class StringListSerializer(serializers.BaseSerializer):
     def to_python(cls, value, **kwargs):
         if not value:
             return []
-        return value.split(',')
+        return value.split(",")
 
 
 class StringListPreference(types.BasePreferenceType):
@@ -47,5 +45,5 @@ class StringListPreference(types.BasePreferenceType):
 
     def get_api_additional_data(self):
         d = super(StringListPreference, self).get_api_additional_data()
-        d['choices'] = self.get('choices')
+        d["choices"] = self.get("choices")
         return d
diff --git a/api/funkwhale_api/common/scripts/__init__.py b/api/funkwhale_api/common/scripts/__init__.py
index 4b2d525202218c43483dae60b6df4f4c6090723e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/api/funkwhale_api/common/scripts/__init__.py
+++ b/api/funkwhale_api/common/scripts/__init__.py
@@ -1,2 +0,0 @@
-from . import django_permissions_to_user_permissions
-from . import test
diff --git a/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py b/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py
index 1bc971f80911de276ef2c210fafddfc052d71ee6..48144f8ea7f737a39d94a1f1b96573557109300d 100644
--- a/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py
+++ b/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py
@@ -2,28 +2,28 @@
 Convert django permissions to user permissions in the database,
 following the work done in #152.
 """
+from django.contrib.auth.models import Permission
 from django.db.models import Q
-from funkwhale_api.users import models
 
-from django.contrib.auth.models import Permission
+from funkwhale_api.users import models
 
 mapping = {
-    'dynamic_preferences.change_globalpreferencemodel': 'settings',
-    'music.add_importbatch': 'library',
-    'federation.change_library': 'federation',
+    "dynamic_preferences.change_globalpreferencemodel": "settings",
+    "music.add_importbatch": "library",
+    "federation.change_library": "federation",
 }
 
 
 def main(command, **kwargs):
     for codename, user_permission in sorted(mapping.items()):
-        app_label, c = codename.split('.')
-        p = Permission.objects.get(
-            content_type__app_label=app_label, codename=c)
+        app_label, c = codename.split(".")
+        p = Permission.objects.get(content_type__app_label=app_label, codename=c)
         users = models.User.objects.filter(
-            Q(groups__permissions=p) | Q(user_permissions=p)).distinct()
+            Q(groups__permissions=p) | Q(user_permissions=p)
+        ).distinct()
         total = users.count()
 
-        command.stdout.write('Updating {} users with {} permission...'.format(
-            total, user_permission
-        ))
-        users.update(**{'permission_{}'.format(user_permission): True})
+        command.stdout.write(
+            "Updating {} users with {} permission...".format(total, user_permission)
+        )
+        users.update(**{"permission_{}".format(user_permission): True})
diff --git a/api/funkwhale_api/common/scripts/test.py b/api/funkwhale_api/common/scripts/test.py
index ab401dca4a7a46be53df6ffc1a09cd6657407ea8..b3a27f402a459af51c9f72cf7896c991989c991a 100644
--- a/api/funkwhale_api/common/scripts/test.py
+++ b/api/funkwhale_api/common/scripts/test.py
@@ -5,4 +5,4 @@ You can launch it just to check how it works.
 
 
 def main(command, **kwargs):
-    command.stdout.write('Test script run successfully')
+    command.stdout.write("Test script run successfully")
diff --git a/api/funkwhale_api/common/serializers.py b/api/funkwhale_api/common/serializers.py
index a995cc360eca4792d02dacd8acffdb255690d5bc..029338ef992c6a57e3d88f096d4c7619df39d63b 100644
--- a/api/funkwhale_api/common/serializers.py
+++ b/api/funkwhale_api/common/serializers.py
@@ -17,67 +17,67 @@ class ActionSerializer(serializers.Serializer):
     dangerous_actions = []
 
     def __init__(self, *args, **kwargs):
-        self.queryset = kwargs.pop('queryset')
+        self.queryset = kwargs.pop("queryset")
         if self.actions is None:
             raise ValueError(
-                'You must declare a list of actions on '
-                'the serializer class')
+                "You must declare a list of actions on " "the serializer class"
+            )
 
         for action in self.actions:
-            handler_name = 'handle_{}'.format(action)
-            assert hasattr(self, handler_name), (
-                '{} miss a {} method'.format(
-                    self.__class__.__name__, handler_name)
+            handler_name = "handle_{}".format(action)
+            assert hasattr(self, handler_name), "{} miss a {} method".format(
+                self.__class__.__name__, handler_name
             )
         super().__init__(self, *args, **kwargs)
 
     def validate_action(self, value):
         if value not in self.actions:
             raise serializers.ValidationError(
-                '{} is not a valid action. Pick one of {}.'.format(
-                    value, ', '.join(self.actions)
+                "{} is not a valid action. Pick one of {}.".format(
+                    value, ", ".join(self.actions)
                 )
             )
         return value
 
     def validate_objects(self, value):
-        qs = None
-        if value == 'all':
-            return self.queryset.all().order_by('id')
+        if value == "all":
+            return self.queryset.all().order_by("id")
         if type(value) in [list, tuple]:
-            return self.queryset.filter(pk__in=value).order_by('id')
+            return self.queryset.filter(pk__in=value).order_by("id")
 
         raise serializers.ValidationError(
-            '{} is not a valid value for objects. You must provide either a '
-            'list of identifiers or the string "all".'.format(value))
+            "{} is not a valid value for objects. You must provide either a "
+            'list of identifiers or the string "all".'.format(value)
+        )
 
     def validate(self, data):
-        dangerous = data['action'] in self.dangerous_actions
-        if dangerous and self.initial_data['objects'] == 'all':
+        dangerous = data["action"] in self.dangerous_actions
+        if dangerous and self.initial_data["objects"] == "all":
             raise serializers.ValidationError(
-                'This action is to dangerous to be applied to all objects')
-        if self.filterset_class and 'filters' in data:
+                "This action is to dangerous to be applied to all objects"
+            )
+        if self.filterset_class and "filters" in data:
             qs_filterset = self.filterset_class(
-                data['filters'], queryset=data['objects'])
+                data["filters"], queryset=data["objects"]
+            )
             try:
                 assert qs_filterset.form.is_valid()
             except (AssertionError, TypeError):
-                raise serializers.ValidationError('Invalid filters')
-            data['objects'] = qs_filterset.qs
+                raise serializers.ValidationError("Invalid filters")
+            data["objects"] = qs_filterset.qs
 
-        data['count'] = data['objects'].count()
-        if data['count'] < 1:
-            raise serializers.ValidationError(
-                'No object matching your request')
+        data["count"] = data["objects"].count()
+        if data["count"] < 1:
+            raise serializers.ValidationError("No object matching your request")
         return data
 
     def save(self):
-        handler_name = 'handle_{}'.format(self.validated_data['action'])
+        handler_name = "handle_{}".format(self.validated_data["action"])
         handler = getattr(self, handler_name)
-        result = handler(self.validated_data['objects'])
+        result = handler(self.validated_data["objects"])
         payload = {
-            'updated': self.validated_data['count'],
-            'action': self.validated_data['action'],
-            'result': result,
+            "updated": self.validated_data["count"],
+            "action": self.validated_data["action"],
+            "result": result,
         }
         return payload
diff --git a/api/funkwhale_api/common/session.py b/api/funkwhale_api/common/session.py
index 7f5584bd1cdbe465605d719d164ba468024160ed..4d5d0bb60be9c009455069bbf1b76d99f1d6eb3a 100644
--- a/api/funkwhale_api/common/session.py
+++ b/api/funkwhale_api/common/session.py
@@ -1,18 +1,16 @@
 import requests
-
 from django.conf import settings
 
 import funkwhale_api
 
 
 def get_user_agent():
-    return 'python-requests (funkwhale/{}; +{})'.format(
-        funkwhale_api.__version__,
-        settings.FUNKWHALE_URL
+    return "python-requests (funkwhale/{}; +{})".format(
+        funkwhale_api.__version__, settings.FUNKWHALE_URL
     )
 
 
 def get_session():
     s = requests.Session()
-    s.headers['User-Agent'] = get_user_agent()
+    s.headers["User-Agent"] = get_user_agent()
     return s
diff --git a/api/funkwhale_api/common/storage.py b/api/funkwhale_api/common/storage.py
index 658ce795a4bad7290b1aa8d07766207d2da5a2b3..c5651693f5c4c060fccbf318c192ff90a19908a1 100644
--- a/api/funkwhale_api/common/storage.py
+++ b/api/funkwhale_api/common/storage.py
@@ -7,6 +7,7 @@ class ASCIIFileSystemStorage(FileSystemStorage):
     """
     Convert unicode characters in name to ASCII characters.
     """
+
     def get_valid_name(self, name):
-        name = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore')
+        name = unicodedata.normalize("NFKD", name).encode("ascii", "ignore")
         return super().get_valid_name(name)
diff --git a/api/funkwhale_api/common/utils.py b/api/funkwhale_api/common/utils.py
index 2d7641bf56c077ae08384a92759008f48d06a7f4..221d2336b753322e5f40ad028c15fe157d218a00 100644
--- a/api/funkwhale_api/common/utils.py
+++ b/api/funkwhale_api/common/utils.py
@@ -1,6 +1,6 @@
-from urllib.parse import urlencode, parse_qs, urlsplit, urlunsplit
 import os
 import shutil
+from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
 
 from django.db import transaction
 
@@ -9,13 +9,13 @@ def rename_file(instance, field_name, new_name, allow_missing_file=False):
     field = getattr(instance, field_name)
     current_name, extension = os.path.splitext(field.name)
 
-    new_name_with_extension = '{}{}'.format(new_name, extension)
+    new_name_with_extension = "{}{}".format(new_name, extension)
     try:
         shutil.move(field.path, new_name_with_extension)
     except FileNotFoundError:
         if not allow_missing_file:
             raise
-        print('Skipped missing file', field.path)
+        print("Skipped missing file", field.path)
     initial_path = os.path.dirname(field.name)
     field.name = os.path.join(initial_path, new_name_with_extension)
     instance.save()
@@ -23,9 +23,7 @@ def rename_file(instance, field_name, new_name, allow_missing_file=False):
 
 
 def on_commit(f, *args, **kwargs):
-    return transaction.on_commit(
-        lambda: f(*args, **kwargs)
-    )
+    return transaction.on_commit(lambda: f(*args, **kwargs))
 
 
 def set_query_parameter(url, **kwargs):
diff --git a/api/funkwhale_api/contrib/sites/migrations/0001_initial.py b/api/funkwhale_api/contrib/sites/migrations/0001_initial.py
index cf95cec6586d758a216749f15d46053f0c05658c..8b7ec088cbdc18ab08156027fe0a2867efd0c2f5 100644
--- a/api/funkwhale_api/contrib/sites/migrations/0001_initial.py
+++ b/api/funkwhale_api/contrib/sites/migrations/0001_initial.py
@@ -7,25 +7,39 @@ import django.contrib.sites.models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-    ]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
-            name='Site',
+            name="Site",
             fields=[
-                ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
-                ('domain', models.CharField(verbose_name='domain name', max_length=100, validators=[django.contrib.sites.models._simple_domain_name_validator])),
-                ('name', models.CharField(verbose_name='display name', max_length=50)),
+                (
+                    "id",
+                    models.AutoField(
+                        verbose_name="ID",
+                        primary_key=True,
+                        serialize=False,
+                        auto_created=True,
+                    ),
+                ),
+                (
+                    "domain",
+                    models.CharField(
+                        verbose_name="domain name",
+                        max_length=100,
+                        validators=[
+                            django.contrib.sites.models._simple_domain_name_validator
+                        ],
+                    ),
+                ),
+                ("name", models.CharField(verbose_name="display name", max_length=50)),
             ],
             options={
-                'verbose_name_plural': 'sites',
-                'verbose_name': 'site',
-                'db_table': 'django_site',
-                'ordering': ('domain',),
+                "verbose_name_plural": "sites",
+                "verbose_name": "site",
+                "db_table": "django_site",
+                "ordering": ("domain",),
             },
-            managers=[
-                ('objects', django.contrib.sites.models.SiteManager()),
-            ],
-        ),
+            managers=[("objects", django.contrib.sites.models.SiteManager())],
+        )
     ]
diff --git a/api/funkwhale_api/contrib/sites/migrations/0002_set_site_domain_and_name.py b/api/funkwhale_api/contrib/sites/migrations/0002_set_site_domain_and_name.py
index e92c8c338f72c8407d4e424a88561677ca86fd9b..7b091708c4403a8e3504813cb007a6896eb1b9c0 100644
--- a/api/funkwhale_api/contrib/sites/migrations/0002_set_site_domain_and_name.py
+++ b/api/funkwhale_api/contrib/sites/migrations/0002_set_site_domain_and_name.py
@@ -10,10 +10,7 @@ def update_site_forward(apps, schema_editor):
     Site = apps.get_model("sites", "Site")
     Site.objects.update_or_create(
         id=settings.SITE_ID,
-        defaults={
-            "domain": "funkwhale.io",
-            "name": "funkwhale_api"
-        }
+        defaults={"domain": "funkwhale.io", "name": "funkwhale_api"},
     )
 
 
@@ -21,20 +18,12 @@ def update_site_backward(apps, schema_editor):
     """Revert site domain and name to default."""
     Site = apps.get_model("sites", "Site")
     Site.objects.update_or_create(
-        id=settings.SITE_ID,
-        defaults={
-            "domain": "example.com",
-            "name": "example.com"
-        }
+        id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"}
     )
 
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('sites', '0001_initial'),
-    ]
+    dependencies = [("sites", "0001_initial")]
 
-    operations = [
-        migrations.RunPython(update_site_forward, update_site_backward),
-    ]
+    operations = [migrations.RunPython(update_site_forward, update_site_backward)]
diff --git a/api/funkwhale_api/contrib/sites/migrations/0003_auto_20171214_2205.py b/api/funkwhale_api/contrib/sites/migrations/0003_auto_20171214_2205.py
index 14a9ec1a8a49ded7ec0458db9f8a9a2f6388dbe5..5a903b8d19e65c74ab8a20152b5ba16293b7230b 100644
--- a/api/funkwhale_api/contrib/sites/migrations/0003_auto_20171214_2205.py
+++ b/api/funkwhale_api/contrib/sites/migrations/0003_auto_20171214_2205.py
@@ -8,20 +8,21 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('sites', '0002_set_site_domain_and_name'),
-    ]
+    dependencies = [("sites", "0002_set_site_domain_and_name")]
 
     operations = [
         migrations.AlterModelManagers(
-            name='site',
-            managers=[
-                ('objects', django.contrib.sites.models.SiteManager()),
-            ],
+            name="site",
+            managers=[("objects", django.contrib.sites.models.SiteManager())],
         ),
         migrations.AlterField(
-            model_name='site',
-            name='domain',
-            field=models.CharField(max_length=100, unique=True, validators=[django.contrib.sites.models._simple_domain_name_validator], verbose_name='domain name'),
+            model_name="site",
+            name="domain",
+            field=models.CharField(
+                max_length=100,
+                unique=True,
+                validators=[django.contrib.sites.models._simple_domain_name_validator],
+                verbose_name="domain name",
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/downloader/__init__.py b/api/funkwhale_api/downloader/__init__.py
index 29ec89954d154f28b8bf47248ca040001dc0be60..eca15e121d9be1167e0a72c65de662189242720c 100644
--- a/api/funkwhale_api/downloader/__init__.py
+++ b/api/funkwhale_api/downloader/__init__.py
@@ -1,2 +1,3 @@
-
 from .downloader import download
+
+__all__ = ["download"]
diff --git a/api/funkwhale_api/downloader/downloader.py b/api/funkwhale_api/downloader/downloader.py
index 7fc237b089a748ccb9555da9a52e0eaf1c506a4e..f2b7568cc5e992a4407edd86100cb73c15100587 100644
--- a/api/funkwhale_api/downloader/downloader.py
+++ b/api/funkwhale_api/downloader/downloader.py
@@ -1,26 +1,19 @@
 import os
-import json
-from urllib.parse import quote_plus
+
 import youtube_dl
 from django.conf import settings
-import glob
 
 
 def download(
-        url,
-        target_directory=settings.MEDIA_ROOT,
-        name="%(id)s.%(ext)s",
-        bitrate=192):
+    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',
-        }],
+        "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'}
+    info["audio_file_path"] = target_path % {"id": info["id"], "ext": "ogg"}
     return info
diff --git a/api/funkwhale_api/factories.py b/api/funkwhale_api/factories.py
index 6fed66edb2ed000e75df819ffb862949672889a5..602037a065440bfde3fba2ff99821b2bebae1d91 100644
--- a/api/funkwhale_api/factories.py
+++ b/api/funkwhale_api/factories.py
@@ -3,7 +3,7 @@ import persisting_theory
 
 
 class FactoriesRegistry(persisting_theory.Registry):
-    look_into = 'factories'
+    look_into = "factories"
 
     def prepare_name(self, data, name=None):
         return name or data._meta.model._meta.label
diff --git a/api/funkwhale_api/favorites/activities.py b/api/funkwhale_api/favorites/activities.py
index a2dbc4e2fa69afae2b8677f166ea72d8a9118f6a..294194e061614a338137a3519aa547823a2d1cf0 100644
--- a/api/funkwhale_api/favorites/activities.py
+++ b/api/funkwhale_api/favorites/activities.py
@@ -1,19 +1,16 @@
-from funkwhale_api.common import channels
 from funkwhale_api.activity import record
+from funkwhale_api.common import channels
 
 from . import serializers
 
-record.registry.register_serializer(
-    serializers.TrackFavoriteActivitySerializer)
+record.registry.register_serializer(serializers.TrackFavoriteActivitySerializer)
 
 
-@record.registry.register_consumer('favorites.TrackFavorite')
+@record.registry.register_consumer("favorites.TrackFavorite")
 def broadcast_track_favorite_to_instance_activity(data, obj):
-    if obj.user.privacy_level not in ['instance', 'everyone']:
+    if obj.user.privacy_level not in ["instance", "everyone"]:
         return
 
-    channels.group_send('instance_activity', {
-        'type': 'event.send',
-        'text': '',
-        'data': data
-    })
+    channels.group_send(
+        "instance_activity", {"type": "event.send", "text": "", "data": data}
+    )
diff --git a/api/funkwhale_api/favorites/admin.py b/api/funkwhale_api/favorites/admin.py
index e8f29fac452f23f5ba544395dca248a41afe3cfe..f56980e8cc9b368fe2c6702588f23ca50f457e6d 100644
--- a/api/funkwhale_api/favorites/admin.py
+++ b/api/funkwhale_api/favorites/admin.py
@@ -5,8 +5,5 @@ from . import models
 
 @admin.register(models.TrackFavorite)
 class TrackFavoriteAdmin(admin.ModelAdmin):
-    list_display = ['user', 'track', 'creation_date']
-    list_select_related = [
-        'user',
-        'track'
-    ]
+    list_display = ["user", "track", "creation_date"]
+    list_select_related = ["user", "track"]
diff --git a/api/funkwhale_api/favorites/factories.py b/api/funkwhale_api/favorites/factories.py
index 233dd049c5477fdb0c83719d397f41c9f62536d4..d96ef1c15180c3c5d425585a3f2d562db015f139 100644
--- a/api/funkwhale_api/favorites/factories.py
+++ b/api/funkwhale_api/favorites/factories.py
@@ -1,7 +1,6 @@
 import factory
 
 from funkwhale_api.factories import registry
-
 from funkwhale_api.music.factories import TrackFactory
 from funkwhale_api.users.factories import UserFactory
 
@@ -12,4 +11,4 @@ class TrackFavorite(factory.django.DjangoModelFactory):
     user = factory.SubFactory(UserFactory)
 
     class Meta:
-        model = 'favorites.TrackFavorite'
+        model = "favorites.TrackFavorite"
diff --git a/api/funkwhale_api/favorites/migrations/0001_initial.py b/api/funkwhale_api/favorites/migrations/0001_initial.py
index c2bd03182d5c6791eda77c23fc593d38ef99e22a..17a66462e1d7504433129397db37b71010981384 100644
--- a/api/funkwhale_api/favorites/migrations/0001_initial.py
+++ b/api/funkwhale_api/favorites/migrations/0001_initial.py
@@ -9,25 +9,47 @@ from django.conf import settings
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('music', '0003_auto_20151222_2233'),
+        ("music", "0003_auto_20151222_2233"),
         migrations.swappable_dependency(settings.AUTH_USER_MODEL),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='TrackFavorite',
+            name="TrackFavorite",
             fields=[
-                ('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('track', models.ForeignKey(related_name='track_favorites', to='music.Track', on_delete=models.CASCADE)),
-                ('user', models.ForeignKey(related_name='track_favorites', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
+                (
+                    "id",
+                    models.AutoField(
+                        serialize=False,
+                        auto_created=True,
+                        verbose_name="ID",
+                        primary_key=True,
+                    ),
+                ),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                (
+                    "track",
+                    models.ForeignKey(
+                        related_name="track_favorites",
+                        to="music.Track",
+                        on_delete=models.CASCADE,
+                    ),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        related_name="track_favorites",
+                        to=settings.AUTH_USER_MODEL,
+                        on_delete=models.CASCADE,
+                    ),
+                ),
             ],
-            options={
-                'ordering': ('-creation_date',),
-            },
+            options={"ordering": ("-creation_date",)},
         ),
         migrations.AlterUniqueTogether(
-            name='trackfavorite',
-            unique_together=set([('track', 'user')]),
+            name="trackfavorite", unique_together=set([("track", "user")])
         ),
     ]
diff --git a/api/funkwhale_api/favorites/models.py b/api/funkwhale_api/favorites/models.py
index 0c6a6b11c6e86083c63a49dfbdf45021554e380c..a6a80cebdd0e70d8d5a67293ef4fcbcad3d0e463 100644
--- a/api/funkwhale_api/favorites/models.py
+++ b/api/funkwhale_api/favorites/models.py
@@ -1,4 +1,3 @@
-from django.conf import settings
 from django.db import models
 from django.utils import timezone
 
@@ -8,13 +7,15 @@ from funkwhale_api.music.models import Track
 class TrackFavorite(models.Model):
     creation_date = models.DateTimeField(default=timezone.now)
     user = models.ForeignKey(
-        'users.User', related_name='track_favorites', on_delete=models.CASCADE)
+        "users.User", related_name="track_favorites", on_delete=models.CASCADE
+    )
     track = models.ForeignKey(
-        Track, related_name='track_favorites', on_delete=models.CASCADE)
+        Track, related_name="track_favorites", on_delete=models.CASCADE
+    )
 
     class Meta:
-        unique_together = ('track', 'user')
-        ordering = ('-creation_date',)
+        unique_together = ("track", "user")
+        ordering = ("-creation_date",)
 
     @classmethod
     def add(cls, track, user):
@@ -22,5 +23,4 @@ class TrackFavorite(models.Model):
         return favorite
 
     def get_activity_url(self):
-        return '{}/favorites/tracks/{}'.format(
-            self.user.get_activity_url(), self.pk)
+        return "{}/favorites/tracks/{}".format(self.user.get_activity_url(), self.pk)
diff --git a/api/funkwhale_api/favorites/serializers.py b/api/funkwhale_api/favorites/serializers.py
index bb4538b2d91c709c86072f0d75d5808e98bbec57..3cafb80f021488cf0c64244a7dc1b5607d185497 100644
--- a/api/funkwhale_api/favorites/serializers.py
+++ b/api/funkwhale_api/favorites/serializers.py
@@ -1,4 +1,3 @@
-from django.conf import settings
 
 from rest_framework import serializers
 
@@ -11,29 +10,22 @@ from . import models
 
 class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer):
     type = serializers.SerializerMethodField()
-    object = TrackActivitySerializer(source='track')
-    actor = UserActivitySerializer(source='user')
-    published = serializers.DateTimeField(source='creation_date')
+    object = TrackActivitySerializer(source="track")
+    actor = UserActivitySerializer(source="user")
+    published = serializers.DateTimeField(source="creation_date")
 
     class Meta:
         model = models.TrackFavorite
-        fields = [
-            'id',
-            'local_id',
-            'object',
-            'type',
-            'actor',
-            'published'
-        ]
+        fields = ["id", "local_id", "object", "type", "actor", "published"]
 
     def get_actor(self, obj):
         return UserActivitySerializer(obj.user).data
 
     def get_type(self, obj):
-        return 'Like'
+        return "Like"
 
 
 class UserTrackFavoriteSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.TrackFavorite
-        fields = ('id', 'track', 'creation_date')
+        fields = ("id", "track", "creation_date")
diff --git a/api/funkwhale_api/favorites/urls.py b/api/funkwhale_api/favorites/urls.py
index 6a9b12a81e7f5dc912a471ad87a777e515b0d2eb..28d0c867667d24c7daa8cd090b670e2060e5d147 100644
--- a/api/funkwhale_api/favorites/urls.py
+++ b/api/funkwhale_api/favorites/urls.py
@@ -1,8 +1,8 @@
-from django.conf.urls import include, url
+from rest_framework import routers
+
 from . import views
 
-from rest_framework import routers
 router = routers.SimpleRouter()
-router.register(r'tracks', views.TrackFavoriteViewSet, 'tracks')
+router.register(r"tracks", views.TrackFavoriteViewSet, "tracks")
 
 urlpatterns = router.urls
diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py
index cd2aa3b61c1c63af7f6fb22e2c3c7c37c36f62c0..4d1c1e756af1bf5ca01345ad0fba172c316ac152 100644
--- a/api/funkwhale_api/favorites/views.py
+++ b/api/funkwhale_api/favorites/views.py
@@ -1,24 +1,23 @@
-from rest_framework import generics, mixins, viewsets
-from rest_framework import status
-from rest_framework.response import Response
-from rest_framework import pagination
+from rest_framework import mixins, status, viewsets
 from rest_framework.decorators import list_route
+from rest_framework.response import Response
 
 from funkwhale_api.activity import record
-from funkwhale_api.music.models import Track
 from funkwhale_api.common.permissions import ConditionalAuthentication
+from funkwhale_api.music.models import Track
 
-from . import models
-from . import serializers
+from . import models, serializers
 
 
-class TrackFavoriteViewSet(mixins.CreateModelMixin,
-                           mixins.DestroyModelMixin,
-                           mixins.ListModelMixin,
-                           viewsets.GenericViewSet):
+class TrackFavoriteViewSet(
+    mixins.CreateModelMixin,
+    mixins.DestroyModelMixin,
+    mixins.ListModelMixin,
+    viewsets.GenericViewSet,
+):
 
     serializer_class = serializers.UserTrackFavoriteSerializer
-    queryset = (models.TrackFavorite.objects.all())
+    queryset = models.TrackFavorite.objects.all()
     permission_classes = [ConditionalAuthentication]
 
     def create(self, request, *args, **kwargs):
@@ -28,20 +27,22 @@ class TrackFavoriteViewSet(mixins.CreateModelMixin,
         serializer = self.get_serializer(instance=instance)
         headers = self.get_success_headers(serializer.data)
         record.send(instance)
-        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+        return Response(
+            serializer.data, status=status.HTTP_201_CREATED, headers=headers
+        )
 
     def get_queryset(self):
         return self.queryset.filter(user=self.request.user)
 
     def perform_create(self, serializer):
-        track = Track.objects.get(pk=serializer.data['track'])
+        track = Track.objects.get(pk=serializer.data["track"])
         favorite = models.TrackFavorite.add(track=track, user=self.request.user)
         return favorite
 
-    @list_route(methods=['delete', 'post'])
+    @list_route(methods=["delete", "post"])
     def remove(self, request, *args, **kwargs):
         try:
-            pk = int(request.data['track'])
+            pk = int(request.data["track"])
             favorite = request.user.track_favorites.get(track__pk=pk)
         except (AttributeError, ValueError, models.TrackFavorite.DoesNotExist):
             return Response({}, status=400)
diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py
index becf6c96f55b68d2503eb7442f673e0fadb4cef7..73e83e334534bd211daa90a684a1441b5a10907b 100644
--- a/api/funkwhale_api/federation/activity.py
+++ b/api/funkwhale_api/federation/activity.py
@@ -1,67 +1,61 @@
-from . import serializers
-from . import tasks
-
 ACTIVITY_TYPES = [
-    'Accept',
-    'Add',
-    'Announce',
-    'Arrive',
-    'Block',
-    'Create',
-    'Delete',
-    'Dislike',
-    'Flag',
-    'Follow',
-    'Ignore',
-    'Invite',
-    'Join',
-    'Leave',
-    'Like',
-    'Listen',
-    'Move',
-    'Offer',
-    'Question',
-    'Reject',
-    'Read',
-    'Remove',
-    'TentativeReject',
-    'TentativeAccept',
-    'Travel',
-    'Undo',
-    'Update',
-    'View',
+    "Accept",
+    "Add",
+    "Announce",
+    "Arrive",
+    "Block",
+    "Create",
+    "Delete",
+    "Dislike",
+    "Flag",
+    "Follow",
+    "Ignore",
+    "Invite",
+    "Join",
+    "Leave",
+    "Like",
+    "Listen",
+    "Move",
+    "Offer",
+    "Question",
+    "Reject",
+    "Read",
+    "Remove",
+    "TentativeReject",
+    "TentativeAccept",
+    "Travel",
+    "Undo",
+    "Update",
+    "View",
 ]
 
 
 OBJECT_TYPES = [
-    'Article',
-    'Audio',
-    'Collection',
-    'Document',
-    'Event',
-    'Image',
-    'Note',
-    'OrderedCollection',
-    'Page',
-    'Place',
-    'Profile',
-    'Relationship',
-    'Tombstone',
-    'Video',
+    "Article",
+    "Audio",
+    "Collection",
+    "Document",
+    "Event",
+    "Image",
+    "Note",
+    "OrderedCollection",
+    "Page",
+    "Place",
+    "Profile",
+    "Relationship",
+    "Tombstone",
+    "Video",
 ] + ACTIVITY_TYPES
 
 
 def deliver(activity, on_behalf_of, to=[]):
-    return tasks.send.delay(
-        activity=activity,
-        actor_id=on_behalf_of.pk,
-        to=to
-    )
+    from . import tasks
+
+    return tasks.send.delay(activity=activity, actor_id=on_behalf_of.pk, to=to)
 
 
 def accept_follow(follow):
+    from . import serializers
+
     serializer = serializers.AcceptFollowSerializer(follow)
-    return deliver(
-        serializer.data,
-        to=[follow.actor.url],
-        on_behalf_of=follow.target)
+    return deliver(serializer.data, to=[follow.actor.url], on_behalf_of=follow.target)
diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py
index 7a209b1ff4cca48028a094685bff8eb258652f89..7fbf815dc50e73bb2c15c577e418650650b5dc68 100644
--- a/api/funkwhale_api/federation/actors.py
+++ b/api/funkwhale_api/federation/actors.py
@@ -1,36 +1,28 @@
 import datetime
 import logging
-import uuid
 import xml
 
 from django.conf import settings
 from django.db import transaction
 from django.urls import reverse
 from django.utils import timezone
-
 from rest_framework.exceptions import PermissionDenied
 
-from dynamic_preferences.registries import global_preferences_registry
-
-from funkwhale_api.common import preferences
-from funkwhale_api.common import session
+from funkwhale_api.common import preferences, session
 from funkwhale_api.common import utils as funkwhale_utils
 from funkwhale_api.music import models as music_models
 from funkwhale_api.music import tasks as music_tasks
 
-from . import activity
-from . import keys
-from . import models
-from . import serializers
-from . import signing
-from . import utils
+from . import activity, keys, models, serializers, signing, utils
 
 logger = logging.getLogger(__name__)
 
 
 def remove_tags(text):
-    logger.debug('Removing tags from %s', text)
-    return ''.join(xml.etree.ElementTree.fromstring('<div>{}</div>'.format(text)).itertext())
+    logger.debug("Removing tags from %s", text)
+    return "".join(
+        xml.etree.ElementTree.fromstring("<div>{}</div>".format(text)).itertext()
+    )
 
 
 def get_actor_data(actor_url):
@@ -38,16 +30,13 @@ def get_actor_data(actor_url):
         actor_url,
         timeout=5,
         verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
-        headers={
-            'Accept': 'application/activity+json',
-        }
+        headers={"Accept": "application/activity+json"},
     )
     response.raise_for_status()
     try:
         return response.json()
-    except:
-        raise ValueError(
-            'Invalid actor payload: {}'.format(response.text))
+    except Exception:
+        raise ValueError("Invalid actor payload: {}".format(response.text))
 
 
 def get_actor(actor_url):
@@ -56,7 +45,8 @@ def get_actor(actor_url):
     except models.Actor.DoesNotExist:
         actor = None
     fetch_delta = datetime.timedelta(
-        minutes=preferences.get('federation__actor_fetch_delay'))
+        minutes=preferences.get("federation__actor_fetch_delay")
+    )
     if actor and actor.last_fetch_date > timezone.now() - fetch_delta:
         # cache is hot, we can return as is
         return actor
@@ -73,8 +63,7 @@ class SystemActor(object):
 
     def get_request_auth(self):
         actor = self.get_actor_instance()
-        return signing.get_auth(
-            actor.private_key, actor.private_key_id)
+        return signing.get_auth(actor.private_key, actor.private_key_id)
 
     def serialize(self):
         actor = self.get_actor_instance()
@@ -88,42 +77,35 @@ class SystemActor(object):
             pass
         private, public = keys.get_key_pair()
         args = self.get_instance_argument(
-            self.id,
-            name=self.name,
-            summary=self.summary,
-            **self.additional_attributes
+            self.id, name=self.name, summary=self.summary, **self.additional_attributes
         )
-        args['private_key'] = private.decode('utf-8')
-        args['public_key'] = public.decode('utf-8')
+        args["private_key"] = private.decode("utf-8")
+        args["public_key"] = public.decode("utf-8")
         return models.Actor.objects.create(**args)
 
     def get_actor_url(self):
         return utils.full_url(
-            reverse(
-                'federation:instance-actors-detail',
-                kwargs={'actor': self.id}))
+            reverse("federation:instance-actors-detail", kwargs={"actor": self.id})
+        )
 
     def get_instance_argument(self, id, name, summary, **kwargs):
         p = {
-            'preferred_username': id,
-            'domain': settings.FEDERATION_HOSTNAME,
-            'type': 'Person',
-            'name': name.format(host=settings.FEDERATION_HOSTNAME),
-            'manually_approves_followers': True,
-            'url': self.get_actor_url(),
-            'shared_inbox_url': utils.full_url(
-                reverse(
-                    'federation:instance-actors-inbox',
-                    kwargs={'actor': id})),
-            'inbox_url': utils.full_url(
-                reverse(
-                    'federation:instance-actors-inbox',
-                    kwargs={'actor': id})),
-            'outbox_url': utils.full_url(
-                reverse(
-                    'federation:instance-actors-outbox',
-                    kwargs={'actor': id})),
-            'summary': summary.format(host=settings.FEDERATION_HOSTNAME)
+            "preferred_username": id,
+            "domain": settings.FEDERATION_HOSTNAME,
+            "type": "Person",
+            "name": name.format(host=settings.FEDERATION_HOSTNAME),
+            "manually_approves_followers": True,
+            "url": self.get_actor_url(),
+            "shared_inbox_url": utils.full_url(
+                reverse("federation:instance-actors-inbox", kwargs={"actor": id})
+            ),
+            "inbox_url": utils.full_url(
+                reverse("federation:instance-actors-inbox", kwargs={"actor": id})
+            ),
+            "outbox_url": utils.full_url(
+                reverse("federation:instance-actors-outbox", kwargs={"actor": id})
+            ),
+            "summary": summary.format(host=settings.FEDERATION_HOSTNAME),
         }
         p.update(kwargs)
         return p
@@ -145,32 +127,29 @@ class SystemActor(object):
         Main entrypoint for handling activities posted to the
         actor's inbox
         """
-        logger.info('Received activity on %s inbox', self.id)
+        logger.info("Received activity on %s inbox", self.id)
 
         if actor is None:
-            raise PermissionDenied('Actor not authenticated')
+            raise PermissionDenied("Actor not authenticated")
 
-        serializer = serializers.ActivitySerializer(
-            data=data, context={'actor': actor})
+        serializer = serializers.ActivitySerializer(data=data, context={"actor": actor})
         serializer.is_valid(raise_exception=True)
 
         ac = serializer.data
         try:
-            handler = getattr(
-                self, 'handle_{}'.format(ac['type'].lower()))
+            handler = getattr(self, "handle_{}".format(ac["type"].lower()))
         except (KeyError, AttributeError):
-            logger.debug(
-                'No handler for activity %s', ac['type'])
+            logger.debug("No handler for activity %s", ac["type"])
             return
 
         return handler(data, actor)
 
     def handle_follow(self, ac, sender):
-        system_actor = self.get_actor_instance()
         serializer = serializers.FollowSerializer(
-            data=ac, context={'follow_actor': sender})
+            data=ac, context={"follow_actor": sender}
+        )
         if not serializer.is_valid():
-            return logger.info('Invalid follow payload')
+            return logger.info("Invalid follow payload")
         approved = True if not self.manually_approves_followers else None
         follow = serializer.save(approved=approved)
         if follow.approved:
@@ -179,26 +158,27 @@ class SystemActor(object):
     def handle_accept(self, ac, sender):
         system_actor = self.get_actor_instance()
         serializer = serializers.AcceptFollowSerializer(
-            data=ac,
-            context={'follow_target': sender, 'follow_actor': system_actor})
+            data=ac, context={"follow_target": sender, "follow_actor": system_actor}
+        )
         if not serializer.is_valid(raise_exception=True):
-            return logger.info('Received invalid payload')
+            return logger.info("Received invalid payload")
 
         return serializer.save()
 
     def handle_undo_follow(self, ac, sender):
         system_actor = self.get_actor_instance()
         serializer = serializers.UndoFollowSerializer(
-            data=ac, context={'actor': sender, 'target': system_actor})
+            data=ac, context={"actor": sender, "target": system_actor}
+        )
         if not serializer.is_valid():
-            return logger.info('Received invalid payload')
+            return logger.info("Received invalid payload")
         serializer.save()
 
     def handle_undo(self, ac, sender):
-        if ac['object']['type'] != 'Follow':
+        if ac["object"]["type"] != "Follow":
             return
 
-        if ac['object']['actor'] != sender.url:
+        if ac["object"]["actor"] != sender.url:
             # not the same actor, permission issue
             return
 
@@ -206,55 +186,52 @@ class SystemActor(object):
 
 
 class LibraryActor(SystemActor):
-    id = 'library'
-    name = '{host}\'s library'
-    summary = 'Bot account to federate with {host}\'s library'
-    additional_attributes = {
-        'manually_approves_followers': True
-    }
+    id = "library"
+    name = "{host}'s library"
+    summary = "Bot account to federate with {host}'s library"
+    additional_attributes = {"manually_approves_followers": True}
 
     def serialize(self):
         data = super().serialize()
-        urls = data.setdefault('url', [])
-        urls.append({
-            'type': 'Link',
-            'mediaType': 'application/activity+json',
-            'name': 'library',
-            'href': utils.full_url(reverse('federation:music:files-list'))
-        })
+        urls = data.setdefault("url", [])
+        urls.append(
+            {
+                "type": "Link",
+                "mediaType": "application/activity+json",
+                "name": "library",
+                "href": utils.full_url(reverse("federation:music:files-list")),
+            }
+        )
         return data
 
     @property
     def manually_approves_followers(self):
-        return preferences.get('federation__music_needs_approval')
+        return preferences.get("federation__music_needs_approval")
 
     @transaction.atomic
     def handle_create(self, ac, sender):
         try:
             remote_library = models.Library.objects.get(
-                actor=sender,
-                federation_enabled=True,
+                actor=sender, federation_enabled=True
             )
         except models.Library.DoesNotExist:
-            logger.info(
-                'Skipping import, we\'re not following %s', sender.url)
+            logger.info("Skipping import, we're not following %s", sender.url)
             return
 
-        if ac['object']['type'] != 'Collection':
+        if ac["object"]["type"] != "Collection":
             return
 
-        if ac['object']['totalItems'] <= 0:
+        if ac["object"]["totalItems"] <= 0:
             return
 
         try:
-            items = ac['object']['items']
+            items = ac["object"]["items"]
         except KeyError:
-            logger.warning('No items in collection!')
+            logger.warning("No items in collection!")
             return
 
         item_serializers = [
-            serializers.AudioSerializer(
-                data=i, context={'library': remote_library})
+            serializers.AudioSerializer(data=i, context={"library": remote_library})
             for i in items
         ]
         now = timezone.now()
@@ -263,27 +240,21 @@ class LibraryActor(SystemActor):
             if s.is_valid():
                 valid_serializers.append(s)
             else:
-                logger.debug(
-                    'Skipping invalid item %s, %s', s.initial_data, s.errors)
+                logger.debug("Skipping invalid item %s, %s", s.initial_data, s.errors)
 
         lts = []
         for s in valid_serializers:
             lts.append(s.save())
 
         if remote_library.autoimport:
-            batch = music_models.ImportBatch.objects.create(
-                source='federation',
-            )
+            batch = music_models.ImportBatch.objects.create(source="federation")
             for lt in lts:
                 if lt.creation_date < now:
                     # track was already in the library, we do not trigger
                     # an import
                     continue
                 job = music_models.ImportJob.objects.create(
-                    batch=batch,
-                    library_track=lt,
-                    mbid=lt.mbid,
-                    source=lt.url,
+                    batch=batch, library_track=lt, mbid=lt.mbid, source=lt.url
                 )
                 funkwhale_utils.on_commit(
                     music_tasks.import_job_run.delay,
@@ -293,15 +264,13 @@ class LibraryActor(SystemActor):
 
 
 class TestActor(SystemActor):
-    id = 'test'
-    name = '{host}\'s test account'
+    id = "test"
+    name = "{host}'s test account"
     summary = (
-        'Bot account to test federation with {host}. '
-        'Send me /ping and I\'ll answer you.'
+        "Bot account to test federation with {host}. "
+        "Send me /ping and I'll answer you."
     )
-    additional_attributes = {
-        'manually_approves_followers': False
-    }
+    additional_attributes = {"manually_approves_followers": False}
     manually_approves_followers = False
 
     def get_outbox(self, data, actor=None):
@@ -309,15 +278,14 @@ class TestActor(SystemActor):
             "@context": [
                 "https://www.w3.org/ns/activitystreams",
                 "https://w3id.org/security/v1",
-                {}
+                {},
             ],
             "id": utils.full_url(
-                reverse(
-                    'federation:instance-actors-outbox',
-                    kwargs={'actor': self.id})),
+                reverse("federation:instance-actors-outbox", kwargs={"actor": self.id})
+            ),
             "type": "OrderedCollection",
             "totalItems": 0,
-            "orderedItems": []
+            "orderedItems": [],
         }
 
     def parse_command(self, message):
@@ -327,99 +295,85 @@ class TestActor(SystemActor):
         """
         raw = remove_tags(message)
         try:
-            return raw.split('/')[1]
+            return raw.split("/")[1]
         except IndexError:
             return
 
     def handle_create(self, ac, sender):
-        if ac['object']['type'] != 'Note':
+        if ac["object"]["type"] != "Note":
             return
 
         # we received a toot \o/
-        command = self.parse_command(ac['object']['content'])
-        logger.debug('Parsed command: %s', command)
-        if command != 'ping':
+        command = self.parse_command(ac["object"]["content"])
+        logger.debug("Parsed command: %s", command)
+        if command != "ping":
             return
 
         now = timezone.now()
         test_actor = self.get_actor_instance()
-        reply_url = 'https://{}/activities/note/{}'.format(
+        reply_url = "https://{}/activities/note/{}".format(
             settings.FEDERATION_HOSTNAME, now.timestamp()
         )
-        reply_content = '{} Pong!'.format(
-            sender.mention_username
-        )
         reply_activity = {
             "@context": [
                 "https://www.w3.org/ns/activitystreams",
                 "https://w3id.org/security/v1",
-                {}
+                {},
             ],
-            'type': 'Create',
-            'actor': test_actor.url,
-            'id': '{}/activity'.format(reply_url),
-            'published': now.isoformat(),
-            'to': ac['actor'],
-            'cc': [],
-            'object': {
-                'type': 'Note',
-                'content': 'Pong!',
-                'summary': None,
-                'published': now.isoformat(),
-                'id': reply_url,
-                'inReplyTo': ac['object']['id'],
-                'sensitive': False,
-                'url': reply_url,
-                'to': [ac['actor']],
-                'attributedTo': test_actor.url,
-                'cc': [],
-                'attachment': [],
-                'tag': [{
-                    "type": "Mention",
-                    "href": ac['actor'],
-                    "name": sender.mention_username
-                }]
-            }
+            "type": "Create",
+            "actor": test_actor.url,
+            "id": "{}/activity".format(reply_url),
+            "published": now.isoformat(),
+            "to": ac["actor"],
+            "cc": [],
+            "object": {
+                "type": "Note",
+                "content": "Pong!",
+                "summary": None,
+                "published": now.isoformat(),
+                "id": reply_url,
+                "inReplyTo": ac["object"]["id"],
+                "sensitive": False,
+                "url": reply_url,
+                "to": [ac["actor"]],
+                "attributedTo": test_actor.url,
+                "cc": [],
+                "attachment": [],
+                "tag": [
+                    {
+                        "type": "Mention",
+                        "href": ac["actor"],
+                        "name": sender.mention_username,
+                    }
+                ],
+            },
         }
-        activity.deliver(
-            reply_activity,
-            to=[ac['actor']],
-            on_behalf_of=test_actor)
+        activity.deliver(reply_activity, to=[ac["actor"]], on_behalf_of=test_actor)
 
     def handle_follow(self, ac, sender):
         super().handle_follow(ac, sender)
         # also, we follow back
         test_actor = self.get_actor_instance()
         follow_back = models.Follow.objects.get_or_create(
-            actor=test_actor,
-            target=sender,
-            approved=None,
+            actor=test_actor, target=sender, approved=None
         )[0]
         activity.deliver(
             serializers.FollowSerializer(follow_back).data,
             to=[follow_back.target.url],
-            on_behalf_of=follow_back.actor)
+            on_behalf_of=follow_back.actor,
+        )
 
     def handle_undo_follow(self, ac, sender):
         super().handle_undo_follow(ac, sender)
         actor = self.get_actor_instance()
         # we also unfollow the sender, if possible
         try:
-            follow = models.Follow.objects.get(
-                target=sender,
-                actor=actor,
-            )
+            follow = models.Follow.objects.get(target=sender, actor=actor)
         except models.Follow.DoesNotExist:
             return
         undo = serializers.UndoFollowSerializer(follow).data
         follow.delete()
-        activity.deliver(
-            undo,
-            to=[sender.url],
-            on_behalf_of=actor)
+        activity.deliver(undo, to=[sender.url], on_behalf_of=actor)
 
 
-SYSTEM_ACTORS = {
-    'library': LibraryActor(),
-    'test': TestActor(),
-}
+SYSTEM_ACTORS = {"library": LibraryActor(), "test": TestActor()}
diff --git a/api/funkwhale_api/federation/admin.py b/api/funkwhale_api/federation/admin.py
index 6a097174b552658e0f9c1d5075932300f23e3bdd..a82e9aaf24086e05e3dbb0f3d1fb4ee7e9dd4b6a 100644
--- a/api/funkwhale_api/federation/admin.py
+++ b/api/funkwhale_api/federation/admin.py
@@ -6,61 +6,43 @@ from . import models
 @admin.register(models.Actor)
 class ActorAdmin(admin.ModelAdmin):
     list_display = [
-        'url',
-        'domain',
-        'preferred_username',
-        'type',
-        'creation_date',
-        'last_fetch_date']
-    search_fields = ['url', 'domain', 'preferred_username']
-    list_filter = [
-        'type'
+        "url",
+        "domain",
+        "preferred_username",
+        "type",
+        "creation_date",
+        "last_fetch_date",
     ]
+    search_fields = ["url", "domain", "preferred_username"]
+    list_filter = ["type"]
 
 
 @admin.register(models.Follow)
 class FollowAdmin(admin.ModelAdmin):
-    list_display = [
-        'actor',
-        'target',
-        'approved',
-        'creation_date'
-    ]
-    list_filter = [
-        'approved'
-    ]
-    search_fields = ['actor__url', 'target__url']
+    list_display = ["actor", "target", "approved", "creation_date"]
+    list_filter = ["approved"]
+    search_fields = ["actor__url", "target__url"]
     list_select_related = True
 
 
 @admin.register(models.Library)
 class LibraryAdmin(admin.ModelAdmin):
-    list_display = [
-        'actor',
-        'url',
-        'creation_date',
-        'fetched_date',
-        'tracks_count']
-    search_fields = ['actor__url', 'url']
-    list_filter = [
-        'federation_enabled',
-        'download_files',
-        'autoimport',
-    ]
+    list_display = ["actor", "url", "creation_date", "fetched_date", "tracks_count"]
+    search_fields = ["actor__url", "url"]
+    list_filter = ["federation_enabled", "download_files", "autoimport"]
     list_select_related = True
 
 
 @admin.register(models.LibraryTrack)
 class LibraryTrackAdmin(admin.ModelAdmin):
     list_display = [
-        'title',
-        'artist_name',
-        'album_title',
-        'url',
-        'library',
-        'creation_date',
-        'published_date',
+        "title",
+        "artist_name",
+        "album_title",
+        "url",
+        "library",
+        "creation_date",
+        "published_date",
     ]
-    search_fields = [
-        'library__url', 'url', 'artist_name', 'title', 'album_title']
+    search_fields = ["library__url", "url", "artist_name", "title", "album_title"]
     list_select_related = True
diff --git a/api/funkwhale_api/federation/api_urls.py b/api/funkwhale_api/federation/api_urls.py
index 41dd1c0f99e44c4602f3592f0aca292f212b4876..625043bf6cb89598d867661512fd580814b1fd82 100644
--- a/api/funkwhale_api/federation/api_urls.py
+++ b/api/funkwhale_api/federation/api_urls.py
@@ -3,13 +3,7 @@ from rest_framework import routers
 from . import views
 
 router = routers.SimpleRouter()
-router.register(
-    r'libraries',
-    views.LibraryViewSet,
-    'libraries')
-router.register(
-    r'library-tracks',
-    views.LibraryTrackViewSet,
-    'library-tracks')
+router.register(r"libraries", views.LibraryViewSet, "libraries")
+router.register(r"library-tracks", views.LibraryTrackViewSet, "library-tracks")
 
 urlpatterns = router.urls
diff --git a/api/funkwhale_api/federation/authentication.py b/api/funkwhale_api/federation/authentication.py
index bfd46084c0dbd2e7d4b8fd3dac67a837809586bf..f32c78ff30f00079d822bbf2921d2c4845fff5fc 100644
--- a/api/funkwhale_api/federation/authentication.py
+++ b/api/funkwhale_api/federation/authentication.py
@@ -1,23 +1,15 @@
 import cryptography
-
 from django.contrib.auth.models import AnonymousUser
+from rest_framework import authentication, exceptions
 
-from rest_framework import authentication
-from rest_framework import exceptions
-
-from . import actors
-from . import keys
-from . import models
-from . import serializers
-from . import signing
-from . import utils
+from . import actors, keys, signing, utils
 
 
 class SignatureAuthentication(authentication.BaseAuthentication):
     def authenticate_actor(self, request):
         headers = utils.clean_wsgi_headers(request.META)
         try:
-            signature = headers['Signature']
+            signature = headers["Signature"]
             key_id = keys.get_key_id_from_signature_header(signature)
         except KeyError:
             return
@@ -25,25 +17,25 @@ class SignatureAuthentication(authentication.BaseAuthentication):
             raise exceptions.AuthenticationFailed(str(e))
 
         try:
-            actor = actors.get_actor(key_id.split('#')[0])
+            actor = actors.get_actor(key_id.split("#")[0])
         except Exception as e:
             raise exceptions.AuthenticationFailed(str(e))
 
         if not actor.public_key:
-            raise exceptions.AuthenticationFailed('No public key found')
+            raise exceptions.AuthenticationFailed("No public key found")
 
         try:
-            signing.verify_django(request, actor.public_key.encode('utf-8'))
+            signing.verify_django(request, actor.public_key.encode("utf-8"))
         except cryptography.exceptions.InvalidSignature:
-            raise exceptions.AuthenticationFailed('Invalid signature')
+            raise exceptions.AuthenticationFailed("Invalid signature")
 
         return actor
 
     def authenticate(self, request):
-        setattr(request, 'actor', None)
+        setattr(request, "actor", None)
         actor = self.authenticate_actor(request)
         if not actor:
             return
         user = AnonymousUser()
-        setattr(request, 'actor', actor)
+        setattr(request, "actor", actor)
         return (user, None)
diff --git a/api/funkwhale_api/federation/dynamic_preferences_registry.py b/api/funkwhale_api/federation/dynamic_preferences_registry.py
index 8b1b2b03f9eb00f0eac1d467c099baf66d72d67d..5119d2596fab750b6312d5afbdc3c6d2447b80d0 100644
--- a/api/funkwhale_api/federation/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/federation/dynamic_preferences_registry.py
@@ -1,80 +1,68 @@
-from django.forms import widgets
 
 from dynamic_preferences import types
 from dynamic_preferences.registries import global_preferences_registry
 
 from funkwhale_api.common import preferences
-federation = types.Section('federation')
+
+federation = types.Section("federation")
 
 
 @global_preferences_registry.register
 class MusicCacheDuration(types.IntPreference):
     show_in_api = True
     section = federation
-    name = 'music_cache_duration'
+    name = "music_cache_duration"
     default = 60 * 24 * 2
-    verbose_name = 'Music cache duration'
+    verbose_name = "Music cache duration"
     help_text = (
-        'How much minutes do you want to keep a copy of federated tracks'
-        'locally? Federated files that were not listened in this interval '
-        'will be erased and refetched from the remote on the next listening.'
+        "How much minutes do you want to keep a copy of federated tracks"
+        "locally? Federated files that were not listened in this interval "
+        "will be erased and refetched from the remote on the next listening."
     )
-    field_kwargs = {
-        'required': False,
-    }
+    field_kwargs = {"required": False}
 
 
 @global_preferences_registry.register
 class Enabled(preferences.DefaultFromSettingMixin, types.BooleanPreference):
     section = federation
-    name = 'enabled'
-    setting = 'FEDERATION_ENABLED'
-    verbose_name = 'Federation enabled'
+    name = "enabled"
+    setting = "FEDERATION_ENABLED"
+    verbose_name = "Federation enabled"
     help_text = (
-        'Use this setting to enable or disable federation logic and API'
-        ' globally.'
+        "Use this setting to enable or disable federation logic and API" " globally."
     )
 
 
 @global_preferences_registry.register
-class CollectionPageSize(
-        preferences.DefaultFromSettingMixin, types.IntPreference):
+class CollectionPageSize(preferences.DefaultFromSettingMixin, types.IntPreference):
     section = federation
-    name = 'collection_page_size'
-    setting = 'FEDERATION_COLLECTION_PAGE_SIZE'
-    verbose_name = 'Federation collection page size'
-    help_text = (
-        'How much items to display in ActivityPub collections.'
-    )
-    field_kwargs = {
-        'required': False,
-    }
+    name = "collection_page_size"
+    setting = "FEDERATION_COLLECTION_PAGE_SIZE"
+    verbose_name = "Federation collection page size"
+    help_text = "How much items to display in ActivityPub collections."
+    field_kwargs = {"required": False}
 
 
 @global_preferences_registry.register
-class ActorFetchDelay(
-        preferences.DefaultFromSettingMixin, types.IntPreference):
+class ActorFetchDelay(preferences.DefaultFromSettingMixin, types.IntPreference):
     section = federation
-    name = 'actor_fetch_delay'
-    setting = 'FEDERATION_ACTOR_FETCH_DELAY'
-    verbose_name = 'Federation actor fetch delay'
+    name = "actor_fetch_delay"
+    setting = "FEDERATION_ACTOR_FETCH_DELAY"
+    verbose_name = "Federation actor fetch delay"
     help_text = (
-        'How much minutes to wait before refetching actors on '
-        'request authentication.'
+        "How much minutes to wait before refetching actors on "
+        "request authentication."
     )
-    field_kwargs = {
-        'required': False,
-    }
+    field_kwargs = {"required": False}
 
 
 @global_preferences_registry.register
-class MusicNeedsApproval(
-        preferences.DefaultFromSettingMixin, types.BooleanPreference):
+class MusicNeedsApproval(preferences.DefaultFromSettingMixin, types.BooleanPreference):
     section = federation
-    name = 'music_needs_approval'
-    setting = 'FEDERATION_MUSIC_NEEDS_APPROVAL'
-    verbose_name = 'Federation music needs approval'
+    name = "music_needs_approval"
+    setting = "FEDERATION_MUSIC_NEEDS_APPROVAL"
+    verbose_name = "Federation music needs approval"
     help_text = (
-        'When true, other federation actors will need your approval'
-        ' before being able to browse your library.'
+        "When true, other federation actors will need your approval"
+        " before being able to browse your library."
     )
diff --git a/api/funkwhale_api/federation/exceptions.py b/api/funkwhale_api/federation/exceptions.py
index 31d864b36c065aacadd8b09ed02278e13bd46fcb..b3fb73ab8ee549d92250c48fd8bffff2667ee15e 100644
--- a/api/funkwhale_api/federation/exceptions.py
+++ b/api/funkwhale_api/federation/exceptions.py
@@ -1,5 +1,3 @@
-
-
 class MalformedPayload(ValueError):
     pass
 
diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py
index 891609cbaa30117bbe0eccf802405a89c56b4914..7370ebd77d73694178a326ede77a2d0269ed16e6 100644
--- a/api/funkwhale_api/federation/factories.py
+++ b/api/funkwhale_api/federation/factories.py
@@ -1,40 +1,34 @@
+import uuid
+
 import factory
 import requests
 import requests_http_signature
-import uuid
-
-from django.utils import timezone
 from django.conf import settings
+from django.utils import timezone
 
 from funkwhale_api.factories import registry
 
-from . import keys
-from . import models
+from . import keys, models
 
+registry.register(keys.get_key_pair, name="federation.KeyPair")
 
-registry.register(keys.get_key_pair, name='federation.KeyPair')
 
-
-@registry.register(name='federation.SignatureAuth')
+@registry.register(name="federation.SignatureAuth")
 class SignatureAuthFactory(factory.Factory):
-    algorithm = 'rsa-sha256'
+    algorithm = "rsa-sha256"
     key = factory.LazyFunction(lambda: keys.get_key_pair()[0])
-    key_id = factory.Faker('url')
+    key_id = factory.Faker("url")
     use_auth_header = False
-    headers = [
-        '(request-target)',
-        'user-agent',
-        'host',
-        'date',
-        'content-type',]
+    headers = ["(request-target)", "user-agent", "host", "date", "content-type"]
+
     class Meta:
         model = requests_http_signature.HTTPSignatureAuth
 
 
-@registry.register(name='federation.SignedRequest')
+@registry.register(name="federation.SignedRequest")
 class SignedRequestFactory(factory.Factory):
-    url = factory.Faker('url')
-    method = 'get'
+    url = factory.Faker("url")
+    method = "get"
     auth = factory.SubFactory(SignatureAuthFactory)
 
     class Meta:
@@ -43,59 +37,62 @@ class SignedRequestFactory(factory.Factory):
     @factory.post_generation
     def headers(self, create, extracted, **kwargs):
         default_headers = {
-            'User-Agent': 'Test',
-            'Host': 'test.host',
-            'Date': 'Right now',
-            'Content-Type': 'application/activity+json'
+            "User-Agent": "Test",
+            "Host": "test.host",
+            "Date": "Right now",
+            "Content-Type": "application/activity+json",
         }
         if extracted:
             default_headers.update(extracted)
         self.headers.update(default_headers)
 
 
-@registry.register(name='federation.Link')
+@registry.register(name="federation.Link")
 class LinkFactory(factory.Factory):
-    type = 'Link'
-    href = factory.Faker('url')
-    mediaType = 'text/html'
+    type = "Link"
+    href = factory.Faker("url")
+    mediaType = "text/html"
 
     class Meta:
         model = dict
 
     class Params:
-        audio = factory.Trait(
-            mediaType=factory.Iterator(['audio/mp3', 'audio/ogg'])
-        )
+        audio = factory.Trait(mediaType=factory.Iterator(["audio/mp3", "audio/ogg"]))
 
 
 @registry.register
 class ActorFactory(factory.DjangoModelFactory):
     public_key = None
     private_key = None
-    preferred_username = factory.Faker('user_name')
-    summary = factory.Faker('paragraph')
-    domain = factory.Faker('domain_name')
-    url = factory.LazyAttribute(lambda o: 'https://{}/users/{}'.format(o.domain, o.preferred_username))
-    inbox_url = factory.LazyAttribute(lambda o: 'https://{}/users/{}/inbox'.format(o.domain, o.preferred_username))
-    outbox_url = factory.LazyAttribute(lambda o: 'https://{}/users/{}/outbox'.format(o.domain, o.preferred_username))
+    preferred_username = factory.Faker("user_name")
+    summary = factory.Faker("paragraph")
+    domain = factory.Faker("domain_name")
+    url = factory.LazyAttribute(
+        lambda o: "https://{}/users/{}".format(o.domain, o.preferred_username)
+    )
+    inbox_url = factory.LazyAttribute(
+        lambda o: "https://{}/users/{}/inbox".format(o.domain, o.preferred_username)
+    )
+    outbox_url = factory.LazyAttribute(
+        lambda o: "https://{}/users/{}/outbox".format(o.domain, o.preferred_username)
+    )
 
     class Meta:
         model = models.Actor
 
     class Params:
         local = factory.Trait(
-            domain=factory.LazyAttribute(
-                lambda o: settings.FEDERATION_HOSTNAME)
+            domain=factory.LazyAttribute(lambda o: settings.FEDERATION_HOSTNAME)
         )
 
     @classmethod
     def _generate(cls, create, attrs):
-        has_public = attrs.get('public_key') is not None
-        has_private = attrs.get('private_key') is not None
+        has_public = attrs.get("public_key") is not None
+        has_private = attrs.get("private_key") is not None
         if not has_public and not has_private:
             private, public = keys.get_key_pair()
-            attrs['private_key'] = private.decode('utf-8')
-            attrs['public_key'] = public.decode('utf-8')
+            attrs["private_key"] = private.decode("utf-8")
+            attrs["public_key"] = public.decode("utf-8")
         return super()._generate(create, attrs)
 
 
@@ -108,15 +105,13 @@ class FollowFactory(factory.DjangoModelFactory):
         model = models.Follow
 
     class Params:
-        local = factory.Trait(
-            actor=factory.SubFactory(ActorFactory, local=True)
-        )
+        local = factory.Trait(actor=factory.SubFactory(ActorFactory, local=True))
 
 
 @registry.register
 class LibraryFactory(factory.DjangoModelFactory):
     actor = factory.SubFactory(ActorFactory)
-    url = factory.Faker('url')
+    url = factory.Faker("url")
     federation_enabled = True
     download_files = False
     autoimport = False
@@ -126,42 +121,36 @@ class LibraryFactory(factory.DjangoModelFactory):
 
 
 class ArtistMetadataFactory(factory.Factory):
-    name = factory.Faker('name')
+    name = factory.Faker("name")
 
     class Meta:
         model = dict
 
     class Params:
-        musicbrainz = factory.Trait(
-            musicbrainz_id=factory.Faker('uuid4')
-        )
+        musicbrainz = factory.Trait(musicbrainz_id=factory.Faker("uuid4"))
 
 
 class ReleaseMetadataFactory(factory.Factory):
-    title = factory.Faker('sentence')
+    title = factory.Faker("sentence")
 
     class Meta:
         model = dict
 
     class Params:
-        musicbrainz = factory.Trait(
-            musicbrainz_id=factory.Faker('uuid4')
-        )
+        musicbrainz = factory.Trait(musicbrainz_id=factory.Faker("uuid4"))
 
 
 class RecordingMetadataFactory(factory.Factory):
-    title = factory.Faker('sentence')
+    title = factory.Faker("sentence")
 
     class Meta:
         model = dict
 
     class Params:
-        musicbrainz = factory.Trait(
-            musicbrainz_id=factory.Faker('uuid4')
-        )
+        musicbrainz = factory.Trait(musicbrainz_id=factory.Faker("uuid4"))
 
 
-@registry.register(name='federation.LibraryTrackMetadata')
+@registry.register(name="federation.LibraryTrackMetadata")
 class LibraryTrackMetadataFactory(factory.Factory):
     artist = factory.SubFactory(ArtistMetadataFactory)
     recording = factory.SubFactory(RecordingMetadataFactory)
@@ -174,64 +163,59 @@ class LibraryTrackMetadataFactory(factory.Factory):
 @registry.register
 class LibraryTrackFactory(factory.DjangoModelFactory):
     library = factory.SubFactory(LibraryFactory)
-    url = factory.Faker('url')
-    title = factory.Faker('sentence')
-    artist_name = factory.Faker('sentence')
-    album_title = factory.Faker('sentence')
-    audio_url = factory.Faker('url')
-    audio_mimetype = 'audio/ogg'
+    url = factory.Faker("url")
+    title = factory.Faker("sentence")
+    artist_name = factory.Faker("sentence")
+    album_title = factory.Faker("sentence")
+    audio_url = factory.Faker("url")
+    audio_mimetype = "audio/ogg"
     metadata = factory.SubFactory(LibraryTrackMetadataFactory)
 
     class Meta:
         model = models.LibraryTrack
 
     class Params:
-        with_audio_file = factory.Trait(
-            audio_file=factory.django.FileField()
-        )
+        with_audio_file = factory.Trait(audio_file=factory.django.FileField())
 
 
-@registry.register(name='federation.Note')
+@registry.register(name="federation.Note")
 class NoteFactory(factory.Factory):
-    type = 'Note'
-    id = factory.Faker('url')
-    published = factory.LazyFunction(
-        lambda: timezone.now().isoformat()
-    )
+    type = "Note"
+    id = factory.Faker("url")
+    published = factory.LazyFunction(lambda: timezone.now().isoformat())
     inReplyTo = None
-    content = factory.Faker('sentence')
+    content = factory.Faker("sentence")
 
     class Meta:
         model = dict
 
 
-@registry.register(name='federation.Activity')
+@registry.register(name="federation.Activity")
 class ActivityFactory(factory.Factory):
-    type = 'Create'
-    id = factory.Faker('url')
-    published = factory.LazyFunction(
-        lambda: timezone.now().isoformat()
-    )
-    actor = factory.Faker('url')
+    type = "Create"
+    id = factory.Faker("url")
+    published = factory.LazyFunction(lambda: timezone.now().isoformat())
+    actor = factory.Faker("url")
     object = factory.SubFactory(
         NoteFactory,
-        actor=factory.SelfAttribute('..actor'),
-        published=factory.SelfAttribute('..published'))
+        actor=factory.SelfAttribute("..actor"),
+        published=factory.SelfAttribute("..published"),
+    )
 
     class Meta:
         model = dict
 
 
-@registry.register(name='federation.AudioMetadata')
+@registry.register(name="federation.AudioMetadata")
 class AudioMetadataFactory(factory.Factory):
     recording = factory.LazyAttribute(
-        lambda o: 'https://musicbrainz.org/recording/{}'.format(uuid.uuid4())
+        lambda o: "https://musicbrainz.org/recording/{}".format(uuid.uuid4())
     )
     artist = factory.LazyAttribute(
-        lambda o: 'https://musicbrainz.org/artist/{}'.format(uuid.uuid4())
+        lambda o: "https://musicbrainz.org/artist/{}".format(uuid.uuid4())
     )
     release = factory.LazyAttribute(
-        lambda o: 'https://musicbrainz.org/release/{}'.format(uuid.uuid4())
+        lambda o: "https://musicbrainz.org/release/{}".format(uuid.uuid4())
     )
     bitrate = 42
     length = 43
@@ -241,14 +225,12 @@ class AudioMetadataFactory(factory.Factory):
         model = dict
 
 
-@registry.register(name='federation.Audio')
+@registry.register(name="federation.Audio")
 class AudioFactory(factory.Factory):
-    type = 'Audio'
-    id = factory.Faker('url')
-    published = factory.LazyFunction(
-        lambda: timezone.now().isoformat()
-    )
-    actor = factory.Faker('url')
+    type = "Audio"
+    id = factory.Faker("url")
+    published = factory.LazyFunction(lambda: timezone.now().isoformat())
+    actor = factory.Faker("url")
     url = factory.SubFactory(LinkFactory, audio=True)
     metadata = factory.SubFactory(LibraryTrackMetadataFactory)
 
diff --git a/api/funkwhale_api/federation/filters.py b/api/funkwhale_api/federation/filters.py
index 1d93f68b993397407ca7b3476c0d8be05d977c4b..3b5bfd7395782f2285a8b7451888078119e8cf10 100644
--- a/api/funkwhale_api/federation/filters.py
+++ b/api/funkwhale_api/federation/filters.py
@@ -6,73 +6,67 @@ from . import models
 
 
 class LibraryFilter(django_filters.FilterSet):
-    approved = django_filters.BooleanFilter('following__approved')
-    q = fields.SearchFilter(search_fields=[
-        'actor__domain',
-    ])
+    approved = django_filters.BooleanFilter("following__approved")
+    q = fields.SearchFilter(search_fields=["actor__domain"])
 
     class Meta:
         model = models.Library
         fields = {
-            'approved': ['exact'],
-            'federation_enabled': ['exact'],
-            'download_files': ['exact'],
-            'autoimport': ['exact'],
-            'tracks_count': ['exact'],
+            "approved": ["exact"],
+            "federation_enabled": ["exact"],
+            "download_files": ["exact"],
+            "autoimport": ["exact"],
+            "tracks_count": ["exact"],
         }
 
 
 class LibraryTrackFilter(django_filters.FilterSet):
-    library = django_filters.CharFilter('library__uuid')
-    status = django_filters.CharFilter(method='filter_status')
-    q = fields.SearchFilter(search_fields=[
-        'artist_name',
-        'title',
-        'album_title',
-        'library__actor__domain',
-    ])
+    library = django_filters.CharFilter("library__uuid")
+    status = django_filters.CharFilter(method="filter_status")
+    q = fields.SearchFilter(
+        search_fields=["artist_name", "title", "album_title", "library__actor__domain"]
+    )
 
     def filter_status(self, queryset, field_name, value):
-        if value == 'imported':
+        if value == "imported":
             return queryset.filter(local_track_file__isnull=False)
-        elif value == 'not_imported':
-            return queryset.filter(
-                local_track_file__isnull=True
-            ).exclude(import_jobs__status='pending')
-        elif value == 'import_pending':
-            return queryset.filter(import_jobs__status='pending')
+        elif value == "not_imported":
+            return queryset.filter(local_track_file__isnull=True).exclude(
+                import_jobs__status="pending"
+            )
+        elif value == "import_pending":
+            return queryset.filter(import_jobs__status="pending")
         return queryset
 
     class Meta:
         model = models.LibraryTrack
         fields = {
-            'library': ['exact'],
-            'artist_name': ['exact', 'icontains'],
-            'title': ['exact', 'icontains'],
-            'album_title': ['exact', 'icontains'],
-            'audio_mimetype': ['exact', 'icontains'],
+            "library": ["exact"],
+            "artist_name": ["exact", "icontains"],
+            "title": ["exact", "icontains"],
+            "album_title": ["exact", "icontains"],
+            "audio_mimetype": ["exact", "icontains"],
         }
 
 
 class FollowFilter(django_filters.FilterSet):
-    pending = django_filters.CharFilter(method='filter_pending')
+    pending = django_filters.CharFilter(method="filter_pending")
     ordering = django_filters.OrderingFilter(
         # tuple-mapping retains order
         fields=(
-            ('creation_date', 'creation_date'),
-            ('modification_date', 'modification_date'),
-        ),
+            ("creation_date", "creation_date"),
+            ("modification_date", "modification_date"),
+        )
+    )
+    q = fields.SearchFilter(
+        search_fields=["actor__domain", "actor__preferred_username"]
     )
-    q = fields.SearchFilter(search_fields=[
-        'actor__domain',
-        'actor__preferred_username',
-    ])
 
     class Meta:
         model = models.Follow
-        fields = ['approved', 'pending', 'q']
+        fields = ["approved", "pending", "q"]
 
     def filter_pending(self, queryset, field_name, value):
-        if value.lower() in ['true', '1', 'yes']:
+        if value.lower() in ["true", "1", "yes"]:
             queryset = queryset.filter(approved__isnull=True)
         return queryset
diff --git a/api/funkwhale_api/federation/keys.py b/api/funkwhale_api/federation/keys.py
index 7e9d316c2582fa4535147265175bbea23e354ecf..e7c30c50aefc866e4ad74b6ef18a2ebaab89cb10 100644
--- a/api/funkwhale_api/federation/keys.py
+++ b/api/funkwhale_api/federation/keys.py
@@ -1,48 +1,44 @@
-from cryptography.hazmat.primitives import serialization as crypto_serialization
-from cryptography.hazmat.primitives.asymmetric import rsa
-from cryptography.hazmat.backends import default_backend as crypto_default_backend
-
 import re
 import urllib.parse
 
-from . import exceptions
+from cryptography.hazmat.backends import default_backend as crypto_default_backend
+from cryptography.hazmat.primitives import serialization as crypto_serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
 
-KEY_ID_REGEX = re.compile(r'keyId=\"(?P<id>.*)\"')
+KEY_ID_REGEX = re.compile(r"keyId=\"(?P<id>.*)\"")
 
 
 def get_key_pair(size=2048):
     key = rsa.generate_private_key(
-        backend=crypto_default_backend(),
-        public_exponent=65537,
-        key_size=size
+        backend=crypto_default_backend(), public_exponent=65537, key_size=size
     )
     private_key = key.private_bytes(
         crypto_serialization.Encoding.PEM,
         crypto_serialization.PrivateFormat.PKCS8,
-        crypto_serialization.NoEncryption())
+        crypto_serialization.NoEncryption(),
+    )
     public_key = key.public_key().public_bytes(
-        crypto_serialization.Encoding.PEM,
-        crypto_serialization.PublicFormat.PKCS1
+        crypto_serialization.Encoding.PEM, crypto_serialization.PublicFormat.PKCS1
     )
 
     return private_key, public_key
 
 
 def get_key_id_from_signature_header(header_string):
-    parts = header_string.split(',')
+    parts = header_string.split(",")
     try:
         raw_key_id = [p for p in parts if p.startswith('keyId="')][0]
     except IndexError:
-        raise ValueError('Missing key id')
+        raise ValueError("Missing key id")
 
     match = KEY_ID_REGEX.match(raw_key_id)
     if not match:
-        raise ValueError('Invalid key id')
+        raise ValueError("Invalid key id")
 
     key_id = match.groups()[0]
     url = urllib.parse.urlparse(key_id)
     if not url.scheme or not url.netloc:
-        raise ValueError('Invalid url')
-    if url.scheme not in ['http', 'https']:
-        raise ValueError('Invalid shceme')
+        raise ValueError("Invalid url")
+    if url.scheme not in ["http", "https"]:
+        raise ValueError("Invalid shceme")
     return key_id
diff --git a/api/funkwhale_api/federation/library.py b/api/funkwhale_api/federation/library.py
index c53ce543067b7a014171ee44baeeafd99d7cc334..d2ccb19524cac60d52469cc795c556cf960f51b9 100644
--- a/api/funkwhale_api/federation/library.py
+++ b/api/funkwhale_api/federation/library.py
@@ -1,15 +1,11 @@
 import json
-import requests
 
+import requests
 from django.conf import settings
 
 from funkwhale_api.common import session
 
-from . import actors
-from . import models
-from . import serializers
-from . import signing
-from . import webfinger
+from . import actors, models, serializers, signing, webfinger
 
 
 def scan_from_account_name(account_name):
@@ -24,87 +20,59 @@ def scan_from_account_name(account_name):
     """
     data = {}
     try:
-        username, domain = webfinger.clean_acct(
-            account_name, ensure_local=False)
+        username, domain = webfinger.clean_acct(account_name, ensure_local=False)
     except serializers.ValidationError:
-        return {
-            'webfinger': {
-                'errors': ['Invalid account string']
-            }
-        }
-    system_library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    library = models.Library.objects.filter(
-        actor__domain=domain,
-        actor__preferred_username=username
-    ).select_related('actor').first()
-    data['local'] = {
-        'following': False,
-        'awaiting_approval': False,
-    }
+        return {"webfinger": {"errors": ["Invalid account string"]}}
+    system_library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    data["local"] = {"following": False, "awaiting_approval": False}
     try:
         follow = models.Follow.objects.get(
             target__preferred_username=username,
             target__domain=username,
             actor=system_library,
         )
-        data['local']['awaiting_approval'] = not bool(follow.approved)
-        data['local']['following'] = True
+        data["local"]["awaiting_approval"] = not bool(follow.approved)
+        data["local"]["following"] = True
     except models.Follow.DoesNotExist:
         pass
 
     try:
-        data['webfinger'] = webfinger.get_resource(
-            'acct:{}'.format(account_name))
+        data["webfinger"] = webfinger.get_resource("acct:{}".format(account_name))
     except requests.ConnectionError:
-        return {
-            'webfinger': {
-                'errors': ['This webfinger resource is not reachable']
-            }
-        }
+        return {"webfinger": {"errors": ["This webfinger resource is not reachable"]}}
     except requests.HTTPError as e:
         return {
-            'webfinger': {
-                'errors': [
-                    'Error {} during webfinger request'.format(
-                        e.response.status_code)]
+            "webfinger": {
+                "errors": [
+                    "Error {} during webfinger request".format(e.response.status_code)
+                ]
             }
         }
     except json.JSONDecodeError as e:
-        return {
-            'webfinger': {
-                'errors': ['Could not process webfinger response']
-            }
-        }
+        return {"webfinger": {"errors": ["Could not process webfinger response"]}}
 
     try:
-        data['actor'] = actors.get_actor_data(data['webfinger']['actor_url'])
+        data["actor"] = actors.get_actor_data(data["webfinger"]["actor_url"])
     except requests.ConnectionError:
-        data['actor'] = {
-            'errors': ['This actor is not reachable']
-        }
+        data["actor"] = {"errors": ["This actor is not reachable"]}
         return data
     except requests.HTTPError as e:
-        data['actor'] = {
-            'errors': [
-                'Error {} during actor request'.format(
-                    e.response.status_code)]
+        data["actor"] = {
+            "errors": ["Error {} during actor request".format(e.response.status_code)]
         }
         return data
 
-    serializer = serializers.LibraryActorSerializer(data=data['actor'])
+    serializer = serializers.LibraryActorSerializer(data=data["actor"])
     if not serializer.is_valid():
-        data['actor'] = {
-            'errors': ['Invalid ActivityPub actor']
-        }
+        data["actor"] = {"errors": ["Invalid ActivityPub actor"]}
         return data
-    data['library'] = get_library_data(
-        serializer.validated_data['library_url'])
+    data["library"] = get_library_data(serializer.validated_data["library_url"])
 
     return data
 
 
 def get_library_data(library_url):
-    actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
     auth = signing.get_auth(actor.private_key, actor.private_key_id)
     try:
         response = session.get_session().get(
@@ -112,55 +80,37 @@ def get_library_data(library_url):
             auth=auth,
             timeout=5,
             verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
-            headers={
-                'Content-Type': 'application/activity+json'
-            }
+            headers={"Content-Type": "application/activity+json"},
         )
     except requests.ConnectionError:
-        return {
-            'errors': ['This library is not reachable']
-        }
+        return {"errors": ["This library is not reachable"]}
     scode = response.status_code
     if scode == 401:
-        return {
-            'errors': ['This library requires authentication']
-        }
+        return {"errors": ["This library requires authentication"]}
     elif scode == 403:
-        return {
-            'errors': ['Permission denied while scanning library']
-        }
+        return {"errors": ["Permission denied while scanning library"]}
     elif scode >= 400:
-        return {
-            'errors': ['Error {} while fetching the library'.format(scode)]
-        }
-    serializer = serializers.PaginatedCollectionSerializer(
-        data=response.json(),
-    )
+        return {"errors": ["Error {} while fetching the library".format(scode)]}
+    serializer = serializers.PaginatedCollectionSerializer(data=response.json())
     if not serializer.is_valid():
-        return {
-            'errors': [
-                'Invalid ActivityPub response from remote library']
-        }
+        return {"errors": ["Invalid ActivityPub response from remote library"]}
 
     return serializer.validated_data
 
 
 def get_library_page(library, page_url):
-    actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
     auth = signing.get_auth(actor.private_key, actor.private_key_id)
     response = session.get_session().get(
         page_url,
         auth=auth,
         timeout=5,
         verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
-        headers={
-            'Content-Type': 'application/activity+json'
-        }
+        headers={"Content-Type": "application/activity+json"},
     )
     serializer = serializers.CollectionPageSerializer(
         data=response.json(),
-        context={
-            'library': library,
-            'item_serializer': serializers.AudioSerializer})
+        context={"library": library, "item_serializer": serializers.AudioSerializer},
+    )
     serializer.is_valid(raise_exception=True)
     return serializer.validated_data
diff --git a/api/funkwhale_api/federation/migrations/0001_initial.py b/api/funkwhale_api/federation/migrations/0001_initial.py
index a9157e57e3fa123c5e79b2d23176f7cd744ee926..a4c641b4e820288f8b086c5a4e2a6321ddcd985a 100644
--- a/api/funkwhale_api/federation/migrations/0001_initial.py
+++ b/api/funkwhale_api/federation/migrations/0001_initial.py
@@ -8,30 +8,74 @@ class Migration(migrations.Migration):
 
     initial = True
 
-    dependencies = [
-    ]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
-            name='Actor',
+            name="Actor",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('url', models.URLField(db_index=True, max_length=500, unique=True)),
-                ('outbox_url', models.URLField(max_length=500)),
-                ('inbox_url', models.URLField(max_length=500)),
-                ('following_url', models.URLField(blank=True, max_length=500, null=True)),
-                ('followers_url', models.URLField(blank=True, max_length=500, null=True)),
-                ('shared_inbox_url', models.URLField(blank=True, max_length=500, null=True)),
-                ('type', models.CharField(choices=[('Person', 'Person'), ('Application', 'Application'), ('Group', 'Group'), ('Organization', 'Organization'), ('Service', 'Service')], default='Person', max_length=25)),
-                ('name', models.CharField(blank=True, max_length=200, null=True)),
-                ('domain', models.CharField(max_length=1000)),
-                ('summary', models.CharField(blank=True, max_length=500, null=True)),
-                ('preferred_username', models.CharField(blank=True, max_length=200, null=True)),
-                ('public_key', models.CharField(blank=True, max_length=5000, null=True)),
-                ('private_key', models.CharField(blank=True, max_length=5000, null=True)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('last_fetch_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('manually_approves_followers', models.NullBooleanField(default=None)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("url", models.URLField(db_index=True, max_length=500, unique=True)),
+                ("outbox_url", models.URLField(max_length=500)),
+                ("inbox_url", models.URLField(max_length=500)),
+                (
+                    "following_url",
+                    models.URLField(blank=True, max_length=500, null=True),
+                ),
+                (
+                    "followers_url",
+                    models.URLField(blank=True, max_length=500, null=True),
+                ),
+                (
+                    "shared_inbox_url",
+                    models.URLField(blank=True, max_length=500, null=True),
+                ),
+                (
+                    "type",
+                    models.CharField(
+                        choices=[
+                            ("Person", "Person"),
+                            ("Application", "Application"),
+                            ("Group", "Group"),
+                            ("Organization", "Organization"),
+                            ("Service", "Service"),
+                        ],
+                        default="Person",
+                        max_length=25,
+                    ),
+                ),
+                ("name", models.CharField(blank=True, max_length=200, null=True)),
+                ("domain", models.CharField(max_length=1000)),
+                ("summary", models.CharField(blank=True, max_length=500, null=True)),
+                (
+                    "preferred_username",
+                    models.CharField(blank=True, max_length=200, null=True),
+                ),
+                (
+                    "public_key",
+                    models.CharField(blank=True, max_length=5000, null=True),
+                ),
+                (
+                    "private_key",
+                    models.CharField(blank=True, max_length=5000, null=True),
+                ),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                (
+                    "last_fetch_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("manually_approves_followers", models.NullBooleanField(default=None)),
             ],
-        ),
+        )
     ]
diff --git a/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py b/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py
index 2200424d8e8cfe9a05c059e305d2533dfcda1121..9c848ac586eccabf44d687cae88c90ce162c1eb5 100644
--- a/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py
+++ b/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py
@@ -5,13 +5,10 @@ from django.db import migrations
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('federation', '0001_initial'),
-    ]
+    dependencies = [("federation", "0001_initial")]
 
     operations = [
         migrations.AlterUniqueTogether(
-            name='actor',
-            unique_together={('domain', 'preferred_username')},
-        ),
+            name="actor", unique_together={("domain", "preferred_username")}
+        )
     ]
diff --git a/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py
index 12e3d73fed327dacfc57a1d4daf2b2099db8d093..021b2ad1c8f32ecfe5e546e0e5bb633d1f834d20 100644
--- a/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py
+++ b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py
@@ -10,7 +10,7 @@ import uuid
 def delete_system_actors(apps, schema_editor):
     """Revert site domain and name to default."""
     Actor = apps.get_model("federation", "Actor")
-    Actor.objects.filter(preferred_username__in=['test', 'library']).delete()
+    Actor.objects.filter(preferred_username__in=["test", "library"]).delete()
 
 
 def backward(apps, schema_editor):
@@ -19,76 +19,168 @@ def backward(apps, schema_editor):
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('federation', '0002_auto_20180403_1620'),
-    ]
+    dependencies = [("federation", "0002_auto_20180403_1620")]
 
     operations = [
         migrations.RunPython(delete_system_actors, backward),
         migrations.CreateModel(
-            name='Follow',
+            name="Follow",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('modification_date', models.DateTimeField(auto_now=True)),
-                ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emitted_follows', to='federation.Actor')),
-                ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follows', to='federation.Actor')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("uuid", models.UUIDField(default=uuid.uuid4, unique=True)),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("modification_date", models.DateTimeField(auto_now=True)),
+                (
+                    "actor",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="emitted_follows",
+                        to="federation.Actor",
+                    ),
+                ),
+                (
+                    "target",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="received_follows",
+                        to="federation.Actor",
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
-            name='FollowRequest',
+            name="FollowRequest",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('modification_date', models.DateTimeField(auto_now=True)),
-                ('approved', models.NullBooleanField(default=None)),
-                ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emmited_follow_requests', to='federation.Actor')),
-                ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follow_requests', to='federation.Actor')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("uuid", models.UUIDField(default=uuid.uuid4, unique=True)),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("modification_date", models.DateTimeField(auto_now=True)),
+                ("approved", models.NullBooleanField(default=None)),
+                (
+                    "actor",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="emmited_follow_requests",
+                        to="federation.Actor",
+                    ),
+                ),
+                (
+                    "target",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="received_follow_requests",
+                        to="federation.Actor",
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
-            name='Library',
+            name="Library",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('modification_date', models.DateTimeField(auto_now=True)),
-                ('fetched_date', models.DateTimeField(blank=True, null=True)),
-                ('uuid', models.UUIDField(default=uuid.uuid4)),
-                ('url', models.URLField()),
-                ('federation_enabled', models.BooleanField()),
-                ('download_files', models.BooleanField()),
-                ('autoimport', models.BooleanField()),
-                ('tracks_count', models.PositiveIntegerField(blank=True, null=True)),
-                ('actor', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='library', to='federation.Actor')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("modification_date", models.DateTimeField(auto_now=True)),
+                ("fetched_date", models.DateTimeField(blank=True, null=True)),
+                ("uuid", models.UUIDField(default=uuid.uuid4)),
+                ("url", models.URLField()),
+                ("federation_enabled", models.BooleanField()),
+                ("download_files", models.BooleanField()),
+                ("autoimport", models.BooleanField()),
+                ("tracks_count", models.PositiveIntegerField(blank=True, null=True)),
+                (
+                    "actor",
+                    models.OneToOneField(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="library",
+                        to="federation.Actor",
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
-            name='LibraryTrack',
+            name="LibraryTrack",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('url', models.URLField(unique=True)),
-                ('audio_url', models.URLField()),
-                ('audio_mimetype', models.CharField(max_length=200)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('modification_date', models.DateTimeField(auto_now=True)),
-                ('fetched_date', models.DateTimeField(blank=True, null=True)),
-                ('published_date', models.DateTimeField(blank=True, null=True)),
-                ('artist_name', models.CharField(max_length=500)),
-                ('album_title', models.CharField(max_length=500)),
-                ('title', models.CharField(max_length=500)),
-                ('metadata', django.contrib.postgres.fields.jsonb.JSONField(default={}, max_length=10000)),
-                ('library', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracks', to='federation.Library')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("url", models.URLField(unique=True)),
+                ("audio_url", models.URLField()),
+                ("audio_mimetype", models.CharField(max_length=200)),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("modification_date", models.DateTimeField(auto_now=True)),
+                ("fetched_date", models.DateTimeField(blank=True, null=True)),
+                ("published_date", models.DateTimeField(blank=True, null=True)),
+                ("artist_name", models.CharField(max_length=500)),
+                ("album_title", models.CharField(max_length=500)),
+                ("title", models.CharField(max_length=500)),
+                (
+                    "metadata",
+                    django.contrib.postgres.fields.jsonb.JSONField(
+                        default={}, max_length=10000
+                    ),
+                ),
+                (
+                    "library",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="tracks",
+                        to="federation.Library",
+                    ),
+                ),
             ],
         ),
         migrations.AddField(
-            model_name='actor',
-            name='followers',
-            field=models.ManyToManyField(related_name='following', through='federation.Follow', to='federation.Actor'),
+            model_name="actor",
+            name="followers",
+            field=models.ManyToManyField(
+                related_name="following",
+                through="federation.Follow",
+                to="federation.Actor",
+            ),
         ),
         migrations.AlterUniqueTogether(
-            name='follow',
-            unique_together={('actor', 'target')},
+            name="follow", unique_together={("actor", "target")}
         ),
     ]
diff --git a/api/funkwhale_api/federation/migrations/0004_auto_20180410_2025.py b/api/funkwhale_api/federation/migrations/0004_auto_20180410_2025.py
index bea4d14ae6502272486b48bfa6f6f2074e06cb2c..f0e5cf1d6ab0d1a8a1e0c5946b39fb363548316e 100644
--- a/api/funkwhale_api/federation/migrations/0004_auto_20180410_2025.py
+++ b/api/funkwhale_api/federation/migrations/0004_auto_20180410_2025.py
@@ -6,30 +6,26 @@ import django.db.models.deletion
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('federation', '0003_auto_20180407_1010'),
-    ]
+    dependencies = [("federation", "0003_auto_20180407_1010")]
 
     operations = [
-        migrations.RemoveField(
-            model_name='followrequest',
-            name='actor',
-        ),
-        migrations.RemoveField(
-            model_name='followrequest',
-            name='target',
-        ),
+        migrations.RemoveField(model_name="followrequest", name="actor"),
+        migrations.RemoveField(model_name="followrequest", name="target"),
         migrations.AddField(
-            model_name='follow',
-            name='approved',
+            model_name="follow",
+            name="approved",
             field=models.NullBooleanField(default=None),
         ),
         migrations.AddField(
-            model_name='library',
-            name='follow',
-            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='library', to='federation.Follow'),
-        ),
-        migrations.DeleteModel(
-            name='FollowRequest',
+            model_name="library",
+            name="follow",
+            field=models.OneToOneField(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name="library",
+                to="federation.Follow",
+            ),
         ),
+        migrations.DeleteModel(name="FollowRequest"),
     ]
diff --git a/api/funkwhale_api/federation/migrations/0005_auto_20180413_1723.py b/api/funkwhale_api/federation/migrations/0005_auto_20180413_1723.py
index 00ba5c83deb9316ab8d509ba5e859e2fe979aafd..0b2029e95c420670e62b499b4747be78053ce7d1 100644
--- a/api/funkwhale_api/federation/migrations/0005_auto_20180413_1723.py
+++ b/api/funkwhale_api/federation/migrations/0005_auto_20180413_1723.py
@@ -8,19 +8,25 @@ import funkwhale_api.federation.models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('federation', '0004_auto_20180410_2025'),
-    ]
+    dependencies = [("federation", "0004_auto_20180410_2025")]
 
     operations = [
         migrations.AddField(
-            model_name='librarytrack',
-            name='audio_file',
-            field=models.FileField(blank=True, null=True, upload_to=funkwhale_api.federation.models.get_file_path),
+            model_name="librarytrack",
+            name="audio_file",
+            field=models.FileField(
+                blank=True,
+                null=True,
+                upload_to=funkwhale_api.federation.models.get_file_path,
+            ),
         ),
         migrations.AlterField(
-            model_name='librarytrack',
-            name='metadata',
-            field=django.contrib.postgres.fields.jsonb.JSONField(default={}, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=10000),
+            model_name="librarytrack",
+            name="metadata",
+            field=django.contrib.postgres.fields.jsonb.JSONField(
+                default={},
+                encoder=django.core.serializers.json.DjangoJSONEncoder,
+                max_length=10000,
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/federation/migrations/0006_auto_20180521_1702.py b/api/funkwhale_api/federation/migrations/0006_auto_20180521_1702.py
index 7dcf85670b1391af985811cda03fb114b2bd3998..eb731f0aa6b1c291c3862aff661f33c220a35445 100644
--- a/api/funkwhale_api/federation/migrations/0006_auto_20180521_1702.py
+++ b/api/funkwhale_api/federation/migrations/0006_auto_20180521_1702.py
@@ -5,24 +5,20 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('federation', '0005_auto_20180413_1723'),
-    ]
+    dependencies = [("federation", "0005_auto_20180413_1723")]
 
     operations = [
         migrations.AlterField(
-            model_name='library',
-            name='url',
-            field=models.URLField(max_length=500),
+            model_name="library", name="url", field=models.URLField(max_length=500)
         ),
         migrations.AlterField(
-            model_name='librarytrack',
-            name='audio_url',
+            model_name="librarytrack",
+            name="audio_url",
             field=models.URLField(max_length=500),
         ),
         migrations.AlterField(
-            model_name='librarytrack',
-            name='url',
+            model_name="librarytrack",
+            name="url",
             field=models.URLField(max_length=500, unique=True),
         ),
     ]
diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index 8b4f284756cebefed6e67e13f92f707aa01ef05f..979b0674a94848dd7c0edb79f041900951629494 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -1,6 +1,6 @@
 import os
-import uuid
 import tempfile
+import uuid
 
 from django.conf import settings
 from django.contrib.postgres.fields import JSONField
@@ -12,16 +12,16 @@ from funkwhale_api.common import session
 from funkwhale_api.music import utils as music_utils
 
 TYPE_CHOICES = [
-    ('Person', 'Person'),
-    ('Application', 'Application'),
-    ('Group', 'Group'),
-    ('Organization', 'Organization'),
-    ('Service', 'Service'),
+    ("Person", "Person"),
+    ("Application", "Application"),
+    ("Group", "Group"),
+    ("Organization", "Organization"),
+    ("Service", "Service"),
 ]
 
 
 class Actor(models.Model):
-    ap_type = 'Actor'
+    ap_type = "Actor"
 
     url = models.URLField(unique=True, max_length=500, db_index=True)
     outbox_url = models.URLField(max_length=500)
@@ -29,49 +29,41 @@ class Actor(models.Model):
     following_url = models.URLField(max_length=500, null=True, blank=True)
     followers_url = models.URLField(max_length=500, null=True, blank=True)
     shared_inbox_url = models.URLField(max_length=500, null=True, blank=True)
-    type = models.CharField(
-        choices=TYPE_CHOICES, default='Person', max_length=25)
+    type = models.CharField(choices=TYPE_CHOICES, default="Person", max_length=25)
     name = models.CharField(max_length=200, null=True, blank=True)
     domain = models.CharField(max_length=1000)
     summary = models.CharField(max_length=500, null=True, blank=True)
-    preferred_username = models.CharField(
-        max_length=200, null=True, blank=True)
+    preferred_username = models.CharField(max_length=200, null=True, blank=True)
     public_key = models.CharField(max_length=5000, null=True, blank=True)
     private_key = models.CharField(max_length=5000, null=True, blank=True)
     creation_date = models.DateTimeField(default=timezone.now)
-    last_fetch_date = models.DateTimeField(
-        default=timezone.now)
+    last_fetch_date = models.DateTimeField(default=timezone.now)
     manually_approves_followers = models.NullBooleanField(default=None)
     followers = models.ManyToManyField(
-        to='self',
+        to="self",
         symmetrical=False,
-        through='Follow',
-        through_fields=('target', 'actor'),
-        related_name='following',
+        through="Follow",
+        through_fields=("target", "actor"),
+        related_name="following",
     )
 
     class Meta:
-        unique_together = ['domain', 'preferred_username']
+        unique_together = ["domain", "preferred_username"]
 
     @property
     def webfinger_subject(self):
-        return '{}@{}'.format(
-            self.preferred_username,
-            settings.FEDERATION_HOSTNAME,
-        )
+        return "{}@{}".format(self.preferred_username, settings.FEDERATION_HOSTNAME)
 
     @property
     def private_key_id(self):
-        return '{}#main-key'.format(self.url)
+        return "{}#main-key".format(self.url)
 
     @property
     def mention_username(self):
-        return '@{}@{}'.format(self.preferred_username, self.domain)
+        return "@{}@{}".format(self.preferred_username, self.domain)
 
     def save(self, **kwargs):
-        lowercase_fields = [
-            'domain',
-        ]
+        lowercase_fields = ["domain"]
         for field in lowercase_fields:
             v = getattr(self, field, None)
             if v:
@@ -86,58 +78,54 @@ class Actor(models.Model):
     @property
     def is_system(self):
         from . import actors
-        return all([
-            settings.FEDERATION_HOSTNAME == self.domain,
-            self.preferred_username in actors.SYSTEM_ACTORS
-        ])
+
+        return all(
+            [
+                settings.FEDERATION_HOSTNAME == self.domain,
+                self.preferred_username in actors.SYSTEM_ACTORS,
+            ]
+        )
 
     @property
     def system_conf(self):
         from . import actors
+
         if self.is_system:
             return actors.SYSTEM_ACTORS[self.preferred_username]
 
     def get_approved_followers(self):
         follows = self.received_follows.filter(approved=True)
-        return self.followers.filter(
-            pk__in=follows.values_list('actor', flat=True))
+        return self.followers.filter(pk__in=follows.values_list("actor", flat=True))
 
 
 class Follow(models.Model):
-    ap_type = 'Follow'
+    ap_type = "Follow"
 
     uuid = models.UUIDField(default=uuid.uuid4, unique=True)
     actor = models.ForeignKey(
-        Actor,
-        related_name='emitted_follows',
-        on_delete=models.CASCADE,
+        Actor, related_name="emitted_follows", on_delete=models.CASCADE
     )
     target = models.ForeignKey(
-        Actor,
-        related_name='received_follows',
-        on_delete=models.CASCADE,
+        Actor, related_name="received_follows", on_delete=models.CASCADE
     )
     creation_date = models.DateTimeField(default=timezone.now)
-    modification_date = models.DateTimeField(
-        auto_now=True)
+    modification_date = models.DateTimeField(auto_now=True)
     approved = models.NullBooleanField(default=None)
 
     class Meta:
-        unique_together = ['actor', 'target']
+        unique_together = ["actor", "target"]
 
     def get_federation_url(self):
-        return '{}#follows/{}'.format(self.actor.url, self.uuid)
+        return "{}#follows/{}".format(self.actor.url, self.uuid)
 
 
 class Library(models.Model):
     creation_date = models.DateTimeField(default=timezone.now)
-    modification_date = models.DateTimeField(
-        auto_now=True)
+    modification_date = models.DateTimeField(auto_now=True)
     fetched_date = models.DateTimeField(null=True, blank=True)
     actor = models.OneToOneField(
-        Actor,
-        on_delete=models.CASCADE,
-        related_name='library')
+        Actor, on_delete=models.CASCADE, related_name="library"
+    )
     uuid = models.UUIDField(default=uuid.uuid4)
     url = models.URLField(max_length=500)
 
@@ -149,69 +137,60 @@ class Library(models.Model):
     autoimport = models.BooleanField()
     tracks_count = models.PositiveIntegerField(null=True, blank=True)
     follow = models.OneToOneField(
-        Follow,
-        related_name='library',
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL,
+        Follow, related_name="library", null=True, blank=True, on_delete=models.SET_NULL
     )
 
 
 def get_file_path(instance, filename):
     uid = str(uuid.uuid4())
     chunk_size = 2
-    chunks = [uid[i:i+chunk_size] for i in range(0, len(uid), chunk_size)]
+    chunks = [uid[i : i + chunk_size] for i in range(0, len(uid), chunk_size)]
     parts = chunks[:3] + [filename]
-    return os.path.join('federation_cache', *parts)
+    return os.path.join("federation_cache", *parts)
 
 
 class LibraryTrack(models.Model):
     url = models.URLField(unique=True, max_length=500)
     audio_url = models.URLField(max_length=500)
     audio_mimetype = models.CharField(max_length=200)
-    audio_file = models.FileField(
-        upload_to=get_file_path,
-        null=True,
-        blank=True)
+    audio_file = models.FileField(upload_to=get_file_path, null=True, blank=True)
 
     creation_date = models.DateTimeField(default=timezone.now)
-    modification_date = models.DateTimeField(
-        auto_now=True)
+    modification_date = models.DateTimeField(auto_now=True)
     fetched_date = models.DateTimeField(null=True, blank=True)
     published_date = models.DateTimeField(null=True, blank=True)
     library = models.ForeignKey(
-        Library, related_name='tracks', on_delete=models.CASCADE)
+        Library, related_name="tracks", on_delete=models.CASCADE
+    )
     artist_name = models.CharField(max_length=500)
     album_title = models.CharField(max_length=500)
     title = models.CharField(max_length=500)
-    metadata = JSONField(
-        default={}, max_length=10000, encoder=DjangoJSONEncoder)
+    metadata = JSONField(default={}, max_length=10000, encoder=DjangoJSONEncoder)
 
     @property
     def mbid(self):
         try:
-            return self.metadata['recording']['musicbrainz_id']
+            return self.metadata["recording"]["musicbrainz_id"]
         except KeyError:
             pass
 
     def download_audio(self):
         from . import actors
-        auth = actors.SYSTEM_ACTORS['library'].get_request_auth()
+
+        auth = actors.SYSTEM_ACTORS["library"].get_request_auth()
         remote_response = session.get_session().get(
             self.audio_url,
             auth=auth,
             stream=True,
             timeout=20,
             verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
-            headers={
-                'Content-Type': 'application/activity+json'
-            }
+            headers={"Content-Type": "application/activity+json"},
         )
         with remote_response as r:
             remote_response.raise_for_status()
             extension = music_utils.get_ext_from_type(self.audio_mimetype)
-            title = ' - '.join([self.title, self.album_title, self.artist_name])
-            filename = '{}.{}'.format(title, extension)
+            title = " - ".join([self.title, self.album_title, self.artist_name])
+            filename = "{}.{}".format(title, extension)
             tmp_file = tempfile.TemporaryFile()
             for chunk in r.iter_content(chunk_size=512):
                 tmp_file.write(chunk)
diff --git a/api/funkwhale_api/federation/parsers.py b/api/funkwhale_api/federation/parsers.py
index 874d808f973dfcdf99372332b3991136c44d8605..8afe21a23a431d735d3175df8bcae64573b52f64 100644
--- a/api/funkwhale_api/federation/parsers.py
+++ b/api/funkwhale_api/federation/parsers.py
@@ -2,4 +2,4 @@ from rest_framework import parsers
 
 
 class ActivityParser(parsers.JSONParser):
-    media_type = 'application/activity+json'
+    media_type = "application/activity+json"
diff --git a/api/funkwhale_api/federation/permissions.py b/api/funkwhale_api/federation/permissions.py
index 438b675cb300a6ba1607e24c639189224488cced..a08d57e5f354dc6dd77e8ef3a477b5886a9cbde1 100644
--- a/api/funkwhale_api/federation/permissions.py
+++ b/api/funkwhale_api/federation/permissions.py
@@ -1,21 +1,19 @@
-from django.conf import settings
 
 from rest_framework.permissions import BasePermission
 
 from funkwhale_api.common import preferences
+
 from . import actors
 
 
 class LibraryFollower(BasePermission):
-
     def has_permission(self, request, view):
-        if not preferences.get('federation__music_needs_approval'):
+        if not preferences.get("federation__music_needs_approval"):
             return True
 
-        actor = getattr(request, 'actor', None)
+        actor = getattr(request, "actor", None)
         if actor is None:
             return False
 
-        library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-        return library.received_follows.filter(
-            approved=True, actor=actor).exists()
+        library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+        return library.received_follows.filter(approved=True, actor=actor).exists()
diff --git a/api/funkwhale_api/federation/renderers.py b/api/funkwhale_api/federation/renderers.py
index 642b634628f2787044eae9b6a96b9e7004b05244..d72c4c06a298b5bb143d620ad4a0c2a72759c3d7 100644
--- a/api/funkwhale_api/federation/renderers.py
+++ b/api/funkwhale_api/federation/renderers.py
@@ -2,8 +2,8 @@ from rest_framework.renderers import JSONRenderer
 
 
 class ActivityPubRenderer(JSONRenderer):
-    media_type = 'application/activity+json'
+    media_type = "application/activity+json"
 
 
 class WebfingerRenderer(JSONRenderer):
-    media_type = 'application/jrd+json'
+    media_type = "application/jrd+json"
diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py
index 6ffffaa9aa37396bda8acb5fe2bcfe6d281b8edf..062f74f476da3b818477306683a9f284a4e126bc 100644
--- a/api/funkwhale_api/federation/serializers.py
+++ b/api/funkwhale_api/federation/serializers.py
@@ -1,27 +1,20 @@
 import logging
 import urllib.parse
 
-from django.urls import reverse
-from django.conf import settings
 from django.core.paginator import Paginator
 from django.db import transaction
-
 from rest_framework import serializers
-from dynamic_preferences.registries import global_preferences_registry
 
-from funkwhale_api.common import utils as funkwhale_utils
 from funkwhale_api.common import serializers as common_serializers
+from funkwhale_api.common import utils as funkwhale_utils
 from funkwhale_api.music import models as music_models
 from funkwhale_api.music import tasks as music_tasks
-from . import activity
-from . import filters
-from . import models
-from . import utils
 
+from . import activity, filters, models, utils
 
 AP_CONTEXT = [
-    'https://www.w3.org/ns/activitystreams',
-    'https://w3id.org/security/v1',
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
     {},
 ]
 
@@ -43,58 +36,58 @@ class ActorSerializer(serializers.Serializer):
 
     def to_representation(self, instance):
         ret = {
-            'id': instance.url,
-            'outbox': instance.outbox_url,
-            'inbox': instance.inbox_url,
-            'preferredUsername': instance.preferred_username,
-            'type': instance.type,
+            "id": instance.url,
+            "outbox": instance.outbox_url,
+            "inbox": instance.inbox_url,
+            "preferredUsername": instance.preferred_username,
+            "type": instance.type,
         }
         if instance.name:
-            ret['name'] = instance.name
+            ret["name"] = instance.name
         if instance.followers_url:
-            ret['followers'] = instance.followers_url
+            ret["followers"] = instance.followers_url
         if instance.following_url:
-            ret['following'] = instance.following_url
+            ret["following"] = instance.following_url
         if instance.summary:
-            ret['summary'] = instance.summary
+            ret["summary"] = instance.summary
         if instance.manually_approves_followers is not None:
-            ret['manuallyApprovesFollowers'] = instance.manually_approves_followers
+            ret["manuallyApprovesFollowers"] = instance.manually_approves_followers
 
-        ret['@context'] = AP_CONTEXT
+        ret["@context"] = AP_CONTEXT
         if instance.public_key:
-            ret['publicKey'] = {
-                'owner': instance.url,
-                'publicKeyPem': instance.public_key,
-                'id': '{}#main-key'.format(instance.url)
+            ret["publicKey"] = {
+                "owner": instance.url,
+                "publicKeyPem": instance.public_key,
+                "id": "{}#main-key".format(instance.url),
             }
-        ret['endpoints'] = {}
+        ret["endpoints"] = {}
         if instance.shared_inbox_url:
-            ret['endpoints']['sharedInbox'] = instance.shared_inbox_url
+            ret["endpoints"]["sharedInbox"] = instance.shared_inbox_url
         return ret
 
     def prepare_missing_fields(self):
         kwargs = {
-            'url': self.validated_data['id'],
-            'outbox_url': self.validated_data['outbox'],
-            'inbox_url': self.validated_data['inbox'],
-            'following_url': self.validated_data.get('following'),
-            'followers_url': self.validated_data.get('followers'),
-            'summary': self.validated_data.get('summary'),
-            'type': self.validated_data['type'],
-            'name': self.validated_data.get('name'),
-            'preferred_username': self.validated_data['preferredUsername'],
+            "url": self.validated_data["id"],
+            "outbox_url": self.validated_data["outbox"],
+            "inbox_url": self.validated_data["inbox"],
+            "following_url": self.validated_data.get("following"),
+            "followers_url": self.validated_data.get("followers"),
+            "summary": self.validated_data.get("summary"),
+            "type": self.validated_data["type"],
+            "name": self.validated_data.get("name"),
+            "preferred_username": self.validated_data["preferredUsername"],
         }
-        maf = self.validated_data.get('manuallyApprovesFollowers')
+        maf = self.validated_data.get("manuallyApprovesFollowers")
         if maf is not None:
-            kwargs['manually_approves_followers'] = maf
-        domain = urllib.parse.urlparse(kwargs['url']).netloc
-        kwargs['domain'] = domain
-        for endpoint, url in self.initial_data.get('endpoints', {}).items():
-            if endpoint == 'sharedInbox':
-                kwargs['shared_inbox_url'] = url
+            kwargs["manually_approves_followers"] = maf
+        domain = urllib.parse.urlparse(kwargs["url"]).netloc
+        kwargs["domain"] = domain
+        for endpoint, url in self.initial_data.get("endpoints", {}).items():
+            if endpoint == "sharedInbox":
+                kwargs["shared_inbox_url"] = url
                 break
         try:
-            kwargs['public_key'] = self.initial_data['publicKey']['publicKeyPem']
+            kwargs["public_key"] = self.initial_data["publicKey"]["publicKeyPem"]
         except KeyError:
             pass
         return kwargs
@@ -106,10 +99,7 @@ class ActorSerializer(serializers.Serializer):
     def save(self, **kwargs):
         d = self.prepare_missing_fields()
         d.update(kwargs)
-        return models.Actor.objects.update_or_create(
-            url=d['url'],
-            defaults=d,
-        )[0]
+        return models.Actor.objects.update_or_create(url=d["url"], defaults=d)[0]
 
     def validate_summary(self, value):
         if value:
@@ -120,35 +110,33 @@ class APIActorSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Actor
         fields = [
-            'id',
-            'url',
-            'creation_date',
-            'summary',
-            'preferred_username',
-            'name',
-            'last_fetch_date',
-            'domain',
-            'type',
-            'manually_approves_followers',
-
+            "id",
+            "url",
+            "creation_date",
+            "summary",
+            "preferred_username",
+            "name",
+            "last_fetch_date",
+            "domain",
+            "type",
+            "manually_approves_followers",
         ]
 
 
 class LibraryActorSerializer(ActorSerializer):
-    url = serializers.ListField(
-        child=serializers.JSONField())
+    url = serializers.ListField(child=serializers.JSONField())
 
     def validate(self, validated_data):
         try:
-            urls = validated_data['url']
+            urls = validated_data["url"]
         except KeyError:
-            raise serializers.ValidationError('Missing URL field')
+            raise serializers.ValidationError("Missing URL field")
 
         for u in urls:
             try:
-                if u['name'] != 'library':
+                if u["name"] != "library":
                     continue
-                validated_data['library_url'] = u['href']
+                validated_data["library_url"] = u["href"]
                 break
             except KeyError:
                 continue
@@ -160,12 +148,12 @@ class APIFollowSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Follow
         fields = [
-            'uuid',
-            'actor',
-            'target',
-            'approved',
-            'creation_date',
-            'modification_date',
+            "uuid",
+            "actor",
+            "target",
+            "approved",
+            "creation_date",
+            "modification_date",
         ]
 
 
@@ -177,19 +165,19 @@ class APILibrarySerializer(serializers.ModelSerializer):
         model = models.Library
 
         read_only_fields = [
-            'actor',
-            'uuid',
-            'url',
-            'tracks_count',
-            'follow',
-            'fetched_date',
-            'modification_date',
-            'creation_date',
+            "actor",
+            "uuid",
+            "url",
+            "tracks_count",
+            "follow",
+            "fetched_date",
+            "modification_date",
+            "creation_date",
         ]
         fields = [
-            'autoimport',
-            'federation_enabled',
-            'download_files',
+            "autoimport",
+            "federation_enabled",
+            "download_files",
         ] + read_only_fields
 
 
@@ -203,24 +191,22 @@ class APILibraryFollowUpdateSerializer(serializers.Serializer):
 
     def validate_follow(self, value):
         from . import actors
-        library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-        qs = models.Follow.objects.filter(
-            pk=value,
-            target=library_actor,
-        )
+
+        library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+        qs = models.Follow.objects.filter(pk=value, target=library_actor)
         try:
             return qs.get()
         except models.Follow.DoesNotExist:
-            raise serializers.ValidationError('Invalid follow')
+            raise serializers.ValidationError("Invalid follow")
 
     def save(self):
-        new_status = self.validated_data['approved']
-        follow = self.validated_data['follow']
+        new_status = self.validated_data["approved"]
+        follow = self.validated_data["follow"]
         if new_status == follow.approved:
             return follow
 
         follow.approved = new_status
-        follow.save(update_fields=['approved', 'modification_date'])
+        follow.save(update_fields=["approved", "modification_date"])
         if new_status:
             activity.accept_follow(follow)
         return follow
@@ -233,19 +219,13 @@ class APILibraryCreateSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = models.Library
-        fields = [
-            'uuid',
-            'actor',
-            'autoimport',
-            'federation_enabled',
-            'download_files',
-        ]
+        fields = ["uuid", "actor", "autoimport", "federation_enabled", "download_files"]
 
     def validate(self, validated_data):
         from . import actors
         from . import library
 
-        actor_url = validated_data['actor']
+        actor_url = validated_data["actor"]
         actor_data = actors.get_actor_data(actor_url)
         acs = LibraryActorSerializer(data=actor_data)
         acs.is_valid(raise_exception=True)
@@ -253,43 +233,39 @@ class APILibraryCreateSerializer(serializers.ModelSerializer):
             actor = models.Actor.objects.get(url=actor_url)
         except models.Actor.DoesNotExist:
             actor = acs.save()
-        library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-        validated_data['follow'] = models.Follow.objects.get_or_create(
-            actor=library_actor,
-            target=actor,
+        library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+        validated_data["follow"] = models.Follow.objects.get_or_create(
+            actor=library_actor, target=actor
         )[0]
-        if validated_data['follow'].approved is None:
+        if validated_data["follow"].approved is None:
             funkwhale_utils.on_commit(
                 activity.deliver,
-                FollowSerializer(validated_data['follow']).data,
-                on_behalf_of=validated_data['follow'].actor,
-                to=[validated_data['follow'].target.url],
+                FollowSerializer(validated_data["follow"]).data,
+                on_behalf_of=validated_data["follow"].actor,
+                to=[validated_data["follow"].target.url],
             )
 
-        library_data = library.get_library_data(
-            acs.validated_data['library_url'])
-        if 'errors' in library_data:
+        library_data = library.get_library_data(acs.validated_data["library_url"])
+        if "errors" in library_data:
             # we pass silently because it may means we require permission
             # before scanning
             pass
-        validated_data['library'] = library_data
-        validated_data['library'].setdefault(
-            'id', acs.validated_data['library_url']
-        )
-        validated_data['actor'] = actor
+        validated_data["library"] = library_data
+        validated_data["library"].setdefault("id", acs.validated_data["library_url"])
+        validated_data["actor"] = actor
         return validated_data
 
     def create(self, validated_data):
         library = models.Library.objects.update_or_create(
-            url=validated_data['library']['id'],
+            url=validated_data["library"]["id"],
             defaults={
-                'actor': validated_data['actor'],
-                'follow': validated_data['follow'],
-                'tracks_count': validated_data['library'].get('totalItems'),
-                'federation_enabled': validated_data['federation_enabled'],
-                'autoimport': validated_data['autoimport'],
-                'download_files': validated_data['download_files'],
-            }
+                "actor": validated_data["actor"],
+                "follow": validated_data["follow"],
+                "tracks_count": validated_data["library"].get("totalItems"),
+                "federation_enabled": validated_data["federation_enabled"],
+                "autoimport": validated_data["autoimport"],
+                "download_files": validated_data["download_files"],
+            },
         )[0]
         return library
 
@@ -301,75 +277,74 @@ class APILibraryTrackSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.LibraryTrack
         fields = [
-            'id',
-            'url',
-            'audio_url',
-            'audio_mimetype',
-            'creation_date',
-            'modification_date',
-            'fetched_date',
-            'published_date',
-            'metadata',
-            'artist_name',
-            'album_title',
-            'title',
-            'library',
-            'local_track_file',
-            'status',
+            "id",
+            "url",
+            "audio_url",
+            "audio_mimetype",
+            "creation_date",
+            "modification_date",
+            "fetched_date",
+            "published_date",
+            "metadata",
+            "artist_name",
+            "album_title",
+            "title",
+            "library",
+            "local_track_file",
+            "status",
         ]
 
     def get_status(self, o):
         try:
             if o.local_track_file is not None:
-                return 'imported'
+                return "imported"
         except music_models.TrackFile.DoesNotExist:
             pass
         for job in o.import_jobs.all():
-            if job.status == 'pending':
-                return 'import_pending'
-        return 'not_imported'
+            if job.status == "pending":
+                return "import_pending"
+        return "not_imported"
 
 
 class FollowSerializer(serializers.Serializer):
     id = serializers.URLField(max_length=500)
     object = serializers.URLField(max_length=500)
     actor = serializers.URLField(max_length=500)
-    type = serializers.ChoiceField(choices=['Follow'])
+    type = serializers.ChoiceField(choices=["Follow"])
 
     def validate_object(self, v):
-        expected = self.context.get('follow_target')
+        expected = self.context.get("follow_target")
         if expected and expected.url != v:
-            raise serializers.ValidationError('Invalid target')
+            raise serializers.ValidationError("Invalid target")
         try:
             return models.Actor.objects.get(url=v)
         except models.Actor.DoesNotExist:
-            raise serializers.ValidationError('Target not found')
+            raise serializers.ValidationError("Target not found")
 
     def validate_actor(self, v):
-        expected = self.context.get('follow_actor')
+        expected = self.context.get("follow_actor")
         if expected and expected.url != v:
-            raise serializers.ValidationError('Invalid actor')
+            raise serializers.ValidationError("Invalid actor")
         try:
             return models.Actor.objects.get(url=v)
         except models.Actor.DoesNotExist:
-            raise serializers.ValidationError('Actor not found')
+            raise serializers.ValidationError("Actor not found")
 
     def save(self, **kwargs):
         return models.Follow.objects.get_or_create(
-            actor=self.validated_data['actor'],
-            target=self.validated_data['object'],
-            **kwargs,
+            actor=self.validated_data["actor"],
+            target=self.validated_data["object"],
+            **kwargs,  # noqa
         )[0]
 
     def to_representation(self, instance):
         return {
-            '@context': AP_CONTEXT,
-            'actor': instance.actor.url,
-            'id': instance.get_federation_url(),
-            'object': instance.target.url,
-            'type': 'Follow'
+            "@context": AP_CONTEXT,
+            "actor": instance.actor.url,
+            "id": instance.get_federation_url(),
+            "object": instance.target.url,
+            "type": "Follow",
         }
-        return ret
 
 
 class APIFollowSerializer(serializers.ModelSerializer):
@@ -379,13 +354,13 @@ class APIFollowSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Follow
         fields = [
-            'uuid',
-            'id',
-            'approved',
-            'creation_date',
-            'modification_date',
-            'actor',
-            'target',
+            "uuid",
+            "id",
+            "approved",
+            "creation_date",
+            "modification_date",
+            "actor",
+            "target",
         ]
 
 
@@ -393,84 +368,87 @@ class AcceptFollowSerializer(serializers.Serializer):
     id = serializers.URLField(max_length=500)
     actor = serializers.URLField(max_length=500)
     object = FollowSerializer()
-    type = serializers.ChoiceField(choices=['Accept'])
+    type = serializers.ChoiceField(choices=["Accept"])
 
     def validate_actor(self, v):
-        expected = self.context.get('follow_target')
+        expected = self.context.get("follow_target")
         if expected and expected.url != v:
-            raise serializers.ValidationError('Invalid actor')
+            raise serializers.ValidationError("Invalid actor")
         try:
             return models.Actor.objects.get(url=v)
         except models.Actor.DoesNotExist:
-            raise serializers.ValidationError('Actor not found')
+            raise serializers.ValidationError("Actor not found")
 
     def validate(self, validated_data):
         # we ensure the accept actor actually match the follow target
-        if validated_data['actor'] != validated_data['object']['object']:
-            raise serializers.ValidationError('Actor mismatch')
+        if validated_data["actor"] != validated_data["object"]["object"]:
+            raise serializers.ValidationError("Actor mismatch")
         try:
-            validated_data['follow'] = models.Follow.objects.filter(
-                target=validated_data['actor'],
-                actor=validated_data['object']['actor']
-            ).exclude(approved=True).get()
+            validated_data["follow"] = (
+                models.Follow.objects.filter(
+                    target=validated_data["actor"],
+                    actor=validated_data["object"]["actor"],
+                )
+                .exclude(approved=True)
+                .get()
+            )
         except models.Follow.DoesNotExist:
-            raise serializers.ValidationError('No follow to accept')
+            raise serializers.ValidationError("No follow to accept")
         return validated_data
 
     def to_representation(self, instance):
         return {
             "@context": AP_CONTEXT,
-            "id": instance.get_federation_url() + '/accept',
+            "id": instance.get_federation_url() + "/accept",
             "type": "Accept",
             "actor": instance.target.url,
-            "object": FollowSerializer(instance).data
+            "object": FollowSerializer(instance).data,
         }
 
     def save(self):
-        self.validated_data['follow'].approved = True
-        self.validated_data['follow'].save()
-        return self.validated_data['follow']
+        self.validated_data["follow"].approved = True
+        self.validated_data["follow"].save()
+        return self.validated_data["follow"]
 
 
 class UndoFollowSerializer(serializers.Serializer):
     id = serializers.URLField(max_length=500)
     actor = serializers.URLField(max_length=500)
     object = FollowSerializer()
-    type = serializers.ChoiceField(choices=['Undo'])
+    type = serializers.ChoiceField(choices=["Undo"])
 
     def validate_actor(self, v):
-        expected = self.context.get('follow_target')
+        expected = self.context.get("follow_target")
         if expected and expected.url != v:
-            raise serializers.ValidationError('Invalid actor')
+            raise serializers.ValidationError("Invalid actor")
         try:
             return models.Actor.objects.get(url=v)
         except models.Actor.DoesNotExist:
-            raise serializers.ValidationError('Actor not found')
+            raise serializers.ValidationError("Actor not found")
 
     def validate(self, validated_data):
         # we ensure the accept actor actually match the follow actor
-        if validated_data['actor'] != validated_data['object']['actor']:
-            raise serializers.ValidationError('Actor mismatch')
+        if validated_data["actor"] != validated_data["object"]["actor"]:
+            raise serializers.ValidationError("Actor mismatch")
         try:
-            validated_data['follow'] = models.Follow.objects.filter(
-                actor=validated_data['actor'],
-                target=validated_data['object']['object']
+            validated_data["follow"] = models.Follow.objects.filter(
+                actor=validated_data["actor"], target=validated_data["object"]["object"]
             ).get()
         except models.Follow.DoesNotExist:
-            raise serializers.ValidationError('No follow to remove')
+            raise serializers.ValidationError("No follow to remove")
         return validated_data
 
     def to_representation(self, instance):
         return {
             "@context": AP_CONTEXT,
-            "id": instance.get_federation_url() + '/undo',
+            "id": instance.get_federation_url() + "/undo",
             "type": "Undo",
             "actor": instance.actor.url,
-            "object": FollowSerializer(instance).data
+            "object": FollowSerializer(instance).data,
         }
 
     def save(self):
-        return self.validated_data['follow'].delete()
+        return self.validated_data["follow"].delete()
 
 
 class ActorWebfingerSerializer(serializers.Serializer):
@@ -480,68 +458,59 @@ class ActorWebfingerSerializer(serializers.Serializer):
     actor_url = serializers.URLField(max_length=500, required=False)
 
     def validate(self, validated_data):
-        validated_data['actor_url'] = None
-        for l in validated_data['links']:
+        validated_data["actor_url"] = None
+        for l in validated_data["links"]:
             try:
-                if not l['rel'] == 'self':
+                if not l["rel"] == "self":
                     continue
-                if not l['type'] == 'application/activity+json':
+                if not l["type"] == "application/activity+json":
                     continue
-                validated_data['actor_url'] = l['href']
+                validated_data["actor_url"] = l["href"]
                 break
             except KeyError:
                 pass
-        if validated_data['actor_url'] is None:
-            raise serializers.ValidationError('No valid actor url found')
+        if validated_data["actor_url"] is None:
+            raise serializers.ValidationError("No valid actor url found")
         return validated_data
 
     def to_representation(self, instance):
         data = {}
-        data['subject'] = 'acct:{}'.format(instance.webfinger_subject)
-        data['links'] = [
-            {
-                'rel': 'self',
-                'href': instance.url,
-                'type': 'application/activity+json'
-            }
-        ]
-        data['aliases'] = [
-            instance.url
+        data["subject"] = "acct:{}".format(instance.webfinger_subject)
+        data["links"] = [
+            {"rel": "self", "href": instance.url, "type": "application/activity+json"}
         ]
+        data["aliases"] = [instance.url]
         return data
 
 
 class ActivitySerializer(serializers.Serializer):
     actor = serializers.URLField(max_length=500)
     id = serializers.URLField(max_length=500, required=False)
-    type = serializers.ChoiceField(
-        choices=[(c, c) for c in activity.ACTIVITY_TYPES])
+    type = serializers.ChoiceField(choices=[(c, c) for c in activity.ACTIVITY_TYPES])
     object = serializers.JSONField()
 
     def validate_object(self, value):
         try:
-            type = value['type']
+            type = value["type"]
         except KeyError:
-            raise serializers.ValidationError('Missing object type')
+            raise serializers.ValidationError("Missing object type")
         except TypeError:
             # probably a URL
             return value
         try:
             object_serializer = OBJECT_SERIALIZERS[type]
         except KeyError:
-            raise serializers.ValidationError(
-                'Unsupported type {}'.format(type))
+            raise serializers.ValidationError("Unsupported type {}".format(type))
 
         serializer = object_serializer(data=value)
         serializer.is_valid(raise_exception=True)
         return serializer.data
 
     def validate_actor(self, value):
-        request_actor = self.context.get('actor')
+        request_actor = self.context.get("actor")
         if request_actor and request_actor.url != value:
             raise serializers.ValidationError(
-                'The actor making the request do not match'
-                ' the activity actor'
+                "The actor making the request do not match" " the activity actor"
             )
         return value
 
@@ -549,47 +518,39 @@ class ActivitySerializer(serializers.Serializer):
         d = {}
         d.update(conf)
 
-        if self.context.get('include_ap_context', True):
-            d['@context'] = AP_CONTEXT
+        if self.context.get("include_ap_context", True):
+            d["@context"] = AP_CONTEXT
         return d
 
 
 class ObjectSerializer(serializers.Serializer):
     id = serializers.URLField(max_length=500)
     url = serializers.URLField(max_length=500, required=False, allow_null=True)
-    type = serializers.ChoiceField(
-        choices=[(c, c) for c in activity.OBJECT_TYPES])
-    content = serializers.CharField(
-        required=False, allow_null=True)
-    summary = serializers.CharField(
-        required=False, allow_null=True)
-    name = serializers.CharField(
-        required=False, allow_null=True)
-    published = serializers.DateTimeField(
-        required=False, allow_null=True)
-    updated = serializers.DateTimeField(
-        required=False, allow_null=True)
+    type = serializers.ChoiceField(choices=[(c, c) for c in activity.OBJECT_TYPES])
+    content = serializers.CharField(required=False, allow_null=True)
+    summary = serializers.CharField(required=False, allow_null=True)
+    name = serializers.CharField(required=False, allow_null=True)
+    published = serializers.DateTimeField(required=False, allow_null=True)
+    updated = serializers.DateTimeField(required=False, allow_null=True)
     to = serializers.ListField(
-        child=serializers.URLField(max_length=500),
-        required=False, allow_null=True)
+        child=serializers.URLField(max_length=500), required=False, allow_null=True
+    )
     cc = serializers.ListField(
-        child=serializers.URLField(max_length=500),
-        required=False, allow_null=True)
+        child=serializers.URLField(max_length=500), required=False, allow_null=True
+    )
     bto = serializers.ListField(
-        child=serializers.URLField(max_length=500),
-        required=False, allow_null=True)
+        child=serializers.URLField(max_length=500), required=False, allow_null=True
+    )
     bcc = serializers.ListField(
-        child=serializers.URLField(max_length=500),
-        required=False, allow_null=True)
+        child=serializers.URLField(max_length=500), required=False, allow_null=True
+    )
+
 
-OBJECT_SERIALIZERS = {
-    t: ObjectSerializer
-    for t in activity.OBJECT_TYPES
-}
+OBJECT_SERIALIZERS = {t: ObjectSerializer for t in activity.OBJECT_TYPES}
 
 
 class PaginatedCollectionSerializer(serializers.Serializer):
-    type = serializers.ChoiceField(choices=['Collection'])
+    type = serializers.ChoiceField(choices=["Collection"])
     totalItems = serializers.IntegerField(min_value=0)
     actor = serializers.URLField(max_length=500)
     id = serializers.URLField(max_length=500)
@@ -597,30 +558,26 @@ class PaginatedCollectionSerializer(serializers.Serializer):
     last = serializers.URLField(max_length=500)
 
     def to_representation(self, conf):
-        paginator = Paginator(
-            conf['items'],
-            conf.get('page_size', 20)
-        )
-        first = funkwhale_utils.set_query_parameter(conf['id'], page=1)
+        paginator = Paginator(conf["items"], conf.get("page_size", 20))
+        first = funkwhale_utils.set_query_parameter(conf["id"], page=1)
         current = first
-        last = funkwhale_utils.set_query_parameter(
-            conf['id'], page=paginator.num_pages)
+        last = funkwhale_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
         d = {
-            'id': conf['id'],
-            'actor': conf['actor'].url,
-            'totalItems': paginator.count,
-            'type': 'Collection',
-            'current': current,
-            'first': first,
-            'last': last,
+            "id": conf["id"],
+            "actor": conf["actor"].url,
+            "totalItems": paginator.count,
+            "type": "Collection",
+            "current": current,
+            "first": first,
+            "last": last,
         }
-        if self.context.get('include_ap_context', True):
-            d['@context'] = AP_CONTEXT
+        if self.context.get("include_ap_context", True):
+            d["@context"] = AP_CONTEXT
         return d
 
 
 class CollectionPageSerializer(serializers.Serializer):
-    type = serializers.ChoiceField(choices=['CollectionPage'])
+    type = serializers.ChoiceField(choices=["CollectionPage"])
     totalItems = serializers.IntegerField(min_value=0)
     items = serializers.ListField()
     actor = serializers.URLField(max_length=500)
@@ -632,7 +589,7 @@ class CollectionPageSerializer(serializers.Serializer):
     partOf = serializers.URLField(max_length=500)
 
     def validate_items(self, v):
-        item_serializer = self.context.get('item_serializer')
+        item_serializer = self.context.get("item_serializer")
         if not item_serializer:
             return v
         raw_items = [item_serializer(data=i, context=self.context) for i in v]
@@ -641,47 +598,45 @@ class CollectionPageSerializer(serializers.Serializer):
             if i.is_valid():
                 valid_items.append(i)
             else:
-                logger.debug('Invalid item %s: %s', i.data, i.errors)
+                logger.debug("Invalid item %s: %s", i.data, i.errors)
 
         return valid_items
 
     def to_representation(self, conf):
-        page = conf['page']
-        first = funkwhale_utils.set_query_parameter(
-            conf['id'], page=1)
+        page = conf["page"]
+        first = funkwhale_utils.set_query_parameter(conf["id"], page=1)
         last = funkwhale_utils.set_query_parameter(
-            conf['id'], page=page.paginator.num_pages)
-        id = funkwhale_utils.set_query_parameter(
-            conf['id'], page=page.number)
+            conf["id"], page=page.paginator.num_pages
+        )
+        id = funkwhale_utils.set_query_parameter(conf["id"], page=page.number)
         d = {
-            'id': id,
-            'partOf': conf['id'],
-            'actor': conf['actor'].url,
-            'totalItems': page.paginator.count,
-            'type': 'CollectionPage',
-            'first': first,
-            'last': last,
-            'items': [
-                conf['item_serializer'](
-                    i,
-                    context={
-                        'actor': conf['actor'],
-                        'include_ap_context': False}
+            "id": id,
+            "partOf": conf["id"],
+            "actor": conf["actor"].url,
+            "totalItems": page.paginator.count,
+            "type": "CollectionPage",
+            "first": first,
+            "last": last,
+            "items": [
+                conf["item_serializer"](
+                    i, context={"actor": conf["actor"], "include_ap_context": False}
                 ).data
                 for i in page.object_list
-            ]
+            ],
         }
 
         if page.has_previous():
-            d['prev'] = funkwhale_utils.set_query_parameter(
-                conf['id'], page=page.previous_page_number())
+            d["prev"] = funkwhale_utils.set_query_parameter(
+                conf["id"], page=page.previous_page_number()
+            )
 
         if page.has_next():
-            d['next'] = funkwhale_utils.set_query_parameter(
-                conf['id'], page=page.next_page_number())
+            d["next"] = funkwhale_utils.set_query_parameter(
+                conf["id"], page=page.next_page_number()
+            )
 
-        if self.context.get('include_ap_context', True):
-            d['@context'] = AP_CONTEXT
+        if self.context.get("include_ap_context", True):
+            d["@context"] = AP_CONTEXT
         return d
 
 
@@ -704,12 +659,9 @@ class AudioMetadataSerializer(serializers.Serializer):
     artist = ArtistMetadataSerializer()
     release = ReleaseMetadataSerializer()
     recording = RecordingMetadataSerializer()
-    bitrate = serializers.IntegerField(
-        required=False, allow_null=True, min_value=0)
-    size = serializers.IntegerField(
-        required=False, allow_null=True, min_value=0)
-    length = serializers.IntegerField(
-        required=False, allow_null=True, min_value=0)
+    bitrate = serializers.IntegerField(required=False, allow_null=True, min_value=0)
+    size = serializers.IntegerField(required=False, allow_null=True, min_value=0)
+    length = serializers.IntegerField(required=False, allow_null=True, min_value=0)
 
 
 class AudioSerializer(serializers.Serializer):
@@ -721,41 +673,39 @@ class AudioSerializer(serializers.Serializer):
     metadata = AudioMetadataSerializer()
 
     def validate_type(self, v):
-        if v != 'Audio':
-            raise serializers.ValidationError('Invalid type for audio')
+        if v != "Audio":
+            raise serializers.ValidationError("Invalid type for audio")
         return v
 
     def validate_url(self, v):
         try:
-            url = v['href']
+            v["href"]
         except (KeyError, TypeError):
-            raise serializers.ValidationError('Missing href')
+            raise serializers.ValidationError("Missing href")
 
         try:
-            media_type = v['mediaType']
+            media_type = v["mediaType"]
         except (KeyError, TypeError):
-            raise serializers.ValidationError('Missing mediaType')
+            raise serializers.ValidationError("Missing mediaType")
 
-        if not media_type or not media_type.startswith('audio/'):
-            raise serializers.ValidationError('Invalid mediaType')
+        if not media_type or not media_type.startswith("audio/"):
+            raise serializers.ValidationError("Invalid mediaType")
 
         return v
 
     def create(self, validated_data):
         defaults = {
-            'audio_mimetype': validated_data['url']['mediaType'],
-            'audio_url': validated_data['url']['href'],
-            'metadata': validated_data['metadata'],
-            'artist_name': validated_data['metadata']['artist']['name'],
-            'album_title': validated_data['metadata']['release']['title'],
-            'title': validated_data['metadata']['recording']['title'],
-            'published_date': validated_data['published'],
-            'modification_date': validated_data.get('updated'),
+            "audio_mimetype": validated_data["url"]["mediaType"],
+            "audio_url": validated_data["url"]["href"],
+            "metadata": validated_data["metadata"],
+            "artist_name": validated_data["metadata"]["artist"]["name"],
+            "album_title": validated_data["metadata"]["release"]["title"],
+            "title": validated_data["metadata"]["recording"]["title"],
+            "published_date": validated_data["published"],
+            "modification_date": validated_data.get("updated"),
         }
         return models.LibraryTrack.objects.get_or_create(
-            library=self.context['library'],
-            url=validated_data['id'],
-            defaults=defaults
+            library=self.context["library"], url=validated_data["id"], defaults=defaults
         )[0]
 
     def to_representation(self, instance):
@@ -764,87 +714,77 @@ class AudioSerializer(serializers.Serializer):
         artist = instance.track.artist
 
         d = {
-            'type': 'Audio',
-            'id': instance.get_federation_url(),
-            'name': instance.track.full_name,
-            'published': instance.creation_date.isoformat(),
-            'updated': instance.modification_date.isoformat(),
-            'metadata': {
-                'artist': {
-                    'musicbrainz_id': str(artist.mbid) if artist.mbid else None,
-                    'name': artist.name,
+            "type": "Audio",
+            "id": instance.get_federation_url(),
+            "name": instance.track.full_name,
+            "published": instance.creation_date.isoformat(),
+            "updated": instance.modification_date.isoformat(),
+            "metadata": {
+                "artist": {
+                    "musicbrainz_id": str(artist.mbid) if artist.mbid else None,
+                    "name": artist.name,
                 },
-                'release': {
-                    'musicbrainz_id': str(album.mbid) if album.mbid else None,
-                    'title': album.title,
+                "release": {
+                    "musicbrainz_id": str(album.mbid) if album.mbid else None,
+                    "title": album.title,
                 },
-                'recording': {
-                    'musicbrainz_id': str(track.mbid) if track.mbid else None,
-                    'title': track.title,
+                "recording": {
+                    "musicbrainz_id": str(track.mbid) if track.mbid else None,
+                    "title": track.title,
                 },
-                'bitrate': instance.bitrate,
-                'size': instance.size,
-                'length': instance.duration,
+                "bitrate": instance.bitrate,
+                "size": instance.size,
+                "length": instance.duration,
             },
-            'url': {
-                'href': utils.full_url(instance.path),
-                'type': 'Link',
-                'mediaType': instance.mimetype
+            "url": {
+                "href": utils.full_url(instance.path),
+                "type": "Link",
+                "mediaType": instance.mimetype,
             },
-            'attributedTo': [
-                self.context['actor'].url
-            ]
+            "attributedTo": [self.context["actor"].url],
         }
-        if self.context.get('include_ap_context', True):
-            d['@context'] = AP_CONTEXT
+        if self.context.get("include_ap_context", True):
+            d["@context"] = AP_CONTEXT
         return d
 
 
 class CollectionSerializer(serializers.Serializer):
-
     def to_representation(self, conf):
         d = {
-            'id': conf['id'],
-            'actor': conf['actor'].url,
-            'totalItems': len(conf['items']),
-            'type': 'Collection',
-            'items': [
-                conf['item_serializer'](
-                    i,
-                    context={
-                        'actor': conf['actor'],
-                        'include_ap_context': False}
+            "id": conf["id"],
+            "actor": conf["actor"].url,
+            "totalItems": len(conf["items"]),
+            "type": "Collection",
+            "items": [
+                conf["item_serializer"](
+                    i, context={"actor": conf["actor"], "include_ap_context": False}
                 ).data
-                for i in conf['items']
-            ]
+                for i in conf["items"]
+            ],
         }
 
-        if self.context.get('include_ap_context', True):
-            d['@context'] = AP_CONTEXT
+        if self.context.get("include_ap_context", True):
+            d["@context"] = AP_CONTEXT
         return d
 
 
 class LibraryTrackActionSerializer(common_serializers.ActionSerializer):
-    actions = ['import']
+    actions = ["import"]
     filterset_class = filters.LibraryTrackFilter
 
     @transaction.atomic
     def handle_import(self, objects):
         batch = music_models.ImportBatch.objects.create(
-            source='federation',
-            submitted_by=self.context['submitted_by']
+            source="federation", submitted_by=self.context["submitted_by"]
         )
         jobs = []
         for lt in objects:
             job = music_models.ImportJob(
-                batch=batch,
-                library_track=lt,
-                mbid=lt.mbid,
-                source=lt.url,
+                batch=batch, library_track=lt, mbid=lt.mbid, source=lt.url
             )
             jobs.append(job)
 
         music_models.ImportJob.objects.bulk_create(jobs)
         music_tasks.import_batch_run.delay(import_batch_id=batch.pk)
 
-        return {'batch': {'id': batch.pk}}
+        return {"batch": {"id": batch.pk}}
diff --git a/api/funkwhale_api/federation/signing.py b/api/funkwhale_api/federation/signing.py
index 8d984d3ffd01cdbdcf8e66dabdeb4065bd3435bf..15525b3e513b2ccc861cbf34b693b30aa0850d29 100644
--- a/api/funkwhale_api/federation/signing.py
+++ b/api/funkwhale_api/federation/signing.py
@@ -1,18 +1,16 @@
 import logging
+
 import requests
 import requests_http_signature
 
-from . import exceptions
-from . import utils
+from . import exceptions, utils
 
 logger = logging.getLogger(__name__)
 
 
 def verify(request, public_key):
     return requests_http_signature.HTTPSignatureAuth.verify(
-        request,
-        key_resolver=lambda **kwargs: public_key,
-        use_auth_header=False,
+        request, key_resolver=lambda **kwargs: public_key, use_auth_header=False
     )
 
 
@@ -27,44 +25,37 @@ def verify_django(django_request, public_key):
         # with requests_http_signature
         headers[h.lower()] = v
     try:
-        signature = headers['Signature']
+        signature = headers["Signature"]
     except KeyError:
         raise exceptions.MissingSignature
-    url = 'http://noop{}'.format(django_request.path)
-    query = django_request.META['QUERY_STRING']
+    url = "http://noop{}".format(django_request.path)
+    query = django_request.META["QUERY_STRING"]
     if query:
-        url += '?{}'.format(query)
+        url += "?{}".format(query)
     signature_headers = signature.split('headers="')[1].split('",')[0]
-    expected = signature_headers.split(' ')
-    logger.debug('Signature expected headers: %s', expected)
+    expected = signature_headers.split(" ")
+    logger.debug("Signature expected headers: %s", expected)
     for header in expected:
         try:
             headers[header]
         except KeyError:
-            logger.debug('Missing header: %s', header)
+            logger.debug("Missing header: %s", header)
     request = requests.Request(
-        method=django_request.method,
-        url=url,
-        data=django_request.body,
-        headers=headers)
+        method=django_request.method, url=url, data=django_request.body, headers=headers
+    )
     for h in request.headers.keys():
         v = request.headers[h]
         if v:
             request.headers[h] = str(v)
-    prepared_request = request.prepare()
+    request.prepare()
     return verify(request, public_key)
 
 
 def get_auth(private_key, private_key_id):
     return requests_http_signature.HTTPSignatureAuth(
         use_auth_header=False,
-        headers=[
-            '(request-target)',
-            'user-agent',
-            'host',
-            'date',
-            'content-type'],
-        algorithm='rsa-sha256',
-        key=private_key.encode('utf-8'),
+        headers=["(request-target)", "user-agent", "host", "date", "content-type"],
+        algorithm="rsa-sha256",
+        key=private_key.encode("utf-8"),
         key_id=private_key_id,
     )
diff --git a/api/funkwhale_api/federation/tasks.py b/api/funkwhale_api/federation/tasks.py
index 8f931b0ed741ff13017475c1ac47411ff67e5df0..d1b5b7bd21b4a588008bf531003c25e666cd3313 100644
--- a/api/funkwhale_api/federation/tasks.py
+++ b/api/funkwhale_api/federation/tasks.py
@@ -6,114 +6,114 @@ import os
 from django.conf import settings
 from django.db.models import Q
 from django.utils import timezone
-
-from requests.exceptions import RequestException
 from dynamic_preferences.registries import global_preferences_registry
+from requests.exceptions import RequestException
 
 from funkwhale_api.common import session
-from funkwhale_api.history.models import Listening
 from funkwhale_api.taskapp import celery
 
 from . import actors
 from . import library as lb
-from . import models
-from . import signing
-
+from . import models, signing
 
 logger = logging.getLogger(__name__)
 
 
 @celery.app.task(
-    name='federation.send',
+    name="federation.send",
     autoretry_for=[RequestException],
     retry_backoff=30,
-    max_retries=5)
-@celery.require_instance(models.Actor, 'actor')
+    max_retries=5,
+)
+@celery.require_instance(models.Actor, "actor")
 def send(activity, actor, to):
-    logger.info('Preparing activity delivery to %s', to)
-    auth = signing.get_auth(
-        actor.private_key, actor.private_key_id)
+    logger.info("Preparing activity delivery to %s", to)
+    auth = signing.get_auth(actor.private_key, actor.private_key_id)
     for url in to:
         recipient_actor = actors.get_actor(url)
-        logger.debug('delivering to %s', recipient_actor.inbox_url)
-        logger.debug('activity content: %s', json.dumps(activity))
+        logger.debug("delivering to %s", recipient_actor.inbox_url)
+        logger.debug("activity content: %s", json.dumps(activity))
         response = session.get_session().post(
             auth=auth,
             json=activity,
             url=recipient_actor.inbox_url,
             timeout=5,
             verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
-            headers={
-                'Content-Type': 'application/activity+json'
-            }
+            headers={"Content-Type": "application/activity+json"},
         )
         response.raise_for_status()
-        logger.debug('Remote answered with %s', response.status_code)
+        logger.debug("Remote answered with %s", response.status_code)
 
 
 @celery.app.task(
-    name='federation.scan_library',
+    name="federation.scan_library",
     autoretry_for=[RequestException],
     retry_backoff=30,
-    max_retries=5)
-@celery.require_instance(models.Library, 'library')
+    max_retries=5,
+)
+@celery.require_instance(models.Library, "library")
 def scan_library(library, until=None):
     if not library.federation_enabled:
         return
 
     data = lb.get_library_data(library.url)
-    scan_library_page.delay(
-        library_id=library.id, page_url=data['first'], until=until)
+    scan_library_page.delay(library_id=library.id, page_url=data["first"], until=until)
     library.fetched_date = timezone.now()
-    library.tracks_count = data['totalItems']
-    library.save(update_fields=['fetched_date', 'tracks_count'])
+    library.tracks_count = data["totalItems"]
+    library.save(update_fields=["fetched_date", "tracks_count"])
 
 
 @celery.app.task(
-    name='federation.scan_library_page',
+    name="federation.scan_library_page",
     autoretry_for=[RequestException],
     retry_backoff=30,
-    max_retries=5)
-@celery.require_instance(models.Library, 'library')
+    max_retries=5,
+)
+@celery.require_instance(models.Library, "library")
 def scan_library_page(library, page_url, until=None):
     if not library.federation_enabled:
         return
 
     data = lb.get_library_page(library, page_url)
     lts = []
-    for item_serializer in data['items']:
-        item_date = item_serializer.validated_data['published']
+    for item_serializer in data["items"]:
+        item_date = item_serializer.validated_data["published"]
         if until and item_date < until:
             return
         lts.append(item_serializer.save())
 
-    next_page = data.get('next')
+    next_page = data.get("next")
     if next_page and next_page != page_url:
         scan_library_page.delay(library_id=library.id, page_url=next_page)
 
 
-@celery.app.task(name='federation.clean_music_cache')
+@celery.app.task(name="federation.clean_music_cache")
 def clean_music_cache():
     preferences = global_preferences_registry.manager()
-    delay = preferences['federation__music_cache_duration']
+    delay = preferences["federation__music_cache_duration"]
     if delay < 1:
         return  # cache clearing disabled
     limit = timezone.now() - datetime.timedelta(minutes=delay)
 
-    candidates = models.LibraryTrack.objects.filter(
-        Q(audio_file__isnull=False) & (
-            Q(local_track_file__accessed_date__lt=limit) |
-            Q(local_track_file__accessed_date=None)
+    candidates = (
+        models.LibraryTrack.objects.filter(
+            Q(audio_file__isnull=False)
+            & (
+                Q(local_track_file__accessed_date__lt=limit)
+                | Q(local_track_file__accessed_date=None)
+            )
         )
-    ).exclude(audio_file='').only('audio_file', 'id')
+        .exclude(audio_file="")
+        .only("audio_file", "id")
+    )
     for lt in candidates:
         lt.audio_file.delete()
 
     # we also delete orphaned files, if any
-    storage = models.LibraryTrack._meta.get_field('audio_file').storage
-    files = get_files(storage, 'federation_cache')
+    storage = models.LibraryTrack._meta.get_field("audio_file").storage
+    files = get_files(storage, "federation_cache")
     existing = models.LibraryTrack.objects.filter(audio_file__in=files)
-    missing = set(files) - set(existing.values_list('audio_file', flat=True))
+    missing = set(files) - set(existing.values_list("audio_file", flat=True))
     for m in missing:
         storage.delete(m)
 
@@ -124,12 +124,9 @@ def get_files(storage, *parts):
     in a given directory using django's storage.
     """
     if not parts:
-        raise ValueError('Missing path')
+        raise ValueError("Missing path")
 
     dirs, files = storage.listdir(os.path.join(*parts))
     for dir in dirs:
         files += get_files(storage, *(list(parts) + [dir]))
-    return [
-        os.path.join(parts[-1], path)
-        for path in files
-    ]
+    return [os.path.join(parts[-1], path) for path in files]
diff --git a/api/funkwhale_api/federation/urls.py b/api/funkwhale_api/federation/urls.py
index 2c24b5257e1937ecd8e82f13766bee238705f2ca..2594f554992779a5eef5c286fee4ba96e3f6393f 100644
--- a/api/funkwhale_api/federation/urls.py
+++ b/api/funkwhale_api/federation/urls.py
@@ -1,24 +1,16 @@
 from django.conf.urls import include, url
-
 from rest_framework import routers
+
 from . import views
 
 router = routers.SimpleRouter(trailing_slash=False)
 music_router = routers.SimpleRouter(trailing_slash=False)
 router.register(
-    r'federation/instance/actors',
-    views.InstanceActorViewSet,
-    'instance-actors')
-router.register(
-    r'.well-known',
-    views.WellKnownViewSet,
-    'well-known')
-
-music_router.register(
-    r'files',
-    views.MusicFilesViewSet,
-    'files',
+    r"federation/instance/actors", views.InstanceActorViewSet, "instance-actors"
 )
+router.register(r".well-known", views.WellKnownViewSet, "well-known")
+
+music_router.register(r"files", views.MusicFilesViewSet, "files")
 urlpatterns = router.urls + [
-    url('federation/music/', include((music_router.urls, 'music'), namespace='music'))
+    url("federation/music/", include((music_router.urls, "music"), namespace="music"))
 ]
diff --git a/api/funkwhale_api/federation/utils.py b/api/funkwhale_api/federation/utils.py
index df093add8f934bbaabae8f6e9e68712f3762c53d..e09870223dedf596e220b49a665f84fe99acb6c6 100644
--- a/api/funkwhale_api/federation/utils.py
+++ b/api/funkwhale_api/federation/utils.py
@@ -6,10 +6,10 @@ def full_url(path):
     Given a relative path, return a full url usable for federation purpose
     """
     root = settings.FUNKWHALE_URL
-    if path.startswith('/') and root.endswith('/'):
+    if path.startswith("/") and root.endswith("/"):
         return root + path[1:]
-    elif not path.startswith('/') and not root.endswith('/'):
-        return root + '/' + path
+    elif not path.startswith("/") and not root.endswith("/"):
+        return root + "/" + path
     else:
         return root + path
 
@@ -19,17 +19,14 @@ def clean_wsgi_headers(raw_headers):
     Convert WSGI headers from CONTENT_TYPE to Content-Type notation
     """
     cleaned = {}
-    non_prefixed = [
-        'content_type',
-        'content_length',
-    ]
+    non_prefixed = ["content_type", "content_length"]
     for raw_header, value in raw_headers.items():
         h = raw_header.lower()
-        if not h.startswith('http_') and h not in non_prefixed:
+        if not h.startswith("http_") and h not in non_prefixed:
             continue
 
-        words = h.replace('http_', '', 1).split('_')
-        cleaned_header = '-'.join([w.capitalize() for w in words])
+        words = h.replace("http_", "", 1).split("_")
+        cleaned_header = "-".join([w.capitalize() for w in words])
         cleaned[cleaned_header] = value
 
     return cleaned
diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py
index 1350ec731ece68010cc907191d6412ad425fdce3..63a1d7b7126d22138c50334d6305c7d03ae9495a 100644
--- a/api/funkwhale_api/federation/views.py
+++ b/api/funkwhale_api/federation/views.py
@@ -1,55 +1,47 @@
 from django import forms
-from django.conf import settings
 from django.core import paginator
 from django.db import transaction
-from django.http import HttpResponse
+from django.http import HttpResponse, Http404
 from django.urls import reverse
-
-from rest_framework import mixins
-from rest_framework import permissions as rest_permissions
-from rest_framework import response
-from rest_framework import views
-from rest_framework import viewsets
-from rest_framework.decorators import list_route, detail_route
-from rest_framework.serializers import ValidationError
+from rest_framework import mixins, response, viewsets
+from rest_framework.decorators import detail_route, list_route
 
 from funkwhale_api.common import preferences
-from funkwhale_api.common import utils as funkwhale_utils
 from funkwhale_api.music import models as music_models
 from funkwhale_api.users.permissions import HasUserPermission
 
-from . import activity
-from . import actors
-from . import authentication
-from . import filters
-from . import library
-from . import models
-from . import permissions
-from . import renderers
-from . import serializers
-from . import tasks
-from . import utils
-from . import webfinger
+from . import (
+    actors,
+    authentication,
+    filters,
+    library,
+    models,
+    permissions,
+    renderers,
+    serializers,
+    tasks,
+    utils,
+    webfinger,
+)
 
 
 class FederationMixin(object):
     def dispatch(self, request, *args, **kwargs):
-        if not preferences.get('federation__enabled'):
+        if not preferences.get("federation__enabled"):
             return HttpResponse(status=405)
         return super().dispatch(request, *args, **kwargs)
 
 
 class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
-    lookup_field = 'actor'
-    lookup_value_regex = '[a-z]*'
-    authentication_classes = [
-        authentication.SignatureAuthentication]
+    lookup_field = "actor"
+    lookup_value_regex = "[a-z]*"
+    authentication_classes = [authentication.SignatureAuthentication]
     permission_classes = []
     renderer_classes = [renderers.ActivityPubRenderer]
 
     def get_object(self):
         try:
-            return actors.SYSTEM_ACTORS[self.kwargs['actor']]
+            return actors.SYSTEM_ACTORS[self.kwargs["actor"]]
         except KeyError:
             raise Http404
 
@@ -59,27 +51,23 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
         data = actor.system_conf.serialize()
         return response.Response(data, status=200)
 
-    @detail_route(methods=['get', 'post'])
+    @detail_route(methods=["get", "post"])
     def inbox(self, request, *args, **kwargs):
         system_actor = self.get_object()
-        handler = getattr(system_actor, '{}_inbox'.format(
-            request.method.lower()
-        ))
+        handler = getattr(system_actor, "{}_inbox".format(request.method.lower()))
 
         try:
-            data = handler(request.data, actor=request.actor)
+            handler(request.data, actor=request.actor)
         except NotImplementedError:
             return response.Response(status=405)
         return response.Response({}, status=200)
 
-    @detail_route(methods=['get', 'post'])
+    @detail_route(methods=["get", "post"])
     def outbox(self, request, *args, **kwargs):
         system_actor = self.get_object()
-        handler = getattr(system_actor, '{}_outbox'.format(
-            request.method.lower()
-        ))
+        handler = getattr(system_actor, "{}_outbox".format(request.method.lower()))
         try:
-            data = handler(request.data, actor=request.actor)
+            handler(request.data, actor=request.actor)
         except NotImplementedError:
             return response.Response(status=405)
         return response.Response({}, status=200)
@@ -90,45 +78,36 @@ class WellKnownViewSet(viewsets.GenericViewSet):
     permission_classes = []
     renderer_classes = [renderers.JSONRenderer, renderers.WebfingerRenderer]
 
-    @list_route(methods=['get'])
+    @list_route(methods=["get"])
     def nodeinfo(self, request, *args, **kwargs):
-        if not preferences.get('instance__nodeinfo_enabled'):
+        if not preferences.get("instance__nodeinfo_enabled"):
             return HttpResponse(status=404)
         data = {
-            'links': [
+            "links": [
                 {
-                    'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0',
-                    'href': utils.full_url(
-                        reverse('api:v1:instance:nodeinfo-2.0')
-                    )
+                    "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
+                    "href": utils.full_url(reverse("api:v1:instance:nodeinfo-2.0")),
                 }
             ]
         }
         return response.Response(data)
 
-    @list_route(methods=['get'])
+    @list_route(methods=["get"])
     def webfinger(self, request, *args, **kwargs):
-        if not preferences.get('federation__enabled'):
+        if not preferences.get("federation__enabled"):
             return HttpResponse(status=405)
         try:
-            resource_type, resource = webfinger.clean_resource(
-                request.GET['resource'])
-            cleaner = getattr(webfinger, 'clean_{}'.format(resource_type))
+            resource_type, resource = webfinger.clean_resource(request.GET["resource"])
+            cleaner = getattr(webfinger, "clean_{}".format(resource_type))
             result = cleaner(resource)
         except forms.ValidationError as e:
-            return response.Response({
-                'errors': {
-                    'resource': e.message
-                }
-            }, status=400)
+            return response.Response({"errors": {"resource": e.message}}, status=400)
         except KeyError:
-            return response.Response({
-                'errors': {
-                    'resource': 'This field is required',
-                }
-            }, status=400)
+            return response.Response(
+                {"errors": {"resource": "This field is required"}}, status=400
+            )
 
-        handler = getattr(self, 'handler_{}'.format(resource_type))
+        handler = getattr(self, "handler_{}".format(resource_type))
         data = handler(result)
 
         return response.Response(data)
@@ -140,46 +119,43 @@ class WellKnownViewSet(viewsets.GenericViewSet):
 
 
 class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
-    authentication_classes = [
-        authentication.SignatureAuthentication]
+    authentication_classes = [authentication.SignatureAuthentication]
     permission_classes = [permissions.LibraryFollower]
     renderer_classes = [renderers.ActivityPubRenderer]
 
     def list(self, request, *args, **kwargs):
-        page = request.GET.get('page')
-        library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-        qs = music_models.TrackFile.objects.order_by(
-            '-creation_date'
-        ).select_related(
-            'track__artist',
-            'track__album__artist'
-        ).filter(library_track__isnull=True)
+        page = request.GET.get("page")
+        library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+        qs = (
+            music_models.TrackFile.objects.order_by("-creation_date")
+            .select_related("track__artist", "track__album__artist")
+            .filter(library_track__isnull=True)
+        )
         if page is None:
             conf = {
-                'id': utils.full_url(reverse('federation:music:files-list')),
-                'page_size': preferences.get(
-                    'federation__collection_page_size'),
-                'items': qs,
-                'item_serializer': serializers.AudioSerializer,
-                'actor': library,
+                "id": utils.full_url(reverse("federation:music:files-list")),
+                "page_size": preferences.get("federation__collection_page_size"),
+                "items": qs,
+                "item_serializer": serializers.AudioSerializer,
+                "actor": library,
             }
             serializer = serializers.PaginatedCollectionSerializer(conf)
             data = serializer.data
         else:
             try:
                 page_number = int(page)
-            except:
-                return response.Response(
-                    {'page': ['Invalid page number']}, status=400)
+            except Exception:
+                return response.Response({"page": ["Invalid page number"]}, status=400)
             p = paginator.Paginator(
-                qs, preferences.get('federation__collection_page_size'))
+                qs, preferences.get("federation__collection_page_size")
+            )
             try:
                 page = p.page(page_number)
                 conf = {
-                    'id': utils.full_url(reverse('federation:music:files-list')),
-                    'page': page,
-                    'item_serializer': serializers.AudioSerializer,
-                    'actor': library,
+                    "id": utils.full_url(reverse("federation:music:files-list")),
+                    "page": page,
+                    "item_serializer": serializers.AudioSerializer,
+                    "actor": library,
                 }
                 serializer = serializers.CollectionPageSerializer(conf)
                 data = serializer.data
@@ -190,134 +166,112 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
 
 
 class LibraryViewSet(
-        mixins.RetrieveModelMixin,
-        mixins.UpdateModelMixin,
-        mixins.ListModelMixin,
-        viewsets.GenericViewSet):
+    mixins.RetrieveModelMixin,
+    mixins.UpdateModelMixin,
+    mixins.ListModelMixin,
+    viewsets.GenericViewSet,
+):
     permission_classes = (HasUserPermission,)
-    required_permissions = ['federation']
-    queryset = models.Library.objects.all().select_related(
-        'actor',
-        'follow',
-    )
-    lookup_field = 'uuid'
+    required_permissions = ["federation"]
+    queryset = models.Library.objects.all().select_related("actor", "follow")
+    lookup_field = "uuid"
     filter_class = filters.LibraryFilter
     serializer_class = serializers.APILibrarySerializer
     ordering_fields = (
-        'id',
-        'creation_date',
-        'fetched_date',
-        'actor__domain',
-        'tracks_count',
+        "id",
+        "creation_date",
+        "fetched_date",
+        "actor__domain",
+        "tracks_count",
     )
 
-    @list_route(methods=['get'])
+    @list_route(methods=["get"])
     def fetch(self, request, *args, **kwargs):
-        account = request.GET.get('account')
+        account = request.GET.get("account")
         if not account:
-            return response.Response(
-                {'account': 'This field is mandatory'}, status=400)
+            return response.Response({"account": "This field is mandatory"}, status=400)
 
         data = library.scan_from_account_name(account)
         return response.Response(data)
 
-    @detail_route(methods=['post'])
+    @detail_route(methods=["post"])
     def scan(self, request, *args, **kwargs):
         library = self.get_object()
-        serializer = serializers.APILibraryScanSerializer(
-            data=request.data
-        )
+        serializer = serializers.APILibraryScanSerializer(data=request.data)
         serializer.is_valid(raise_exception=True)
         result = tasks.scan_library.delay(
-            library_id=library.pk,
-            until=serializer.validated_data.get('until')
+            library_id=library.pk, until=serializer.validated_data.get("until")
         )
-        return response.Response({'task': result.id})
+        return response.Response({"task": result.id})
 
-    @list_route(methods=['get'])
+    @list_route(methods=["get"])
     def following(self, request, *args, **kwargs):
-        library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-        queryset = models.Follow.objects.filter(
-            actor=library_actor
-        ).select_related(
-            'actor',
-            'target',
-        ).order_by('-creation_date')
+        library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+        queryset = (
+            models.Follow.objects.filter(actor=library_actor)
+            .select_related("actor", "target")
+            .order_by("-creation_date")
+        )
         filterset = filters.FollowFilter(request.GET, queryset=queryset)
         final_qs = filterset.qs
         serializer = serializers.APIFollowSerializer(final_qs, many=True)
-        data = {
-            'results': serializer.data,
-            'count': len(final_qs),
-        }
+        data = {"results": serializer.data, "count": len(final_qs)}
         return response.Response(data)
 
-    @list_route(methods=['get', 'patch'])
+    @list_route(methods=["get", "patch"])
     def followers(self, request, *args, **kwargs):
-        if request.method.lower() == 'patch':
-            serializer = serializers.APILibraryFollowUpdateSerializer(
-                data=request.data)
+        if request.method.lower() == "patch":
+            serializer = serializers.APILibraryFollowUpdateSerializer(data=request.data)
             serializer.is_valid(raise_exception=True)
             follow = serializer.save()
-            return response.Response(
-                serializers.APIFollowSerializer(follow).data
-            )
+            return response.Response(serializers.APIFollowSerializer(follow).data)
 
-        library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-        queryset = models.Follow.objects.filter(
-            target=library_actor
-        ).select_related(
-            'actor',
-            'target',
-        ).order_by('-creation_date')
+        library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+        queryset = (
+            models.Follow.objects.filter(target=library_actor)
+            .select_related("actor", "target")
+            .order_by("-creation_date")
+        )
         filterset = filters.FollowFilter(request.GET, queryset=queryset)
         final_qs = filterset.qs
         serializer = serializers.APIFollowSerializer(final_qs, many=True)
-        data = {
-            'results': serializer.data,
-            'count': len(final_qs),
-        }
+        data = {"results": serializer.data, "count": len(final_qs)}
         return response.Response(data)
 
     @transaction.atomic
     def create(self, request, *args, **kwargs):
         serializer = serializers.APILibraryCreateSerializer(data=request.data)
         serializer.is_valid(raise_exception=True)
-        library = serializer.save()
+        serializer.save()
         return response.Response(serializer.data, status=201)
 
 
-class LibraryTrackViewSet(
-        mixins.ListModelMixin,
-        viewsets.GenericViewSet):
+class LibraryTrackViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
     permission_classes = (HasUserPermission,)
-    required_permissions = ['federation']
-    queryset = models.LibraryTrack.objects.all().select_related(
-        'library__actor',
-        'library__follow',
-        'local_track_file',
-    ).prefetch_related('import_jobs')
+    required_permissions = ["federation"]
+    queryset = (
+        models.LibraryTrack.objects.all()
+        .select_related("library__actor", "library__follow", "local_track_file")
+        .prefetch_related("import_jobs")
+    )
     filter_class = filters.LibraryTrackFilter
     serializer_class = serializers.APILibraryTrackSerializer
     ordering_fields = (
-        'id',
-        'artist_name',
-        'title',
-        'album_title',
-        'creation_date',
-        'modification_date',
-        'fetched_date',
-        'published_date',
+        "id",
+        "artist_name",
+        "title",
+        "album_title",
+        "creation_date",
+        "modification_date",
+        "fetched_date",
+        "published_date",
     )
 
-    @list_route(methods=['post'])
+    @list_route(methods=["post"])
     def action(self, request, *args, **kwargs):
-        queryset = models.LibraryTrack.objects.filter(
-            local_track_file__isnull=True)
+        queryset = models.LibraryTrack.objects.filter(local_track_file__isnull=True)
         serializer = serializers.LibraryTrackActionSerializer(
-            request.data,
-            queryset=queryset,
-            context={'submitted_by': request.user}
+            request.data, queryset=queryset, context={"submitted_by": request.user}
         )
         serializer.is_valid(raise_exception=True)
         result = serializer.save()
diff --git a/api/funkwhale_api/federation/webfinger.py b/api/funkwhale_api/federation/webfinger.py
index f5cb996359fdfcd072d4715c06318230596692fa..b899fe20725ec929e8a81088e8e1600d9124678e 100644
--- a/api/funkwhale_api/federation/webfinger.py
+++ b/api/funkwhale_api/federation/webfinger.py
@@ -1,43 +1,39 @@
 from django import forms
 from django.conf import settings
-from django.urls import reverse
 
 from funkwhale_api.common import session
 
-from . import actors
-from . import utils
-from . import serializers
+from . import actors, serializers
 
-VALID_RESOURCE_TYPES = ['acct']
+VALID_RESOURCE_TYPES = ["acct"]
 
 
 def clean_resource(resource_string):
     if not resource_string:
-        raise forms.ValidationError('Invalid resource string')
+        raise forms.ValidationError("Invalid resource string")
 
     try:
-        resource_type, resource = resource_string.split(':', 1)
+        resource_type, resource = resource_string.split(":", 1)
     except ValueError:
-        raise forms.ValidationError('Missing webfinger resource type')
+        raise forms.ValidationError("Missing webfinger resource type")
 
     if resource_type not in VALID_RESOURCE_TYPES:
-        raise forms.ValidationError('Invalid webfinger resource type')
+        raise forms.ValidationError("Invalid webfinger resource type")
 
     return resource_type, resource
 
 
 def clean_acct(acct_string, ensure_local=True):
     try:
-        username, hostname = acct_string.split('@')
+        username, hostname = acct_string.split("@")
     except ValueError:
-        raise forms.ValidationError('Invalid format')
+        raise forms.ValidationError("Invalid format")
 
     if ensure_local and hostname.lower() != settings.FEDERATION_HOSTNAME:
-        raise forms.ValidationError(
-            'Invalid hostname {}'.format(hostname))
+        raise forms.ValidationError("Invalid hostname {}".format(hostname))
 
     if ensure_local and username not in actors.SYSTEM_ACTORS:
-        raise forms.ValidationError('Invalid username')
+        raise forms.ValidationError("Invalid username")
 
     return username, hostname
 
@@ -45,12 +41,12 @@ def clean_acct(acct_string, ensure_local=True):
 def get_resource(resource_string):
     resource_type, resource = clean_resource(resource_string)
     username, hostname = clean_acct(resource, ensure_local=False)
-    url = 'https://{}/.well-known/webfinger?resource={}'.format(
-        hostname, resource_string)
+    url = "https://{}/.well-known/webfinger?resource={}".format(
+        hostname, resource_string
+    )
     response = session.get_session().get(
-        url,
-        verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
-        timeout=5)
+        url, verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, timeout=5
+    )
     response.raise_for_status()
     serializer = serializers.ActorWebfingerSerializer(data=response.json())
     serializer.is_valid(raise_exception=True)
diff --git a/api/funkwhale_api/history/activities.py b/api/funkwhale_api/history/activities.py
index e478f9b7f677a4d4e524e343e84b0a08420bbcb3..b63de1f26fc7ccc256365d1b4aa63178956101da 100644
--- a/api/funkwhale_api/history/activities.py
+++ b/api/funkwhale_api/history/activities.py
@@ -1,19 +1,16 @@
-from funkwhale_api.common import channels
 from funkwhale_api.activity import record
+from funkwhale_api.common import channels
 
 from . import serializers
 
-record.registry.register_serializer(
-    serializers.ListeningActivitySerializer)
+record.registry.register_serializer(serializers.ListeningActivitySerializer)
 
 
-@record.registry.register_consumer('history.Listening')
+@record.registry.register_consumer("history.Listening")
 def broadcast_listening_to_instance_activity(data, obj):
-    if obj.user.privacy_level not in ['instance', 'everyone']:
+    if obj.user.privacy_level not in ["instance", "everyone"]:
         return
 
-    channels.group_send('instance_activity', {
-        'type': 'event.send',
-        'text': '',
-        'data': data
-    })
+    channels.group_send(
+        "instance_activity", {"type": "event.send", "text": "", "data": data}
+    )
diff --git a/api/funkwhale_api/history/admin.py b/api/funkwhale_api/history/admin.py
index 5ddfb899848f389d776632eaf9e3b6d389cd7f58..cbc7f89dd45e5cf57e082790acbf40b570d5f93b 100644
--- a/api/funkwhale_api/history/admin.py
+++ b/api/funkwhale_api/history/admin.py
@@ -2,11 +2,9 @@ from django.contrib import admin
 
 from . import models
 
+
 @admin.register(models.Listening)
 class ListeningAdmin(admin.ModelAdmin):
-    list_display = ['track', 'creation_date', 'user', 'session_key']
-    search_fields = ['track__name', 'user__username']
-    list_select_related = [
-        'user',
-        'track'
-    ]
+    list_display = ["track", "creation_date", "user", "session_key"]
+    search_fields = ["track__name", "user__username"]
+    list_select_related = ["user", "track"]
diff --git a/api/funkwhale_api/history/factories.py b/api/funkwhale_api/history/factories.py
index 86fea64d251c3a0228bc2a0c5edd5525e10cd7dd..0524eff1967d9b525e3c029b9cc6336eafdfcad0 100644
--- a/api/funkwhale_api/history/factories.py
+++ b/api/funkwhale_api/history/factories.py
@@ -11,4 +11,4 @@ class ListeningFactory(factory.django.DjangoModelFactory):
     track = factory.SubFactory(factories.TrackFactory)
 
     class Meta:
-        model = 'history.Listening'
+        model = "history.Listening"
diff --git a/api/funkwhale_api/history/migrations/0001_initial.py b/api/funkwhale_api/history/migrations/0001_initial.py
index 7b6f950edf38d474e5fa6488f2c42be8b3dbdb85..cd27772307e6e0d1c78bd135a3df7e68f589a014 100644
--- a/api/funkwhale_api/history/migrations/0001_initial.py
+++ b/api/funkwhale_api/history/migrations/0001_initial.py
@@ -9,22 +9,52 @@ import django.utils.timezone
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('music', '0008_auto_20160529_1456'),
+        ("music", "0008_auto_20160529_1456"),
         migrations.swappable_dependency(settings.AUTH_USER_MODEL),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='Listening',
+            name="Listening",
             fields=[
-                ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
-                ('end_date', models.DateTimeField(null=True, blank=True, default=django.utils.timezone.now)),
-                ('session_key', models.CharField(null=True, blank=True, max_length=100)),
-                ('track', models.ForeignKey(related_name='listenings', to='music.Track', on_delete=models.CASCADE)),
-                ('user', models.ForeignKey(blank=True, null=True, related_name='listenings', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
+                (
+                    "id",
+                    models.AutoField(
+                        verbose_name="ID",
+                        primary_key=True,
+                        serialize=False,
+                        auto_created=True,
+                    ),
+                ),
+                (
+                    "end_date",
+                    models.DateTimeField(
+                        null=True, blank=True, default=django.utils.timezone.now
+                    ),
+                ),
+                (
+                    "session_key",
+                    models.CharField(null=True, blank=True, max_length=100),
+                ),
+                (
+                    "track",
+                    models.ForeignKey(
+                        related_name="listenings",
+                        to="music.Track",
+                        on_delete=models.CASCADE,
+                    ),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        related_name="listenings",
+                        to=settings.AUTH_USER_MODEL,
+                        on_delete=models.CASCADE,
+                    ),
+                ),
             ],
-            options={
-                'ordering': ('-end_date',),
-            },
-        ),
+            options={"ordering": ("-end_date",)},
+        )
     ]
diff --git a/api/funkwhale_api/history/migrations/0002_auto_20180325_1433.py b/api/funkwhale_api/history/migrations/0002_auto_20180325_1433.py
index d83dbb0a466b668279619e53406b8ae977ab5dc7..efc02092557204f3afc57d907f5a8b783e23c861 100644
--- a/api/funkwhale_api/history/migrations/0002_auto_20180325_1433.py
+++ b/api/funkwhale_api/history/migrations/0002_auto_20180325_1433.py
@@ -5,18 +5,13 @@ from django.db import migrations
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('history', '0001_initial'),
-    ]
+    dependencies = [("history", "0001_initial")]
 
     operations = [
         migrations.AlterModelOptions(
-            name='listening',
-            options={'ordering': ('-creation_date',)},
+            name="listening", options={"ordering": ("-creation_date",)}
         ),
         migrations.RenameField(
-            model_name='listening',
-            old_name='end_date',
-            new_name='creation_date',
+            model_name="listening", old_name="end_date", new_name="creation_date"
         ),
     ]
diff --git a/api/funkwhale_api/history/models.py b/api/funkwhale_api/history/models.py
index 480461d35ed60fc0629e417879b6f75e27a95418..8da4e67cd5a508a6778886c1a5d0f898db4e115c 100644
--- a/api/funkwhale_api/history/models.py
+++ b/api/funkwhale_api/history/models.py
@@ -1,26 +1,25 @@
-from django.utils import timezone
 from django.db import models
-from django.core.exceptions import ValidationError
+from django.utils import timezone
 
 from funkwhale_api.music.models import Track
 
 
 class Listening(models.Model):
-    creation_date = models.DateTimeField(
-        default=timezone.now, null=True, blank=True)
+    creation_date = models.DateTimeField(default=timezone.now, null=True, blank=True)
     track = models.ForeignKey(
-        Track, related_name="listenings", on_delete=models.CASCADE)
+        Track, related_name="listenings", on_delete=models.CASCADE
+    )
     user = models.ForeignKey(
-        'users.User',
+        "users.User",
         related_name="listenings",
         null=True,
         blank=True,
-        on_delete=models.CASCADE)
+        on_delete=models.CASCADE,
+    )
     session_key = models.CharField(max_length=100, null=True, blank=True)
 
     class Meta:
-        ordering = ('-creation_date',)
+        ordering = ("-creation_date",)
 
     def get_activity_url(self):
-        return '{}/listenings/tracks/{}'.format(
-            self.user.get_activity_url(), self.pk)
+        return "{}/listenings/tracks/{}".format(self.user.get_activity_url(), self.pk)
diff --git a/api/funkwhale_api/history/serializers.py b/api/funkwhale_api/history/serializers.py
index 572787ae0031cb5be38c80014b7484f286de26e3..e493227988a7d608910db46d2ebd01b0a95a96db 100644
--- a/api/funkwhale_api/history/serializers.py
+++ b/api/funkwhale_api/history/serializers.py
@@ -9,35 +9,27 @@ from . import models
 
 class ListeningActivitySerializer(activity_serializers.ModelSerializer):
     type = serializers.SerializerMethodField()
-    object = TrackActivitySerializer(source='track')
-    actor = UserActivitySerializer(source='user')
-    published = serializers.DateTimeField(source='creation_date')
+    object = TrackActivitySerializer(source="track")
+    actor = UserActivitySerializer(source="user")
+    published = serializers.DateTimeField(source="creation_date")
 
     class Meta:
         model = models.Listening
-        fields = [
-            'id',
-            'local_id',
-            'object',
-            'type',
-            'actor',
-            'published'
-        ]
+        fields = ["id", "local_id", "object", "type", "actor", "published"]
 
     def get_actor(self, obj):
         return UserActivitySerializer(obj.user).data
 
     def get_type(self, obj):
-        return 'Listen'
+        return "Listen"
 
 
 class ListeningSerializer(serializers.ModelSerializer):
-
     class Meta:
         model = models.Listening
-        fields = ('id', 'user', 'track', 'creation_date')
+        fields = ("id", "user", "track", "creation_date")
 
     def create(self, validated_data):
-        validated_data['user'] = self.context['user']
+        validated_data["user"] = self.context["user"]
 
         return super().create(validated_data)
diff --git a/api/funkwhale_api/history/urls.py b/api/funkwhale_api/history/urls.py
index 6bd72a8a24f2cfd23ddf10f27191aa03e1c92ad6..707e95cd7d3056ced030a6f52b584456134c19e3 100644
--- a/api/funkwhale_api/history/urls.py
+++ b/api/funkwhale_api/history/urls.py
@@ -1,8 +1,8 @@
-from django.conf.urls import include, url
+from rest_framework import routers
+
 from . import views
 
-from rest_framework import routers
 router = routers.SimpleRouter()
-router.register(r'listenings', views.ListeningViewSet, 'listenings')
+router.register(r"listenings", views.ListeningViewSet, "listenings")
 
 urlpatterns = router.urls
diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py
index 3da8b2a38bed467ffe5fb83f7ea4c8d1d000c406..e104a2aa3dc44f3c538bc747df63a6e5d630f10d 100644
--- a/api/funkwhale_api/history/views.py
+++ b/api/funkwhale_api/history/views.py
@@ -1,20 +1,13 @@
-from rest_framework import generics, mixins, viewsets
-from rest_framework import permissions
-from rest_framework import status
-from rest_framework.response import Response
-from rest_framework.decorators import detail_route
+from rest_framework import mixins, permissions, viewsets
 
 from funkwhale_api.activity import record
-from funkwhale_api.common.permissions import ConditionalAuthentication
 
-from . import models
-from . import serializers
+from . import models, serializers
 
 
 class ListeningViewSet(
-        mixins.CreateModelMixin,
-        mixins.RetrieveModelMixin,
-        viewsets.GenericViewSet):
+    mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
+):
 
     serializer_class = serializers.ListeningSerializer
     queryset = models.Listening.objects.all()
@@ -31,5 +24,5 @@ class ListeningViewSet(
 
     def get_serializer_context(self):
         context = super().get_serializer_context()
-        context['user'] = self.request.user
+        context["user"] = self.request.user
         return context
diff --git a/api/funkwhale_api/instance/consumers.py b/api/funkwhale_api/instance/consumers.py
index eee5f7f0e38641bfd2d9c16fce49c8cd10d8c8a9..bb213a001330cd3e41cf51ed52b601c30faa29e8 100644
--- a/api/funkwhale_api/instance/consumers.py
+++ b/api/funkwhale_api/instance/consumers.py
@@ -5,4 +5,4 @@ class InstanceActivityConsumer(JsonAuthConsumer):
     groups = ["instance_activity"]
 
     def event_send(self, message):
-        self.send_json(message['data'])
+        self.send_json(message["data"])
diff --git a/api/funkwhale_api/instance/dynamic_preferences_registry.py b/api/funkwhale_api/instance/dynamic_preferences_registry.py
index 8ccf80dd93448b4f2512ec2109cda32beaa33bf7..0edb94482d0bb090ed15b79156b565e9ea33c1f7 100644
--- a/api/funkwhale_api/instance/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/instance/dynamic_preferences_registry.py
@@ -1,93 +1,84 @@
 from django.forms import widgets
-
 from dynamic_preferences import types
 from dynamic_preferences.registries import global_preferences_registry
 
-raven = types.Section('raven')
-instance = types.Section('instance')
+raven = types.Section("raven")
+instance = types.Section("instance")
 
 
 @global_preferences_registry.register
 class InstanceName(types.StringPreference):
     show_in_api = True
     section = instance
-    name = 'name'
-    default = ''
-    verbose_name = 'Public name'
-    help_text = 'The public name of your instance, displayed in the about page.'
-    field_kwargs = {
-        'required': False,
-    }
+    name = "name"
+    default = ""
+    verbose_name = "Public name"
+    help_text = "The public name of your instance, displayed in the about page."
+    field_kwargs = {"required": False}
 
 
 @global_preferences_registry.register
 class InstanceShortDescription(types.StringPreference):
     show_in_api = True
     section = instance
-    name = 'short_description'
-    default = ''
-    verbose_name = 'Short description'
-    help_text = 'Instance succinct description, displayed in the about page.'
-    field_kwargs = {
-        'required': False,
-    }
+    name = "short_description"
+    default = ""
+    verbose_name = "Short description"
+    help_text = "Instance succinct description, displayed in the about page."
+    field_kwargs = {"required": False}
 
 
 @global_preferences_registry.register
 class InstanceLongDescription(types.StringPreference):
     show_in_api = True
     section = instance
-    name = 'long_description'
-    verbose_name = 'Long description'
-    default = ''
-    help_text = 'Instance long description, displayed in the about page (markdown allowed).'
+    name = "long_description"
+    verbose_name = "Long description"
+    default = ""
+    help_text = (
+        "Instance long description, displayed in the about page (markdown allowed)."
+    )
     widget = widgets.Textarea
-    field_kwargs = {
-        'required': False,
-    }
+    field_kwargs = {"required": False}
 
 
 @global_preferences_registry.register
 class RavenDSN(types.StringPreference):
     show_in_api = True
     section = raven
-    name = 'front_dsn'
-    default = 'https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4'
-    verbose_name = 'Raven DSN key (front-end)'
+    name = "front_dsn"
+    default = "https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4"
+    verbose_name = "Raven DSN key (front-end)"
 
     help_text = (
-        'A Raven DSN key used to report front-ent errors to '
-        'a sentry instance. Keeping the default one will report errors to '
-        'Funkwhale developers.'
+        "A Raven DSN key used to report front-ent errors to "
+        "a sentry instance. Keeping the default one will report errors to "
+        "Funkwhale developers."
     )
-    field_kwargs = {
-        'required': False,
-    }
+    field_kwargs = {"required": False}
 
 
 @global_preferences_registry.register
 class RavenEnabled(types.BooleanPreference):
     show_in_api = True
     section = raven
-    name = 'front_enabled'
+    name = "front_enabled"
     default = False
-    verbose_name = (
-        'Report front-end errors with Raven'
-    )
+    verbose_name = "Report front-end errors with Raven"
 
 
 @global_preferences_registry.register
 class InstanceNodeinfoEnabled(types.BooleanPreference):
     show_in_api = False
     section = instance
-    name = 'nodeinfo_enabled'
+    name = "nodeinfo_enabled"
     default = True
-    verbose_name = 'Enable nodeinfo endpoint'
+    verbose_name = "Enable nodeinfo endpoint"
     help_text = (
-        'This endpoint is needed for your about page to work. '
-        'It\'s also helpful for the various monitoring '
-        'tools that map and analyzize the fediverse, '
-        'but you can disable it completely if needed.'
+        "This endpoint is needed for your about page to work. "
+        "It's also helpful for the various monitoring "
+        "tools that map and analyzize the fediverse, "
+        "but you can disable it completely if needed."
     )
 
 
@@ -95,13 +86,13 @@ class InstanceNodeinfoEnabled(types.BooleanPreference):
 class InstanceNodeinfoPrivate(types.BooleanPreference):
     show_in_api = False
     section = instance
-    name = 'nodeinfo_private'
+    name = "nodeinfo_private"
     default = False
-    verbose_name = 'Private mode in nodeinfo'
+    verbose_name = "Private mode in nodeinfo"
     help_text = (
-        'Indicate in the nodeinfo endpoint that you do not want your instance '
-        'to be tracked by third-party services. '
-        'There is no guarantee these tools will honor this setting though.'
+        "Indicate in the nodeinfo endpoint that you do not want your instance "
+        "to be tracked by third-party services. "
+        "There is no guarantee these tools will honor this setting though."
     )
 
 
@@ -109,10 +100,10 @@ class InstanceNodeinfoPrivate(types.BooleanPreference):
 class InstanceNodeinfoStatsEnabled(types.BooleanPreference):
     show_in_api = False
     section = instance
-    name = 'nodeinfo_stats_enabled'
+    name = "nodeinfo_stats_enabled"
     default = True
-    verbose_name = 'Enable usage and library stats in nodeinfo endpoint'
+    verbose_name = "Enable usage and library stats in nodeinfo endpoint"
     help_text = (
-        'Disable this if you don\'t want to share usage and library statistics '
-        'in the nodeinfo endpoint but don\'t want to disable it completely.'
+        "Disable this if you don't want to share usage and library statistics "
+        "in the nodeinfo endpoint but don't want to disable it completely."
     )
diff --git a/api/funkwhale_api/instance/nodeinfo.py b/api/funkwhale_api/instance/nodeinfo.py
index dbc005af7b965b55a62abf7f47eb099174a88eac..0b8f4b3cecc5d456637b20d051550e48a160834f 100644
--- a/api/funkwhale_api/instance/nodeinfo.py
+++ b/api/funkwhale_api/instance/nodeinfo.py
@@ -5,71 +5,46 @@ from funkwhale_api.common import preferences
 
 from . import stats
 
-
-store = memoize.djangocache.Cache('default')
-memo = memoize.Memoizer(store, namespace='instance:stats')
+store = memoize.djangocache.Cache("default")
+memo = memoize.Memoizer(store, namespace="instance:stats")
 
 
 def get():
-    share_stats = preferences.get('instance__nodeinfo_stats_enabled')
-    private = preferences.get('instance__nodeinfo_private')
+    share_stats = preferences.get("instance__nodeinfo_stats_enabled")
     data = {
-        'version': '2.0',
-        'software': {
-            'name': 'funkwhale',
-            'version': funkwhale_api.__version__
-        },
-        'protocols': ['activitypub'],
-        'services': {
-            'inbound': [],
-            'outbound': []
-        },
-        'openRegistrations': preferences.get('users__registration_enabled'),
-        'usage': {
-            'users': {
-                'total': 0,
-            }
-        },
-        'metadata': {
-            'private': preferences.get('instance__nodeinfo_private'),
-            'shortDescription': preferences.get('instance__short_description'),
-            'longDescription': preferences.get('instance__long_description'),
-            'nodeName': preferences.get('instance__name'),
-            'library': {
-                'federationEnabled': preferences.get('federation__enabled'),
-                'federationNeedsApproval': preferences.get('federation__music_needs_approval'),
-                'anonymousCanListen': preferences.get('common__api_authentication_required'),
+        "version": "2.0",
+        "software": {"name": "funkwhale", "version": funkwhale_api.__version__},
+        "protocols": ["activitypub"],
+        "services": {"inbound": [], "outbound": []},
+        "openRegistrations": preferences.get("users__registration_enabled"),
+        "usage": {"users": {"total": 0}},
+        "metadata": {
+            "private": preferences.get("instance__nodeinfo_private"),
+            "shortDescription": preferences.get("instance__short_description"),
+            "longDescription": preferences.get("instance__long_description"),
+            "nodeName": preferences.get("instance__name"),
+            "library": {
+                "federationEnabled": preferences.get("federation__enabled"),
+                "federationNeedsApproval": preferences.get(
+                    "federation__music_needs_approval"
+                ),
+                "anonymousCanListen": preferences.get(
+                    "common__api_authentication_required"
+                ),
             },
-        }
+        },
     }
     if share_stats:
-        getter = memo(
-            lambda: stats.get(),
-            max_age=600
-        )
+        getter = memo(lambda: stats.get(), max_age=600)
         statistics = getter()
-        data['usage']['users']['total'] = statistics['users']
-        data['metadata']['library']['tracks'] = {
-            'total': statistics['tracks'],
-        }
-        data['metadata']['library']['artists'] = {
-            'total': statistics['artists'],
-        }
-        data['metadata']['library']['albums'] = {
-            'total': statistics['albums'],
-        }
-        data['metadata']['library']['music'] = {
-            'hours': statistics['music_duration']
-        }
+        data["usage"]["users"]["total"] = statistics["users"]
+        data["metadata"]["library"]["tracks"] = {"total": statistics["tracks"]}
+        data["metadata"]["library"]["artists"] = {"total": statistics["artists"]}
+        data["metadata"]["library"]["albums"] = {"total": statistics["albums"]}
+        data["metadata"]["library"]["music"] = {"hours": statistics["music_duration"]}
 
-        data['metadata']['usage'] = {
-            'favorites': {
-                'tracks': {
-                    'total': statistics['track_favorites'],
-                }
-            },
-            'listenings': {
-                'total': statistics['listenings']
-            }
+        data["metadata"]["usage"] = {
+            "favorites": {"tracks": {"total": statistics["track_favorites"]}},
+            "listenings": {"total": statistics["listenings"]},
         }
     return data
diff --git a/api/funkwhale_api/instance/stats.py b/api/funkwhale_api/instance/stats.py
index 167b333d6d2c7f64f33055caa83cbea170569751..061aade750ab0f6f83b9149d56dd19a6cf77d7fc 100644
--- a/api/funkwhale_api/instance/stats.py
+++ b/api/funkwhale_api/instance/stats.py
@@ -8,13 +8,13 @@ from funkwhale_api.users.models import User
 
 def get():
     return {
-        'users': get_users(),
-        'tracks': get_tracks(),
-        'albums': get_albums(),
-        'artists': get_artists(),
-        'track_favorites': get_track_favorites(),
-        'listenings': get_listenings(),
-        'music_duration': get_music_duration(),
+        "users": get_users(),
+        "tracks": get_tracks(),
+        "albums": get_albums(),
+        "artists": get_artists(),
+        "track_favorites": get_track_favorites(),
+        "listenings": get_listenings(),
+        "music_duration": get_music_duration(),
     }
 
 
@@ -43,9 +43,7 @@ def get_artists():
 
 
 def get_music_duration():
-    seconds = models.TrackFile.objects.aggregate(
-        d=Sum('duration'),
-    )['d']
+    seconds = models.TrackFile.objects.aggregate(d=Sum("duration"))["d"]
     if seconds:
         return seconds / 3600
     return 0
diff --git a/api/funkwhale_api/instance/urls.py b/api/funkwhale_api/instance/urls.py
index 7992842c030c636057ed13236da282febda9cc4e..05682b1e762ace43a2f9f14168f9ca42530ef439 100644
--- a/api/funkwhale_api/instance/urls.py
+++ b/api/funkwhale_api/instance/urls.py
@@ -2,10 +2,11 @@ from django.conf.urls import url
 from rest_framework import routers
 
 from . import views
+
 admin_router = routers.SimpleRouter()
-admin_router.register(r'admin/settings', views.AdminSettings, 'admin-settings')
+admin_router.register(r"admin/settings", views.AdminSettings, "admin-settings")
 
 urlpatterns = [
-    url(r'^nodeinfo/2.0/$', views.NodeInfo.as_view(), name='nodeinfo-2.0'),
-    url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'),
+    url(r"^nodeinfo/2.0/$", views.NodeInfo.as_view(), name="nodeinfo-2.0"),
+    url(r"^settings/$", views.InstanceSettings.as_view(), name="settings"),
 ] + admin_router.urls
diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py
index b905acd3e6c1ce78811e44f691b17506b041ab6f..ea6311033333ad0603a1f88d52fd51bda44ceeaa 100644
--- a/api/funkwhale_api/instance/views.py
+++ b/api/funkwhale_api/instance/views.py
@@ -1,26 +1,22 @@
-from rest_framework import views
-from rest_framework.response import Response
-
 from dynamic_preferences.api import serializers
 from dynamic_preferences.api import viewsets as preferences_viewsets
 from dynamic_preferences.registries import global_preferences_registry
+from rest_framework import views
+from rest_framework.response import Response
 
 from funkwhale_api.common import preferences
 from funkwhale_api.users.permissions import HasUserPermission
 
 from . import nodeinfo
-from . import stats
-
 
-NODEINFO_2_CONTENT_TYPE = (
-    'application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8'  # noqa
-)
+NODEINFO_2_CONTENT_TYPE = "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"  # noqa
 
 
 class AdminSettings(preferences_viewsets.GlobalPreferencesViewSet):
     pagination_class = None
     permission_classes = (HasUserPermission,)
-    required_permissions = ['settings']
+    required_permissions = ["settings"]
+
 
 class InstanceSettings(views.APIView):
     permission_classes = []
@@ -29,16 +25,11 @@ class InstanceSettings(views.APIView):
     def get(self, request, *args, **kwargs):
         manager = global_preferences_registry.manager()
         manager.all()
-        all_preferences = manager.model.objects.all().order_by(
-            'section', 'name'
-        )
+        all_preferences = manager.model.objects.all().order_by("section", "name")
         api_preferences = [
-            p
-            for p in all_preferences
-            if getattr(p.preference, 'show_in_api', False)
+            p for p in all_preferences if getattr(p.preference, "show_in_api", False)
         ]
-        data = serializers.GlobalPreferenceSerializer(
-            api_preferences, many=True).data
+        data = serializers.GlobalPreferenceSerializer(api_preferences, many=True).data
         return Response(data, status=200)
 
 
@@ -47,8 +38,7 @@ class NodeInfo(views.APIView):
     authentication_classes = []
 
     def get(self, request, *args, **kwargs):
-        if not preferences.get('instance__nodeinfo_enabled'):
+        if not preferences.get("instance__nodeinfo_enabled"):
             return Response(status=404)
         data = nodeinfo.get()
-        return Response(
-            data, status=200, content_type=NODEINFO_2_CONTENT_TYPE)
+        return Response(data, status=200, content_type=NODEINFO_2_CONTENT_TYPE)
diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py
index 9853b7a61fb3f0018f41ad2c497e635613523edd..2f2bde838fa90695ebc1f077e88650f2d69536c5 100644
--- a/api/funkwhale_api/manage/filters.py
+++ b/api/funkwhale_api/manage/filters.py
@@ -1,4 +1,3 @@
-from django.db.models import Count
 
 from django_filters import rest_framework as filters
 
@@ -7,19 +6,15 @@ from funkwhale_api.music import models as music_models
 
 
 class ManageTrackFileFilterSet(filters.FilterSet):
-    q = fields.SearchFilter(search_fields=[
-        'track__title',
-        'track__album__title',
-        'track__artist__name',
-        'source',
-    ])
+    q = fields.SearchFilter(
+        search_fields=[
+            "track__title",
+            "track__album__title",
+            "track__artist__name",
+            "source",
+        ]
+    )
 
     class Meta:
         model = music_models.TrackFile
-        fields = [
-            'q',
-            'track__album',
-            'track__artist',
-            'track',
-            'library_track'
-        ]
+        fields = ["q", "track__album", "track__artist", "track", "library_track"]
diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py
index 02300ec0689c16f7250e5557a3c05d1eb2f07006..1c94cf5538171973a16e29a8f54591daf7778f2e 100644
--- a/api/funkwhale_api/manage/serializers.py
+++ b/api/funkwhale_api/manage/serializers.py
@@ -10,12 +10,7 @@ from . import filters
 class ManageTrackFileArtistSerializer(serializers.ModelSerializer):
     class Meta:
         model = music_models.Artist
-        fields = [
-            'id',
-            'mbid',
-            'creation_date',
-            'name',
-        ]
+        fields = ["id", "mbid", "creation_date", "name"]
 
 
 class ManageTrackFileAlbumSerializer(serializers.ModelSerializer):
@@ -24,13 +19,13 @@ class ManageTrackFileAlbumSerializer(serializers.ModelSerializer):
     class Meta:
         model = music_models.Album
         fields = (
-            'id',
-            'mbid',
-            'title',
-            'artist',
-            'release_date',
-            'cover',
-            'creation_date',
+            "id",
+            "mbid",
+            "title",
+            "artist",
+            "release_date",
+            "cover",
+            "creation_date",
         )
 
 
@@ -40,15 +35,7 @@ class ManageTrackFileTrackSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = music_models.Track
-        fields = (
-            'id',
-            'mbid',
-            'title',
-            'album',
-            'artist',
-            'creation_date',
-            'position',
-        )
+        fields = ("id", "mbid", "title", "album", "artist", "creation_date", "position")
 
 
 class ManageTrackFileSerializer(serializers.ModelSerializer):
@@ -57,24 +44,24 @@ class ManageTrackFileSerializer(serializers.ModelSerializer):
     class Meta:
         model = music_models.TrackFile
         fields = (
-            'id',
-            'path',
-            'source',
-            'filename',
-            'mimetype',
-            'track',
-            'duration',
-            'mimetype',
-            'bitrate',
-            'size',
-            'path',
-            'library_track',
+            "id",
+            "path",
+            "source",
+            "filename",
+            "mimetype",
+            "track",
+            "duration",
+            "mimetype",
+            "bitrate",
+            "size",
+            "path",
+            "library_track",
         )
 
 
 class ManageTrackFileActionSerializer(common_serializers.ActionSerializer):
-    actions = ['delete']
-    dangerous_actions = ['delete']
+    actions = ["delete"]
+    dangerous_actions = ["delete"]
     filterset_class = filters.ManageTrackFileFilterSet
 
     @transaction.atomic
diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py
index c434581ecde46de48d4b87f36984923dfcff84fa..60853034f0a0552c01b67b6a0354158691d49783 100644
--- a/api/funkwhale_api/manage/urls.py
+++ b/api/funkwhale_api/manage/urls.py
@@ -1,11 +1,11 @@
 from django.conf.urls import include, url
+from rest_framework import routers
+
 from . import views
 
-from rest_framework import routers
 library_router = routers.SimpleRouter()
-library_router.register(r'track-files', views.ManageTrackFileViewSet, 'track-files')
+library_router.register(r"track-files", views.ManageTrackFileViewSet, "track-files")
 
 urlpatterns = [
-    url(r'^library/',
-        include((library_router.urls, 'instance'), namespace='library')),
+    url(r"^library/", include((library_router.urls, "instance"), namespace="library"))
 ]
diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py
index 74059caa1d97a1cef31fcf7408a49e7486447f93..8511732c96b287e7c2c82da799dc864c7a455e6a 100644
--- a/api/funkwhale_api/manage/views.py
+++ b/api/funkwhale_api/manage/views.py
@@ -1,48 +1,42 @@
-from rest_framework import mixins
-from rest_framework import response
-from rest_framework import viewsets
+from rest_framework import mixins, response, viewsets
 from rest_framework.decorators import list_route
 
 from funkwhale_api.music import models as music_models
 from funkwhale_api.users.permissions import HasUserPermission
 
-from . import filters
-from . import serializers
+from . import filters, serializers
 
 
 class ManageTrackFileViewSet(
-        mixins.ListModelMixin,
-        mixins.RetrieveModelMixin,
-        mixins.DestroyModelMixin,
-        viewsets.GenericViewSet):
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.DestroyModelMixin,
+    viewsets.GenericViewSet,
+):
     queryset = (
         music_models.TrackFile.objects.all()
-            .select_related(
-                'track__artist',
-                'track__album__artist',
-                'library_track')
-            .order_by('-id')
+        .select_related("track__artist", "track__album__artist", "library_track")
+        .order_by("-id")
     )
     serializer_class = serializers.ManageTrackFileSerializer
     filter_class = filters.ManageTrackFileFilterSet
     permission_classes = (HasUserPermission,)
-    required_permissions = ['library']
+    required_permissions = ["library"]
     ordering_fields = [
-        'accessed_date',
-        'modification_date',
-        'creation_date',
-        'track__artist__name',
-        'bitrate',
-        'size',
-        'duration',
+        "accessed_date",
+        "modification_date",
+        "creation_date",
+        "track__artist__name",
+        "bitrate",
+        "size",
+        "duration",
     ]
 
-    @list_route(methods=['post'])
+    @list_route(methods=["post"])
     def action(self, request, *args, **kwargs):
         queryset = self.get_queryset()
         serializer = serializers.ManageTrackFileActionSerializer(
-            request.data,
-            queryset=queryset,
+            request.data, queryset=queryset
         )
         serializer.is_valid(raise_exception=True)
         result = serializer.save()
diff --git a/api/funkwhale_api/music/admin.py b/api/funkwhale_api/music/admin.py
index 1654428baf866df98f9888c6b101cce2425702cd..a5775acd68022e7530a586bfd21111da4125a104 100644
--- a/api/funkwhale_api/music/admin.py
+++ b/api/funkwhale_api/music/admin.py
@@ -5,85 +5,73 @@ from . import models
 
 @admin.register(models.Artist)
 class ArtistAdmin(admin.ModelAdmin):
-    list_display = ['name', 'mbid', 'creation_date']
-    search_fields = ['name', 'mbid']
+    list_display = ["name", "mbid", "creation_date"]
+    search_fields = ["name", "mbid"]
 
 
 @admin.register(models.Album)
 class AlbumAdmin(admin.ModelAdmin):
-    list_display = ['title', 'artist', 'mbid', 'release_date', 'creation_date']
-    search_fields = ['title', 'artist__name', 'mbid']
+    list_display = ["title", "artist", "mbid", "release_date", "creation_date"]
+    search_fields = ["title", "artist__name", "mbid"]
     list_select_related = True
 
 
 @admin.register(models.Track)
 class TrackAdmin(admin.ModelAdmin):
-    list_display = ['title', 'artist', 'album', 'mbid']
-    search_fields = ['title', 'artist__name', 'album__title', 'mbid']
+    list_display = ["title", "artist", "album", "mbid"]
+    search_fields = ["title", "artist__name", "album__title", "mbid"]
     list_select_related = True
 
 
 @admin.register(models.ImportBatch)
 class ImportBatchAdmin(admin.ModelAdmin):
-    list_display = [
-        'submitted_by',
-        'creation_date',
-        'import_request',
-        'status']
-    list_select_related = [
-        'submitted_by',
-        'import_request',
-    ]
-    list_filter = ['status']
-    search_fields = [
-        'import_request__name', 'source', 'batch__pk', 'mbid']
+    list_display = ["submitted_by", "creation_date", "import_request", "status"]
+    list_select_related = ["submitted_by", "import_request"]
+    list_filter = ["status"]
+    search_fields = ["import_request__name", "source", "batch__pk", "mbid"]
 
 
 @admin.register(models.ImportJob)
 class ImportJobAdmin(admin.ModelAdmin):
-    list_display = ['source', 'batch', 'track_file', 'status', 'mbid']
-    list_select_related = [
-        'track_file',
-        'batch',
-    ]
-    search_fields = ['source', 'batch__pk', 'mbid']
-    list_filter = ['status']
+    list_display = ["source", "batch", "track_file", "status", "mbid"]
+    list_select_related = ["track_file", "batch"]
+    search_fields = ["source", "batch__pk", "mbid"]
+    list_filter = ["status"]
 
 
 @admin.register(models.Work)
 class WorkAdmin(admin.ModelAdmin):
-    list_display = ['title', 'mbid', 'language', 'nature']
+    list_display = ["title", "mbid", "language", "nature"]
     list_select_related = True
-    search_fields = ['title']
-    list_filter = ['language', 'nature']
+    search_fields = ["title"]
+    list_filter = ["language", "nature"]
 
 
 @admin.register(models.Lyrics)
 class LyricsAdmin(admin.ModelAdmin):
-    list_display = ['url', 'id', 'url']
+    list_display = ["url", "id", "url"]
     list_select_related = True
-    search_fields = ['url', 'work__title']
-    list_filter = ['work__language']
+    search_fields = ["url", "work__title"]
+    list_filter = ["work__language"]
 
 
 @admin.register(models.TrackFile)
 class TrackFileAdmin(admin.ModelAdmin):
     list_display = [
-        'track',
-        'audio_file',
-        'source',
-        'duration',
-        'mimetype',
-        'size',
-        'bitrate'
-    ]
-    list_select_related = [
-        'track'
+        "track",
+        "audio_file",
+        "source",
+        "duration",
+        "mimetype",
+        "size",
+        "bitrate",
     ]
+    list_select_related = ["track"]
     search_fields = [
-        'source',
-        'acoustid_track_id',
-        'track__title',
-        'track__album__title',
-        'track__artist__name']
-    list_filter = ['mimetype']
+        "source",
+        "acoustid_track_id",
+        "track__title",
+        "track__album__title",
+        "track__artist__name",
+    ]
+    list_filter = ["mimetype"]
diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py
index 11423f5b0134936b9efeed1b352e3e4dc6bdcd1c..2dd4ba3038593cc2bb2c60610735caeb7f61bc8f 100644
--- a/api/funkwhale_api/music/factories.py
+++ b/api/funkwhale_api/music/factories.py
@@ -1,79 +1,74 @@
-import factory
 import os
 
-from funkwhale_api.factories import registry, ManyToManyFromList
-from funkwhale_api.federation.factories import (
-    LibraryTrackFactory,
-)
+import factory
+
+from funkwhale_api.factories import ManyToManyFromList, registry
+from funkwhale_api.federation.factories import LibraryTrackFactory
 from funkwhale_api.users.factories import UserFactory
 
 SAMPLES_PATH = os.path.join(
     os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
-    'tests', 'music'
+    "tests",
+    "music",
 )
 
 
 @registry.register
 class ArtistFactory(factory.django.DjangoModelFactory):
-    name = factory.Faker('name')
-    mbid = factory.Faker('uuid4')
+    name = factory.Faker("name")
+    mbid = factory.Faker("uuid4")
 
     class Meta:
-        model = 'music.Artist'
+        model = "music.Artist"
 
 
 @registry.register
 class AlbumFactory(factory.django.DjangoModelFactory):
-    title = factory.Faker('sentence', nb_words=3)
-    mbid = factory.Faker('uuid4')
-    release_date = factory.Faker('date_object')
+    title = factory.Faker("sentence", nb_words=3)
+    mbid = factory.Faker("uuid4")
+    release_date = factory.Faker("date_object")
     cover = factory.django.ImageField()
     artist = factory.SubFactory(ArtistFactory)
-    release_group_id = factory.Faker('uuid4')
+    release_group_id = factory.Faker("uuid4")
 
     class Meta:
-        model = 'music.Album'
+        model = "music.Album"
 
 
 @registry.register
 class TrackFactory(factory.django.DjangoModelFactory):
-    title = factory.Faker('sentence', nb_words=3)
-    mbid = factory.Faker('uuid4')
+    title = factory.Faker("sentence", nb_words=3)
+    mbid = factory.Faker("uuid4")
     album = factory.SubFactory(AlbumFactory)
-    artist = factory.SelfAttribute('album.artist')
+    artist = factory.SelfAttribute("album.artist")
     position = 1
-    tags = ManyToManyFromList('tags')
+    tags = ManyToManyFromList("tags")
 
     class Meta:
-        model = 'music.Track'
+        model = "music.Track"
 
 
 @registry.register
 class TrackFileFactory(factory.django.DjangoModelFactory):
     track = factory.SubFactory(TrackFactory)
     audio_file = factory.django.FileField(
-        from_path=os.path.join(SAMPLES_PATH, 'test.ogg'))
+        from_path=os.path.join(SAMPLES_PATH, "test.ogg")
+    )
 
     bitrate = None
     size = None
     duration = None
 
     class Meta:
-        model = 'music.TrackFile'
+        model = "music.TrackFile"
 
     class Params:
-        in_place = factory.Trait(
-            audio_file=None,
-        )
+        in_place = factory.Trait(audio_file=None)
         federation = factory.Trait(
             audio_file=None,
             library_track=factory.SubFactory(LibraryTrackFactory),
-            mimetype=factory.LazyAttribute(
-                lambda o: o.library_track.audio_mimetype
-            ),
-            source=factory.LazyAttribute(
-                lambda o: o.library_track.audio_url
-            ),
+            mimetype=factory.LazyAttribute(lambda o: o.library_track.audio_mimetype),
+            source=factory.LazyAttribute(lambda o: o.library_track.audio_url),
         )
 
 
@@ -82,26 +77,21 @@ class ImportBatchFactory(factory.django.DjangoModelFactory):
     submitted_by = factory.SubFactory(UserFactory)
 
     class Meta:
-        model = 'music.ImportBatch'
+        model = "music.ImportBatch"
 
     class Params:
-        federation = factory.Trait(
-            submitted_by=None,
-            source='federation',
-        )
-        finished = factory.Trait(
-            status='finished',
-        )
+        federation = factory.Trait(submitted_by=None, source="federation")
+        finished = factory.Trait(status="finished")
 
 
 @registry.register
 class ImportJobFactory(factory.django.DjangoModelFactory):
     batch = factory.SubFactory(ImportBatchFactory)
-    source = factory.Faker('url')
-    mbid = factory.Faker('uuid4')
+    source = factory.Faker("url")
+    mbid = factory.Faker("uuid4")
 
     class Meta:
-        model = 'music.ImportJob'
+        model = "music.ImportJob"
 
     class Params:
         federation = factory.Trait(
@@ -110,53 +100,51 @@ class ImportJobFactory(factory.django.DjangoModelFactory):
             batch=factory.SubFactory(ImportBatchFactory, federation=True),
         )
         finished = factory.Trait(
-            status='finished',
-            track_file=factory.SubFactory(TrackFileFactory),
-        )
-        in_place = factory.Trait(
-            status='finished',
-            audio_file=None,
+            status="finished", track_file=factory.SubFactory(TrackFileFactory)
         )
+        in_place = factory.Trait(status="finished", audio_file=None)
         with_audio_file = factory.Trait(
-            status='finished',
+            status="finished",
             audio_file=factory.django.FileField(
-                from_path=os.path.join(SAMPLES_PATH, 'test.ogg')),
+                from_path=os.path.join(SAMPLES_PATH, "test.ogg")
+            ),
         )
 
 
-@registry.register(name='music.FileImportJob')
+@registry.register(name="music.FileImportJob")
 class FileImportJobFactory(ImportJobFactory):
-    source = 'file://'
+    source = "file://"
     mbid = None
     audio_file = factory.django.FileField(
-        from_path=os.path.join(SAMPLES_PATH, 'test.ogg'))
+        from_path=os.path.join(SAMPLES_PATH, "test.ogg")
+    )
 
 
 @registry.register
 class WorkFactory(factory.django.DjangoModelFactory):
-    mbid = factory.Faker('uuid4')
-    language = 'eng'
-    nature = 'song'
-    title = factory.Faker('sentence', nb_words=3)
+    mbid = factory.Faker("uuid4")
+    language = "eng"
+    nature = "song"
+    title = factory.Faker("sentence", nb_words=3)
 
     class Meta:
-        model = 'music.Work'
+        model = "music.Work"
 
 
 @registry.register
 class LyricsFactory(factory.django.DjangoModelFactory):
     work = factory.SubFactory(WorkFactory)
-    url = factory.Faker('url')
-    content = factory.Faker('paragraphs', nb=4)
+    url = factory.Faker("url")
+    content = factory.Faker("paragraphs", nb=4)
 
     class Meta:
-        model = 'music.Lyrics'
+        model = "music.Lyrics"
 
 
 @registry.register
 class TagFactory(factory.django.DjangoModelFactory):
-    name = factory.SelfAttribute('slug')
-    slug = factory.Faker('slug')
+    name = factory.SelfAttribute("slug")
+    slug = factory.Faker("slug")
 
     class Meta:
-        model = 'taggit.Tag'
+        model = "taggit.Tag"
diff --git a/api/funkwhale_api/music/fake_data.py b/api/funkwhale_api/music/fake_data.py
index 892b784caec0c7bcdd3db7927852bd83b5ad9b2b..e5fd65d8ebcb2bb24e0dd0f936cd6fe8e1efcf91 100644
--- a/api/funkwhale_api/music/fake_data.py
+++ b/api/funkwhale_api/music/fake_data.py
@@ -3,20 +3,21 @@ Populates the database with fake data
 """
 import random
 
-from funkwhale_api.music import models
 from funkwhale_api.music import factories
 
 
 def create_data(count=25):
     artists = factories.ArtistFactory.create_batch(size=count)
     for artist in artists:
-        print('Creating data for', artist)
+        print("Creating data for", artist)
         albums = factories.AlbumFactory.create_batch(
-            artist=artist, size=random.randint(1, 5))
+            artist=artist, size=random.randint(1, 5)
+        )
         for album in albums:
             factories.TrackFileFactory.create_batch(
-                track__album=album, size=random.randint(3, 18))
+                track__album=album, size=random.randint(3, 18)
+            )
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     create_data()
diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py
index dc7aafc219644ee87c70d0d3e495c9f4b79be04b..1f73fc9b0638df2d15520f5522fb13c013989de0 100644
--- a/api/funkwhale_api/music/filters.py
+++ b/api/funkwhale_api/music/filters.py
@@ -1,18 +1,16 @@
 from django.db.models import Count
-
 from django_filters import rest_framework as filters
 
 from funkwhale_api.common import fields
+
 from . import models
 
 
 class ListenableMixin(filters.FilterSet):
-    listenable = filters.BooleanFilter(name='_', method='filter_listenable')
+    listenable = filters.BooleanFilter(name="_", method="filter_listenable")
 
     def filter_listenable(self, queryset, name, value):
-        queryset = queryset.annotate(
-            files_count=Count('tracks__files')
-        )
+        queryset = queryset.annotate(files_count=Count("tracks__files"))
         if value:
             return queryset.filter(files_count__gt=0)
         else:
@@ -20,39 +18,31 @@ class ListenableMixin(filters.FilterSet):
 
 
 class ArtistFilter(ListenableMixin):
-    q = fields.SearchFilter(search_fields=[
-        'name',
-    ])
+    q = fields.SearchFilter(search_fields=["name"])
 
     class Meta:
         model = models.Artist
         fields = {
-            'name': ['exact', 'iexact', 'startswith', 'icontains'],
-            'listenable': 'exact',
+            "name": ["exact", "iexact", "startswith", "icontains"],
+            "listenable": "exact",
         }
 
 
 class TrackFilter(filters.FilterSet):
-    q = fields.SearchFilter(search_fields=[
-        'title',
-        'album__title',
-        'artist__name',
-    ])
-    listenable = filters.BooleanFilter(name='_', method='filter_listenable')
+    q = fields.SearchFilter(search_fields=["title", "album__title", "artist__name"])
+    listenable = filters.BooleanFilter(name="_", method="filter_listenable")
 
     class Meta:
         model = models.Track
         fields = {
-            'title': ['exact', 'iexact', 'startswith', 'icontains'],
-            'listenable': ['exact'],
-            'artist': ['exact'],
-            'album': ['exact'],
+            "title": ["exact", "iexact", "startswith", "icontains"],
+            "listenable": ["exact"],
+            "artist": ["exact"],
+            "album": ["exact"],
         }
 
     def filter_listenable(self, queryset, name, value):
-        queryset = queryset.annotate(
-            files_count=Count('files')
-        )
+        queryset = queryset.annotate(files_count=Count("files"))
         if value:
             return queryset.filter(files_count__gt=0)
         else:
@@ -60,46 +50,32 @@ class TrackFilter(filters.FilterSet):
 
 
 class ImportBatchFilter(filters.FilterSet):
-    q = fields.SearchFilter(search_fields=[
-        'submitted_by__username',
-        'source',
-    ])
+    q = fields.SearchFilter(search_fields=["submitted_by__username", "source"])
 
     class Meta:
         model = models.ImportBatch
-        fields = {
-            'status': ['exact'],
-            'source': ['exact'],
-            'submitted_by': ['exact'],
-        }
+        fields = {"status": ["exact"], "source": ["exact"], "submitted_by": ["exact"]}
 
 
 class ImportJobFilter(filters.FilterSet):
-    q = fields.SearchFilter(search_fields=[
-        'batch__submitted_by__username',
-        'source',
-    ])
+    q = fields.SearchFilter(search_fields=["batch__submitted_by__username", "source"])
 
     class Meta:
         model = models.ImportJob
         fields = {
-            'batch': ['exact'],
-            'batch__status': ['exact'],
-            'batch__source': ['exact'],
-            'batch__submitted_by': ['exact'],
-            'status': ['exact'],
-            'source': ['exact'],
+            "batch": ["exact"],
+            "batch__status": ["exact"],
+            "batch__source": ["exact"],
+            "batch__submitted_by": ["exact"],
+            "status": ["exact"],
+            "source": ["exact"],
         }
 
 
 class AlbumFilter(ListenableMixin):
-    listenable = filters.BooleanFilter(name='_', method='filter_listenable')
-    q = fields.SearchFilter(search_fields=[
-        'title',
-        'artist__name'
-        'source',
-    ])
+    listenable = filters.BooleanFilter(name="_", method="filter_listenable")
+    q = fields.SearchFilter(search_fields=["title", "artist__name" "source"])
 
     class Meta:
         model = models.Album
-        fields = ['listenable', 'q', 'artist']
+        fields = ["listenable", "q", "artist"]
diff --git a/api/funkwhale_api/music/importers.py b/api/funkwhale_api/music/importers.py
index 7e26fe9689571cccd86d888efc8a1eaaacacc3d5..ce7ded02b48822c2740c6c9e57765ea8b458dccc 100644
--- a/api/funkwhale_api/music/importers.py
+++ b/api/funkwhale_api/music/importers.py
@@ -1,42 +1,43 @@
-
-
 def load(model, *args, **kwargs):
     importer = registry[model.__name__](model=model)
     return importer.load(*args, **kwargs)
 
+
 class Importer(object):
     def __init__(self, model):
         self.model = model
 
     def load(self, cleaned_data, raw_data, import_hooks):
-        mbid = cleaned_data.pop('mbid')
+        mbid = cleaned_data.pop("mbid")
         m = self.model.objects.update_or_create(mbid=mbid, defaults=cleaned_data)[0]
         for hook in import_hooks:
             hook(m, cleaned_data, raw_data)
         return m
 
+
 class Mapping(object):
     """Cast musicbrainz data to funkwhale data and vice-versa"""
+
     def __init__(self, musicbrainz_mapping):
         self.musicbrainz_mapping = musicbrainz_mapping
 
         self._from_musicbrainz = {}
         self._to_musicbrainz = {}
         for field_name, conf in self.musicbrainz_mapping.items():
-            self._from_musicbrainz[conf['musicbrainz_field_name']] = {
-                'field_name': field_name,
-                'converter': conf.get('converter', lambda v: v)
+            self._from_musicbrainz[conf["musicbrainz_field_name"]] = {
+                "field_name": field_name,
+                "converter": conf.get("converter", lambda v: v),
             }
             self._to_musicbrainz[field_name] = {
-                'field_name': conf['musicbrainz_field_name'],
-                'converter': conf.get('converter', lambda v: v)
+                "field_name": conf["musicbrainz_field_name"],
+                "converter": conf.get("converter", lambda v: v),
             }
+
     def from_musicbrainz(self, key, value):
-        return self._from_musicbrainz[key]['field_name'], self._from_musicbrainz[key]['converter'](value)
-
-registry = {
-    'Artist': Importer,
-    'Track': Importer,
-    'Album': Importer,
-    'Work': Importer,
-}
+        return (
+            self._from_musicbrainz[key]["field_name"],
+            self._from_musicbrainz[key]["converter"](value),
+        )
+
+
+registry = {"Artist": Importer, "Track": Importer, "Album": Importer, "Work": Importer}
diff --git a/api/funkwhale_api/music/lyrics.py b/api/funkwhale_api/music/lyrics.py
index 1ad69ce25f2e2f7e5f4693ee65c28e6e84647ec8..6d5f20e44d54320db9ebc4fb5b344de6585ba6cf 100644
--- a/api/funkwhale_api/music/lyrics.py
+++ b/api/funkwhale_api/music/lyrics.py
@@ -1,27 +1,27 @@
 import urllib.request
-import html.parser
+
 from bs4 import BeautifulSoup
 
 
 def _get_html(url):
     with urllib.request.urlopen(url) as response:
         html = response.read()
-    return html.decode('utf-8')
+    return html.decode("utf-8")
 
 
 def extract_content(html):
     soup = BeautifulSoup(html, "html.parser")
-    return soup.find_all("div", class_='lyricbox')[0].contents
+    return soup.find_all("div", class_="lyricbox")[0].contents
 
 
 def clean_content(contents):
     final_content = ""
     for e in contents:
-        if e == '\n':
+        if e == "\n":
             continue
-        if e.name == 'script':
+        if e.name == "script":
             continue
-        if e.name == 'br':
+        if e.name == "br":
             final_content += "\n"
             continue
         try:
diff --git a/api/funkwhale_api/music/management/commands/fix_track_files.py b/api/funkwhale_api/music/management/commands/fix_track_files.py
index c18e2b255a6842d4dc998551b0b1b55acbbac5cf..988f9bed36e605c9417021314482c132008680c4 100644
--- a/api/funkwhale_api/music/management/commands/fix_track_files.py
+++ b/api/funkwhale_api/music/management/commands/fix_track_files.py
@@ -1,29 +1,26 @@
 import cacheops
-import os
-
+from django.core.management.base import BaseCommand
 from django.db import transaction
 from django.db.models import Q
-from django.conf import settings
-from django.core.management.base import BaseCommand, CommandError
 
 from funkwhale_api.music import models, utils
 
 
 class Command(BaseCommand):
-    help = 'Run common checks and fix against imported tracks'
+    help = "Run common checks and fix against imported tracks"
 
     def add_arguments(self, parser):
         parser.add_argument(
-            '--dry-run',
-            action='store_true',
-            dest='dry_run',
+            "--dry-run",
+            action="store_true",
+            dest="dry_run",
             default=False,
-            help='Do not execute anything'
+            help="Do not execute anything",
         )
 
     def handle(self, *args, **options):
-        if options['dry_run']:
-            self.stdout.write('Dry-run on, will not commit anything')
+        if options["dry_run"]:
+            self.stdout.write("Dry-run on, will not commit anything")
         self.fix_mimetypes(**options)
         self.fix_file_data(**options)
         self.fix_file_size(**options)
@@ -31,75 +28,72 @@ class Command(BaseCommand):
 
     @transaction.atomic
     def fix_mimetypes(self, dry_run, **kwargs):
-        self.stdout.write('Fixing missing mimetypes...')
+        self.stdout.write("Fixing missing mimetypes...")
         matching = models.TrackFile.objects.filter(
-            source__startswith='file://').exclude(mimetype__startswith='audio/')
+            source__startswith="file://"
+        ).exclude(mimetype__startswith="audio/")
         self.stdout.write(
-            '[mimetypes] {} entries found with bad or no mimetype'.format(
-                matching.count()))
+            "[mimetypes] {} entries found with bad or no mimetype".format(
+                matching.count()
+            )
+        )
         for extension, mimetype in utils.EXTENSION_TO_MIMETYPE.items():
-            qs = matching.filter(source__endswith='.{}'.format(extension))
+            qs = matching.filter(source__endswith=".{}".format(extension))
             self.stdout.write(
-                '[mimetypes] setting {} {} files to {}'.format(
+                "[mimetypes] setting {} {} files to {}".format(
                     qs.count(), extension, mimetype
-                ))
+                )
+            )
             if not dry_run:
-                self.stdout.write('[mimetypes] commiting...')
+                self.stdout.write("[mimetypes] commiting...")
                 qs.update(mimetype=mimetype)
 
     def fix_file_data(self, dry_run, **kwargs):
-        self.stdout.write('Fixing missing bitrate or length...')
+        self.stdout.write("Fixing missing bitrate or length...")
         matching = models.TrackFile.objects.filter(
-            Q(bitrate__isnull=True) | Q(duration__isnull=True))
+            Q(bitrate__isnull=True) | Q(duration__isnull=True)
+        )
         total = matching.count()
         self.stdout.write(
-            '[bitrate/length] {} entries found with missing values'.format(
-                total))
+            "[bitrate/length] {} entries found with missing values".format(total)
+        )
         if dry_run:
             return
-        for i, tf in enumerate(matching.only('audio_file')):
+        for i, tf in enumerate(matching.only("audio_file")):
             self.stdout.write(
-                '[bitrate/length] {}/{} fixing file #{}'.format(
-                    i+1, total, tf.pk
-                ))
+                "[bitrate/length] {}/{} fixing file #{}".format(i + 1, total, tf.pk)
+            )
 
             try:
                 audio_file = tf.get_audio_file()
                 if audio_file:
-                    with audio_file as f:
-                        data = utils.get_audio_file_data(audio_file)
-                    tf.bitrate = data['bitrate']
-                    tf.duration = data['length']
-                    tf.save(update_fields=['duration', 'bitrate'])
+                    data = utils.get_audio_file_data(audio_file)
+                    tf.bitrate = data["bitrate"]
+                    tf.duration = data["length"]
+                    tf.save(update_fields=["duration", "bitrate"])
                 else:
-                    self.stderr.write('[bitrate/length] no file found')
+                    self.stderr.write("[bitrate/length] no file found")
             except Exception as e:
                 self.stderr.write(
-                    '[bitrate/length] error with file #{}: {}'.format(
-                        tf.pk, str(e)
-                    )
+                    "[bitrate/length] error with file #{}: {}".format(tf.pk, str(e))
                 )
 
     def fix_file_size(self, dry_run, **kwargs):
-        self.stdout.write('Fixing missing size...')
+        self.stdout.write("Fixing missing size...")
         matching = models.TrackFile.objects.filter(size__isnull=True)
         total = matching.count()
-        self.stdout.write(
-            '[size] {} entries found with missing values'.format(total))
+        self.stdout.write("[size] {} entries found with missing values".format(total))
         if dry_run:
             return
-        for i, tf in enumerate(matching.only('size')):
+        for i, tf in enumerate(matching.only("size")):
             self.stdout.write(
-                '[size] {}/{} fixing file #{}'.format(
-                    i+1, total, tf.pk
-                ))
+                "[size] {}/{} fixing file #{}".format(i + 1, total, tf.pk)
+            )
 
             try:
                 tf.size = tf.get_file_size()
-                tf.save(update_fields=['size'])
+                tf.save(update_fields=["size"])
             except Exception as e:
                 self.stderr.write(
-                    '[size] error with file #{}: {}'.format(
-                        tf.pk, str(e)
-                    )
+                    "[size] error with file #{}: {}".format(tf.pk, str(e))
                 )
diff --git a/api/funkwhale_api/music/metadata.py b/api/funkwhale_api/music/metadata.py
index 4c17c42c0d51ac9c92501d43ecbf1aab3de0377f..d2534f6b241ad3e7b6cc98fa8e1e0c49f41f0450 100644
--- a/api/funkwhale_api/music/metadata.py
+++ b/api/funkwhale_api/music/metadata.py
@@ -1,6 +1,6 @@
-from django import forms
 import arrow
 import mutagen
+from django import forms
 
 NODEFAULT = object()
 
@@ -14,21 +14,17 @@ class UnsupportedTag(KeyError):
 
 
 def get_id3_tag(f, k):
-    if k == 'pictures':
-        return f.tags.getall('APIC')
+    if k == "pictures":
+        return f.tags.getall("APIC")
     # First we try to grab the standard key
     try:
         return f.tags[k].text[0]
     except KeyError:
         pass
     # then we fallback on parsing non standard tags
-    all_tags = f.tags.getall('TXXX')
+    all_tags = f.tags.getall("TXXX")
     try:
-        matches = [
-            t
-            for t in all_tags
-            if t.desc.lower() == k.lower()
-        ]
+        matches = [t for t in all_tags if t.desc.lower() == k.lower()]
         return matches[0].text[0]
     except (KeyError, IndexError):
         raise TagNotFound(k)
@@ -37,17 +33,19 @@ def get_id3_tag(f, k):
 def clean_id3_pictures(apic):
     pictures = []
     for p in list(apic):
-        pictures.append({
-            'mimetype': p.mime,
-            'content': p.data,
-            'description': p.desc,
-            'type': p.type.real,
-        })
+        pictures.append(
+            {
+                "mimetype": p.mime,
+                "content": p.data,
+                "description": p.desc,
+                "type": p.type.real,
+            }
+        )
     return pictures
 
 
 def get_flac_tag(f, k):
-    if k == 'pictures':
+    if k == "pictures":
         return f.pictures
     try:
         return f.get(k, [])[0]
@@ -58,22 +56,22 @@ def get_flac_tag(f, k):
 def clean_flac_pictures(apic):
     pictures = []
     for p in list(apic):
-        pictures.append({
-            'mimetype': p.mime,
-            'content': p.data,
-            'description': p.desc,
-            'type': p.type.real,
-        })
+        pictures.append(
+            {
+                "mimetype": p.mime,
+                "content": p.data,
+                "description": p.desc,
+                "type": p.type.real,
+            }
+        )
     return pictures
 
 
 def get_mp3_recording_id(f, k):
     try:
-        return [
-            t
-            for t in f.tags.getall('UFID')
-            if 'musicbrainz.org' in t.owner
-        ][0].data.decode('utf-8')
+        return [t for t in f.tags.getall("UFID") if "musicbrainz.org" in t.owner][
+            0
+        ].data.decode("utf-8")
     except IndexError:
         raise TagNotFound(k)
 
@@ -86,18 +84,17 @@ def convert_track_number(v):
         pass
 
     try:
-        return int(v.split('/')[0])
+        return int(v.split("/")[0])
     except (ValueError, AttributeError, IndexError):
         pass
 
 
-
 class FirstUUIDField(forms.UUIDField):
     def to_python(self, value):
         try:
             # sometimes, Picard leaves to uuids in the field, separated
             # by a slash
-            value = value.split('/')[0]
+            value = value.split("/")[0]
         except (AttributeError, IndexError, TypeError):
             pass
 
@@ -105,150 +102,119 @@ class FirstUUIDField(forms.UUIDField):
 
 
 VALIDATION = {
-    'musicbrainz_artistid': FirstUUIDField(),
-    'musicbrainz_albumid': FirstUUIDField(),
-    'musicbrainz_recordingid': FirstUUIDField(),
+    "musicbrainz_artistid": FirstUUIDField(),
+    "musicbrainz_albumid": FirstUUIDField(),
+    "musicbrainz_recordingid": FirstUUIDField(),
 }
 
 CONF = {
-    'OggVorbis': {
-        'getter': lambda f, k: f[k][0],
-        'fields': {
-            'track_number': {
-                'field': 'TRACKNUMBER',
-                'to_application': convert_track_number
-            },
-            'title': {},
-            'artist': {},
-            'album': {},
-            'date': {
-                'field': 'date',
-                'to_application': lambda v: arrow.get(v).date()
-            },
-            'musicbrainz_albumid': {},
-            'musicbrainz_artistid': {},
-            'musicbrainz_recordingid': {
-                'field': 'musicbrainz_trackid'
-            },
-        }
+    "OggVorbis": {
+        "getter": lambda f, k: f[k][0],
+        "fields": {
+            "track_number": {
+                "field": "TRACKNUMBER",
+                "to_application": convert_track_number,
+            },
+            "title": {},
+            "artist": {},
+            "album": {},
+            "date": {"field": "date", "to_application": lambda v: arrow.get(v).date()},
+            "musicbrainz_albumid": {},
+            "musicbrainz_artistid": {},
+            "musicbrainz_recordingid": {"field": "musicbrainz_trackid"},
+        },
     },
-    'OggTheora': {
-        'getter': lambda f, k: f[k][0],
-        'fields': {
-            'track_number': {
-                'field': 'TRACKNUMBER',
-                'to_application': convert_track_number
-            },
-            'title': {},
-            'artist': {},
-            'album': {},
-            'date': {
-                'field': 'date',
-                'to_application': lambda v: arrow.get(v).date()
-            },
-            'musicbrainz_albumid': {
-                'field': 'MusicBrainz Album Id'
-            },
-            'musicbrainz_artistid': {
-                'field': 'MusicBrainz Artist Id'
-            },
-            'musicbrainz_recordingid': {
-                'field': 'MusicBrainz Track Id'
-            },
-        }
+    "OggTheora": {
+        "getter": lambda f, k: f[k][0],
+        "fields": {
+            "track_number": {
+                "field": "TRACKNUMBER",
+                "to_application": convert_track_number,
+            },
+            "title": {},
+            "artist": {},
+            "album": {},
+            "date": {"field": "date", "to_application": lambda v: arrow.get(v).date()},
+            "musicbrainz_albumid": {"field": "MusicBrainz Album Id"},
+            "musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
+            "musicbrainz_recordingid": {"field": "MusicBrainz Track Id"},
+        },
     },
-    'MP3': {
-        'getter': get_id3_tag,
-        'clean_pictures': clean_id3_pictures,
-        'fields': {
-            'track_number': {
-                'field': 'TRCK',
-                'to_application': convert_track_number
-            },
-            'title': {
-                'field': 'TIT2'
-            },
-            'artist': {
-                'field': 'TPE1'
-            },
-            'album': {
-                'field': 'TALB'
-            },
-            'date': {
-                'field': 'TDRC',
-                'to_application': lambda v: arrow.get(str(v)).date()
-            },
-            'musicbrainz_albumid': {
-                'field': 'MusicBrainz Album Id'
-            },
-            'musicbrainz_artistid': {
-                'field': 'MusicBrainz Artist Id'
-            },
-            'musicbrainz_recordingid': {
-                'field': 'UFID',
-                'getter': get_mp3_recording_id,
-            },
-            'pictures': {},
-        }
+    "MP3": {
+        "getter": get_id3_tag,
+        "clean_pictures": clean_id3_pictures,
+        "fields": {
+            "track_number": {"field": "TRCK", "to_application": convert_track_number},
+            "title": {"field": "TIT2"},
+            "artist": {"field": "TPE1"},
+            "album": {"field": "TALB"},
+            "date": {
+                "field": "TDRC",
+                "to_application": lambda v: arrow.get(str(v)).date(),
+            },
+            "musicbrainz_albumid": {"field": "MusicBrainz Album Id"},
+            "musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
+            "musicbrainz_recordingid": {
+                "field": "UFID",
+                "getter": get_mp3_recording_id,
+            },
+            "pictures": {},
+        },
     },
-    'FLAC': {
-        'getter': get_flac_tag,
-        'clean_pictures': clean_flac_pictures,
-        'fields': {
-            'track_number': {
-                'field': 'tracknumber',
-                'to_application': convert_track_number
-            },
-            'title': {},
-            'artist': {},
-            'album': {},
-            'date': {
-                'field': 'date',
-                'to_application': lambda v: arrow.get(str(v)).date()
-            },
-            'musicbrainz_albumid': {},
-            'musicbrainz_artistid': {},
-            'musicbrainz_recordingid': {
-                'field': 'musicbrainz_trackid'
-            },
-            'test': {},
-            'pictures': {},
-        }
+    "FLAC": {
+        "getter": get_flac_tag,
+        "clean_pictures": clean_flac_pictures,
+        "fields": {
+            "track_number": {
+                "field": "tracknumber",
+                "to_application": convert_track_number,
+            },
+            "title": {},
+            "artist": {},
+            "album": {},
+            "date": {
+                "field": "date",
+                "to_application": lambda v: arrow.get(str(v)).date(),
+            },
+            "musicbrainz_albumid": {},
+            "musicbrainz_artistid": {},
+            "musicbrainz_recordingid": {"field": "musicbrainz_trackid"},
+            "test": {},
+            "pictures": {},
+        },
     },
 }
 
 
 class Metadata(object):
-
     def __init__(self, path):
         self._file = mutagen.File(path)
         if self._file is None:
-            raise ValueError('Cannot parse metadata from {}'.format(path))
+            raise ValueError("Cannot parse metadata from {}".format(path))
         ft = self.get_file_type(self._file)
         try:
             self._conf = CONF[ft]
         except KeyError:
-            raise ValueError('Unsupported format {}'.format(ft))
+            raise ValueError("Unsupported format {}".format(ft))
 
     def get_file_type(self, f):
         return f.__class__.__name__
 
     def get(self, key, default=NODEFAULT):
         try:
-            field_conf = self._conf['fields'][key]
+            field_conf = self._conf["fields"][key]
         except KeyError:
-            raise UnsupportedTag(
-                '{} is not supported for this file format'.format(key))
-        real_key = field_conf.get('field', key)
+            raise UnsupportedTag("{} is not supported for this file format".format(key))
+        real_key = field_conf.get("field", key)
         try:
-            getter = field_conf.get('getter', self._conf['getter'])
+            getter = field_conf.get("getter", self._conf["getter"])
             v = getter(self._file, real_key)
         except KeyError:
             if default == NODEFAULT:
                 raise TagNotFound(real_key)
             return default
 
-        converter = field_conf.get('to_application')
+        converter = field_conf.get("to_application")
         if converter:
             v = converter(v)
         field = VALIDATION.get(key)
@@ -256,15 +222,15 @@ class Metadata(object):
             v = field.to_python(v)
         return v
 
-    def get_picture(self, picture_type='cover_front'):
+    def get_picture(self, picture_type="cover_front"):
         ptype = getattr(mutagen.id3.PictureType, picture_type.upper())
         try:
-            pictures = self.get('pictures')
+            pictures = self.get("pictures")
         except (UnsupportedTag, TagNotFound):
             return
 
-        cleaner = self._conf.get('clean_pictures', lambda v: v)
+        cleaner = self._conf.get("clean_pictures", lambda v: v)
         pictures = cleaner(pictures)
         for p in pictures:
-            if p['type'] == ptype:
+            if p["type"] == ptype:
                 return p
diff --git a/api/funkwhale_api/music/migrations/0001_initial.py b/api/funkwhale_api/music/migrations/0001_initial.py
index 265b81577f7ca52a4cb37e48f499b96cc4622d5b..0bb12342ded06420064a06cdead37e627c799dc9 100644
--- a/api/funkwhale_api/music/migrations/0001_initial.py
+++ b/api/funkwhale_api/music/migrations/0001_initial.py
@@ -8,82 +8,183 @@ import django.utils.timezone
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-    ]
+    dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
 
     operations = [
         migrations.CreateModel(
-            name='Album',
+            name="Album",
             fields=[
-                ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
-                ('mbid', models.UUIDField(editable=False, blank=True, null=True)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('title', models.CharField(max_length=255)),
-                ('release_date', models.DateField()),
-                ('type', models.CharField(default='album', choices=[('album', 'Album')], max_length=30)),
+                (
+                    "id",
+                    models.AutoField(
+                        primary_key=True,
+                        auto_created=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("mbid", models.UUIDField(editable=False, blank=True, null=True)),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("title", models.CharField(max_length=255)),
+                ("release_date", models.DateField()),
+                (
+                    "type",
+                    models.CharField(
+                        default="album", choices=[("album", "Album")], max_length=30
+                    ),
+                ),
             ],
-            options={
-                'abstract': False,
-            },
+            options={"abstract": False},
         ),
         migrations.CreateModel(
-            name='Artist',
+            name="Artist",
             fields=[
-                ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
-                ('mbid', models.UUIDField(editable=False, blank=True, null=True)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('name', models.CharField(max_length=255)),
+                (
+                    "id",
+                    models.AutoField(
+                        primary_key=True,
+                        auto_created=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("mbid", models.UUIDField(editable=False, blank=True, null=True)),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("name", models.CharField(max_length=255)),
             ],
-            options={
-                'abstract': False,
-            },
+            options={"abstract": False},
         ),
         migrations.CreateModel(
-            name='ImportBatch',
+            name="ImportBatch",
             fields=[
-                ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('submitted_by', models.ForeignKey(related_name='imports', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
+                (
+                    "id",
+                    models.AutoField(
+                        primary_key=True,
+                        auto_created=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                (
+                    "submitted_by",
+                    models.ForeignKey(
+                        related_name="imports",
+                        to=settings.AUTH_USER_MODEL,
+                        on_delete=models.CASCADE,
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
-            name='ImportJob',
+            name="ImportJob",
             fields=[
-                ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
-                ('source', models.URLField()),
-                ('mbid', models.UUIDField(editable=False)),
-                ('status', models.CharField(default='pending', choices=[('pending', 'Pending'), ('finished', 'finished')], max_length=30)),
-                ('batch', models.ForeignKey(related_name='jobs', to='music.ImportBatch', on_delete=models.CASCADE)),
+                (
+                    "id",
+                    models.AutoField(
+                        primary_key=True,
+                        auto_created=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("source", models.URLField()),
+                ("mbid", models.UUIDField(editable=False)),
+                (
+                    "status",
+                    models.CharField(
+                        default="pending",
+                        choices=[("pending", "Pending"), ("finished", "finished")],
+                        max_length=30,
+                    ),
+                ),
+                (
+                    "batch",
+                    models.ForeignKey(
+                        related_name="jobs",
+                        to="music.ImportBatch",
+                        on_delete=models.CASCADE,
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
-            name='Track',
+            name="Track",
             fields=[
-                ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
-                ('mbid', models.UUIDField(editable=False, blank=True, null=True)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('title', models.CharField(max_length=255)),
-                ('album', models.ForeignKey(related_name='tracks', blank=True, null=True, to='music.Album', on_delete=models.CASCADE)),
-                ('artist', models.ForeignKey(related_name='tracks', to='music.Artist', on_delete=models.CASCADE)),
+                (
+                    "id",
+                    models.AutoField(
+                        primary_key=True,
+                        auto_created=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("mbid", models.UUIDField(editable=False, blank=True, null=True)),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("title", models.CharField(max_length=255)),
+                (
+                    "album",
+                    models.ForeignKey(
+                        related_name="tracks",
+                        blank=True,
+                        null=True,
+                        to="music.Album",
+                        on_delete=models.CASCADE,
+                    ),
+                ),
+                (
+                    "artist",
+                    models.ForeignKey(
+                        related_name="tracks",
+                        to="music.Artist",
+                        on_delete=models.CASCADE,
+                    ),
+                ),
             ],
-            options={
-                'abstract': False,
-            },
+            options={"abstract": False},
         ),
         migrations.CreateModel(
-            name='TrackFile',
+            name="TrackFile",
             fields=[
-                ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')),
-                ('audio_file', models.FileField(upload_to='tracks')),
-                ('source', models.URLField(blank=True, null=True)),
-                ('duration', models.IntegerField(blank=True, null=True)),
-                ('track', models.ForeignKey(related_name='files', to='music.Track', on_delete=models.CASCADE)),
+                (
+                    "id",
+                    models.AutoField(
+                        primary_key=True,
+                        auto_created=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("audio_file", models.FileField(upload_to="tracks")),
+                ("source", models.URLField(blank=True, null=True)),
+                ("duration", models.IntegerField(blank=True, null=True)),
+                (
+                    "track",
+                    models.ForeignKey(
+                        related_name="files", to="music.Track", on_delete=models.CASCADE
+                    ),
+                ),
             ],
         ),
         migrations.AddField(
-            model_name='album',
-            name='artist',
-            field=models.ForeignKey(related_name='albums', to='music.Artist', on_delete=models.CASCADE),
+            model_name="album",
+            name="artist",
+            field=models.ForeignKey(
+                related_name="albums", to="music.Artist", on_delete=models.CASCADE
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/music/migrations/0002_auto_20151215_1645.py b/api/funkwhale_api/music/migrations/0002_auto_20151215_1645.py
index 1b54a5cfc742cbcf8e6058d1b796c62c60380ab6..094c679dad70901ce7e050cccbc30f0fe9913279 100644
--- a/api/funkwhale_api/music/migrations/0002_auto_20151215_1645.py
+++ b/api/funkwhale_api/music/migrations/0002_auto_20151215_1645.py
@@ -6,35 +6,31 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0001_initial'),
-    ]
+    dependencies = [("music", "0001_initial")]
 
     operations = [
         migrations.AlterModelOptions(
-            name='album',
-            options={'ordering': ['-creation_date']},
+            name="album", options={"ordering": ["-creation_date"]}
         ),
         migrations.AlterModelOptions(
-            name='artist',
-            options={'ordering': ['-creation_date']},
+            name="artist", options={"ordering": ["-creation_date"]}
         ),
         migrations.AlterModelOptions(
-            name='importbatch',
-            options={'ordering': ['-creation_date']},
+            name="importbatch", options={"ordering": ["-creation_date"]}
         ),
         migrations.AlterModelOptions(
-            name='track',
-            options={'ordering': ['-creation_date']},
+            name="track", options={"ordering": ["-creation_date"]}
         ),
         migrations.AddField(
-            model_name='album',
-            name='cover',
-            field=models.ImageField(upload_to='albums/covers/%Y/%m/%d', null=True, blank=True),
+            model_name="album",
+            name="cover",
+            field=models.ImageField(
+                upload_to="albums/covers/%Y/%m/%d", null=True, blank=True
+            ),
         ),
         migrations.AlterField(
-            model_name='trackfile',
-            name='audio_file',
-            field=models.FileField(upload_to='tracks/%Y/%m/%d'),
+            model_name="trackfile",
+            name="audio_file",
+            field=models.FileField(upload_to="tracks/%Y/%m/%d"),
         ),
     ]
diff --git a/api/funkwhale_api/music/migrations/0003_auto_20151222_2233.py b/api/funkwhale_api/music/migrations/0003_auto_20151222_2233.py
index 060957dc16e1f5f8d7dedf5d46a50d3a6568d50e..d8337a7816144abc969bfedca77fc79db3d14348 100644
--- a/api/funkwhale_api/music/migrations/0003_auto_20151222_2233.py
+++ b/api/funkwhale_api/music/migrations/0003_auto_20151222_2233.py
@@ -6,14 +6,10 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0002_auto_20151215_1645'),
-    ]
+    dependencies = [("music", "0002_auto_20151215_1645")]
 
     operations = [
         migrations.AlterField(
-            model_name='album',
-            name='release_date',
-            field=models.DateField(null=True),
-        ),
+            model_name="album", name="release_date", field=models.DateField(null=True)
+        )
     ]
diff --git a/api/funkwhale_api/music/migrations/0004_track_tags.py b/api/funkwhale_api/music/migrations/0004_track_tags.py
index f95b08b0e8f4dd5105fbd19ae5ea320d51f4f6ea..b999a70313ce7951fdfed67c534592c55cb834e1 100644
--- a/api/funkwhale_api/music/migrations/0004_track_tags.py
+++ b/api/funkwhale_api/music/migrations/0004_track_tags.py
@@ -1,21 +1,26 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
-from django.db import migrations, models
+from django.db import migrations
 import taggit.managers
 
 
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('taggit', '0002_auto_20150616_2121'),
-        ('music', '0003_auto_20151222_2233'),
+        ("taggit", "0002_auto_20150616_2121"),
+        ("music", "0003_auto_20151222_2233"),
     ]
 
     operations = [
         migrations.AddField(
-            model_name='track',
-            name='tags',
-            field=taggit.managers.TaggableManager(verbose_name='Tags', help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag'),
-        ),
+            model_name="track",
+            name="tags",
+            field=taggit.managers.TaggableManager(
+                verbose_name="Tags",
+                help_text="A comma-separated list of tags.",
+                through="taggit.TaggedItem",
+                to="taggit.Tag",
+            ),
+        )
     ]
diff --git a/api/funkwhale_api/music/migrations/0005_deduplicate.py b/api/funkwhale_api/music/migrations/0005_deduplicate.py
index 82dca0caacbdbc8ff99898feb9e159c6efd563e3..0dfdd78f402ac580909961bae0974d4722d7a0c8 100644
--- a/api/funkwhale_api/music/migrations/0005_deduplicate.py
+++ b/api/funkwhale_api/music/migrations/0005_deduplicate.py
@@ -5,7 +5,14 @@ from django.db import migrations, models
 
 
 def get_duplicates(model):
-    return [i['mbid'] for i in model.objects.values('mbid').annotate(idcount=models.Count('mbid')).order_by('-idcount') if i['idcount'] > 1]
+    return [
+        i["mbid"]
+        for i in model.objects.values("mbid")
+        .annotate(idcount=models.Count("mbid"))
+        .order_by("-idcount")
+        if i["idcount"] > 1
+    ]
+
 
 def deduplicate(apps, schema_editor):
     Artist = apps.get_model("music", "Artist")
@@ -13,28 +20,25 @@ def deduplicate(apps, schema_editor):
     Track = apps.get_model("music", "Track")
 
     for mbid in get_duplicates(Artist):
-        ref = Artist.objects.filter(mbid=mbid).order_by('pk').first()
+        ref = Artist.objects.filter(mbid=mbid).order_by("pk").first()
         duplicates = Artist.objects.filter(mbid=mbid).exclude(pk=ref.pk)
         Album.objects.filter(artist__in=duplicates).update(artist=ref)
         Track.objects.filter(artist__in=duplicates).update(artist=ref)
         duplicates.delete()
 
     for mbid in get_duplicates(Album):
-        ref = Album.objects.filter(mbid=mbid).order_by('pk').first()
+        ref = Album.objects.filter(mbid=mbid).order_by("pk").first()
         duplicates = Album.objects.filter(mbid=mbid).exclude(pk=ref.pk)
         Track.objects.filter(album__in=duplicates).update(album=ref)
         duplicates.delete()
 
+
 def rewind(*args, **kwargs):
     pass
 
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0004_track_tags'),
-    ]
+    dependencies = [("music", "0004_track_tags")]
 
-    operations = [
-        migrations.RunPython(deduplicate, rewind),
-    ]
+    operations = [migrations.RunPython(deduplicate, rewind)]
diff --git a/api/funkwhale_api/music/migrations/0006_unique_mbid.py b/api/funkwhale_api/music/migrations/0006_unique_mbid.py
index e13e3a743b503f212e6d17d6799c6dbcf66df2b9..7d926e373155cfe2976b18ce37bcf0b7610f3e69 100644
--- a/api/funkwhale_api/music/migrations/0006_unique_mbid.py
+++ b/api/funkwhale_api/music/migrations/0006_unique_mbid.py
@@ -3,26 +3,31 @@ from __future__ import unicode_literals
 
 from django.db import migrations, models
 
+
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0005_deduplicate'),
-    ]
+    dependencies = [("music", "0005_deduplicate")]
 
     operations = [
         migrations.AlterField(
-            model_name='album',
-            name='mbid',
-            field=models.UUIDField(null=True, editable=False, unique=True, blank=True, db_index=True),
+            model_name="album",
+            name="mbid",
+            field=models.UUIDField(
+                null=True, editable=False, unique=True, blank=True, db_index=True
+            ),
         ),
         migrations.AlterField(
-            model_name='artist',
-            name='mbid',
-            field=models.UUIDField(null=True, editable=False, unique=True, blank=True, db_index=True),
+            model_name="artist",
+            name="mbid",
+            field=models.UUIDField(
+                null=True, editable=False, unique=True, blank=True, db_index=True
+            ),
         ),
         migrations.AlterField(
-            model_name='track',
-            name='mbid',
-            field=models.UUIDField(null=True, editable=False, unique=True, blank=True, db_index=True),
+            model_name="track",
+            name="mbid",
+            field=models.UUIDField(
+                null=True, editable=False, unique=True, blank=True, db_index=True
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/music/migrations/0007_track_position.py b/api/funkwhale_api/music/migrations/0007_track_position.py
index 089e0128f319d7957406cf843f411cb6269142e1..d43dcaea383caa7f7c0217b145f68a2908a31e7f 100644
--- a/api/funkwhale_api/music/migrations/0007_track_position.py
+++ b/api/funkwhale_api/music/migrations/0007_track_position.py
@@ -6,14 +6,12 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0006_unique_mbid'),
-    ]
+    dependencies = [("music", "0006_unique_mbid")]
 
     operations = [
         migrations.AddField(
-            model_name='track',
-            name='position',
+            model_name="track",
+            name="position",
             field=models.PositiveIntegerField(blank=True, null=True),
-        ),
+        )
     ]
diff --git a/api/funkwhale_api/music/migrations/0008_auto_20160529_1456.py b/api/funkwhale_api/music/migrations/0008_auto_20160529_1456.py
index e7fa5c8f486a9ea256e87b030ae08d66aab1432c..8812c65a4595b6739b49d4258dba8c33d18ca5c1 100644
--- a/api/funkwhale_api/music/migrations/0008_auto_20160529_1456.py
+++ b/api/funkwhale_api/music/migrations/0008_auto_20160529_1456.py
@@ -6,24 +6,22 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0007_track_position'),
-    ]
+    dependencies = [("music", "0007_track_position")]
 
     operations = [
         migrations.AlterField(
-            model_name='album',
-            name='mbid',
+            model_name="album",
+            name="mbid",
             field=models.UUIDField(null=True, db_index=True, unique=True, blank=True),
         ),
         migrations.AlterField(
-            model_name='artist',
-            name='mbid',
+            model_name="artist",
+            name="mbid",
             field=models.UUIDField(null=True, db_index=True, unique=True, blank=True),
         ),
         migrations.AlterField(
-            model_name='track',
-            name='mbid',
+            model_name="track",
+            name="mbid",
             field=models.UUIDField(null=True, db_index=True, unique=True, blank=True),
         ),
     ]
diff --git a/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py b/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py
index 3a3d93989cdd436a6c9cbed010d1140e7bd933e1..eff0f82a032af7d5441b0d885db5aca3b477f31a 100644
--- a/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py
+++ b/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py
@@ -3,47 +3,75 @@ from __future__ import unicode_literals
 
 from django.db import migrations, models
 import django.utils.timezone
-import versatileimagefield.fields
 
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0008_auto_20160529_1456'),
-    ]
+    dependencies = [("music", "0008_auto_20160529_1456")]
 
     operations = [
         migrations.CreateModel(
-            name='Lyrics',
+            name="Lyrics",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)),
-                ('url', models.URLField()),
-                ('content', models.TextField(null=True, blank=True)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        verbose_name="ID",
+                        serialize=False,
+                    ),
+                ),
+                ("url", models.URLField()),
+                ("content", models.TextField(null=True, blank=True)),
             ],
         ),
         migrations.CreateModel(
-            name='Work',
+            name="Work",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)),
-                ('mbid', models.UUIDField(unique=True, null=True, db_index=True, blank=True)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('language', models.CharField(max_length=20)),
-                ('nature', models.CharField(max_length=50)),
-                ('title', models.CharField(max_length=255)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        verbose_name="ID",
+                        serialize=False,
+                    ),
+                ),
+                (
+                    "mbid",
+                    models.UUIDField(unique=True, null=True, db_index=True, blank=True),
+                ),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("language", models.CharField(max_length=20)),
+                ("nature", models.CharField(max_length=50)),
+                ("title", models.CharField(max_length=255)),
             ],
-            options={
-                'ordering': ['-creation_date'],
-                'abstract': False,
-            },
+            options={"ordering": ["-creation_date"], "abstract": False},
         ),
         migrations.AddField(
-            model_name='lyrics',
-            name='work',
-            field=models.ForeignKey(related_name='lyrics', to='music.Work', blank=True, null=True, on_delete=models.CASCADE),
+            model_name="lyrics",
+            name="work",
+            field=models.ForeignKey(
+                related_name="lyrics",
+                to="music.Work",
+                blank=True,
+                null=True,
+                on_delete=models.CASCADE,
+            ),
         ),
         migrations.AddField(
-            model_name='track',
-            name='work',
-            field=models.ForeignKey(related_name='tracks', to='music.Work', blank=True, null=True, on_delete=models.CASCADE),
+            model_name="track",
+            name="work",
+            field=models.ForeignKey(
+                related_name="tracks",
+                to="music.Work",
+                blank=True,
+                null=True,
+                on_delete=models.CASCADE,
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/music/migrations/0010_auto_20160920_1742.py b/api/funkwhale_api/music/migrations/0010_auto_20160920_1742.py
index 03ac057935ff16c90fdcbc479c548b95dadf13eb..2b5ce935b7f44f10d6601e1e9e0bae54b8e95e29 100644
--- a/api/funkwhale_api/music/migrations/0010_auto_20160920_1742.py
+++ b/api/funkwhale_api/music/migrations/0010_auto_20160920_1742.py
@@ -2,19 +2,14 @@
 from __future__ import unicode_literals
 
 from django.db import migrations, models
-import versatileimagefield.fields
 
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0009_auto_20160920_1614'),
-    ]
+    dependencies = [("music", "0009_auto_20160920_1614")]
 
     operations = [
         migrations.AlterField(
-            model_name='lyrics',
-            name='url',
-            field=models.URLField(unique=True),
-        ),
+            model_name="lyrics", name="url", field=models.URLField(unique=True)
+        )
     ]
diff --git a/api/funkwhale_api/music/migrations/0011_rename_files.py b/api/funkwhale_api/music/migrations/0011_rename_files.py
index 1c59535f5e72a9a2695d74cae71ceb47bda81c5e..2aafb126c5795ed95e889c31f11ba31eb44faf5f 100644
--- a/api/funkwhale_api/music/migrations/0011_rename_files.py
+++ b/api/funkwhale_api/music/migrations/0011_rename_files.py
@@ -1,9 +1,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
-import os
 
 from django.db import migrations, models
-from funkwhale_api.common.utils import rename_file
 
 
 def rename_files(apps, schema_editor):
@@ -47,15 +45,13 @@ def rewind(apps, schema_editor):
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0010_auto_20160920_1742'),
-    ]
+    dependencies = [("music", "0010_auto_20160920_1742")]
 
     operations = [
         migrations.AlterField(
-            model_name='trackfile',
-            name='audio_file',
-            field=models.FileField(upload_to='tracks/%Y/%m/%d', max_length=255),
+            model_name="trackfile",
+            name="audio_file",
+            field=models.FileField(upload_to="tracks/%Y/%m/%d", max_length=255),
         ),
         migrations.RunPython(rename_files, rewind),
     ]
diff --git a/api/funkwhale_api/music/migrations/0012_auto_20161122_1905.py b/api/funkwhale_api/music/migrations/0012_auto_20161122_1905.py
index 8d7e25246bbea637c2f5d91fbac7b06cab851af1..0cf1e44f074f4b0d7ffb0ae6bdd12cb783f51123 100644
--- a/api/funkwhale_api/music/migrations/0012_auto_20161122_1905.py
+++ b/api/funkwhale_api/music/migrations/0012_auto_20161122_1905.py
@@ -1,20 +1,20 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
-from django.db import migrations, models
+from django.db import migrations
 import versatileimagefield.fields
 
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0011_rename_files'),
-    ]
+    dependencies = [("music", "0011_rename_files")]
 
     operations = [
         migrations.AlterField(
-            model_name='album',
-            name='cover',
-            field=versatileimagefield.fields.VersatileImageField(null=True, blank=True, upload_to='albums/covers/%Y/%m/%d'),
-        ),
+            model_name="album",
+            name="cover",
+            field=versatileimagefield.fields.VersatileImageField(
+                null=True, blank=True, upload_to="albums/covers/%Y/%m/%d"
+            ),
+        )
     ]
diff --git a/api/funkwhale_api/music/migrations/0013_auto_20171213_2211.py b/api/funkwhale_api/music/migrations/0013_auto_20171213_2211.py
index 00ccbb6218d303d95b65273c058f596dbd7e3da6..2874aa81f9bdefdc6b8488f4c18f5ad0088236cd 100644
--- a/api/funkwhale_api/music/migrations/0013_auto_20171213_2211.py
+++ b/api/funkwhale_api/music/migrations/0013_auto_20171213_2211.py
@@ -7,22 +7,16 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0012_auto_20161122_1905'),
-    ]
+    dependencies = [("music", "0012_auto_20161122_1905")]
 
     operations = [
+        migrations.AlterModelOptions(name="importjob", options={"ordering": ("id",)}),
         migrations.AlterModelOptions(
-            name='importjob',
-            options={'ordering': ('id',)},
-        ),
-        migrations.AlterModelOptions(
-            name='track',
-            options={'ordering': ['album', 'position']},
+            name="track", options={"ordering": ["album", "position"]}
         ),
         migrations.AddField(
-            model_name='album',
-            name='release_group_id',
+            model_name="album",
+            name="release_group_id",
             field=models.UUIDField(blank=True, null=True),
         ),
     ]
diff --git a/api/funkwhale_api/music/migrations/0014_importjob_track_file.py b/api/funkwhale_api/music/migrations/0014_importjob_track_file.py
index 6950fd3c1d55d0f5352d04cf217068d30b127381..004e247eae7dfbe88fed38dda086f0d579f4c5ee 100644
--- a/api/funkwhale_api/music/migrations/0014_importjob_track_file.py
+++ b/api/funkwhale_api/music/migrations/0014_importjob_track_file.py
@@ -8,14 +8,18 @@ import django.db.models.deletion
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0013_auto_20171213_2211'),
-    ]
+    dependencies = [("music", "0013_auto_20171213_2211")]
 
     operations = [
         migrations.AddField(
-            model_name='importjob',
-            name='track_file',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='music.TrackFile'),
-        ),
+            model_name="importjob",
+            name="track_file",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="jobs",
+                to="music.TrackFile",
+            ),
+        )
     ]
diff --git a/api/funkwhale_api/music/migrations/0015_bind_track_file_to_import_job.py b/api/funkwhale_api/music/migrations/0015_bind_track_file_to_import_job.py
index edb5e6470dc87de3c282ea362d1e247adb7c75cd..c8bd1c5e3c3a464cdd6860f0cf3e1e6a9b0f843c 100644
--- a/api/funkwhale_api/music/migrations/0015_bind_track_file_to_import_job.py
+++ b/api/funkwhale_api/music/migrations/0015_bind_track_file_to_import_job.py
@@ -1,22 +1,20 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
-import os
 
-from django.db import migrations, models
-from funkwhale_api.common.utils import rename_file
+from django.db import migrations
 
 
 def bind_jobs(apps, schema_editor):
     TrackFile = apps.get_model("music", "TrackFile")
     ImportJob = apps.get_model("music", "ImportJob")
 
-    for job in ImportJob.objects.all().only('mbid'):
+    for job in ImportJob.objects.all().only("mbid"):
         f = TrackFile.objects.filter(track__mbid=job.mbid).first()
         if not f:
-            print('No file for mbid {}'.format(job.mbid))
+            print("No file for mbid {}".format(job.mbid))
             continue
         job.track_file = f
-        job.save(update_fields=['track_file'])
+        job.save(update_fields=["track_file"])
 
 
 def rewind(apps, schema_editor):
@@ -25,10 +23,6 @@ def rewind(apps, schema_editor):
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0014_importjob_track_file'),
-    ]
+    dependencies = [("music", "0014_importjob_track_file")]
 
-    operations = [
-        migrations.RunPython(bind_jobs, rewind),
-    ]
+    operations = [migrations.RunPython(bind_jobs, rewind)]
diff --git a/api/funkwhale_api/music/migrations/0016_trackfile_acoustid_track_id.py b/api/funkwhale_api/music/migrations/0016_trackfile_acoustid_track_id.py
index 21d8ce8ea4dfbade6a5bef39abb36b5cb80f6d15..467fb0eef01a530ba23339251abb50c0bfcca32b 100644
--- a/api/funkwhale_api/music/migrations/0016_trackfile_acoustid_track_id.py
+++ b/api/funkwhale_api/music/migrations/0016_trackfile_acoustid_track_id.py
@@ -5,14 +5,12 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0015_bind_track_file_to_import_job'),
-    ]
+    dependencies = [("music", "0015_bind_track_file_to_import_job")]
 
     operations = [
         migrations.AddField(
-            model_name='trackfile',
-            name='acoustid_track_id',
+            model_name="trackfile",
+            name="acoustid_track_id",
             field=models.UUIDField(blank=True, null=True),
-        ),
+        )
     ]
diff --git a/api/funkwhale_api/music/migrations/0017_auto_20171227_1728.py b/api/funkwhale_api/music/migrations/0017_auto_20171227_1728.py
index dfca664370edffbcaecffd764b4f08289e21d4d1..10a8ed1e8cf993aab51af498fb9053ce5f159e68 100644
--- a/api/funkwhale_api/music/migrations/0017_auto_20171227_1728.py
+++ b/api/funkwhale_api/music/migrations/0017_auto_20171227_1728.py
@@ -5,24 +5,28 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0016_trackfile_acoustid_track_id'),
-    ]
+    dependencies = [("music", "0016_trackfile_acoustid_track_id")]
 
     operations = [
         migrations.AddField(
-            model_name='importbatch',
-            name='source',
-            field=models.CharField(choices=[('api', 'api'), ('shell', 'shell')], default='api', max_length=30),
+            model_name="importbatch",
+            name="source",
+            field=models.CharField(
+                choices=[("api", "api"), ("shell", "shell")],
+                default="api",
+                max_length=30,
+            ),
         ),
         migrations.AddField(
-            model_name='importjob',
-            name='audio_file',
-            field=models.FileField(blank=True, max_length=255, null=True, upload_to='imports/%Y/%m/%d'),
+            model_name="importjob",
+            name="audio_file",
+            field=models.FileField(
+                blank=True, max_length=255, null=True, upload_to="imports/%Y/%m/%d"
+            ),
         ),
         migrations.AlterField(
-            model_name='importjob',
-            name='mbid',
+            model_name="importjob",
+            name="mbid",
             field=models.UUIDField(blank=True, editable=False, null=True),
         ),
     ]
diff --git a/api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py b/api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py
index c45298798b87f52dfe101b1d5f4a10a94407f10b..bfc26b011738a15d3ff54254b001ce9679b20cac 100644
--- a/api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py
+++ b/api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py
@@ -5,24 +5,31 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0017_auto_20171227_1728'),
-    ]
+    dependencies = [("music", "0017_auto_20171227_1728")]
 
     operations = [
         migrations.AddField(
-            model_name='trackfile',
-            name='mimetype',
+            model_name="trackfile",
+            name="mimetype",
             field=models.CharField(blank=True, max_length=200, null=True),
         ),
         migrations.AlterField(
-            model_name='importjob',
-            name='source',
+            model_name="importjob",
+            name="source",
             field=models.CharField(max_length=500),
         ),
         migrations.AlterField(
-            model_name='importjob',
-            name='status',
-            field=models.CharField(choices=[('pending', 'Pending'), ('finished', 'Finished'), ('errored', 'Errored'), ('skipped', 'Skipped')], default='pending', max_length=30),
+            model_name="importjob",
+            name="status",
+            field=models.CharField(
+                choices=[
+                    ("pending", "Pending"),
+                    ("finished", "Finished"),
+                    ("errored", "Errored"),
+                    ("skipped", "Skipped"),
+                ],
+                default="pending",
+                max_length=30,
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/music/migrations/0019_populate_mimetypes.py b/api/funkwhale_api/music/migrations/0019_populate_mimetypes.py
index 127aa5e69a245215e2b0f16e0b5e3081efb7db49..11678efbcabfecaefc5d4a58e8459dfd4a04beca 100644
--- a/api/funkwhale_api/music/migrations/0019_populate_mimetypes.py
+++ b/api/funkwhale_api/music/migrations/0019_populate_mimetypes.py
@@ -1,22 +1,23 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
-import os
 
-from django.db import migrations, models
+from django.db import migrations
 from funkwhale_api.music.utils import guess_mimetype
 
 
 def populate_mimetype(apps, schema_editor):
     TrackFile = apps.get_model("music", "TrackFile")
 
-    for tf in TrackFile.objects.filter(audio_file__isnull=False, mimetype__isnull=True).only('audio_file'):
+    for tf in TrackFile.objects.filter(
+        audio_file__isnull=False, mimetype__isnull=True
+    ).only("audio_file"):
         try:
             tf.mimetype = guess_mimetype(tf.audio_file)
         except Exception as e:
-            print('Error on track file {}: {}'.format(tf.pk, e))
+            print("Error on track file {}: {}".format(tf.pk, e))
             continue
-        print('Track file {}: {}'.format(tf.pk, tf.mimetype))
-        tf.save(update_fields=['mimetype'])
+        print("Track file {}: {}".format(tf.pk, tf.mimetype))
+        tf.save(update_fields=["mimetype"])
 
 
 def rewind(apps, schema_editor):
@@ -25,10 +26,6 @@ def rewind(apps, schema_editor):
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0018_auto_20180218_1554'),
-    ]
+    dependencies = [("music", "0018_auto_20180218_1554")]
 
-    operations = [
-        migrations.RunPython(populate_mimetype, rewind),
-    ]
+    operations = [migrations.RunPython(populate_mimetype, rewind)]
diff --git a/api/funkwhale_api/music/migrations/0020_importbatch_status.py b/api/funkwhale_api/music/migrations/0020_importbatch_status.py
index 265d1ba5d5312d086f25d0861039b3a94a5df4e5..e02aa0859f1b9a33f45a3e276b8769b6c1cb63e4 100644
--- a/api/funkwhale_api/music/migrations/0020_importbatch_status.py
+++ b/api/funkwhale_api/music/migrations/0020_importbatch_status.py
@@ -5,14 +5,21 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0019_populate_mimetypes'),
-    ]
+    dependencies = [("music", "0019_populate_mimetypes")]
 
     operations = [
         migrations.AddField(
-            model_name='importbatch',
-            name='status',
-            field=models.CharField(choices=[('pending', 'Pending'), ('finished', 'Finished'), ('errored', 'Errored'), ('skipped', 'Skipped')], default='pending', max_length=30),
-        ),
+            model_name="importbatch",
+            name="status",
+            field=models.CharField(
+                choices=[
+                    ("pending", "Pending"),
+                    ("finished", "Finished"),
+                    ("errored", "Errored"),
+                    ("skipped", "Skipped"),
+                ],
+                default="pending",
+                max_length=30,
+            ),
+        )
     ]
diff --git a/api/funkwhale_api/music/migrations/0021_populate_batch_status.py b/api/funkwhale_api/music/migrations/0021_populate_batch_status.py
index 061d649b06115d4871b6c6eb7ae57b25c27661e8..065384a97c7858c30107cbdb31e2b007ca2d10a9 100644
--- a/api/funkwhale_api/music/migrations/0021_populate_batch_status.py
+++ b/api/funkwhale_api/music/migrations/0021_populate_batch_status.py
@@ -1,17 +1,17 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
-import os
 
-from django.db import migrations, models
+from django.db import migrations
 
 
 def populate_status(apps, schema_editor):
     from funkwhale_api.music.utils import compute_status
+
     ImportBatch = apps.get_model("music", "ImportBatch")
 
-    for ib in ImportBatch.objects.prefetch_related('jobs'):
+    for ib in ImportBatch.objects.prefetch_related("jobs"):
         ib.status = compute_status(ib.jobs.all())
-        ib.save(update_fields=['status'])
+        ib.save(update_fields=["status"])
 
 
 def rewind(apps, schema_editor):
@@ -20,10 +20,6 @@ def rewind(apps, schema_editor):
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0020_importbatch_status'),
-    ]
+    dependencies = [("music", "0020_importbatch_status")]
 
-    operations = [
-        migrations.RunPython(populate_status, rewind),
-    ]
+    operations = [migrations.RunPython(populate_status, rewind)]
diff --git a/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py b/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py
index d9f6f01d9121f1148e1f3d5a729b1fc476a89f42..89fca02d62b87f14052c0037aa99759b4099d974 100644
--- a/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py
+++ b/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py
@@ -6,15 +6,18 @@ import django.db.models.deletion
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('requests', '__first__'),
-        ('music', '0021_populate_batch_status'),
-    ]
+    dependencies = [("requests", "__first__"), ("music", "0021_populate_batch_status")]
 
     operations = [
         migrations.AddField(
-            model_name='importbatch',
-            name='import_request',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='import_batches', to='requests.ImportRequest'),
-        ),
+            model_name="importbatch",
+            name="import_request",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="import_batches",
+                to="requests.ImportRequest",
+            ),
+        )
     ]
diff --git a/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py b/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py
index ed7404ac4f396b32fc43e0879391649333ae52ce..8c6537d855fa6e518f177fc99c7c69bbff7533e0 100644
--- a/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py
+++ b/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py
@@ -9,79 +9,105 @@ import django.utils.timezone
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('federation', '0003_auto_20180407_1010'),
-        ('music', '0022_importbatch_import_request'),
+        ("federation", "0003_auto_20180407_1010"),
+        ("music", "0022_importbatch_import_request"),
     ]
 
     operations = [
         migrations.AddField(
-            model_name='album',
-            name='uuid',
+            model_name="album",
+            name="uuid",
             field=models.UUIDField(db_index=True, null=True, unique=True),
         ),
         migrations.AddField(
-            model_name='artist',
-            name='uuid',
+            model_name="artist",
+            name="uuid",
             field=models.UUIDField(db_index=True, null=True, unique=True),
         ),
         migrations.AddField(
-            model_name='importbatch',
-            name='uuid',
+            model_name="importbatch",
+            name="uuid",
             field=models.UUIDField(db_index=True, null=True, unique=True),
         ),
         migrations.AddField(
-            model_name='importjob',
-            name='library_track',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='import_jobs', to='federation.LibraryTrack'),
+            model_name="importjob",
+            name="library_track",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name="import_jobs",
+                to="federation.LibraryTrack",
+            ),
         ),
         migrations.AddField(
-            model_name='importjob',
-            name='uuid',
+            model_name="importjob",
+            name="uuid",
             field=models.UUIDField(db_index=True, null=True, unique=True),
         ),
         migrations.AddField(
-            model_name='lyrics',
-            name='uuid',
+            model_name="lyrics",
+            name="uuid",
             field=models.UUIDField(db_index=True, null=True, unique=True),
         ),
         migrations.AddField(
-            model_name='track',
-            name='uuid',
+            model_name="track",
+            name="uuid",
             field=models.UUIDField(db_index=True, null=True, unique=True),
         ),
         migrations.AddField(
-            model_name='trackfile',
-            name='creation_date',
+            model_name="trackfile",
+            name="creation_date",
             field=models.DateTimeField(default=django.utils.timezone.now),
         ),
         migrations.AddField(
-            model_name='trackfile',
-            name='library_track',
-            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='local_track_file', to='federation.LibraryTrack'),
+            model_name="trackfile",
+            name="library_track",
+            field=models.OneToOneField(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="local_track_file",
+                to="federation.LibraryTrack",
+            ),
         ),
         migrations.AddField(
-            model_name='trackfile',
-            name='modification_date',
+            model_name="trackfile",
+            name="modification_date",
             field=models.DateTimeField(auto_now=True),
         ),
         migrations.AddField(
-            model_name='trackfile',
-            name='uuid',
+            model_name="trackfile",
+            name="uuid",
             field=models.UUIDField(db_index=True, null=True, unique=True),
         ),
         migrations.AddField(
-            model_name='work',
-            name='uuid',
+            model_name="work",
+            name="uuid",
             field=models.UUIDField(db_index=True, null=True, unique=True),
         ),
         migrations.AlterField(
-            model_name='importbatch',
-            name='source',
-            field=models.CharField(choices=[('api', 'api'), ('shell', 'shell'), ('federation', 'federation')], default='api', max_length=30),
+            model_name="importbatch",
+            name="source",
+            field=models.CharField(
+                choices=[
+                    ("api", "api"),
+                    ("shell", "shell"),
+                    ("federation", "federation"),
+                ],
+                default="api",
+                max_length=30,
+            ),
         ),
         migrations.AlterField(
-            model_name='importbatch',
-            name='submitted_by',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='imports', to=settings.AUTH_USER_MODEL),
+            model_name="importbatch",
+            name="submitted_by",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="imports",
+                to=settings.AUTH_USER_MODEL,
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/music/migrations/0024_populate_uuid.py b/api/funkwhale_api/music/migrations/0024_populate_uuid.py
index 10c78a3db03a9b3be2b8fca24629ae5e58bcd558..63ab6386257bdbd3dc51a9b57ffbbbacc4208286 100644
--- a/api/funkwhale_api/music/migrations/0024_populate_uuid.py
+++ b/api/funkwhale_api/music/migrations/0024_populate_uuid.py
@@ -1,28 +1,27 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
-import os
 import uuid
 from django.db import migrations, models
 
 
 def populate_uuids(apps, schema_editor):
     models = [
-        'Album',
-        'Artist',
-        'Importbatch',
-        'Importjob',
-        'Lyrics',
-        'Track',
-        'Trackfile',
-        'Work',
+        "Album",
+        "Artist",
+        "Importbatch",
+        "Importjob",
+        "Lyrics",
+        "Track",
+        "Trackfile",
+        "Work",
     ]
     for m in models:
-        kls = apps.get_model('music', m)
-        qs = kls.objects.filter(uuid__isnull=True).only('id')
-        print('Setting uuids for {} ({} objects)'.format(m, len(qs)))
+        kls = apps.get_model("music", m)
+        qs = kls.objects.filter(uuid__isnull=True).only("id")
+        print("Setting uuids for {} ({} objects)".format(m, len(qs)))
         for o in qs:
             o.uuid = uuid.uuid4()
-            o.save(update_fields=['uuid'])
+            o.save(update_fields=["uuid"])
 
 
 def rewind(apps, schema_editor):
@@ -31,50 +30,48 @@ def rewind(apps, schema_editor):
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0023_auto_20180407_1010'),
-    ]
+    dependencies = [("music", "0023_auto_20180407_1010")]
 
     operations = [
         migrations.RunPython(populate_uuids, rewind),
         migrations.AlterField(
-            model_name='album',
-            name='uuid',
+            model_name="album",
+            name="uuid",
             field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
         ),
         migrations.AlterField(
-            model_name='artist',
-            name='uuid',
+            model_name="artist",
+            name="uuid",
             field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
         ),
         migrations.AlterField(
-            model_name='importbatch',
-            name='uuid',
+            model_name="importbatch",
+            name="uuid",
             field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
         ),
         migrations.AlterField(
-            model_name='importjob',
-            name='uuid',
+            model_name="importjob",
+            name="uuid",
             field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
         ),
         migrations.AlterField(
-            model_name='lyrics',
-            name='uuid',
+            model_name="lyrics",
+            name="uuid",
             field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
         ),
         migrations.AlterField(
-            model_name='track',
-            name='uuid',
+            model_name="track",
+            name="uuid",
             field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
         ),
         migrations.AlterField(
-            model_name='trackfile',
-            name='uuid',
+            model_name="trackfile",
+            name="uuid",
             field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
         ),
         migrations.AlterField(
-            model_name='work',
-            name='uuid',
+            model_name="work",
+            name="uuid",
             field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
         ),
     ]
diff --git a/api/funkwhale_api/music/migrations/0025_auto_20180419_2023.py b/api/funkwhale_api/music/migrations/0025_auto_20180419_2023.py
index 6b0230d505235f0f920fc4b84e7adf735601f990..be685f1fe78bea25a7a9d57cb6c51e9101a627e9 100644
--- a/api/funkwhale_api/music/migrations/0025_auto_20180419_2023.py
+++ b/api/funkwhale_api/music/migrations/0025_auto_20180419_2023.py
@@ -5,14 +5,12 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0024_populate_uuid'),
-    ]
+    dependencies = [("music", "0024_populate_uuid")]
 
     operations = [
         migrations.AlterField(
-            model_name='trackfile',
-            name='source',
+            model_name="trackfile",
+            name="source",
             field=models.URLField(blank=True, max_length=500, null=True),
-        ),
+        )
     ]
diff --git a/api/funkwhale_api/music/migrations/0026_trackfile_accessed_date.py b/api/funkwhale_api/music/migrations/0026_trackfile_accessed_date.py
index 1d5327d93969e821a6fa3e20e55e56964fc1f841..f7f46f35aa4da0edebe34beb2c4519f9c5cf38d5 100644
--- a/api/funkwhale_api/music/migrations/0026_trackfile_accessed_date.py
+++ b/api/funkwhale_api/music/migrations/0026_trackfile_accessed_date.py
@@ -5,14 +5,12 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0025_auto_20180419_2023'),
-    ]
+    dependencies = [("music", "0025_auto_20180419_2023")]
 
     operations = [
         migrations.AddField(
-            model_name='trackfile',
-            name='accessed_date',
+            model_name="trackfile",
+            name="accessed_date",
             field=models.DateTimeField(blank=True, null=True),
-        ),
+        )
     ]
diff --git a/api/funkwhale_api/music/migrations/0027_auto_20180515_1808.py b/api/funkwhale_api/music/migrations/0027_auto_20180515_1808.py
index 835e115a6571c010148c939f11f15023a1d42475..1e3949da4823b5a3859277facfaa3f577f8360d6 100644
--- a/api/funkwhale_api/music/migrations/0027_auto_20180515_1808.py
+++ b/api/funkwhale_api/music/migrations/0027_auto_20180515_1808.py
@@ -6,24 +6,28 @@ import taggit.managers
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('music', '0026_trackfile_accessed_date'),
-    ]
+    dependencies = [("music", "0026_trackfile_accessed_date")]
 
     operations = [
         migrations.AddField(
-            model_name='trackfile',
-            name='bitrate',
+            model_name="trackfile",
+            name="bitrate",
             field=models.IntegerField(blank=True, null=True),
         ),
         migrations.AddField(
-            model_name='trackfile',
-            name='size',
+            model_name="trackfile",
+            name="size",
             field=models.IntegerField(blank=True, null=True),
         ),
         migrations.AlterField(
-            model_name='track',
-            name='tags',
-            field=taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
+            model_name="track",
+            name="tags",
+            field=taggit.managers.TaggableManager(
+                blank=True,
+                help_text="A comma-separated list of tags.",
+                through="taggit.TaggedItem",
+                to="taggit.Tag",
+                verbose_name="Tags",
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index bf3f9e12c29ebbada7e158a57a37b755aafecb1d..8b638ce7daff025cd7d68eaba2d93dcb7ec1f562 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -1,43 +1,38 @@
-import os
-import io
-import arrow
 import datetime
-import tempfile
+import os
 import shutil
-import markdown
+import tempfile
 import uuid
 
+import arrow
+import markdown
 from django.conf import settings
-from django.db import models
-from django.core.files.base import ContentFile
 from django.core.files import File
+from django.core.files.base import ContentFile
+from django.db import models
 from django.db.models.signals import post_save
 from django.dispatch import receiver
 from django.urls import reverse
 from django.utils import timezone
-
 from taggit.managers import TaggableManager
 from versatileimagefield.fields import VersatileImageField
 
-from funkwhale_api import downloader
-from funkwhale_api import musicbrainz
+from funkwhale_api import downloader, musicbrainz
 from funkwhale_api.federation import utils as federation_utils
-from . import importers
-from . import metadata
-from . import utils
+
+from . import importers, metadata, utils
 
 
 class APIModelMixin(models.Model):
     mbid = models.UUIDField(unique=True, db_index=True, null=True, blank=True)
-    uuid = models.UUIDField(
-        unique=True, db_index=True, default=uuid.uuid4)
+    uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4)
     api_includes = []
     creation_date = models.DateTimeField(default=timezone.now)
     import_hooks = []
 
     class Meta:
         abstract = True
-        ordering = ['-creation_date']
+        ordering = ["-creation_date"]
 
     @classmethod
     def get_or_create_from_api(cls, mbid):
@@ -47,14 +42,20 @@ class APIModelMixin(models.Model):
             return cls.create_from_api(id=mbid), True
 
     def get_api_data(self):
-        return self.__class__.api.get(id=self.mbid, includes=self.api_includes)[self.musicbrainz_model]
+        return self.__class__.api.get(id=self.mbid, includes=self.api_includes)[
+            self.musicbrainz_model
+        ]
 
     @classmethod
     def create_from_api(cls, **kwargs):
-        if kwargs.get('id'):
-            raw_data = cls.api.get(id=kwargs['id'], includes=cls.api_includes)[cls.musicbrainz_model]
+        if kwargs.get("id"):
+            raw_data = cls.api.get(id=kwargs["id"], includes=cls.api_includes)[
+                cls.musicbrainz_model
+            ]
         else:
-            raw_data = cls.api.search(**kwargs)['{0}-list'.format(cls.musicbrainz_model)][0]
+            raw_data = cls.api.search(**kwargs)[
+                "{0}-list".format(cls.musicbrainz_model)
+            ][0]
         cleaned_data = cls.clean_musicbrainz_data(raw_data)
         return importers.load(cls, cleaned_data, raw_data, cls.import_hooks)
 
@@ -66,39 +67,35 @@ class APIModelMixin(models.Model):
             try:
                 cleaned_key, cleaned_value = mapping.from_musicbrainz(key, value)
                 cleaned_data[cleaned_key] = cleaned_value
-            except KeyError as e:
+            except KeyError:
                 pass
         return cleaned_data
 
     @property
     def musicbrainz_url(self):
         if self.mbid:
-            return 'https://musicbrainz.org/{}/{}'.format(
-                self.musicbrainz_model, self.mbid)
+            return "https://musicbrainz.org/{}/{}".format(
+                self.musicbrainz_model, self.mbid
+            )
 
 
 class ArtistQuerySet(models.QuerySet):
     def with_albums_count(self):
-        return self.annotate(_albums_count=models.Count('albums'))
+        return self.annotate(_albums_count=models.Count("albums"))
 
     def with_albums(self):
         return self.prefetch_related(
-            models.Prefetch(
-                'albums', queryset=Album.objects.with_tracks_count())
+            models.Prefetch("albums", queryset=Album.objects.with_tracks_count())
         )
 
 
 class Artist(APIModelMixin):
     name = models.CharField(max_length=255)
 
-    musicbrainz_model = 'artist'
+    musicbrainz_model = "artist"
     musicbrainz_mapping = {
-        'mbid': {
-            'musicbrainz_field_name': 'id'
-        },
-        'name': {
-            'musicbrainz_field_name': 'name'
-        }
+        "mbid": {"musicbrainz_field_name": "id"},
+        "name": {"musicbrainz_field_name": "name"},
     }
     api = musicbrainz.api.artists
     objects = ArtistQuerySet.as_manager()
@@ -116,14 +113,12 @@ class Artist(APIModelMixin):
 
     @classmethod
     def get_or_create_from_name(cls, name, **kwargs):
-        kwargs.update({'name': name})
-        return cls.objects.get_or_create(
-            name__iexact=name,
-            defaults=kwargs)
+        kwargs.update({"name": name})
+        return cls.objects.get_or_create(name__iexact=name, defaults=kwargs)
 
 
 def import_artist(v):
-    a = Artist.get_or_create_from_api(mbid=v[0]['artist']['id'])[0]
+    a = Artist.get_or_create_from_api(mbid=v[0]["artist"]["id"])[0]
     return a
 
 
@@ -135,78 +130,62 @@ def parse_date(v):
 
 
 def import_tracks(instance, cleaned_data, raw_data):
-    for track_data in raw_data['medium-list'][0]['track-list']:
-        track_cleaned_data = Track.clean_musicbrainz_data(track_data['recording'])
-        track_cleaned_data['album'] = instance
-        track_cleaned_data['position'] = int(track_data['position'])
-        track = importers.load(Track, track_cleaned_data, track_data, Track.import_hooks)
+    for track_data in raw_data["medium-list"][0]["track-list"]:
+        track_cleaned_data = Track.clean_musicbrainz_data(track_data["recording"])
+        track_cleaned_data["album"] = instance
+        track_cleaned_data["position"] = int(track_data["position"])
+        importers.load(Track, track_cleaned_data, track_data, Track.import_hooks)
 
 
 class AlbumQuerySet(models.QuerySet):
     def with_tracks_count(self):
-        return self.annotate(_tracks_count=models.Count('tracks'))
+        return self.annotate(_tracks_count=models.Count("tracks"))
 
 
 class Album(APIModelMixin):
     title = models.CharField(max_length=255)
-    artist = models.ForeignKey(
-        Artist, related_name='albums', on_delete=models.CASCADE)
+    artist = models.ForeignKey(Artist, related_name="albums", on_delete=models.CASCADE)
     release_date = models.DateField(null=True)
     release_group_id = models.UUIDField(null=True, blank=True)
-    cover = VersatileImageField(upload_to='albums/covers/%Y/%m/%d', null=True, blank=True)
-    TYPE_CHOICES = (
-        ('album', 'Album'),
+    cover = VersatileImageField(
+        upload_to="albums/covers/%Y/%m/%d", null=True, blank=True
     )
-    type = models.CharField(choices=TYPE_CHOICES, max_length=30, default='album')
+    TYPE_CHOICES = (("album", "Album"),)
+    type = models.CharField(choices=TYPE_CHOICES, max_length=30, default="album")
 
-    api_includes = ['artist-credits', 'recordings', 'media', 'release-groups']
+    api_includes = ["artist-credits", "recordings", "media", "release-groups"]
     api = musicbrainz.api.releases
-    musicbrainz_model = 'release'
+    musicbrainz_model = "release"
     musicbrainz_mapping = {
-        'mbid': {
-            'musicbrainz_field_name': 'id',
-        },
-        'position': {
-            'musicbrainz_field_name': 'release-list',
-            'converter': lambda v: int(v[0]['medium-list'][0]['position']),
-        },
-        'release_group_id': {
-            'musicbrainz_field_name': 'release-group',
-            'converter': lambda v: v['id'],
-        },
-        'title': {
-            'musicbrainz_field_name': 'title',
+        "mbid": {"musicbrainz_field_name": "id"},
+        "position": {
+            "musicbrainz_field_name": "release-list",
+            "converter": lambda v: int(v[0]["medium-list"][0]["position"]),
         },
-        'release_date': {
-            'musicbrainz_field_name': 'date',
-            'converter': parse_date,
-
+        "release_group_id": {
+            "musicbrainz_field_name": "release-group",
+            "converter": lambda v: v["id"],
         },
-        'type': {
-            'musicbrainz_field_name': 'type',
-            'converter': lambda v: v.lower(),
+        "title": {"musicbrainz_field_name": "title"},
+        "release_date": {"musicbrainz_field_name": "date", "converter": parse_date},
+        "type": {"musicbrainz_field_name": "type", "converter": lambda v: v.lower()},
+        "artist": {
+            "musicbrainz_field_name": "artist-credit",
+            "converter": import_artist,
         },
-        'artist': {
-            'musicbrainz_field_name': 'artist-credit',
-            'converter': import_artist,
-        }
     }
     objects = AlbumQuerySet.as_manager()
 
     def get_image(self, data=None):
         if data:
-            f = ContentFile(data['content'])
-            extensions = {
-                'image/jpeg': 'jpg',
-                'image/png': 'png',
-                'image/gif': 'gif',
-            }
-            extension = extensions.get(data['mimetype'], 'jpg')
-            self.cover.save('{}.{}'.format(self.uuid, extension), f)
+            f = ContentFile(data["content"])
+            extensions = {"image/jpeg": "jpg", "image/png": "png", "image/gif": "gif"}
+            extension = extensions.get(data["mimetype"], "jpg")
+            self.cover.save("{}.{}".format(self.uuid, extension), f)
         else:
-            image_data =  musicbrainz.api.images.get_front(str(self.mbid))
+            image_data = musicbrainz.api.images.get_front(str(self.mbid))
             f = ContentFile(image_data)
-            self.cover.save('{0}.jpg'.format(self.mbid), f)
+            self.cover.save("{0}.jpg".format(self.mbid), f)
         return self.cover.file
 
     def __str__(self):
@@ -222,35 +201,30 @@ class Album(APIModelMixin):
 
     @classmethod
     def get_or_create_from_title(cls, title, **kwargs):
-        kwargs.update({'title': title})
-        return cls.objects.get_or_create(
-            title__iexact=title,
-            defaults=kwargs)
+        kwargs.update({"title": title})
+        return cls.objects.get_or_create(title__iexact=title, defaults=kwargs)
 
 
 def import_tags(instance, cleaned_data, raw_data):
     MINIMUM_COUNT = 2
     tags_to_add = []
-    for tag_data in raw_data.get('tag-list', []):
+    for tag_data in raw_data.get("tag-list", []):
         try:
-            if int(tag_data['count']) < MINIMUM_COUNT:
+            if int(tag_data["count"]) < MINIMUM_COUNT:
                 continue
         except ValueError:
             continue
-        tags_to_add.append(tag_data['name'])
+        tags_to_add.append(tag_data["name"])
     instance.tags.add(*tags_to_add)
 
 
 def import_album(v):
-    a = Album.get_or_create_from_api(mbid=v[0]['id'])[0]
+    a = Album.get_or_create_from_api(mbid=v[0]["id"])[0]
     return a
 
 
 def link_recordings(instance, cleaned_data, raw_data):
-    tracks = [
-        r['target']
-        for r in raw_data['recording-relation-list']
-    ]
+    tracks = [r["target"] for r in raw_data["recording-relation-list"]]
     Track.objects.filter(mbid__in=tracks).update(work=instance)
 
 
@@ -258,9 +232,9 @@ def import_lyrics(instance, cleaned_data, raw_data):
     try:
         url = [
             url_data
-            for url_data in raw_data['url-relation-list']
-            if url_data['type'] == 'lyrics'
-        ][0]['target']
+            for url_data in raw_data["url-relation-list"]
+            if url_data["type"] == "lyrics"
+        ][0]["target"]
     except (IndexError, KeyError):
         return
     l, _ = Lyrics.objects.get_or_create(work=instance, url=url)
@@ -274,47 +248,31 @@ class Work(APIModelMixin):
     title = models.CharField(max_length=255)
 
     api = musicbrainz.api.works
-    api_includes = ['url-rels', 'recording-rels']
-    musicbrainz_model = 'work'
+    api_includes = ["url-rels", "recording-rels"]
+    musicbrainz_model = "work"
     musicbrainz_mapping = {
-        'mbid': {
-            'musicbrainz_field_name': 'id'
-        },
-        'title': {
-            'musicbrainz_field_name': 'title'
-        },
-        'language': {
-            'musicbrainz_field_name': 'language',
-        },
-        'nature': {
-            'musicbrainz_field_name': 'type',
-            'converter': lambda v: v.lower(),
-        },
+        "mbid": {"musicbrainz_field_name": "id"},
+        "title": {"musicbrainz_field_name": "title"},
+        "language": {"musicbrainz_field_name": "language"},
+        "nature": {"musicbrainz_field_name": "type", "converter": lambda v: v.lower()},
     }
-    import_hooks = [
-        import_lyrics,
-        link_recordings
-    ]
+    import_hooks = [import_lyrics, link_recordings]
 
     def fetch_lyrics(self):
-        l = self.lyrics.first()
-        if l:
-            return l
-        data = self.api.get(self.mbid, includes=['url-rels'])['work']
-        l = import_lyrics(self, {}, data)
+        lyric = self.lyrics.first()
+        if lyric:
+            return lyric
+        data = self.api.get(self.mbid, includes=["url-rels"])["work"]
+        lyric = import_lyrics(self, {}, data)
 
-        return l
+        return lyric
 
 
 class Lyrics(models.Model):
-    uuid = models.UUIDField(
-        unique=True, db_index=True, default=uuid.uuid4)
+    uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4)
     work = models.ForeignKey(
-        Work,
-        related_name='lyrics',
-        null=True,
-        blank=True,
-        on_delete=models.CASCADE)
+        Work, related_name="lyrics", null=True, blank=True, on_delete=models.CASCADE
+    )
     url = models.URLField(unique=True)
     content = models.TextField(null=True, blank=True)
 
@@ -324,67 +282,55 @@ class Lyrics(models.Model):
             self.content,
             safe_mode=True,
             enable_attributes=False,
-            extensions=['markdown.extensions.nl2br'])
+            extensions=["markdown.extensions.nl2br"],
+        )
 
 
 class TrackQuerySet(models.QuerySet):
     def for_nested_serialization(self):
-        return (self.select_related()
-                    .select_related('album__artist', 'artist')
-                    .prefetch_related('files'))
+        return (
+            self.select_related()
+            .select_related("album__artist", "artist")
+            .prefetch_related("files")
+        )
 
 
 def get_artist(release_list):
     return Artist.get_or_create_from_api(
-        mbid=release_list[0]['artist-credits'][0]['artists']['id'])[0]
+        mbid=release_list[0]["artist-credits"][0]["artists"]["id"]
+    )[0]
 
 
 class Track(APIModelMixin):
     title = models.CharField(max_length=255)
-    artist = models.ForeignKey(
-        Artist, related_name='tracks', on_delete=models.CASCADE)
+    artist = models.ForeignKey(Artist, related_name="tracks", on_delete=models.CASCADE)
     position = models.PositiveIntegerField(null=True, blank=True)
     album = models.ForeignKey(
-        Album,
-        related_name='tracks',
-        null=True,
-        blank=True,
-        on_delete=models.CASCADE)
+        Album, related_name="tracks", null=True, blank=True, on_delete=models.CASCADE
+    )
     work = models.ForeignKey(
-        Work,
-        related_name='tracks',
-        null=True,
-        blank=True,
-        on_delete=models.CASCADE)
+        Work, related_name="tracks", null=True, blank=True, on_delete=models.CASCADE
+    )
 
-    musicbrainz_model = 'recording'
+    musicbrainz_model = "recording"
     api = musicbrainz.api.recordings
-    api_includes = ['artist-credits', 'releases', 'media', 'tags', 'work-rels']
+    api_includes = ["artist-credits", "releases", "media", "tags", "work-rels"]
     musicbrainz_mapping = {
-        'mbid': {
-            'musicbrainz_field_name': 'id'
-        },
-        'title': {
-            'musicbrainz_field_name': 'title'
-        },
-        'artist': {
+        "mbid": {"musicbrainz_field_name": "id"},
+        "title": {"musicbrainz_field_name": "title"},
+        "artist": {
             # we use the artist from the release to avoid #237
-            'musicbrainz_field_name': 'release-list',
-            'converter': get_artist,
-        },
-        'album': {
-            'musicbrainz_field_name': 'release-list',
-            'converter': import_album,
+            "musicbrainz_field_name": "release-list",
+            "converter": get_artist,
         },
+        "album": {"musicbrainz_field_name": "release-list", "converter": import_album},
     }
-    import_hooks = [
-        import_tags
-    ]
+    import_hooks = [import_tags]
     objects = TrackQuerySet.as_manager()
     tags = TaggableManager(blank=True)
 
     class Meta:
-        ordering = ['album', 'position']
+        ordering = ["album", "position"]
 
     def __str__(self):
         return self.title
@@ -399,43 +345,33 @@ class Track(APIModelMixin):
     def get_work(self):
         if self.work:
             return self.work
-        data = self.api.get(self.mbid, includes=['work-rels'])
+        data = self.api.get(self.mbid, includes=["work-rels"])
         try:
-            work_data = data['recording']['work-relation-list'][0]['work']
+            work_data = data["recording"]["work-relation-list"][0]["work"]
         except (IndexError, KeyError):
             return
-        work, _ = Work.get_or_create_from_api(mbid=work_data['id'])
+        work, _ = Work.get_or_create_from_api(mbid=work_data["id"])
         return work
 
     def get_lyrics_url(self):
-        return reverse('api:v1:tracks-lyrics', kwargs={'pk': self.pk})
+        return reverse("api:v1:tracks-lyrics", kwargs={"pk": self.pk})
 
     @property
     def full_name(self):
         try:
-            return '{} - {} - {}'.format(
-                self.artist.name,
-                self.album.title,
-                self.title,
-            )
+            return "{} - {} - {}".format(self.artist.name, self.album.title, self.title)
         except AttributeError:
-            return '{} - {}'.format(
-                self.artist.name,
-                self.title,
-            )
+            return "{} - {}".format(self.artist.name, self.title)
 
     def get_activity_url(self):
         if self.mbid:
-            return 'https://musicbrainz.org/recording/{}'.format(
-                self.mbid)
-        return settings.FUNKWHALE_URL + '/tracks/{}'.format(self.pk)
+            return "https://musicbrainz.org/recording/{}".format(self.mbid)
+        return settings.FUNKWHALE_URL + "/tracks/{}".format(self.pk)
 
     @classmethod
     def get_or_create_from_title(cls, title, **kwargs):
-        kwargs.update({'title': title})
-        return cls.objects.get_or_create(
-            title__iexact=title,
-            defaults=kwargs)
+        kwargs.update({"title": title})
+        return cls.objects.get_or_create(title__iexact=title, defaults=kwargs)
 
     @classmethod
     def get_or_create_from_release(cls, release_mbid, mbid):
@@ -448,35 +384,32 @@ class Track(APIModelMixin):
 
         album = Album.get_or_create_from_api(release_mbid)[0]
         data = musicbrainz.client.api.releases.get(
-            str(album.mbid), includes=Album.api_includes)
-        tracks = [
-            t
-            for m in data['release']['medium-list']
-            for t in m['track-list']
-        ]
+            str(album.mbid), includes=Album.api_includes
+        )
+        tracks = [t for m in data["release"]["medium-list"] for t in m["track-list"]]
         track_data = None
         for track in tracks:
-            if track['recording']['id'] == mbid:
+            if track["recording"]["id"] == mbid:
                 track_data = track
                 break
         if not track_data:
-            raise ValueError('No track found matching this ID')
+            raise ValueError("No track found matching this ID")
 
         return cls.objects.update_or_create(
             mbid=mbid,
             defaults={
-                'position': int(track['position']),
-                'title': track['recording']['title'],
-                'album': album,
-                'artist': album.artist,
-            }
+                "position": int(track["position"]),
+                "title": track["recording"]["title"],
+                "album": album,
+                "artist": album.artist,
+            },
         )
+
+
 class TrackFile(models.Model):
-    uuid = models.UUIDField(
-        unique=True, db_index=True, default=uuid.uuid4)
-    track = models.ForeignKey(
-        Track, related_name='files', on_delete=models.CASCADE)
-    audio_file = models.FileField(upload_to='tracks/%Y/%m/%d', max_length=255)
+    uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4)
+    track = models.ForeignKey(Track, related_name="files", on_delete=models.CASCADE)
+    audio_file = models.FileField(upload_to="tracks/%Y/%m/%d", max_length=255)
     source = models.URLField(null=True, blank=True, max_length=500)
     creation_date = models.DateTimeField(default=timezone.now)
     modification_date = models.DateTimeField(auto_now=True)
@@ -488,8 +421,8 @@ class TrackFile(models.Model):
     mimetype = models.CharField(null=True, blank=True, max_length=200)
 
     library_track = models.OneToOneField(
-        'federation.LibraryTrack',
-        related_name='local_track_file',
+        "federation.LibraryTrack",
+        related_name="local_track_file",
         on_delete=models.CASCADE,
         null=True,
         blank=True,
@@ -499,45 +432,38 @@ class TrackFile(models.Model):
         # import the track file, since there is not any
         # we create a tmp dir for the download
         tmp_dir = tempfile.mkdtemp()
-        data = downloader.download(
-            self.source,
-            target_directory=tmp_dir)
-        self.duration = data.get('duration', None)
+        data = downloader.download(self.source, target_directory=tmp_dir)
+        self.duration = data.get("duration", None)
         self.audio_file.save(
-            os.path.basename(data['audio_file_path']),
-            File(open(data['audio_file_path'], 'rb'))
+            os.path.basename(data["audio_file_path"]),
+            File(open(data["audio_file_path"], "rb")),
         )
         shutil.rmtree(tmp_dir)
         return self.audio_file
 
     def get_federation_url(self):
-        return federation_utils.full_url(
-            '/federation/music/file/{}'.format(self.uuid)
-        )
+        return federation_utils.full_url("/federation/music/file/{}".format(self.uuid))
 
     @property
     def path(self):
-        return reverse(
-            'api:v1:trackfiles-serve', kwargs={'pk': self.pk})
+        return reverse("api:v1:trackfiles-serve", kwargs={"pk": self.pk})
 
     @property
     def filename(self):
-        return '{}.{}'.format(
-            self.track.full_name,
-            self.extension)
+        return "{}.{}".format(self.track.full_name, self.extension)
 
     @property
     def extension(self):
         if not self.audio_file:
             return
-        return os.path.splitext(self.audio_file.name)[-1].replace('.', '', 1)
+        return os.path.splitext(self.audio_file.name)[-1].replace(".", "", 1)
 
     def get_file_size(self):
         if self.audio_file:
             return self.audio_file.size
 
-        if self.source.startswith('file://'):
-            return os.path.getsize(self.source.replace('file://', '', 1))
+        if self.source.startswith("file://"):
+            return os.path.getsize(self.source.replace("file://", "", 1))
 
         if self.library_track and self.library_track.audio_file:
             return self.library_track.audio_file.size
@@ -545,8 +471,8 @@ class TrackFile(models.Model):
     def get_audio_file(self):
         if self.audio_file:
             return self.audio_file.open()
-        if self.source.startswith('file://'):
-            return open(self.source.replace('file://', '', 1), 'rb')
+        if self.source.startswith("file://"):
+            return open(self.source.replace("file://", "", 1), "rb")
         if self.library_track and self.library_track.audio_file:
             return self.library_track.audio_file.open()
 
@@ -557,15 +483,15 @@ class TrackFile(models.Model):
                 audio_data = utils.get_audio_file_data(f)
             if not audio_data:
                 return
-            self.duration = int(audio_data['length'])
-            self.bitrate = audio_data['bitrate']
+            self.duration = int(audio_data["length"])
+            self.bitrate = audio_data["bitrate"]
             self.size = self.get_file_size()
         else:
             lt = self.library_track
             if lt:
-                self.duration = lt.get_metadata('length')
-                self.size = lt.get_metadata('size')
-                self.bitrate = lt.get_metadata('bitrate')
+                self.duration = lt.get_metadata("length")
+                self.size = lt.get_metadata("size")
+                self.bitrate = lt.get_metadata("bitrate")
 
     def save(self, **kwargs):
         if not self.mimetype and self.audio_file:
@@ -580,41 +506,44 @@ class TrackFile(models.Model):
 
 
 IMPORT_STATUS_CHOICES = (
-    ('pending', 'Pending'),
-    ('finished', 'Finished'),
-    ('errored', 'Errored'),
-    ('skipped', 'Skipped'),
+    ("pending", "Pending"),
+    ("finished", "Finished"),
+    ("errored", "Errored"),
+    ("skipped", "Skipped"),
 )
 
 
 class ImportBatch(models.Model):
-    uuid = models.UUIDField(
-        unique=True, db_index=True, default=uuid.uuid4)
+    uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4)
     IMPORT_BATCH_SOURCES = [
-        ('api', 'api'),
-        ('shell', 'shell'),
-        ('federation', 'federation'),
+        ("api", "api"),
+        ("shell", "shell"),
+        ("federation", "federation"),
     ]
     source = models.CharField(
-        max_length=30, default='api', choices=IMPORT_BATCH_SOURCES)
+        max_length=30, default="api", choices=IMPORT_BATCH_SOURCES
+    )
     creation_date = models.DateTimeField(default=timezone.now)
     submitted_by = models.ForeignKey(
-        'users.User',
-        related_name='imports',
+        "users.User",
+        related_name="imports",
         null=True,
         blank=True,
-        on_delete=models.CASCADE)
+        on_delete=models.CASCADE,
+    )
     status = models.CharField(
-        choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30)
+        choices=IMPORT_STATUS_CHOICES, default="pending", max_length=30
+    )
     import_request = models.ForeignKey(
-        'requests.ImportRequest',
-        related_name='import_batches',
+        "requests.ImportRequest",
+        related_name="import_batches",
         null=True,
         blank=True,
-        on_delete=models.CASCADE)
+        on_delete=models.CASCADE,
+    )
 
     class Meta:
-        ordering = ['-creation_date']
+        ordering = ["-creation_date"]
 
     def __str__(self):
         return str(self.pk)
@@ -624,46 +553,46 @@ class ImportBatch(models.Model):
         self.status = utils.compute_status(self.jobs.all())
         if self.status == old_status:
             return
-        self.save(update_fields=['status'])
-        if self.status != old_status and self.status == 'finished':
+        self.save(update_fields=["status"])
+        if self.status != old_status and self.status == "finished":
             from . import tasks
+
             tasks.import_batch_notify_followers.delay(import_batch_id=self.pk)
 
     def get_federation_url(self):
         return federation_utils.full_url(
-            '/federation/music/import/batch/{}'.format(self.uuid)
+            "/federation/music/import/batch/{}".format(self.uuid)
         )
 
 
 class ImportJob(models.Model):
-    uuid = models.UUIDField(
-        unique=True, db_index=True, default=uuid.uuid4)
+    uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4)
     batch = models.ForeignKey(
-        ImportBatch, related_name='jobs', on_delete=models.CASCADE)
+        ImportBatch, related_name="jobs", on_delete=models.CASCADE
+    )
     track_file = models.ForeignKey(
-        TrackFile,
-        related_name='jobs',
-        null=True,
-        blank=True,
-        on_delete=models.CASCADE)
+        TrackFile, related_name="jobs", null=True, blank=True, on_delete=models.CASCADE
+    )
     source = models.CharField(max_length=500)
     mbid = models.UUIDField(editable=False, null=True, blank=True)
 
     status = models.CharField(
-        choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30)
+        choices=IMPORT_STATUS_CHOICES, default="pending", max_length=30
+    )
     audio_file = models.FileField(
-        upload_to='imports/%Y/%m/%d', max_length=255, null=True, blank=True)
+        upload_to="imports/%Y/%m/%d", max_length=255, null=True, blank=True
+    )
 
     library_track = models.ForeignKey(
-        'federation.LibraryTrack',
-        related_name='import_jobs',
+        "federation.LibraryTrack",
+        related_name="import_jobs",
         on_delete=models.SET_NULL,
         null=True,
-        blank=True
+        blank=True,
     )
 
     class Meta:
-        ordering = ('id', )
+        ordering = ("id",)
 
 
 @receiver(post_save, sender=ImportJob)
@@ -673,22 +602,22 @@ def update_batch_status(sender, instance, **kwargs):
 
 @receiver(post_save, sender=ImportBatch)
 def update_request_status(sender, instance, created, **kwargs):
-    update_fields = kwargs.get('update_fields', []) or []
+    update_fields = kwargs.get("update_fields", []) or []
     if not instance.import_request:
         return
 
-    if not created and not 'status' in update_fields:
+    if not created and "status" not in update_fields:
         return
 
     r_status = instance.import_request.status
     status = instance.status
 
-    if status == 'pending' and r_status == 'pending':
+    if status == "pending" and r_status == "pending":
         # let's mark the request as accepted since we started an import
-        instance.import_request.status = 'accepted'
-        return instance.import_request.save(update_fields=['status'])
+        instance.import_request.status = "accepted"
+        return instance.import_request.save(update_fields=["status"])
 
-    if status == 'finished' and r_status == 'accepted':
+    if status == "finished" and r_status == "accepted":
         # let's mark the request as imported since the import is over
-        instance.import_request.status = 'imported'
-        return instance.import_request.save(update_fields=['status'])
+        instance.import_request.status = "imported"
+        return instance.import_request.save(update_fields=["status"])
diff --git a/api/funkwhale_api/music/permissions.py b/api/funkwhale_api/music/permissions.py
index d31e1c5d503f2d00c771ebe50e4593b58c0f5146..dc589b5dde4b3dbbd7c720cc73684d1b648497e4 100644
--- a/api/funkwhale_api/music/permissions.py
+++ b/api/funkwhale_api/music/permissions.py
@@ -1,29 +1,24 @@
-from django.conf import settings
 
 from rest_framework.permissions import BasePermission
 
 from funkwhale_api.common import preferences
-from funkwhale_api.federation import actors
-from funkwhale_api.federation import models
+from funkwhale_api.federation import actors, models
 
 
 class Listen(BasePermission):
-
     def has_permission(self, request, view):
-        if not preferences.get('common__api_authentication_required'):
+        if not preferences.get("common__api_authentication_required"):
             return True
 
-        user = getattr(request, 'user', None)
+        user = getattr(request, "user", None)
         if user and user.is_authenticated:
             return True
 
-        actor = getattr(request, 'actor', None)
+        actor = getattr(request, "actor", None)
         if actor is None:
             return False
 
-        library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+        library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
         return models.Follow.objects.filter(
-            target=library,
-            actor=actor,
-            approved=True
+            target=library, actor=actor, approved=True
         ).exists()
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index b72bb8c4a63cce3c9b23ed2a62082527a00455d4..c34970d0b5cbcf29155c7b008c0ba4acd6246f1c 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -1,16 +1,11 @@
-from django.db import transaction
 from django.db.models import Q
 from rest_framework import serializers
 from taggit.models import Tag
 
 from funkwhale_api.activity import serializers as activity_serializers
-from funkwhale_api.federation import utils as federation_utils
-from funkwhale_api.federation.models import LibraryTrack
-from funkwhale_api.federation.serializers import AP_CONTEXT
 from funkwhale_api.users.serializers import UserBasicSerializer
 
-from . import models
-from . import tasks
+from . import models, tasks
 
 
 class ArtistAlbumSerializer(serializers.ModelSerializer):
@@ -19,14 +14,14 @@ class ArtistAlbumSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Album
         fields = (
-            'id',
-            'mbid',
-            'title',
-            'artist',
-            'release_date',
-            'cover',
-            'creation_date',
-            'tracks_count',
+            "id",
+            "mbid",
+            "title",
+            "artist",
+            "release_date",
+            "cover",
+            "creation_date",
+            "tracks_count",
         )
 
     def get_tracks_count(self, o):
@@ -38,13 +33,7 @@ class ArtistWithAlbumsSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = models.Artist
-        fields = (
-            'id',
-            'mbid',
-            'name',
-            'creation_date',
-            'albums',
-        )
+        fields = ("id", "mbid", "name", "creation_date", "albums")
 
 
 class TrackFileSerializer(serializers.ModelSerializer):
@@ -53,23 +42,18 @@ class TrackFileSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.TrackFile
         fields = (
-            'id',
-            'path',
-            'source',
-            'filename',
-            'mimetype',
-            'track',
-            'duration',
-            'mimetype',
-            'bitrate',
-            'size',
+            "id",
+            "path",
+            "source",
+            "filename",
+            "mimetype",
+            "track",
+            "duration",
+            "mimetype",
+            "bitrate",
+            "size",
         )
-        read_only_fields = [
-            'duration',
-            'mimetype',
-            'bitrate',
-            'size',
-        ]
+        read_only_fields = ["duration", "mimetype", "bitrate", "size"]
 
     def get_path(self, o):
         url = o.path
@@ -82,26 +66,21 @@ class AlbumTrackSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Track
         fields = (
-            'id',
-            'mbid',
-            'title',
-            'album',
-            'artist',
-            'creation_date',
-            'files',
-            'position',
+            "id",
+            "mbid",
+            "title",
+            "album",
+            "artist",
+            "creation_date",
+            "files",
+            "position",
         )
 
 
 class ArtistSimpleSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Artist
-        fields = (
-            'id',
-            'mbid',
-            'name',
-            'creation_date',
-        )
+        fields = ("id", "mbid", "name", "creation_date")
 
 
 class AlbumSerializer(serializers.ModelSerializer):
@@ -111,20 +90,20 @@ class AlbumSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Album
         fields = (
-            'id',
-            'mbid',
-            'title',
-            'artist',
-            'tracks',
-            'release_date',
-            'cover',
-            'creation_date',
+            "id",
+            "mbid",
+            "title",
+            "artist",
+            "tracks",
+            "release_date",
+            "cover",
+            "creation_date",
         )
 
     def get_tracks(self, o):
         ordered_tracks = sorted(
             o.tracks.all(),
-            key=lambda v: (v.position, v.title) if v.position else (99999, v.title)
+            key=lambda v: (v.position, v.title) if v.position else (99999, v.title),
         )
         return AlbumTrackSerializer(ordered_tracks, many=True).data
 
@@ -135,13 +114,13 @@ class TrackAlbumSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Album
         fields = (
-            'id',
-            'mbid',
-            'title',
-            'artist',
-            'release_date',
-            'cover',
-            'creation_date',
+            "id",
+            "mbid",
+            "title",
+            "artist",
+            "release_date",
+            "cover",
+            "creation_date",
         )
 
 
@@ -154,15 +133,15 @@ class TrackSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Track
         fields = (
-            'id',
-            'mbid',
-            'title',
-            'album',
-            'artist',
-            'creation_date',
-            'files',
-            'position',
-            'lyrics',
+            "id",
+            "mbid",
+            "title",
+            "album",
+            "artist",
+            "creation_date",
+            "files",
+            "position",
+            "lyrics",
         )
 
     def get_lyrics(self, obj):
@@ -172,20 +151,19 @@ class TrackSerializer(serializers.ModelSerializer):
 class TagSerializer(serializers.ModelSerializer):
     class Meta:
         model = Tag
-        fields = ('id', 'name', 'slug')
+        fields = ("id", "name", "slug")
 
 
 class SimpleAlbumSerializer(serializers.ModelSerializer):
-
     class Meta:
         model = models.Album
-        fields = ('id', 'mbid', 'title', 'release_date', 'cover')
+        fields = ("id", "mbid", "title", "release_date", "cover")
 
 
 class LyricsSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Lyrics
-        fields = ('id', 'work', 'content', 'content_rendered')
+        fields = ("id", "work", "content", "content_rendered")
 
 
 class ImportJobSerializer(serializers.ModelSerializer):
@@ -193,15 +171,8 @@ class ImportJobSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = models.ImportJob
-        fields = (
-            'id',
-            'mbid',
-            'batch',
-            'source',
-            'status',
-            'track_file',
-            'audio_file')
-        read_only_fields = ('status', 'track_file')
+        fields = ("id", "mbid", "batch", "source", "status", "track_file", "audio_file")
+        read_only_fields = ("status", "track_file")
 
 
 class ImportBatchSerializer(serializers.ModelSerializer):
@@ -210,19 +181,19 @@ class ImportBatchSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.ImportBatch
         fields = (
-            'id',
-            'submitted_by',
-            'source',
-            'status',
-            'creation_date',
-            'import_request')
-        read_only_fields = (
-            'creation_date', 'submitted_by', 'source')
+            "id",
+            "submitted_by",
+            "source",
+            "status",
+            "creation_date",
+            "import_request",
+        )
+        read_only_fields = ("creation_date", "submitted_by", "source")
 
     def to_representation(self, instance):
         repr = super().to_representation(instance)
         try:
-            repr['job_count'] = instance.job_count
+            repr["job_count"] = instance.job_count
         except AttributeError:
             # Queryset was not annotated
             pass
@@ -231,50 +202,43 @@ class ImportBatchSerializer(serializers.ModelSerializer):
 
 class TrackActivitySerializer(activity_serializers.ModelSerializer):
     type = serializers.SerializerMethodField()
-    name = serializers.CharField(source='title')
-    artist = serializers.CharField(source='artist.name')
-    album = serializers.CharField(source='album.title')
+    name = serializers.CharField(source="title")
+    artist = serializers.CharField(source="artist.name")
+    album = serializers.CharField(source="album.title")
 
     class Meta:
         model = models.Track
-        fields = [
-            'id',
-            'local_id',
-            'name',
-            'type',
-            'artist',
-            'album',
-        ]
+        fields = ["id", "local_id", "name", "type", "artist", "album"]
 
     def get_type(self, obj):
-        return 'Audio'
+        return "Audio"
 
 
 class ImportJobRunSerializer(serializers.Serializer):
     jobs = serializers.PrimaryKeyRelatedField(
         many=True,
-        queryset=models.ImportJob.objects.filter(
-            status__in=['pending', 'errored']
-        )
+        queryset=models.ImportJob.objects.filter(status__in=["pending", "errored"]),
     )
     batches = serializers.PrimaryKeyRelatedField(
-        many=True,
-        queryset=models.ImportBatch.objects.all()
+        many=True, queryset=models.ImportBatch.objects.all()
     )
 
     def validate(self, validated_data):
-        jobs = validated_data['jobs']
-        batches_ids = [b.pk for b in validated_data['batches']]
+        jobs = validated_data["jobs"]
+        batches_ids = [b.pk for b in validated_data["batches"]]
         query = Q(batch__pk__in=batches_ids)
         query |= Q(pk__in=[j.id for j in jobs])
-        queryset = models.ImportJob.objects.filter(query).filter(
-            status__in=['pending', 'errored']).distinct()
-        validated_data['_jobs'] = queryset
+        queryset = (
+            models.ImportJob.objects.filter(query)
+            .filter(status__in=["pending", "errored"])
+            .distinct()
+        )
+        validated_data["_jobs"] = queryset
         return validated_data
 
     def create(self, validated_data):
-        ids = validated_data['_jobs'].values_list('id', flat=True)
-        validated_data['_jobs'].update(status='pending')
+        ids = validated_data["_jobs"].values_list("id", flat=True)
+        validated_data["_jobs"].update(status="pending")
         for id in ids:
             tasks.import_job_run.delay(import_job_id=id)
-        return {'jobs': list(ids)}
+        return {"jobs": list(ids)}
diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py
index 7b1b4898111f71b6947724dd4119fd1e36e1dfb5..355af7706d6cca3d2d637311a6205dfa26c77c1c 100644
--- a/api/funkwhale_api/music/tasks.py
+++ b/api/funkwhale_api/music/tasks.py
@@ -1,45 +1,43 @@
 import logging
 import os
 
+from django.conf import settings
 from django.core.files.base import ContentFile
-
 from musicbrainzngs import ResponseError
 
 from funkwhale_api.common import preferences
-from funkwhale_api.federation import activity
-from funkwhale_api.federation import actors
-from funkwhale_api.federation import models as federation_models
+from funkwhale_api.federation import activity, actors
 from funkwhale_api.federation import serializers as federation_serializers
-from funkwhale_api.taskapp import celery
 from funkwhale_api.providers.acoustid import get_acoustid_client
 from funkwhale_api.providers.audiofile import tasks as audiofile_tasks
+from funkwhale_api.taskapp import celery
 
-from django.conf import settings
-from . import models
 from . import lyrics as lyrics_utils
+from . import models
 from . import utils as music_utils
 
 logger = logging.getLogger(__name__)
 
 
-@celery.app.task(name='acoustid.set_on_track_file')
-@celery.require_instance(models.TrackFile, 'track_file')
+@celery.app.task(name="acoustid.set_on_track_file")
+@celery.require_instance(models.TrackFile, "track_file")
 def set_acoustid_on_track_file(track_file):
     client = get_acoustid_client()
     result = client.get_best_match(track_file.audio_file.path)
 
     def update(id):
         track_file.acoustid_track_id = id
-        track_file.save(update_fields=['acoustid_track_id'])
+        track_file.save(update_fields=["acoustid_track_id"])
         return id
+
     if result:
-        return update(result['id'])
+        return update(result["id"])
 
 
 def import_track_from_remote(library_track):
     metadata = library_track.metadata
     try:
-        track_mbid = metadata['recording']['musicbrainz_id']
+        track_mbid = metadata["recording"]["musicbrainz_id"]
         assert track_mbid  # for null/empty values
     except (KeyError, AssertionError):
         pass
@@ -47,39 +45,43 @@ def import_track_from_remote(library_track):
         return models.Track.get_or_create_from_api(mbid=track_mbid)[0]
 
     try:
-        album_mbid = metadata['release']['musicbrainz_id']
+        album_mbid = metadata["release"]["musicbrainz_id"]
         assert album_mbid  # for null/empty values
     except (KeyError, AssertionError):
         pass
     else:
         album, _ = models.Album.get_or_create_from_api(mbid=album_mbid)
         return models.Track.get_or_create_from_title(
-            library_track.title, artist=album.artist, album=album)[0]
+            library_track.title, artist=album.artist, album=album
+        )[0]
 
     try:
-        artist_mbid = metadata['artist']['musicbrainz_id']
+        artist_mbid = metadata["artist"]["musicbrainz_id"]
         assert artist_mbid  # for null/empty values
     except (KeyError, AssertionError):
         pass
     else:
         artist, _ = models.Artist.get_or_create_from_api(mbid=artist_mbid)
         album, _ = models.Album.get_or_create_from_title(
-            library_track.album_title, artist=artist)
+            library_track.album_title, artist=artist
+        )
         return models.Track.get_or_create_from_title(
-            library_track.title, artist=artist, album=album)[0]
+            library_track.title, artist=artist, album=album
+        )[0]
 
     # worst case scenario, we have absolutely no way to link to a
     # musicbrainz resource, we rely on the name/titles
-    artist, _ = models.Artist.get_or_create_from_name(
-        library_track.artist_name)
+    artist, _ = models.Artist.get_or_create_from_name(library_track.artist_name)
     album, _ = models.Album.get_or_create_from_title(
-        library_track.album_title, artist=artist)
+        library_track.album_title, artist=artist
+    )
     return models.Track.get_or_create_from_title(
-        library_track.title, artist=artist, album=album)[0]
+        library_track.title, artist=artist, album=album
+    )[0]
 
 
 def _do_import(import_job, replace=False, use_acoustid=False):
-    logger.info('[Import Job %s] starting job', import_job.pk)
+    logger.info("[Import Job %s] starting job", import_job.pk)
     from_file = bool(import_job.audio_file)
     mbid = import_job.mbid
     acoustid_track_id = None
@@ -93,58 +95,60 @@ def _do_import(import_job, replace=False, use_acoustid=False):
         client = get_acoustid_client()
         match = client.get_best_match(import_job.audio_file.path)
         if match:
-            duration = match['recordings'][0]['duration']
-            mbid = match['recordings'][0]['id']
-            acoustid_track_id = match['id']
+            duration = match["recordings"][0]["duration"]
+            mbid = match["recordings"][0]["id"]
+            acoustid_track_id = match["id"]
     if mbid:
         logger.info(
-            '[Import Job %s] importing track from musicbrainz recording %s',
+            "[Import Job %s] importing track from musicbrainz recording %s",
             import_job.pk,
-            str(mbid))
+            str(mbid),
+        )
         track, _ = models.Track.get_or_create_from_api(mbid=mbid)
     elif import_job.audio_file:
         logger.info(
-            '[Import Job %s] importing track from uploaded track data at %s',
+            "[Import Job %s] importing track from uploaded track data at %s",
             import_job.pk,
-            import_job.audio_file.path)
-        track = audiofile_tasks.import_track_data_from_path(
-            import_job.audio_file.path)
+            import_job.audio_file.path,
+        )
+        track = audiofile_tasks.import_track_data_from_path(import_job.audio_file.path)
     elif import_job.library_track:
         logger.info(
-            '[Import Job %s] importing track from federated library track %s',
+            "[Import Job %s] importing track from federated library track %s",
             import_job.pk,
-            import_job.library_track.pk)
+            import_job.library_track.pk,
+        )
         track = import_track_from_remote(import_job.library_track)
-    elif import_job.source.startswith('file://'):
-        tf_path = import_job.source.replace('file://', '', 1)
+    elif import_job.source.startswith("file://"):
+        tf_path = import_job.source.replace("file://", "", 1)
         logger.info(
-            '[Import Job %s] importing track from local track data at %s',
+            "[Import Job %s] importing track from local track data at %s",
             import_job.pk,
-            tf_path)
-        track = audiofile_tasks.import_track_data_from_path(
-            tf_path)
+            tf_path,
+        )
+        track = audiofile_tasks.import_track_data_from_path(tf_path)
     else:
         raise ValueError(
-            'Not enough data to process import, '
-            'add a mbid, an audio file or a library track')
+            "Not enough data to process import, "
+            "add a mbid, an audio file or a library track"
+        )
 
     track_file = None
     if replace:
-        logger.info(
-            '[Import Job %s] replacing existing audio file', import_job.pk)
+        logger.info("[Import Job %s] replacing existing audio file", import_job.pk)
         track_file = track.files.first()
     elif track.files.count() > 0:
         logger.info(
-            '[Import Job %s] skipping, we already have a file for this track',
-            import_job.pk)
+            "[Import Job %s] skipping, we already have a file for this track",
+            import_job.pk,
+        )
         if import_job.audio_file:
             import_job.audio_file.delete()
-        import_job.status = 'skipped'
+        import_job.status = "skipped"
         import_job.save()
         return
 
-    track_file = track_file or models.TrackFile(
-        track=track, source=import_job.source)
+    track_file = track_file or models.TrackFile(track=track, source=import_job.source)
     track_file.acoustid_track_id = acoustid_track_id
     if from_file:
         track_file.audio_file = ContentFile(import_job.audio_file.read())
@@ -158,13 +162,11 @@ def _do_import(import_job, replace=False, use_acoustid=False):
         else:
             # no downloading, we hotlink
             pass
-    elif not import_job.audio_file and not import_job.source.startswith('file://'):
+    elif not import_job.audio_file and not import_job.source.startswith("file://"):
         # not an implace import, and we have a source, so let's download it
-        logger.info(
-            '[Import Job %s] downloading audio file from remote',
-            import_job.pk)
+        logger.info("[Import Job %s] downloading audio file from remote", import_job.pk)
         track_file.download_file()
-    elif not import_job.audio_file and import_job.source.startswith('file://'):
+    elif not import_job.audio_file and import_job.source.startswith("file://"):
         # in place import, we set mimetype from extension
         path, ext = os.path.splitext(import_job.source)
         track_file.mimetype = music_utils.get_type_from_ext(ext)
@@ -172,19 +174,15 @@ def _do_import(import_job, replace=False, use_acoustid=False):
     track_file.save()
     # if no cover is set on track album, we try to update it as well:
     if not track.album.cover:
-        logger.info(
-            '[Import Job %s] retrieving album cover',
-            import_job.pk)
+        logger.info("[Import Job %s] retrieving album cover", import_job.pk)
         update_album_cover(track.album, track_file)
-    import_job.status = 'finished'
+    import_job.status = "finished"
     import_job.track_file = track_file
     if import_job.audio_file:
         # it's imported on the track, we don't need it anymore
         import_job.audio_file.delete()
     import_job.save()
-    logger.info(
-        '[Import Job %s] job finished',
-        import_job.pk)
+    logger.info("[Import Job %s] job finished", import_job.pk)
     return track_file
 
 
@@ -199,20 +197,15 @@ def update_album_cover(album, track_file, replace=False):
         except FileNotFoundError:
             metadata = None
         if metadata:
-            cover = metadata.get_picture('cover_front')
+            cover = metadata.get_picture("cover_front")
             if cover:
                 # best case scenario, cover is embedded in the track
-                logger.info(
-                    '[Album %s] Using cover embedded in file',
-                    album.pk)
+                logger.info("[Album %s] Using cover embedded in file", album.pk)
                 return album.get_image(data=cover)
-        if track_file.source and track_file.source.startswith('file://'):
+        if track_file.source and track_file.source.startswith("file://"):
             # let's look for a cover in the same directory
-            path = os.path.dirname(track_file.source.replace('file://', '', 1))
-            logger.info(
-                '[Album %s] scanning covers from %s',
-                album.pk,
-                path)
+            path = os.path.dirname(track_file.source.replace("file://", "", 1))
+            logger.info("[Album %s] scanning covers from %s", album.pk, path)
             cover = get_cover_from_fs(path)
             if cover:
                 return album.get_image(data=cover)
@@ -220,50 +213,41 @@ def update_album_cover(album, track_file, replace=False):
         return
     try:
         logger.info(
-            '[Album %s] Fetching cover from musicbrainz release %s',
+            "[Album %s] Fetching cover from musicbrainz release %s",
             album.pk,
-            str(album.mbid))
+            str(album.mbid),
+        )
         return album.get_image()
     except ResponseError as exc:
         logger.warning(
-            '[Album %s] cannot fetch cover from musicbrainz: %s',
-            album.pk,
-            str(exc))
+            "[Album %s] cannot fetch cover from musicbrainz: %s", album.pk, str(exc)
+        )
+
 
+IMAGE_TYPES = [("jpg", "image/jpeg"), ("png", "image/png")]
 
-IMAGE_TYPES = [
-    ('jpg', 'image/jpeg'),
-    ('png', 'image/png'),
-]
 
 def get_cover_from_fs(dir_path):
     if os.path.exists(dir_path):
         for e, m in IMAGE_TYPES:
-            cover_path = os.path.join(dir_path, 'cover.{}'.format(e))
+            cover_path = os.path.join(dir_path, "cover.{}".format(e))
             if not os.path.exists(cover_path):
-                logger.debug('Cover %s does not exists', cover_path)
+                logger.debug("Cover %s does not exists", cover_path)
                 continue
-            with open(cover_path, 'rb') as c:
-                logger.info('Found cover at %s', cover_path)
-                return {
-                    'mimetype': m,
-                    'content': c.read(),
-                }
+            with open(cover_path, "rb") as c:
+                logger.info("Found cover at %s", cover_path)
+                return {"mimetype": m, "content": c.read()}
 
 
-
-@celery.app.task(name='ImportJob.run', bind=True)
+@celery.app.task(name="ImportJob.run", bind=True)
 @celery.require_instance(
-    models.ImportJob.objects.filter(
-        status__in=['pending', 'errored']),
-    'import_job')
+    models.ImportJob.objects.filter(status__in=["pending", "errored"]), "import_job"
+)
 def import_job_run(self, import_job, replace=False, use_acoustid=False):
     def mark_errored(exc):
-        logger.error(
-            '[Import Job %s] Error during import: %s',
-            import_job.pk, str(exc))
-        import_job.status = 'errored'
-        import_job.save(update_fields=['status'])
+        logger.error("[Import Job %s] Error during import: %s", import_job.pk, str(exc))
+        import_job.status = "errored"
+        import_job.save(update_fields=["status"])
 
     try:
         tf = _do_import(import_job, replace, use_acoustid=use_acoustid)
@@ -272,65 +256,63 @@ def import_job_run(self, import_job, replace=False, use_acoustid=False):
         if not settings.DEBUG:
             try:
                 self.retry(exc=exc, countdown=30, max_retries=3)
-            except:
+            except Exception:
                 mark_errored(exc)
                 raise
         mark_errored(exc)
         raise
 
 
-@celery.app.task(name='ImportBatch.run')
-@celery.require_instance(models.ImportBatch, 'import_batch')
+@celery.app.task(name="ImportBatch.run")
+@celery.require_instance(models.ImportBatch, "import_batch")
 def import_batch_run(import_batch):
-    for job_id in import_batch.jobs.order_by('id').values_list('id', flat=True):
+    for job_id in import_batch.jobs.order_by("id").values_list("id", flat=True):
         import_job_run.delay(import_job_id=job_id)
 
 
-@celery.app.task(name='Lyrics.fetch_content')
-@celery.require_instance(models.Lyrics, 'lyrics')
+@celery.app.task(name="Lyrics.fetch_content")
+@celery.require_instance(models.Lyrics, "lyrics")
 def fetch_content(lyrics):
     html = lyrics_utils._get_html(lyrics.url)
     content = lyrics_utils.extract_content(html)
     cleaned_content = lyrics_utils.clean_content(content)
     lyrics.content = cleaned_content
-    lyrics.save(update_fields=['content'])
+    lyrics.save(update_fields=["content"])
 
 
-@celery.app.task(name='music.import_batch_notify_followers')
+@celery.app.task(name="music.import_batch_notify_followers")
 @celery.require_instance(
-    models.ImportBatch.objects.filter(status='finished'), 'import_batch')
+    models.ImportBatch.objects.filter(status="finished"), "import_batch"
+)
 def import_batch_notify_followers(import_batch):
-    if not preferences.get('federation__enabled'):
+    if not preferences.get("federation__enabled"):
         return
 
-    if import_batch.source == 'federation':
+    if import_batch.source == "federation":
         return
 
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
     followers = library_actor.get_approved_followers()
     jobs = import_batch.jobs.filter(
-        status='finished',
-        library_track__isnull=True,
-        track_file__isnull=False,
-    ).select_related(
-        'track_file__track__artist',
-        'track_file__track__album__artist',
-    )
+        status="finished", library_track__isnull=True, track_file__isnull=False
+    ).select_related("track_file__track__artist", "track_file__track__album__artist")
     track_files = [job.track_file for job in jobs]
-    collection = federation_serializers.CollectionSerializer({
-        'actor': library_actor,
-        'id': import_batch.get_federation_url(),
-        'items': track_files,
-        'item_serializer': federation_serializers.AudioSerializer
-    }).data
+    collection = federation_serializers.CollectionSerializer(
+        {
+            "actor": library_actor,
+            "id": import_batch.get_federation_url(),
+            "items": track_files,
+            "item_serializer": federation_serializers.AudioSerializer,
+        }
+    ).data
     for f in followers:
         create = federation_serializers.ActivitySerializer(
             {
-                'type': 'Create',
-                'id': collection['id'],
-                'object': collection,
-                'actor': library_actor.url,
-                'to': [f.url],
+                "type": "Create",
+                "id": collection["id"],
+                "object": collection,
+                "actor": library_actor.url,
+                "to": [f.url],
             }
         ).data
 
diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py
index 3b9fbb21476d8b4e45fa8fc8356d0b0b635e1685..3080c1c6c056b84aa29842d1040087d3f0492ad6 100644
--- a/api/funkwhale_api/music/utils.py
+++ b/api/funkwhale_api/music/utils.py
@@ -1,34 +1,36 @@
-import magic
 import mimetypes
-import mutagen
 import re
 
+import magic
+import mutagen
 from django.db.models import Q
 
 
-def normalize_query(query_string,
-                    findterms=re.compile(r'"([^"]+)"|(\S+)').findall,
-                    normspace=re.compile(r'\s{2,}').sub):
-    ''' Splits the query string in invidual keywords, getting rid of unecessary spaces
+def normalize_query(
+    query_string,
+    findterms=re.compile(r'"([^"]+)"|(\S+)').findall,
+    normspace=re.compile(r"\s{2,}").sub,
+):
+    """ Splits the query string in invidual keywords, getting rid of unecessary spaces
         and grouping quoted words together.
         Example:
 
         >>> normalize_query('  some random  words "with   quotes  " and   spaces')
         ['some', 'random', 'words', 'with quotes', 'and', 'spaces']
 
-    '''
-    return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)]
+    """
+    return [normspace(" ", (t[0] or t[1]).strip()) for t in findterms(query_string)]
 
 
 def get_query(query_string, search_fields):
-    ''' Returns a query, that is a combination of Q objects. That combination
+    """ Returns a query, that is a combination of Q objects. That combination
         aims to search keywords within a model by testing the given search fields.
 
-    '''
-    query = None # Query to search for every search term
+    """
+    query = None  # Query to search for every search term
     terms = normalize_query(query_string)
     for term in terms:
-        or_query = None # Query to search for a given term in each field
+        or_query = None  # Query to search for a given term in each field
         for field_name in search_fields:
             q = Q(**{"%s__icontains" % field_name: term})
             if or_query is None:
@@ -45,7 +47,7 @@ def get_query(query_string, search_fields):
 def guess_mimetype(f):
     b = min(1000000, f.size)
     t = magic.from_buffer(f.read(b), mime=True)
-    if not t.startswith('audio/'):
+    if not t.startswith("audio/"):
         # failure, we try guessing by extension
         mt, _ = mimetypes.guess_type(f.path)
         if mt:
@@ -54,20 +56,20 @@ def guess_mimetype(f):
 
 
 def compute_status(jobs):
-    statuses = jobs.order_by().values_list('status', flat=True).distinct()
-    errored = any([status == 'errored' for status in statuses])
+    statuses = jobs.order_by().values_list("status", flat=True).distinct()
+    errored = any([status == "errored" for status in statuses])
     if errored:
-        return 'errored'
-    pending = any([status == 'pending' for status in statuses])
+        return "errored"
+    pending = any([status == "pending" for status in statuses])
     if pending:
-        return 'pending'
-    return 'finished'
+        return "pending"
+    return "finished"
 
 
 AUDIO_EXTENSIONS_AND_MIMETYPE = [
-    ('ogg', 'audio/ogg'),
-    ('mp3', 'audio/mpeg'),
-    ('flac', 'audio/x-flac'),
+    ("ogg", "audio/ogg"),
+    ("mp3", "audio/mpeg"),
+    ("flac", "audio/x-flac"),
 ]
 
 EXTENSION_TO_MIMETYPE = {ext: mt for ext, mt in AUDIO_EXTENSIONS_AND_MIMETYPE}
@@ -79,7 +81,7 @@ def get_ext_from_type(mimetype):
 
 
 def get_type_from_ext(extension):
-    if extension.startswith('.'):
+    if extension.startswith("."):
         # we remove leading dot
         extension = extension[1:]
     return EXTENSION_TO_MIMETYPE.get(extension)
@@ -90,7 +92,7 @@ def get_audio_file_data(f):
     if not data:
         return
     d = {}
-    d['bitrate'] = data.info.bitrate
-    d['length'] = data.info.length
+    d["bitrate"] = data.info.bitrate
+    d["length"] = data.info.length
 
     return d
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 2850c077051a7a25b210f3d1eebe728b981bdba4..77a82dd21d36b2502eec37d2f188e58d1ab31501 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -1,55 +1,40 @@
-import ffmpeg
-import os
 import json
 import logging
-import subprocess
-import unicodedata
 import urllib
 
-from django.contrib.auth.decorators import login_required
-from django.core.exceptions import ObjectDoesNotExist
 from django.conf import settings
-from django.db import models, transaction
-from django.db.models.functions import Length
+from django.core.exceptions import ObjectDoesNotExist
+from django.db import transaction
 from django.db.models import Count
-from django.http import StreamingHttpResponse
-from django.urls import reverse
+from django.db.models.functions import Length
 from django.utils import timezone
-from django.utils.decorators import method_decorator
-
-from rest_framework import viewsets, views, mixins
+from musicbrainzngs import ResponseError
+from rest_framework import mixins
+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.response import Response
-from rest_framework import settings as rest_settings
-from rest_framework import permissions
-from musicbrainzngs import ResponseError
+from taggit.models import Tag
 
 from funkwhale_api.common import utils as funkwhale_utils
 from funkwhale_api.common.permissions import ConditionalAuthentication
-from funkwhale_api.users.permissions import HasUserPermission
-from taggit.models import Tag
-from funkwhale_api.federation import actors
 from funkwhale_api.federation.authentication import SignatureAuthentication
 from funkwhale_api.federation.models import LibraryTrack
 from funkwhale_api.musicbrainz import api
 from funkwhale_api.requests.models import ImportRequest
+from funkwhale_api.users.permissions import HasUserPermission
 
-from . import filters
-from . import importers
-from . import models
+from . import filters, importers, models
 from . import permissions as music_permissions
-from . import serializers
-from . import tasks
-from . import utils
+from . import serializers, tasks, utils
 
 logger = logging.getLogger(__name__)
 
 
 class TagViewSetMixin(object):
-
     def get_queryset(self):
         queryset = super().get_queryset()
-        tag = self.request.query_params.get('tag')
+        tag = self.request.query_params.get("tag")
         if tag:
             queryset = queryset.filter(tags__pk=tag)
         return queryset
@@ -60,38 +45,37 @@ class ArtistViewSet(viewsets.ReadOnlyModelViewSet):
     serializer_class = serializers.ArtistWithAlbumsSerializer
     permission_classes = [ConditionalAuthentication]
     filter_class = filters.ArtistFilter
-    ordering_fields = ('id', 'name', 'creation_date')
+    ordering_fields = ("id", "name", "creation_date")
 
 
 class AlbumViewSet(viewsets.ReadOnlyModelViewSet):
     queryset = (
         models.Album.objects.all()
-                            .order_by('artist', 'release_date')
-                            .select_related()
-                            .prefetch_related(
-                                'tracks__artist',
-                                'tracks__files'))
+        .order_by("artist", "release_date")
+        .select_related()
+        .prefetch_related("tracks__artist", "tracks__files")
+    )
     serializer_class = serializers.AlbumSerializer
     permission_classes = [ConditionalAuthentication]
-    ordering_fields = ('creation_date', 'release_date', 'title')
+    ordering_fields = ("creation_date", "release_date", "title")
     filter_class = filters.AlbumFilter
 
 
 class ImportBatchViewSet(
-        mixins.CreateModelMixin,
-        mixins.ListModelMixin,
-        mixins.RetrieveModelMixin,
-        viewsets.GenericViewSet):
+    mixins.CreateModelMixin,
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    viewsets.GenericViewSet,
+):
     queryset = (
-        models.ImportBatch.objects
-              .select_related()
-              .order_by('-creation_date')
-              .annotate(job_count=Count('jobs'))
+        models.ImportBatch.objects.select_related()
+        .order_by("-creation_date")
+        .annotate(job_count=Count("jobs"))
     )
     serializer_class = serializers.ImportBatchSerializer
     permission_classes = (HasUserPermission,)
-    required_permissions = ['library', 'upload']
-    permission_operator = 'or'
+    required_permissions = ["library", "upload"]
+    permission_operator = "or"
     filter_class = filters.ImportBatchFilter
 
     def perform_create(self, serializer):
@@ -101,51 +85,50 @@ class ImportBatchViewSet(
         qs = super().get_queryset()
         # if user do not have library permission, we limit to their
         # own jobs
-        if not self.request.user.has_permissions('library'):
+        if not self.request.user.has_permissions("library"):
             qs = qs.filter(submitted_by=self.request.user)
         return qs
 
 
 class ImportJobViewSet(
-        mixins.CreateModelMixin,
-        mixins.ListModelMixin,
-        viewsets.GenericViewSet):
-    queryset = (models.ImportJob.objects.all().select_related())
+    mixins.CreateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet
+):
+    queryset = models.ImportJob.objects.all().select_related()
     serializer_class = serializers.ImportJobSerializer
     permission_classes = (HasUserPermission,)
-    required_permissions = ['library', 'upload']
-    permission_operator = 'or'
+    required_permissions = ["library", "upload"]
+    permission_operator = "or"
     filter_class = filters.ImportJobFilter
 
     def get_queryset(self):
         qs = super().get_queryset()
         # if user do not have library permission, we limit to their
         # own jobs
-        if not self.request.user.has_permissions('library'):
+        if not self.request.user.has_permissions("library"):
             qs = qs.filter(batch__submitted_by=self.request.user)
         return qs
 
-    @list_route(methods=['get'])
+    @list_route(methods=["get"])
     def stats(self, request, *args, **kwargs):
-        if not request.user.has_permissions('library'):
+        if not request.user.has_permissions("library"):
             return Response(status=403)
         qs = models.ImportJob.objects.all()
         filterset = filters.ImportJobFilter(request.GET, queryset=qs)
         qs = filterset.qs
-        qs = qs.values('status').order_by('status')
-        qs = qs.annotate(status_count=Count('status'))
+        qs = qs.values("status").order_by("status")
+        qs = qs.annotate(status_count=Count("status"))
 
         data = {}
         for row in qs:
-            data[row['status']] = row['status_count']
+            data[row["status"]] = row["status_count"]
 
         for s, _ in models.IMPORT_STATUS_CHOICES:
             data.setdefault(s, 0)
 
-        data['count'] = sum([v for v in data.values()])
+        data["count"] = sum([v for v in data.values()])
         return Response(data)
 
-    @list_route(methods=['post'])
+    @list_route(methods=["post"])
     def run(self, request, *args, **kwargs):
         serializer = serializers.ImportJobRunSerializer(data=request.data)
         serializer.is_valid(raise_exception=True)
@@ -154,11 +137,10 @@ class ImportJobViewSet(
         return Response(payload)
 
     def perform_create(self, serializer):
-        source = 'file://' + serializer.validated_data['audio_file'].name
+        source = "file://" + serializer.validated_data["audio_file"].name
         serializer.save(source=source)
         funkwhale_utils.on_commit(
-            tasks.import_job_run.delay,
-            import_job_id=serializer.instance.pk
+            tasks.import_job_run.delay, import_job_id=serializer.instance.pk
         )
 
 
@@ -166,33 +148,34 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
     """
     A simple ViewSet for viewing and editing accounts.
     """
-    queryset = (models.Track.objects.all().for_nested_serialization())
+
+    queryset = models.Track.objects.all().for_nested_serialization()
     serializer_class = serializers.TrackSerializer
     permission_classes = [ConditionalAuthentication]
     filter_class = filters.TrackFilter
     ordering_fields = (
-        'creation_date',
-        'title',
-        'album__title',
-        'album__release_date',
-        'position',
-        'artist__name',
+        "creation_date",
+        "title",
+        "album__title",
+        "album__release_date",
+        "position",
+        "artist__name",
     )
 
     def get_queryset(self):
         queryset = super().get_queryset()
-        filter_favorites = self.request.GET.get('favorites', None)
+        filter_favorites = self.request.GET.get("favorites", None)
         user = self.request.user
-        if user.is_authenticated and filter_favorites == 'true':
+        if user.is_authenticated and filter_favorites == "true":
             queryset = queryset.filter(track_favorites__user=user)
 
         return queryset
 
-    @detail_route(methods=['get'])
+    @detail_route(methods=["get"])
     @transaction.non_atomic_requests
     def lyrics(self, request, *args, **kwargs):
         try:
-            track = models.Track.objects.get(pk=kwargs['pk'])
+            track = models.Track.objects.get(pk=kwargs["pk"])
         except models.Track.DoesNotExist:
             return Response(status=404)
 
@@ -201,7 +184,7 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
             work = track.get_work()
 
         if not work:
-            return Response({'error': 'unavailable work '}, status=404)
+            return Response({"error": "unavailable work "}, status=404)
 
         lyrics = work.fetch_lyrics()
         try:
@@ -209,7 +192,7 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet):
                 tasks.fetch_content(lyrics_id=lyrics.pk)
                 lyrics.refresh_from_db()
         except AttributeError:
-            return Response({'error': 'unavailable lyrics'}, status=404)
+            return Response({"error": "unavailable lyrics"}, status=404)
         serializer = serializers.LyricsSerializer(lyrics)
         return Response(serializer.data)
 
@@ -218,7 +201,7 @@ def get_file_path(audio_file):
     serve_path = settings.MUSIC_DIRECTORY_SERVE_PATH
     prefix = settings.MUSIC_DIRECTORY_PATH
     t = settings.REVERSE_PROXY_TYPE
-    if t == 'nginx':
+    if t == "nginx":
         # we have to use the internal locations
         try:
             path = audio_file.url
@@ -226,30 +209,30 @@ def get_file_path(audio_file):
             # a path was given
             if not serve_path or not prefix:
                 raise ValueError(
-                    'You need to specify MUSIC_DIRECTORY_SERVE_PATH and '
-                    'MUSIC_DIRECTORY_PATH to serve in-place imported files'
+                    "You need to specify MUSIC_DIRECTORY_SERVE_PATH and "
+                    "MUSIC_DIRECTORY_PATH to serve in-place imported files"
                 )
-            path = '/music' + audio_file.replace(prefix, '', 1)
-        return (settings.PROTECT_FILES_PATH + path).encode('utf-8')
-    if t == 'apache2':
+            path = "/music" + audio_file.replace(prefix, "", 1)
+        return (settings.PROTECT_FILES_PATH + path).encode("utf-8")
+    if t == "apache2":
         try:
             path = audio_file.path
         except AttributeError:
             # a path was given
             if not serve_path or not prefix:
                 raise ValueError(
-                    'You need to specify MUSIC_DIRECTORY_SERVE_PATH and '
-                    'MUSIC_DIRECTORY_PATH to serve in-place imported files'
+                    "You need to specify MUSIC_DIRECTORY_SERVE_PATH and "
+                    "MUSIC_DIRECTORY_PATH to serve in-place imported files"
                 )
             path = audio_file.replace(prefix, serve_path, 1)
-        return path.encode('utf-8')
+        return path.encode("utf-8")
 
 
 def handle_serve(track_file):
     f = track_file
     # we update the accessed_date
     f.accessed_date = timezone.now()
-    f.save(update_fields=['accessed_date'])
+    f.save(update_fields=["accessed_date"])
 
     mt = f.mimetype
     audio_file = f.audio_file
@@ -270,28 +253,24 @@ def handle_serve(track_file):
                 library_track.download_audio()
             track_file.library_track = library_track
             track_file.set_audio_data()
-            track_file.save(update_fields=['bitrate', 'duration', 'size'])
+            track_file.save(update_fields=["bitrate", "duration", "size"])
 
         audio_file = library_track.audio_file
         file_path = get_file_path(audio_file)
         mt = library_track.audio_mimetype
     elif audio_file:
         file_path = get_file_path(audio_file)
-    elif f.source and f.source.startswith('file://'):
-        file_path = get_file_path(f.source.replace('file://', '', 1))
+    elif f.source and f.source.startswith("file://"):
+        file_path = get_file_path(f.source.replace("file://", "", 1))
     if mt:
         response = Response(content_type=mt)
     else:
         response = Response()
     filename = f.filename
-    mapping = {
-        'nginx': 'X-Accel-Redirect',
-        'apache2': 'X-Sendfile',
-    }
+    mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
     file_header = mapping[settings.REVERSE_PROXY_TYPE]
     response[file_header] = file_path
-    filename = "filename*=UTF-8''{}".format(
-        urllib.parse.quote(filename))
+    filename = "filename*=UTF-8''{}".format(urllib.parse.quote(filename))
     response["Content-Disposition"] = "attachment; {}".format(filename)
     if mt:
         response["Content-Type"] = mt
@@ -302,30 +281,29 @@ def handle_serve(track_file):
 class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
     queryset = (
         models.TrackFile.objects.all()
-            .select_related('track__artist', 'track__album')
-            .order_by('-id')
+        .select_related("track__artist", "track__album")
+        .order_by("-id")
     )
     serializer_class = serializers.TrackFileSerializer
-    authentication_classes = rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES + [
-        SignatureAuthentication
-    ]
+    authentication_classes = (
+        rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES
+        + [SignatureAuthentication]
+    )
     permission_classes = [music_permissions.Listen]
 
-    @detail_route(methods=['get'])
+    @detail_route(methods=["get"])
     def serve(self, request, *args, **kwargs):
         queryset = models.TrackFile.objects.select_related(
-            'library_track',
-            'track__album__artist',
-            'track__artist',
+            "library_track", "track__album__artist", "track__artist"
         )
         try:
-            return handle_serve(queryset.get(pk=kwargs['pk']))
+            return handle_serve(queryset.get(pk=kwargs["pk"]))
         except models.TrackFile.DoesNotExist:
             return Response(status=404)
 
 
 class TagViewSet(viewsets.ReadOnlyModelViewSet):
-    queryset = Tag.objects.all().order_by('name')
+    queryset = Tag.objects.all().order_by("name")
     serializer_class = serializers.TagSerializer
     permission_classes = [ConditionalAuthentication]
 
@@ -335,85 +313,91 @@ class Search(views.APIView):
     permission_classes = [ConditionalAuthentication]
 
     def get(self, request, *args, **kwargs):
-        query = request.GET['query']
+        query = request.GET["query"]
         results = {
             # 'tags': serializers.TagSerializer(self.get_tags(query), many=True).data,
-            'artists': serializers.ArtistWithAlbumsSerializer(self.get_artists(query), many=True).data,
-            'tracks': serializers.TrackSerializer(self.get_tracks(query), many=True).data,
-            'albums': serializers.AlbumSerializer(self.get_albums(query), many=True).data,
+            "artists": serializers.ArtistWithAlbumsSerializer(
+                self.get_artists(query), many=True
+            ).data,
+            "tracks": serializers.TrackSerializer(
+                self.get_tracks(query), many=True
+            ).data,
+            "albums": serializers.AlbumSerializer(
+                self.get_albums(query), many=True
+            ).data,
         }
         return Response(results, status=200)
 
     def get_tracks(self, query):
         search_fields = [
-            'mbid',
-            'title__unaccent',
-            'album__title__unaccent',
-            'artist__name__unaccent']
+            "mbid",
+            "title__unaccent",
+            "album__title__unaccent",
+            "artist__name__unaccent",
+        ]
         query_obj = utils.get_query(query, search_fields)
         return (
             models.Track.objects.all()
-                        .filter(query_obj)
-                        .select_related('artist', 'album__artist')
-                        .prefetch_related('files')
-        )[:self.max_results]
+            .filter(query_obj)
+            .select_related("artist", "album__artist")
+            .prefetch_related("files")
+        )[: self.max_results]
 
     def get_albums(self, query):
-        search_fields = [
-            'mbid',
-            'title__unaccent',
-            'artist__name__unaccent']
+        search_fields = ["mbid", "title__unaccent", "artist__name__unaccent"]
         query_obj = utils.get_query(query, search_fields)
         return (
             models.Album.objects.all()
-                        .filter(query_obj)
-                        .select_related()
-                        .prefetch_related(
-                            'tracks__files',
-                        )
-        )[:self.max_results]
+            .filter(query_obj)
+            .select_related()
+            .prefetch_related("tracks__files")
+        )[: self.max_results]
 
     def get_artists(self, query):
-        search_fields = ['mbid', 'name__unaccent']
+        search_fields = ["mbid", "name__unaccent"]
         query_obj = utils.get_query(query, search_fields)
-        return (
-            models.Artist.objects.all()
-                         .filter(query_obj)
-                         .with_albums()
-        )[:self.max_results]
+        return (models.Artist.objects.all().filter(query_obj).with_albums())[
+            : self.max_results
+        ]
 
     def get_tags(self, query):
-        search_fields = ['slug', 'name__unaccent']
+        search_fields = ["slug", "name__unaccent"]
         query_obj = utils.get_query(query, search_fields)
 
         # We want the shortest tag first
-        qs = Tag.objects.all().annotate(slug_length=Length('slug')).order_by('slug_length')
+        qs = (
+            Tag.objects.all()
+            .annotate(slug_length=Length("slug"))
+            .order_by("slug_length")
+        )
 
-        return qs.filter(query_obj)[:self.max_results]
+        return qs.filter(query_obj)[: self.max_results]
 
 
 class SubmitViewSet(viewsets.ViewSet):
     queryset = models.ImportBatch.objects.none()
     permission_classes = (HasUserPermission,)
-    required_permissions = ['library']
+    required_permissions = ["library"]
 
-    @list_route(methods=['post'])
+    @list_route(methods=["post"])
     @transaction.non_atomic_requests
     def single(self, request, *args, **kwargs):
         try:
-            models.Track.objects.get(mbid=request.POST['mbid'])
+            models.Track.objects.get(mbid=request.POST["mbid"])
             return Response({})
         except models.Track.DoesNotExist:
             pass
         batch = models.ImportBatch.objects.create(submitted_by=request.user)
-        job = models.ImportJob.objects.create(mbid=request.POST['mbid'], batch=batch, source=request.POST['import_url'])
+        job = models.ImportJob.objects.create(
+            mbid=request.POST["mbid"], batch=batch, source=request.POST["import_url"]
+        )
         tasks.import_job_run.delay(import_job_id=job.pk)
         serializer = serializers.ImportBatchSerializer(batch)
         return Response(serializer.data, status=201)
 
     def get_import_request(self, data):
         try:
-            raw = data['importRequest']
+            raw = data["importRequest"]
         except KeyError:
             return
 
@@ -423,57 +407,64 @@ class SubmitViewSet(viewsets.ViewSet):
         except ImportRequest.DoesNotExist:
             pass
 
-    @list_route(methods=['post'])
+    @list_route(methods=["post"])
     @transaction.non_atomic_requests
     def album(self, request, *args, **kwargs):
-        data = json.loads(request.body.decode('utf-8'))
+        data = json.loads(request.body.decode("utf-8"))
         import_request = self.get_import_request(data)
         import_data, batch = self._import_album(
-            data, request, batch=None, import_request=import_request)
+            data, request, batch=None, import_request=import_request
+        )
         return Response(import_data)
 
     @transaction.atomic
     def _import_album(self, data, request, batch=None, import_request=None):
         # we import the whole album here to prevent race conditions that occurs
         # when using get_or_create_from_api in tasks
-        album_data = api.releases.get(id=data['releaseId'], includes=models.Album.api_includes)['release']
+        album_data = api.releases.get(
+            id=data["releaseId"], includes=models.Album.api_includes
+        )["release"]
         cleaned_data = models.Album.clean_musicbrainz_data(album_data)
-        album = importers.load(models.Album, cleaned_data, album_data, import_hooks=[models.import_tracks])
+        album = importers.load(
+            models.Album, cleaned_data, album_data, import_hooks=[models.import_tracks]
+        )
         try:
             album.get_image()
         except ResponseError:
             pass
         if not batch:
             batch = models.ImportBatch.objects.create(
-                submitted_by=request.user,
-                import_request=import_request)
-        for row in data['tracks']:
+                submitted_by=request.user, import_request=import_request
+            )
+        for row in data["tracks"]:
             try:
-                models.TrackFile.objects.get(track__mbid=row['mbid'])
+                models.TrackFile.objects.get(track__mbid=row["mbid"])
             except models.TrackFile.DoesNotExist:
-                job = models.ImportJob.objects.create(mbid=row['mbid'], batch=batch, source=row['source'])
+                job = models.ImportJob.objects.create(
+                    mbid=row["mbid"], batch=batch, source=row["source"]
+                )
                 funkwhale_utils.on_commit(
-                    tasks.import_job_run.delay,
-                    import_job_id=job.pk
+                    tasks.import_job_run.delay, import_job_id=job.pk
                 )
 
         serializer = serializers.ImportBatchSerializer(batch)
         return serializer.data, batch
 
-    @list_route(methods=['post'])
+    @list_route(methods=["post"])
     @transaction.non_atomic_requests
     def artist(self, request, *args, **kwargs):
-        data = json.loads(request.body.decode('utf-8'))
+        data = json.loads(request.body.decode("utf-8"))
         import_request = self.get_import_request(data)
-        artist_data = api.artists.get(id=data['artistId'])['artist']
+        artist_data = api.artists.get(id=data["artistId"])["artist"]
         cleaned_data = models.Artist.clean_musicbrainz_data(artist_data)
-        artist = importers.load(models.Artist, cleaned_data, artist_data, import_hooks=[])
+        importers.load(models.Artist, cleaned_data, artist_data, import_hooks=[])
 
         import_data = []
         batch = None
-        for row in data['albums']:
+        for row in data["albums"]:
             row_data, batch = self._import_album(
-                row, request, batch=batch, import_request=import_request)
+                row, request, batch=batch, import_request=import_request
+            )
             import_data.append(row_data)
 
         return Response(import_data[0])
diff --git a/api/funkwhale_api/musicbrainz/__init__.py b/api/funkwhale_api/musicbrainz/__init__.py
index 00aa85d5c3ce804e66c3f346e391544a1a4705ee..103da679f942d74894ac500c0ea081d4f4c31439 100644
--- a/api/funkwhale_api/musicbrainz/__init__.py
+++ b/api/funkwhale_api/musicbrainz/__init__.py
@@ -1 +1,3 @@
 from .client import api
+
+__all__ = ["api"]
diff --git a/api/funkwhale_api/musicbrainz/client.py b/api/funkwhale_api/musicbrainz/client.py
index 8e7076a78b1ae25f47c521c1360d98f077576813..deae0672fdd994c3914149d18a561f96ad2a77ac 100644
--- a/api/funkwhale_api/musicbrainz/client.py
+++ b/api/funkwhale_api/musicbrainz/client.py
@@ -1,21 +1,21 @@
-import musicbrainzngs
 import memoize.djangocache
-
+import musicbrainzngs
 from django.conf import settings
+
 from funkwhale_api import __version__
 
 _api = musicbrainzngs
-_api.set_useragent('funkwhale', str(__version__), settings.FUNKWHALE_URL)
+_api.set_useragent("funkwhale", str(__version__), settings.FUNKWHALE_URL)
 
 
-store = memoize.djangocache.Cache('default')
-memo = memoize.Memoizer(store, namespace='memoize:musicbrainz')
+store = memoize.djangocache.Cache("default")
+memo = memoize.Memoizer(store, namespace="memoize:musicbrainz")
 
 
 def clean_artist_search(query, **kwargs):
     cleaned_kwargs = {}
-    if kwargs.get('name'):
-        cleaned_kwargs['artist'] = kwargs.get('name')
+    if kwargs.get("name"):
+        cleaned_kwargs["artist"] = kwargs.get("name")
     return _api.search_artists(query, **cleaned_kwargs)
 
 
@@ -23,55 +23,43 @@ class API(object):
     _api = _api
 
     class artists(object):
-        search = memo(
-            clean_artist_search, max_age=settings.MUSICBRAINZ_CACHE_DURATION)
-        get = memo(
-            _api.get_artist_by_id,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+        search = memo(clean_artist_search, max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+        get = memo(_api.get_artist_by_id, max_age=settings.MUSICBRAINZ_CACHE_DURATION)
 
     class images(object):
         get_front = memo(
-            _api.get_image_front,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+            _api.get_image_front, max_age=settings.MUSICBRAINZ_CACHE_DURATION
+        )
 
     class recordings(object):
         search = memo(
-            _api.search_recordings,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+            _api.search_recordings, max_age=settings.MUSICBRAINZ_CACHE_DURATION
+        )
         get = memo(
-            _api.get_recording_by_id,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+            _api.get_recording_by_id, max_age=settings.MUSICBRAINZ_CACHE_DURATION
+        )
 
     class works(object):
-        search = memo(
-            _api.search_works,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
-        get = memo(
-            _api.get_work_by_id,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+        search = memo(_api.search_works, max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+        get = memo(_api.get_work_by_id, max_age=settings.MUSICBRAINZ_CACHE_DURATION)
 
     class releases(object):
-        search = memo(
-            _api.search_releases,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
-        get = memo(
-            _api.get_release_by_id,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
-        browse = memo(
-            _api.browse_releases,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+        search = memo(_api.search_releases, max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+        get = memo(_api.get_release_by_id, max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+        browse = memo(_api.browse_releases, max_age=settings.MUSICBRAINZ_CACHE_DURATION)
         # get_image_front = _api.get_image_front
 
     class release_groups(object):
         search = memo(
-            _api.search_release_groups,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+            _api.search_release_groups, max_age=settings.MUSICBRAINZ_CACHE_DURATION
+        )
         get = memo(
-            _api.get_release_group_by_id,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+            _api.get_release_group_by_id, max_age=settings.MUSICBRAINZ_CACHE_DURATION
+        )
         browse = memo(
-            _api.browse_release_groups,
-            max_age=settings.MUSICBRAINZ_CACHE_DURATION)
+            _api.browse_release_groups, max_age=settings.MUSICBRAINZ_CACHE_DURATION
+        )
         # get_image_front = _api.get_image_front
 
+
 api = API()
diff --git a/api/funkwhale_api/musicbrainz/urls.py b/api/funkwhale_api/musicbrainz/urls.py
index 7befe49ab59a2a112f2ce540bebb0e6a37fa6ee7..d14447f14a62a73a494e9c6752d67274824f18ed 100644
--- a/api/funkwhale_api/musicbrainz/urls.py
+++ b/api/funkwhale_api/musicbrainz/urls.py
@@ -1,23 +1,31 @@
-from django.conf.urls import include, url
+from django.conf.urls import url
 from rest_framework import routers
 
 from . import views
 
 router = routers.SimpleRouter()
-router.register(r'search', views.SearchViewSet, 'search')
+router.register(r"search", views.SearchViewSet, "search")
 urlpatterns = [
-    url('releases/(?P<uuid>[0-9a-z-]+)/$',
+    url(
+        "releases/(?P<uuid>[0-9a-z-]+)/$",
         views.ReleaseDetail.as_view(),
-        name='release-detail'),
-    url('artists/(?P<uuid>[0-9a-z-]+)/$',
+        name="release-detail",
+    ),
+    url(
+        "artists/(?P<uuid>[0-9a-z-]+)/$",
         views.ArtistDetail.as_view(),
-        name='artist-detail'),
-    url('release-groups/browse/(?P<artist_uuid>[0-9a-z-]+)/$',
+        name="artist-detail",
+    ),
+    url(
+        "release-groups/browse/(?P<artist_uuid>[0-9a-z-]+)/$",
         views.ReleaseGroupBrowse.as_view(),
-        name='release-group-browse'),
-    url('releases/browse/(?P<release_group_uuid>[0-9a-z-]+)/$',
+        name="release-group-browse",
+    ),
+    url(
+        "releases/browse/(?P<release_group_uuid>[0-9a-z-]+)/$",
         views.ReleaseBrowse.as_view(),
-        name='release-browse'),
+        name="release-browse",
+    ),
     # url('release-groups/(?P<uuid>[0-9a-z-]+)/$',
     #     views.ReleaseGroupDetail.as_view(),
     #     name='release-group-detail'),
diff --git a/api/funkwhale_api/musicbrainz/views.py b/api/funkwhale_api/musicbrainz/views.py
index 5c101b161900d5f2eada7ecf0c0ad7f1dabc2c74..b6f009dca7531dd932f9d5c32c6e3ae1f65cb03e 100644
--- a/api/funkwhale_api/musicbrainz/views.py
+++ b/api/funkwhale_api/musicbrainz/views.py
@@ -1,12 +1,10 @@
 from rest_framework import viewsets
-from rest_framework.views import APIView
-from rest_framework.response import Response
 from rest_framework.decorators import list_route
-import musicbrainzngs
+from rest_framework.response import Response
+from rest_framework.views import APIView
 
 from funkwhale_api.common.permissions import ConditionalAuthentication
 
-
 from .client import api
 
 
@@ -14,8 +12,7 @@ class ReleaseDetail(APIView):
     permission_classes = [ConditionalAuthentication]
 
     def get(self, request, *args, **kwargs):
-        result = api.releases.get(
-            id=kwargs['uuid'], includes=['artists', 'recordings'])
+        result = api.releases.get(id=kwargs["uuid"], includes=["artists", "recordings"])
         return Response(result)
 
 
@@ -23,9 +20,7 @@ class ArtistDetail(APIView):
     permission_classes = [ConditionalAuthentication]
 
     def get(self, request, *args, **kwargs):
-        result = api.artists.get(
-            id=kwargs['uuid'],
-            includes=['release-groups'])
+        result = api.artists.get(id=kwargs["uuid"], includes=["release-groups"])
         # import json; print(json.dumps(result, indent=4))
         return Response(result)
 
@@ -34,8 +29,7 @@ class ReleaseGroupBrowse(APIView):
     permission_classes = [ConditionalAuthentication]
 
     def get(self, request, *args, **kwargs):
-        result = api.release_groups.browse(
-            artist=kwargs['artist_uuid'])
+        result = api.release_groups.browse(artist=kwargs["artist_uuid"])
         return Response(result)
 
 
@@ -44,29 +38,30 @@ class ReleaseBrowse(APIView):
 
     def get(self, request, *args, **kwargs):
         result = api.releases.browse(
-            release_group=kwargs['release_group_uuid'],
-            includes=['recordings', 'artist-credits'])
+            release_group=kwargs["release_group_uuid"],
+            includes=["recordings", "artist-credits"],
+        )
         return Response(result)
 
 
 class SearchViewSet(viewsets.ViewSet):
     permission_classes = [ConditionalAuthentication]
 
-    @list_route(methods=['get'])
+    @list_route(methods=["get"])
     def recordings(self, request, *args, **kwargs):
-        query = request.GET['query']
+        query = request.GET["query"]
         results = api.recordings.search(query)
         return Response(results)
 
-    @list_route(methods=['get'])
+    @list_route(methods=["get"])
     def releases(self, request, *args, **kwargs):
-        query = request.GET['query']
+        query = request.GET["query"]
         results = api.releases.search(query)
         return Response(results)
 
-    @list_route(methods=['get'])
+    @list_route(methods=["get"])
     def artists(self, request, *args, **kwargs):
-        query = request.GET['query']
+        query = request.GET["query"]
         results = api.artists.search(query)
         # results = musicbrainzngs.search_artists(query)
         return Response(results)
diff --git a/api/funkwhale_api/playlists/admin.py b/api/funkwhale_api/playlists/admin.py
index 68e447f3842d61ea392b262db83a70e87d09cd89..98ced232ee2f24c79db58529ace1539fa7fbb3de 100644
--- a/api/funkwhale_api/playlists/admin.py
+++ b/api/funkwhale_api/playlists/admin.py
@@ -5,13 +5,13 @@ from . import models
 
 @admin.register(models.Playlist)
 class PlaylistAdmin(admin.ModelAdmin):
-    list_display = ['name', 'user', 'privacy_level', 'creation_date']
-    search_fields = ['name', ]
+    list_display = ["name", "user", "privacy_level", "creation_date"]
+    search_fields = ["name"]
     list_select_related = True
 
 
 @admin.register(models.PlaylistTrack)
 class PlaylistTrackAdmin(admin.ModelAdmin):
-    list_display = ['playlist', 'track', 'index']
-    search_fields = ['track__name', 'playlist__name']
+    list_display = ["playlist", "track", "index"]
+    search_fields = ["track__name", "playlist__name"]
     list_select_related = True
diff --git a/api/funkwhale_api/playlists/dynamic_preferences_registry.py b/api/funkwhale_api/playlists/dynamic_preferences_registry.py
index b717177a2368ccfb267f651bd76def8c6e9fc443..5a2043452bc734841c85e7dde2c7f31ba0c20cab 100644
--- a/api/funkwhale_api/playlists/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/playlists/dynamic_preferences_registry.py
@@ -3,16 +3,14 @@ from dynamic_preferences.registries import global_preferences_registry
 
 from funkwhale_api.common import preferences
 
-playlists = types.Section('playlists')
+playlists = types.Section("playlists")
 
 
 @global_preferences_registry.register
 class MaxTracks(preferences.DefaultFromSettingMixin, types.IntegerPreference):
     show_in_api = True
     section = playlists
-    name = 'max_tracks'
-    verbose_name = 'Max tracks per playlist'
-    setting = 'PLAYLISTS_MAX_TRACKS'
-    field_kwargs = {
-        'required': False,
-    }
+    name = "max_tracks"
+    verbose_name = "Max tracks per playlist"
+    setting = "PLAYLISTS_MAX_TRACKS"
+    field_kwargs = {"required": False}
diff --git a/api/funkwhale_api/playlists/factories.py b/api/funkwhale_api/playlists/factories.py
index cddea60024bec3f6b02941b0d0ae5335f0322b8f..ff031945a57163a633f8863487a6f68299c08cd5 100644
--- a/api/funkwhale_api/playlists/factories.py
+++ b/api/funkwhale_api/playlists/factories.py
@@ -7,11 +7,11 @@ from funkwhale_api.users.factories import UserFactory
 
 @registry.register
 class PlaylistFactory(factory.django.DjangoModelFactory):
-    name = factory.Faker('name')
+    name = factory.Faker("name")
     user = factory.SubFactory(UserFactory)
 
     class Meta:
-        model = 'playlists.Playlist'
+        model = "playlists.Playlist"
 
 
 @registry.register
@@ -20,4 +20,4 @@ class PlaylistTrackFactory(factory.django.DjangoModelFactory):
     track = factory.SubFactory(TrackFactory)
 
     class Meta:
-        model = 'playlists.PlaylistTrack'
+        model = "playlists.PlaylistTrack"
diff --git a/api/funkwhale_api/playlists/filters.py b/api/funkwhale_api/playlists/filters.py
index bc49415100a3c5d3d53fb5fcb680195934d538c3..ae9f0226f265c41e1db2c185aa9c609fed635478 100644
--- a/api/funkwhale_api/playlists/filters.py
+++ b/api/funkwhale_api/playlists/filters.py
@@ -5,18 +5,13 @@ from funkwhale_api.music import utils
 from . import models
 
 
-
 class PlaylistFilter(filters.FilterSet):
-    q = filters.CharFilter(name='_', method='filter_q')
+    q = filters.CharFilter(name="_", method="filter_q")
 
     class Meta:
         model = models.Playlist
-        fields = {
-            'user': ['exact'],
-            'name': ['exact', 'icontains'],
-            'q': 'exact',
-        }
+        fields = {"user": ["exact"], "name": ["exact", "icontains"], "q": "exact"}
 
     def filter_q(self, queryset, name, value):
-        query = utils.get_query(value, ['name', 'user__username'])
+        query = utils.get_query(value, ["name", "user__username"])
         return queryset.filter(query)
diff --git a/api/funkwhale_api/playlists/migrations/0001_initial.py b/api/funkwhale_api/playlists/migrations/0001_initial.py
index 987b2f9cfec9be140658b6f5428d70273333c4b8..68e66d7637723e927f215663996f1ab39a079c09 100644
--- a/api/funkwhale_api/playlists/migrations/0001_initial.py
+++ b/api/funkwhale_api/playlists/migrations/0001_initial.py
@@ -10,34 +10,84 @@ class Migration(migrations.Migration):
 
     dependencies = [
         migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        ('music', '0012_auto_20161122_1905'),
+        ("music", "0012_auto_20161122_1905"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='Playlist',
+            name="Playlist",
             fields=[
-                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
-                ('name', models.CharField(max_length=50)),
-                ('is_public', models.BooleanField(default=False)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='playlists', on_delete=models.CASCADE)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        verbose_name="ID",
+                        primary_key=True,
+                        serialize=False,
+                    ),
+                ),
+                ("name", models.CharField(max_length=50)),
+                ("is_public", models.BooleanField(default=False)),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        to=settings.AUTH_USER_MODEL,
+                        related_name="playlists",
+                        on_delete=models.CASCADE,
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
-            name='PlaylistTrack',
+            name="PlaylistTrack",
             fields=[
-                ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
-                ('lft', models.PositiveIntegerField(db_index=True, editable=False)),
-                ('rght', models.PositiveIntegerField(db_index=True, editable=False)),
-                ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
-                ('position', models.PositiveIntegerField(db_index=True, editable=False)),
-                ('playlist', models.ForeignKey(to='playlists.Playlist', related_name='playlist_tracks', on_delete=models.CASCADE)),
-                ('previous', models.OneToOneField(null=True, to='playlists.PlaylistTrack', related_name='next', blank=True, on_delete=models.CASCADE)),
-                ('track', models.ForeignKey(to='music.Track', related_name='playlist_tracks', on_delete=models.CASCADE)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        verbose_name="ID",
+                        primary_key=True,
+                        serialize=False,
+                    ),
+                ),
+                ("lft", models.PositiveIntegerField(db_index=True, editable=False)),
+                ("rght", models.PositiveIntegerField(db_index=True, editable=False)),
+                ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)),
+                (
+                    "position",
+                    models.PositiveIntegerField(db_index=True, editable=False),
+                ),
+                (
+                    "playlist",
+                    models.ForeignKey(
+                        to="playlists.Playlist",
+                        related_name="playlist_tracks",
+                        on_delete=models.CASCADE,
+                    ),
+                ),
+                (
+                    "previous",
+                    models.OneToOneField(
+                        null=True,
+                        to="playlists.PlaylistTrack",
+                        related_name="next",
+                        blank=True,
+                        on_delete=models.CASCADE,
+                    ),
+                ),
+                (
+                    "track",
+                    models.ForeignKey(
+                        to="music.Track",
+                        related_name="playlist_tracks",
+                        on_delete=models.CASCADE,
+                    ),
+                ),
             ],
-            options={
-                'ordering': ('-playlist', 'position'),
-            },
+            options={"ordering": ("-playlist", "position")},
         ),
     ]
diff --git a/api/funkwhale_api/playlists/migrations/0002_auto_20180316_2217.py b/api/funkwhale_api/playlists/migrations/0002_auto_20180316_2217.py
index 23d0a8eab6a0d174397ff7c6ea62d2fafa9654a7..8245797bfc9ca084145193494a9024ff77d50b6f 100644
--- a/api/funkwhale_api/playlists/migrations/0002_auto_20180316_2217.py
+++ b/api/funkwhale_api/playlists/migrations/0002_auto_20180316_2217.py
@@ -5,18 +5,22 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('playlists', '0001_initial'),
-    ]
+    dependencies = [("playlists", "0001_initial")]
 
     operations = [
-        migrations.RemoveField(
-            model_name='playlist',
-            name='is_public',
-        ),
+        migrations.RemoveField(model_name="playlist", name="is_public"),
         migrations.AddField(
-            model_name='playlist',
-            name='privacy_level',
-            field=models.CharField(choices=[('me', 'Only me'), ('followers', 'Me and my followers'), ('instance', 'Everyone on my instance, and my followers'), ('everyone', 'Everyone, including people on other instances')], default='instance', max_length=30),
+            model_name="playlist",
+            name="privacy_level",
+            field=models.CharField(
+                choices=[
+                    ("me", "Only me"),
+                    ("followers", "Me and my followers"),
+                    ("instance", "Everyone on my instance, and my followers"),
+                    ("everyone", "Everyone, including people on other instances"),
+                ],
+                default="instance",
+                max_length=30,
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/playlists/migrations/0003_auto_20180319_1214.py b/api/funkwhale_api/playlists/migrations/0003_auto_20180319_1214.py
index 0284f8f2cf88f1b04e5a05334b73789fb9233e8c..d4d28b9e049d1658cf877ad02ce41d449635aeb3 100644
--- a/api/funkwhale_api/playlists/migrations/0003_auto_20180319_1214.py
+++ b/api/funkwhale_api/playlists/migrations/0003_auto_20180319_1214.py
@@ -6,47 +6,28 @@ import django.utils.timezone
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('playlists', '0002_auto_20180316_2217'),
-    ]
+    dependencies = [("playlists", "0002_auto_20180316_2217")]
 
     operations = [
         migrations.AlterModelOptions(
-            name='playlisttrack',
-            options={'ordering': ('-playlist', 'index')},
+            name="playlisttrack", options={"ordering": ("-playlist", "index")}
         ),
         migrations.AddField(
-            model_name='playlisttrack',
-            name='creation_date',
+            model_name="playlisttrack",
+            name="creation_date",
             field=models.DateTimeField(default=django.utils.timezone.now),
         ),
         migrations.AddField(
-            model_name='playlisttrack',
-            name='index',
+            model_name="playlisttrack",
+            name="index",
             field=models.PositiveIntegerField(null=True),
         ),
-        migrations.RemoveField(
-            model_name='playlisttrack',
-            name='lft',
-        ),
-        migrations.RemoveField(
-            model_name='playlisttrack',
-            name='position',
-        ),
-        migrations.RemoveField(
-            model_name='playlisttrack',
-            name='previous',
-        ),
-        migrations.RemoveField(
-            model_name='playlisttrack',
-            name='rght',
-        ),
-        migrations.RemoveField(
-            model_name='playlisttrack',
-            name='tree_id',
-        ),
+        migrations.RemoveField(model_name="playlisttrack", name="lft"),
+        migrations.RemoveField(model_name="playlisttrack", name="position"),
+        migrations.RemoveField(model_name="playlisttrack", name="previous"),
+        migrations.RemoveField(model_name="playlisttrack", name="rght"),
+        migrations.RemoveField(model_name="playlisttrack", name="tree_id"),
         migrations.AlterUniqueTogether(
-            name='playlisttrack',
-            unique_together={('playlist', 'index')},
+            name="playlisttrack", unique_together={("playlist", "index")}
         ),
     ]
diff --git a/api/funkwhale_api/playlists/migrations/0004_auto_20180320_1713.py b/api/funkwhale_api/playlists/migrations/0004_auto_20180320_1713.py
index 415b53612a43178051a4103a0d415ff2f7583d02..75c42a5c0c8ab89a83b26ba2a0d9ffe198fc58db 100644
--- a/api/funkwhale_api/playlists/migrations/0004_auto_20180320_1713.py
+++ b/api/funkwhale_api/playlists/migrations/0004_auto_20180320_1713.py
@@ -5,23 +5,18 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('playlists', '0003_auto_20180319_1214'),
-    ]
+    dependencies = [("playlists", "0003_auto_20180319_1214")]
 
     operations = [
         migrations.AddField(
-            model_name='playlist',
-            name='modification_date',
+            model_name="playlist",
+            name="modification_date",
             field=models.DateTimeField(auto_now=True),
         ),
         migrations.AlterField(
-            model_name='playlisttrack',
-            name='index',
+            model_name="playlisttrack",
+            name="index",
             field=models.PositiveIntegerField(blank=True, null=True),
         ),
-        migrations.AlterUniqueTogether(
-            name='playlisttrack',
-            unique_together=set(),
-        ),
+        migrations.AlterUniqueTogether(name="playlisttrack", unique_together=set()),
     ]
diff --git a/api/funkwhale_api/playlists/models.py b/api/funkwhale_api/playlists/models.py
index f5132e12dd9d9beeb03db32090310d46b9efc6b9..e9df4624de9b32d9ef45a1599f545f3cccf72e98 100644
--- a/api/funkwhale_api/playlists/models.py
+++ b/api/funkwhale_api/playlists/models.py
@@ -1,27 +1,22 @@
-from django.conf import settings
-from django.db import models
-from django.db import transaction
+from django.db import models, transaction
 from django.utils import timezone
-
 from rest_framework import exceptions
 
-from funkwhale_api.common import fields
-from funkwhale_api.common import preferences
+from funkwhale_api.common import fields, preferences
 
 
 class PlaylistQuerySet(models.QuerySet):
     def with_tracks_count(self):
-        return self.annotate(
-            _tracks_count=models.Count('playlist_tracks'))
+        return self.annotate(_tracks_count=models.Count("playlist_tracks"))
 
 
 class Playlist(models.Model):
     name = models.CharField(max_length=50)
     user = models.ForeignKey(
-        'users.User', related_name="playlists", on_delete=models.CASCADE)
+        "users.User", related_name="playlists", on_delete=models.CASCADE
+    )
     creation_date = models.DateTimeField(default=timezone.now)
-    modification_date = models.DateTimeField(
-        auto_now=True)
+    modification_date = models.DateTimeField(auto_now=True)
     privacy_level = fields.get_privacy_field()
 
     objects = PlaylistQuerySet.as_manager()
@@ -51,89 +46,91 @@ class Playlist(models.Model):
             index = total
 
         if index > total:
-            raise exceptions.ValidationError('Index is not continuous')
+            raise exceptions.ValidationError("Index is not continuous")
 
         if index < 0:
-            raise exceptions.ValidationError('Index must be zero or positive')
+            raise exceptions.ValidationError("Index must be zero or positive")
 
         if move:
             # we remove the index temporarily, to avoid integrity errors
             plt.index = None
-            plt.save(update_fields=['index'])
+            plt.save(update_fields=["index"])
             if index > old_index:
                 # new index is higher than current, we decrement previous tracks
-                to_update = existing.filter(
-                    index__gt=old_index, index__lte=index)
-                to_update.update(index=models.F('index') - 1)
+                to_update = existing.filter(index__gt=old_index, index__lte=index)
+                to_update.update(index=models.F("index") - 1)
             if index < old_index:
                 # new index is lower than current, we increment next tracks
                 to_update = existing.filter(index__lt=old_index, index__gte=index)
-                to_update.update(index=models.F('index') + 1)
+                to_update.update(index=models.F("index") + 1)
         else:
             to_update = existing.filter(index__gte=index)
-            to_update.update(index=models.F('index') + 1)
+            to_update.update(index=models.F("index") + 1)
 
         plt.index = index
-        plt.save(update_fields=['index'])
-        self.save(update_fields=['modification_date'])
+        plt.save(update_fields=["index"])
+        self.save(update_fields=["modification_date"])
         return index
 
     @transaction.atomic
     def remove(self, index):
         existing = self.playlist_tracks.select_for_update()
-        self.save(update_fields=['modification_date'])
+        self.save(update_fields=["modification_date"])
         to_update = existing.filter(index__gt=index)
-        return to_update.update(index=models.F('index') - 1)
+        return to_update.update(index=models.F("index") - 1)
 
     @transaction.atomic
     def insert_many(self, tracks):
         existing = self.playlist_tracks.select_for_update()
         now = timezone.now()
         total = existing.filter(index__isnull=False).count()
-        max_tracks = preferences.get('playlists__max_tracks')
+        max_tracks = preferences.get("playlists__max_tracks")
         if existing.count() + len(tracks) > max_tracks:
             raise exceptions.ValidationError(
-                'Playlist would reach the maximum of {} tracks'.format(
-                    max_tracks))
-        self.save(update_fields=['modification_date'])
+                "Playlist would reach the maximum of {} tracks".format(max_tracks)
+            )
+        self.save(update_fields=["modification_date"])
         start = total
         plts = [
             PlaylistTrack(
-                creation_date=now, playlist=self, track=track, index=start+i)
+                creation_date=now, playlist=self, track=track, index=start + i
+            )
             for i, track in enumerate(tracks)
         ]
         return PlaylistTrack.objects.bulk_create(plts)
 
+
 class PlaylistTrackQuerySet(models.QuerySet):
     def for_nested_serialization(self):
-        return (self.select_related()
-                    .select_related('track__album__artist')
-                    .prefetch_related(
-                        'track__tags',
-                        'track__files',
-                        'track__artist__albums__tracks__tags'))
+        return (
+            self.select_related()
+            .select_related("track__album__artist")
+            .prefetch_related(
+                "track__tags", "track__files", "track__artist__albums__tracks__tags"
+            )
+        )
 
 
 class PlaylistTrack(models.Model):
     track = models.ForeignKey(
-        'music.Track',
-        related_name='playlist_tracks',
-        on_delete=models.CASCADE)
+        "music.Track", related_name="playlist_tracks", on_delete=models.CASCADE
+    )
     index = models.PositiveIntegerField(null=True, blank=True)
     playlist = models.ForeignKey(
-        Playlist, related_name='playlist_tracks', on_delete=models.CASCADE)
+        Playlist, related_name="playlist_tracks", on_delete=models.CASCADE
+    )
     creation_date = models.DateTimeField(default=timezone.now)
 
     objects = PlaylistTrackQuerySet.as_manager()
 
     class Meta:
-        ordering = ('-playlist', 'index')
-        unique_together = ('playlist', 'index')
+        ordering = ("-playlist", "index")
+        unique_together = ("playlist", "index")
 
     def delete(self, *args, **kwargs):
         playlist = self.playlist
         index = self.index
-        update_indexes = kwargs.pop('update_indexes', False)
+        update_indexes = kwargs.pop("update_indexes", False)
         r = super().delete(*args, **kwargs)
         if index is not None and update_indexes:
             playlist.remove(index)
diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py
index 3f01fd68942b01b0f109e8f32a0eeca29c990585..17cc06b10bdab8f17fb4f3cc246a5e5e26d12fab 100644
--- a/api/funkwhale_api/playlists/serializers.py
+++ b/api/funkwhale_api/playlists/serializers.py
@@ -1,12 +1,11 @@
-from django.conf import settings
 from django.db import transaction
 from rest_framework import serializers
-from taggit.models import Tag
 
 from funkwhale_api.common import preferences
 from funkwhale_api.music.models import Track
 from funkwhale_api.music.serializers import TrackSerializer
 from funkwhale_api.users.serializers import UserBasicSerializer
+
 from . import models
 
 
@@ -15,42 +14,42 @@ class PlaylistTrackSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = models.PlaylistTrack
-        fields = ('id', 'track', 'playlist', 'index', 'creation_date')
+        fields = ("id", "track", "playlist", "index", "creation_date")
 
 
 class PlaylistTrackWriteSerializer(serializers.ModelSerializer):
-    index = serializers.IntegerField(
-        required=False, min_value=0, allow_null=True)
+    index = serializers.IntegerField(required=False, min_value=0, allow_null=True)
 
     class Meta:
         model = models.PlaylistTrack
-        fields = ('id', 'track', 'playlist', 'index')
+        fields = ("id", "track", "playlist", "index")
 
     def validate_playlist(self, value):
-        if self.context.get('request'):
+        if self.context.get("request"):
             # validate proper ownership on the playlist
-            if self.context['request'].user != value.user:
+            if self.context["request"].user != value.user:
                 raise serializers.ValidationError(
-                    'You do not have the permission to edit this playlist')
+                    "You do not have the permission to edit this playlist"
+                )
         existing = value.playlist_tracks.count()
-        max_tracks = preferences.get('playlists__max_tracks')
+        max_tracks = preferences.get("playlists__max_tracks")
         if existing >= max_tracks:
             raise serializers.ValidationError(
-                'Playlist has reached the maximum of {} tracks'.format(
-                    max_tracks))
+                "Playlist has reached the maximum of {} tracks".format(max_tracks)
+            )
         return value
 
     @transaction.atomic
     def create(self, validated_data):
-        index = validated_data.pop('index', None)
+        index = validated_data.pop("index", None)
         instance = super().create(validated_data)
         instance.playlist.insert(instance, index)
         return instance
 
     @transaction.atomic
     def update(self, instance, validated_data):
-        update_index = 'index' in validated_data
-        index = validated_data.pop('index', None)
+        update_index = "index" in validated_data
+        index = validated_data.pop("index", None)
         super().update(instance, validated_data)
         if update_index:
             instance.playlist.insert(instance, index)
@@ -71,17 +70,15 @@ class PlaylistSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Playlist
         fields = (
-            'id',
-            'name',
-            'tracks_count',
-            'user',
-            'modification_date',
-            'creation_date',
-            'privacy_level',)
-        read_only_fields = [
-            'id',
-            'modification_date',
-            'creation_date',]
+            "id",
+            "name",
+            "tracks_count",
+            "user",
+            "modification_date",
+            "creation_date",
+            "privacy_level",
+        )
+        read_only_fields = ["id", "modification_date", "creation_date"]
 
     def get_tracks_count(self, obj):
         try:
@@ -93,4 +90,5 @@ class PlaylistSerializer(serializers.ModelSerializer):
 
 class PlaylistAddManySerializer(serializers.Serializer):
     tracks = serializers.PrimaryKeyRelatedField(
-        many=True, queryset=Track.objects.for_nested_serialization())
+        many=True, queryset=Track.objects.for_nested_serialization()
+    )
diff --git a/api/funkwhale_api/playlists/views.py b/api/funkwhale_api/playlists/views.py
index 683f90388885ecb5337bed10b16ef3c7d9b866a4..d5d19df74b66289c0230c38633dc591c19200bb1 100644
--- a/api/funkwhale_api/playlists/views.py
+++ b/api/funkwhale_api/playlists/views.py
@@ -1,123 +1,118 @@
-from django.db.models import Count
 from django.db import transaction
-
-from rest_framework import exceptions
-from rest_framework import generics, mixins, viewsets
-from rest_framework import status
+from django.db.models import Count
+from rest_framework import exceptions, mixins, viewsets
 from rest_framework.decorators import detail_route
-from rest_framework.response import Response
 from rest_framework.permissions import IsAuthenticatedOrReadOnly
+from rest_framework.response import Response
 
-from funkwhale_api.common import permissions
-from funkwhale_api.common import fields
-from funkwhale_api.music.models import Track
+from funkwhale_api.common import fields, permissions
+
+from . import filters, models, serializers
 
-from . import filters
-from . import models
-from . import serializers
 
 class PlaylistViewSet(
-        mixins.RetrieveModelMixin,
-        mixins.CreateModelMixin,
-        mixins.UpdateModelMixin,
-        mixins.DestroyModelMixin,
-        mixins.ListModelMixin,
-        viewsets.GenericViewSet):
+    mixins.RetrieveModelMixin,
+    mixins.CreateModelMixin,
+    mixins.UpdateModelMixin,
+    mixins.DestroyModelMixin,
+    mixins.ListModelMixin,
+    viewsets.GenericViewSet,
+):
 
     serializer_class = serializers.PlaylistSerializer
     queryset = (
-        models.Playlist.objects.all().select_related('user')
-              .annotate(tracks_count=Count('playlist_tracks'))
+        models.Playlist.objects.all()
+        .select_related("user")
+        .annotate(tracks_count=Count("playlist_tracks"))
     )
     permission_classes = [
         permissions.ConditionalAuthentication,
         permissions.OwnerPermission,
         IsAuthenticatedOrReadOnly,
     ]
-    owner_checks = ['write']
+    owner_checks = ["write"]
     filter_class = filters.PlaylistFilter
-    ordering_fields = ('id', 'name', 'creation_date', 'modification_date')
+    ordering_fields = ("id", "name", "creation_date", "modification_date")
 
-    @detail_route(methods=['get'])
+    @detail_route(methods=["get"])
     def tracks(self, request, *args, **kwargs):
         playlist = self.get_object()
         plts = playlist.playlist_tracks.all().for_nested_serialization()
         serializer = serializers.PlaylistTrackSerializer(plts, many=True)
-        data = {
-            'count': len(plts),
-            'results': serializer.data
-        }
+        data = {"count": len(plts), "results": serializer.data}
         return Response(data, status=200)
 
-    @detail_route(methods=['post'])
+    @detail_route(methods=["post"])
     @transaction.atomic
     def add(self, request, *args, **kwargs):
         playlist = self.get_object()
         serializer = serializers.PlaylistAddManySerializer(data=request.data)
         serializer.is_valid(raise_exception=True)
         try:
-            plts = playlist.insert_many(serializer.validated_data['tracks'])
+            plts = playlist.insert_many(serializer.validated_data["tracks"])
         except exceptions.ValidationError as e:
-            payload = {'playlist': e.detail}
+            payload = {"playlist": e.detail}
             return Response(payload, status=400)
         ids = [p.id for p in plts]
-        plts = models.PlaylistTrack.objects.filter(
-            pk__in=ids).order_by('index').for_nested_serialization()
+        plts = (
+            models.PlaylistTrack.objects.filter(pk__in=ids)
+            .order_by("index")
+            .for_nested_serialization()
+        )
         serializer = serializers.PlaylistTrackSerializer(plts, many=True)
-        data = {
-            'count': len(plts),
-            'results': serializer.data
-        }
+        data = {"count": len(plts), "results": serializer.data}
         return Response(data, status=201)
 
-    @detail_route(methods=['delete'])
+    @detail_route(methods=["delete"])
     @transaction.atomic
     def clear(self, request, *args, **kwargs):
         playlist = self.get_object()
         playlist.playlist_tracks.all().delete()
-        playlist.save(update_fields=['modification_date'])
+        playlist.save(update_fields=["modification_date"])
         return Response(status=204)
 
     def get_queryset(self):
-        return self.queryset.filter(
-            fields.privacy_level_query(self.request.user))
+        return self.queryset.filter(fields.privacy_level_query(self.request.user))
 
     def perform_create(self, serializer):
         return serializer.save(
             user=self.request.user,
             privacy_level=serializer.validated_data.get(
-                'privacy_level', self.request.user.privacy_level)
+                "privacy_level", self.request.user.privacy_level
+            ),
         )
 
 
 class PlaylistTrackViewSet(
-        mixins.RetrieveModelMixin,
-        mixins.CreateModelMixin,
-        mixins.UpdateModelMixin,
-        mixins.DestroyModelMixin,
-        mixins.ListModelMixin,
-        viewsets.GenericViewSet):
+    mixins.RetrieveModelMixin,
+    mixins.CreateModelMixin,
+    mixins.UpdateModelMixin,
+    mixins.DestroyModelMixin,
+    mixins.ListModelMixin,
+    viewsets.GenericViewSet,
+):
 
     serializer_class = serializers.PlaylistTrackSerializer
-    queryset = (models.PlaylistTrack.objects.all().for_nested_serialization())
+    queryset = models.PlaylistTrack.objects.all().for_nested_serialization()
     permission_classes = [
         permissions.ConditionalAuthentication,
         permissions.OwnerPermission,
         IsAuthenticatedOrReadOnly,
     ]
-    owner_field = 'playlist.user'
-    owner_checks = ['write']
+    owner_field = "playlist.user"
+    owner_checks = ["write"]
 
     def get_serializer_class(self):
-        if self.request.method in ['PUT', 'PATCH', 'DELETE', 'POST']:
+        if self.request.method in ["PUT", "PATCH", "DELETE", "POST"]:
             return serializers.PlaylistTrackWriteSerializer
         return self.serializer_class
 
     def get_queryset(self):
         return self.queryset.filter(
             fields.privacy_level_query(
-                self.request.user,
-                lookup_field='playlist__privacy_level'))
+                self.request.user, lookup_field="playlist__privacy_level"
+            )
+        )
 
     def perform_destroy(self, instance):
         instance.delete(update_indexes=True)
diff --git a/api/funkwhale_api/providers/acoustid/__init__.py b/api/funkwhale_api/providers/acoustid/__init__.py
index 69fe058b3fa6495de9197efe1d39349c1efb801b..558a95bb80fedc6b9c9f192f86ae9a3136d7d8c1 100644
--- a/api/funkwhale_api/providers/acoustid/__init__.py
+++ b/api/funkwhale_api/providers/acoustid/__init__.py
@@ -14,14 +14,14 @@ class Client(object):
         results = self.match(file_path=file_path)
         MIN_SCORE_FOR_MATCH = 0.8
         try:
-            rows = results['results']
+            rows = results["results"]
         except KeyError:
             return
         for row in rows:
-            if row['score'] >= MIN_SCORE_FOR_MATCH:
+            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'])
+    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
index 33c9643b00b4ec96665a7e8cbcfae13098d13a04..2411de86add2154645f670491fd0377996ef216c 100644
--- a/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py
@@ -1,19 +1,16 @@
 from django import forms
-
-from dynamic_preferences.types import StringPreference, Section
 from dynamic_preferences.registries import global_preferences_registry
+from dynamic_preferences.types import Section, StringPreference
 
-acoustid = Section('providers_acoustid')
+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.'
+    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,
-    }
+    field_kwargs = {"required": False}
diff --git a/api/funkwhale_api/providers/audiofile/management/commands/import_files.py b/api/funkwhale_api/providers/audiofile/management/commands/import_files.py
index 70ff90ffac349858a0f239eecf6b42373d2d9a3c..de2560d3c8d064a0bddd93208de82e0b7d3c99ca 100644
--- a/api/funkwhale_api/providers/audiofile/management/commands/import_files.py
+++ b/api/funkwhale_api/providers/audiofile/management/commands/import_files.py
@@ -5,192 +5,199 @@ from django.conf import settings
 from django.core.files import File
 from django.core.management.base import BaseCommand, CommandError
 
-from funkwhale_api.music import models
-from funkwhale_api.music import tasks
+from funkwhale_api.music import models, tasks
 from funkwhale_api.users.models import User
 
 
 class Command(BaseCommand):
-    help = 'Import audio files mathinc given glob pattern'
+    help = "Import audio files mathinc given glob pattern"
 
     def add_arguments(self, parser):
-        parser.add_argument('path', type=str)
+        parser.add_argument("path", type=str)
         parser.add_argument(
-            '--recursive',
-            action='store_true',
-            dest='recursive',
+            "--recursive",
+            action="store_true",
+            dest="recursive",
             default=False,
-            help='Will match the pattern recursively (including subdirectories)',
+            help="Will match the pattern recursively (including subdirectories)",
         )
         parser.add_argument(
-            '--username',
-            dest='username',
-            help='The username of the user you want to be bound to the import',
+            "--username",
+            dest="username",
+            help="The username of the user you want to be bound to the import",
         )
         parser.add_argument(
-            '--async',
-            action='store_true',
-            dest='async',
+            "--async",
+            action="store_true",
+            dest="async",
             default=False,
-            help='Will launch celery tasks for each file to import instead of doing it synchronously and block the CLI',
+            help="Will launch celery tasks for each file to import instead of doing it synchronously and block the CLI",
         )
         parser.add_argument(
-            '--exit', '-x',
-            action='store_true',
-            dest='exit_on_failure',
+            "--exit",
+            "-x",
+            action="store_true",
+            dest="exit_on_failure",
             default=False,
-            help='Use this flag to disable error catching',
+            help="Use this flag to disable error catching",
         )
         parser.add_argument(
-            '--in-place', '-i',
-            action='store_true',
-            dest='in_place',
+            "--in-place",
+            "-i",
+            action="store_true",
+            dest="in_place",
             default=False,
             help=(
-                'Import files without duplicating them into the media directory.'
-                'For in-place import to work, the music files must be readable'
-                'by the web-server and funkwhale api and celeryworker processes.'
-                'You may want to use this if you have a big music library to '
-                'import and not much disk space available.'
-            )
+                "Import files without duplicating them into the media directory."
+                "For in-place import to work, the music files must be readable"
+                "by the web-server and funkwhale api and celeryworker processes."
+                "You may want to use this if you have a big music library to "
+                "import and not much disk space available."
+            ),
         )
         parser.add_argument(
-            '--noinput', '--no-input', action='store_false', dest='interactive',
+            "--noinput",
+            "--no-input",
+            action="store_false",
+            dest="interactive",
             help="Do NOT prompt the user for input of any kind.",
         )
 
     def handle(self, *args, **options):
         glob_kwargs = {}
-        if options['recursive']:
-            glob_kwargs['recursive'] = True
+        if options["recursive"]:
+            glob_kwargs["recursive"] = True
         try:
-            matching = sorted(glob.glob(options['path'], **glob_kwargs))
+            matching = sorted(glob.glob(options["path"], **glob_kwargs))
         except TypeError:
-            raise Exception('You need Python 3.5 to use the --recursive flag')
+            raise Exception("You need Python 3.5 to use the --recursive flag")
 
-        if options['in_place']:
+        if options["in_place"]:
             self.stdout.write(
-                'Checking imported paths against settings.MUSIC_DIRECTORY_PATH')
+                "Checking imported paths against settings.MUSIC_DIRECTORY_PATH"
+            )
             p = settings.MUSIC_DIRECTORY_PATH
             if not p:
                 raise CommandError(
-                    'Importing in-place requires setting the '
-                    'MUSIC_DIRECTORY_PATH variable')
+                    "Importing in-place requires setting the "
+                    "MUSIC_DIRECTORY_PATH variable"
+                )
             for m in matching:
                 if not m.startswith(p):
                     raise CommandError(
-                        'Importing in-place only works if importing'
-                        'from {} (MUSIC_DIRECTORY_PATH), as this directory'
-                        'needs to be accessible by the webserver.'
-                        'Culprit: {}'.format(p, m))
+                        "Importing in-place only works if importing"
+                        "from {} (MUSIC_DIRECTORY_PATH), as this directory"
+                        "needs to be accessible by the webserver."
+                        "Culprit: {}".format(p, m)
+                    )
         if not matching:
-            raise CommandError('No file matching pattern, aborting')
+            raise CommandError("No file matching pattern, aborting")
 
         user = None
-        if options['username']:
+        if options["username"]:
             try:
-                user = User.objects.get(username=options['username'])
+                user = User.objects.get(username=options["username"])
             except User.DoesNotExist:
-                raise CommandError('Invalid username')
+                raise CommandError("Invalid username")
         else:
             # we bind the import to the first registered superuser
             try:
-                user = User.objects.filter(is_superuser=True).order_by('pk').first()
+                user = User.objects.filter(is_superuser=True).order_by("pk").first()
                 assert user is not None
             except AssertionError:
                 raise CommandError(
-                    'No superuser available, please provide a --username')
+                    "No superuser available, please provide a --username"
+                )
 
         filtered = self.filter_matching(matching, options)
-        self.stdout.write('Import summary:')
-        self.stdout.write('- {} files found matching this pattern: {}'.format(
-            len(matching), options['path']))
-        self.stdout.write('- {} files already found in database'.format(
-            len(filtered['skipped'])))
-        self.stdout.write('- {} new files'.format(
-            len(filtered['new'])))
-
-        self.stdout.write('Selected options: {}'.format(', '.join([
-            'in place' if options['in_place'] else 'copy music files',
-        ])))
-        if len(filtered['new']) == 0:
-            self.stdout.write('Nothing new to import, exiting')
+        self.stdout.write("Import summary:")
+        self.stdout.write(
+            "- {} files found matching this pattern: {}".format(
+                len(matching), options["path"]
+            )
+        )
+        self.stdout.write(
+            "- {} files already found in database".format(len(filtered["skipped"]))
+        )
+        self.stdout.write("- {} new files".format(len(filtered["new"])))
+
+        self.stdout.write(
+            "Selected options: {}".format(
+                ", ".join(["in place" if options["in_place"] else "copy music files"])
+            )
+        )
+        if len(filtered["new"]) == 0:
+            self.stdout.write("Nothing new to import, exiting")
             return
 
-        if options['interactive']:
+        if options["interactive"]:
             message = (
-                'Are you sure you want to do this?\n\n'
+                "Are you sure you want to do this?\n\n"
                 "Type 'yes' to continue, or 'no' to cancel: "
             )
-            if input(''.join(message)) != 'yes':
+            if input("".join(message)) != "yes":
                 raise CommandError("Import cancelled.")
 
-        batch, errors = self.do_import(
-            filtered['new'], user=user, options=options)
-        message = 'Successfully imported {} tracks'
-        if options['async']:
-            message = 'Successfully launched import for {} tracks'
+        batch, errors = self.do_import(filtered["new"], user=user, options=options)
+        message = "Successfully imported {} tracks"
+        if options["async"]:
+            message = "Successfully launched import for {} tracks"
 
-        self.stdout.write(message.format(len(filtered['new'])))
+        self.stdout.write(message.format(len(filtered["new"])))
         if len(errors) > 0:
-            self.stderr.write(
-                '{} tracks could not be imported:'.format(len(errors)))
+            self.stderr.write("{} tracks could not be imported:".format(len(errors)))
 
             for path, error in errors:
-                self.stderr.write('- {}: {}'.format(path, error))
+                self.stderr.write("- {}: {}".format(path, error))
         self.stdout.write(
-            "For details, please refer to import batch #{}".format(batch.pk))
+            "For details, please refer to import batch #{}".format(batch.pk)
+        )
 
     def filter_matching(self, matching, options):
-        sources = ['file://{}'.format(p) for p in matching]
+        sources = ["file://{}".format(p) for p in matching]
         # we skip reimport for path that are already found
         # as a TrackFile.source
         existing = models.TrackFile.objects.filter(source__in=sources)
-        existing = existing.values_list('source', flat=True)
-        existing = set([p.replace('file://', '', 1) for p in existing])
+        existing = existing.values_list("source", flat=True)
+        existing = set([p.replace("file://", "", 1) for p in existing])
         skipped = set(matching) & existing
         result = {
-            'initial': matching,
-            'skipped': list(sorted(skipped)),
-            'new': list(sorted(set(matching) - skipped)),
+            "initial": matching,
+            "skipped": list(sorted(skipped)),
+            "new": list(sorted(set(matching) - skipped)),
         }
         return result
 
     def do_import(self, paths, user, options):
-        message = '{i}/{total} Importing {path}...'
-        if options['async']:
-            message = '{i}/{total} Launching import for {path}...'
+        message = "{i}/{total} Importing {path}..."
+        if options["async"]:
+            message = "{i}/{total} Launching import for {path}..."
 
         # we create an import batch binded to the user
-        async = options['async']
+        async = options["async"]
         import_handler = tasks.import_job_run.delay if async else tasks.import_job_run
-        batch = user.imports.create(source='shell')
-        total = len(paths)
+        batch = user.imports.create(source="shell")
         errors = []
         for i, path in list(enumerate(paths)):
             try:
-                self.stdout.write(
-                    message.format(path=path, i=i+1, total=len(paths)))
+                self.stdout.write(message.format(path=path, i=i + 1, total=len(paths)))
                 self.import_file(path, batch, import_handler, options)
             except Exception as e:
-                if options['exit_on_failure']:
+                if options["exit_on_failure"]:
                     raise
-                m = 'Error while importing {}: {} {}'.format(
-                    path, e.__class__.__name__, e)
+                m = "Error while importing {}: {} {}".format(
+                    path, e.__class__.__name__, e
+                )
                 self.stderr.write(m)
-                errors.append((path, '{} {}'.format(e.__class__.__name__, e)))
+                errors.append((path, "{} {}".format(e.__class__.__name__, e)))
         return batch, errors
 
     def import_file(self, path, batch, import_handler, options):
-        job = batch.jobs.create(
-            source='file://' + path,
-        )
-        if not options['in_place']:
+        job = batch.jobs.create(source="file://" + path)
+        if not options["in_place"]:
             name = os.path.basename(path)
-            with open(path, 'rb') as f:
+            with open(path, "rb") as f:
                 job.audio_file.save(name, File(f))
 
             job.save()
-        import_handler(
-            import_job_id=job.pk,
-            use_acoustid=False)
+        import_handler(import_job_id=job.pk, use_acoustid=False)
diff --git a/api/funkwhale_api/providers/audiofile/tasks.py b/api/funkwhale_api/providers/audiofile/tasks.py
index fb630673555bbb0cff380a5b835d725506a287da..ee486345a7cc3535e64edec9a994df7e7b8f935c 100644
--- a/api/funkwhale_api/providers/audiofile/tasks.py
+++ b/api/funkwhale_api/providers/audiofile/tasks.py
@@ -1,95 +1,45 @@
-import acoustid
-import os
-import datetime
-from django.core.files import File
 from django.db import transaction
 
-from funkwhale_api.taskapp import celery
-from funkwhale_api.providers.acoustid import get_acoustid_client
-from funkwhale_api.music import models, metadata
+from funkwhale_api.music import metadata, models
 
 
 @transaction.atomic
 def import_track_data_from_path(path):
     data = metadata.Metadata(path)
     album = None
-    track_mbid = data.get('musicbrainz_recordingid', None)
-    album_mbid = data.get('musicbrainz_albumid', None)
+    track_mbid = data.get("musicbrainz_recordingid", None)
+    album_mbid = data.get("musicbrainz_albumid", None)
 
     if album_mbid and track_mbid:
         # to gain performance and avoid additional mb lookups,
         # we import from the release data, which is already cached
-        return models.Track.get_or_create_from_release(
-            album_mbid, track_mbid)[0]
+        return models.Track.get_or_create_from_release(album_mbid, track_mbid)[0]
     elif track_mbid:
         return models.Track.get_or_create_from_api(track_mbid)[0]
     elif album_mbid:
         album = models.Album.get_or_create_from_api(album_mbid)[0]
 
     artist = album.artist if album else None
-    artist_mbid = data.get('musicbrainz_artistid', None)
+    artist_mbid = data.get("musicbrainz_artistid", None)
     if not artist:
         if artist_mbid:
             artist = models.Artist.get_or_create_from_api(artist_mbid)[0]
         else:
             artist = models.Artist.objects.get_or_create(
-                name__iexact=data.get('artist'),
-                defaults={
-                    'name': data.get('artist'),
-                },
+                name__iexact=data.get("artist"), defaults={"name": data.get("artist")}
             )[0]
 
-    release_date = data.get('date', default=None)
+    release_date = data.get("date", default=None)
     if not album:
         album = models.Album.objects.get_or_create(
-            title__iexact=data.get('album'),
+            title__iexact=data.get("album"),
             artist=artist,
-            defaults={
-                'title': data.get('album'),
-                'release_date': release_date,
-            },
+            defaults={"title": data.get("album"), "release_date": release_date},
         )[0]
-    position = data.get('track_number', default=None)
+    position = data.get("track_number", default=None)
     track = models.Track.objects.get_or_create(
-        title__iexact=data.get('title'),
+        title__iexact=data.get("title"),
         album=album,
-        defaults={
-            'title': data.get('title'),
-            'position': position,
-        },
+        defaults={"title": data.get("title"), "position": position},
     )[0]
     return track
-
-
-def import_metadata_with_musicbrainz(path):
-    pass
-
-
-@celery.app.task(name='audiofile.from_path')
-def from_path(path):
-    acoustid_track_id = None
-    try:
-        client = get_acoustid_client()
-        result = client.get_best_match(path)
-        acoustid_track_id = result['id']
-    except acoustid.WebServiceError:
-        track = import_track_data_from_path(path)
-    except (TypeError, KeyError):
-        track = import_metadata_without_musicbrainz(path)
-    else:
-        track, created = models.Track.get_or_create_from_api(
-            mbid=result['recordings'][0]['id']
-        )
-
-    if track.files.count() > 0:
-        raise ValueError('File already exists for track {}'.format(track.pk))
-
-    track_file = models.TrackFile(
-        track=track, acoustid_track_id=acoustid_track_id)
-    track_file.audio_file.save(
-        os.path.basename(path),
-        File(open(path, 'rb'))
-    )
-    track_file.save()
-
-    return track_file
diff --git a/api/funkwhale_api/providers/urls.py b/api/funkwhale_api/providers/urls.py
index 10975da53b6db28599d61d8d1e75d19ea136a0a5..55a1193f503c33784a1876fd6700ab0f28d79041 100644
--- a/api/funkwhale_api/providers/urls.py
+++ b/api/funkwhale_api/providers/urls.py
@@ -1,11 +1,16 @@
 from django.conf.urls import include, url
-from funkwhale_api.music import views
 
 urlpatterns = [
-    url(r'^youtube/', include(
-        ('funkwhale_api.providers.youtube.urls', 'youtube'),
-        namespace='youtube')),
-    url(r'^musicbrainz/', include(
-        ('funkwhale_api.musicbrainz.urls', 'musicbrainz'),
-        namespace='musicbrainz')),
+    url(
+        r"^youtube/",
+        include(
+            ("funkwhale_api.providers.youtube.urls", "youtube"), namespace="youtube"
+        ),
+    ),
+    url(
+        r"^musicbrainz/",
+        include(
+            ("funkwhale_api.musicbrainz.urls", "musicbrainz"), namespace="musicbrainz"
+        ),
+    ),
 ]
diff --git a/api/funkwhale_api/providers/youtube/client.py b/api/funkwhale_api/providers/youtube/client.py
index 792e501d7462456aec9ce2fb330096bf72282e4a..2235fcdc83a994ea0da1024962e9d6bd45704184 100644
--- a/api/funkwhale_api/providers/youtube/client.py
+++ b/api/funkwhale_api/providers/youtube/client.py
@@ -1,15 +1,11 @@
 import threading
 
 from apiclient.discovery import build
-from apiclient.errors import HttpError
-from oauth2client.tools import argparser
-
-from dynamic_preferences.registries import (
-    global_preferences_registry as registry)
+from dynamic_preferences.registries import global_preferences_registry as registry
 
 YOUTUBE_API_SERVICE_NAME = "youtube"
 YOUTUBE_API_VERSION = "v3"
-VIDEO_BASE_URL = 'https://www.youtube.com/watch?v={0}'
+VIDEO_BASE_URL = "https://www.youtube.com/watch?v={0}"
 
 
 def _do_search(query):
@@ -17,23 +13,21 @@ def _do_search(query):
     youtube = build(
         YOUTUBE_API_SERVICE_NAME,
         YOUTUBE_API_VERSION,
-        developerKey=manager['providers_youtube__api_key'])
+        developerKey=manager["providers_youtube__api_key"],
+    )
 
-    return youtube.search().list(
-        q=query,
-        part="id,snippet",
-        maxResults=25
-    ).execute()
+    return youtube.search().list(q=query, part="id,snippet", maxResults=25).execute()
 
 
 class Client(object):
-
     def search(self, query):
         search_response = _do_search(query)
         videos = []
         for search_result in search_response.get("items", []):
             if search_result["id"]["kind"] == "youtube#video":
-                search_result['full_url'] = VIDEO_BASE_URL.format(search_result["id"]['videoId'])
+                search_result["full_url"] = VIDEO_BASE_URL.format(
+                    search_result["id"]["videoId"]
+                )
                 videos.append(search_result)
         return videos
 
@@ -44,7 +38,7 @@ class Client(object):
             results[key] = self.search(query)
 
         threads = [
-            threading.Thread(target=search, args=(key, query,))
+            threading.Thread(target=search, args=(key, query))
             for key, query in queries.items()
         ]
         for thread in threads:
@@ -71,16 +65,16 @@ class Client(object):
         }
         """
         return {
-            'id': result['id']['videoId'],
-            'url': 'https://www.youtube.com/watch?v={}'.format(
-                result['id']['videoId']),
-            'type': result['id']['kind'],
-            'title': result['snippet']['title'],
-            'description': result['snippet']['description'],
-            'channelId': result['snippet']['channelId'],
-            'channelTitle': result['snippet']['channelTitle'],
-            'publishedAt': result['snippet']['publishedAt'],
-            'cover': result['snippet']['thumbnails']['high']['url'],
+            "id": result["id"]["videoId"],
+            "url": "https://www.youtube.com/watch?v={}".format(result["id"]["videoId"]),
+            "type": result["id"]["kind"],
+            "title": result["snippet"]["title"],
+            "description": result["snippet"]["description"],
+            "channelId": result["snippet"]["channelId"],
+            "channelTitle": result["snippet"]["channelTitle"],
+            "publishedAt": result["snippet"]["publishedAt"],
+            "cover": result["snippet"]["thumbnails"]["high"]["url"],
         }
 
+
 client = Client()
diff --git a/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py b/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py
index ac5fc4bde29a313429b7644b51846fa3e33b0465..2d950eb6b202f1f728b4f345733488e689261695 100644
--- a/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py
@@ -1,19 +1,16 @@
 from django import forms
-
-from dynamic_preferences.types import StringPreference, Section
 from dynamic_preferences.registries import global_preferences_registry
+from dynamic_preferences.types import Section, StringPreference
 
-youtube = Section('providers_youtube')
+youtube = Section("providers_youtube")
 
 
 @global_preferences_registry.register
 class APIKey(StringPreference):
     section = youtube
-    name = 'api_key'
-    default = 'CHANGEME'
-    verbose_name = 'YouTube API key'
-    help_text = 'The API key used to query YouTube. Get one at https://console.developers.google.com/.'
+    name = "api_key"
+    default = "CHANGEME"
+    verbose_name = "YouTube API key"
+    help_text = "The API key used to query YouTube. Get one at https://console.developers.google.com/."
     widget = forms.PasswordInput
-    field_kwargs = {
-        'required': False,
-    }
+    field_kwargs = {"required": False}
diff --git a/api/funkwhale_api/providers/youtube/urls.py b/api/funkwhale_api/providers/youtube/urls.py
index 243d2b85277cf60ef58e2249e4dbb2515962c1d4..d9687ac9f8f0e1af94eef00868ce7c64dbe3d406 100644
--- a/api/funkwhale_api/providers/youtube/urls.py
+++ b/api/funkwhale_api/providers/youtube/urls.py
@@ -1,8 +1,8 @@
-from django.conf.urls import include, url
-from .views import APISearch, APISearchs
+from django.conf.urls import url
 
+from .views import APISearch, APISearchs
 
 urlpatterns = [
-    url(r'^search/$', APISearch.as_view(), name='search'),
-    url(r'^searchs/$', APISearchs.as_view(), name='searchs'),
+    url(r"^search/$", APISearch.as_view(), name="search"),
+    url(r"^searchs/$", APISearchs.as_view(), name="searchs"),
 ]
diff --git a/api/funkwhale_api/providers/youtube/views.py b/api/funkwhale_api/providers/youtube/views.py
index 989b33090cad266921de3f4161b1422f63a4c876..5e1982f48e80ca5722c5ebbb712151cd12a02eb2 100644
--- a/api/funkwhale_api/providers/youtube/views.py
+++ b/api/funkwhale_api/providers/youtube/views.py
@@ -1,5 +1,6 @@
-from rest_framework.views import APIView
 from rest_framework.response import Response
+from rest_framework.views import APIView
+
 from funkwhale_api.common.permissions import ConditionalAuthentication
 
 from .client import client
@@ -9,11 +10,8 @@ class APISearch(APIView):
     permission_classes = [ConditionalAuthentication]
 
     def get(self, request, *args, **kwargs):
-        results = client.search(request.GET['query'])
-        return Response([
-            client.to_funkwhale(result)
-            for result in results
-        ])
+        results = client.search(request.GET["query"])
+        return Response([client.to_funkwhale(result) for result in results])
 
 
 class APISearchs(APIView):
@@ -21,10 +19,9 @@ class APISearchs(APIView):
 
     def post(self, request, *args, **kwargs):
         results = client.search_multiple(request.data)
-        return Response({
-            key: [
-                client.to_funkwhale(result)
-                for result in group
-            ]
-            for key, group in results.items()
-        })
+        return Response(
+            {
+                key: [client.to_funkwhale(result) for result in group]
+                for key, group in results.items()
+            }
+        )
diff --git a/api/funkwhale_api/radios/__init__.py b/api/funkwhale_api/radios/__init__.py
index 1258181b5c52430b720cfc558eb44399ce17b0f1..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/api/funkwhale_api/radios/__init__.py
+++ b/api/funkwhale_api/radios/__init__.py
@@ -1 +0,0 @@
-from .registries import registry
diff --git a/api/funkwhale_api/radios/admin.py b/api/funkwhale_api/radios/admin.py
index 6d5abadaff79fcbff2a32e6713800146b6edf05f..187950aeb2a011b7f332d493b97ba20622468efc 100644
--- a/api/funkwhale_api/radios/admin.py
+++ b/api/funkwhale_api/radios/admin.py
@@ -5,44 +5,28 @@ from . import models
 
 @admin.register(models.Radio)
 class RadioAdmin(admin.ModelAdmin):
-    list_display = [
-        'user', 'name', 'is_public', 'creation_date', 'config']
-    list_select_related = [
-        'user',
-    ]
-    list_filter = [
-        'is_public',
-    ]
-    search_fields = ['name', 'description']
+    list_display = ["user", "name", "is_public", "creation_date", "config"]
+    list_select_related = ["user"]
+    list_filter = ["is_public"]
+    search_fields = ["name", "description"]
 
 
 @admin.register(models.RadioSession)
 class RadioSessionAdmin(admin.ModelAdmin):
     list_display = [
-        'user',
-        'custom_radio',
-        'radio_type',
-        'creation_date',
-        'related_object']
-
-    list_select_related = [
-        'user',
-        'custom_radio'
-    ]
-    list_filter = [
-        'radio_type',
+        "user",
+        "custom_radio",
+        "radio_type",
+        "creation_date",
+        "related_object",
     ]
 
+    list_select_related = ["user", "custom_radio"]
+    list_filter = ["radio_type"]
+
 
 @admin.register(models.RadioSessionTrack)
 class RadioSessionTrackAdmin(admin.ModelAdmin):
-    list_display = [
-        'id',
-        'session',
-        'position',
-        'track',]
-
-    list_select_related = [
-        'track',
-        'session'
-    ]
+    list_display = ["id", "session", "position", "track"]
+
+    list_select_related = ["track", "session"]
diff --git a/api/funkwhale_api/radios/factories.py b/api/funkwhale_api/radios/factories.py
index 6a80323beaba10032d98aa5bf80f23b86fa619b4..a83c53737a9b174a37fa97ebdb46d13fdea805e5 100644
--- a/api/funkwhale_api/radios/factories.py
+++ b/api/funkwhale_api/radios/factories.py
@@ -6,13 +6,13 @@ from funkwhale_api.users.factories import UserFactory
 
 @registry.register
 class RadioFactory(factory.django.DjangoModelFactory):
-    name = factory.Faker('name')
-    description = factory.Faker('paragraphs')
+    name = factory.Faker("name")
+    description = factory.Faker("paragraphs")
     user = factory.SubFactory(UserFactory)
     config = []
 
     class Meta:
-        model = 'radios.Radio'
+        model = "radios.Radio"
 
 
 @registry.register
@@ -20,15 +20,16 @@ class RadioSessionFactory(factory.django.DjangoModelFactory):
     user = factory.SubFactory(UserFactory)
 
     class Meta:
-        model = 'radios.RadioSession'
+        model = "radios.RadioSession"
 
 
-@registry.register(name='radios.CustomRadioSession')
-class RadioSessionFactory(factory.django.DjangoModelFactory):
+@registry.register(name="radios.CustomRadioSession")
+class CustomRadioSessionFactory(factory.django.DjangoModelFactory):
     user = factory.SubFactory(UserFactory)
-    radio_type = 'custom'
+    radio_type = "custom"
     custom_radio = factory.SubFactory(
-        RadioFactory, user=factory.SelfAttribute('..user'))
+        RadioFactory, user=factory.SelfAttribute("..user")
+    )
 
     class Meta:
-        model = 'radios.RadioSession'
+        model = "radios.RadioSession"
diff --git a/api/funkwhale_api/radios/filters.py b/api/funkwhale_api/radios/filters.py
index d0d338d663a62008ce6f1ccf03e3e4f8ed679b37..810673bd664f6b7ddf56aee7403a2dfac5445b83 100644
--- a/api/funkwhale_api/radios/filters.py
+++ b/api/funkwhale_api/radios/filters.py
@@ -1,17 +1,14 @@
 import collections
 
+import persisting_theory
 from django.core.exceptions import ValidationError
 from django.db.models import Q
 from django.urls import reverse_lazy
 
-import persisting_theory
-
 from funkwhale_api.music import models
-from funkwhale_api.taskapp.celery import require_instance
 
 
 class RadioFilterRegistry(persisting_theory.Registry):
-
     def prepare_data(self, data):
         return data()
 
@@ -20,31 +17,27 @@ class RadioFilterRegistry(persisting_theory.Registry):
 
     @property
     def exposed_filters(self):
-        return [
-            f for f in self.values() if f.expose_in_api
-        ]
+        return [f for f in self.values() if f.expose_in_api]
 
 
 registry = RadioFilterRegistry()
 
 
 def run(filters, **kwargs):
-    candidates = kwargs.pop('candidates', models.Track.objects.all())
+    candidates = kwargs.pop("candidates", models.Track.objects.all())
     final_query = None
-    final_query = registry['group'].get_query(
-        candidates, filters=filters, **kwargs)
+    final_query = registry["group"].get_query(candidates, filters=filters, **kwargs)
 
     if final_query:
         candidates = candidates.filter(final_query)
-    return candidates.order_by('pk')
+    return candidates.order_by("pk")
 
 
 def validate(filter_config):
     try:
-        f = registry[filter_config['type']]
+        f = registry[filter_config["type"]]
     except KeyError:
-        raise ValidationError(
-            'Invalid type "{}"'.format(filter_config['type']))
+        raise ValidationError('Invalid type "{}"'.format(filter_config["type"]))
     f.validate(filter_config)
     return True
 
@@ -53,28 +46,22 @@ def test(filter_config, **kwargs):
     """
     Run validation and also gather the candidates for the given config
     """
-    data = {
-        'errors': [],
-        'candidates': {
-            'count': None,
-            'sample': None,
-        }
-    }
+    data = {"errors": [], "candidates": {"count": None, "sample": None}}
     try:
         validate(filter_config)
     except ValidationError as e:
-        data['errors'] = [e.message]
+        data["errors"] = [e.message]
         return data
 
     candidates = run([filter_config], **kwargs)
-    data['candidates']['count'] = candidates.count()
-    data['candidates']['sample'] = candidates[:10]
+    data["candidates"]["count"] = candidates.count()
+    data["candidates"]["sample"] = candidates[:10]
 
     return data
 
 
 def clean_config(filter_config):
-    f = registry[filter_config['type']]
+    f = registry[filter_config["type"]]
     return f.clean_config(filter_config)
 
 
@@ -91,74 +78,75 @@ class RadioFilter(object):
         return filter_config
 
     def validate(self, config):
-        operator = config.get('operator', 'and')
+        operator = config.get("operator", "and")
         try:
-            assert operator in ['or', 'and']
+            assert operator in ["or", "and"]
         except AssertionError:
-            raise ValidationError(
-                'Invalid operator "{}"'.format(config['operator']))
+            raise ValidationError('Invalid operator "{}"'.format(config["operator"]))
 
 
 @registry.register
 class GroupFilter(RadioFilter):
-    code = 'group'
+    code = "group"
     expose_in_api = False
+
     def get_query(self, candidates, filters, **kwargs):
         if not filters:
             return
 
         final_query = None
         for filter_config in filters:
-            f = registry[filter_config['type']]
+            f = registry[filter_config["type"]]
             conf = collections.ChainMap(filter_config, kwargs)
             query = f.get_query(candidates, **conf)
-            if filter_config.get('not', False):
+            if filter_config.get("not", False):
                 query = ~query
 
             if not final_query:
                 final_query = query
             else:
-                operator = filter_config.get('operator', 'and')
-                if operator == 'and':
+                operator = filter_config.get("operator", "and")
+                if operator == "and":
                     final_query &= query
-                elif operator == 'or':
+                elif operator == "or":
                     final_query |= query
                 else:
-                    raise ValueError(
-                        'Invalid query operator "{}"'.format(operator))
+                    raise ValueError('Invalid query operator "{}"'.format(operator))
         return final_query
 
     def validate(self, config):
         super().validate(config)
-        for fc in config['filters']:
-            registry[fc['type']].validate(fc)
+        for fc in config["filters"]:
+            registry[fc["type"]].validate(fc)
 
 
 @registry.register
 class ArtistFilter(RadioFilter):
-    code = 'artist'
-    label = 'Artist'
-    help_text = 'Select tracks for a given artist'
+    code = "artist"
+    label = "Artist"
+    help_text = "Select tracks for a given artist"
     fields = [
         {
-            'name': 'ids',
-            'type': 'list',
-            'subtype': 'number',
-            'autocomplete': reverse_lazy('api:v1:artists-list'),
-            'autocomplete_qs': 'q={query}',
-            'autocomplete_fields': {'name': 'name', 'value': 'id'},
-            'label': 'Artist',
-            'placeholder': 'Select artists'
+            "name": "ids",
+            "type": "list",
+            "subtype": "number",
+            "autocomplete": reverse_lazy("api:v1:artists-list"),
+            "autocomplete_qs": "q={query}",
+            "autocomplete_fields": {"name": "name", "value": "id"},
+            "label": "Artist",
+            "placeholder": "Select artists",
         }
     ]
 
     def clean_config(self, filter_config):
         filter_config = super().clean_config(filter_config)
-        filter_config['ids'] = sorted(filter_config['ids'])
-        names = models.Artist.objects.filter(
-            pk__in=filter_config['ids']
-        ).order_by('id').values_list('name', flat=True)
-        filter_config['names'] = list(names)
+        filter_config["ids"] = sorted(filter_config["ids"])
+        names = (
+            models.Artist.objects.filter(pk__in=filter_config["ids"])
+            .order_by("id")
+            .values_list("name", flat=True)
+        )
+        filter_config["names"] = list(names)
         return filter_config
 
     def get_query(self, candidates, ids, **kwargs):
@@ -167,35 +155,38 @@ class ArtistFilter(RadioFilter):
     def validate(self, config):
         super().validate(config)
         try:
-            pks = models.Artist.objects.filter(
-                pk__in=config['ids']).values_list('pk', flat=True)
-            diff = set(config['ids']) - set(pks)
+            pks = models.Artist.objects.filter(pk__in=config["ids"]).values_list(
+                "pk", flat=True
+            )
+            diff = set(config["ids"]) - set(pks)
             assert len(diff) == 0
         except KeyError:
-            raise ValidationError('You must provide an id')
+            raise ValidationError("You must provide an id")
         except AssertionError:
-            raise ValidationError(
-                'No artist matching ids "{}"'.format(diff))
+            raise ValidationError('No artist matching ids "{}"'.format(diff))
 
 
 @registry.register
 class TagFilter(RadioFilter):
-    code = 'tag'
+    code = "tag"
     fields = [
         {
-            'name': 'names',
-            'type': 'list',
-            'subtype': 'string',
-            'autocomplete': reverse_lazy('api:v1:tags-list'),
-            'autocomplete_qs': '',
-            'autocomplete_fields': {'remoteValues': 'results', 'name': 'name', 'value': 'slug'},
-            'autocomplete_qs': 'query={query}',
-            'label': 'Tags',
-            'placeholder': 'Select tags'
+            "name": "names",
+            "type": "list",
+            "subtype": "string",
+            "autocomplete": reverse_lazy("api:v1:tags-list"),
+            "autocomplete_fields": {
+                "remoteValues": "results",
+                "name": "name",
+                "value": "slug",
+            },
+            "autocomplete_qs": "query={query}",
+            "label": "Tags",
+            "placeholder": "Select tags",
         }
     ]
-    help_text = 'Select tracks with a given tag'
-    label = 'Tag'
+    help_text = "Select tracks with a given tag"
+    label = "Tag"
 
     def get_query(self, candidates, names, **kwargs):
         return Q(tags__slug__in=names)
diff --git a/api/funkwhale_api/radios/filtersets.py b/api/funkwhale_api/radios/filtersets.py
index 49f471373e55cccdd58d1d574cdd0961e3bb10f7..d8d7c9ed097f6c55fa67204e979fe7a3f15da8cf 100644
--- a/api/funkwhale_api/radios/filtersets.py
+++ b/api/funkwhale_api/radios/filtersets.py
@@ -4,9 +4,6 @@ from . import models
 
 
 class RadioFilter(django_filters.FilterSet):
-
     class Meta:
         model = models.Radio
-        fields = {
-            'name': ['exact', 'iexact', 'startswith', 'icontains']
-        }
+        fields = {"name": ["exact", "iexact", "startswith", "icontains"]}
diff --git a/api/funkwhale_api/radios/migrations/0001_initial.py b/api/funkwhale_api/radios/migrations/0001_initial.py
index 46faf749edaf0d0baa5e98ce320ad4636b8426a4..912da7be3b58d28374411e49e1b76ee9e60011ec 100644
--- a/api/funkwhale_api/radios/migrations/0001_initial.py
+++ b/api/funkwhale_api/radios/migrations/0001_initial.py
@@ -10,33 +10,72 @@ class Migration(migrations.Migration):
 
     dependencies = [
         migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        ('music', '0004_track_tags'),
+        ("music", "0004_track_tags"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='RadioSession',
+            name="RadioSession",
             fields=[
-                ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
-                ('radio_type', models.CharField(max_length=50)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('user', models.ForeignKey(related_name='radio_sessions', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)),
+                (
+                    "id",
+                    models.AutoField(
+                        verbose_name="ID",
+                        primary_key=True,
+                        serialize=False,
+                        auto_created=True,
+                    ),
+                ),
+                ("radio_type", models.CharField(max_length=50)),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        related_name="radio_sessions",
+                        blank=True,
+                        to=settings.AUTH_USER_MODEL,
+                        null=True,
+                        on_delete=models.CASCADE,
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
-            name='RadioSessionTrack',
+            name="RadioSessionTrack",
             fields=[
-                ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
-                ('position', models.IntegerField(default=1)),
-                ('session', models.ForeignKey(to='radios.RadioSession', related_name='session_tracks', on_delete=models.CASCADE)),
-                ('track', models.ForeignKey(to='music.Track', related_name='radio_session_tracks', on_delete=models.CASCADE)),
+                (
+                    "id",
+                    models.AutoField(
+                        verbose_name="ID",
+                        primary_key=True,
+                        serialize=False,
+                        auto_created=True,
+                    ),
+                ),
+                ("position", models.IntegerField(default=1)),
+                (
+                    "session",
+                    models.ForeignKey(
+                        to="radios.RadioSession",
+                        related_name="session_tracks",
+                        on_delete=models.CASCADE,
+                    ),
+                ),
+                (
+                    "track",
+                    models.ForeignKey(
+                        to="music.Track",
+                        related_name="radio_session_tracks",
+                        on_delete=models.CASCADE,
+                    ),
+                ),
             ],
-            options={
-                'ordering': ('session', 'position'),
-            },
+            options={"ordering": ("session", "position")},
         ),
         migrations.AlterUniqueTogether(
-            name='radiosessiontrack',
-            unique_together=set([('session', 'position')]),
+            name="radiosessiontrack", unique_together=set([("session", "position")])
         ),
     ]
diff --git a/api/funkwhale_api/radios/migrations/0002_radiosession_session_key.py b/api/funkwhale_api/radios/migrations/0002_radiosession_session_key.py
index a903ae3ea885a49329378af93ffae6e998d6c858..6c206aa625c42bc46c4d914137164edc53eb6b76 100644
--- a/api/funkwhale_api/radios/migrations/0002_radiosession_session_key.py
+++ b/api/funkwhale_api/radios/migrations/0002_radiosession_session_key.py
@@ -6,14 +6,12 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('radios', '0001_initial'),
-    ]
+    dependencies = [("radios", "0001_initial")]
 
     operations = [
         migrations.AddField(
-            model_name='radiosession',
-            name='session_key',
+            model_name="radiosession",
+            name="session_key",
             field=models.CharField(null=True, blank=True, max_length=100),
-        ),
+        )
     ]
diff --git a/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py b/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py
index 7c70abc2e12ed6309de4bf274ff94a26b2ba20e6..2af084a871c349e55b3d9328ace0762239b61804 100644
--- a/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py
+++ b/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py
@@ -7,19 +7,24 @@ from django.db import migrations, models
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('contenttypes', '0002_remove_content_type_name'),
-        ('radios', '0002_radiosession_session_key'),
+        ("contenttypes", "0002_remove_content_type_name"),
+        ("radios", "0002_radiosession_session_key"),
     ]
 
     operations = [
         migrations.AddField(
-            model_name='radiosession',
-            name='related_object_content_type',
-            field=models.ForeignKey(null=True, to='contenttypes.ContentType', blank=True, on_delete=models.CASCADE),
+            model_name="radiosession",
+            name="related_object_content_type",
+            field=models.ForeignKey(
+                null=True,
+                to="contenttypes.ContentType",
+                blank=True,
+                on_delete=models.CASCADE,
+            ),
         ),
         migrations.AddField(
-            model_name='radiosession',
-            name='related_object_id',
+            model_name="radiosession",
+            name="related_object_id",
             field=models.PositiveIntegerField(blank=True, null=True),
         ),
     ]
diff --git a/api/funkwhale_api/radios/migrations/0004_auto_20180107_1813.py b/api/funkwhale_api/radios/migrations/0004_auto_20180107_1813.py
index fc768b303365e3146054b7f05749d5dd0fc9d259..72f2a7d3159beac21aaca2c55173967b3edb7680 100644
--- a/api/funkwhale_api/radios/migrations/0004_auto_20180107_1813.py
+++ b/api/funkwhale_api/radios/migrations/0004_auto_20180107_1813.py
@@ -11,26 +11,52 @@ class Migration(migrations.Migration):
 
     dependencies = [
         migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-        ('radios', '0003_auto_20160521_1708'),
+        ("radios", "0003_auto_20160521_1708"),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='Radio',
+            name="Radio",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=100)),
-                ('description', models.TextField(blank=True)),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('is_public', models.BooleanField(default=False)),
-                ('version', models.PositiveIntegerField(default=0)),
-                ('config', django.contrib.postgres.fields.jsonb.JSONField()),
-                ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='radios', to=settings.AUTH_USER_MODEL)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=100)),
+                ("description", models.TextField(blank=True)),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("is_public", models.BooleanField(default=False)),
+                ("version", models.PositiveIntegerField(default=0)),
+                ("config", django.contrib.postgres.fields.jsonb.JSONField()),
+                (
+                    "user",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="radios",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
             ],
         ),
         migrations.AddField(
-            model_name='radiosession',
-            name='custom_radio',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sessions', to='radios.Radio'),
+            model_name="radiosession",
+            name="custom_radio",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="sessions",
+                to="radios.Radio",
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/radios/models.py b/api/funkwhale_api/radios/models.py
index 8758abc619d05c03b3fe9c9b0953c919ef005479..d0c3d1716a4efb3d2698054646abf0db04535b10 100644
--- a/api/funkwhale_api/radios/models.py
+++ b/api/funkwhale_api/radios/models.py
@@ -1,10 +1,9 @@
-from django.db import models
-from django.utils import timezone
-from django.core.exceptions import ValidationError
-from django.contrib.postgres.fields import JSONField
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.postgres.fields import JSONField
 from django.core.serializers.json import DjangoJSONEncoder
+from django.db import models
+from django.utils import timezone
 
 from funkwhale_api.music.models import Track
 
@@ -14,11 +13,12 @@ from . import filters
 class Radio(models.Model):
     CONFIG_VERSION = 0
     user = models.ForeignKey(
-        'users.User',
-        related_name='radios',
+        "users.User",
+        related_name="radios",
         null=True,
         blank=True,
-        on_delete=models.CASCADE)
+        on_delete=models.CASCADE,
+    )
     name = models.CharField(max_length=100)
     description = models.TextField(blank=True)
     creation_date = models.DateTimeField(default=timezone.now)
@@ -32,27 +32,25 @@ class Radio(models.Model):
 
 class RadioSession(models.Model):
     user = models.ForeignKey(
-        'users.User',
-        related_name='radio_sessions',
+        "users.User",
+        related_name="radio_sessions",
         null=True,
         blank=True,
-        on_delete=models.CASCADE)
+        on_delete=models.CASCADE,
+    )
     session_key = models.CharField(max_length=100, null=True, blank=True)
     radio_type = models.CharField(max_length=50)
     custom_radio = models.ForeignKey(
-        Radio,
-        related_name='sessions',
-        null=True,
-        blank=True,
-        on_delete=models.CASCADE)
+        Radio, related_name="sessions", null=True, blank=True, on_delete=models.CASCADE
+    )
     creation_date = models.DateTimeField(default=timezone.now)
     related_object_content_type = models.ForeignKey(
-        ContentType,
-        blank=True,
-        null=True,
-        on_delete=models.CASCADE)
+        ContentType, blank=True, null=True, on_delete=models.CASCADE
+    )
     related_object_id = models.PositiveIntegerField(blank=True, null=True)
-    related_object = GenericForeignKey('related_object_content_type', 'related_object_id')
+    related_object = GenericForeignKey(
+        "related_object_content_type", "related_object_id"
+    )
 
     def save(self, **kwargs):
         self.radio.clean(self)
@@ -62,31 +60,35 @@ class RadioSession(models.Model):
     def next_position(self):
         next_position = 1
 
-        last_session_track = self.session_tracks.all().order_by('-position').first()
+        last_session_track = self.session_tracks.all().order_by("-position").first()
         if last_session_track:
             next_position = last_session_track.position + 1
 
         return next_position
 
     def add(self, track):
-        new_session_track = RadioSessionTrack.objects.create(track=track, session=self, position=self.next_position)
+        new_session_track = RadioSessionTrack.objects.create(
+            track=track, session=self, position=self.next_position
+        )
 
         return new_session_track
 
     @property
     def radio(self):
         from .registries import registry
-        from . import radios
+
         return registry[self.radio_type](session=self)
 
 
 class RadioSessionTrack(models.Model):
     session = models.ForeignKey(
-        RadioSession, related_name='session_tracks', on_delete=models.CASCADE)
+        RadioSession, related_name="session_tracks", on_delete=models.CASCADE
+    )
     position = models.IntegerField(default=1)
     track = models.ForeignKey(
-        Track, related_name='radio_session_tracks', on_delete=models.CASCADE)
+        Track, related_name="radio_session_tracks", on_delete=models.CASCADE
+    )
 
     class Meta:
-        ordering = ('session', 'position')
-        unique_together = ('session', 'position')
+        ordering = ("session", "position")
+        unique_together = ("session", "position")
diff --git a/api/funkwhale_api/radios/radios.py b/api/funkwhale_api/radios/radios.py
index 0d045ea4dc89ba2d545364e2a3123f3f625a001e..c7c361de9df143074867882caf7068a498b9a0b7 100644
--- a/api/funkwhale_api/radios/radios.py
+++ b/api/funkwhale_api/radios/radios.py
@@ -1,18 +1,18 @@
 import random
-from rest_framework import serializers
-from django.db.models import Count
+
 from django.core.exceptions import ValidationError
+from django.db.models import Count
+from rest_framework import serializers
 from taggit.models import Tag
+
+from funkwhale_api.music.models import Artist, Track
 from funkwhale_api.users.models import User
-from funkwhale_api.music.models import Track, Artist
 
-from . import filters
-from . import models
+from . import filters, models
 from .registries import registry
 
 
 class SimpleRadio(object):
-
     def clean(self, instance):
         return
 
@@ -37,13 +37,13 @@ class SessionRadio(SimpleRadio):
         self.session = session
 
     def start_session(self, user, **kwargs):
-        self.session = models.RadioSession.objects.create(user=user, radio_type=self.radio_type, **kwargs)
+        self.session = models.RadioSession.objects.create(
+            user=user, radio_type=self.radio_type, **kwargs
+        )
         return self.session
 
     def get_queryset(self, **kwargs):
-        qs = Track.objects.annotate(
-            files_count=Count('files')
-        )
+        qs = Track.objects.annotate(files_count=Count("files"))
         return qs.filter(files_count__gt=0)
 
     def get_queryset_kwargs(self):
@@ -57,7 +57,9 @@ class SessionRadio(SimpleRadio):
         return queryset
 
     def filter_from_session(self, queryset):
-        already_played = self.session.session_tracks.all().values_list('track', flat=True)
+        already_played = self.session.session_tracks.all().values_list(
+            "track", flat=True
+        )
         queryset = queryset.exclude(pk__in=already_played)
         return queryset
 
@@ -76,60 +78,51 @@ class SessionRadio(SimpleRadio):
         return data
 
 
-@registry.register(name='random')
+@registry.register(name="random")
 class RandomRadio(SessionRadio):
     def get_queryset(self, **kwargs):
         qs = super().get_queryset(**kwargs)
-        return qs.order_by('?')
+        return qs.order_by("?")
 
 
-@registry.register(name='favorites')
+@registry.register(name="favorites")
 class FavoritesRadio(SessionRadio):
-
     def get_queryset_kwargs(self):
         kwargs = super().get_queryset_kwargs()
         if self.session:
-            kwargs['user'] = self.session.user
+            kwargs["user"] = self.session.user
         return kwargs
 
     def get_queryset(self, **kwargs):
         qs = super().get_queryset(**kwargs)
-        track_ids = kwargs['user'].track_favorites.all().values_list('track', flat=True)
+        track_ids = kwargs["user"].track_favorites.all().values_list("track", flat=True)
         return qs.filter(pk__in=track_ids)
 
 
-@registry.register(name='custom')
+@registry.register(name="custom")
 class CustomRadio(SessionRadio):
-
     def get_queryset_kwargs(self):
         kwargs = super().get_queryset_kwargs()
-        kwargs['user'] = self.session.user
-        kwargs['custom_radio'] = self.session.custom_radio
+        kwargs["user"] = self.session.user
+        kwargs["custom_radio"] = self.session.custom_radio
         return kwargs
 
     def get_queryset(self, **kwargs):
         qs = super().get_queryset(**kwargs)
-        return filters.run(
-            kwargs['custom_radio'].config,
-            candidates=qs,
-        )
+        return filters.run(kwargs["custom_radio"].config, candidates=qs)
 
     def validate_session(self, data, **context):
         data = super().validate_session(data, **context)
         try:
-            user = data['user']
+            user = data["user"]
         except KeyError:
-            user = context['user']
+            user = context["user"]
         try:
-            assert (
-                data['custom_radio'].user == user or
-                data['custom_radio'].is_public)
+            assert data["custom_radio"].user == user or data["custom_radio"].is_public
         except KeyError:
-            raise serializers.ValidationError(
-                'You must provide a custom radio')
+            raise serializers.ValidationError("You must provide a custom radio")
         except AssertionError:
-            raise serializers.ValidationError(
-                "You don't have access to this radio")
+            raise serializers.ValidationError("You don't have access to this radio")
         return data
 
 
@@ -139,23 +132,26 @@ class RelatedObjectRadio(SessionRadio):
     def clean(self, instance):
         super().clean(instance)
         if not instance.related_object:
-            raise ValidationError('Cannot start RelatedObjectRadio without related object')
+            raise ValidationError(
+                "Cannot start RelatedObjectRadio without related object"
+            )
         if not isinstance(instance.related_object, self.model):
-            raise ValidationError('Trying to start radio with bad related object')
+            raise ValidationError("Trying to start radio with bad related object")
 
     def get_related_object(self, pk):
         return self.model.objects.get(pk=pk)
 
 
-@registry.register(name='tag')
+@registry.register(name="tag")
 class TagRadio(RelatedObjectRadio):
     model = Tag
 
     def get_queryset(self, **kwargs):
         qs = super().get_queryset(**kwargs)
-        return Track.objects.filter(tags__in=[self.session.related_object])
+        return qs.filter(tags__in=[self.session.related_object])
+
 
-@registry.register(name='artist')
+@registry.register(name="artist")
 class ArtistRadio(RelatedObjectRadio):
     model = Artist
 
@@ -164,7 +160,7 @@ class ArtistRadio(RelatedObjectRadio):
         return qs.filter(artist=self.session.related_object)
 
 
-@registry.register(name='less-listened')
+@registry.register(name="less-listened")
 class LessListenedRadio(RelatedObjectRadio):
     model = User
 
@@ -174,5 +170,5 @@ class LessListenedRadio(RelatedObjectRadio):
 
     def get_queryset(self, **kwargs):
         qs = super().get_queryset(**kwargs)
-        listened = self.session.user.listenings.all().values_list('track', flat=True)
-        return qs.exclude(pk__in=listened).order_by('?')
+        listened = self.session.user.listenings.all().values_list("track", flat=True)
+        return qs.exclude(pk__in=listened).order_by("?")
diff --git a/api/funkwhale_api/radios/registries.py b/api/funkwhale_api/radios/registries.py
index eec223539daeb626e043c621597863501c4a1eb9..4a30102b79e81043422a0ecf61c041aa19637dc6 100644
--- a/api/funkwhale_api/radios/registries.py
+++ b/api/funkwhale_api/radios/registries.py
@@ -1,8 +1,10 @@
 import persisting_theory
 
+
 class RadioRegistry(persisting_theory.Registry):
     def prepare_name(self, data, name=None):
-        setattr(data, 'radio_type', name)
+        setattr(data, "radio_type", name)
         return name
 
-registry =  RadioRegistry()
+
+registry = RadioRegistry()
diff --git a/api/funkwhale_api/radios/serializers.py b/api/funkwhale_api/radios/serializers.py
index 8c59f87156888d66a8c0f329ae91b436eb09b506..9bffbf5b9cd3ce0c04100dcf3a68de008964b416 100644
--- a/api/funkwhale_api/radios/serializers.py
+++ b/api/funkwhale_api/radios/serializers.py
@@ -3,13 +3,12 @@ from rest_framework import serializers
 from funkwhale_api.music.serializers import TrackSerializer
 from funkwhale_api.users.serializers import UserBasicSerializer
 
-from . import filters
-from . import models
+from . import filters, models
 from .radios import registry
 
 
 class FilterSerializer(serializers.Serializer):
-    type = serializers.CharField(source='code')
+    type = serializers.CharField(source="code")
     label = serializers.CharField()
     help_text = serializers.CharField()
     fields = serializers.ReadOnlyField()
@@ -21,19 +20,20 @@ class RadioSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.Radio
         fields = (
-            'id',
-            'is_public',
-            'name',
-            'creation_date',
-            'user',
-            'config',
-            'description')
-        read_only_fields = ('user', 'creation_date')
+            "id",
+            "is_public",
+            "name",
+            "creation_date",
+            "user",
+            "config",
+            "description",
+        )
+        read_only_fields = ("user", "creation_date")
 
     def save(self, **kwargs):
-        kwargs['config'] = [
-            filters.registry[f['type']].clean_config(f)
-            for f in self.validated_data['config']
+        kwargs["config"] = [
+            filters.registry[f["type"]].clean_config(f)
+            for f in self.validated_data["config"]
         ]
 
         return super().save(**kwargs)
@@ -42,7 +42,7 @@ class RadioSerializer(serializers.ModelSerializer):
 class RadioSessionTrackSerializerCreate(serializers.ModelSerializer):
     class Meta:
         model = models.RadioSessionTrack
-        fields = ('session',)
+        fields = ("session",)
 
 
 class RadioSessionTrackSerializer(serializers.ModelSerializer):
@@ -50,28 +50,30 @@ class RadioSessionTrackSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = models.RadioSessionTrack
-        fields = ('id', 'session', 'position', 'track')
+        fields = ("id", "session", "position", "track")
 
 
 class RadioSessionSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.RadioSession
         fields = (
-            'id',
-            'radio_type',
-            'related_object_id',
-            'user',
-            'creation_date',
-            'custom_radio',
+            "id",
+            "radio_type",
+            "related_object_id",
+            "user",
+            "creation_date",
+            "custom_radio",
         )
 
     def validate(self, data):
-        registry[data['radio_type']]().validate_session(data, **self.context)
+        registry[data["radio_type"]]().validate_session(data, **self.context)
         return data
 
     def create(self, validated_data):
-        validated_data['user'] = self.context['user']
-        if validated_data.get('related_object_id'):
-            radio = registry[validated_data['radio_type']]()
-            validated_data['related_object'] = radio.get_related_object(validated_data['related_object_id'])
+        validated_data["user"] = self.context["user"]
+        if validated_data.get("related_object_id"):
+            radio = registry[validated_data["radio_type"]]()
+            validated_data["related_object"] = radio.get_related_object(
+                validated_data["related_object_id"]
+            )
         return super().create(validated_data)
diff --git a/api/funkwhale_api/radios/urls.py b/api/funkwhale_api/radios/urls.py
index d84615ca57ceba484ba5b6580804948294b2fc4f..8b9fd52c8a440d7ec41a8afa1e703f8b2a823143 100644
--- a/api/funkwhale_api/radios/urls.py
+++ b/api/funkwhale_api/radios/urls.py
@@ -1,11 +1,11 @@
-from django.conf.urls import include, url
+from rest_framework import routers
+
 from . import views
 
-from rest_framework import routers
 router = routers.SimpleRouter()
-router.register(r'sessions', views.RadioSessionViewSet, 'sessions')
-router.register(r'radios', views.RadioViewSet, 'radios')
-router.register(r'tracks', views.RadioSessionTrackViewSet, 'tracks')
+router.register(r"sessions", views.RadioSessionViewSet, "sessions")
+router.register(r"radios", views.RadioViewSet, "radios")
+router.register(r"tracks", views.RadioSessionTrackViewSet, "tracks")
 
 
 urlpatterns = router.urls
diff --git a/api/funkwhale_api/radios/views.py b/api/funkwhale_api/radios/views.py
index ca510b82c431beb89f08a7e173eb12ab3fd2abaf..fb2c4d855d266fbda530b974f7b725af31d2b7c4 100644
--- a/api/funkwhale_api/radios/views.py
+++ b/api/funkwhale_api/radios/views.py
@@ -1,48 +1,46 @@
 from django.db.models import Q
-from django.http import Http404
-
-from rest_framework import generics, mixins, viewsets
-from rest_framework import permissions
-from rest_framework import status
-from rest_framework.response import Response
+from rest_framework import mixins, permissions, status, viewsets
 from rest_framework.decorators import detail_route, list_route
+from rest_framework.response import Response
 
+from funkwhale_api.common import permissions as common_permissions
 from funkwhale_api.music.serializers import TrackSerializer
-from funkwhale_api.common.permissions import ConditionalAuthentication
 
-from . import models
-from . import filters
-from . import filtersets
-from . import serializers
+from . import filters, filtersets, models, serializers
 
 
 class RadioViewSet(
-        mixins.CreateModelMixin,
-        mixins.RetrieveModelMixin,
-        mixins.UpdateModelMixin,
-        mixins.ListModelMixin,
-        mixins.DestroyModelMixin,
-        viewsets.GenericViewSet):
+    mixins.CreateModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.UpdateModelMixin,
+    mixins.ListModelMixin,
+    mixins.DestroyModelMixin,
+    viewsets.GenericViewSet,
+):
 
     serializer_class = serializers.RadioSerializer
-    permission_classes = [permissions.IsAuthenticated]
+    permission_classes = [
+        permissions.IsAuthenticated,
+        common_permissions.OwnerPermission,
+    ]
     filter_class = filtersets.RadioFilter
+    owner_field = "user"
+    owner_checks = ["write"]
 
     def get_queryset(self):
+        queryset = models.Radio.objects.all()
         query = Q(is_public=True)
         if self.request.user.is_authenticated:
             query |= Q(user=self.request.user)
-        return models.Radio.objects.filter(query)
+        return queryset.filter(query)
 
     def perform_create(self, serializer):
         return serializer.save(user=self.request.user)
 
     def perform_update(self, serializer):
-        if serializer.instance.user != self.request.user:
-            raise Http404
         return serializer.save(user=self.request.user)
 
-    @detail_route(methods=['get'])
+    @detail_route(methods=["get"])
     def tracks(self, request, *args, **kwargs):
         radio = self.get_object()
         tracks = radio.get_candidates().for_nested_serialization()
@@ -52,36 +50,33 @@ class RadioViewSet(
             serializer = TrackSerializer(page, many=True)
             return self.get_paginated_response(serializer.data)
 
-    @list_route(methods=['get'])
+    @list_route(methods=["get"])
     def filters(self, request, *args, **kwargs):
         serializer = serializers.FilterSerializer(
-            filters.registry.exposed_filters, many=True)
+            filters.registry.exposed_filters, many=True
+        )
         return Response(serializer.data)
 
-    @list_route(methods=['post'])
+    @list_route(methods=["post"])
     def validate(self, request, *args, **kwargs):
         try:
-            f_list = request.data['filters']
+            f_list = request.data["filters"]
         except KeyError:
-            return Response(
-                {'error': 'You must provide a filters list'}, status=400)
-        data = {
-            'filters': []
-        }
+            return Response({"error": "You must provide a filters list"}, status=400)
+        data = {"filters": []}
         for f in f_list:
             results = filters.test(f)
-            if results['candidates']['sample']:
-                qs = results['candidates']['sample'].for_nested_serialization()
-                results['candidates']['sample'] = TrackSerializer(
-                    qs, many=True).data
-            data['filters'].append(results)
+            if results["candidates"]["sample"]:
+                qs = results["candidates"]["sample"].for_nested_serialization()
+                results["candidates"]["sample"] = TrackSerializer(qs, many=True).data
+            data["filters"].append(results)
 
         return Response(data)
 
 
-class RadioSessionViewSet(mixins.CreateModelMixin,
-                          mixins.RetrieveModelMixin,
-                          viewsets.GenericViewSet):
+class RadioSessionViewSet(
+    mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
+):
 
     serializer_class = serializers.RadioSessionSerializer
     queryset = models.RadioSession.objects.all()
@@ -93,12 +88,11 @@ class RadioSessionViewSet(mixins.CreateModelMixin,
 
     def get_serializer_context(self):
         context = super().get_serializer_context()
-        context['user'] = self.request.user
+        context["user"] = self.request.user
         return context
 
 
-class RadioSessionTrackViewSet(mixins.CreateModelMixin,
-                               viewsets.GenericViewSet):
+class RadioSessionTrackViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
     serializer_class = serializers.RadioSessionTrackSerializer
     queryset = models.RadioSessionTrack.objects.all()
     permission_classes = [permissions.IsAuthenticated]
@@ -106,20 +100,24 @@ class RadioSessionTrackViewSet(mixins.CreateModelMixin,
     def create(self, request, *args, **kwargs):
         serializer = self.get_serializer(data=request.data)
         serializer.is_valid(raise_exception=True)
-        session = serializer.validated_data['session']
+        session = serializer.validated_data["session"]
         try:
             assert request.user == session.user
         except AssertionError:
             return Response(status=status.HTTP_403_FORBIDDEN)
-        track = session.radio.pick()
-        session_track = session.session_tracks.all().latest('id')
+        session.radio.pick()
+        session_track = session.session_tracks.all().latest("id")
         # self.perform_create(serializer)
         # dirty override here, since we use a different serializer for creation and detail
-        serializer = self.serializer_class(instance=session_track, context=self.get_serializer_context())
+        serializer = self.serializer_class(
+            instance=session_track, context=self.get_serializer_context()
+        )
         headers = self.get_success_headers(serializer.data)
-        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+        return Response(
+            serializer.data, status=status.HTTP_201_CREATED, headers=headers
+        )
 
     def get_serializer_class(self, *args, **kwargs):
-        if self.action == 'create':
+        if self.action == "create":
             return serializers.RadioSessionTrackSerializerCreate
         return super().get_serializer_class(*args, **kwargs)
diff --git a/api/funkwhale_api/requests/admin.py b/api/funkwhale_api/requests/admin.py
index 8ca008a03a43607240e3102ade0b2ea8a75ac2d6..b0f1a7990ef55fe7659073835db4fc6ef48e4c96 100644
--- a/api/funkwhale_api/requests/admin.py
+++ b/api/funkwhale_api/requests/admin.py
@@ -5,11 +5,7 @@ from . import models
 
 @admin.register(models.ImportRequest)
 class ImportRequestAdmin(admin.ModelAdmin):
-    list_display = ['artist_name', 'user', 'status', 'creation_date']
-    list_select_related = [
-        'user'
-    ]
-    list_filter = [
-        'status',
-    ]
-    search_fields = ['artist_name', 'comment', 'albums']
+    list_display = ["artist_name", "user", "status", "creation_date"]
+    list_select_related = ["user"]
+    list_filter = ["status"]
+    search_fields = ["artist_name", "comment", "albums"]
diff --git a/api/funkwhale_api/requests/api_urls.py b/api/funkwhale_api/requests/api_urls.py
index 37459a664a4d7e1fd9bc229fbb678daa02adb012..403a0953bac9fa4d822466ccc9a286477d57b89b 100644
--- a/api/funkwhale_api/requests/api_urls.py
+++ b/api/funkwhale_api/requests/api_urls.py
@@ -1,11 +1,8 @@
-from django.conf.urls import include, url
+from rest_framework import routers
+
 from . import views
 
-from rest_framework import routers
 router = routers.SimpleRouter()
-router.register(
-    r'import-requests',
-    views.ImportRequestViewSet,
-    'import-requests')
+router.register(r"import-requests", views.ImportRequestViewSet, "import-requests")
 
 urlpatterns = router.urls
diff --git a/api/funkwhale_api/requests/factories.py b/api/funkwhale_api/requests/factories.py
index 2bcdeb6a9699ee47a16cc4fb2e05ebe9b19286e0..d6673aebdc94078b8eba7323a67b8b65f61298cd 100644
--- a/api/funkwhale_api/requests/factories.py
+++ b/api/funkwhale_api/requests/factories.py
@@ -6,10 +6,10 @@ from funkwhale_api.users.factories import UserFactory
 
 @registry.register
 class ImportRequestFactory(factory.django.DjangoModelFactory):
-    artist_name = factory.Faker('name')
-    albums = factory.Faker('sentence')
+    artist_name = factory.Faker("name")
+    albums = factory.Faker("sentence")
     user = factory.SubFactory(UserFactory)
-    comment = factory.Faker('paragraph')
+    comment = factory.Faker("paragraph")
 
     class Meta:
-        model = 'requests.ImportRequest'
+        model = "requests.ImportRequest"
diff --git a/api/funkwhale_api/requests/filters.py b/api/funkwhale_api/requests/filters.py
index 7d06033629280bfd212608573b1dcea7ad04697c..4a06dea1b754f35f830b8cad488fa34ea04e28be 100644
--- a/api/funkwhale_api/requests/filters.py
+++ b/api/funkwhale_api/requests/filters.py
@@ -1,22 +1,20 @@
 import django_filters
 
 from funkwhale_api.common import fields
+
 from . import models
 
 
 class ImportRequestFilter(django_filters.FilterSet):
 
-    q = fields.SearchFilter(search_fields=[
-        'artist_name',
-        'user__username',
-        'albums',
-        'comment',
-    ])
+    q = fields.SearchFilter(
+        search_fields=["artist_name", "user__username", "albums", "comment"]
+    )
 
     class Meta:
         model = models.ImportRequest
         fields = {
-            'artist_name': ['exact', 'iexact', 'startswith', 'icontains'],
-            'status': ['exact'],
-            'user__username': ['exact'],
+            "artist_name": ["exact", "iexact", "startswith", "icontains"],
+            "status": ["exact"],
+            "user__username": ["exact"],
         }
diff --git a/api/funkwhale_api/requests/migrations/0001_initial.py b/api/funkwhale_api/requests/migrations/0001_initial.py
index 7c239b3c079234ec130be85c300e3a9956a7ad41..ab9b619ef50d035e9cb58414a4f969b7d842d2c7 100644
--- a/api/funkwhale_api/requests/migrations/0001_initial.py
+++ b/api/funkwhale_api/requests/migrations/0001_initial.py
@@ -10,22 +10,50 @@ class Migration(migrations.Migration):
 
     initial = True
 
-    dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
-    ]
+    dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
 
     operations = [
         migrations.CreateModel(
-            name='ImportRequest',
+            name="ImportRequest",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
-                ('imported_date', models.DateTimeField(blank=True, null=True)),
-                ('artist_name', models.CharField(max_length=250)),
-                ('albums', models.CharField(blank=True, max_length=3000, null=True)),
-                ('status', models.CharField(choices=[('pending', 'pending'), ('accepted', 'accepted'), ('imported', 'imported'), ('closed', 'closed')], default='pending', max_length=50)),
-                ('comment', models.TextField(blank=True, max_length=3000, null=True)),
-                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='import_requests', to=settings.AUTH_USER_MODEL)),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "creation_date",
+                    models.DateTimeField(default=django.utils.timezone.now),
+                ),
+                ("imported_date", models.DateTimeField(blank=True, null=True)),
+                ("artist_name", models.CharField(max_length=250)),
+                ("albums", models.CharField(blank=True, max_length=3000, null=True)),
+                (
+                    "status",
+                    models.CharField(
+                        choices=[
+                            ("pending", "pending"),
+                            ("accepted", "accepted"),
+                            ("imported", "imported"),
+                            ("closed", "closed"),
+                        ],
+                        default="pending",
+                        max_length=50,
+                    ),
+                ),
+                ("comment", models.TextField(blank=True, max_length=3000, null=True)),
+                (
+                    "user",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="import_requests",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
             ],
-        ),
+        )
     ]
diff --git a/api/funkwhale_api/requests/models.py b/api/funkwhale_api/requests/models.py
index d08dd4004f9ea34e943ac7e61a5783f4a7e7fcd3..a24086d7a695c59e94acc55d76981d486222c2dd 100644
--- a/api/funkwhale_api/requests/models.py
+++ b/api/funkwhale_api/requests/models.py
@@ -1,18 +1,13 @@
 from django.db import models
-
 from django.utils import timezone
 
-NATURE_CHOICES = [
-    ('artist', 'artist'),
-    ('album', 'album'),
-    ('track', 'track'),
-]
+NATURE_CHOICES = [("artist", "artist"), ("album", "album"), ("track", "track")]
 
 STATUS_CHOICES = [
-    ('pending', 'pending'),
-    ('accepted', 'accepted'),
-    ('imported', 'imported'),
-    ('closed', 'closed'),
+    ("pending", "pending"),
+    ("accepted", "accepted"),
+    ("imported", "imported"),
+    ("closed", "closed"),
 ]
 
 
@@ -20,11 +15,9 @@ class ImportRequest(models.Model):
     creation_date = models.DateTimeField(default=timezone.now)
     imported_date = models.DateTimeField(null=True, blank=True)
     user = models.ForeignKey(
-        'users.User',
-        related_name='import_requests',
-        on_delete=models.CASCADE)
+        "users.User", related_name="import_requests", on_delete=models.CASCADE
+    )
     artist_name = models.CharField(max_length=250)
     albums = models.CharField(max_length=3000, null=True, blank=True)
-    status = models.CharField(
-        choices=STATUS_CHOICES, max_length=50, default='pending')
+    status = models.CharField(choices=STATUS_CHOICES, max_length=50, default="pending")
     comment = models.TextField(null=True, blank=True, max_length=3000)
diff --git a/api/funkwhale_api/requests/serializers.py b/api/funkwhale_api/requests/serializers.py
index 51a709514e0cb95a2f03289dd981df918734c795..2a810a9997196e8ad3b0d19477d7f71151cd1253 100644
--- a/api/funkwhale_api/requests/serializers.py
+++ b/api/funkwhale_api/requests/serializers.py
@@ -11,20 +11,17 @@ class ImportRequestSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.ImportRequest
         fields = (
-            'id',
-            'status',
-            'albums',
-            'artist_name',
-            'user',
-            'creation_date',
-            'imported_date',
-            'comment')
-        read_only_fields = (
-            'creation_date',
-            'imported_date',
-            'user',
-            'status')
+            "id",
+            "status",
+            "albums",
+            "artist_name",
+            "user",
+            "creation_date",
+            "imported_date",
+            "comment",
+        )
+        read_only_fields = ("creation_date", "imported_date", "user", "status")
 
     def create(self, validated_data):
-        validated_data['user'] = self.context['user']
+        validated_data["user"] = self.context["user"]
         return super().create(validated_data)
diff --git a/api/funkwhale_api/requests/views.py b/api/funkwhale_api/requests/views.py
index 6553f3316fb3cbd50f040202d0bb36a38a7fceb6..96d8c89279333b442a7afade7bf53f2022769cd6 100644
--- a/api/funkwhale_api/requests/views.py
+++ b/api/funkwhale_api/requests/views.py
@@ -1,26 +1,21 @@
-from rest_framework import generics, mixins, viewsets
-from rest_framework import status
-from rest_framework.response import Response
-from rest_framework.decorators import detail_route
+from rest_framework import mixins, viewsets
 
-from . import filters
-from . import models
-from . import serializers
+from . import filters, models, serializers
 
 
 class ImportRequestViewSet(
-        mixins.CreateModelMixin,
-        mixins.RetrieveModelMixin,
-        mixins.ListModelMixin,
-        viewsets.GenericViewSet):
+    mixins.CreateModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.ListModelMixin,
+    viewsets.GenericViewSet,
+):
 
     serializer_class = serializers.ImportRequestSerializer
     queryset = (
-        models.ImportRequest.objects.all()
-              .select_related()
-              .order_by('-creation_date'))
+        models.ImportRequest.objects.all().select_related().order_by("-creation_date")
+    )
     filter_class = filters.ImportRequestFilter
-    ordering_fields = ('id', 'artist_name', 'creation_date', 'status')
+    ordering_fields = ("id", "artist_name", "creation_date", "status")
 
     def perform_create(self, serializer):
         return serializer.save(user=self.request.user)
@@ -28,5 +23,5 @@ class ImportRequestViewSet(
     def get_serializer_context(self):
         context = super().get_serializer_context()
         if self.request.user.is_authenticated:
-            context['user'] = self.request.user
+            context["user"] = self.request.user
         return context
diff --git a/api/funkwhale_api/subsonic/authentication.py b/api/funkwhale_api/subsonic/authentication.py
index fe9b08dc8afda3c0171dd671b79e9ba20ea7ca05..a573a109249972e5a1e693d2c84edfe66efd0f46 100644
--- a/api/funkwhale_api/subsonic/authentication.py
+++ b/api/funkwhale_api/subsonic/authentication.py
@@ -1,8 +1,7 @@
 import binascii
 import hashlib
 
-from rest_framework import authentication
-from rest_framework import exceptions
+from rest_framework import authentication, exceptions
 
 from funkwhale_api.users.models import User
 
@@ -10,23 +9,20 @@ from funkwhale_api.users.models import User
 def get_token(salt, password):
     to_hash = password + salt
     h = hashlib.md5()
-    h.update(to_hash.encode('utf-8'))
+    h.update(to_hash.encode("utf-8"))
     return h.hexdigest()
 
 
 def authenticate(username, password):
     try:
-        if password.startswith('enc:'):
-            password = password.replace('enc:', '', 1)
-            password = binascii.unhexlify(password).decode('utf-8')
+        if password.startswith("enc:"):
+            password = password.replace("enc:", "", 1)
+            password = binascii.unhexlify(password).decode("utf-8")
         user = User.objects.get(
-            username=username,
-            is_active=True,
-            subsonic_api_token=password)
-    except (User.DoesNotExist, binascii.Error):
-        raise exceptions.AuthenticationFailed(
-            'Wrong username or password.'
+            username=username, is_active=True, subsonic_api_token=password
         )
+    except (User.DoesNotExist, binascii.Error):
+        raise exceptions.AuthenticationFailed("Wrong username or password.")
 
     return (user, None)
 
@@ -34,18 +30,13 @@ def authenticate(username, password):
 def authenticate_salt(username, salt, token):
     try:
         user = User.objects.get(
-            username=username,
-            is_active=True,
-            subsonic_api_token__isnull=False)
-    except User.DoesNotExist:
-        raise exceptions.AuthenticationFailed(
-            'Wrong username or password.'
+            username=username, is_active=True, subsonic_api_token__isnull=False
         )
+    except User.DoesNotExist:
+        raise exceptions.AuthenticationFailed("Wrong username or password.")
     expected = get_token(salt, user.subsonic_api_token)
     if expected != token:
-        raise exceptions.AuthenticationFailed(
-            'Wrong username or password.'
-        )
+        raise exceptions.AuthenticationFailed("Wrong username or password.")
 
     return (user, None)
 
@@ -53,15 +44,15 @@ def authenticate_salt(username, salt, token):
 class SubsonicAuthentication(authentication.BaseAuthentication):
     def authenticate(self, request):
         data = request.GET or request.POST
-        username = data.get('u')
+        username = data.get("u")
         if not username:
             return None
 
-        p = data.get('p')
-        s = data.get('s')
-        t = data.get('t')
+        p = data.get("p")
+        s = data.get("s")
+        t = data.get("t")
         if not p and (not s or not t):
-            raise exceptions.AuthenticationFailed('Missing credentials')
+            raise exceptions.AuthenticationFailed("Missing credentials")
 
         if p:
             return authenticate(username, p)
diff --git a/api/funkwhale_api/subsonic/dynamic_preferences_registry.py b/api/funkwhale_api/subsonic/dynamic_preferences_registry.py
index 93482702ff110e39885dc72395836598f4e7e6bc..439d16de39b4eec8cbe3168505e8f11be1bf20b9 100644
--- a/api/funkwhale_api/subsonic/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/subsonic/dynamic_preferences_registry.py
@@ -1,22 +1,20 @@
 from dynamic_preferences import types
 from dynamic_preferences.registries import global_preferences_registry
 
-from funkwhale_api.common import preferences
-
-subsonic = types.Section('subsonic')
+subsonic = types.Section("subsonic")
 
 
 @global_preferences_registry.register
 class APIAutenticationRequired(types.BooleanPreference):
     section = subsonic
     show_in_api = True
-    name = 'enabled'
+    name = "enabled"
     default = True
-    verbose_name = 'Enabled Subsonic API'
+    verbose_name = "Enabled Subsonic API"
     help_text = (
-        'Funkwhale supports a subset of the Subsonic API, that makes '
-        'it compatible with existing clients such as DSub for Android '
-        'or Clementine for desktop. However, Subsonic protocol is less '
-        'than ideal in terms of security and you can disable this feature '
-        'completely using this flag.'
+        "Funkwhale supports a subset of the Subsonic API, that makes "
+        "it compatible with existing clients such as DSub for Android "
+        "or Clementine for desktop. However, Subsonic protocol is less "
+        "than ideal in terms of security and you can disable this feature "
+        "completely using this flag."
     )
diff --git a/api/funkwhale_api/subsonic/filters.py b/api/funkwhale_api/subsonic/filters.py
index b7b639fac319bc6f8da17ead83f23f52324c23c7..a354e23f111fd354d80bac3e4929083bf2b599b9 100644
--- a/api/funkwhale_api/subsonic/filters.py
+++ b/api/funkwhale_api/subsonic/filters.py
@@ -4,18 +4,18 @@ from funkwhale_api.music import models as music_models
 
 
 class AlbumList2FilterSet(filters.FilterSet):
-    type = filters.CharFilter(name='_', method='filter_type')
+    type = filters.CharFilter(name="_", method="filter_type")
 
     class Meta:
         model = music_models.Album
-        fields = ['type']
+        fields = ["type"]
 
     def filter_type(self, queryset, name, value):
         ORDERING = {
-            'random': '?',
-            'newest': '-creation_date',
-            'alphabeticalByArtist': 'artist__name',
-            'alphabeticalByName': 'title',
+            "random": "?",
+            "newest": "-creation_date",
+            "alphabeticalByArtist": "artist__name",
+            "alphabeticalByName": "title",
         }
         if value not in ORDERING:
             return queryset
diff --git a/api/funkwhale_api/subsonic/negotiation.py b/api/funkwhale_api/subsonic/negotiation.py
index 3335fda45b2d59564b6687bb686cc98b6cf93700..96b41589e0aea74ad4eef8be4fe9c21f2d89d5cc 100644
--- a/api/funkwhale_api/subsonic/negotiation.py
+++ b/api/funkwhale_api/subsonic/negotiation.py
@@ -1,20 +1,17 @@
-from rest_framework import exceptions
-from rest_framework import negotiation
+from rest_framework import exceptions, negotiation
 
 from . import renderers
 
-
 MAPPING = {
-    'json': (renderers.SubsonicJSONRenderer(), 'application/json'),
-    'xml': (renderers.SubsonicXMLRenderer(), 'text/xml'),
+    "json": (renderers.SubsonicJSONRenderer(), "application/json"),
+    "xml": (renderers.SubsonicXMLRenderer(), "text/xml"),
 }
 
 
 class SubsonicContentNegociation(negotiation.DefaultContentNegotiation):
     def select_renderer(self, request, renderers, format_suffix=None):
-        path = request.path
         data = request.GET or request.POST
-        requested_format = data.get('f', 'xml')
+        requested_format = data.get("f", "xml")
         try:
             return MAPPING[requested_format]
         except KeyError:
diff --git a/api/funkwhale_api/subsonic/renderers.py b/api/funkwhale_api/subsonic/renderers.py
index 3a56645012f07910a9c0e175b3b3e3d6f6bec8f2..fac12d6c1928f9e429944a98e1ca81ee3381b0ef 100644
--- a/api/funkwhale_api/subsonic/renderers.py
+++ b/api/funkwhale_api/subsonic/renderers.py
@@ -8,37 +8,34 @@ class SubsonicJSONRenderer(renderers.JSONRenderer):
         if not data:
             # when stream view is called, we don't have any data
             return super().render(data, accepted_media_type, renderer_context)
-        final = {
-            'subsonic-response': {
-                'status': 'ok',
-                'version': '1.16.0',
-            }
-        }
-        final['subsonic-response'].update(data)
-        if 'error' in final:
+        final = {"subsonic-response": {"status": "ok", "version": "1.16.0"}}
+        final["subsonic-response"].update(data)
+        if "error" in final:
             # an error was returned
-            final['subsonic-response']['status'] = 'failed'
+            final["subsonic-response"]["status"] = "failed"
         return super().render(final, accepted_media_type, renderer_context)
 
 
 class SubsonicXMLRenderer(renderers.JSONRenderer):
-    media_type = 'text/xml'
+    media_type = "text/xml"
 
     def render(self, data, accepted_media_type=None, renderer_context=None):
         if not data:
             # when stream view is called, we don't have any data
             return super().render(data, accepted_media_type, renderer_context)
         final = {
-            'xmlns': 'http://subsonic.org/restapi',
-            'status': 'ok',
-            'version': '1.16.0',
+            "xmlns": "http://subsonic.org/restapi",
+            "status": "ok",
+            "version": "1.16.0",
         }
         final.update(data)
-        if 'error' in final:
+        if "error" in final:
             # an error was returned
-            final['status'] = 'failed'
-        tree = dict_to_xml_tree('subsonic-response', final)
-        return b'<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(tree, encoding='utf-8')
+            final["status"] = "failed"
+        tree = dict_to_xml_tree("subsonic-response", final)
+        return b'<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(
+            tree, encoding="utf-8"
+        )
 
 
 def dict_to_xml_tree(root_tag, d, parent=None):
diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py
index 97cdbcfc692b97bc763771a903888372768d47a7..fc21a99f2aa540024754810d96865638d2a1eb28 100644
--- a/api/funkwhale_api/subsonic/serializers.py
+++ b/api/funkwhale_api/subsonic/serializers.py
@@ -1,7 +1,6 @@
 import collections
 
-from django.db.models import functions, Count
-
+from django.db.models import Count, functions
 from rest_framework import serializers
 
 from funkwhale_api.history import models as history_models
@@ -10,106 +9,100 @@ from funkwhale_api.music import models as music_models
 
 def get_artist_data(artist_values):
     return {
-        'id': artist_values['id'],
-        'name': artist_values['name'],
-        'albumCount': artist_values['_albums_count']
+        "id": artist_values["id"],
+        "name": artist_values["name"],
+        "albumCount": artist_values["_albums_count"],
     }
 
 
 class GetArtistsSerializer(serializers.Serializer):
     def to_representation(self, queryset):
-        payload = {
-            'ignoredArticles': '',
-            'index': []
-        }
+        payload = {"ignoredArticles": "", "index": []}
         queryset = queryset.with_albums_count()
-        queryset = queryset.order_by(functions.Lower('name'))
-        values = queryset.values('id', '_albums_count', 'name')
+        queryset = queryset.order_by(functions.Lower("name"))
+        values = queryset.values("id", "_albums_count", "name")
 
         first_letter_mapping = collections.defaultdict(list)
         for artist in values:
-            first_letter_mapping[artist['name'][0].upper()].append(artist)
+            first_letter_mapping[artist["name"][0].upper()].append(artist)
 
         for letter, artists in sorted(first_letter_mapping.items()):
             letter_data = {
-                'name': letter,
-                'artist': [
-                    get_artist_data(v)
-                    for v in artists
-                ]
+                "name": letter,
+                "artist": [get_artist_data(v) for v in artists],
             }
-            payload['index'].append(letter_data)
+            payload["index"].append(letter_data)
         return payload
 
 
 class GetArtistSerializer(serializers.Serializer):
     def to_representation(self, artist):
-        albums = artist.albums.prefetch_related('tracks__files')
+        albums = artist.albums.prefetch_related("tracks__files")
         payload = {
-            'id': artist.pk,
-            'name': artist.name,
-            'albumCount': len(albums),
-            'album': [],
+            "id": artist.pk,
+            "name": artist.name,
+            "albumCount": len(albums),
+            "album": [],
         }
         for album in albums:
             album_data = {
-                'id': album.id,
-                'artistId': artist.id,
-                'name': album.title,
-                'artist': artist.name,
-                'created': album.creation_date,
-                'songCount': len(album.tracks.all()),
+                "id": album.id,
+                "artistId": artist.id,
+                "name": album.title,
+                "artist": artist.name,
+                "created": album.creation_date,
+                "songCount": len(album.tracks.all()),
             }
             if album.cover:
-                album_data['coverArt'] = 'al-{}'.format(album.id)
+                album_data["coverArt"] = "al-{}".format(album.id)
             if album.release_date:
-                album_data['year'] = album.release_date.year
-            payload['album'].append(album_data)
+                album_data["year"] = album.release_date.year
+            payload["album"].append(album_data)
         return payload
 
 
 def get_track_data(album, track, tf):
     data = {
-        'id': track.pk,
-        'isDir': 'false',
-        'title': track.title,
-        'album': album.title,
-        'artist': album.artist.name,
-        'track': track.position or 1,
-        'contentType': tf.mimetype,
-        'suffix': tf.extension or '',
-        'duration': tf.duration or 0,
-        'created': track.creation_date,
-        'albumId': album.pk,
-        'artistId': album.artist.pk,
-        'type': 'music',
+        "id": track.pk,
+        "isDir": "false",
+        "title": track.title,
+        "album": album.title,
+        "artist": album.artist.name,
+        "track": track.position or 1,
+        "contentType": tf.mimetype,
+        "suffix": tf.extension or "",
+        "duration": tf.duration or 0,
+        "created": track.creation_date,
+        "albumId": album.pk,
+        "artistId": album.artist.pk,
+        "type": "music",
     }
     if track.album.cover:
-        data['coverArt'] = 'al-{}'.format(track.album.id)
+        data["coverArt"] = "al-{}".format(track.album.id)
     if tf.bitrate:
-        data['bitrate'] = int(tf.bitrate/1000)
+        data["bitrate"] = int(tf.bitrate / 1000)
     if tf.size:
-        data['size'] = tf.size
+        data["size"] = tf.size
     if album.release_date:
-        data['year'] = album.release_date.year
+        data["year"] = album.release_date.year
     return data
 
 
 def get_album2_data(album):
     payload = {
-        'id': album.id,
-        'artistId': album.artist.id,
-        'name': album.title,
-        'artist': album.artist.name,
-        'created': album.creation_date,
+        "id": album.id,
+        "artistId": album.artist.id,
+        "name": album.title,
+        "artist": album.artist.name,
+        "created": album.creation_date,
     }
     if album.cover:
-        payload['coverArt'] = 'al-{}'.format(album.id)
+        payload["coverArt"] = "al-{}".format(album.id)
 
     try:
-        payload['songCount'] = album._tracks_count
+        payload["songCount"] = album._tracks_count
     except AttributeError:
-        payload['songCount'] = len(album.tracks.prefetch_related('files'))
+        payload["songCount"] = len(album.tracks.prefetch_related("files"))
     return payload
 
 
@@ -127,24 +120,23 @@ def get_song_list_data(tracks):
 
 class GetAlbumSerializer(serializers.Serializer):
     def to_representation(self, album):
-        tracks = album.tracks.prefetch_related('files').select_related('album')
+        tracks = album.tracks.prefetch_related("files").select_related("album")
         payload = get_album2_data(album)
         if album.release_date:
-            payload['year'] = album.release_date.year
+            payload["year"] = album.release_date.year
 
-        payload['song'] = get_song_list_data(tracks)
+        payload["song"] = get_song_list_data(tracks)
         return payload
 
 
 def get_starred_tracks_data(favorites):
-    by_track_id = {
-        f.track_id: f
-        for f in favorites
-    }
-    tracks = music_models.Track.objects.filter(
-        pk__in=by_track_id.keys()
-    ).select_related('album__artist').prefetch_related('files')
-    tracks = tracks.order_by('-creation_date')
+    by_track_id = {f.track_id: f for f in favorites}
+    tracks = (
+        music_models.Track.objects.filter(pk__in=by_track_id.keys())
+        .select_related("album__artist")
+        .prefetch_related("files")
+    )
+    tracks = tracks.order_by("-creation_date")
     data = []
     for t in tracks:
         try:
@@ -152,54 +144,48 @@ def get_starred_tracks_data(favorites):
         except IndexError:
             continue
         td = get_track_data(t.album, t, tf)
-        td['starred'] = by_track_id[t.pk].creation_date
+        td["starred"] = by_track_id[t.pk].creation_date
         data.append(td)
     return data
 
 
 def get_album_list2_data(albums):
-    return [
-        get_album2_data(a)
-        for a in albums
-    ]
+    return [get_album2_data(a) for a in albums]
 
 
 def get_playlist_data(playlist):
     return {
-        'id': playlist.pk,
-        'name': playlist.name,
-        'owner': playlist.user.username,
-        'public': 'false',
-        'songCount': playlist._tracks_count,
-        'duration': 0,
-        'created': playlist.creation_date,
+        "id": playlist.pk,
+        "name": playlist.name,
+        "owner": playlist.user.username,
+        "public": "false",
+        "songCount": playlist._tracks_count,
+        "duration": 0,
+        "created": playlist.creation_date,
     }
 
 
 def get_playlist_detail_data(playlist):
     data = get_playlist_data(playlist)
-    qs = playlist.playlist_tracks.select_related(
-        'track__album__artist'
-    ).prefetch_related('track__files').order_by('index')
-    data['entry'] = []
+    qs = (
+        playlist.playlist_tracks.select_related("track__album__artist")
+        .prefetch_related("track__files")
+        .order_by("index")
+    )
+    data["entry"] = []
     for plt in qs:
         try:
             tf = [tf for tf in plt.track.files.all()][0]
         except IndexError:
             continue
         td = get_track_data(plt.track.album, plt.track, tf)
-        data['entry'].append(td)
+        data["entry"].append(td)
     return data
 
 
 def get_music_directory_data(artist):
-    tracks = artist.tracks.select_related('album').prefetch_related('files')
-    data = {
-        'id': artist.pk,
-        'parent': 1,
-        'name': artist.name,
-        'child': []
-    }
+    tracks = artist.tracks.select_related("album").prefetch_related("files")
+    data = {"id": artist.pk, "parent": 1, "name": artist.name, "child": []}
     for track in tracks:
         try:
             tf = [tf for tf in track.files.all()][0]
@@ -207,40 +193,39 @@ def get_music_directory_data(artist):
             continue
         album = track.album
         td = {
-            'id': track.pk,
-            'isDir': 'false',
-            'title': track.title,
-            'album': album.title,
-            'artist': artist.name,
-            'track': track.position or 1,
-            'year': track.album.release_date.year if track.album.release_date else 0,
-            'contentType': tf.mimetype,
-            'suffix': tf.extension or '',
-            'duration': tf.duration or 0,
-            'created': track.creation_date,
-            'albumId': album.pk,
-            'artistId': artist.pk,
-            'parent': artist.id,
-            'type': 'music',
+            "id": track.pk,
+            "isDir": "false",
+            "title": track.title,
+            "album": album.title,
+            "artist": artist.name,
+            "track": track.position or 1,
+            "year": track.album.release_date.year if track.album.release_date else 0,
+            "contentType": tf.mimetype,
+            "suffix": tf.extension or "",
+            "duration": tf.duration or 0,
+            "created": track.creation_date,
+            "albumId": album.pk,
+            "artistId": artist.pk,
+            "parent": artist.id,
+            "type": "music",
         }
         if tf.bitrate:
-            td['bitrate'] = int(tf.bitrate/1000)
+            td["bitrate"] = int(tf.bitrate / 1000)
         if tf.size:
-            td['size'] = tf.size
-        data['child'].append(td)
+            td["size"] = tf.size
+        data["child"].append(td)
     return data
 
 
 class ScrobbleSerializer(serializers.Serializer):
     submission = serializers.BooleanField(default=True, required=False)
     id = serializers.PrimaryKeyRelatedField(
-        queryset=music_models.Track.objects.annotate(
-            files_count=Count('files')
-        ).filter(files_count__gt=0)
+        queryset=music_models.Track.objects.annotate(files_count=Count("files")).filter(
+            files_count__gt=0
+        )
     )
 
     def create(self, data):
         return history_models.Listening.objects.create(
-            user=self.context['user'],
-            track=data['id'],
+            user=self.context["user"], track=data["id"]
         )
diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py
index cc75b5279e8d9be15cf97a3339f389710db75043..bb5f44166875f34ee7b625c60d93dd004d9bc6a0 100644
--- a/api/funkwhale_api/subsonic/views.py
+++ b/api/funkwhale_api/subsonic/views.py
@@ -2,12 +2,9 @@ import datetime
 
 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
-from rest_framework import response
-from rest_framework import viewsets
+from rest_framework import renderers, response, viewsets
 from rest_framework.decorators import list_route
 from rest_framework.serializers import ValidationError
 
@@ -19,50 +16,58 @@ from funkwhale_api.music import utils
 from funkwhale_api.music import views as music_views
 from funkwhale_api.playlists import models as playlists_models
 
-from . import authentication
-from . import filters
-from . import negotiation
-from . import serializers
+from . import authentication, filters, negotiation, serializers
 
 
-def find_object(queryset, model_field='pk', field='id', cast=int):
+def find_object(queryset, model_field="pk", field="id", cast=int):
     def decorator(func):
         def inner(self, request, *args, **kwargs):
             data = request.GET or request.POST
             try:
                 raw_value = data[field]
             except KeyError:
-                return response.Response({
-                    'error': {
-                        'code': 10,
-                        'message': "required parameter '{}' not present".format(field)
+                return response.Response(
+                    {
+                        "error": {
+                            "code": 10,
+                            "message": "required parameter '{}' not present".format(
+                                field
+                            ),
+                        }
                     }
-                })
+                )
             try:
                 value = cast(raw_value)
             except (TypeError, ValidationError):
-                return response.Response({
-                    'error': {
-                        'code': 0,
-                        'message': 'For input string "{}"'.format(raw_value)
+                return response.Response(
+                    {
+                        "error": {
+                            "code": 0,
+                            "message": 'For input string "{}"'.format(raw_value),
+                        }
                     }
-                })
+                )
             qs = queryset
-            if hasattr(qs, '__call__'):
+            if hasattr(qs, "__call__"):
                 qs = qs(request)
             try:
                 obj = qs.get(**{model_field: value})
             except qs.model.DoesNotExist:
-                return response.Response({
-                    'error': {
-                        'code': 70,
-                        'message': '{} not found'.format(
-                            qs.model.__class__.__name__)
+                return response.Response(
+                    {
+                        "error": {
+                            "code": 70,
+                            "message": "{} not found".format(
+                                qs.model.__class__.__name__
+                            ),
+                        }
                     }
-                })
-            kwargs['obj'] = obj
+                )
+            kwargs["obj"] = obj
             return func(self, request, *args, **kwargs)
+
         return inner
+
     return decorator
 
 
@@ -72,10 +77,10 @@ class SubsonicViewSet(viewsets.GenericViewSet):
     permissions_classes = [rest_permissions.IsAuthenticated]
 
     def dispatch(self, request, *args, **kwargs):
-        if not preferences.get('subsonic__enabled'):
+        if not preferences.get("subsonic__enabled"):
             r = response.Response({}, status=405)
             r.accepted_renderer = renderers.JSONRenderer()
-            r.accepted_media_type = 'application/json'
+            r.accepted_media_type = "application/json"
             r.renderer_context = {}
             return r
         return super().dispatch(request, *args, **kwargs)
@@ -83,261 +88,187 @@ class SubsonicViewSet(viewsets.GenericViewSet):
     def handle_exception(self, exc):
         # subsonic API sends 200 status code with custom error
         # codes in the payload
-        mapping = {
-            exceptions.AuthenticationFailed: (
-                40, 'Wrong username or password.'
-            )
-        }
-        payload = {
-            'status': 'failed'
-        }
+        mapping = {exceptions.AuthenticationFailed: (40, "Wrong username or password.")}
+        payload = {"status": "failed"}
         if exc.__class__ in mapping:
             code, message = mapping[exc.__class__]
         else:
             return super().handle_exception(exc)
-        payload['error'] = {
-            'code': code,
-            'message': message
-        }
+        payload["error"] = {"code": code, "message": message}
 
         return response.Response(payload, status=200)
 
-    @list_route(
-        methods=['get', 'post'],
-        permission_classes=[])
+    @list_route(methods=["get", "post"], permission_classes=[])
     def ping(self, request, *args, **kwargs):
-        data = {
-            'status': 'ok',
-            'version': '1.16.0'
-        }
+        data = {"status": "ok", "version": "1.16.0"}
         return response.Response(data, status=200)
 
     @list_route(
-        methods=['get', 'post'],
-        url_name='get_license',
+        methods=["get", "post"],
+        url_name="get_license",
         permissions_classes=[],
-        url_path='getLicense')
+        url_path="getLicense",
+    )
     def get_license(self, request, *args, **kwargs):
         now = timezone.now()
         data = {
-            'status': 'ok',
-            'version': '1.16.0',
-            'license': {
-                'valid': 'true',
-                'email': 'valid@valid.license',
-                'licenseExpires': now + datetime.timedelta(days=365)
-            }
+            "status": "ok",
+            "version": "1.16.0",
+            "license": {
+                "valid": "true",
+                "email": "valid@valid.license",
+                "licenseExpires": now + datetime.timedelta(days=365),
+            },
         }
         return response.Response(data, status=200)
 
-    @list_route(
-        methods=['get', 'post'],
-        url_name='get_artists',
-        url_path='getArtists')
+    @list_route(methods=["get", "post"], url_name="get_artists", url_path="getArtists")
     def get_artists(self, request, *args, **kwargs):
         artists = music_models.Artist.objects.all()
         data = serializers.GetArtistsSerializer(artists).data
-        payload = {
-            'artists': data
-        }
+        payload = {"artists": data}
 
         return response.Response(payload, status=200)
 
-    @list_route(
-        methods=['get', 'post'],
-        url_name='get_indexes',
-        url_path='getIndexes')
+    @list_route(methods=["get", "post"], url_name="get_indexes", url_path="getIndexes")
     def get_indexes(self, request, *args, **kwargs):
         artists = music_models.Artist.objects.all()
         data = serializers.GetArtistsSerializer(artists).data
-        payload = {
-            'indexes': data
-        }
+        payload = {"indexes": data}
 
         return response.Response(payload, status=200)
 
-    @list_route(
-        methods=['get', 'post'],
-        url_name='get_artist',
-        url_path='getArtist')
+    @list_route(methods=["get", "post"], url_name="get_artist", url_path="getArtist")
     @find_object(music_models.Artist.objects.all())
     def get_artist(self, request, *args, **kwargs):
-        artist = kwargs.pop('obj')
+        artist = kwargs.pop("obj")
         data = serializers.GetArtistSerializer(artist).data
-        payload = {
-            'artist': data
-        }
+        payload = {"artist": data}
 
         return response.Response(payload, status=200)
 
     @list_route(
-        methods=['get', 'post'],
-        url_name='get_artist_info2',
-        url_path='getArtistInfo2')
+        methods=["get", "post"], url_name="get_artist_info2", url_path="getArtistInfo2"
+    )
     @find_object(music_models.Artist.objects.all())
     def get_artist_info2(self, request, *args, **kwargs):
-        artist = kwargs.pop('obj')
-        payload = {
-            'artist-info2': {}
-        }
+        payload = {"artist-info2": {}}
 
         return response.Response(payload, status=200)
 
-    @list_route(
-        methods=['get', 'post'],
-        url_name='get_album',
-        url_path='getAlbum')
-    @find_object(
-        music_models.Album.objects.select_related('artist'))
+    @list_route(methods=["get", "post"], url_name="get_album", url_path="getAlbum")
+    @find_object(music_models.Album.objects.select_related("artist"))
     def get_album(self, request, *args, **kwargs):
-        album = kwargs.pop('obj')
+        album = kwargs.pop("obj")
         data = serializers.GetAlbumSerializer(album).data
-        payload = {
-            'album': data
-        }
+        payload = {"album": data}
         return response.Response(payload, status=200)
 
-    @list_route(
-        methods=['get', 'post'],
-        url_name='stream',
-        url_path='stream')
-    @find_object(
-        music_models.Track.objects.all())
+    @list_route(methods=["get", "post"], url_name="stream", url_path="stream")
+    @find_object(music_models.Track.objects.all())
     def stream(self, request, *args, **kwargs):
-        track = kwargs.pop('obj')
+        track = kwargs.pop("obj")
         queryset = track.files.select_related(
-            'library_track',
-            'track__album__artist',
-            'track__artist',
+            "library_track", "track__album__artist", "track__artist"
         )
         track_file = queryset.first()
         if not track_file:
             return response.Response(status=404)
         return music_views.handle_serve(track_file)
 
-    @list_route(
-        methods=['get', 'post'],
-        url_name='star',
-        url_path='star')
-    @find_object(
-        music_models.Track.objects.all())
+    @list_route(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')
+        track = kwargs.pop("obj")
         TrackFavorite.add(user=request.user, track=track)
-        return response.Response({'status': 'ok'})
+        return response.Response({"status": "ok"})
 
-    @list_route(
-        methods=['get', 'post'],
-        url_name='unstar',
-        url_path='unstar')
-    @find_object(
-        music_models.Track.objects.all())
+    @list_route(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')
+        track = kwargs.pop("obj")
         request.user.track_favorites.filter(track=track).delete()
-        return response.Response({'status': 'ok'})
+        return response.Response({"status": "ok"})
 
     @list_route(
-        methods=['get', 'post'],
-        url_name='get_starred2',
-        url_path='getStarred2')
+        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)
-            }
-        }
+        data = {"starred2": {"song": serializers.get_starred_tracks_data(favorites)}}
         return response.Response(data)
 
-    @list_route(
-        methods=['get', 'post'],
-        url_name='get_starred',
-        url_path='getStarred')
+    @list_route(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)
-            }
-        }
+        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')
+        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()
+        queryset = music_models.Album.objects.with_tracks_count().order_by(
+            "artist__name"
+        )
         data = request.GET or request.POST
         filterset = filters.AlbumList2FilterSet(data, queryset=queryset)
         queryset = filterset.qs
         try:
-            offset = int(data['offset'])
+            offset = int(data["offset"])
         except (TypeError, KeyError, ValueError):
             offset = 0
 
         try:
-            size = int(data['size'])
+            size = int(data["size"])
         except (TypeError, KeyError, ValueError):
             size = 50
 
         size = min(size, 500)
-        queryset = queryset[offset:size]
-        data = {
-            'albumList2': {
-                'album': serializers.get_album_list2_data(queryset)
-            }
-        }
+        queryset = queryset[offset : offset + size]
+        data = {"albumList2": {"album": serializers.get_album_list2_data(queryset)}}
         return response.Response(data)
 
-    @list_route(
-        methods=['get', 'post'],
-        url_name='search3',
-        url_path='search3')
+    @list_route(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('*', '')
+        query = str(data.get("query", "")).replace("*", "")
         conf = [
             {
-                'subsonic': 'artist',
-                'search_fields': ['name'],
-                'queryset': (
-                    music_models.Artist.objects
-                                       .with_albums_count()
-                                       .values('id', '_albums_count', 'name')
+                "subsonic": "artist",
+                "search_fields": ["name"],
+                "queryset": (
+                    music_models.Artist.objects.with_albums_count().values(
+                        "id", "_albums_count", "name"
+                    )
                 ),
-                'serializer': lambda qs: [
-                    serializers.get_artist_data(a) for a in qs
-                ]
+                "serializer": lambda qs: [serializers.get_artist_data(a) for a in qs],
             },
             {
-                'subsonic': 'album',
-                'search_fields': ['title'],
-                'queryset': (
-                    music_models.Album.objects
-                                .with_tracks_count()
-                                .select_related('artist')
+                "subsonic": "album",
+                "search_fields": ["title"],
+                "queryset": (
+                    music_models.Album.objects.with_tracks_count().select_related(
+                        "artist"
+                    )
                 ),
-                'serializer': serializers.get_album_list2_data,
+                "serializer": serializers.get_album_list2_data,
             },
             {
-                'subsonic': 'song',
-                'search_fields': ['title'],
-                'queryset': (
-                    music_models.Track.objects
-                                .prefetch_related('files')
-                                .select_related('album__artist')
+                "subsonic": "song",
+                "search_fields": ["title"],
+                "queryset": (
+                    music_models.Track.objects.prefetch_related("files").select_related(
+                        "album__artist"
+                    )
                 ),
-                'serializer': serializers.get_song_list_data,
+                "serializer": serializers.get_song_list_data,
             },
         ]
-        payload = {
-            'searchResult3': {}
-        }
+        payload = {"searchResult3": {}}
         for c in conf:
-            offsetKey = '{}Offset'.format(c['subsonic'])
-            countKey = '{}Count'.format(c['subsonic'])
+            offsetKey = "{}Offset".format(c["subsonic"])
+            countKey = "{}Count".format(c["subsonic"])
             try:
                 offset = int(data[offsetKey])
             except (TypeError, KeyError, ValueError):
@@ -349,60 +280,49 @@ class SubsonicViewSet(viewsets.GenericViewSet):
                 size = 20
 
             size = min(size, 100)
-            queryset = c['queryset']
+            queryset = c["queryset"]
             if query:
-                queryset = c['queryset'].filter(
-                    utils.get_query(query, c['search_fields'])
+                queryset = c["queryset"].filter(
+                    utils.get_query(query, c["search_fields"])
                 )
-            queryset = queryset[offset:size]
-            payload['searchResult3'][c['subsonic']] = c['serializer'](queryset)
+            queryset = queryset[offset : offset + size]
+            payload["searchResult3"][c["subsonic"]] = c["serializer"](queryset)
         return response.Response(payload)
 
     @list_route(
-        methods=['get', 'post'],
-        url_name='get_playlists',
-        url_path='getPlaylists')
+        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'
-        )
+        playlists = request.user.playlists.with_tracks_count().select_related("user")
         data = {
-            'playlists': {
-                'playlist': [
-                    serializers.get_playlist_data(p) for p in playlists]
+            "playlists": {
+                "playlist": [serializers.get_playlist_data(p) for p in playlists]
             }
         }
         return response.Response(data)
 
     @list_route(
-        methods=['get', 'post'],
-        url_name='get_playlist',
-        url_path='getPlaylist')
-    @find_object(
-        playlists_models.Playlist.objects.with_tracks_count())
+        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):
-        playlist = kwargs.pop('obj')
-        data = {
-            'playlist': serializers.get_playlist_detail_data(playlist)
-        }
+        playlist = kwargs.pop("obj")
+        data = {"playlist": serializers.get_playlist_detail_data(playlist)}
         return response.Response(data)
 
     @list_route(
-        methods=['get', 'post'],
-        url_name='update_playlist',
-        url_path='updatePlaylist')
-    @find_object(
-        lambda request: request.user.playlists.all(),
-        field='playlistId')
+        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):
-        playlist = kwargs.pop('obj')
+        playlist = kwargs.pop("obj")
         data = request.GET or request.POST
-        new_name = data.get('name', '')
+        new_name = data.get("name", "")
         if new_name:
             playlist.name = new_name
-            playlist.save(update_fields=['name', 'modification_date'])
+            playlist.save(update_fields=["name", "modification_date"])
         try:
-            to_remove = int(data['songIndexToRemove'])
+            to_remove = int(data["songIndexToRemove"])
             plt = playlist.playlist_tracks.get(index=to_remove)
         except (TypeError, ValueError, KeyError):
             pass
@@ -412,7 +332,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
             plt.delete(update_indexes=True)
 
         ids = []
-        for i in data.getlist('songIdToAdd'):
+        for i in data.getlist("songIdToAdd"):
             try:
                 ids.append(int(i))
             except (TypeError, ValueError):
@@ -429,45 +349,38 @@ class SubsonicViewSet(viewsets.GenericViewSet):
             if sorted_tracks:
                 playlist.insert_many(sorted_tracks)
 
-        data = {
-            'status': 'ok'
-        }
+        data = {"status": "ok"}
         return response.Response(data)
 
     @list_route(
-        methods=['get', 'post'],
-        url_name='delete_playlist',
-        url_path='deletePlaylist')
-    @find_object(
-        lambda request: request.user.playlists.all())
+        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):
-        playlist = kwargs.pop('obj')
+        playlist = kwargs.pop("obj")
         playlist.delete()
-        data = {
-            'status': 'ok'
-        }
+        data = {"status": "ok"}
         return response.Response(data)
 
     @list_route(
-        methods=['get', 'post'],
-        url_name='create_playlist',
-        url_path='createPlaylist')
+        methods=["get", "post"], url_name="create_playlist", url_path="createPlaylist"
+    )
     def create_playlist(self, request, *args, **kwargs):
         data = request.GET or request.POST
-        name = data.get('name', '')
+        name = data.get("name", "")
         if not name:
-            return response.Response({
-                'error': {
-                    'code': 10,
-                    'message': 'Playlist ID or name must be specified.'
+            return response.Response(
+                {
+                    "error": {
+                        "code": 10,
+                        "message": "Playlist ID or name must be specified.",
+                    }
                 }
-            })
+            )
 
-        playlist = request.user.playlists.create(
-            name=name
-        )
+        playlist = request.user.playlists.create(name=name)
         ids = []
-        for i in data.getlist('songId'):
+        for i in data.getlist("songId"):
             try:
                 ids.append(int(i))
             except (TypeError, ValueError):
@@ -484,92 +397,67 @@ class SubsonicViewSet(viewsets.GenericViewSet):
                     pass
             if sorted_tracks:
                 playlist.insert_many(sorted_tracks)
-        playlist = request.user.playlists.with_tracks_count().get(
-            pk=playlist.pk)
-        data = {
-            'playlist': serializers.get_playlist_detail_data(playlist)
-        }
+        playlist = request.user.playlists.with_tracks_count().get(pk=playlist.pk)
+        data = {"playlist": serializers.get_playlist_detail_data(playlist)}
         return response.Response(data)
 
     @list_route(
-        methods=['get', 'post'],
-        url_name='get_music_folders',
-        url_path='getMusicFolders')
+        methods=["get", "post"],
+        url_name="get_music_folders",
+        url_path="getMusicFolders",
+    )
     def get_music_folders(self, request, *args, **kwargs):
-        data = {
-            'musicFolders': {
-                'musicFolder': [{
-                    'id': 1,
-                    'name': 'Music'
-                }]
-            }
-        }
+        data = {"musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]}}
         return response.Response(data)
 
     @list_route(
-        methods=['get', 'post'],
-        url_name='get_cover_art',
-        url_path='getCoverArt')
+        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
-        id = data.get('id', '')
+        id = data.get("id", "")
         if not id:
-            return response.Response({
-                'error': {
-                    'code': 10,
-                    'message': 'cover art ID must be specified.'
-                }
-            })
+            return response.Response(
+                {"error": {"code": 10, "message": "cover art ID must be specified."}}
+            )
 
-        if id.startswith('al-'):
+        if id.startswith("al-"):
             try:
-                album_id = int(id.replace('al-', ''))
-                album = music_models.Album.objects.exclude(
-                    cover__isnull=True
-                ).exclude(cover='').get(pk=album_id)
+                album_id = int(id.replace("al-", ""))
+                album = (
+                    music_models.Album.objects.exclude(cover__isnull=True)
+                    .exclude(cover="")
+                    .get(pk=album_id)
+                )
             except (TypeError, ValueError, music_models.Album.DoesNotExist):
-                return response.Response({
-                    'error': {
-                        'code': 70,
-                        'message': 'cover art not found.'
-                    }
-                })
+                return response.Response(
+                    {"error": {"code": 70, "message": "cover art not found."}}
+                )
             cover = album.cover
         else:
-            return response.Response({
-                'error': {
-                    'code': 70,
-                    'message': 'cover art not found.'
-                }
-            })
+            return response.Response(
+                {"error": {"code": 70, "message": "cover art not found."}}
+            )
 
-        mapping = {
-            'nginx': 'X-Accel-Redirect',
-            'apache2': 'X-Sendfile',
-        }
+        mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
         path = music_views.get_file_path(cover)
         file_header = mapping[settings.REVERSE_PROXY_TYPE]
         # let the proxy set the content-type
-        r = response.Response({}, content_type='')
+        r = response.Response({}, content_type="")
         r[file_header] = path
         return r
 
-    @list_route(
-        methods=['get', 'post'],
-        url_name='scrobble',
-        url_path='scrobble')
+    @list_route(methods=["get", "post"], url_name="scrobble", url_path="scrobble")
     def scrobble(self, request, *args, **kwargs):
         data = request.GET or request.POST
         serializer = serializers.ScrobbleSerializer(
-            data=data, context={'user': request.user})
+            data=data, context={"user": request.user}
+        )
         if not serializer.is_valid():
-            return response.Response({
-                'error': {
-                    'code': 0,
-                    'message': 'Invalid payload'
-                }
-            })
-        if serializer.validated_data['submission']:
-            l = serializer.save()
-            record.send(l)
+            return response.Response(
+                {"error": {"code": 0, "message": "Invalid payload"}}
+            )
+        if serializer.validated_data["submission"]:
+            listening = serializer.save()
+            record.send(listening)
         return response.Response({})
diff --git a/api/funkwhale_api/taskapp/celery.py b/api/funkwhale_api/taskapp/celery.py
index 60b09bece8a6bb2e7aa457383602279431b0575e..98e980f07273c640c08871bf8de0c42abcd344a1 100644
--- a/api/funkwhale_api/taskapp/celery.py
+++ b/api/funkwhale_api/taskapp/celery.py
@@ -1,29 +1,31 @@
 
 from __future__ import absolute_import
-import os
+
 import functools
+import os
 
 from celery import Celery
 from django.apps import AppConfig
 from django.conf import settings
 
-
 if not settings.configured:
     # set the default Django settings module for the 'celery' program.
-    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")  # pragma: no cover
+    os.environ.setdefault(
+        "DJANGO_SETTINGS_MODULE", "config.settings.local"
+    )  # pragma: no cover
 
 
-app = Celery('funkwhale_api')
+app = Celery("funkwhale_api")
 
 
 class CeleryConfig(AppConfig):
-    name = 'funkwhale_api.taskapp'
-    verbose_name = 'Celery Config'
+    name = "funkwhale_api.taskapp"
+    verbose_name = "Celery Config"
 
     def ready(self):
         # Using a string here means the worker will not have to
         # pickle the object when using Windows.
-        app.config_from_object('django.conf:settings', namespace='CELERY')
+        app.config_from_object("django.conf:settings", namespace="CELERY")
         app.autodiscover_tasks(lambda: settings.INSTALLED_APPS, force=True)
 
 
@@ -31,7 +33,7 @@ def require_instance(model_or_qs, parameter_name, id_kwarg_name=None):
     def decorator(function):
         @functools.wraps(function)
         def inner(*args, **kwargs):
-            kw = id_kwarg_name or '_'.join([parameter_name, 'id'])
+            kw = id_kwarg_name or "_".join([parameter_name, "id"])
             pk = kwargs.pop(kw)
             try:
                 instance = model_or_qs.get(pk=pk)
@@ -39,5 +41,7 @@ def require_instance(model_or_qs, parameter_name, id_kwarg_name=None):
                 instance = model_or_qs.objects.get(pk=pk)
             kwargs[parameter_name] = instance
             return function(*args, **kwargs)
+
         return inner
+
     return decorator
diff --git a/api/funkwhale_api/users/adapters.py b/api/funkwhale_api/users/adapters.py
index 7bd341d14e07062376dbc22a16559a6ff23d9321..6d8c365d52be08db4551dc05044dd6cbb63cfa06 100644
--- a/api/funkwhale_api/users/adapters.py
+++ b/api/funkwhale_api/users/adapters.py
@@ -1,15 +1,13 @@
-from django.conf import settings
-
 from allauth.account.adapter import DefaultAccountAdapter
+from django.conf import settings
 from dynamic_preferences.registries import global_preferences_registry
 
 
 class FunkwhaleAccountAdapter(DefaultAccountAdapter):
-
     def is_open_for_signup(self, request):
         manager = global_preferences_registry.manager()
-        return manager['users__registration_enabled']
+        return manager["users__registration_enabled"]
 
     def send_mail(self, template_prefix, email, context):
-        context['funkwhale_url'] = settings.FUNKWHALE_URL
+        context["funkwhale_url"] = settings.FUNKWHALE_URL
         return super().send_mail(template_prefix, email, context)
diff --git a/api/funkwhale_api/users/admin.py b/api/funkwhale_api/users/admin.py
index cb74abf0e737509bc1fd94eb22e114902513a9f9..5c694ab0ee962c2977dd227c709861dfef93098f 100644
--- a/api/funkwhale_api/users/admin.py
+++ b/api/funkwhale_api/users/admin.py
@@ -17,9 +17,9 @@ class MyUserChangeForm(UserChangeForm):
 
 class MyUserCreationForm(UserCreationForm):
 
-    error_message = UserCreationForm.error_messages.update({
-        'duplicate_username': 'This username has already been taken.'
-    })
+    error_message = UserCreationForm.error_messages.update(
+        {"duplicate_username": "This username has already been taken."}
+    )
 
     class Meta(UserCreationForm.Meta):
         model = User
@@ -30,7 +30,7 @@ class MyUserCreationForm(UserCreationForm):
             User.objects.get(username=username)
         except User.DoesNotExist:
             return username
-        raise forms.ValidationError(self.error_messages['duplicate_username'])
+        raise forms.ValidationError(self.error_messages["duplicate_username"])
 
 
 @admin.register(User)
@@ -38,38 +38,39 @@ class UserAdmin(AuthUserAdmin):
     form = MyUserChangeForm
     add_form = MyUserCreationForm
     list_display = [
-        'username',
-        'email',
-        'date_joined',
-        'last_login',
-        'is_staff',
-        'is_superuser',
+        "username",
+        "email",
+        "date_joined",
+        "last_login",
+        "is_staff",
+        "is_superuser",
     ]
     list_filter = [
-        'is_superuser',
-        'is_staff',
-        'privacy_level',
-        'permission_settings',
-        'permission_library',
-        'permission_federation',
+        "is_superuser",
+        "is_staff",
+        "privacy_level",
+        "permission_settings",
+        "permission_library",
+        "permission_federation",
     ]
 
     fieldsets = (
-        (None, {'fields': ('username', 'password', 'privacy_level')}),
-        (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
-        (_('Permissions'), {
-            'fields': (
-                'is_active',
-                'is_staff',
-                'is_superuser',
-                'permission_upload',
-                'permission_library',
-                'permission_settings',
-                'permission_federation')}),
-        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
-        (_('Useless fields'), {
-            'fields': (
-                'user_permissions',
-                'groups',
-            )})
-        )
+        (None, {"fields": ("username", "password", "privacy_level")}),
+        (_("Personal info"), {"fields": ("first_name", "last_name", "email")}),
+        (
+            _("Permissions"),
+            {
+                "fields": (
+                    "is_active",
+                    "is_staff",
+                    "is_superuser",
+                    "permission_upload",
+                    "permission_library",
+                    "permission_settings",
+                    "permission_federation",
+                )
+            },
+        ),
+        (_("Important dates"), {"fields": ("last_login", "date_joined")}),
+        (_("Useless fields"), {"fields": ("user_permissions", "groups")}),
+    )
diff --git a/api/funkwhale_api/users/api_urls.py b/api/funkwhale_api/users/api_urls.py
index 8aba7f1a8b2777e970a260f2a4dd80805214e4e1..267ee2d69ad6dc1dd9d1b874072e415a9bdda41d 100644
--- a/api/funkwhale_api/users/api_urls.py
+++ b/api/funkwhale_api/users/api_urls.py
@@ -1,7 +1,8 @@
 from rest_framework import routers
+
 from . import views
 
 router = routers.SimpleRouter()
-router.register(r'users', views.UserViewSet, 'users')
+router.register(r"users", views.UserViewSet, "users")
 
 urlpatterns = router.urls
diff --git a/api/funkwhale_api/users/dynamic_preferences_registry.py b/api/funkwhale_api/users/dynamic_preferences_registry.py
index 7108360b9a6d68be3afa827d804245a87bc3d8a4..08f5730a81f7f9791fbc109eb14321f3de9dad64 100644
--- a/api/funkwhale_api/users/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/users/dynamic_preferences_registry.py
@@ -5,36 +5,26 @@ from funkwhale_api.common import preferences as common_preferences
 
 from . import models
 
-users = types.Section('users')
+users = types.Section("users")
 
 
 @global_preferences_registry.register
 class RegistrationEnabled(types.BooleanPreference):
     show_in_api = True
     section = users
-    name = 'registration_enabled'
+    name = "registration_enabled"
     default = False
-    verbose_name = 'Open registrations to new users'
-    help_text = (
-        'When enabled, new users will be able to register on this instance.'
-    )
+    verbose_name = "Open registrations to new users"
+    help_text = "When enabled, new users will be able to register on this instance."
 
 
 @global_preferences_registry.register
 class DefaultPermissions(common_preferences.StringListPreference):
     show_in_api = True
     section = users
-    name = 'default_permissions'
+    name = "default_permissions"
     default = []
-    verbose_name = 'Default permissions'
-    help_text = (
-        'A list of default preferences to give to all registered users.'
-    )
-    choices = [
-        (k, c['label'])
-        for k, c in models.PERMISSIONS_CONFIGURATION.items()
-    ]
-    field_kwargs = {
-        'choices': choices,
-        'required': False,
-    }
+    verbose_name = "Default permissions"
+    help_text = "A list of default preferences to give to all registered users."
+    choices = [(k, c["label"]) for k, c in models.PERMISSIONS_CONFIGURATION.items()]
+    field_kwargs = {"choices": choices, "required": False}
diff --git a/api/funkwhale_api/users/factories.py b/api/funkwhale_api/users/factories.py
index cd28f44073ded3ad425ee3c1f84a7c4b6dcabec6..eed8c7175a2dacdb65404aaf827c2bee04582dbb 100644
--- a/api/funkwhale_api/users/factories.py
+++ b/api/funkwhale_api/users/factories.py
@@ -1,15 +1,15 @@
 import factory
-
-from funkwhale_api.factories import registry, ManyToManyFromList
 from django.contrib.auth.models import Permission
 
+from funkwhale_api.factories import ManyToManyFromList, registry
+
 
 @registry.register
 class GroupFactory(factory.django.DjangoModelFactory):
-    name = factory.Sequence(lambda n: 'group-{0}'.format(n))
+    name = factory.Sequence(lambda n: "group-{0}".format(n))
 
     class Meta:
-        model = 'auth.Group'
+        model = "auth.Group"
 
     @factory.post_generation
     def perms(self, create, extracted, **kwargs):
@@ -20,8 +20,7 @@ class GroupFactory(factory.django.DjangoModelFactory):
         if extracted:
             perms = [
                 Permission.objects.get(
-                    content_type__app_label=p.split('.')[0],
-                    codename=p.split('.')[1],
+                    content_type__app_label=p.split(".")[0], codename=p.split(".")[1]
                 )
                 for p in extracted
             ]
@@ -31,15 +30,15 @@ class GroupFactory(factory.django.DjangoModelFactory):
 
 @registry.register
 class UserFactory(factory.django.DjangoModelFactory):
-    username = factory.Sequence(lambda n: 'user-{0}'.format(n))
-    email = factory.Sequence(lambda n: 'user-{0}@example.com'.format(n))
-    password = factory.PostGenerationMethodCall('set_password', 'test')
+    username = factory.Sequence(lambda n: "user-{0}".format(n))
+    email = factory.Sequence(lambda n: "user-{0}@example.com".format(n))
+    password = factory.PostGenerationMethodCall("set_password", "test")
     subsonic_api_token = None
-    groups = ManyToManyFromList('groups')
+    groups = ManyToManyFromList("groups")
 
     class Meta:
-        model = 'users.User'
-        django_get_or_create = ('username', )
+        model = "users.User"
+        django_get_or_create = ("username",)
 
     @factory.post_generation
     def perms(self, create, extracted, **kwargs):
@@ -50,8 +49,7 @@ class UserFactory(factory.django.DjangoModelFactory):
         if extracted:
             perms = [
                 Permission.objects.get(
-                    content_type__app_label=p.split('.')[0],
-                    codename=p.split('.')[1],
+                    content_type__app_label=p.split(".")[0], codename=p.split(".")[1]
                 )
                 for p in extracted
             ]
@@ -59,7 +57,7 @@ class UserFactory(factory.django.DjangoModelFactory):
             self.user_permissions.add(*perms)
 
 
-@registry.register(name='users.SuperUser')
+@registry.register(name="users.SuperUser")
 class SuperUserFactory(UserFactory):
     is_staff = True
     is_superuser = True
diff --git a/api/funkwhale_api/users/migrations/0001_initial.py b/api/funkwhale_api/users/migrations/0001_initial.py
index ef9240c9158d795b0e1201f176ec14e8df5b5e86..cc8307c882dd52ecfed12ca82da4a0692e4736c4 100644
--- a/api/funkwhale_api/users/migrations/0001_initial.py
+++ b/api/funkwhale_api/users/migrations/0001_initial.py
@@ -9,36 +9,129 @@ import django.core.validators
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('auth', '0006_require_contenttypes_0002'),
-    ]
+    dependencies = [("auth", "0006_require_contenttypes_0002")]
 
     operations = [
         migrations.CreateModel(
-            name='User',
+            name="User",
             fields=[
-                ('id', models.AutoField(primary_key=True, verbose_name='ID', serialize=False, auto_created=True)),
-                ('password', models.CharField(max_length=128, verbose_name='password')),
-                ('last_login', models.DateTimeField(null=True, verbose_name='last login', blank=True)),
-                ('is_superuser', models.BooleanField(help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status', default=False)),
-                ('username', models.CharField(max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], verbose_name='username', error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True)),
-                ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
-                ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
-                ('email', models.EmailField(max_length=254, verbose_name='email address', blank=True)),
-                ('is_staff', models.BooleanField(help_text='Designates whether the user can log into this admin site.', verbose_name='staff status', default=False)),
-                ('is_active', models.BooleanField(help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active', default=True)),
-                ('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)),
-                ('groups', models.ManyToManyField(related_name='user_set', blank=True, verbose_name='groups', to='auth.Group', help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_query_name='user')),
-                ('user_permissions', models.ManyToManyField(related_name='user_set', blank=True, verbose_name='user permissions', to='auth.Permission', help_text='Specific permissions for this user.', related_query_name='user')),
-                ('name', models.CharField(max_length=255, verbose_name='Name of User', blank=True)),
+                (
+                    "id",
+                    models.AutoField(
+                        primary_key=True,
+                        verbose_name="ID",
+                        serialize=False,
+                        auto_created=True,
+                    ),
+                ),
+                ("password", models.CharField(max_length=128, verbose_name="password")),
+                (
+                    "last_login",
+                    models.DateTimeField(
+                        null=True, verbose_name="last login", blank=True
+                    ),
+                ),
+                (
+                    "is_superuser",
+                    models.BooleanField(
+                        help_text="Designates that this user has all permissions without explicitly assigning them.",
+                        verbose_name="superuser status",
+                        default=False,
+                    ),
+                ),
+                (
+                    "username",
+                    models.CharField(
+                        max_length=30,
+                        validators=[
+                            django.core.validators.RegexValidator(
+                                "^[\\w.@+-]+$",
+                                "Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.",
+                                "invalid",
+                            )
+                        ],
+                        verbose_name="username",
+                        error_messages={
+                            "unique": "A user with that username already exists."
+                        },
+                        help_text="Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.",
+                        unique=True,
+                    ),
+                ),
+                (
+                    "first_name",
+                    models.CharField(
+                        max_length=30, verbose_name="first name", blank=True
+                    ),
+                ),
+                (
+                    "last_name",
+                    models.CharField(
+                        max_length=30, verbose_name="last name", blank=True
+                    ),
+                ),
+                (
+                    "email",
+                    models.EmailField(
+                        max_length=254, verbose_name="email address", blank=True
+                    ),
+                ),
+                (
+                    "is_staff",
+                    models.BooleanField(
+                        help_text="Designates whether the user can log into this admin site.",
+                        verbose_name="staff status",
+                        default=False,
+                    ),
+                ),
+                (
+                    "is_active",
+                    models.BooleanField(
+                        help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
+                        verbose_name="active",
+                        default=True,
+                    ),
+                ),
+                (
+                    "date_joined",
+                    models.DateTimeField(
+                        verbose_name="date joined", default=django.utils.timezone.now
+                    ),
+                ),
+                (
+                    "groups",
+                    models.ManyToManyField(
+                        related_name="user_set",
+                        blank=True,
+                        verbose_name="groups",
+                        to="auth.Group",
+                        help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
+                        related_query_name="user",
+                    ),
+                ),
+                (
+                    "user_permissions",
+                    models.ManyToManyField(
+                        related_name="user_set",
+                        blank=True,
+                        verbose_name="user permissions",
+                        to="auth.Permission",
+                        help_text="Specific permissions for this user.",
+                        related_query_name="user",
+                    ),
+                ),
+                (
+                    "name",
+                    models.CharField(
+                        max_length=255, verbose_name="Name of User", blank=True
+                    ),
+                ),
             ],
             options={
-                'verbose_name': 'user',
-                'abstract': False,
-                'verbose_name_plural': 'users',
+                "verbose_name": "user",
+                "abstract": False,
+                "verbose_name_plural": "users",
             },
-            managers=[
-                ('objects', django.contrib.auth.models.UserManager()),
-            ],
-        ),
+            managers=[("objects", django.contrib.auth.models.UserManager())],
+        )
     ]
diff --git a/api/funkwhale_api/users/migrations/0002_auto_20171214_2205.py b/api/funkwhale_api/users/migrations/0002_auto_20171214_2205.py
index 4bbbaa62bcc44591449333ef4faa82e3fdd49c80..75fc22035a5e8693a1b9d3f6d62efd044c98293b 100644
--- a/api/funkwhale_api/users/migrations/0002_auto_20171214_2205.py
+++ b/api/funkwhale_api/users/migrations/0002_auto_20171214_2205.py
@@ -9,20 +9,23 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('users', '0001_initial'),
-    ]
+    dependencies = [("users", "0001_initial")]
 
     operations = [
         migrations.AlterModelManagers(
-            name='user',
-            managers=[
-                ('objects', django.contrib.auth.models.UserManager()),
-            ],
+            name="user",
+            managers=[("objects", django.contrib.auth.models.UserManager())],
         ),
         migrations.AlterField(
-            model_name='user',
-            name='username',
-            field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
+            model_name="user",
+            name="username",
+            field=models.CharField(
+                error_messages={"unique": "A user with that username already exists."},
+                help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
+                max_length=150,
+                unique=True,
+                validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
+                verbose_name="username",
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/users/migrations/0003_auto_20171226_1357.py b/api/funkwhale_api/users/migrations/0003_auto_20171226_1357.py
index fd75795d3fae3b63cb1e5f8830d1aaec7acf2118..62c038b7a8a6507cae8fc779dab64bdb4b0af68b 100644
--- a/api/funkwhale_api/users/migrations/0003_auto_20171226_1357.py
+++ b/api/funkwhale_api/users/migrations/0003_auto_20171226_1357.py
@@ -6,19 +6,19 @@ import uuid
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('users', '0002_auto_20171214_2205'),
-    ]
+    dependencies = [("users", "0002_auto_20171214_2205")]
 
     operations = [
         migrations.AddField(
-            model_name='user',
-            name='secret_key',
+            model_name="user",
+            name="secret_key",
             field=models.UUIDField(default=uuid.uuid4, null=True),
         ),
         migrations.AlterField(
-            model_name='user',
-            name='last_name',
-            field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
+            model_name="user",
+            name="last_name",
+            field=models.CharField(
+                blank=True, max_length=150, verbose_name="last name"
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/users/migrations/0004_user_privacy_level.py b/api/funkwhale_api/users/migrations/0004_user_privacy_level.py
index 81891eb0f0abc85657e6e16ec185c697c257cfe5..86b2c7581285fb3b1915b0b23cc7bf170676d198 100644
--- a/api/funkwhale_api/users/migrations/0004_user_privacy_level.py
+++ b/api/funkwhale_api/users/migrations/0004_user_privacy_level.py
@@ -5,14 +5,21 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('users', '0003_auto_20171226_1357'),
-    ]
+    dependencies = [("users", "0003_auto_20171226_1357")]
 
     operations = [
         migrations.AddField(
-            model_name='user',
-            name='privacy_level',
-            field=models.CharField(choices=[('me', 'Only me'), ('followers', 'Me and my followers'), ('instance', 'Everyone on my instance, and my followers'), ('everyone', 'Everyone, including people on other instances')], default='instance', max_length=30),
-        ),
+            model_name="user",
+            name="privacy_level",
+            field=models.CharField(
+                choices=[
+                    ("me", "Only me"),
+                    ("followers", "Me and my followers"),
+                    ("instance", "Everyone on my instance, and my followers"),
+                    ("everyone", "Everyone, including people on other instances"),
+                ],
+                default="instance",
+                max_length=30,
+            ),
+        )
     ]
diff --git a/api/funkwhale_api/users/migrations/0005_user_subsonic_api_token.py b/api/funkwhale_api/users/migrations/0005_user_subsonic_api_token.py
index 689b3ef7791943bc92d505aa49b6289bc2f52b14..65a1f1935fb1b3aaaea8a6e7037efce7c1542d81 100644
--- a/api/funkwhale_api/users/migrations/0005_user_subsonic_api_token.py
+++ b/api/funkwhale_api/users/migrations/0005_user_subsonic_api_token.py
@@ -5,14 +5,12 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('users', '0004_user_privacy_level'),
-    ]
+    dependencies = [("users", "0004_user_privacy_level")]
 
     operations = [
         migrations.AddField(
-            model_name='user',
-            name='subsonic_api_token',
+            model_name="user",
+            name="subsonic_api_token",
             field=models.CharField(blank=True, max_length=255, null=True),
-        ),
+        )
     ]
diff --git a/api/funkwhale_api/users/migrations/0006_auto_20180517_2324.py b/api/funkwhale_api/users/migrations/0006_auto_20180517_2324.py
index 7c9ab0fadc99016e8a62f609ace117ea0b941965..d5f6d911b8409db187c37c0c2106dfb29cc7a77c 100644
--- a/api/funkwhale_api/users/migrations/0006_auto_20180517_2324.py
+++ b/api/funkwhale_api/users/migrations/0006_auto_20180517_2324.py
@@ -5,24 +5,22 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('users', '0005_user_subsonic_api_token'),
-    ]
+    dependencies = [("users", "0005_user_subsonic_api_token")]
 
     operations = [
         migrations.AddField(
-            model_name='user',
-            name='permission_federation',
+            model_name="user",
+            name="permission_federation",
             field=models.BooleanField(default=False),
         ),
         migrations.AddField(
-            model_name='user',
-            name='permission_library',
+            model_name="user",
+            name="permission_library",
             field=models.BooleanField(default=False),
         ),
         migrations.AddField(
-            model_name='user',
-            name='permission_settings',
+            model_name="user",
+            name="permission_settings",
             field=models.BooleanField(default=False),
         ),
     ]
diff --git a/api/funkwhale_api/users/migrations/0007_auto_20180524_2009.py b/api/funkwhale_api/users/migrations/0007_auto_20180524_2009.py
index e3d582c53d10cb41d072efe4676327a2750ab5ce..218aa7e481ada04c687ef30d8b729c04ac9f6392 100644
--- a/api/funkwhale_api/users/migrations/0007_auto_20180524_2009.py
+++ b/api/funkwhale_api/users/migrations/0007_auto_20180524_2009.py
@@ -5,29 +5,37 @@ from django.db import migrations, models
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('users', '0006_auto_20180517_2324'),
-    ]
+    dependencies = [("users", "0006_auto_20180517_2324")]
 
     operations = [
         migrations.AddField(
-            model_name='user',
-            name='permission_upload',
-            field=models.BooleanField(default=False, verbose_name='Upload new content to the library'),
+            model_name="user",
+            name="permission_upload",
+            field=models.BooleanField(
+                default=False, verbose_name="Upload new content to the library"
+            ),
         ),
         migrations.AlterField(
-            model_name='user',
-            name='permission_federation',
-            field=models.BooleanField(default=False, help_text='Follow other instances, accept/deny library follow requests...', verbose_name='Manage library federation'),
+            model_name="user",
+            name="permission_federation",
+            field=models.BooleanField(
+                default=False,
+                help_text="Follow other instances, accept/deny library follow requests...",
+                verbose_name="Manage library federation",
+            ),
         ),
         migrations.AlterField(
-            model_name='user',
-            name='permission_library',
-            field=models.BooleanField(default=False, help_text='Manage library', verbose_name='Manage library'),
+            model_name="user",
+            name="permission_library",
+            field=models.BooleanField(
+                default=False, help_text="Manage library", verbose_name="Manage library"
+            ),
         ),
         migrations.AlterField(
-            model_name='user',
-            name='permission_settings',
-            field=models.BooleanField(default=False, verbose_name='Manage instance-level settings'),
+            model_name="user",
+            name="permission_settings",
+            field=models.BooleanField(
+                default=False, verbose_name="Manage instance-level settings"
+            ),
         ),
     ]
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index fcf78d0473efa38a00e7d8e1f12d1e4a0533768e..caf1e452bbcab42ff52587a50d35c6039fdf132c 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -1,42 +1,35 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, absolute_import
+from __future__ import absolute_import, unicode_literals
 
 import binascii
 import os
 import uuid
 
 from django.conf import settings
-from django.contrib.auth.models import AbstractUser, Permission
-from django.urls import reverse
+from django.contrib.auth.models import AbstractUser
 from django.db import models
+from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
 
-from funkwhale_api.common import fields
-from funkwhale_api.common import preferences
+from funkwhale_api.common import fields, preferences
 
 
 def get_token():
-    return binascii.b2a_hex(os.urandom(15)).decode('utf-8')
+    return binascii.b2a_hex(os.urandom(15)).decode("utf-8")
 
 
 PERMISSIONS_CONFIGURATION = {
-    'federation': {
-        'label': 'Manage library federation',
-        'help_text': 'Follow other instances, accept/deny library follow requests...',
-    },
-    'library': {
-        'label': 'Manage library',
-        'help_text': 'Manage library, delete files, tracks, artists, albums...',
-    },
-    'settings': {
-        'label': 'Manage instance-level settings',
-        'help_text': '',
+    "federation": {
+        "label": "Manage library federation",
+        "help_text": "Follow other instances, accept/deny library follow requests...",
     },
-    'upload': {
-        'label': 'Upload new content to the library',
-        'help_text': '',
+    "library": {
+        "label": "Manage library",
+        "help_text": "Manage library, delete files, tracks, artists, albums...",
     },
+    "settings": {"label": "Manage instance-level settings", "help_text": ""},
+    "upload": {"label": "Upload new content to the library", "help_text": ""},
 }
 
 PERMISSIONS = sorted(PERMISSIONS_CONFIGURATION.keys())
@@ -58,51 +51,55 @@ class User(AbstractUser):
     # anyway since django use stronger schemes for storing passwords.
     # Users that want to use the subsonic API from external client
     # should set this token and use it as their password in such clients
-    subsonic_api_token = models.CharField(
-        blank=True, null=True, max_length=255)
+    subsonic_api_token = models.CharField(blank=True, null=True, max_length=255)
 
     # permissions
     permission_federation = models.BooleanField(
-        PERMISSIONS_CONFIGURATION['federation']['label'],
-        help_text=PERMISSIONS_CONFIGURATION['federation']['help_text'],
-        default=False)
+        PERMISSIONS_CONFIGURATION["federation"]["label"],
+        help_text=PERMISSIONS_CONFIGURATION["federation"]["help_text"],
+        default=False,
+    )
     permission_library = models.BooleanField(
-        PERMISSIONS_CONFIGURATION['library']['label'],
-        help_text=PERMISSIONS_CONFIGURATION['library']['help_text'],
-        default=False)
+        PERMISSIONS_CONFIGURATION["library"]["label"],
+        help_text=PERMISSIONS_CONFIGURATION["library"]["help_text"],
+        default=False,
+    )
     permission_settings = models.BooleanField(
-        PERMISSIONS_CONFIGURATION['settings']['label'],
-        help_text=PERMISSIONS_CONFIGURATION['settings']['help_text'],
-        default=False)
+        PERMISSIONS_CONFIGURATION["settings"]["label"],
+        help_text=PERMISSIONS_CONFIGURATION["settings"]["help_text"],
+        default=False,
+    )
     permission_upload = models.BooleanField(
-        PERMISSIONS_CONFIGURATION['upload']['label'],
-        help_text=PERMISSIONS_CONFIGURATION['upload']['help_text'],
-        default=False)
+        PERMISSIONS_CONFIGURATION["upload"]["label"],
+        help_text=PERMISSIONS_CONFIGURATION["upload"]["help_text"],
+        default=False,
+    )
 
     def __str__(self):
         return self.username
 
     def get_permissions(self):
-        defaults = preferences.get('users__default_permissions')
+        defaults = preferences.get("users__default_permissions")
         perms = {}
         for p in PERMISSIONS:
             v = (
-                self.is_superuser or
-                getattr(self, 'permission_{}'.format(p)) or
-                p in defaults
+                self.is_superuser
+                or getattr(self, "permission_{}".format(p))
+                or p in defaults
             )
             perms[p] = v
         return perms
 
-    def has_permissions(self, *perms, operator='and'):
-        if operator not in ['and', 'or']:
-            raise ValueError('Invalid operator {}'.format(operator))
+    def has_permissions(self, *perms, **kwargs):
+        operator = kwargs.pop("operator", "and")
+        if operator not in ["and", "or"]:
+            raise ValueError("Invalid operator {}".format(operator))
         permissions = self.get_permissions()
-        checker = all if operator == 'and' else any
+        checker = all if operator == "and" else any
         return checker([permissions[p] for p in perms])
 
     def get_absolute_url(self):
-        return reverse('users:detail', kwargs={'username': self.username})
+        return reverse("users:detail", kwargs={"username": self.username})
 
     def update_secret_key(self):
         self.secret_key = uuid.uuid4()
@@ -119,4 +116,4 @@ class User(AbstractUser):
             self.update_subsonic_api_token()
 
     def get_activity_url(self):
-        return settings.FUNKWHALE_URL + '/@{}'.format(self.username)
+        return settings.FUNKWHALE_URL + "/@{}".format(self.username)
diff --git a/api/funkwhale_api/users/permissions.py b/api/funkwhale_api/users/permissions.py
index 146bc5e1c2ffae96ce7ae448496b69e2194522aa..02c1198e8cb208a201bd257e910bce3c2d53174b 100644
--- a/api/funkwhale_api/users/permissions.py
+++ b/api/funkwhale_api/users/permissions.py
@@ -11,11 +11,13 @@ class HasUserPermission(BasePermission):
         permission_classes = [HasUserPermission]
         required_permissions = ['federation']
     """
+
     def has_permission(self, request, view):
-        if not hasattr(request, 'user') or not request.user:
+        if not hasattr(request, "user") or not request.user:
             return False
         if request.user.is_anonymous:
             return False
-        operator = getattr(view, 'permission_operator', 'and')
+        operator = getattr(view, "permission_operator", "and")
         return request.user.has_permissions(
-            *view.required_permissions, operator=operator)
+            *view.required_permissions, operator=operator
+        )
diff --git a/api/funkwhale_api/users/rest_auth_urls.py b/api/funkwhale_api/users/rest_auth_urls.py
index fa6c425cc5227f8f9d4078338ff436ccc8e883d4..732a3bbbcead15c5603f16872a97c6703dcbecca 100644
--- a/api/funkwhale_api/users/rest_auth_urls.py
+++ b/api/funkwhale_api/users/rest_auth_urls.py
@@ -1,32 +1,35 @@
-from django.views.generic import TemplateView
 from django.conf.urls import url
-
-from rest_auth.registration import views as registration_views
+from django.views.generic import TemplateView
 from rest_auth import views as rest_auth_views
+from rest_auth.registration import views as registration_views
 
 from . import views
 
-
 urlpatterns = [
-    url(r'^$', views.RegisterView.as_view(), name='rest_register'),
-    url(r'^verify-email/$',
+    url(r"^$", views.RegisterView.as_view(), name="rest_register"),
+    url(
+        r"^verify-email/$",
         registration_views.VerifyEmailView.as_view(),
-        name='rest_verify_email'),
-    url(r'^change-password/$',
+        name="rest_verify_email",
+    ),
+    url(
+        r"^change-password/$",
         rest_auth_views.PasswordChangeView.as_view(),
-        name='change_password'),
-
+        name="change_password",
+    ),
     # This url is used by django-allauth and empty TemplateView is
     # defined just to allow reverse() call inside app, for example when email
     # with verification link is being sent, then it's required to render email
     # content.
-
     # account_confirm_email - You should override this view to handle it in
     # your API client somehow and then, send post to /verify-email/ endpoint
     # with proper key.
     # If you don't want to use API on that step, then just use ConfirmEmailView
     # view from:
     # djang-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190
-    url(r'^account-confirm-email/(?P<key>\w+)/$', TemplateView.as_view(),
-        name='account_confirm_email'),
+    url(
+        r"^account-confirm-email/(?P<key>\w+)/$",
+        TemplateView.as_view(),
+        name="account_confirm_email",
+    ),
 ]
diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py
index 3a095e78aa727b52ed35153cfffc8419b6142172..b3bd431c722fc5f8e4751270a9b1690973119de2 100644
--- a/api/funkwhale_api/users/serializers.py
+++ b/api/funkwhale_api/users/serializers.py
@@ -1,7 +1,7 @@
 from django.conf import settings
-
-from rest_framework import serializers
 from rest_auth.serializers import PasswordResetSerializer as PRS
+from rest_framework import serializers
+
 from funkwhale_api.activity import serializers as activity_serializers
 
 from . import models
@@ -9,35 +9,27 @@ from . import models
 
 class UserActivitySerializer(activity_serializers.ModelSerializer):
     type = serializers.SerializerMethodField()
-    name = serializers.CharField(source='username')
-    local_id = serializers.CharField(source='username')
+    name = serializers.CharField(source="username")
+    local_id = serializers.CharField(source="username")
 
     class Meta:
         model = models.User
-        fields = [
-            'id',
-            'local_id',
-            'name',
-            'type'
-        ]
+        fields = ["id", "local_id", "name", "type"]
 
     def get_type(self, obj):
-        return 'Person'
+        return "Person"
 
 
 class UserBasicSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.User
-        fields = ['id', 'username', 'name', 'date_joined']
+        fields = ["id", "username", "name", "date_joined"]
 
 
 class UserWriteSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.User
-        fields = [
-            'name',
-            'privacy_level'
-        ]
+        fields = ["name", "privacy_level"]
 
 
 class UserReadSerializer(serializers.ModelSerializer):
@@ -47,15 +39,15 @@ class UserReadSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.User
         fields = [
-            'id',
-            'username',
-            'name',
-            'email',
-            'is_staff',
-            'is_superuser',
-            'permissions',
-            'date_joined',
-            'privacy_level',
+            "id",
+            "username",
+            "name",
+            "email",
+            "is_staff",
+            "is_superuser",
+            "permissions",
+            "date_joined",
+            "privacy_level",
         ]
 
     def get_permissions(self, o):
@@ -64,8 +56,4 @@ class UserReadSerializer(serializers.ModelSerializer):
 
 class PasswordResetSerializer(PRS):
     def get_email_options(self):
-        return {
-            'extra_email_context': {
-                'funkwhale_url': settings.FUNKWHALE_URL
-            }
-        }
+        return {"extra_email_context": {"funkwhale_url": settings.FUNKWHALE_URL}}
diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py
index 0cc317889196703e9435f8af36ed63fb001b887f..69e69d26e6b987426ef59a450d07e3d6458f6ab2 100644
--- a/api/funkwhale_api/users/views.py
+++ b/api/funkwhale_api/users/views.py
@@ -1,24 +1,18 @@
-from rest_framework.response import Response
-from rest_framework import mixins
-from rest_framework import viewsets
-from rest_framework.decorators import detail_route, list_route
-
-from rest_auth.registration.views import RegisterView as BaseRegisterView
 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.response import Response
 
 from funkwhale_api.common import preferences
 
-from . import models
-from . import serializers
+from . import models, serializers
 
 
 class RegisterView(BaseRegisterView):
-
     def create(self, request, *args, **kwargs):
         if not self.is_open_for_signup(request):
-            r = {
-                'detail': 'Registration has been disabled',
-            }
+            r = {"detail": "Registration has been disabled"}
             return Response(r, status=403)
         return super().create(request, *args, **kwargs)
 
@@ -26,47 +20,42 @@ class RegisterView(BaseRegisterView):
         return get_adapter().is_open_for_signup(request)
 
 
-class UserViewSet(
-        mixins.UpdateModelMixin,
-        viewsets.GenericViewSet):
+class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
     queryset = models.User.objects.all()
     serializer_class = serializers.UserWriteSerializer
-    lookup_field = 'username'
+    lookup_field = "username"
 
-    @list_route(methods=['get'])
+    @list_route(methods=["get"])
     def me(self, request, *args, **kwargs):
         """Return information about the current user"""
         serializer = serializers.UserReadSerializer(request.user)
         return Response(serializer.data)
 
-    @detail_route(
-        methods=['get', 'post', 'delete'], url_path='subsonic-token')
+    @detail_route(methods=["get", "post", "delete"], url_path="subsonic-token")
     def subsonic_token(self, request, *args, **kwargs):
-        if not self.request.user.username == kwargs.get('username'):
+        if not self.request.user.username == kwargs.get("username"):
             return Response(status=403)
-        if not preferences.get('subsonic__enabled'):
+        if not preferences.get("subsonic__enabled"):
             return Response(status=405)
-        if request.method.lower() == 'get':
-            return Response({
-                'subsonic_api_token': self.request.user.subsonic_api_token
-            })
-        if request.method.lower() == 'delete':
+        if request.method.lower() == "get":
+            return Response(
+                {"subsonic_api_token": self.request.user.subsonic_api_token}
+            )
+        if request.method.lower() == "delete":
             self.request.user.subsonic_api_token = None
-            self.request.user.save(update_fields=['subsonic_api_token'])
+            self.request.user.save(update_fields=["subsonic_api_token"])
             return Response(status=204)
         self.request.user.update_subsonic_api_token()
-        self.request.user.save(update_fields=['subsonic_api_token'])
-        data = {
-            'subsonic_api_token': self.request.user.subsonic_api_token
-        }
+        self.request.user.save(update_fields=["subsonic_api_token"])
+        data = {"subsonic_api_token": self.request.user.subsonic_api_token}
         return Response(data)
 
     def update(self, request, *args, **kwargs):
-        if not self.request.user.username == kwargs.get('username'):
+        if not self.request.user.username == kwargs.get("username"):
             return Response(status=403)
         return super().update(request, *args, **kwargs)
 
     def partial_update(self, request, *args, **kwargs):
-        if not self.request.user.username == kwargs.get('username'):
+        if not self.request.user.username == kwargs.get("username"):
             return Response(status=403)
         return super().partial_update(request, *args, **kwargs)
diff --git a/api/requirements/local.txt b/api/requirements/local.txt
index c5f2ad0b7f7921a3af127f971f93eb4a3c80ad30..f11f976b8b165a1fff0fa219fd921a1644ff9084 100644
--- a/api/requirements/local.txt
+++ b/api/requirements/local.txt
@@ -9,3 +9,4 @@ django-debug-toolbar>=1.9,<1.10
 
 # improved REPL
 ipdb==0.8.1
+black
diff --git a/api/setup.cfg b/api/setup.cfg
index b1267c904cc94dc623dddf93c9af1293ec869122..18e34bc3543290d54135c6757f1a64ca6c560c5c 100644
--- a/api/setup.cfg
+++ b/api/setup.cfg
@@ -1,6 +1,10 @@
 [flake8]
 max-line-length = 120
-exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules
+exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,tests/data,tests/music/conftest.py
+ignore = F405,W503,E203
+
+[isort]
+skip_glob = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules
 
 [pep8]
 max-line-length = 120
diff --git a/api/tests/activity/test_record.py b/api/tests/activity/test_record.py
index 41846ba6f109cdc94b24c4e1ab01fb9812065310..69d3a28cfa5ec0b325025a0d1bfcf5548a6bfaf5 100644
--- a/api/tests/activity/test_record.py
+++ b/api/tests/activity/test_record.py
@@ -1,4 +1,3 @@
-import pytest
 
 from django.db import models
 from rest_framework import serializers
@@ -8,36 +7,35 @@ from funkwhale_api.activity import record
 
 class FakeModel(models.Model):
     class Meta:
-        app_label = 'tests'
+        app_label = "tests"
 
 
 class FakeSerializer(serializers.ModelSerializer):
     class Meta:
         model = FakeModel
-        fields = ['id']
-
-
+        fields = ["id"]
 
 
 def test_can_bind_serializer_to_model(activity_registry):
     activity_registry.register_serializer(FakeSerializer)
 
-    assert activity_registry['tests.FakeModel']['serializer'] == FakeSerializer
+    assert activity_registry["tests.FakeModel"]["serializer"] == FakeSerializer
 
 
 def test_can_bind_consumer_to_model(activity_registry):
     activity_registry.register_serializer(FakeSerializer)
-    @activity_registry.register_consumer('tests.FakeModel')
+
+    @activity_registry.register_consumer("tests.FakeModel")
     def propagate(data, obj):
         return True
 
-    assert activity_registry['tests.FakeModel']['consumers'] == [propagate]
+    assert activity_registry["tests.FakeModel"]["consumers"] == [propagate]
 
 
 def test_record_object_calls_consumer(activity_registry, mocker):
     activity_registry.register_serializer(FakeSerializer)
     stub = mocker.stub()
-    activity_registry.register_consumer('tests.FakeModel')(stub)
+    activity_registry.register_consumer("tests.FakeModel")(stub)
     o = FakeModel(id=1)
     data = FakeSerializer(o).data
     record.send(o)
diff --git a/api/tests/activity/test_serializers.py b/api/tests/activity/test_serializers.py
index 792fa74b9cbb3ed778c5e84bd746fb210e738acf..2561b5c8c56c2048fc86b8adbf10d84c90a58931 100644
--- a/api/tests/activity/test_serializers.py
+++ b/api/tests/activity/test_serializers.py
@@ -1,12 +1,11 @@
 from funkwhale_api.activity import serializers
 from funkwhale_api.favorites.serializers import TrackFavoriteActivitySerializer
-from funkwhale_api.history.serializers import \
-    ListeningActivitySerializer
+from funkwhale_api.history.serializers import ListeningActivitySerializer
 
 
 def test_autoserializer(factories):
-    favorite = factories['favorites.TrackFavorite']()
-    listening = factories['history.Listening']()
+    favorite = factories["favorites.TrackFavorite"]()
+    listening = factories["history.Listening"]()
     objects = [favorite, listening]
     serializer = serializers.AutoSerializer(objects, many=True)
     expected = [
diff --git a/api/tests/activity/test_utils.py b/api/tests/activity/test_utils.py
index 43bb45df84931ccd3ba2a56e555b991627c3a62c..0dabd3a28518b36134993bde1a21a3f070cf6d79 100644
--- a/api/tests/activity/test_utils.py
+++ b/api/tests/activity/test_utils.py
@@ -2,20 +2,18 @@ from funkwhale_api.activity import utils
 
 
 def test_get_activity(factories):
-    user = factories['users.User']()
-    listening = factories['history.Listening']()
-    favorite = factories['favorites.TrackFavorite']()
+    user = factories["users.User"]()
+    listening = factories["history.Listening"]()
+    favorite = factories["favorites.TrackFavorite"]()
 
     objects = list(utils.get_activity(user))
     assert objects == [favorite, listening]
 
 
 def test_get_activity_honors_privacy_level(factories, anonymous_user):
-    listening = factories['history.Listening'](user__privacy_level='me')
-    favorite1 = factories['favorites.TrackFavorite'](
-        user__privacy_level='everyone')
-    favorite2 = factories['favorites.TrackFavorite'](
-        user__privacy_level='instance')
+    factories["history.Listening"](user__privacy_level="me")
+    favorite1 = factories["favorites.TrackFavorite"](user__privacy_level="everyone")
+    factories["favorites.TrackFavorite"](user__privacy_level="instance")
 
     objects = list(utils.get_activity(anonymous_user))
     assert objects == [favorite1]
diff --git a/api/tests/activity/test_views.py b/api/tests/activity/test_views.py
index 9b24f3ad3a9359f53264d6b6d0be52f3490a19f6..1f5efae51377ad6bb4ce1dd8267d698c07049826 100644
--- a/api/tests/activity/test_views.py
+++ b/api/tests/activity/test_views.py
@@ -1,18 +1,16 @@
 from django.urls import reverse
 
-from funkwhale_api.activity import serializers
-from funkwhale_api.activity import utils
+from funkwhale_api.activity import serializers, utils
 
 
 def test_activity_view(factories, api_client, preferences, anonymous_user):
-    preferences['common__api_authentication_required'] = False
-    favorite = factories['favorites.TrackFavorite'](
-        user__privacy_level='everyone')
-    listening = factories['history.Listening']()
-    url = reverse('api:v1:activity-list')
+    preferences["common__api_authentication_required"] = False
+    factories["favorites.TrackFavorite"](user__privacy_level="everyone")
+    factories["history.Listening"]()
+    url = reverse("api:v1:activity-list")
     objects = utils.get_activity(anonymous_user)
     serializer = serializers.AutoSerializer(objects, many=True)
     response = api_client.get(url)
 
     assert response.status_code == 200
-    assert response.data['results'] == serializer.data
+    assert response.data["results"] == serializer.data
diff --git a/api/tests/channels/test_auth.py b/api/tests/channels/test_auth.py
index a2b7eaf0ca685c1c197ed339a5963aeceea215c2..505bef1c0761cd972f40f7b4cf09e14cd1fcb627 100644
--- a/api/tests/channels/test_auth.py
+++ b/api/tests/channels/test_auth.py
@@ -1,5 +1,4 @@
 import pytest
-
 from rest_framework_jwt.settings import api_settings
 
 from funkwhale_api.common.auth import TokenAuthMiddleware
@@ -8,30 +7,24 @@ jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
 jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
 
 
-@pytest.mark.parametrize('query_string', [
-    b'token=wrong',
-    b'',
-])
+@pytest.mark.parametrize("query_string", [b"token=wrong", b""])
 def test_header_anonymous(query_string, factories):
     def callback(scope):
-        assert scope['user'].is_anonymous
+        assert scope["user"].is_anonymous
 
-    scope = {
-        'query_string': query_string
-    }
+    scope = {"query_string": query_string}
     consumer = TokenAuthMiddleware(callback)
     consumer(scope)
 
 
 def test_header_correct_token(factories):
-    user = factories['users.User']()
+    user = factories["users.User"]()
     payload = jwt_payload_handler(user)
     token = jwt_encode_handler(payload)
+
     def callback(scope):
-        assert scope['user'] == user
+        assert scope["user"] == user
 
-    scope = {
-        'query_string': 'token={}'.format(token).encode('utf-8')
-    }
+    scope = {"query_string": "token={}".format(token).encode("utf-8")}
     consumer = TokenAuthMiddleware(callback)
     consumer(scope)
diff --git a/api/tests/channels/test_consumers.py b/api/tests/channels/test_consumers.py
index f1648efb3a614fb2f74b8e941083f99491043dd0..f4f5f3ad6f1641e7185c07a88e70654405bf3661 100644
--- a/api/tests/channels/test_consumers.py
+++ b/api/tests/channels/test_consumers.py
@@ -2,15 +2,15 @@ from funkwhale_api.common import consumers
 
 
 def test_auth_consumer_requires_valid_user(mocker):
-    m = mocker.patch('funkwhale_api.common.consumers.JsonAuthConsumer.close')
-    scope = {'user': None}
+    m = mocker.patch("funkwhale_api.common.consumers.JsonAuthConsumer.close")
+    scope = {"user": None}
     consumer = consumers.JsonAuthConsumer(scope=scope)
     consumer.connect()
     m.assert_called_once_with()
 
 
 def test_auth_consumer_requires_user_in_scope(mocker):
-    m = mocker.patch('funkwhale_api.common.consumers.JsonAuthConsumer.close')
+    m = mocker.patch("funkwhale_api.common.consumers.JsonAuthConsumer.close")
     scope = {}
     consumer = consumers.JsonAuthConsumer(scope=scope)
     consumer.connect()
@@ -18,9 +18,9 @@ def test_auth_consumer_requires_user_in_scope(mocker):
 
 
 def test_auth_consumer_accepts_connection(mocker, factories):
-    user = factories['users.User']()
-    m = mocker.patch('funkwhale_api.common.consumers.JsonAuthConsumer.accept')
-    scope = {'user': user}
+    user = factories["users.User"]()
+    m = mocker.patch("funkwhale_api.common.consumers.JsonAuthConsumer.accept")
+    scope = {"user": user}
     consumer = consumers.JsonAuthConsumer(scope=scope)
     consumer.connect()
     m.assert_called_once_with()
diff --git a/api/tests/common/test_fields.py b/api/tests/common/test_fields.py
index 29a8fb05c4ead7d9be810de7b64ba430a17f7fee..d2692314854c3c6761974789ed82f96132beb9dd 100644
--- a/api/tests/common/test_fields.py
+++ b/api/tests/common/test_fields.py
@@ -1,5 +1,4 @@
 import pytest
-
 from django.contrib.auth.models import AnonymousUser
 from django.db.models import Q
 
@@ -7,11 +6,16 @@ from funkwhale_api.common import fields
 from funkwhale_api.users.factories import UserFactory
 
 
-@pytest.mark.parametrize('user,expected', [
-    (AnonymousUser(), Q(privacy_level='everyone')),
-    (UserFactory.build(pk=1),
-     Q(privacy_level__in=['followers', 'instance', 'everyone'])),
-])
-def test_privacy_level_query(user,expected):
+@pytest.mark.parametrize(
+    "user,expected",
+    [
+        (AnonymousUser(), Q(privacy_level="everyone")),
+        (
+            UserFactory.build(pk=1),
+            Q(privacy_level__in=["followers", "instance", "everyone"]),
+        ),
+    ],
+)
+def test_privacy_level_query(user, expected):
     query = fields.privacy_level_query(user)
     assert query == expected
diff --git a/api/tests/common/test_permissions.py b/api/tests/common/test_permissions.py
index f04f12e0b0e19a75992ad364eee4f05219d0e3f7..bf4d8bde5cd3b918030befd43bca81402ce72abd 100644
--- a/api/tests/common/test_permissions.py
+++ b/api/tests/common/test_permissions.py
@@ -1,43 +1,41 @@
 import pytest
-
-from rest_framework.views import APIView
-
 from django.http import Http404
+from rest_framework.views import APIView
 
 from funkwhale_api.common import permissions
 
 
 def test_owner_permission_owner_field_ok(nodb_factories, api_request):
-    playlist = nodb_factories['playlists.Playlist']()
+    playlist = nodb_factories["playlists.Playlist"]()
     view = APIView.as_view()
     permission = permissions.OwnerPermission()
-    request = api_request.get('/')
-    setattr(request, 'user', playlist.user)
+    request = api_request.get("/")
+    setattr(request, "user", playlist.user)
     check = permission.has_object_permission(request, view, playlist)
 
     assert check is True
 
 
 def test_owner_permission_owner_field_not_ok(
-        anonymous_user, nodb_factories, api_request):
-    playlist = nodb_factories['playlists.Playlist']()
+    anonymous_user, nodb_factories, api_request
+):
+    playlist = nodb_factories["playlists.Playlist"]()
     view = APIView.as_view()
     permission = permissions.OwnerPermission()
-    request = api_request.get('/')
-    setattr(request, 'user', anonymous_user)
+    request = api_request.get("/")
+    setattr(request, "user", anonymous_user)
 
     with pytest.raises(Http404):
         permission.has_object_permission(request, view, playlist)
 
 
-def test_owner_permission_read_only(
-        anonymous_user, nodb_factories, api_request):
-    playlist = nodb_factories['playlists.Playlist']()
+def test_owner_permission_read_only(anonymous_user, nodb_factories, api_request):
+    playlist = nodb_factories["playlists.Playlist"]()
     view = APIView.as_view()
-    setattr(view, 'owner_checks', ['write'])
+    setattr(view, "owner_checks", ["write"])
     permission = permissions.OwnerPermission()
-    request = api_request.get('/')
-    setattr(request, 'user', anonymous_user)
+    request = api_request.get("/")
+    setattr(request, "user", anonymous_user)
     check = permission.has_object_permission(request, view, playlist)
 
     assert check is True
diff --git a/api/tests/common/test_preferences.py b/api/tests/common/test_preferences.py
index 475610a937c6c007dca06dda7045e2d41b61f6d9..7f941a45006be86550c8622bde019be61ebb0abc 100644
--- a/api/tests/common/test_preferences.py
+++ b/api/tests/common/test_preferences.py
@@ -1,44 +1,45 @@
 import pytest
-
 from dynamic_preferences.registries import global_preferences_registry
+
 from funkwhale_api.common import preferences as common_preferences
 
 
 @pytest.fixture
 def string_list_pref(preferences):
-
     @global_preferences_registry.register
     class P(common_preferences.StringListPreference):
-        default = ['hello']
-        section = 'test'
-        name = 'string_list'
-    yield
-    del global_preferences_registry['test']['string_list']
-
+        default = ["hello"]
+        section = "test"
+        name = "string_list"
 
-@pytest.mark.parametrize('input,output', [
-    (['a', 'b', 'c'], 'a,b,c'),
-    (['a', 'c', 'b'], 'a,b,c'),
-    (('a', 'c', 'b'), 'a,b,c'),
-    ([], None),
-])
+    yield
+    del global_preferences_registry["test"]["string_list"]
+
+
+@pytest.mark.parametrize(
+    "input,output",
+    [
+        (["a", "b", "c"], "a,b,c"),
+        (["a", "c", "b"], "a,b,c"),
+        (("a", "c", "b"), "a,b,c"),
+        ([], None),
+    ],
+)
 def test_string_list_serializer_to_db(input, output):
-    s = common_preferences.StringListSerializer.to_db(input) == output
+    common_preferences.StringListSerializer.to_db(input) == output
 
 
-@pytest.mark.parametrize('input,output', [
-    ('a,b,c', ['a', 'b', 'c'], ),
-    (None, []),
-    ('', []),
-])
+@pytest.mark.parametrize(
+    "input,output", [("a,b,c", ["a", "b", "c"]), (None, []), ("", [])]
+)
 def test_string_list_serializer_to_python(input, output):
-    s = common_preferences.StringListSerializer.to_python(input) == output
+    common_preferences.StringListSerializer.to_python(input) == output
 
 
 def test_string_list_pref_default(string_list_pref, preferences):
-    assert preferences['test__string_list'] == ['hello']
+    assert preferences["test__string_list"] == ["hello"]
 
 
 def test_string_list_pref_set(string_list_pref, preferences):
-    preferences['test__string_list'] = ['world', 'hello']
-    assert preferences['test__string_list'] == ['hello', 'world']
+    preferences["test__string_list"] = ["world", "hello"]
+    assert preferences["test__string_list"] == ["hello", "world"]
diff --git a/api/tests/common/test_scripts.py b/api/tests/common/test_scripts.py
index ce478ba048043b6c1da61d0febf72fd91e8c2444..40d9ea0a7aae1f18611a74e956bcd2ac5d77dbdd 100644
--- a/api/tests/common/test_scripts.py
+++ b/api/tests/common/test_scripts.py
@@ -1,7 +1,7 @@
 import pytest
 
-from funkwhale_api.common.management.commands import script
 from funkwhale_api.common import scripts
+from funkwhale_api.common.management.commands import script
 
 
 @pytest.fixture
@@ -9,38 +9,26 @@ def command():
     return script.Command()
 
 
-@pytest.mark.parametrize('script_name', [
-    'django_permissions_to_user_permissions',
-    'test',
-])
+@pytest.mark.parametrize(
+    "script_name", ["django_permissions_to_user_permissions", "test"]
+)
 def test_script_command_list(command, script_name, mocker):
-    mocked = mocker.patch(
-        'funkwhale_api.common.scripts.{}.main'.format(script_name))
+    mocked = mocker.patch("funkwhale_api.common.scripts.{}.main".format(script_name))
 
     command.handle(script_name=script_name, interactive=False)
 
-    mocked.assert_called_once_with(
-        command, script_name=script_name, interactive=False)
+    mocked.assert_called_once_with(command, script_name=script_name, interactive=False)
 
 
 def test_django_permissions_to_user_permissions(factories, command):
-    group = factories['auth.Group'](
+    group = factories["auth.Group"](perms=["federation.change_library"])
+    user1 = factories["users.User"](
         perms=[
-            'federation.change_library'
+            "dynamic_preferences.change_globalpreferencemodel",
+            "music.add_importbatch",
         ]
     )
-    user1 = factories['users.User'](
-        perms=[
-            'dynamic_preferences.change_globalpreferencemodel',
-            'music.add_importbatch',
-        ]
-    )
-    user2 = factories['users.User'](
-        perms=[
-            'music.add_importbatch',
-        ],
-        groups=[group]
-    )
+    user2 = factories["users.User"](perms=["music.add_importbatch"], groups=[group])
 
     scripts.django_permissions_to_user_permissions.main(command)
 
diff --git a/api/tests/common/test_serializers.py b/api/tests/common/test_serializers.py
index f0f5fb7e61c8bd1d30fd14dec1b0edb28c5d31ab..ca5e5ad8f6fd2317af880eef0aec3f28b1184b04 100644
--- a/api/tests/common/test_serializers.py
+++ b/api/tests/common/test_serializers.py
@@ -7,20 +7,20 @@ from funkwhale_api.users import models
 class TestActionFilterSet(django_filters.FilterSet):
     class Meta:
         model = models.User
-        fields = ['is_active']
+        fields = ["is_active"]
 
 
 class TestSerializer(serializers.ActionSerializer):
-    actions = ['test']
+    actions = ["test"]
     filterset_class = TestActionFilterSet
 
     def handle_test(self, objects):
-        return {'hello': 'world'}
+        return {"hello": "world"}
 
 
 class TestDangerousSerializer(serializers.ActionSerializer):
-    actions = ['test', 'test_dangerous']
-    dangerous_actions = ['test_dangerous']
+    actions = ["test", "test_dangerous"]
+    dangerous_actions = ["test_dangerous"]
 
     def handle_test(self, objects):
         pass
@@ -30,107 +30,88 @@ class TestDangerousSerializer(serializers.ActionSerializer):
 
 
 def test_action_serializer_validates_action():
-    data = {'objects': 'all', 'action': 'nope'}
+    data = {"objects": "all", "action": "nope"}
     serializer = TestSerializer(data, queryset=models.User.objects.none())
 
     assert serializer.is_valid() is False
-    assert 'action' in serializer.errors
+    assert "action" in serializer.errors
 
 
 def test_action_serializer_validates_objects():
-    data = {'objects': 'nope', 'action': 'test'}
+    data = {"objects": "nope", "action": "test"}
     serializer = TestSerializer(data, queryset=models.User.objects.none())
 
     assert serializer.is_valid() is False
-    assert 'objects' in serializer.errors
+    assert "objects" in serializer.errors
 
 
 def test_action_serializers_objects_clean_ids(factories):
-    user1 = factories['users.User']()
-    user2 = factories['users.User']()
+    user1 = factories["users.User"]()
+    factories["users.User"]()
 
-    data = {'objects': [user1.pk], 'action': 'test'}
+    data = {"objects": [user1.pk], "action": "test"}
     serializer = TestSerializer(data, queryset=models.User.objects.all())
 
     assert serializer.is_valid() is True
-    assert list(serializer.validated_data['objects']) == [user1]
+    assert list(serializer.validated_data["objects"]) == [user1]
 
 
 def test_action_serializers_objects_clean_all(factories):
-    user1 = factories['users.User']()
-    user2 = factories['users.User']()
+    user1 = factories["users.User"]()
+    user2 = factories["users.User"]()
 
-    data = {'objects': 'all', 'action': 'test'}
+    data = {"objects": "all", "action": "test"}
     serializer = TestSerializer(data, queryset=models.User.objects.all())
 
     assert serializer.is_valid() is True
-    assert list(serializer.validated_data['objects']) == [user1, user2]
+    assert list(serializer.validated_data["objects"]) == [user1, user2]
 
 
 def test_action_serializers_save(factories, mocker):
-    handler = mocker.spy(TestSerializer, 'handle_test')
-    user1 = factories['users.User']()
-    user2 = factories['users.User']()
+    handler = mocker.spy(TestSerializer, "handle_test")
+    factories["users.User"]()
+    factories["users.User"]()
 
-    data = {'objects': 'all', 'action': 'test'}
+    data = {"objects": "all", "action": "test"}
     serializer = TestSerializer(data, queryset=models.User.objects.all())
 
     assert serializer.is_valid() is True
     result = serializer.save()
-    assert result == {
-        'updated': 2,
-        'action': 'test',
-        'result': {'hello': 'world'},
-    }
+    assert result == {"updated": 2, "action": "test", "result": {"hello": "world"}}
     handler.assert_called_once()
 
 
 def test_action_serializers_filterset(factories):
-    user1 = factories['users.User'](is_active=False)
-    user2 = factories['users.User'](is_active=True)
-
-    data = {
-        'objects': 'all',
-        'action': 'test',
-        'filters': {'is_active': True},
-    }
+    factories["users.User"](is_active=False)
+    user2 = factories["users.User"](is_active=True)
+
+    data = {"objects": "all", "action": "test", "filters": {"is_active": True}}
     serializer = TestSerializer(data, queryset=models.User.objects.all())
 
     assert serializer.is_valid() is True
-    assert list(serializer.validated_data['objects']) == [user2]
+    assert list(serializer.validated_data["objects"]) == [user2]
 
 
 def test_action_serializers_validates_at_least_one_object():
-    data = {
-        'objects': 'all',
-        'action': 'test',
-    }
+    data = {"objects": "all", "action": "test"}
     serializer = TestSerializer(data, queryset=models.User.objects.none())
 
     assert serializer.is_valid() is False
-    assert 'non_field_errors' in serializer.errors
+    assert "non_field_errors" in serializer.errors
 
 
 def test_dangerous_actions_refuses_all(factories):
-    factories['users.User']()
-    data = {
-        'objects': 'all',
-        'action': 'test_dangerous',
-    }
-    serializer = TestDangerousSerializer(
-        data, queryset=models.User.objects.all())
+    factories["users.User"]()
+    data = {"objects": "all", "action": "test_dangerous"}
+    serializer = TestDangerousSerializer(data, queryset=models.User.objects.all())
 
     assert serializer.is_valid() is False
-    assert 'non_field_errors' in serializer.errors
+    assert "non_field_errors" in serializer.errors
 
 
 def test_dangerous_actions_refuses_not_listed(factories):
-    factories['users.User']()
-    data = {
-        'objects': 'all',
-        'action': 'test',
-    }
-    serializer = TestDangerousSerializer(
-        data, queryset=models.User.objects.all())
+    factories["users.User"]()
+    data = {"objects": "all", "action": "test"}
+    serializer = TestDangerousSerializer(data, queryset=models.User.objects.all())
 
     assert serializer.is_valid() is True
diff --git a/api/tests/common/test_session.py b/api/tests/common/test_session.py
index 7ff1e660bc74936ce945c968bad919871b14ad21..531543455d43cb92268ea196f15c1fcdd9472d6f 100644
--- a/api/tests/common/test_session.py
+++ b/api/tests/common/test_session.py
@@ -1,18 +1,16 @@
 import funkwhale_api
-
 from funkwhale_api.common import session
 
 
 def test_get_user_agent(settings):
-    settings.FUNKWHALE_URL = 'https://test.com'
-    'http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)'
-    expected = 'python-requests (funkwhale/{}; +{})'.format(
-        funkwhale_api.__version__,
-        settings.FUNKWHALE_URL
+    settings.FUNKWHALE_URL = "https://test.com"
+    "http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)"
+    expected = "python-requests (funkwhale/{}; +{})".format(
+        funkwhale_api.__version__, settings.FUNKWHALE_URL
     )
     assert session.get_user_agent() == expected
 
 
 def test_get_session():
     expected = session.get_user_agent()
-    assert session.get_session().headers['User-Agent'] == expected
+    assert session.get_session().headers["User-Agent"] == expected
diff --git a/api/tests/conftest.py b/api/tests/conftest.py
index 7caff2009974143584621a2d6f9e2239e622430a..40203ee3d472693d7304d92ab68094949535fab9 100644
--- a/api/tests/conftest.py
+++ b/api/tests/conftest.py
@@ -1,29 +1,26 @@
 import datetime
-import factory
-import pytest
-import requests_mock
 import shutil
 import tempfile
 
+import factory
+import pytest
+import requests_mock
 from django.contrib.auth.models import AnonymousUser
 from django.core.cache import cache as django_cache
 from django.test import client
-
 from dynamic_preferences.registries import global_preferences_registry
-
 from rest_framework import fields as rest_fields
-from rest_framework.test import APIClient
-from rest_framework.test import APIRequestFactory
+from rest_framework.test import APIClient, APIRequestFactory
 
 from funkwhale_api.activity import record
 from funkwhale_api.users.permissions import HasUserPermission
-from funkwhale_api.taskapp import celery
 
 
 @pytest.fixture(scope="session", autouse=True)
 def factories_autodiscover():
     from django.apps import apps
     from funkwhale_api import factories
+
     app_names = [app.name for app in apps.app_configs.values()]
     factories.registry.autodiscover(app_names)
 
@@ -44,6 +41,7 @@ def factories(db):
     users.User or music.Track
     """
     from funkwhale_api import factories
+
     for v in factories.registry.values():
         try:
             v._meta.strategy = factory.CREATE_STRATEGY
@@ -60,6 +58,7 @@ def nodb_factories():
     that does not require access to the database
     """
     from funkwhale_api import factories
+
     for v in factories.registry.values():
         try:
             v._meta.strategy = factory.BUILD_STRATEGY
@@ -104,11 +103,11 @@ def logged_in_client(db, factories, client):
     Returns a logged-in, non-API client with an authenticated ``User``
     stored in the ``user`` attribute
     """
-    user = factories['users.User']()
-    assert client.login(username=user.username, password='test')
-    setattr(client, 'user', user)
+    user = factories["users.User"]()
+    assert client.login(username=user.username, password="test")
+    setattr(client, "user", user)
     yield client
-    delattr(client, 'user')
+    delattr(client, "user")
 
 
 @pytest.fixture
@@ -131,12 +130,12 @@ def logged_in_api_client(db, factories, api_client):
     Return a logged-in API client with an authenticated ``User``
     stored in the ``user`` attribute
     """
-    user = factories['users.User']()
-    assert api_client.login(username=user.username, password='test')
+    user = factories["users.User"]()
+    assert api_client.login(username=user.username, password="test")
     api_client.force_authenticate(user=user)
-    setattr(api_client, 'user', user)
+    setattr(api_client, "user", user)
     yield api_client
-    delattr(api_client, 'user')
+    delattr(api_client, "user")
 
 
 @pytest.fixture
@@ -145,11 +144,11 @@ def superuser_api_client(db, factories, api_client):
     Return a logged-in API client with an authenticated superuser
     stored in the ``user`` attribute
     """
-    user = factories['users.SuperUser']()
-    assert api_client.login(username=user.username, password='test')
-    setattr(api_client, 'user', user)
+    user = factories["users.SuperUser"]()
+    assert api_client.login(username=user.username, password="test")
+    setattr(api_client, "user", user)
     yield api_client
-    delattr(api_client, 'user')
+    delattr(api_client, "user")
 
 
 @pytest.fixture
@@ -158,11 +157,11 @@ def superuser_client(db, factories, client):
     Return a logged-in, non-API client with an authenticated ``User``
     stored in the ``user`` attribute
     """
-    user = factories['users.SuperUser']()
-    assert client.login(username=user.username, password='test')
-    setattr(client, 'user', user)
+    user = factories["users.SuperUser"]()
+    assert client.login(username=user.username, password="test")
+    setattr(client, "user", user)
     yield client
-    delattr(client, 'user')
+    delattr(client, "user")
 
 
 @pytest.fixture
@@ -183,7 +182,6 @@ def fake_request():
 
 @pytest.fixture
 def activity_registry():
-    r = record.registry
     state = list(record.registry.items())
     yield record.registry
     record.registry.clear()
@@ -193,7 +191,7 @@ def activity_registry():
 
 @pytest.fixture
 def activity_muted(activity_registry, mocker):
-    yield mocker.patch.object(record, 'send')
+    yield mocker.patch.object(record, "send")
 
 
 @pytest.fixture(autouse=True)
@@ -222,19 +220,21 @@ def authenticated_actor(factories, mocker):
     """
     Returns an authenticated ActivityPub actor
     """
-    actor = factories['federation.Actor']()
+    actor = factories["federation.Actor"]()
     mocker.patch(
-        'funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor',
-        return_value=actor)
+        "funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor",
+        return_value=actor,
+    )
     yield actor
 
 
 @pytest.fixture
 def assert_user_permission():
-    def inner(view, permissions, operator='and'):
+    def inner(view, permissions, operator="and"):
         assert HasUserPermission in view.permission_classes
-        assert getattr(view, 'permission_operator', 'and') == operator
+        assert getattr(view, "permission_operator", "and") == operator
         assert set(view.required_permissions) == set(permissions)
+
     return inner
 
 
@@ -247,5 +247,6 @@ def to_api_date():
         if isinstance(value, datetime.date):
             f = rest_fields.DateField()
             return f.to_representation(value)
-        raise ValueError('Invalid value: {}'.format(value))
+        raise ValueError("Invalid value: {}".format(value))
+
     return inner
diff --git a/api/tests/data/youtube.py b/api/tests/data/youtube.py
index a8372d4c96facba1cafa444cf583b1d27d1a3b4d..9c8f9e68f16c55a815ebab45bf5d732143efbc28 100644
--- a/api/tests/data/youtube.py
+++ b/api/tests/data/youtube.py
@@ -1,25 +1,17 @@
-
-
 search = {}
 
 
-search['8 bit adventure'] = {
-    "pageInfo": {
-        "totalResults": 1000000,
-        "resultsPerPage": 25
-    },
+search["8 bit adventure"] = {
+    "pageInfo": {"totalResults": 1000000, "resultsPerPage": 25},
     "nextPageToken": "CBkQAA",
-    "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/1L34zetsKWv-raAFiz0MuT0SsfQ\"",
+    "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/1L34zetsKWv-raAFiz0MuT0SsfQ"',
     "items": [
         {
-            "id": {
-                "videoId": "0HxZn6CzOIo",
-                "kind": "youtube#video"
-            },
-            "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/GxK-wHBWUYfrJsd1dijBPTufrVE\"",
+            "id": {"videoId": "0HxZn6CzOIo", "kind": "youtube#video"},
+            "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/GxK-wHBWUYfrJsd1dijBPTufrVE"',
             "snippet": {
                 "liveBroadcastContent": "none",
-                "description": "Make sure to apply adhesive evenly before use. GET IT HERE: http://adhesivewombat.bandcamp.com/album/marsupial-madness Facebook: ...",
+                "description": "Description",
                 "channelId": "UCps63j3krzAG4OyXeEyuhFw",
                 "title": "AdhesiveWombat - 8 Bit Adventure",
                 "channelTitle": "AdhesiveWombat",
@@ -28,28 +20,25 @@ search['8 bit adventure'] = {
                     "medium": {
                         "url": "https://i.ytimg.com/vi/0HxZn6CzOIo/mqdefault.jpg",
                         "height": 180,
-                        "width": 320
+                        "width": 320,
                     },
                     "high": {
                         "url": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg",
                         "height": 360,
-                        "width": 480
+                        "width": 480,
                     },
                     "default": {
                         "url": "https://i.ytimg.com/vi/0HxZn6CzOIo/default.jpg",
                         "height": 90,
-                        "width": 120
-                    }
-                }
+                        "width": 120,
+                    },
+                },
             },
-            "kind": "youtube#searchResult"
+            "kind": "youtube#searchResult",
         },
         {
-            "id": {
-                "videoId": "n4A_F5SXmgo",
-                "kind": "youtube#video"
-            },
-            "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/aRVESw24jlgiErDgJKxNrazKRDc\"",
+            "id": {"videoId": "n4A_F5SXmgo", "kind": "youtube#video"},
+            "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/aRVESw24jlgiErDgJKxNrazKRDc"',
             "snippet": {
                 "liveBroadcastContent": "none",
                 "description": "Free Download: http://bit.ly/1fZ1pMJ I don't post 8 bit'ish music much but damn I must admit this is goood! Enjoy \u2665 \u25bbSpikedGrin: ...",
@@ -61,34 +50,31 @@ search['8 bit adventure'] = {
                     "medium": {
                         "url": "https://i.ytimg.com/vi/n4A_F5SXmgo/mqdefault.jpg",
                         "height": 180,
-                        "width": 320
+                        "width": 320,
                     },
                     "high": {
                         "url": "https://i.ytimg.com/vi/n4A_F5SXmgo/hqdefault.jpg",
                         "height": 360,
-                        "width": 480
+                        "width": 480,
                     },
                     "default": {
                         "url": "https://i.ytimg.com/vi/n4A_F5SXmgo/default.jpg",
                         "height": 90,
-                        "width": 120
-                    }
-                }
+                        "width": 120,
+                    },
+                },
             },
-            "kind": "youtube#searchResult"
+            "kind": "youtube#searchResult",
         },
     ],
     "regionCode": "FR",
-    "kind": "youtube#searchListResponse"
+    "kind": "youtube#searchListResponse",
 }
 
-search['system of a down toxicity'] = {
+search["system of a down toxicity"] = {
     "items": [
         {
-            "id": {
-                "kind": "youtube#video",
-                "videoId": "BorYwGi2SJc"
-            },
+            "id": {"kind": "youtube#video", "videoId": "BorYwGi2SJc"},
             "kind": "youtube#searchResult",
             "snippet": {
                 "title": "System of a Down: Toxicity",
@@ -98,30 +84,27 @@ search['system of a down toxicity'] = {
                     "default": {
                         "height": 90,
                         "width": 120,
-                        "url": "https://i.ytimg.com/vi/BorYwGi2SJc/default.jpg"
+                        "url": "https://i.ytimg.com/vi/BorYwGi2SJc/default.jpg",
                     },
                     "high": {
                         "height": 360,
                         "width": 480,
-                        "url": "https://i.ytimg.com/vi/BorYwGi2SJc/hqdefault.jpg"
+                        "url": "https://i.ytimg.com/vi/BorYwGi2SJc/hqdefault.jpg",
                     },
                     "medium": {
                         "height": 180,
                         "width": 320,
-                        "url": "https://i.ytimg.com/vi/BorYwGi2SJc/mqdefault.jpg"
-                    }
+                        "url": "https://i.ytimg.com/vi/BorYwGi2SJc/mqdefault.jpg",
+                    },
                 },
                 "publishedAt": "2007-12-17T12:39:54.000Z",
                 "description": "http://www.vedrescsaba.uw.hu The System of a Down song Toxicity arranged for a classical piano quintet, played by Vedres Csaba and the Kairosz quartet.",
-                "liveBroadcastContent": "none"
+                "liveBroadcastContent": "none",
             },
-            "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/UwR8H6P6kbijNZmBNkYd2jAzDnI\""
+            "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/UwR8H6P6kbijNZmBNkYd2jAzDnI"',
         },
         {
-            "id": {
-                "kind": "youtube#video",
-                "videoId": "ENBv2i88g6Y"
-            },
+            "id": {"kind": "youtube#video", "videoId": "ENBv2i88g6Y"},
             "kind": "youtube#searchResult",
             "snippet": {
                 "title": "System Of A Down - Question!",
@@ -131,32 +114,29 @@ search['system of a down toxicity'] = {
                     "default": {
                         "height": 90,
                         "width": 120,
-                        "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/default.jpg"
+                        "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/default.jpg",
                     },
                     "high": {
                         "height": 360,
                         "width": 480,
-                        "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/hqdefault.jpg"
+                        "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/hqdefault.jpg",
                     },
                     "medium": {
                         "height": 180,
                         "width": 320,
-                        "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/mqdefault.jpg"
-                    }
+                        "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/mqdefault.jpg",
+                    },
                 },
                 "publishedAt": "2009-10-03T04:49:03.000Z",
                 "description": "System of a Down's official music video for 'Question!'. Click to listen to System of a Down on Spotify: http://smarturl.it/SystemSpotify?IQid=SystemQu As featured ...",
-                "liveBroadcastContent": "none"
+                "liveBroadcastContent": "none",
             },
-            "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/dB-M0N9mB4xE-k4yAF_4d8aU0I4\""
+            "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/dB-M0N9mB4xE-k4yAF_4d8aU0I4"',
         },
     ],
-    "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/yhLQgSpeObNnybd5JqSzlGiJ8Ew\"",
+    "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/yhLQgSpeObNnybd5JqSzlGiJ8Ew"',
     "nextPageToken": "CBkQAA",
-    "pageInfo": {
-        "resultsPerPage": 25,
-        "totalResults": 26825
-    },
+    "pageInfo": {"resultsPerPage": 25, "totalResults": 26825},
     "kind": "youtube#searchListResponse",
-    "regionCode": "FR"
+    "regionCode": "FR",
 }
diff --git a/api/tests/favorites/test_activity.py b/api/tests/favorites/test_activity.py
index 63174f9e2693a3fb0923c1aa0c1b682a06b2226a..e4c040b20e9025f0f5293131d6ba7e09f9972265 100644
--- a/api/tests/favorites/test_activity.py
+++ b/api/tests/favorites/test_activity.py
@@ -1,19 +1,17 @@
-from funkwhale_api.users.serializers import UserActivitySerializer
+from funkwhale_api.favorites import activities, serializers
 from funkwhale_api.music.serializers import TrackActivitySerializer
-from funkwhale_api.favorites import serializers
-from funkwhale_api.favorites import activities
+from funkwhale_api.users.serializers import UserActivitySerializer
 
 
 def test_get_favorite_activity_url(settings, factories):
-    favorite = factories['favorites.TrackFavorite']()
+    favorite = factories["favorites.TrackFavorite"]()
     user_url = favorite.user.get_activity_url()
-    expected = '{}/favorites/tracks/{}'.format(
-        user_url, favorite.pk)
+    expected = "{}/favorites/tracks/{}".format(user_url, favorite.pk)
     assert favorite.get_activity_url() == expected
 
 
 def test_activity_favorite_serializer(factories):
-    favorite = factories['favorites.TrackFavorite']()
+    favorite = factories["favorites.TrackFavorite"]()
 
     actor = UserActivitySerializer(favorite.user).data
     field = serializers.serializers.DateTimeField()
@@ -32,44 +30,30 @@ def test_activity_favorite_serializer(factories):
 
 
 def test_track_favorite_serializer_is_connected(activity_registry):
-    conf = activity_registry['favorites.TrackFavorite']
-    assert conf['serializer'] == serializers.TrackFavoriteActivitySerializer
+    conf = activity_registry["favorites.TrackFavorite"]
+    assert conf["serializer"] == serializers.TrackFavoriteActivitySerializer
 
 
-def test_track_favorite_serializer_instance_activity_consumer(
-        activity_registry):
-    conf = activity_registry['favorites.TrackFavorite']
+def test_track_favorite_serializer_instance_activity_consumer(activity_registry):
+    conf = activity_registry["favorites.TrackFavorite"]
     consumer = activities.broadcast_track_favorite_to_instance_activity
-    assert consumer in conf['consumers']
+    assert consumer in conf["consumers"]
 
 
-def test_broadcast_track_favorite_to_instance_activity(
-        factories, mocker):
-    p = mocker.patch('funkwhale_api.common.channels.group_send')
-    favorite = factories['favorites.TrackFavorite']()
+def test_broadcast_track_favorite_to_instance_activity(factories, mocker):
+    p = mocker.patch("funkwhale_api.common.channels.group_send")
+    favorite = factories["favorites.TrackFavorite"]()
     data = serializers.TrackFavoriteActivitySerializer(favorite).data
     consumer = activities.broadcast_track_favorite_to_instance_activity
-    message = {
-        "type": 'event.send',
-        "text": '',
-        "data": data
-    }
+    message = {"type": "event.send", "text": "", "data": data}
     consumer(data=data, obj=favorite)
-    p.assert_called_once_with('instance_activity', message)
+    p.assert_called_once_with("instance_activity", message)
 
 
-def test_broadcast_track_favorite_to_instance_activity_private(
-        factories, mocker):
-    p = mocker.patch('funkwhale_api.common.channels.group_send')
-    favorite = factories['favorites.TrackFavorite'](
-        user__privacy_level='me'
-    )
+def test_broadcast_track_favorite_to_instance_activity_private(factories, mocker):
+    p = mocker.patch("funkwhale_api.common.channels.group_send")
+    favorite = factories["favorites.TrackFavorite"](user__privacy_level="me")
     data = serializers.TrackFavoriteActivitySerializer(favorite).data
     consumer = activities.broadcast_track_favorite_to_instance_activity
-    message = {
-        "type": 'event.send',
-        "text": '',
-        "data": data
-    }
     consumer(data=data, obj=favorite)
     p.assert_not_called()
diff --git a/api/tests/favorites/test_favorites.py b/api/tests/favorites/test_favorites.py
index 591fe7c9c8f36fbe8c3b625ee9bd26c4be67ad0d..cd75b0d26e3bf77a866eea745105a41642bcf828 100644
--- a/api/tests/favorites/test_favorites.py
+++ b/api/tests/favorites/test_favorites.py
@@ -1,15 +1,14 @@
 import json
+
 import pytest
 from django.urls import reverse
 
-from funkwhale_api.music.models import Track, Artist
 from funkwhale_api.favorites.models import TrackFavorite
 
 
-
 def test_user_can_add_favorite(factories):
-    track = factories['music.Track']()
-    user = factories['users.User']()
+    track = factories["music.Track"]()
+    user = factories["users.User"]()
     f = TrackFavorite.add(track, user)
 
     assert f.track == track
@@ -17,35 +16,34 @@ def test_user_can_add_favorite(factories):
 
 
 def test_user_can_get_his_favorites(factories, logged_in_client, client):
-    favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user)
-    url = reverse('api:v1:favorites:tracks-list')
+    favorite = factories["favorites.TrackFavorite"](user=logged_in_client.user)
+    url = reverse("api:v1:favorites:tracks-list")
     response = logged_in_client.get(url)
 
     expected = [
         {
-            'track': favorite.track.pk,
-            'id': favorite.id,
-            'creation_date': favorite.creation_date.isoformat().replace('+00:00', 'Z'),
+            "track": favorite.track.pk,
+            "id": favorite.id,
+            "creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"),
         }
     ]
-    parsed_json = json.loads(response.content.decode('utf-8'))
+    parsed_json = json.loads(response.content.decode("utf-8"))
 
-    assert expected == parsed_json['results']
+    assert expected == parsed_json["results"]
 
 
-def test_user_can_add_favorite_via_api(
-        factories, logged_in_client, activity_muted):
-    track = factories['music.Track']()
-    url = reverse('api:v1:favorites:tracks-list')
-    response = logged_in_client.post(url, {'track': track.pk})
+def test_user_can_add_favorite_via_api(factories, logged_in_client, activity_muted):
+    track = factories["music.Track"]()
+    url = reverse("api:v1:favorites:tracks-list")
+    response = logged_in_client.post(url, {"track": track.pk})
 
-    favorite = TrackFavorite.objects.latest('id')
+    favorite = TrackFavorite.objects.latest("id")
     expected = {
-        'track': track.pk,
-        'id': favorite.id,
-        'creation_date': favorite.creation_date.isoformat().replace('+00:00', 'Z'),
+        "track": track.pk,
+        "id": favorite.id,
+        "creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"),
     }
-    parsed_json = json.loads(response.content.decode('utf-8'))
+    parsed_json = json.loads(response.content.decode("utf-8"))
 
     assert expected == parsed_json
     assert favorite.track == track
@@ -53,18 +51,19 @@ def test_user_can_add_favorite_via_api(
 
 
 def test_adding_favorites_calls_activity_record(
-        factories, logged_in_client, activity_muted):
-    track = factories['music.Track']()
-    url = reverse('api:v1:favorites:tracks-list')
-    response = logged_in_client.post(url, {'track': track.pk})
+    factories, logged_in_client, activity_muted
+):
+    track = factories["music.Track"]()
+    url = reverse("api:v1:favorites:tracks-list")
+    response = logged_in_client.post(url, {"track": track.pk})
 
-    favorite = TrackFavorite.objects.latest('id')
+    favorite = TrackFavorite.objects.latest("id")
     expected = {
-        'track': track.pk,
-        'id': favorite.id,
-        'creation_date': favorite.creation_date.isoformat().replace('+00:00', 'Z'),
+        "track": track.pk,
+        "id": favorite.id,
+        "creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"),
     }
-    parsed_json = json.loads(response.content.decode('utf-8'))
+    parsed_json = json.loads(response.content.decode("utf-8"))
 
     assert expected == parsed_json
     assert favorite.track == track
@@ -74,44 +73,42 @@ def test_adding_favorites_calls_activity_record(
 
 
 def test_user_can_remove_favorite_via_api(logged_in_client, factories, client):
-    favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user)
-    url = reverse('api:v1:favorites:tracks-detail', kwargs={'pk': favorite.pk})
-    response = client.delete(url, {'track': favorite.track.pk})
+    favorite = factories["favorites.TrackFavorite"](user=logged_in_client.user)
+    url = reverse("api:v1:favorites:tracks-detail", kwargs={"pk": favorite.pk})
+    response = client.delete(url, {"track": favorite.track.pk})
     assert response.status_code == 204
     assert TrackFavorite.objects.count() == 0
 
 
-@pytest.mark.parametrize('method', ['delete', 'post'])
+@pytest.mark.parametrize("method", ["delete", "post"])
 def test_user_can_remove_favorite_via_api_using_track_id(
-        method, factories, logged_in_client):
-    favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user)
+    method, factories, logged_in_client
+):
+    favorite = factories["favorites.TrackFavorite"](user=logged_in_client.user)
 
-    url = reverse('api:v1:favorites:tracks-remove')
+    url = reverse("api:v1:favorites:tracks-remove")
     response = getattr(logged_in_client, method)(
-        url, json.dumps({'track': favorite.track.pk}),
-        content_type='application/json'
+        url, json.dumps({"track": favorite.track.pk}), content_type="application/json"
     )
 
     assert response.status_code == 204
     assert TrackFavorite.objects.count() == 0
 
 
-@pytest.mark.parametrize('url,method', [
-    ('api:v1:favorites:tracks-list', 'get'),
-])
+@pytest.mark.parametrize("url,method", [("api:v1:favorites:tracks-list", "get")])
 def test_url_require_auth(url, method, db, preferences, client):
-    preferences['common__api_authentication_required'] = True
+    preferences["common__api_authentication_required"] = True
     url = reverse(url)
     response = getattr(client, method)(url)
     assert response.status_code == 401
 
 
 def test_can_filter_tracks_by_favorites(factories, logged_in_client):
-    favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user)
+    favorite = factories["favorites.TrackFavorite"](user=logged_in_client.user)
 
-    url = reverse('api:v1:tracks-list')
-    response = logged_in_client.get(url, data={'favorites': True})
+    url = reverse("api:v1:tracks-list")
+    response = logged_in_client.get(url, data={"favorites": True})
 
-    parsed_json = json.loads(response.content.decode('utf-8'))
-    assert parsed_json['count'] == 1
-    assert parsed_json['results'][0]['id'] == favorite.track.id
+    parsed_json = json.loads(response.content.decode("utf-8"))
+    assert parsed_json["count"] == 1
+    assert parsed_json["results"][0]["id"] == favorite.track.id
diff --git a/api/tests/federation/test_activity.py b/api/tests/federation/test_activity.py
index c2673ff3bd1ff18f7cb1f2486c2e18516e36d86d..9c7bb70ecc43400215119a2cef9f8403c6a02783 100644
--- a/api/tests/federation/test_activity.py
+++ b/api/tests/federation/test_activity.py
@@ -1,46 +1,37 @@
-import uuid
 
-from funkwhale_api.federation import activity
-from funkwhale_api.federation import serializers
+from funkwhale_api.federation import activity, serializers
 
 
 def test_deliver(factories, r_mock, mocker, settings):
     settings.CELERY_TASK_ALWAYS_EAGER = True
-    to = factories['federation.Actor']()
-    mocker.patch(
-        'funkwhale_api.federation.actors.get_actor',
-        return_value=to)
-    sender = factories['federation.Actor']()
+    to = factories["federation.Actor"]()
+    mocker.patch("funkwhale_api.federation.actors.get_actor", return_value=to)
+    sender = factories["federation.Actor"]()
     ac = {
-        'id': 'http://test.federation/activity',
-        'type': 'Create',
-        'actor': sender.url,
-        'object': {
-            'id': 'http://test.federation/note',
-            'type': 'Note',
-            'content': 'Hello',
-        }
+        "id": "http://test.federation/activity",
+        "type": "Create",
+        "actor": sender.url,
+        "object": {
+            "id": "http://test.federation/note",
+            "type": "Note",
+            "content": "Hello",
+        },
     }
 
     r_mock.post(to.inbox_url)
 
-    activity.deliver(
-        ac,
-        to=[to.url],
-        on_behalf_of=sender,
-    )
+    activity.deliver(ac, to=[to.url], on_behalf_of=sender)
     request = r_mock.request_history[0]
 
     assert r_mock.called is True
     assert r_mock.call_count == 1
     assert request.url == to.inbox_url
-    assert request.headers['content-type'] == 'application/activity+json'
+    assert request.headers["content-type"] == "application/activity+json"
 
 
 def test_accept_follow(mocker, factories):
-    deliver = mocker.patch(
-        'funkwhale_api.federation.activity.deliver')
-    follow = factories['federation.Follow'](approved=None)
+    deliver = mocker.patch("funkwhale_api.federation.activity.deliver")
+    follow = factories["federation.Follow"](approved=None)
     expected_accept = serializers.AcceptFollowSerializer(follow).data
     activity.accept_follow(follow)
     deliver.assert_called_once_with(
diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py
index 6f73a9b9b2fc2fae47933898ba6c518158fcafbd..736ec8bf2fb9ea6f4d2a0dd01cd2b8ff326c349a 100644
--- a/api/tests/federation/test_actors.py
+++ b/api/tests/federation/test_actors.py
@@ -1,28 +1,21 @@
 import arrow
 import pytest
-import uuid
-
 from django.urls import reverse
 from django.utils import timezone
-
 from rest_framework import exceptions
 
-from funkwhale_api.federation import activity
-from funkwhale_api.federation import actors
-from funkwhale_api.federation import models
-from funkwhale_api.federation import serializers
-from funkwhale_api.federation import utils
+from funkwhale_api.federation import actors, models, serializers, utils
 from funkwhale_api.music import models as music_models
 from funkwhale_api.music import tasks as music_tasks
 
 
 def test_actor_fetching(r_mock):
     payload = {
-        'id': 'https://actor.mock/users/actor#main-key',
-        'owner': 'test',
-        'publicKeyPem': 'test_pem',
+        "id": "https://actor.mock/users/actor#main-key",
+        "owner": "test",
+        "publicKeyPem": "test_pem",
     }
-    actor_url = 'https://actor.mock/'
+    actor_url = "https://actor.mock/"
     r_mock.get(actor_url, json=payload)
     r = actors.get_actor_data(actor_url)
 
@@ -30,7 +23,7 @@ def test_actor_fetching(r_mock):
 
 
 def test_get_actor(factories, r_mock):
-    actor = factories['federation.Actor'].build()
+    actor = factories["federation.Actor"].build()
     payload = serializers.ActorSerializer(actor).data
     r_mock.get(actor.url, json=payload)
     new_actor = actors.get_actor(actor.url)
@@ -40,9 +33,9 @@ def test_get_actor(factories, r_mock):
 
 
 def test_get_actor_use_existing(factories, preferences, mocker):
-    preferences['federation__actor_fetch_delay'] = 60
-    actor = factories['federation.Actor']()
-    get_data = mocker.patch('funkwhale_api.federation.actors.get_actor_data')
+    preferences["federation__actor_fetch_delay"] = 60
+    actor = factories["federation.Actor"]()
+    get_data = mocker.patch("funkwhale_api.federation.actors.get_actor_data")
     new_actor = actors.get_actor(actor.url)
 
     assert new_actor == actor
@@ -50,87 +43,81 @@ def test_get_actor_use_existing(factories, preferences, mocker):
 
 
 def test_get_actor_refresh(factories, preferences, mocker):
-    preferences['federation__actor_fetch_delay'] = 0
-    actor = factories['federation.Actor']()
+    preferences["federation__actor_fetch_delay"] = 0
+    actor = factories["federation.Actor"]()
     payload = serializers.ActorSerializer(actor).data
     # actor changed their username in the meantime
-    payload['preferredUsername'] = 'New me'
-    get_data = mocker.patch(
-        'funkwhale_api.federation.actors.get_actor_data',
-        return_value=payload)
+    payload["preferredUsername"] = "New me"
+    mocker.patch("funkwhale_api.federation.actors.get_actor_data", return_value=payload)
     new_actor = actors.get_actor(actor.url)
 
     assert new_actor == actor
     assert new_actor.last_fetch_date > actor.last_fetch_date
-    assert new_actor.preferred_username == 'New me'
+    assert new_actor.preferred_username == "New me"
 
 
 def test_get_library(db, settings, mocker):
-    get_key_pair = mocker.patch(
-        'funkwhale_api.federation.keys.get_key_pair',
-        return_value=(b'private', b'public'))
+    mocker.patch(
+        "funkwhale_api.federation.keys.get_key_pair",
+        return_value=(b"private", b"public"),
+    )
     expected = {
-        'preferred_username': 'library',
-        'domain': settings.FEDERATION_HOSTNAME,
-        'type': 'Person',
-        'name': '{}\'s library'.format(settings.FEDERATION_HOSTNAME),
-        'manually_approves_followers': True,
-        'public_key': 'public',
-        'url': utils.full_url(
-            reverse(
-                'federation:instance-actors-detail',
-                kwargs={'actor': 'library'})),
-        'shared_inbox_url': utils.full_url(
-            reverse(
-                'federation:instance-actors-inbox',
-                kwargs={'actor': 'library'})),
-        'inbox_url': utils.full_url(
-            reverse(
-                'federation:instance-actors-inbox',
-                kwargs={'actor': 'library'})),
-        'outbox_url': utils.full_url(
-            reverse(
-                'federation:instance-actors-outbox',
-                kwargs={'actor': 'library'})),
-        'summary': 'Bot account to federate with {}\'s library'.format(
-        settings.FEDERATION_HOSTNAME),
+        "preferred_username": "library",
+        "domain": settings.FEDERATION_HOSTNAME,
+        "type": "Person",
+        "name": "{}'s library".format(settings.FEDERATION_HOSTNAME),
+        "manually_approves_followers": True,
+        "public_key": "public",
+        "url": utils.full_url(
+            reverse("federation:instance-actors-detail", kwargs={"actor": "library"})
+        ),
+        "shared_inbox_url": utils.full_url(
+            reverse("federation:instance-actors-inbox", kwargs={"actor": "library"})
+        ),
+        "inbox_url": utils.full_url(
+            reverse("federation:instance-actors-inbox", kwargs={"actor": "library"})
+        ),
+        "outbox_url": utils.full_url(
+            reverse("federation:instance-actors-outbox", kwargs={"actor": "library"})
+        ),
+        "summary": "Bot account to federate with {}'s library".format(
+            settings.FEDERATION_HOSTNAME
+        ),
     }
-    actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
     for key, value in expected.items():
         assert getattr(actor, key) == value
 
 
 def test_get_test(db, mocker, settings):
-    get_key_pair = mocker.patch(
-        'funkwhale_api.federation.keys.get_key_pair',
-        return_value=(b'private', b'public'))
+    mocker.patch(
+        "funkwhale_api.federation.keys.get_key_pair",
+        return_value=(b"private", b"public"),
+    )
     expected = {
-        'preferred_username': 'test',
-        'domain': settings.FEDERATION_HOSTNAME,
-        'type': 'Person',
-        'name': '{}\'s test account'.format(settings.FEDERATION_HOSTNAME),
-        'manually_approves_followers': False,
-        'public_key': 'public',
-        'url': utils.full_url(
-            reverse(
-                'federation:instance-actors-detail',
-                kwargs={'actor': 'test'})),
-        'shared_inbox_url': utils.full_url(
-            reverse(
-                'federation:instance-actors-inbox',
-                kwargs={'actor': 'test'})),
-        'inbox_url': utils.full_url(
-            reverse(
-                'federation:instance-actors-inbox',
-                kwargs={'actor': 'test'})),
-        'outbox_url': utils.full_url(
-            reverse(
-                'federation:instance-actors-outbox',
-                kwargs={'actor': 'test'})),
-        'summary': 'Bot account to test federation with {}. Send me /ping and I\'ll answer you.'.format(
-        settings.FEDERATION_HOSTNAME),
+        "preferred_username": "test",
+        "domain": settings.FEDERATION_HOSTNAME,
+        "type": "Person",
+        "name": "{}'s test account".format(settings.FEDERATION_HOSTNAME),
+        "manually_approves_followers": False,
+        "public_key": "public",
+        "url": utils.full_url(
+            reverse("federation:instance-actors-detail", kwargs={"actor": "test"})
+        ),
+        "shared_inbox_url": utils.full_url(
+            reverse("federation:instance-actors-inbox", kwargs={"actor": "test"})
+        ),
+        "inbox_url": utils.full_url(
+            reverse("federation:instance-actors-inbox", kwargs={"actor": "test"})
+        ),
+        "outbox_url": utils.full_url(
+            reverse("federation:instance-actors-outbox", kwargs={"actor": "test"})
+        ),
+        "summary": "Bot account to test federation with {}. Send me /ping and I'll answer you.".format(
+            settings.FEDERATION_HOSTNAME
+        ),
     }
-    actor = actors.SYSTEM_ACTORS['test'].get_actor_instance()
+    actor = actors.SYSTEM_ACTORS["test"].get_actor_instance()
     for key, value in expected.items():
         assert getattr(actor, key) == value
 
@@ -140,233 +127,208 @@ def test_test_get_outbox():
         "@context": [
             "https://www.w3.org/ns/activitystreams",
             "https://w3id.org/security/v1",
-            {}
+            {},
         ],
         "id": utils.full_url(
-            reverse(
-                'federation:instance-actors-outbox',
-                kwargs={'actor': 'test'})),
+            reverse("federation:instance-actors-outbox", kwargs={"actor": "test"})
+        ),
         "type": "OrderedCollection",
         "totalItems": 0,
-        "orderedItems": []
+        "orderedItems": [],
     }
 
-    data = actors.SYSTEM_ACTORS['test'].get_outbox({}, actor=None)
+    data = actors.SYSTEM_ACTORS["test"].get_outbox({}, actor=None)
 
     assert data == expected
 
 
 def test_test_post_inbox_requires_authenticated_actor():
     with pytest.raises(exceptions.PermissionDenied):
-        actors.SYSTEM_ACTORS['test'].post_inbox({}, actor=None)
+        actors.SYSTEM_ACTORS["test"].post_inbox({}, actor=None)
 
 
 def test_test_post_outbox_validates_actor(nodb_factories):
-    actor = nodb_factories['federation.Actor']()
-    data = {
-        'actor': 'noop'
-    }
+    actor = nodb_factories["federation.Actor"]()
+    data = {"actor": "noop"}
     with pytest.raises(exceptions.ValidationError) as exc_info:
-        actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor)
-        msg = 'The actor making the request do not match'
+        actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor)
+        msg = "The actor making the request do not match"
         assert msg in exc_info.value
 
 
-def test_test_post_inbox_handles_create_note(
-        settings, mocker, factories):
-    deliver = mocker.patch(
-        'funkwhale_api.federation.activity.deliver')
-    actor = factories['federation.Actor']()
+def test_test_post_inbox_handles_create_note(settings, mocker, factories):
+    deliver = mocker.patch("funkwhale_api.federation.activity.deliver")
+    actor = factories["federation.Actor"]()
     now = timezone.now()
-    mocker.patch('django.utils.timezone.now', return_value=now)
+    mocker.patch("django.utils.timezone.now", return_value=now)
     data = {
-        'actor': actor.url,
-        'type': 'Create',
-        'id': 'http://test.federation/activity',
-        'object': {
-            'type': 'Note',
-            'id': 'http://test.federation/object',
-            'content': '<p><a>@mention</a> /ping</p>'
-        }
+        "actor": actor.url,
+        "type": "Create",
+        "id": "http://test.federation/activity",
+        "object": {
+            "type": "Note",
+            "id": "http://test.federation/object",
+            "content": "<p><a>@mention</a> /ping</p>",
+        },
     }
-    test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance()
-    expected_note = factories['federation.Note'](
-        id='https://test.federation/activities/note/{}'.format(
-            now.timestamp()
-        ),
-        content='Pong!',
+    test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance()
+    expected_note = factories["federation.Note"](
+        id="https://test.federation/activities/note/{}".format(now.timestamp()),
+        content="Pong!",
         published=now.isoformat(),
-        inReplyTo=data['object']['id'],
+        inReplyTo=data["object"]["id"],
         cc=[],
         summary=None,
         sensitive=False,
         attributedTo=test_actor.url,
         attachment=[],
         to=[actor.url],
-        url='https://{}/activities/note/{}'.format(
+        url="https://{}/activities/note/{}".format(
             settings.FEDERATION_HOSTNAME, now.timestamp()
         ),
-        tag=[{
-            'href': actor.url,
-            'name': actor.mention_username,
-            'type': 'Mention',
-        }]
+        tag=[{"href": actor.url, "name": actor.mention_username, "type": "Mention"}],
     )
     expected_activity = {
-        '@context': serializers.AP_CONTEXT,
-        'actor': test_actor.url,
-        'id': 'https://{}/activities/note/{}/activity'.format(
+        "@context": serializers.AP_CONTEXT,
+        "actor": test_actor.url,
+        "id": "https://{}/activities/note/{}/activity".format(
             settings.FEDERATION_HOSTNAME, now.timestamp()
         ),
-        'to': actor.url,
-        'type': 'Create',
-        'published': now.isoformat(),
-        'object': expected_note,
-        'cc': [],
+        "to": actor.url,
+        "type": "Create",
+        "published": now.isoformat(),
+        "object": expected_note,
+        "cc": [],
     }
-    actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor)
+    actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor)
     deliver.assert_called_once_with(
         expected_activity,
         to=[actor.url],
-        on_behalf_of=actors.SYSTEM_ACTORS['test'].get_actor_instance()
+        on_behalf_of=actors.SYSTEM_ACTORS["test"].get_actor_instance(),
     )
 
 
 def test_getting_actor_instance_persists_in_db(db):
-    test = actors.SYSTEM_ACTORS['test'].get_actor_instance()
+    test = actors.SYSTEM_ACTORS["test"].get_actor_instance()
     from_db = models.Actor.objects.get(url=test.url)
 
     for f in test._meta.fields:
         assert getattr(from_db, f.name) == getattr(test, f.name)
 
 
-@pytest.mark.parametrize('username,domain,expected', [
-    ('test', 'wrongdomain.com', False),
-    ('notsystem', '', False),
-    ('test', '', True),
-])
-def test_actor_is_system(
-        username, domain, expected, nodb_factories, settings):
+@pytest.mark.parametrize(
+    "username,domain,expected",
+    [("test", "wrongdomain.com", False), ("notsystem", "", False), ("test", "", True)],
+)
+def test_actor_is_system(username, domain, expected, nodb_factories, settings):
     if not domain:
         domain = settings.FEDERATION_HOSTNAME
 
-    actor = nodb_factories['federation.Actor'](
-        preferred_username=username,
-        domain=domain,
+    actor = nodb_factories["federation.Actor"](
+        preferred_username=username, domain=domain
     )
     assert actor.is_system is expected
 
 
-@pytest.mark.parametrize('username,domain,expected', [
-    ('test', 'wrongdomain.com', None),
-    ('notsystem', '', None),
-    ('test', '', actors.SYSTEM_ACTORS['test']),
-])
-def test_actor_is_system(
-        username, domain, expected, nodb_factories, settings):
+@pytest.mark.parametrize(
+    "username,domain,expected",
+    [
+        ("test", "wrongdomain.com", None),
+        ("notsystem", "", None),
+        ("test", "", actors.SYSTEM_ACTORS["test"]),
+    ],
+)
+def test_actor_system_conf(username, domain, expected, nodb_factories, settings):
     if not domain:
         domain = settings.FEDERATION_HOSTNAME
-    actor = nodb_factories['federation.Actor'](
-        preferred_username=username,
-        domain=domain,
+    actor = nodb_factories["federation.Actor"](
+        preferred_username=username, domain=domain
     )
     assert actor.system_conf == expected
 
 
-@pytest.mark.parametrize('value', [False, True])
-def test_library_actor_manually_approves_based_on_preference(
-        value, preferences):
-    preferences['federation__music_needs_approval'] = value
-    library_conf = actors.SYSTEM_ACTORS['library']
+@pytest.mark.parametrize("value", [False, True])
+def test_library_actor_manually_approves_based_on_preference(value, preferences):
+    preferences["federation__music_needs_approval"] = value
+    library_conf = actors.SYSTEM_ACTORS["library"]
     assert library_conf.manually_approves_followers is value
 
 
 def test_system_actor_handle(mocker, nodb_factories):
-    handler = mocker.patch(
-        'funkwhale_api.federation.actors.TestActor.handle_create')
-    actor = nodb_factories['federation.Actor']()
-    activity = nodb_factories['federation.Activity'](
-        type='Create', actor=actor.url)
-    serializer = serializers.ActivitySerializer(
-        data=activity
-    )
+    handler = mocker.patch("funkwhale_api.federation.actors.TestActor.handle_create")
+    actor = nodb_factories["federation.Actor"]()
+    activity = nodb_factories["federation.Activity"](type="Create", actor=actor.url)
+    serializer = serializers.ActivitySerializer(data=activity)
     assert serializer.is_valid()
-    actors.SYSTEM_ACTORS['test'].handle(activity, actor)
+    actors.SYSTEM_ACTORS["test"].handle(activity, actor)
     handler.assert_called_once_with(activity, actor)
 
 
-def test_test_actor_handles_follow(
-        settings, mocker, factories):
-    deliver = mocker.patch(
-        'funkwhale_api.federation.activity.deliver')
-    actor = factories['federation.Actor']()
-    accept_follow = mocker.patch(
-        'funkwhale_api.federation.activity.accept_follow')
-    test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance()
+def test_test_actor_handles_follow(settings, mocker, factories):
+    deliver = mocker.patch("funkwhale_api.federation.activity.deliver")
+    actor = factories["federation.Actor"]()
+    accept_follow = mocker.patch("funkwhale_api.federation.activity.accept_follow")
+    test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance()
     data = {
-        'actor': actor.url,
-        'type': 'Follow',
-        'id': 'http://test.federation/user#follows/267',
-        'object': test_actor.url,
+        "actor": actor.url,
+        "type": "Follow",
+        "id": "http://test.federation/user#follows/267",
+        "object": test_actor.url,
     }
-    actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor)
+    actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor)
     follow = models.Follow.objects.get(target=test_actor, approved=True)
     follow_back = models.Follow.objects.get(actor=test_actor, approved=None)
     accept_follow.assert_called_once_with(follow)
     deliver.assert_called_once_with(
         serializers.FollowSerializer(follow_back).data,
         on_behalf_of=test_actor,
-        to=[actor.url]
+        to=[actor.url],
     )
 
 
-def test_test_actor_handles_undo_follow(
-        settings, mocker, factories):
-    deliver = mocker.patch(
-        'funkwhale_api.federation.activity.deliver')
-    test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance()
-    follow = factories['federation.Follow'](target=test_actor)
-    reverse_follow = factories['federation.Follow'](
-        actor=test_actor, target=follow.actor)
+def test_test_actor_handles_undo_follow(settings, mocker, factories):
+    deliver = mocker.patch("funkwhale_api.federation.activity.deliver")
+    test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance()
+    follow = factories["federation.Follow"](target=test_actor)
+    reverse_follow = factories["federation.Follow"](
+        actor=test_actor, target=follow.actor
+    )
     follow_serializer = serializers.FollowSerializer(follow)
-    reverse_follow_serializer = serializers.FollowSerializer(
-        reverse_follow)
+    reverse_follow_serializer = serializers.FollowSerializer(reverse_follow)
     undo = {
-        '@context': serializers.AP_CONTEXT,
-        'type': 'Undo',
-        'id': follow_serializer.data['id'] + '/undo',
-        'actor': follow.actor.url,
-        'object': follow_serializer.data,
+        "@context": serializers.AP_CONTEXT,
+        "type": "Undo",
+        "id": follow_serializer.data["id"] + "/undo",
+        "actor": follow.actor.url,
+        "object": follow_serializer.data,
     }
     expected_undo = {
-        '@context': serializers.AP_CONTEXT,
-        'type': 'Undo',
-        'id': reverse_follow_serializer.data['id'] + '/undo',
-        'actor': reverse_follow.actor.url,
-        'object': reverse_follow_serializer.data,
+        "@context": serializers.AP_CONTEXT,
+        "type": "Undo",
+        "id": reverse_follow_serializer.data["id"] + "/undo",
+        "actor": reverse_follow.actor.url,
+        "object": reverse_follow_serializer.data,
     }
 
-    actors.SYSTEM_ACTORS['test'].post_inbox(undo, actor=follow.actor)
+    actors.SYSTEM_ACTORS["test"].post_inbox(undo, actor=follow.actor)
     deliver.assert_called_once_with(
-        expected_undo,
-        to=[follow.actor.url],
-        on_behalf_of=test_actor,)
+        expected_undo, to=[follow.actor.url], on_behalf_of=test_actor
+    )
 
     assert models.Follow.objects.count() == 0
 
 
-def test_library_actor_handles_follow_manual_approval(
-        preferences, mocker, factories):
-    preferences['federation__music_needs_approval'] = True
-    actor = factories['federation.Actor']()
+def test_library_actor_handles_follow_manual_approval(preferences, mocker, factories):
+    preferences["federation__music_needs_approval"] = True
+    actor = factories["federation.Actor"]()
     now = timezone.now()
-    mocker.patch('django.utils.timezone.now', return_value=now)
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    mocker.patch("django.utils.timezone.now", return_value=now)
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
     data = {
-        'actor': actor.url,
-        'type': 'Follow',
-        'id': 'http://test.federation/user#follows/267',
-        'object': library_actor.url,
+        "actor": actor.url,
+        "type": "Follow",
+        "id": "http://test.federation/user#follows/267",
+        "object": library_actor.url,
     }
 
     library_actor.system_conf.post_inbox(data, actor=actor)
@@ -376,18 +338,16 @@ def test_library_actor_handles_follow_manual_approval(
     assert follow.approved is None
 
 
-def test_library_actor_handles_follow_auto_approval(
-        preferences, mocker, factories):
-    preferences['federation__music_needs_approval'] = False
-    actor = factories['federation.Actor']()
-    accept_follow = mocker.patch(
-        'funkwhale_api.federation.activity.accept_follow')
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+def test_library_actor_handles_follow_auto_approval(preferences, mocker, factories):
+    preferences["federation__music_needs_approval"] = False
+    actor = factories["federation.Actor"]()
+    mocker.patch("funkwhale_api.federation.activity.accept_follow")
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
     data = {
-        'actor': actor.url,
-        'type': 'Follow',
-        'id': 'http://test.federation/user#follows/267',
-        'object': library_actor.url,
+        "actor": actor.url,
+        "type": "Follow",
+        "id": "http://test.federation/user#follows/267",
+        "object": library_actor.url,
     }
     library_actor.system_conf.post_inbox(data, actor=actor)
 
@@ -397,14 +357,11 @@ def test_library_actor_handles_follow_auto_approval(
     assert follow.approved is True
 
 
-def test_library_actor_handles_accept(
-        mocker, factories):
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    actor = factories['federation.Actor']()
-    pending_follow = factories['federation.Follow'](
-        actor=library_actor,
-        target=actor,
-        approved=None,
+def test_library_actor_handles_accept(mocker, factories):
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    actor = factories["federation.Actor"]()
+    pending_follow = factories["federation.Follow"](
+        actor=library_actor, target=actor, approved=None
     )
     serializer = serializers.AcceptFollowSerializer(pending_follow)
     library_actor.system_conf.post_inbox(serializer.data, actor=actor)
@@ -418,19 +375,19 @@ def test_library_actor_handle_create_audio_no_library(mocker, factories):
     # when we receive inbox create audio, we should not do anything
     # if we don't have a configured library matching the sender
     mocked_create = mocker.patch(
-        'funkwhale_api.federation.serializers.AudioSerializer.create'
+        "funkwhale_api.federation.serializers.AudioSerializer.create"
     )
-    actor = factories['federation.Actor']()
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    actor = factories["federation.Actor"]()
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
     data = {
-        'actor': actor.url,
-        'type': 'Create',
-        'id': 'http://test.federation/audio/create',
-        'object': {
-            'id': 'https://batch.import',
-            'type': 'Collection',
-            'totalItems': 2,
-            'items': factories['federation.Audio'].create_batch(size=2)
+        "actor": actor.url,
+        "type": "Create",
+        "id": "http://test.federation/audio/create",
+        "object": {
+            "id": "https://batch.import",
+            "type": "Collection",
+            "totalItems": 2,
+            "items": factories["federation.Audio"].create_batch(size=2),
         },
     }
     library_actor.system_conf.post_inbox(data, actor=actor)
@@ -439,26 +396,24 @@ def test_library_actor_handle_create_audio_no_library(mocker, factories):
     models.LibraryTrack.objects.count() == 0
 
 
-def test_library_actor_handle_create_audio_no_library_enabled(
-        mocker, factories):
+def test_library_actor_handle_create_audio_no_library_enabled(mocker, factories):
     # when we receive inbox create audio, we should not do anything
     # if we don't have an enabled library
     mocked_create = mocker.patch(
-        'funkwhale_api.federation.serializers.AudioSerializer.create'
+        "funkwhale_api.federation.serializers.AudioSerializer.create"
     )
-    disabled_library = factories['federation.Library'](
-        federation_enabled=False)
+    disabled_library = factories["federation.Library"](federation_enabled=False)
     actor = disabled_library.actor
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
     data = {
-        'actor': actor.url,
-        'type': 'Create',
-        'id': 'http://test.federation/audio/create',
-        'object': {
-            'id': 'https://batch.import',
-            'type': 'Collection',
-            'totalItems': 2,
-            'items': factories['federation.Audio'].create_batch(size=2)
+        "actor": actor.url,
+        "type": "Create",
+        "id": "http://test.federation/audio/create",
+        "object": {
+            "id": "https://batch.import",
+            "type": "Collection",
+            "totalItems": 2,
+            "items": factories["federation.Audio"].create_batch(size=2),
         },
     }
     library_actor.system_conf.post_inbox(data, actor=actor)
@@ -468,97 +423,91 @@ def test_library_actor_handle_create_audio_no_library_enabled(
 
 
 def test_library_actor_handle_create_audio(mocker, factories):
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    remote_library = factories['federation.Library'](
-        federation_enabled=True
-    )
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    remote_library = factories["federation.Library"](federation_enabled=True)
 
     data = {
-        'actor': remote_library.actor.url,
-        'type': 'Create',
-        'id': 'http://test.federation/audio/create',
-        'object': {
-            'id': 'https://batch.import',
-            'type': 'Collection',
-            'totalItems': 2,
-            'items': factories['federation.Audio'].create_batch(size=2)
+        "actor": remote_library.actor.url,
+        "type": "Create",
+        "id": "http://test.federation/audio/create",
+        "object": {
+            "id": "https://batch.import",
+            "type": "Collection",
+            "totalItems": 2,
+            "items": factories["federation.Audio"].create_batch(size=2),
         },
     }
 
     library_actor.system_conf.post_inbox(data, actor=remote_library.actor)
 
-    lts = list(remote_library.tracks.order_by('id'))
+    lts = list(remote_library.tracks.order_by("id"))
 
     assert len(lts) == 2
 
-    for i, a in enumerate(data['object']['items']):
+    for i, a in enumerate(data["object"]["items"]):
         lt = lts[i]
         assert lt.pk is not None
-        assert lt.url == a['id']
+        assert lt.url == a["id"]
         assert lt.library == remote_library
-        assert lt.audio_url == a['url']['href']
-        assert lt.audio_mimetype == a['url']['mediaType']
-        assert lt.metadata == a['metadata']
-        assert lt.title == a['metadata']['recording']['title']
-        assert lt.artist_name == a['metadata']['artist']['name']
-        assert lt.album_title == a['metadata']['release']['title']
-        assert lt.published_date == arrow.get(a['published'])
+        assert lt.audio_url == a["url"]["href"]
+        assert lt.audio_mimetype == a["url"]["mediaType"]
+        assert lt.metadata == a["metadata"]
+        assert lt.title == a["metadata"]["recording"]["title"]
+        assert lt.artist_name == a["metadata"]["artist"]["name"]
+        assert lt.album_title == a["metadata"]["release"]["title"]
+        assert lt.published_date == arrow.get(a["published"])
 
 
 def test_library_actor_handle_create_audio_autoimport(mocker, factories):
-    mocked_import = mocker.patch(
-        'funkwhale_api.common.utils.on_commit')
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    remote_library = factories['federation.Library'](
-        federation_enabled=True,
-        autoimport=True,
+    mocked_import = mocker.patch("funkwhale_api.common.utils.on_commit")
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    remote_library = factories["federation.Library"](
+        federation_enabled=True, autoimport=True
     )
 
     data = {
-        'actor': remote_library.actor.url,
-        'type': 'Create',
-        'id': 'http://test.federation/audio/create',
-        'object': {
-            'id': 'https://batch.import',
-            'type': 'Collection',
-            'totalItems': 2,
-            'items': factories['federation.Audio'].create_batch(size=2)
+        "actor": remote_library.actor.url,
+        "type": "Create",
+        "id": "http://test.federation/audio/create",
+        "object": {
+            "id": "https://batch.import",
+            "type": "Collection",
+            "totalItems": 2,
+            "items": factories["federation.Audio"].create_batch(size=2),
         },
     }
 
     library_actor.system_conf.post_inbox(data, actor=remote_library.actor)
 
-    lts = list(remote_library.tracks.order_by('id'))
+    lts = list(remote_library.tracks.order_by("id"))
 
     assert len(lts) == 2
 
-    for i, a in enumerate(data['object']['items']):
+    for i, a in enumerate(data["object"]["items"]):
         lt = lts[i]
         assert lt.pk is not None
-        assert lt.url == a['id']
+        assert lt.url == a["id"]
         assert lt.library == remote_library
-        assert lt.audio_url == a['url']['href']
-        assert lt.audio_mimetype == a['url']['mediaType']
-        assert lt.metadata == a['metadata']
-        assert lt.title == a['metadata']['recording']['title']
-        assert lt.artist_name == a['metadata']['artist']['name']
-        assert lt.album_title == a['metadata']['release']['title']
-        assert lt.published_date == arrow.get(a['published'])
+        assert lt.audio_url == a["url"]["href"]
+        assert lt.audio_mimetype == a["url"]["mediaType"]
+        assert lt.metadata == a["metadata"]
+        assert lt.title == a["metadata"]["recording"]["title"]
+        assert lt.artist_name == a["metadata"]["artist"]["name"]
+        assert lt.album_title == a["metadata"]["release"]["title"]
+        assert lt.published_date == arrow.get(a["published"])
 
-    batch = music_models.ImportBatch.objects.latest('id')
+    batch = music_models.ImportBatch.objects.latest("id")
 
     assert batch.jobs.count() == len(lts)
-    assert batch.source == 'federation'
+    assert batch.source == "federation"
     assert batch.submitted_by is None
 
-    for i, job in enumerate(batch.jobs.order_by('id')):
+    for i, job in enumerate(batch.jobs.order_by("id")):
         lt = lts[i]
         assert job.library_track == lt
         assert job.mbid == lt.mbid
         assert job.source == lt.url
 
         mocked_import.assert_any_call(
-            music_tasks.import_job_run.delay,
-            import_job_id=job.pk,
-            use_acoustid=False,
+            music_tasks.import_job_run.delay, import_job_id=job.pk, use_acoustid=False
         )
diff --git a/api/tests/federation/test_authentication.py b/api/tests/federation/test_authentication.py
index 2f69e4d4f1dcd2718e7148ebb9887c22e2def3df..95cec5d2ac80d94a7b04de1dd5121c3d270b883b 100644
--- a/api/tests/federation/test_authentication.py
+++ b/api/tests/federation/test_authentication.py
@@ -1,38 +1,33 @@
-from funkwhale_api.federation import authentication
-from funkwhale_api.federation import keys
-from funkwhale_api.federation import signing
+from funkwhale_api.federation import authentication, keys
 
 
 def test_authenticate(factories, mocker, api_request):
     private, public = keys.get_key_pair()
-    actor_url = 'https://test.federation/actor'
+    actor_url = "https://test.federation/actor"
     mocker.patch(
-        'funkwhale_api.federation.actors.get_actor_data',
+        "funkwhale_api.federation.actors.get_actor_data",
         return_value={
-            'id': actor_url,
-            'type': 'Person',
-            'outbox': 'https://test.com',
-            'inbox': 'https://test.com',
-            'preferredUsername': 'test',
-            'publicKey': {
-                'publicKeyPem': public.decode('utf-8'),
-                'owner': actor_url,
-                'id': actor_url + '#main-key',
-            }
-        })
-    signed_request = factories['federation.SignedRequest'](
-        auth__key=private,
-        auth__key_id=actor_url + '#main-key',
-        auth__headers=[
-            'date',
-        ]
+            "id": actor_url,
+            "type": "Person",
+            "outbox": "https://test.com",
+            "inbox": "https://test.com",
+            "preferredUsername": "test",
+            "publicKey": {
+                "publicKeyPem": public.decode("utf-8"),
+                "owner": actor_url,
+                "id": actor_url + "#main-key",
+            },
+        },
+    )
+    signed_request = factories["federation.SignedRequest"](
+        auth__key=private, auth__key_id=actor_url + "#main-key", auth__headers=["date"]
     )
     prepared = signed_request.prepare()
     django_request = api_request.get(
-        '/',
+        "/",
         **{
-            'HTTP_DATE': prepared.headers['date'],
-            'HTTP_SIGNATURE': prepared.headers['signature'],
+            "HTTP_DATE": prepared.headers["date"],
+            "HTTP_SIGNATURE": prepared.headers["signature"],
         }
     )
     authenticator = authentication.SignatureAuthentication()
@@ -40,5 +35,5 @@ def test_authenticate(factories, mocker, api_request):
     actor = django_request.actor
 
     assert user.is_anonymous is True
-    assert actor.public_key == public.decode('utf-8')
+    assert actor.public_key == public.decode("utf-8")
     assert actor.url == actor_url
diff --git a/api/tests/federation/test_keys.py b/api/tests/federation/test_keys.py
index 9dd71be092bc39dcfb09f9b0e931c51a9ea37f5c..0f615868058448e52ee48b3492e7c4e78f25eb85 100644
--- a/api/tests/federation/test_keys.py
+++ b/api/tests/federation/test_keys.py
@@ -3,23 +3,29 @@ import pytest
 from funkwhale_api.federation import keys
 
 
-@pytest.mark.parametrize('raw, expected', [
-    ('algorithm="test",keyId="https://test.com"', 'https://test.com'),
-    ('keyId="https://test.com",algorithm="test"', 'https://test.com'),
-])
+@pytest.mark.parametrize(
+    "raw, expected",
+    [
+        ('algorithm="test",keyId="https://test.com"', "https://test.com"),
+        ('keyId="https://test.com",algorithm="test"', "https://test.com"),
+    ],
+)
 def test_get_key_from_header(raw, expected):
     r = keys.get_key_id_from_signature_header(raw)
     assert r == expected
 
 
-@pytest.mark.parametrize('raw', [
-    'algorithm="test",keyid="badCase"',
-    'algorithm="test",wrong="wrong"',
-    'keyId = "wrong"',
-    'keyId=\'wrong\'',
-    'keyId="notanurl"',
-    'keyId="wrong://test.com"',
-])
+@pytest.mark.parametrize(
+    "raw",
+    [
+        'algorithm="test",keyid="badCase"',
+        'algorithm="test",wrong="wrong"',
+        'keyId = "wrong"',
+        "keyId='wrong'",
+        'keyId="notanurl"',
+        'keyId="wrong://test.com"',
+    ],
+)
 def test_get_key_from_header_invalid(raw):
     with pytest.raises(ValueError):
         keys.get_key_id_from_signature_header(raw)
diff --git a/api/tests/federation/test_library.py b/api/tests/federation/test_library.py
index 7a3abf5d8375ee63598998b40a3b8977b2942198..4e187e4792005e82cfe18cf9a066d31a10f8bbd3 100644
--- a/api/tests/federation/test_library.py
+++ b/api/tests/federation/test_library.py
@@ -1,70 +1,64 @@
-from funkwhale_api.federation import library
-from funkwhale_api.federation import serializers
+from funkwhale_api.federation import library, serializers
 
 
 def test_library_scan_from_account_name(mocker, factories):
-    actor = factories['federation.Actor'](
-        preferred_username='library',
-        domain='test.library'
+    actor = factories["federation.Actor"](
+        preferred_username="library", domain="test.library"
     )
-    get_resource_result = {'actor_url': actor.url}
+    get_resource_result = {"actor_url": actor.url}
     get_resource = mocker.patch(
-        'funkwhale_api.federation.webfinger.get_resource',
-        return_value=get_resource_result)
+        "funkwhale_api.federation.webfinger.get_resource",
+        return_value=get_resource_result,
+    )
 
     actor_data = serializers.ActorSerializer(actor).data
-    actor_data['manuallyApprovesFollowers'] = False
-    actor_data['url'] = [{
-        'type': 'Link',
-        'name': 'library',
-        'mediaType': 'application/activity+json',
-        'href': 'https://test.library'
-    }]
+    actor_data["manuallyApprovesFollowers"] = False
+    actor_data["url"] = [
+        {
+            "type": "Link",
+            "name": "library",
+            "mediaType": "application/activity+json",
+            "href": "https://test.library",
+        }
+    ]
     get_actor_data = mocker.patch(
-        'funkwhale_api.federation.actors.get_actor_data',
-        return_value=actor_data)
+        "funkwhale_api.federation.actors.get_actor_data", return_value=actor_data
+    )
 
-    get_library_data_result = {'test': 'test'}
+    get_library_data_result = {"test": "test"}
     get_library_data = mocker.patch(
-        'funkwhale_api.federation.library.get_library_data',
-        return_value=get_library_data_result)
+        "funkwhale_api.federation.library.get_library_data",
+        return_value=get_library_data_result,
+    )
 
-    result = library.scan_from_account_name('library@test.actor')
+    result = library.scan_from_account_name("library@test.actor")
 
-    get_resource.assert_called_once_with('acct:library@test.actor')
+    get_resource.assert_called_once_with("acct:library@test.actor")
     get_actor_data.assert_called_once_with(actor.url)
-    get_library_data.assert_called_once_with(actor_data['url'][0]['href'])
+    get_library_data.assert_called_once_with(actor_data["url"][0]["href"])
 
     assert result == {
-        'webfinger': get_resource_result,
-        'actor': actor_data,
-        'library': get_library_data_result,
-        'local': {
-            'following': False,
-            'awaiting_approval': False,
-        },
+        "webfinger": get_resource_result,
+        "actor": actor_data,
+        "library": get_library_data_result,
+        "local": {"following": False, "awaiting_approval": False},
     }
 
 
 def test_get_library_data(r_mock, factories):
-    actor = factories['federation.Actor']()
-    url = 'https://test.library'
-    conf = {
-        'id': url,
-        'items': [],
-        'actor': actor,
-        'page_size': 5,
-    }
+    actor = factories["federation.Actor"]()
+    url = "https://test.library"
+    conf = {"id": url, "items": [], "actor": actor, "page_size": 5}
     data = serializers.PaginatedCollectionSerializer(conf).data
     r_mock.get(url, json=data)
 
     result = library.get_library_data(url)
-    for f in ['totalItems', 'actor', 'id', 'type']:
+    for f in ["totalItems", "actor", "id", "type"]:
         assert result[f] == data[f]
 
 
 def test_get_library_data_requires_authentication(r_mock, factories):
-    url = 'https://test.library'
+    url = "https://test.library"
     r_mock.get(url, status_code=403)
     result = library.get_library_data(url)
-    assert result['errors'] == ['Permission denied while scanning library']
+    assert result["errors"] == ["Permission denied while scanning library"]
diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py
index ae158e659fc105bf18a96264cc6968fdab387719..61d0aea96dc9c66a3a96ce47230d62ceef6e5e57 100644
--- a/api/tests/federation/test_models.py
+++ b/api/tests/federation/test_models.py
@@ -1,41 +1,31 @@
 import pytest
-import uuid
-
 from django import db
 
-from funkwhale_api.federation import models
-from funkwhale_api.federation import serializers
-
 
 def test_cannot_duplicate_actor(factories):
-    actor = factories['federation.Actor']()
+    actor = factories["federation.Actor"]()
 
     with pytest.raises(db.IntegrityError):
-        factories['federation.Actor'](
-            domain=actor.domain,
-            preferred_username=actor.preferred_username,
+        factories["federation.Actor"](
+            domain=actor.domain, preferred_username=actor.preferred_username
         )
 
 
 def test_cannot_duplicate_follow(factories):
-    follow = factories['federation.Follow']()
+    follow = factories["federation.Follow"]()
 
     with pytest.raises(db.IntegrityError):
-        factories['federation.Follow'](
-            target=follow.target,
-            actor=follow.actor,
-        )
+        factories["federation.Follow"](target=follow.target, actor=follow.actor)
 
 
 def test_follow_federation_url(factories):
-    follow = factories['federation.Follow'](local=True)
-    expected = '{}#follows/{}'.format(
-        follow.actor.url, follow.uuid)
+    follow = factories["federation.Follow"](local=True)
+    expected = "{}#follows/{}".format(follow.actor.url, follow.uuid)
 
     assert follow.get_federation_url() == expected
 
 
 def test_library_model_unique_per_actor(factories):
-    library = factories['federation.Library']()
+    library = factories["federation.Library"]()
     with pytest.raises(db.IntegrityError):
-        factories['federation.Library'](actor=library.actor)
+        factories["federation.Library"](actor=library.actor)
diff --git a/api/tests/federation/test_permissions.py b/api/tests/federation/test_permissions.py
index a87f26f1b910b77b63ba65061f319d9f8ab4ba50..75f76077cf4828049245e82a553f61e11e1e496b 100644
--- a/api/tests/federation/test_permissions.py
+++ b/api/tests/federation/test_permissions.py
@@ -1,60 +1,61 @@
 from rest_framework.views import APIView
 
-from funkwhale_api.federation import actors
-from funkwhale_api.federation import permissions
+from funkwhale_api.federation import actors, permissions
 
 
-def test_library_follower(
-        factories, api_request, anonymous_user, preferences):
-    preferences['federation__music_needs_approval'] = True
+def test_library_follower(factories, api_request, anonymous_user, preferences):
+    preferences["federation__music_needs_approval"] = True
     view = APIView.as_view()
     permission = permissions.LibraryFollower()
-    request = api_request.get('/')
-    setattr(request, 'user', anonymous_user)
+    request = api_request.get("/")
+    setattr(request, "user", anonymous_user)
     check = permission.has_permission(request, view)
 
     assert check is False
 
 
 def test_library_follower_actor_non_follower(
-        factories, api_request, anonymous_user, preferences):
-    preferences['federation__music_needs_approval'] = True
-    actor = factories['federation.Actor']()
+    factories, api_request, anonymous_user, preferences
+):
+    preferences["federation__music_needs_approval"] = True
+    actor = factories["federation.Actor"]()
     view = APIView.as_view()
     permission = permissions.LibraryFollower()
-    request = api_request.get('/')
-    setattr(request, 'user', anonymous_user)
-    setattr(request, 'actor', actor)
+    request = api_request.get("/")
+    setattr(request, "user", anonymous_user)
+    setattr(request, "actor", actor)
     check = permission.has_permission(request, view)
 
     assert check is False
 
 
 def test_library_follower_actor_follower_not_approved(
-        factories, api_request, anonymous_user, preferences):
-    preferences['federation__music_needs_approval'] = True
-    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    follow = factories['federation.Follow'](target=library, approved=False)
+    factories, api_request, anonymous_user, preferences
+):
+    preferences["federation__music_needs_approval"] = True
+    library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    follow = factories["federation.Follow"](target=library, approved=False)
     view = APIView.as_view()
     permission = permissions.LibraryFollower()
-    request = api_request.get('/')
-    setattr(request, 'user', anonymous_user)
-    setattr(request, 'actor', follow.actor)
+    request = api_request.get("/")
+    setattr(request, "user", anonymous_user)
+    setattr(request, "actor", follow.actor)
     check = permission.has_permission(request, view)
 
     assert check is False
 
 
 def test_library_follower_actor_follower(
-        factories, api_request, anonymous_user, preferences):
-    preferences['federation__music_needs_approval'] = True
-    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    follow = factories['federation.Follow'](target=library, approved=True)
+    factories, api_request, anonymous_user, preferences
+):
+    preferences["federation__music_needs_approval"] = True
+    library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    follow = factories["federation.Follow"](target=library, approved=True)
     view = APIView.as_view()
     permission = permissions.LibraryFollower()
-    request = api_request.get('/')
-    setattr(request, 'user', anonymous_user)
-    setattr(request, 'actor', follow.actor)
+    request = api_request.get("/")
+    setattr(request, "user", anonymous_user)
+    setattr(request, "actor", follow.actor)
     check = permission.has_permission(request, view)
 
     assert check is True
diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py
index fcf2ba1b673d876660aa8825e2f8495bd10a9d99..e966d1711ba3f4f847a7b081560a9901f610b67f 100644
--- a/api/tests/federation/test_serializers.py
+++ b/api/tests/federation/test_serializers.py
@@ -1,37 +1,29 @@
 import arrow
 import pytest
-
-from django.urls import reverse
 from django.core.paginator import Paginator
 
-from funkwhale_api.federation import actors
-from funkwhale_api.federation import keys
-from funkwhale_api.federation import models
-from funkwhale_api.federation import serializers
-from funkwhale_api.federation import utils
+from funkwhale_api.federation import actors, models, serializers, utils
 
 
 def test_actor_serializer_from_ap(db):
     payload = {
-        'id': 'https://test.federation/user',
-        'type': 'Person',
-        'following': 'https://test.federation/user/following',
-        'followers': 'https://test.federation/user/followers',
-        'inbox': 'https://test.federation/user/inbox',
-        'outbox': 'https://test.federation/user/outbox',
-        'preferredUsername': 'user',
-        'name': 'Real User',
-        'summary': 'Hello world',
-        'url': 'https://test.federation/@user',
-        'manuallyApprovesFollowers': False,
-        'publicKey': {
-            'id': 'https://test.federation/user#main-key',
-            'owner': 'https://test.federation/user',
-            'publicKeyPem': 'yolo'
-        },
-        'endpoints': {
-            'sharedInbox': 'https://test.federation/inbox'
+        "id": "https://test.federation/user",
+        "type": "Person",
+        "following": "https://test.federation/user/following",
+        "followers": "https://test.federation/user/followers",
+        "inbox": "https://test.federation/user/inbox",
+        "outbox": "https://test.federation/user/outbox",
+        "preferredUsername": "user",
+        "name": "Real User",
+        "summary": "Hello world",
+        "url": "https://test.federation/@user",
+        "manuallyApprovesFollowers": False,
+        "publicKey": {
+            "id": "https://test.federation/user#main-key",
+            "owner": "https://test.federation/user",
+            "publicKeyPem": "yolo",
         },
+        "endpoints": {"sharedInbox": "https://test.federation/inbox"},
     }
 
     serializer = serializers.ActorSerializer(data=payload)
@@ -39,30 +31,30 @@ def test_actor_serializer_from_ap(db):
 
     actor = serializer.build()
 
-    assert actor.url == payload['id']
-    assert actor.inbox_url == payload['inbox']
-    assert actor.outbox_url == payload['outbox']
-    assert actor.shared_inbox_url == payload['endpoints']['sharedInbox']
-    assert actor.followers_url == payload['followers']
-    assert actor.following_url == payload['following']
-    assert actor.public_key == payload['publicKey']['publicKeyPem']
-    assert actor.preferred_username == payload['preferredUsername']
-    assert actor.name == payload['name']
-    assert actor.domain == 'test.federation'
-    assert actor.summary == payload['summary']
-    assert actor.type == 'Person'
-    assert actor.manually_approves_followers == payload['manuallyApprovesFollowers']
+    assert actor.url == payload["id"]
+    assert actor.inbox_url == payload["inbox"]
+    assert actor.outbox_url == payload["outbox"]
+    assert actor.shared_inbox_url == payload["endpoints"]["sharedInbox"]
+    assert actor.followers_url == payload["followers"]
+    assert actor.following_url == payload["following"]
+    assert actor.public_key == payload["publicKey"]["publicKeyPem"]
+    assert actor.preferred_username == payload["preferredUsername"]
+    assert actor.name == payload["name"]
+    assert actor.domain == "test.federation"
+    assert actor.summary == payload["summary"]
+    assert actor.type == "Person"
+    assert actor.manually_approves_followers == payload["manuallyApprovesFollowers"]
 
 
 def test_actor_serializer_only_mandatory_field_from_ap(db):
     payload = {
-        'id': 'https://test.federation/user',
-        'type': 'Person',
-        'following': 'https://test.federation/user/following',
-        'followers': 'https://test.federation/user/followers',
-        'inbox': 'https://test.federation/user/inbox',
-        'outbox': 'https://test.federation/user/outbox',
-        'preferredUsername': 'user',
+        "id": "https://test.federation/user",
+        "type": "Person",
+        "following": "https://test.federation/user/following",
+        "followers": "https://test.federation/user/followers",
+        "inbox": "https://test.federation/user/inbox",
+        "outbox": "https://test.federation/user/outbox",
+        "preferredUsername": "user",
     }
 
     serializer = serializers.ActorSerializer(data=payload)
@@ -70,58 +62,55 @@ def test_actor_serializer_only_mandatory_field_from_ap(db):
 
     actor = serializer.build()
 
-    assert actor.url == payload['id']
-    assert actor.inbox_url == payload['inbox']
-    assert actor.outbox_url == payload['outbox']
-    assert actor.followers_url == payload['followers']
-    assert actor.following_url == payload['following']
-    assert actor.preferred_username == payload['preferredUsername']
-    assert actor.domain == 'test.federation'
-    assert actor.type == 'Person'
+    assert actor.url == payload["id"]
+    assert actor.inbox_url == payload["inbox"]
+    assert actor.outbox_url == payload["outbox"]
+    assert actor.followers_url == payload["followers"]
+    assert actor.following_url == payload["following"]
+    assert actor.preferred_username == payload["preferredUsername"]
+    assert actor.domain == "test.federation"
+    assert actor.type == "Person"
     assert actor.manually_approves_followers is None
 
 
 def test_actor_serializer_to_ap():
     expected = {
-        '@context': [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
+        "@context": [
+            "https://www.w3.org/ns/activitystreams",
+            "https://w3id.org/security/v1",
             {},
         ],
-        'id': 'https://test.federation/user',
-        'type': 'Person',
-        'following': 'https://test.federation/user/following',
-        'followers': 'https://test.federation/user/followers',
-        'inbox': 'https://test.federation/user/inbox',
-        'outbox': 'https://test.federation/user/outbox',
-        'preferredUsername': 'user',
-        'name': 'Real User',
-        'summary': 'Hello world',
-        'manuallyApprovesFollowers': False,
-        'publicKey': {
-            'id': 'https://test.federation/user#main-key',
-            'owner': 'https://test.federation/user',
-            'publicKeyPem': 'yolo'
-        },
-        'endpoints': {
-            'sharedInbox': 'https://test.federation/inbox'
+        "id": "https://test.federation/user",
+        "type": "Person",
+        "following": "https://test.federation/user/following",
+        "followers": "https://test.federation/user/followers",
+        "inbox": "https://test.federation/user/inbox",
+        "outbox": "https://test.federation/user/outbox",
+        "preferredUsername": "user",
+        "name": "Real User",
+        "summary": "Hello world",
+        "manuallyApprovesFollowers": False,
+        "publicKey": {
+            "id": "https://test.federation/user#main-key",
+            "owner": "https://test.federation/user",
+            "publicKeyPem": "yolo",
         },
+        "endpoints": {"sharedInbox": "https://test.federation/inbox"},
     }
     ac = models.Actor(
-        url=expected['id'],
-        inbox_url=expected['inbox'],
-        outbox_url=expected['outbox'],
-        shared_inbox_url=expected['endpoints']['sharedInbox'],
-        followers_url=expected['followers'],
-        following_url=expected['following'],
-        public_key=expected['publicKey']['publicKeyPem'],
-        preferred_username=expected['preferredUsername'],
-        name=expected['name'],
-        domain='test.federation',
-        summary=expected['summary'],
-        type='Person',
+        url=expected["id"],
+        inbox_url=expected["inbox"],
+        outbox_url=expected["outbox"],
+        shared_inbox_url=expected["endpoints"]["sharedInbox"],
+        followers_url=expected["followers"],
+        following_url=expected["following"],
+        public_key=expected["publicKey"]["publicKeyPem"],
+        preferred_username=expected["preferredUsername"],
+        name=expected["name"],
+        domain="test.federation",
+        summary=expected["summary"],
+        type="Person",
         manually_approves_followers=False,
-
     )
     serializer = serializers.ActorSerializer(ac)
 
@@ -130,22 +119,20 @@ def test_actor_serializer_to_ap():
 
 def test_webfinger_serializer():
     expected = {
-        'subject': 'acct:service@test.federation',
-        'links': [
+        "subject": "acct:service@test.federation",
+        "links": [
             {
-                'rel': 'self',
-                'href': 'https://test.federation/federation/instance/actor',
-                'type': 'application/activity+json',
+                "rel": "self",
+                "href": "https://test.federation/federation/instance/actor",
+                "type": "application/activity+json",
             }
         ],
-        'aliases': [
-            'https://test.federation/federation/instance/actor',
-        ]
+        "aliases": ["https://test.federation/federation/instance/actor"],
     }
     actor = models.Actor(
-        url=expected['links'][0]['href'],
-        preferred_username='service',
-        domain='test.federation',
+        url=expected["links"][0]["href"],
+        preferred_username="service",
+        domain="test.federation",
     )
     serializer = serializers.ActorWebfingerSerializer(actor)
 
@@ -153,33 +140,33 @@ def test_webfinger_serializer():
 
 
 def test_follow_serializer_to_ap(factories):
-    follow = factories['federation.Follow'](local=True)
+    follow = factories["federation.Follow"](local=True)
     serializer = serializers.FollowSerializer(follow)
 
     expected = {
-        '@context': [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
+        "@context": [
+            "https://www.w3.org/ns/activitystreams",
+            "https://w3id.org/security/v1",
             {},
         ],
-        'id': follow.get_federation_url(),
-        'type': 'Follow',
-        'actor': follow.actor.url,
-        'object': follow.target.url,
+        "id": follow.get_federation_url(),
+        "type": "Follow",
+        "actor": follow.actor.url,
+        "object": follow.target.url,
     }
 
     assert serializer.data == expected
 
 
 def test_follow_serializer_save(factories):
-    actor = factories['federation.Actor']()
-    target = factories['federation.Actor']()
-
-    data = expected = {
-        'id': 'https://test.follow',
-        'type': 'Follow',
-        'actor': actor.url,
-        'object': target.url,
+    actor = factories["federation.Actor"]()
+    target = factories["federation.Actor"]()
+
+    data = {
+        "id": "https://test.follow",
+        "type": "Follow",
+        "actor": actor.url,
+        "object": target.url,
     }
     serializer = serializers.FollowSerializer(data=data)
 
@@ -194,39 +181,39 @@ def test_follow_serializer_save(factories):
 
 
 def test_follow_serializer_save_validates_on_context(factories):
-    actor = factories['federation.Actor']()
-    target = factories['federation.Actor']()
-    impostor = factories['federation.Actor']()
-
-    data = expected = {
-        'id': 'https://test.follow',
-        'type': 'Follow',
-        'actor': actor.url,
-        'object': target.url,
+    actor = factories["federation.Actor"]()
+    target = factories["federation.Actor"]()
+    impostor = factories["federation.Actor"]()
+
+    data = {
+        "id": "https://test.follow",
+        "type": "Follow",
+        "actor": actor.url,
+        "object": target.url,
     }
     serializer = serializers.FollowSerializer(
-        data=data,
-        context={'follow_actor': impostor, 'follow_target': impostor})
+        data=data, context={"follow_actor": impostor, "follow_target": impostor}
+    )
 
     assert serializer.is_valid() is False
 
-    assert 'actor' in serializer.errors
-    assert 'object' in serializer.errors
+    assert "actor" in serializer.errors
+    assert "object" in serializer.errors
 
 
 def test_accept_follow_serializer_representation(factories):
-    follow = factories['federation.Follow'](approved=None)
+    follow = factories["federation.Follow"](approved=None)
 
     expected = {
-        '@context': [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
+        "@context": [
+            "https://www.w3.org/ns/activitystreams",
+            "https://w3id.org/security/v1",
             {},
         ],
-        'id': follow.get_federation_url() + '/accept',
-        'type': 'Accept',
-        'actor': follow.target.url,
-        'object': serializers.FollowSerializer(follow).data,
+        "id": follow.get_federation_url() + "/accept",
+        "type": "Accept",
+        "actor": follow.target.url,
+        "object": serializers.FollowSerializer(follow).data,
     }
 
     serializer = serializers.AcceptFollowSerializer(follow)
@@ -235,18 +222,18 @@ def test_accept_follow_serializer_representation(factories):
 
 
 def test_accept_follow_serializer_save(factories):
-    follow = factories['federation.Follow'](approved=None)
+    follow = factories["federation.Follow"](approved=None)
 
     data = {
-        '@context': [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
+        "@context": [
+            "https://www.w3.org/ns/activitystreams",
+            "https://w3id.org/security/v1",
             {},
         ],
-        'id': follow.get_federation_url() + '/accept',
-        'type': 'Accept',
-        'actor': follow.target.url,
-        'object': serializers.FollowSerializer(follow).data,
+        "id": follow.get_federation_url() + "/accept",
+        "type": "Accept",
+        "actor": follow.target.url,
+        "object": serializers.FollowSerializer(follow).data,
     }
 
     serializer = serializers.AcceptFollowSerializer(data=data)
@@ -259,42 +246,42 @@ def test_accept_follow_serializer_save(factories):
 
 
 def test_accept_follow_serializer_validates_on_context(factories):
-    follow = factories['federation.Follow'](approved=None)
-    impostor = factories['federation.Actor']()
+    follow = factories["federation.Follow"](approved=None)
+    impostor = factories["federation.Actor"]()
     data = {
-        '@context': [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
+        "@context": [
+            "https://www.w3.org/ns/activitystreams",
+            "https://w3id.org/security/v1",
             {},
         ],
-        'id': follow.get_federation_url() + '/accept',
-        'type': 'Accept',
-        'actor': impostor.url,
-        'object': serializers.FollowSerializer(follow).data,
+        "id": follow.get_federation_url() + "/accept",
+        "type": "Accept",
+        "actor": impostor.url,
+        "object": serializers.FollowSerializer(follow).data,
     }
 
     serializer = serializers.AcceptFollowSerializer(
-        data=data,
-        context={'follow_actor': impostor, 'follow_target': impostor})
+        data=data, context={"follow_actor": impostor, "follow_target": impostor}
+    )
 
     assert serializer.is_valid() is False
-    assert 'actor' in serializer.errors['object']
-    assert 'object' in serializer.errors['object']
+    assert "actor" in serializer.errors["object"]
+    assert "object" in serializer.errors["object"]
 
 
 def test_undo_follow_serializer_representation(factories):
-    follow = factories['federation.Follow'](approved=True)
+    follow = factories["federation.Follow"](approved=True)
 
     expected = {
-        '@context': [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
+        "@context": [
+            "https://www.w3.org/ns/activitystreams",
+            "https://w3id.org/security/v1",
             {},
         ],
-        'id': follow.get_federation_url() + '/undo',
-        'type': 'Undo',
-        'actor': follow.actor.url,
-        'object': serializers.FollowSerializer(follow).data,
+        "id": follow.get_federation_url() + "/undo",
+        "type": "Undo",
+        "actor": follow.actor.url,
+        "object": serializers.FollowSerializer(follow).data,
     }
 
     serializer = serializers.UndoFollowSerializer(follow)
@@ -303,18 +290,18 @@ def test_undo_follow_serializer_representation(factories):
 
 
 def test_undo_follow_serializer_save(factories):
-    follow = factories['federation.Follow'](approved=True)
+    follow = factories["federation.Follow"](approved=True)
 
     data = {
-        '@context': [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
+        "@context": [
+            "https://www.w3.org/ns/activitystreams",
+            "https://w3id.org/security/v1",
             {},
         ],
-        'id': follow.get_federation_url() + '/undo',
-        'type': 'Undo',
-        'actor': follow.actor.url,
-        'object': serializers.FollowSerializer(follow).data,
+        "id": follow.get_federation_url() + "/undo",
+        "type": "Undo",
+        "actor": follow.actor.url,
+        "object": serializers.FollowSerializer(follow).data,
     }
 
     serializer = serializers.UndoFollowSerializer(data=data)
@@ -326,53 +313,53 @@ def test_undo_follow_serializer_save(factories):
 
 
 def test_undo_follow_serializer_validates_on_context(factories):
-    follow = factories['federation.Follow'](approved=True)
-    impostor = factories['federation.Actor']()
+    follow = factories["federation.Follow"](approved=True)
+    impostor = factories["federation.Actor"]()
     data = {
-        '@context': [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
+        "@context": [
+            "https://www.w3.org/ns/activitystreams",
+            "https://w3id.org/security/v1",
             {},
         ],
-        'id': follow.get_federation_url() + '/undo',
-        'type': 'Undo',
-        'actor': impostor.url,
-        'object': serializers.FollowSerializer(follow).data,
+        "id": follow.get_federation_url() + "/undo",
+        "type": "Undo",
+        "actor": impostor.url,
+        "object": serializers.FollowSerializer(follow).data,
     }
 
     serializer = serializers.UndoFollowSerializer(
-        data=data,
-        context={'follow_actor': impostor, 'follow_target': impostor})
+        data=data, context={"follow_actor": impostor, "follow_target": impostor}
+    )
 
     assert serializer.is_valid() is False
-    assert 'actor' in serializer.errors['object']
-    assert 'object' in serializer.errors['object']
+    assert "actor" in serializer.errors["object"]
+    assert "object" in serializer.errors["object"]
 
 
 def test_paginated_collection_serializer(factories):
-    tfs = factories['music.TrackFile'].create_batch(size=5)
-    actor = factories['federation.Actor'](local=True)
+    tfs = factories["music.TrackFile"].create_batch(size=5)
+    actor = factories["federation.Actor"](local=True)
 
     conf = {
-        'id': 'https://test.federation/test',
-        'items': tfs,
-        'item_serializer': serializers.AudioSerializer,
-        'actor': actor,
-        'page_size': 2,
+        "id": "https://test.federation/test",
+        "items": tfs,
+        "item_serializer": serializers.AudioSerializer,
+        "actor": actor,
+        "page_size": 2,
     }
     expected = {
-        '@context': [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
+        "@context": [
+            "https://www.w3.org/ns/activitystreams",
+            "https://w3id.org/security/v1",
             {},
         ],
-        'type': 'Collection',
-        'id': conf['id'],
-        'actor': actor.url,
-        'totalItems': len(tfs),
-        'current': conf['id'] + '?page=1',
-        'last': conf['id'] + '?page=3',
-        'first': conf['id'] + '?page=1',
+        "type": "Collection",
+        "id": conf["id"],
+        "actor": actor.url,
+        "totalItems": len(tfs),
+        "current": conf["id"] + "?page=1",
+        "last": conf["id"] + "?page=3",
+        "first": conf["id"] + "?page=1",
     }
 
     serializer = serializers.PaginatedCollectionSerializer(conf)
@@ -382,108 +369,102 @@ def test_paginated_collection_serializer(factories):
 
 def test_paginated_collection_serializer_validation():
     data = {
-        'type': 'Collection',
-        'id': 'https://test.federation/test',
-        'totalItems': 5,
-        'actor': 'http://test.actor',
-        'first': 'https://test.federation/test?page=1',
-        'last': 'https://test.federation/test?page=1',
-        'items': []
+        "type": "Collection",
+        "id": "https://test.federation/test",
+        "totalItems": 5,
+        "actor": "http://test.actor",
+        "first": "https://test.federation/test?page=1",
+        "last": "https://test.federation/test?page=1",
+        "items": [],
     }
 
-    serializer = serializers.PaginatedCollectionSerializer(
-        data=data
-    )
+    serializer = serializers.PaginatedCollectionSerializer(data=data)
 
     assert serializer.is_valid(raise_exception=True) is True
-    assert serializer.validated_data['totalItems'] == 5
-    assert serializer.validated_data['id'] == data['id']
-    assert serializer.validated_data['actor'] == data['actor']
+    assert serializer.validated_data["totalItems"] == 5
+    assert serializer.validated_data["id"] == data["id"]
+    assert serializer.validated_data["actor"] == data["actor"]
 
 
 def test_collection_page_serializer_validation():
-    base = 'https://test.federation/test'
+    base = "https://test.federation/test"
     data = {
-        'type': 'CollectionPage',
-        'id': base + '?page=2',
-        'totalItems': 5,
-        'actor': 'https://test.actor',
-        'items': [],
-        'first': 'https://test.federation/test?page=1',
-        'last': 'https://test.federation/test?page=3',
-        'prev': base + '?page=1',
-        'next': base + '?page=3',
-        'partOf': base,
+        "type": "CollectionPage",
+        "id": base + "?page=2",
+        "totalItems": 5,
+        "actor": "https://test.actor",
+        "items": [],
+        "first": "https://test.federation/test?page=1",
+        "last": "https://test.federation/test?page=3",
+        "prev": base + "?page=1",
+        "next": base + "?page=3",
+        "partOf": base,
     }
 
-    serializer = serializers.CollectionPageSerializer(
-        data=data
-    )
+    serializer = serializers.CollectionPageSerializer(data=data)
 
     assert serializer.is_valid(raise_exception=True) is True
-    assert serializer.validated_data['totalItems'] == 5
-    assert serializer.validated_data['id'] == data['id']
-    assert serializer.validated_data['actor'] == data['actor']
-    assert serializer.validated_data['items'] == []
-    assert serializer.validated_data['prev'] == data['prev']
-    assert serializer.validated_data['next'] == data['next']
-    assert serializer.validated_data['partOf'] == data['partOf']
+    assert serializer.validated_data["totalItems"] == 5
+    assert serializer.validated_data["id"] == data["id"]
+    assert serializer.validated_data["actor"] == data["actor"]
+    assert serializer.validated_data["items"] == []
+    assert serializer.validated_data["prev"] == data["prev"]
+    assert serializer.validated_data["next"] == data["next"]
+    assert serializer.validated_data["partOf"] == data["partOf"]
 
 
 def test_collection_page_serializer_can_validate_child():
     data = {
-        'type': 'CollectionPage',
-        'id': 'https://test.page?page=2',
-        'actor': 'https://test.actor',
-        'first': 'https://test.page?page=1',
-        'last': 'https://test.page?page=3',
-        'partOf': 'https://test.page',
-        'totalItems': 1,
-        'items': [{'in': 'valid'}],
+        "type": "CollectionPage",
+        "id": "https://test.page?page=2",
+        "actor": "https://test.actor",
+        "first": "https://test.page?page=1",
+        "last": "https://test.page?page=3",
+        "partOf": "https://test.page",
+        "totalItems": 1,
+        "items": [{"in": "valid"}],
     }
 
     serializer = serializers.CollectionPageSerializer(
-        data=data,
-        context={'item_serializer': serializers.AudioSerializer}
+        data=data, context={"item_serializer": serializers.AudioSerializer}
     )
 
     # child are validated but not included in data if not valid
     assert serializer.is_valid(raise_exception=True) is True
-    assert len(serializer.validated_data['items']) == 0
+    assert len(serializer.validated_data["items"]) == 0
 
 
 def test_collection_page_serializer(factories):
-    tfs = factories['music.TrackFile'].create_batch(size=5)
-    actor = factories['federation.Actor'](local=True)
+    tfs = factories["music.TrackFile"].create_batch(size=5)
+    actor = factories["federation.Actor"](local=True)
 
     conf = {
-        'id': 'https://test.federation/test',
-        'item_serializer': serializers.AudioSerializer,
-        'actor': actor,
-        'page': Paginator(tfs, 2).page(2),
+        "id": "https://test.federation/test",
+        "item_serializer": serializers.AudioSerializer,
+        "actor": actor,
+        "page": Paginator(tfs, 2).page(2),
     }
     expected = {
-        '@context': [
-            'https://www.w3.org/ns/activitystreams',
-            'https://w3id.org/security/v1',
+        "@context": [
+            "https://www.w3.org/ns/activitystreams",
+            "https://w3id.org/security/v1",
             {},
         ],
-        'type': 'CollectionPage',
-        'id': conf['id'] + '?page=2',
-        'actor': actor.url,
-        'totalItems': len(tfs),
-        'partOf': conf['id'],
-        'prev': conf['id'] + '?page=1',
-        'next': conf['id'] + '?page=3',
-        'first': conf['id'] + '?page=1',
-        'last': conf['id'] + '?page=3',
-        'items': [
-            conf['item_serializer'](
-                i,
-                context={'actor': actor, 'include_ap_context': False}
+        "type": "CollectionPage",
+        "id": conf["id"] + "?page=2",
+        "actor": actor.url,
+        "totalItems": len(tfs),
+        "partOf": conf["id"],
+        "prev": conf["id"] + "?page=1",
+        "next": conf["id"] + "?page=3",
+        "first": conf["id"] + "?page=1",
+        "last": conf["id"] + "?page=3",
+        "items": [
+            conf["item_serializer"](
+                i, context={"actor": actor, "include_ap_context": False}
             ).data
-            for i in conf['page'].object_list
-        ]
+            for i in conf["page"].object_list
+        ],
     }
 
     serializer = serializers.CollectionPageSerializer(conf)
@@ -492,35 +473,37 @@ def test_collection_page_serializer(factories):
 
 
 def test_activity_pub_audio_serializer_to_library_track(factories):
-    remote_library = factories['federation.Library']()
-    audio = factories['federation.Audio']()
+    remote_library = factories["federation.Library"]()
+    audio = factories["federation.Audio"]()
     serializer = serializers.AudioSerializer(
-        data=audio, context={'library': remote_library})
+        data=audio, context={"library": remote_library}
+    )
 
     assert serializer.is_valid(raise_exception=True)
 
     lt = serializer.save()
 
     assert lt.pk is not None
-    assert lt.url == audio['id']
+    assert lt.url == audio["id"]
     assert lt.library == remote_library
-    assert lt.audio_url == audio['url']['href']
-    assert lt.audio_mimetype == audio['url']['mediaType']
-    assert lt.metadata == audio['metadata']
-    assert lt.title == audio['metadata']['recording']['title']
-    assert lt.artist_name == audio['metadata']['artist']['name']
-    assert lt.album_title == audio['metadata']['release']['title']
-    assert lt.published_date == arrow.get(audio['published'])
-
-
-def test_activity_pub_audio_serializer_to_library_track_no_duplicate(
-        factories):
-    remote_library = factories['federation.Library']()
-    audio = factories['federation.Audio']()
+    assert lt.audio_url == audio["url"]["href"]
+    assert lt.audio_mimetype == audio["url"]["mediaType"]
+    assert lt.metadata == audio["metadata"]
+    assert lt.title == audio["metadata"]["recording"]["title"]
+    assert lt.artist_name == audio["metadata"]["artist"]["name"]
+    assert lt.album_title == audio["metadata"]["release"]["title"]
+    assert lt.published_date == arrow.get(audio["published"])
+
+
+def test_activity_pub_audio_serializer_to_library_track_no_duplicate(factories):
+    remote_library = factories["federation.Library"]()
+    audio = factories["federation.Audio"]()
     serializer1 = serializers.AudioSerializer(
-        data=audio, context={'library': remote_library})
+        data=audio, context={"library": remote_library}
+    )
     serializer2 = serializers.AudioSerializer(
-        data=audio, context={'library': remote_library})
+        data=audio, context={"library": remote_library}
+    )
 
     assert serializer1.is_valid() is True
     assert serializer2.is_valid() is True
@@ -533,192 +516,168 @@ def test_activity_pub_audio_serializer_to_library_track_no_duplicate(
 
 
 def test_activity_pub_audio_serializer_to_ap(factories):
-    tf = factories['music.TrackFile'](
-        mimetype='audio/mp3',
-        bitrate=42,
-        duration=43,
-        size=44,
+    tf = factories["music.TrackFile"](
+        mimetype="audio/mp3", bitrate=42, duration=43, size=44
     )
-    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
     expected = {
-        '@context': serializers.AP_CONTEXT,
-        'type': 'Audio',
-        'id': tf.get_federation_url(),
-        'name': tf.track.full_name,
-        'published': tf.creation_date.isoformat(),
-        'updated': tf.modification_date.isoformat(),
-        'metadata': {
-            'artist': {
-                'musicbrainz_id': tf.track.artist.mbid,
-                'name': tf.track.artist.name,
-            },
-            'release': {
-                'musicbrainz_id': tf.track.album.mbid,
-                'title': tf.track.album.title,
+        "@context": serializers.AP_CONTEXT,
+        "type": "Audio",
+        "id": tf.get_federation_url(),
+        "name": tf.track.full_name,
+        "published": tf.creation_date.isoformat(),
+        "updated": tf.modification_date.isoformat(),
+        "metadata": {
+            "artist": {
+                "musicbrainz_id": tf.track.artist.mbid,
+                "name": tf.track.artist.name,
             },
-            'recording': {
-                'musicbrainz_id': tf.track.mbid,
-                'title': tf.track.title,
+            "release": {
+                "musicbrainz_id": tf.track.album.mbid,
+                "title": tf.track.album.title,
             },
-            'size': tf.size,
-            'length': tf.duration,
-            'bitrate': tf.bitrate,
+            "recording": {"musicbrainz_id": tf.track.mbid, "title": tf.track.title},
+            "size": tf.size,
+            "length": tf.duration,
+            "bitrate": tf.bitrate,
         },
-        'url': {
-            'href': utils.full_url(tf.path),
-            'type': 'Link',
-            'mediaType': 'audio/mp3'
+        "url": {
+            "href": utils.full_url(tf.path),
+            "type": "Link",
+            "mediaType": "audio/mp3",
         },
-        'attributedTo': [
-            library.url
-        ]
+        "attributedTo": [library.url],
     }
 
-    serializer = serializers.AudioSerializer(tf, context={'actor': library})
+    serializer = serializers.AudioSerializer(tf, context={"actor": library})
 
     assert serializer.data == expected
 
 
 def test_activity_pub_audio_serializer_to_ap_no_mbid(factories):
-    tf = factories['music.TrackFile'](
-        mimetype='audio/mp3',
+    tf = factories["music.TrackFile"](
+        mimetype="audio/mp3",
         track__mbid=None,
         track__album__mbid=None,
         track__album__artist__mbid=None,
     )
-    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
     expected = {
-        '@context': serializers.AP_CONTEXT,
-        'type': 'Audio',
-        'id': tf.get_federation_url(),
-        'name': tf.track.full_name,
-        'published': tf.creation_date.isoformat(),
-        'updated': tf.modification_date.isoformat(),
-        'metadata': {
-            'artist': {
-                'name': tf.track.artist.name,
-                'musicbrainz_id': None,
-            },
-            'release': {
-                'title': tf.track.album.title,
-                'musicbrainz_id': None,
-            },
-            'recording': {
-                'title': tf.track.title,
-                'musicbrainz_id': None,
-            },
-            'size': None,
-            'length': None,
-            'bitrate': None,
+        "@context": serializers.AP_CONTEXT,
+        "type": "Audio",
+        "id": tf.get_federation_url(),
+        "name": tf.track.full_name,
+        "published": tf.creation_date.isoformat(),
+        "updated": tf.modification_date.isoformat(),
+        "metadata": {
+            "artist": {"name": tf.track.artist.name, "musicbrainz_id": None},
+            "release": {"title": tf.track.album.title, "musicbrainz_id": None},
+            "recording": {"title": tf.track.title, "musicbrainz_id": None},
+            "size": None,
+            "length": None,
+            "bitrate": None,
         },
-        'url': {
-            'href': utils.full_url(tf.path),
-            'type': 'Link',
-            'mediaType': 'audio/mp3'
+        "url": {
+            "href": utils.full_url(tf.path),
+            "type": "Link",
+            "mediaType": "audio/mp3",
         },
-        'attributedTo': [
-            library.url
-        ]
+        "attributedTo": [library.url],
     }
 
-    serializer = serializers.AudioSerializer(tf, context={'actor': library})
+    serializer = serializers.AudioSerializer(tf, context={"actor": library})
 
     assert serializer.data == expected
 
 
 def test_collection_serializer_to_ap(factories):
-    tf1 = factories['music.TrackFile'](mimetype='audio/mp3')
-    tf2 = factories['music.TrackFile'](mimetype='audio/ogg')
-    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+    tf1 = factories["music.TrackFile"](mimetype="audio/mp3")
+    tf2 = factories["music.TrackFile"](mimetype="audio/ogg")
+    library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
     expected = {
-        '@context': serializers.AP_CONTEXT,
-        'id': 'https://test.id',
-        'actor': library.url,
-        'totalItems': 2,
-        'type': 'Collection',
-        'items': [
+        "@context": serializers.AP_CONTEXT,
+        "id": "https://test.id",
+        "actor": library.url,
+        "totalItems": 2,
+        "type": "Collection",
+        "items": [
             serializers.AudioSerializer(
-                tf1, context={'actor': library, 'include_ap_context': False}
+                tf1, context={"actor": library, "include_ap_context": False}
             ).data,
             serializers.AudioSerializer(
-                tf2, context={'actor': library, 'include_ap_context': False}
+                tf2, context={"actor": library, "include_ap_context": False}
             ).data,
-        ]
+        ],
     }
 
     collection = {
-        'id': expected['id'],
-        'actor': library,
-        'items': [tf1, tf2],
-        'item_serializer': serializers.AudioSerializer
+        "id": expected["id"],
+        "actor": library,
+        "items": [tf1, tf2],
+        "item_serializer": serializers.AudioSerializer,
     }
     serializer = serializers.CollectionSerializer(
-        collection, context={'actor': library, 'id': 'https://test.id'})
+        collection, context={"actor": library, "id": "https://test.id"}
+    )
 
     assert serializer.data == expected
 
 
 def test_api_library_create_serializer_save(factories, r_mock):
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    actor = factories['federation.Actor']()
-    follow = factories['federation.Follow'](
-        target=actor,
-        actor=library_actor,
-    )
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    actor = factories["federation.Actor"]()
+    follow = factories["federation.Follow"](target=actor, actor=library_actor)
     actor_data = serializers.ActorSerializer(actor).data
-    actor_data['url'] = [{
-        'href': 'https://test.library',
-        'name': 'library',
-        'type': 'Link',
-    }]
+    actor_data["url"] = [
+        {"href": "https://test.library", "name": "library", "type": "Link"}
+    ]
     library_conf = {
-        'id': 'https://test.library',
-        'items': range(10),
-        'actor': actor,
-        'page_size': 5,
+        "id": "https://test.library",
+        "items": range(10),
+        "actor": actor,
+        "page_size": 5,
     }
     library_data = serializers.PaginatedCollectionSerializer(library_conf).data
     r_mock.get(actor.url, json=actor_data)
-    r_mock.get('https://test.library', json=library_data)
+    r_mock.get("https://test.library", json=library_data)
     data = {
-        'actor': actor.url,
-        'autoimport': False,
-        'federation_enabled': True,
-        'download_files': False,
+        "actor": actor.url,
+        "autoimport": False,
+        "federation_enabled": True,
+        "download_files": False,
     }
 
     serializer = serializers.APILibraryCreateSerializer(data=data)
     assert serializer.is_valid(raise_exception=True) is True
     library = serializer.save()
-    follow = models.Follow.objects.get(
-        target=actor, actor=library_actor, approved=None)
+    follow = models.Follow.objects.get(target=actor, actor=library_actor, approved=None)
 
-    assert library.autoimport is data['autoimport']
-    assert library.federation_enabled is data['federation_enabled']
-    assert library.download_files is data['download_files']
+    assert library.autoimport is data["autoimport"]
+    assert library.federation_enabled is data["federation_enabled"]
+    assert library.download_files is data["download_files"]
     assert library.tracks_count == 10
     assert library.actor == actor
     assert library.follow == follow
 
 
 def test_tapi_library_track_serializer_not_imported(factories):
-    lt = factories['federation.LibraryTrack']()
+    lt = factories["federation.LibraryTrack"]()
     serializer = serializers.APILibraryTrackSerializer(lt)
 
-    assert serializer.get_status(lt) == 'not_imported'
+    assert serializer.get_status(lt) == "not_imported"
 
 
 def test_tapi_library_track_serializer_imported(factories):
-    tf = factories['music.TrackFile'](federation=True)
+    tf = factories["music.TrackFile"](federation=True)
     lt = tf.library_track
     serializer = serializers.APILibraryTrackSerializer(lt)
 
-    assert serializer.get_status(lt) == 'imported'
+    assert serializer.get_status(lt) == "imported"
 
 
 def test_tapi_library_track_serializer_import_pending(factories):
-    job = factories['music.ImportJob'](federation=True, status='pending')
+    job = factories["music.ImportJob"](federation=True, status="pending")
     lt = job.library_track
     serializer = serializers.APILibraryTrackSerializer(lt)
 
-    assert serializer.get_status(lt) == 'import_pending'
+    assert serializer.get_status(lt) == "import_pending"
diff --git a/api/tests/federation/test_signing.py b/api/tests/federation/test_signing.py
index 0c1ec2e0ba1dcd16722c2615eeeb48a5f01d7e87..159f31cd96fdce747cd48120d4999940de5dd66c 100644
--- a/api/tests/federation/test_signing.py
+++ b/api/tests/federation/test_signing.py
@@ -1,43 +1,35 @@
 import cryptography.exceptions
-import io
 import pytest
-import requests_http_signature
 
-from funkwhale_api.federation import signing
-from funkwhale_api.federation import keys
+from funkwhale_api.federation import keys, signing
 
 
 def test_can_sign_and_verify_request(nodb_factories):
-    private, public = nodb_factories['federation.KeyPair']()
-    auth = nodb_factories['federation.SignatureAuth'](key=private)
-    request = nodb_factories['federation.SignedRequest'](
-        auth=auth
-    )
+    private, public = nodb_factories["federation.KeyPair"]()
+    auth = nodb_factories["federation.SignatureAuth"](key=private)
+    request = nodb_factories["federation.SignedRequest"](auth=auth)
     prepared_request = request.prepare()
-    assert 'date' in prepared_request.headers
-    assert 'signature' in prepared_request.headers
-    assert signing.verify(
-        prepared_request, public) is None
+    assert "date" in prepared_request.headers
+    assert "signature" in prepared_request.headers
+    assert signing.verify(prepared_request, public) is None
 
 
 def test_can_sign_and_verify_request_digest(nodb_factories):
-    private, public = nodb_factories['federation.KeyPair']()
-    auth = nodb_factories['federation.SignatureAuth'](key=private)
-    request = nodb_factories['federation.SignedRequest'](
-        auth=auth,
-        method='post',
-        data=b'hello=world'
+    private, public = nodb_factories["federation.KeyPair"]()
+    auth = nodb_factories["federation.SignatureAuth"](key=private)
+    request = nodb_factories["federation.SignedRequest"](
+        auth=auth, method="post", data=b"hello=world"
     )
     prepared_request = request.prepare()
-    assert 'date' in prepared_request.headers
-    assert 'digest' in prepared_request.headers
-    assert 'signature' in prepared_request.headers
+    assert "date" in prepared_request.headers
+    assert "digest" in prepared_request.headers
+    assert "signature" in prepared_request.headers
     assert signing.verify(prepared_request, public) is None
 
 
 def test_verify_fails_with_wrong_key(nodb_factories):
-    wrong_private, wrong_public = nodb_factories['federation.KeyPair']()
-    request = nodb_factories['federation.SignedRequest']()
+    wrong_private, wrong_public = nodb_factories["federation.KeyPair"]()
+    request = nodb_factories["federation.SignedRequest"]()
     prepared_request = request.prepare()
 
     with pytest.raises(cryptography.exceptions.InvalidSignature):
@@ -46,18 +38,15 @@ def test_verify_fails_with_wrong_key(nodb_factories):
 
 def test_can_verify_django_request(factories, fake_request):
     private_key, public_key = keys.get_key_pair()
-    signed_request = factories['federation.SignedRequest'](
-        auth__key=private_key,
-        auth__headers=[
-            'date',
-        ]
+    signed_request = factories["federation.SignedRequest"](
+        auth__key=private_key, auth__headers=["date"]
     )
     prepared = signed_request.prepare()
     django_request = fake_request.get(
-        '/',
+        "/",
         **{
-            'HTTP_DATE': prepared.headers['date'],
-            'HTTP_SIGNATURE': prepared.headers['signature'],
+            "HTTP_DATE": prepared.headers["date"],
+            "HTTP_SIGNATURE": prepared.headers["signature"],
         }
     )
     assert signing.verify_django(django_request, public_key) is None
@@ -65,22 +54,19 @@ def test_can_verify_django_request(factories, fake_request):
 
 def test_can_verify_django_request_digest(factories, fake_request):
     private_key, public_key = keys.get_key_pair()
-    signed_request = factories['federation.SignedRequest'](
+    signed_request = factories["federation.SignedRequest"](
         auth__key=private_key,
-        method='post',
-        data=b'hello=world',
-        auth__headers=[
-            'date',
-            'digest',
-        ]
+        method="post",
+        data=b"hello=world",
+        auth__headers=["date", "digest"],
     )
     prepared = signed_request.prepare()
     django_request = fake_request.post(
-        '/',
+        "/",
         **{
-            'HTTP_DATE': prepared.headers['date'],
-            'HTTP_DIGEST': prepared.headers['digest'],
-            'HTTP_SIGNATURE': prepared.headers['signature'],
+            "HTTP_DATE": prepared.headers["date"],
+            "HTTP_DIGEST": prepared.headers["digest"],
+            "HTTP_SIGNATURE": prepared.headers["signature"],
         }
     )
 
@@ -89,22 +75,19 @@ def test_can_verify_django_request_digest(factories, fake_request):
 
 def test_can_verify_django_request_digest_failure(factories, fake_request):
     private_key, public_key = keys.get_key_pair()
-    signed_request = factories['federation.SignedRequest'](
+    signed_request = factories["federation.SignedRequest"](
         auth__key=private_key,
-        method='post',
-        data=b'hello=world',
-        auth__headers=[
-            'date',
-            'digest',
-        ]
+        method="post",
+        data=b"hello=world",
+        auth__headers=["date", "digest"],
     )
     prepared = signed_request.prepare()
     django_request = fake_request.post(
-        '/',
+        "/",
         **{
-            'HTTP_DATE': prepared.headers['date'],
-            'HTTP_DIGEST': prepared.headers['digest'] + 'noop',
-            'HTTP_SIGNATURE': prepared.headers['signature'],
+            "HTTP_DATE": prepared.headers["date"],
+            "HTTP_DIGEST": prepared.headers["digest"] + "noop",
+            "HTTP_SIGNATURE": prepared.headers["signature"],
         }
     )
 
@@ -114,19 +97,12 @@ def test_can_verify_django_request_digest_failure(factories, fake_request):
 
 def test_can_verify_django_request_failure(factories, fake_request):
     private_key, public_key = keys.get_key_pair()
-    signed_request = factories['federation.SignedRequest'](
-        auth__key=private_key,
-        auth__headers=[
-            'date',
-        ]
+    signed_request = factories["federation.SignedRequest"](
+        auth__key=private_key, auth__headers=["date"]
     )
     prepared = signed_request.prepare()
     django_request = fake_request.get(
-        '/',
-        **{
-            'HTTP_DATE': 'Wrong',
-            'HTTP_SIGNATURE': prepared.headers['signature'],
-        }
+        "/", **{"HTTP_DATE": "Wrong", "HTTP_SIGNATURE": prepared.headers["signature"]}
     )
     with pytest.raises(cryptography.exceptions.InvalidSignature):
         signing.verify_django(django_request, public_key)
diff --git a/api/tests/federation/test_tasks.py b/api/tests/federation/test_tasks.py
index 3517e8feb2c317f319192991dd09917a3070e553..bc10eae9564cb4a6ae89882ba6478c92ad9137fd 100644
--- a/api/tests/federation/test_tasks.py
+++ b/api/tests/federation/test_tasks.py
@@ -1,132 +1,119 @@
 import datetime
 import os
 import pathlib
-import pytest
 
 from django.core.paginator import Paginator
 from django.utils import timezone
 
-from funkwhale_api.federation import serializers
-from funkwhale_api.federation import tasks
+from funkwhale_api.federation import serializers, tasks
 
 
 def test_scan_library_does_nothing_if_federation_disabled(mocker, factories):
-    library = factories['federation.Library'](federation_enabled=False)
+    library = factories["federation.Library"](federation_enabled=False)
     tasks.scan_library(library_id=library.pk)
 
     assert library.tracks.count() == 0
 
 
-def test_scan_library_page_does_nothing_if_federation_disabled(
-        mocker, factories):
-    library = factories['federation.Library'](federation_enabled=False)
+def test_scan_library_page_does_nothing_if_federation_disabled(mocker, factories):
+    library = factories["federation.Library"](federation_enabled=False)
     tasks.scan_library_page(library_id=library.pk, page_url=None)
 
     assert library.tracks.count() == 0
 
 
-def test_scan_library_fetches_page_and_calls_scan_page(
-        mocker, factories, r_mock):
+def test_scan_library_fetches_page_and_calls_scan_page(mocker, factories, r_mock):
     now = timezone.now()
-    library = factories['federation.Library'](federation_enabled=True)
+    library = factories["federation.Library"](federation_enabled=True)
     collection_conf = {
-        'actor': library.actor,
-        'id': library.url,
-        'page_size': 10,
-        'items': range(10),
+        "actor": library.actor,
+        "id": library.url,
+        "page_size": 10,
+        "items": range(10),
     }
     collection = serializers.PaginatedCollectionSerializer(collection_conf)
-    scan_page = mocker.patch(
-        'funkwhale_api.federation.tasks.scan_library_page.delay')
-    r_mock.get(collection_conf['id'], json=collection.data)
+    scan_page = mocker.patch("funkwhale_api.federation.tasks.scan_library_page.delay")
+    r_mock.get(collection_conf["id"], json=collection.data)
     tasks.scan_library(library_id=library.pk)
 
     scan_page.assert_called_once_with(
-        library_id=library.id,
-        page_url=collection.data['first'],
-        until=None,
+        library_id=library.id, page_url=collection.data["first"], until=None
     )
     library.refresh_from_db()
     assert library.fetched_date > now
 
 
-def test_scan_page_fetches_page_and_creates_tracks(
-        mocker, factories, r_mock):
-    library = factories['federation.Library'](federation_enabled=True)
-    tfs = factories['music.TrackFile'].create_batch(size=5)
+def test_scan_page_fetches_page_and_creates_tracks(mocker, factories, r_mock):
+    library = factories["federation.Library"](federation_enabled=True)
+    tfs = factories["music.TrackFile"].create_batch(size=5)
     page_conf = {
-        'actor': library.actor,
-        'id': library.url,
-        'page': Paginator(tfs, 5).page(1),
-        'item_serializer': serializers.AudioSerializer,
+        "actor": library.actor,
+        "id": library.url,
+        "page": Paginator(tfs, 5).page(1),
+        "item_serializer": serializers.AudioSerializer,
     }
     page = serializers.CollectionPageSerializer(page_conf)
-    r_mock.get(page.data['id'], json=page.data)
+    r_mock.get(page.data["id"], json=page.data)
 
-    tasks.scan_library_page(library_id=library.pk, page_url=page.data['id'])
+    tasks.scan_library_page(library_id=library.pk, page_url=page.data["id"])
 
-    lts = list(library.tracks.all().order_by('-published_date'))
+    lts = list(library.tracks.all().order_by("-published_date"))
     assert len(lts) == 5
 
 
-def test_scan_page_trigger_next_page_scan_skip_if_same(
-        mocker, factories, r_mock):
+def test_scan_page_trigger_next_page_scan_skip_if_same(mocker, factories, r_mock):
     patched_scan = mocker.patch(
-        'funkwhale_api.federation.tasks.scan_library_page.delay'
+        "funkwhale_api.federation.tasks.scan_library_page.delay"
     )
-    library = factories['federation.Library'](federation_enabled=True)
-    tfs = factories['music.TrackFile'].create_batch(size=1)
+    library = factories["federation.Library"](federation_enabled=True)
+    tfs = factories["music.TrackFile"].create_batch(size=1)
     page_conf = {
-        'actor': library.actor,
-        'id': library.url,
-        'page': Paginator(tfs, 3).page(1),
-        'item_serializer': serializers.AudioSerializer,
+        "actor": library.actor,
+        "id": library.url,
+        "page": Paginator(tfs, 3).page(1),
+        "item_serializer": serializers.AudioSerializer,
     }
     page = serializers.CollectionPageSerializer(page_conf)
     data = page.data
-    data['next'] = data['id']
-    r_mock.get(page.data['id'], json=data)
+    data["next"] = data["id"]
+    r_mock.get(page.data["id"], json=data)
 
-    tasks.scan_library_page(library_id=library.pk, page_url=data['id'])
+    tasks.scan_library_page(library_id=library.pk, page_url=data["id"])
     patched_scan.assert_not_called()
 
 
-def test_scan_page_stops_once_until_is_reached(
-        mocker, factories, r_mock):
-    library = factories['federation.Library'](federation_enabled=True)
-    tfs = list(reversed(factories['music.TrackFile'].create_batch(size=5)))
+def test_scan_page_stops_once_until_is_reached(mocker, factories, r_mock):
+    library = factories["federation.Library"](federation_enabled=True)
+    tfs = list(reversed(factories["music.TrackFile"].create_batch(size=5)))
     page_conf = {
-        'actor': library.actor,
-        'id': library.url,
-        'page': Paginator(tfs, 3).page(1),
-        'item_serializer': serializers.AudioSerializer,
+        "actor": library.actor,
+        "id": library.url,
+        "page": Paginator(tfs, 3).page(1),
+        "item_serializer": serializers.AudioSerializer,
     }
     page = serializers.CollectionPageSerializer(page_conf)
-    r_mock.get(page.data['id'], json=page.data)
+    r_mock.get(page.data["id"], json=page.data)
 
     tasks.scan_library_page(
-        library_id=library.pk,
-        page_url=page.data['id'],
-        until=tfs[1].creation_date)
+        library_id=library.pk, page_url=page.data["id"], until=tfs[1].creation_date
+    )
 
-    lts = list(library.tracks.all().order_by('-published_date'))
+    lts = list(library.tracks.all().order_by("-published_date"))
     assert len(lts) == 2
     for i, tf in enumerate(tfs[:1]):
         assert tf.creation_date == lts[i].published_date
 
 
 def test_clean_federation_music_cache_if_no_listen(preferences, factories):
-    preferences['federation__music_cache_duration'] = 60
-    lt1 = factories['federation.LibraryTrack'](with_audio_file=True)
-    lt2 = factories['federation.LibraryTrack'](with_audio_file=True)
-    lt3 = factories['federation.LibraryTrack'](with_audio_file=True)
-    tf1 = factories['music.TrackFile'](
-        accessed_date=timezone.now(), library_track=lt1)
-    tf2 = factories['music.TrackFile'](
-        accessed_date=timezone.now()-datetime.timedelta(minutes=61),
-        library_track=lt2)
-    tf3 = factories['music.TrackFile'](
-        accessed_date=None, library_track=lt3)
+    preferences["federation__music_cache_duration"] = 60
+    lt1 = factories["federation.LibraryTrack"](with_audio_file=True)
+    lt2 = factories["federation.LibraryTrack"](with_audio_file=True)
+    lt3 = factories["federation.LibraryTrack"](with_audio_file=True)
+    factories["music.TrackFile"](accessed_date=timezone.now(), library_track=lt1)
+    factories["music.TrackFile"](
+        accessed_date=timezone.now() - datetime.timedelta(minutes=61), library_track=lt2
+    )
+    factories["music.TrackFile"](accessed_date=None, library_track=lt3)
     path1 = lt1.audio_file.path
     path2 = lt2.audio_file.path
     path3 = lt3.audio_file.path
@@ -145,22 +132,19 @@ def test_clean_federation_music_cache_if_no_listen(preferences, factories):
     assert os.path.exists(path3) is False
 
 
-def test_clean_federation_music_cache_orphaned(
-        settings, preferences, factories):
-    preferences['federation__music_cache_duration'] = 60
-    path = os.path.join(settings.MEDIA_ROOT, 'federation_cache')
-    keep_path = os.path.join(os.path.join(path, '1a', 'b2'), 'keep.ogg')
-    remove_path = os.path.join(os.path.join(path, 'c3', 'd4'), 'remove.ogg')
+def test_clean_federation_music_cache_orphaned(settings, preferences, factories):
+    preferences["federation__music_cache_duration"] = 60
+    path = os.path.join(settings.MEDIA_ROOT, "federation_cache")
+    keep_path = os.path.join(os.path.join(path, "1a", "b2"), "keep.ogg")
+    remove_path = os.path.join(os.path.join(path, "c3", "d4"), "remove.ogg")
     os.makedirs(os.path.dirname(keep_path), exist_ok=True)
     os.makedirs(os.path.dirname(remove_path), exist_ok=True)
     pathlib.Path(keep_path).touch()
     pathlib.Path(remove_path).touch()
-    lt = factories['federation.LibraryTrack'](
-        with_audio_file=True,
-        audio_file__path=keep_path)
-    tf = factories['music.TrackFile'](
-        library_track=lt,
-        accessed_date=timezone.now())
+    lt = factories["federation.LibraryTrack"](
+        with_audio_file=True, audio_file__path=keep_path
+    )
+    factories["music.TrackFile"](library_track=lt, accessed_date=timezone.now())
 
     tasks.clean_music_cache()
 
diff --git a/api/tests/federation/test_utils.py b/api/tests/federation/test_utils.py
index dc371ad9ed976a5d1179a14d9dc58e831e2873e8..dbebe0fdc2a5b074c7d392b838df7027b77f1836 100644
--- a/api/tests/federation/test_utils.py
+++ b/api/tests/federation/test_utils.py
@@ -3,12 +3,15 @@ import pytest
 from funkwhale_api.federation import utils
 
 
-@pytest.mark.parametrize('url,path,expected', [
-    ('http://test.com', '/hello', 'http://test.com/hello'),
-    ('http://test.com/', 'hello', 'http://test.com/hello'),
-    ('http://test.com/', '/hello', 'http://test.com/hello'),
-    ('http://test.com', 'hello', 'http://test.com/hello'),
-])
+@pytest.mark.parametrize(
+    "url,path,expected",
+    [
+        ("http://test.com", "/hello", "http://test.com/hello"),
+        ("http://test.com/", "hello", "http://test.com/hello"),
+        ("http://test.com/", "/hello", "http://test.com/hello"),
+        ("http://test.com", "hello", "http://test.com/hello"),
+    ],
+)
 def test_full_url(settings, url, path, expected):
     settings.FUNKWHALE_URL = url
     assert utils.full_url(path) == expected
@@ -16,33 +19,34 @@ def test_full_url(settings, url, path, expected):
 
 def test_extract_headers_from_meta():
     wsgi_headers = {
-        'HTTP_HOST': 'nginx',
-        'HTTP_X_REAL_IP': '172.20.0.4',
-        'HTTP_X_FORWARDED_FOR': '188.165.228.227, 172.20.0.4',
-        'HTTP_X_FORWARDED_PROTO': 'http',
-        'HTTP_X_FORWARDED_HOST': 'localhost:80',
-        'HTTP_X_FORWARDED_PORT': '80',
-        'HTTP_CONNECTION': 'close',
-        'CONTENT_LENGTH': '1155',
-        'CONTENT_TYPE': 'txt/application',
-        'HTTP_SIGNATURE': 'Hello',
-        'HTTP_DATE': 'Sat, 31 Mar 2018 13:53:55 GMT',
-        'HTTP_USER_AGENT': 'http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)'}
+        "HTTP_HOST": "nginx",
+        "HTTP_X_REAL_IP": "172.20.0.4",
+        "HTTP_X_FORWARDED_FOR": "188.165.228.227, 172.20.0.4",
+        "HTTP_X_FORWARDED_PROTO": "http",
+        "HTTP_X_FORWARDED_HOST": "localhost:80",
+        "HTTP_X_FORWARDED_PORT": "80",
+        "HTTP_CONNECTION": "close",
+        "CONTENT_LENGTH": "1155",
+        "CONTENT_TYPE": "txt/application",
+        "HTTP_SIGNATURE": "Hello",
+        "HTTP_DATE": "Sat, 31 Mar 2018 13:53:55 GMT",
+        "HTTP_USER_AGENT": "http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)",
+    }
 
     cleaned_headers = utils.clean_wsgi_headers(wsgi_headers)
 
     expected = {
-        'Host': 'nginx',
-        'X-Real-Ip': '172.20.0.4',
-        'X-Forwarded-For': '188.165.228.227, 172.20.0.4',
-        'X-Forwarded-Proto': 'http',
-        'X-Forwarded-Host': 'localhost:80',
-        'X-Forwarded-Port': '80',
-        'Connection': 'close',
-        'Content-Length': '1155',
-        'Content-Type': 'txt/application',
-        'Signature': 'Hello',
-        'Date': 'Sat, 31 Mar 2018 13:53:55 GMT',
-        'User-Agent': 'http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)'
+        "Host": "nginx",
+        "X-Real-Ip": "172.20.0.4",
+        "X-Forwarded-For": "188.165.228.227, 172.20.0.4",
+        "X-Forwarded-Proto": "http",
+        "X-Forwarded-Host": "localhost:80",
+        "X-Forwarded-Port": "80",
+        "Connection": "close",
+        "Content-Length": "1155",
+        "Content-Type": "txt/application",
+        "Signature": "Hello",
+        "Date": "Sat, 31 Mar 2018 13:53:55 GMT",
+        "User-Agent": "http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)",
     }
     assert cleaned_headers == expected
diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py
index 04a419aed6f5fae696cbd3d94f02c8420d9e9021..9e2d66a620241d384ca1546c690c2836f4820c1a 100644
--- a/api/tests/federation/test_views.py
+++ b/api/tests/federation/test_views.py
@@ -1,327 +1,308 @@
+import pytest
 from django.core.paginator import Paginator
 from django.urls import reverse
 from django.utils import timezone
 
-import pytest
-
-from funkwhale_api.federation import actors
-from funkwhale_api.federation import activity
-from funkwhale_api.federation import models
-from funkwhale_api.federation import serializers
-from funkwhale_api.federation import utils
-from funkwhale_api.federation import views
-from funkwhale_api.federation import webfinger
-
-
-@pytest.mark.parametrize('view,permissions', [
-    (views.LibraryViewSet, ['federation']),
-    (views.LibraryTrackViewSet, ['federation']),
-])
+from funkwhale_api.federation import (
+    activity,
+    actors,
+    models,
+    serializers,
+    utils,
+    views,
+    webfinger,
+)
+
+
+@pytest.mark.parametrize(
+    "view,permissions",
+    [
+        (views.LibraryViewSet, ["federation"]),
+        (views.LibraryTrackViewSet, ["federation"]),
+    ],
+)
 def test_permissions(assert_user_permission, view, permissions):
     assert_user_permission(view, permissions)
 
 
-@pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys())
+@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys())
 def test_instance_actors(system_actor, db, api_client):
     actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance()
-    url = reverse(
-        'federation:instance-actors-detail',
-        kwargs={'actor': system_actor})
+    url = reverse("federation:instance-actors-detail", kwargs={"actor": system_actor})
     response = api_client.get(url)
     serializer = serializers.ActorSerializer(actor)
 
-    if system_actor == 'library':
-        response.data.pop('url')
+    if system_actor == "library":
+        response.data.pop("url")
     assert response.status_code == 200
     assert response.data == serializer.data
 
 
-@pytest.mark.parametrize('route,kwargs', [
-    ('instance-actors-outbox', {'actor': 'library'}),
-    ('instance-actors-inbox', {'actor': 'library'}),
-    ('instance-actors-detail', {'actor': 'library'}),
-    ('well-known-webfinger', {}),
-])
+@pytest.mark.parametrize(
+    "route,kwargs",
+    [
+        ("instance-actors-outbox", {"actor": "library"}),
+        ("instance-actors-inbox", {"actor": "library"}),
+        ("instance-actors-detail", {"actor": "library"}),
+        ("well-known-webfinger", {}),
+    ],
+)
 def test_instance_endpoints_405_if_federation_disabled(
-        authenticated_actor, db, preferences, api_client, route, kwargs):
-    preferences['federation__enabled'] = False
-    url = reverse('federation:{}'.format(route), kwargs=kwargs)
+    authenticated_actor, db, preferences, api_client, route, kwargs
+):
+    preferences["federation__enabled"] = False
+    url = reverse("federation:{}".format(route), kwargs=kwargs)
     response = api_client.get(url)
 
     assert response.status_code == 405
 
 
-def test_wellknown_webfinger_validates_resource(
-        db, api_client, settings, mocker):
-    clean = mocker.spy(webfinger, 'clean_resource')
-    url = reverse('federation:well-known-webfinger')
-    response = api_client.get(url, data={'resource': 'something'})
+def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker):
+    clean = mocker.spy(webfinger, "clean_resource")
+    url = reverse("federation:well-known-webfinger")
+    response = api_client.get(url, data={"resource": "something"})
 
-    clean.assert_called_once_with('something')
-    assert url == '/.well-known/webfinger'
+    clean.assert_called_once_with("something")
+    assert url == "/.well-known/webfinger"
     assert response.status_code == 400
-    assert response.data['errors']['resource'] == (
-        'Missing webfinger resource type'
-    )
+    assert response.data["errors"]["resource"] == ("Missing webfinger resource type")
 
 
-@pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys())
-def test_wellknown_webfinger_system(
-        system_actor, db, api_client, settings, mocker):
+@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys())
+def test_wellknown_webfinger_system(system_actor, db, api_client, settings, mocker):
     actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance()
-    url = reverse('federation:well-known-webfinger')
+    url = reverse("federation:well-known-webfinger")
     response = api_client.get(
         url,
-        data={'resource': 'acct:{}'.format(actor.webfinger_subject)},
-        HTTP_ACCEPT='application/jrd+json',
+        data={"resource": "acct:{}".format(actor.webfinger_subject)},
+        HTTP_ACCEPT="application/jrd+json",
     )
     serializer = serializers.ActorWebfingerSerializer(actor)
 
     assert response.status_code == 200
-    assert response['Content-Type'] == 'application/jrd+json'
+    assert response["Content-Type"] == "application/jrd+json"
     assert response.data == serializer.data
 
 
 def test_wellknown_nodeinfo(db, preferences, api_client, settings):
     expected = {
-        'links': [
+        "links": [
             {
-                'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0',
-                'href': '{}{}'.format(
-                    settings.FUNKWHALE_URL,
-                    reverse('api:v1:instance:nodeinfo-2.0')
-                )
+                "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
+                "href": "{}{}".format(
+                    settings.FUNKWHALE_URL, reverse("api:v1:instance:nodeinfo-2.0")
+                ),
             }
         ]
     }
-    url = reverse('federation:well-known-nodeinfo')
-    response = api_client.get(url, HTTP_ACCEPT='application/jrd+json')
+    url = reverse("federation:well-known-nodeinfo")
+    response = api_client.get(url, HTTP_ACCEPT="application/jrd+json")
     assert response.status_code == 200
-    assert response['Content-Type'] == 'application/jrd+json'
+    assert response["Content-Type"] == "application/jrd+json"
     assert response.data == expected
 
 
 def test_wellknown_nodeinfo_disabled(db, preferences, api_client):
-    preferences['instance__nodeinfo_enabled'] = False
-    url = reverse('federation:well-known-nodeinfo')
+    preferences["instance__nodeinfo_enabled"] = False
+    url = reverse("federation:well-known-nodeinfo")
     response = api_client.get(url)
     assert response.status_code == 404
 
 
-def test_audio_file_list_requires_authenticated_actor(
-        db, preferences, api_client):
-    preferences['federation__music_needs_approval'] = True
-    url = reverse('federation:music:files-list')
+def test_audio_file_list_requires_authenticated_actor(db, preferences, api_client):
+    preferences["federation__music_needs_approval"] = True
+    url = reverse("federation:music:files-list")
     response = api_client.get(url)
 
     assert response.status_code == 403
 
 
-def test_audio_file_list_actor_no_page(
-        db, preferences, api_client, factories):
-    preferences['federation__music_needs_approval'] = False
-    preferences['federation__collection_page_size'] = 2
-    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    tfs = factories['music.TrackFile'].create_batch(size=5)
+def test_audio_file_list_actor_no_page(db, preferences, api_client, factories):
+    preferences["federation__music_needs_approval"] = False
+    preferences["federation__collection_page_size"] = 2
+    library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    tfs = factories["music.TrackFile"].create_batch(size=5)
     conf = {
-        'id': utils.full_url(reverse('federation:music:files-list')),
-        'page_size': 2,
-        'items': list(reversed(tfs)),  # we order by -creation_date
-        'item_serializer': serializers.AudioSerializer,
-        'actor': library
+        "id": utils.full_url(reverse("federation:music:files-list")),
+        "page_size": 2,
+        "items": list(reversed(tfs)),  # we order by -creation_date
+        "item_serializer": serializers.AudioSerializer,
+        "actor": library,
     }
     expected = serializers.PaginatedCollectionSerializer(conf).data
-    url = reverse('federation:music:files-list')
+    url = reverse("federation:music:files-list")
     response = api_client.get(url)
 
     assert response.status_code == 200
     assert response.data == expected
 
 
-def test_audio_file_list_actor_page(
-        db, preferences, api_client, factories):
-    preferences['federation__music_needs_approval'] = False
-    preferences['federation__collection_page_size'] = 2
-    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    tfs = factories['music.TrackFile'].create_batch(size=5)
+def test_audio_file_list_actor_page(db, preferences, api_client, factories):
+    preferences["federation__music_needs_approval"] = False
+    preferences["federation__collection_page_size"] = 2
+    library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    tfs = factories["music.TrackFile"].create_batch(size=5)
     conf = {
-        'id': utils.full_url(reverse('federation:music:files-list')),
-        'page': Paginator(list(reversed(tfs)), 2).page(2),
-        'item_serializer': serializers.AudioSerializer,
-        'actor': library
+        "id": utils.full_url(reverse("federation:music:files-list")),
+        "page": Paginator(list(reversed(tfs)), 2).page(2),
+        "item_serializer": serializers.AudioSerializer,
+        "actor": library,
     }
     expected = serializers.CollectionPageSerializer(conf).data
-    url = reverse('federation:music:files-list')
-    response = api_client.get(url, data={'page': 2})
+    url = reverse("federation:music:files-list")
+    response = api_client.get(url, data={"page": 2})
 
     assert response.status_code == 200
     assert response.data == expected
 
 
 def test_audio_file_list_actor_page_exclude_federated_files(
-        db, preferences, api_client, factories):
-    preferences['federation__music_needs_approval'] = False
-    library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    tfs = factories['music.TrackFile'].create_batch(size=5, federation=True)
+    db, preferences, api_client, factories
+):
+    preferences["federation__music_needs_approval"] = False
+    factories["music.TrackFile"].create_batch(size=5, federation=True)
 
-    url = reverse('federation:music:files-list')
+    url = reverse("federation:music:files-list")
     response = api_client.get(url)
 
     assert response.status_code == 200
-    assert response.data['totalItems'] == 0
+    assert response.data["totalItems"] == 0
 
 
-def test_audio_file_list_actor_page_error(
-        db, preferences, api_client, factories):
-    preferences['federation__music_needs_approval'] = False
-    url = reverse('federation:music:files-list')
-    response = api_client.get(url, data={'page': 'nope'})
+def test_audio_file_list_actor_page_error(db, preferences, api_client, factories):
+    preferences["federation__music_needs_approval"] = False
+    url = reverse("federation:music:files-list")
+    response = api_client.get(url, data={"page": "nope"})
 
     assert response.status_code == 400
 
 
 def test_audio_file_list_actor_page_error_too_far(
-        db, preferences, api_client, factories):
-    preferences['federation__music_needs_approval'] = False
-    url = reverse('federation:music:files-list')
-    response = api_client.get(url, data={'page': 5000})
+    db, preferences, api_client, factories
+):
+    preferences["federation__music_needs_approval"] = False
+    url = reverse("federation:music:files-list")
+    response = api_client.get(url, data={"page": 5000})
 
     assert response.status_code == 404
 
 
 def test_library_actor_includes_library_link(db, preferences, api_client):
-    actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    url = reverse(
-        'federation:instance-actors-detail',
-        kwargs={'actor': 'library'})
+    url = reverse("federation:instance-actors-detail", kwargs={"actor": "library"})
     response = api_client.get(url)
     expected_links = [
         {
-            'type': 'Link',
-            'name': 'library',
-            'mediaType': 'application/activity+json',
-            'href': utils.full_url(reverse('federation:music:files-list'))
+            "type": "Link",
+            "name": "library",
+            "mediaType": "application/activity+json",
+            "href": utils.full_url(reverse("federation:music:files-list")),
         }
     ]
     assert response.status_code == 200
-    assert response.data['url'] == expected_links
+    assert response.data["url"] == expected_links
 
 
 def test_can_fetch_library(superuser_api_client, mocker):
-    result = {'test': 'test'}
+    result = {"test": "test"}
     scan = mocker.patch(
-        'funkwhale_api.federation.library.scan_from_account_name',
-        return_value=result)
+        "funkwhale_api.federation.library.scan_from_account_name", return_value=result
+    )
 
-    url = reverse('api:v1:federation:libraries-fetch')
-    response = superuser_api_client.get(
-        url, data={'account': 'test@test.library'})
+    url = reverse("api:v1:federation:libraries-fetch")
+    response = superuser_api_client.get(url, data={"account": "test@test.library"})
 
     assert response.status_code == 200
     assert response.data == result
-    scan.assert_called_once_with('test@test.library')
+    scan.assert_called_once_with("test@test.library")
 
 
 def test_follow_library(superuser_api_client, mocker, factories, r_mock):
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    actor = factories['federation.Actor']()
-    follow = {'test': 'follow'}
-    on_commit = mocker.patch(
-        'funkwhale_api.common.utils.on_commit')
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    actor = factories["federation.Actor"]()
+    follow = {"test": "follow"}
+    on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
     actor_data = serializers.ActorSerializer(actor).data
-    actor_data['url'] = [{
-        'href': 'https://test.library',
-        'name': 'library',
-        'type': 'Link',
-    }]
+    actor_data["url"] = [
+        {"href": "https://test.library", "name": "library", "type": "Link"}
+    ]
     library_conf = {
-        'id': 'https://test.library',
-        'items': range(10),
-        'actor': actor,
-        'page_size': 5,
+        "id": "https://test.library",
+        "items": range(10),
+        "actor": actor,
+        "page_size": 5,
     }
     library_data = serializers.PaginatedCollectionSerializer(library_conf).data
     r_mock.get(actor.url, json=actor_data)
-    r_mock.get('https://test.library', json=library_data)
+    r_mock.get("https://test.library", json=library_data)
     data = {
-        'actor': actor.url,
-        'autoimport': False,
-        'federation_enabled': True,
-        'download_files': False,
+        "actor": actor.url,
+        "autoimport": False,
+        "federation_enabled": True,
+        "download_files": False,
     }
 
-    url = reverse('api:v1:federation:libraries-list')
-    response = superuser_api_client.post(
-        url, data)
+    url = reverse("api:v1:federation:libraries-list")
+    response = superuser_api_client.post(url, data)
 
     assert response.status_code == 201
 
-    follow = models.Follow.objects.get(
-        actor=library_actor,
-        target=actor,
-        approved=None,
-    )
+    follow = models.Follow.objects.get(actor=library_actor, target=actor, approved=None)
     library = follow.library
 
-    assert response.data == serializers.APILibraryCreateSerializer(
-        library).data
+    assert response.data == serializers.APILibraryCreateSerializer(library).data
 
     on_commit.assert_called_once_with(
         activity.deliver,
         serializers.FollowSerializer(follow).data,
         on_behalf_of=library_actor,
-        to=[actor.url]
+        to=[actor.url],
     )
 
 
 def test_can_list_system_actor_following(factories, superuser_api_client):
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    follow1 = factories['federation.Follow'](actor=library_actor)
-    follow2 = factories['federation.Follow']()
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    follow1 = factories["federation.Follow"](actor=library_actor)
+    factories["federation.Follow"]()
 
-    url = reverse('api:v1:federation:libraries-following')
+    url = reverse("api:v1:federation:libraries-following")
     response = superuser_api_client.get(url)
 
     assert response.status_code == 200
-    assert response.data['results'] == [
-        serializers.APIFollowSerializer(follow1).data
-    ]
+    assert response.data["results"] == [serializers.APIFollowSerializer(follow1).data]
 
 
 def test_can_list_system_actor_followers(factories, superuser_api_client):
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    follow1 = factories['federation.Follow'](actor=library_actor)
-    follow2 = factories['federation.Follow'](target=library_actor)
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    factories["federation.Follow"](actor=library_actor)
+    follow2 = factories["federation.Follow"](target=library_actor)
 
-    url = reverse('api:v1:federation:libraries-followers')
+    url = reverse("api:v1:federation:libraries-followers")
     response = superuser_api_client.get(url)
 
     assert response.status_code == 200
-    assert response.data['results'] == [
-        serializers.APIFollowSerializer(follow2).data
-    ]
+    assert response.data["results"] == [serializers.APIFollowSerializer(follow2).data]
 
 
 def test_can_list_libraries(factories, superuser_api_client):
-    library1 = factories['federation.Library']()
-    library2 = factories['federation.Library']()
+    library1 = factories["federation.Library"]()
+    library2 = factories["federation.Library"]()
 
-    url = reverse('api:v1:federation:libraries-list')
+    url = reverse("api:v1:federation:libraries-list")
     response = superuser_api_client.get(url)
 
     assert response.status_code == 200
-    assert response.data['results'] == [
+    assert response.data["results"] == [
         serializers.APILibrarySerializer(library1).data,
         serializers.APILibrarySerializer(library2).data,
     ]
 
 
 def test_can_detail_library(factories, superuser_api_client):
-    library = factories['federation.Library']()
+    library = factories["federation.Library"]()
 
     url = reverse(
-        'api:v1:federation:libraries-detail',
-        kwargs={'uuid': str(library.uuid)})
+        "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)}
+    )
     response = superuser_api_client.get(url)
 
     assert response.status_code == 200
@@ -329,15 +310,15 @@ def test_can_detail_library(factories, superuser_api_client):
 
 
 def test_can_patch_library(factories, superuser_api_client):
-    library = factories['federation.Library']()
+    library = factories["federation.Library"]()
     data = {
-        'federation_enabled': not library.federation_enabled,
-        'download_files': not library.download_files,
-        'autoimport': not library.autoimport,
+        "federation_enabled": not library.federation_enabled,
+        "download_files": not library.download_files,
+        "autoimport": not library.autoimport,
     }
     url = reverse(
-        'api:v1:federation:libraries-detail',
-        kwargs={'uuid': str(library.uuid)})
+        "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)}
+    )
     response = superuser_api_client.patch(url, data)
 
     assert response.status_code == 200
@@ -349,55 +330,49 @@ def test_can_patch_library(factories, superuser_api_client):
 
 def test_scan_library(factories, mocker, superuser_api_client):
     scan = mocker.patch(
-        'funkwhale_api.federation.tasks.scan_library.delay',
-        return_value=mocker.Mock(id='id'))
-    library = factories['federation.Library']()
+        "funkwhale_api.federation.tasks.scan_library.delay",
+        return_value=mocker.Mock(id="id"),
+    )
+    library = factories["federation.Library"]()
     now = timezone.now()
-    data = {
-        'until': now,
-    }
+    data = {"until": now}
     url = reverse(
-        'api:v1:federation:libraries-scan',
-        kwargs={'uuid': str(library.uuid)})
+        "api:v1:federation:libraries-scan", kwargs={"uuid": str(library.uuid)}
+    )
     response = superuser_api_client.post(url, data)
 
     assert response.status_code == 200
-    assert response.data == {'task': 'id'}
-    scan.assert_called_once_with(
-        library_id=library.pk,
-        until=now
-    )
+    assert response.data == {"task": "id"}
+    scan.assert_called_once_with(library_id=library.pk, until=now)
 
 
 def test_list_library_tracks(factories, superuser_api_client):
-    library = factories['federation.Library']()
-    lts = list(reversed(factories['federation.LibraryTrack'].create_batch(
-        size=5, library=library)))
-    factories['federation.LibraryTrack'].create_batch(size=5)
-    url = reverse('api:v1:federation:library-tracks-list')
-    response = superuser_api_client.get(url, {'library': library.uuid})
+    library = factories["federation.Library"]()
+    lts = list(
+        reversed(
+            factories["federation.LibraryTrack"].create_batch(size=5, library=library)
+        )
+    )
+    factories["federation.LibraryTrack"].create_batch(size=5)
+    url = reverse("api:v1:federation:library-tracks-list")
+    response = superuser_api_client.get(url, {"library": library.uuid})
 
     assert response.status_code == 200
     assert response.data == {
-        'results': serializers.APILibraryTrackSerializer(lts, many=True).data,
-        'count': 5,
-        'previous': None,
-        'next': None,
+        "results": serializers.APILibraryTrackSerializer(lts, many=True).data,
+        "count": 5,
+        "previous": None,
+        "next": None,
     }
 
 
 def test_can_update_follow_status(factories, superuser_api_client, mocker):
-    patched_accept = mocker.patch(
-        'funkwhale_api.federation.activity.accept_follow'
-    )
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    follow = factories['federation.Follow'](target=library_actor)
+    patched_accept = mocker.patch("funkwhale_api.federation.activity.accept_follow")
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    follow = factories["federation.Follow"](target=library_actor)
 
-    payload = {
-        'follow': follow.pk,
-        'approved': True
-    }
-    url = reverse('api:v1:federation:libraries-followers')
+    payload = {"follow": follow.pk, "approved": True}
+    url = reverse("api:v1:federation:libraries-followers")
     response = superuser_api_client.patch(url, payload)
     follow.refresh_from_db()
 
@@ -407,45 +382,33 @@ def test_can_update_follow_status(factories, superuser_api_client, mocker):
 
 
 def test_can_filter_pending_follows(factories, superuser_api_client):
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    follow = factories['federation.Follow'](
-        target=library_actor,
-        approved=True)
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    factories["federation.Follow"](target=library_actor, approved=True)
 
-    params = {'pending': True}
-    url = reverse('api:v1:federation:libraries-followers')
+    params = {"pending": True}
+    url = reverse("api:v1:federation:libraries-followers")
     response = superuser_api_client.get(url, params)
 
     assert response.status_code == 200
-    assert len(response.data['results']) == 0
+    assert len(response.data["results"]) == 0
 
 
-def test_library_track_action_import(
-        factories, superuser_api_client, mocker):
-    lt1 = factories['federation.LibraryTrack']()
-    lt2 = factories['federation.LibraryTrack'](library=lt1.library)
-    lt3 = factories['federation.LibraryTrack']()
-    lt4 = factories['federation.LibraryTrack'](library=lt3.library)
-    mocked_run = mocker.patch(
-        'funkwhale_api.music.tasks.import_batch_run.delay')
+def test_library_track_action_import(factories, superuser_api_client, mocker):
+    lt1 = factories["federation.LibraryTrack"]()
+    lt2 = factories["federation.LibraryTrack"](library=lt1.library)
+    lt3 = factories["federation.LibraryTrack"]()
+    factories["federation.LibraryTrack"](library=lt3.library)
+    mocked_run = mocker.patch("funkwhale_api.music.tasks.import_batch_run.delay")
 
     payload = {
-        'objects': 'all',
-        'action': 'import',
-        'filters': {
-            'library': lt1.library.uuid
-        }
-    }
-    url = reverse('api:v1:federation:library-tracks-action')
-    response = superuser_api_client.post(url, payload, format='json')
-    batch = superuser_api_client.user.imports.latest('id')
-    expected = {
-        'updated': 2,
-        'action': 'import',
-        'result': {
-            'batch': {'id': batch.pk}
-        }
+        "objects": "all",
+        "action": "import",
+        "filters": {"library": lt1.library.uuid},
     }
+    url = reverse("api:v1:federation:library-tracks-action")
+    response = superuser_api_client.post(url, payload, format="json")
+    batch = superuser_api_client.user.imports.latest("id")
+    expected = {"updated": 2, "action": "import", "result": {"batch": {"id": batch.pk}}}
 
     imported_lts = [lt1, lt2]
     assert response.status_code == 200
diff --git a/api/tests/federation/test_webfinger.py b/api/tests/federation/test_webfinger.py
index 4b8dca207ad57b36ee878e68009b0a778b72bcfd..0608df3e2b38ffecc00d016c99c226da9d8b2705 100644
--- a/api/tests/federation/test_webfinger.py
+++ b/api/tests/federation/test_webfinger.py
@@ -1,22 +1,23 @@
 import pytest
-
 from django import forms
-from django.urls import reverse
 
 from funkwhale_api.federation import webfinger
 
 
 def test_webfinger_clean_resource():
-    t, r = webfinger.clean_resource('acct:service@test.federation')
-    assert t == 'acct'
-    assert r == 'service@test.federation'
-
-
-@pytest.mark.parametrize('resource,message', [
-    ('', 'Invalid resource string'),
-    ('service@test.com', 'Missing webfinger resource type'),
-    ('noop:service@test.com', 'Invalid webfinger resource type'),
-])
+    t, r = webfinger.clean_resource("acct:service@test.federation")
+    assert t == "acct"
+    assert r == "service@test.federation"
+
+
+@pytest.mark.parametrize(
+    "resource,message",
+    [
+        ("", "Invalid resource string"),
+        ("service@test.com", "Missing webfinger resource type"),
+        ("noop:service@test.com", "Invalid webfinger resource type"),
+    ],
+)
 def test_webfinger_clean_resource_errors(resource, message):
     with pytest.raises(forms.ValidationError) as excinfo:
         webfinger.clean_resource(resource)
@@ -25,16 +26,19 @@ def test_webfinger_clean_resource_errors(resource, message):
 
 
 def test_webfinger_clean_acct(settings):
-    username, hostname = webfinger.clean_acct('library@test.federation')
-    assert username == 'library'
-    assert hostname == 'test.federation'
-
-
-@pytest.mark.parametrize('resource,message', [
-    ('service', 'Invalid format'),
-    ('service@test.com', 'Invalid hostname test.com'),
-    ('noop@test.federation', 'Invalid account'),
-])
+    username, hostname = webfinger.clean_acct("library@test.federation")
+    assert username == "library"
+    assert hostname == "test.federation"
+
+
+@pytest.mark.parametrize(
+    "resource,message",
+    [
+        ("service", "Invalid format"),
+        ("service@test.com", "Invalid hostname test.com"),
+        ("noop@test.federation", "Invalid account"),
+    ],
+)
 def test_webfinger_clean_acct_errors(resource, message, settings):
     with pytest.raises(forms.ValidationError) as excinfo:
         webfinger.clean_resource(resource)
@@ -43,26 +47,24 @@ def test_webfinger_clean_acct_errors(resource, message, settings):
 
 
 def test_webfinger_get_resource(r_mock):
-    resource = 'acct:test@test.webfinger'
+    resource = "acct:test@test.webfinger"
     payload = {
-        'subject': resource,
-        'aliases': ['https://test.webfinger'],
-        'links': [
+        "subject": resource,
+        "aliases": ["https://test.webfinger"],
+        "links": [
             {
-                'rel': 'self',
-                'type': 'application/activity+json',
-                'href': 'https://test.webfinger/user/test'
+                "rel": "self",
+                "type": "application/activity+json",
+                "href": "https://test.webfinger/user/test",
             }
-        ]
+        ],
     }
     r_mock.get(
-        'https://test.webfinger/.well-known/webfinger?resource={}'.format(
-            resource
-        ),
-        json=payload
+        "https://test.webfinger/.well-known/webfinger?resource={}".format(resource),
+        json=payload,
     )
 
-    data = webfinger.get_resource('acct:test@test.webfinger')
+    data = webfinger.get_resource("acct:test@test.webfinger")
 
-    assert data['actor_url'] == 'https://test.webfinger/user/test'
-    assert data['subject'] == resource
+    assert data["actor_url"] == "https://test.webfinger/user/test"
+    assert data["subject"] == resource
diff --git a/api/tests/history/test_activity.py b/api/tests/history/test_activity.py
index 04000604b264394ab7c3c1425e49a1f2b59bff71..f3ada50522d9bbd73943137a0f90ffe654b4002e 100644
--- a/api/tests/history/test_activity.py
+++ b/api/tests/history/test_activity.py
@@ -1,19 +1,17 @@
-from funkwhale_api.users.serializers import UserActivitySerializer
+from funkwhale_api.history import activities, serializers
 from funkwhale_api.music.serializers import TrackActivitySerializer
-from funkwhale_api.history import serializers
-from funkwhale_api.history import activities
+from funkwhale_api.users.serializers import UserActivitySerializer
 
 
 def test_get_listening_activity_url(settings, factories):
-    listening = factories['history.Listening']()
+    listening = factories["history.Listening"]()
     user_url = listening.user.get_activity_url()
-    expected = '{}/listenings/tracks/{}'.format(
-        user_url, listening.pk)
+    expected = "{}/listenings/tracks/{}".format(user_url, listening.pk)
     assert listening.get_activity_url() == expected
 
 
 def test_activity_listening_serializer(factories):
-    listening = factories['history.Listening']()
+    listening = factories["history.Listening"]()
 
     actor = UserActivitySerializer(listening.user).data
     field = serializers.serializers.DateTimeField()
@@ -32,44 +30,30 @@ def test_activity_listening_serializer(factories):
 
 
 def test_track_listening_serializer_is_connected(activity_registry):
-    conf = activity_registry['history.Listening']
-    assert conf['serializer'] == serializers.ListeningActivitySerializer
+    conf = activity_registry["history.Listening"]
+    assert conf["serializer"] == serializers.ListeningActivitySerializer
 
 
-def test_track_listening_serializer_instance_activity_consumer(
-        activity_registry):
-    conf = activity_registry['history.Listening']
+def test_track_listening_serializer_instance_activity_consumer(activity_registry):
+    conf = activity_registry["history.Listening"]
     consumer = activities.broadcast_listening_to_instance_activity
-    assert consumer in conf['consumers']
+    assert consumer in conf["consumers"]
 
 
-def test_broadcast_listening_to_instance_activity(
-        factories, mocker):
-    p = mocker.patch('funkwhale_api.common.channels.group_send')
-    listening = factories['history.Listening']()
+def test_broadcast_listening_to_instance_activity(factories, mocker):
+    p = mocker.patch("funkwhale_api.common.channels.group_send")
+    listening = factories["history.Listening"]()
     data = serializers.ListeningActivitySerializer(listening).data
     consumer = activities.broadcast_listening_to_instance_activity
-    message = {
-        "type": 'event.send',
-        "text": '',
-        "data": data
-    }
+    message = {"type": "event.send", "text": "", "data": data}
     consumer(data=data, obj=listening)
-    p.assert_called_once_with('instance_activity', message)
+    p.assert_called_once_with("instance_activity", message)
 
 
-def test_broadcast_listening_to_instance_activity_private(
-        factories, mocker):
-    p = mocker.patch('funkwhale_api.common.channels.group_send')
-    listening = factories['history.Listening'](
-        user__privacy_level='me'
-    )
+def test_broadcast_listening_to_instance_activity_private(factories, mocker):
+    p = mocker.patch("funkwhale_api.common.channels.group_send")
+    listening = factories["history.Listening"](user__privacy_level="me")
     data = serializers.ListeningActivitySerializer(listening).data
     consumer = activities.broadcast_listening_to_instance_activity
-    message = {
-        "type": 'event.send',
-        "text": '',
-        "data": data
-    }
     consumer(data=data, obj=listening)
     p.assert_not_called()
diff --git a/api/tests/history/test_history.py b/api/tests/history/test_history.py
index 20272559636119d51e066d7ebe0e97fd944a5fc2..9cc4e3d1471dbc310f354a872e2707a71fc64fb3 100644
--- a/api/tests/history/test_history.py
+++ b/api/tests/history/test_history.py
@@ -1,43 +1,36 @@
-import random
-import json
 from django.urls import reverse
-from django.core.exceptions import ValidationError
-from django.utils import timezone
 
 from funkwhale_api.history import models
 
 
 def test_can_create_listening(factories):
-    track = factories['music.Track']()
-    user = factories['users.User']()
-    now = timezone.now()
-    l = models.Listening.objects.create(user=user, track=track)
+    track = factories["music.Track"]()
+    user = factories["users.User"]()
+    models.Listening.objects.create(user=user, track=track)
 
 
 def test_logged_in_user_can_create_listening_via_api(
-        logged_in_client, factories, activity_muted):
-    track = factories['music.Track']()
+    logged_in_client, factories, activity_muted
+):
+    track = factories["music.Track"]()
 
-    url = reverse('api:v1:history:listenings-list')
-    response = logged_in_client.post(url, {
-        'track': track.pk,
-    })
+    url = reverse("api:v1:history:listenings-list")
+    logged_in_client.post(url, {"track": track.pk})
 
-    listening = models.Listening.objects.latest('id')
+    listening = models.Listening.objects.latest("id")
 
     assert listening.track == track
     assert listening.user == logged_in_client.user
 
 
 def test_adding_listening_calls_activity_record(
-        factories, logged_in_client, activity_muted):
-    track = factories['music.Track']()
+    factories, logged_in_client, activity_muted
+):
+    track = factories["music.Track"]()
 
-    url = reverse('api:v1:history:listenings-list')
-    response = logged_in_client.post(url, {
-        'track': track.pk,
-    })
+    url = reverse("api:v1:history:listenings-list")
+    logged_in_client.post(url, {"track": track.pk})
 
-    listening = models.Listening.objects.latest('id')
+    listening = models.Listening.objects.latest("id")
 
     activity_muted.assert_called_once_with(listening)
diff --git a/api/tests/instance/test_nodeinfo.py b/api/tests/instance/test_nodeinfo.py
index 87b8882880752d14a5572ca7a324d399688974e7..181ddf2772f3af2cb838e84ebf7718c8cb82eddd 100644
--- a/api/tests/instance/test_nodeinfo.py
+++ b/api/tests/instance/test_nodeinfo.py
@@ -1,107 +1,79 @@
-from django.urls import reverse
 
 import funkwhale_api
-
 from funkwhale_api.instance import nodeinfo
 
 
 def test_nodeinfo_dump(preferences, mocker):
-    preferences['instance__nodeinfo_stats_enabled'] = True
+    preferences["instance__nodeinfo_stats_enabled"] = True
     stats = {
-        'users': 1,
-        'tracks': 2,
-        'albums': 3,
-        'artists': 4,
-        'track_favorites': 5,
-        'music_duration': 6,
-        'listenings': 7,
+        "users": 1,
+        "tracks": 2,
+        "albums": 3,
+        "artists": 4,
+        "track_favorites": 5,
+        "music_duration": 6,
+        "listenings": 7,
     }
-    mocker.patch('funkwhale_api.instance.stats.get', return_value=stats)
+    mocker.patch("funkwhale_api.instance.stats.get", return_value=stats)
 
     expected = {
-        'version': '2.0',
-        'software': {
-            'name': 'funkwhale',
-            'version': funkwhale_api.__version__
-        },
-        'protocols': ['activitypub'],
-        'services': {
-            'inbound': [],
-            'outbound': []
-        },
-        'openRegistrations': preferences['users__registration_enabled'],
-        'usage': {
-            'users': {
-                'total': stats['users'],
-            }
-        },
-        'metadata': {
-            'private': preferences['instance__nodeinfo_private'],
-            'shortDescription': preferences['instance__short_description'],
-            'longDescription': preferences['instance__long_description'],
-            'nodeName': preferences['instance__name'],
-            'library': {
-                'federationEnabled': preferences['federation__enabled'],
-                'federationNeedsApproval': preferences['federation__music_needs_approval'],
-                'anonymousCanListen': preferences['common__api_authentication_required'],
-                'tracks': {
-                    'total': stats['tracks'],
-                },
-                'artists': {
-                    'total': stats['artists'],
-                },
-                'albums': {
-                    'total': stats['albums'],
-                },
-                'music': {
-                    'hours': stats['music_duration']
-                },
+        "version": "2.0",
+        "software": {"name": "funkwhale", "version": funkwhale_api.__version__},
+        "protocols": ["activitypub"],
+        "services": {"inbound": [], "outbound": []},
+        "openRegistrations": preferences["users__registration_enabled"],
+        "usage": {"users": {"total": stats["users"]}},
+        "metadata": {
+            "private": preferences["instance__nodeinfo_private"],
+            "shortDescription": preferences["instance__short_description"],
+            "longDescription": preferences["instance__long_description"],
+            "nodeName": preferences["instance__name"],
+            "library": {
+                "federationEnabled": preferences["federation__enabled"],
+                "federationNeedsApproval": preferences[
+                    "federation__music_needs_approval"
+                ],
+                "anonymousCanListen": preferences[
+                    "common__api_authentication_required"
+                ],
+                "tracks": {"total": stats["tracks"]},
+                "artists": {"total": stats["artists"]},
+                "albums": {"total": stats["albums"]},
+                "music": {"hours": stats["music_duration"]},
+            },
+            "usage": {
+                "favorites": {"tracks": {"total": stats["track_favorites"]}},
+                "listenings": {"total": stats["listenings"]},
             },
-            'usage': {
-                'favorites': {
-                    'tracks': {
-                        'total': stats['track_favorites'],
-                    }
-                },
-                'listenings': {
-                    'total': stats['listenings']
-                }
-            }
-        }
+        },
     }
     assert nodeinfo.get() == expected
 
 
 def test_nodeinfo_dump_stats_disabled(preferences, mocker):
-    preferences['instance__nodeinfo_stats_enabled'] = False
+    preferences["instance__nodeinfo_stats_enabled"] = False
 
     expected = {
-        'version': '2.0',
-        'software': {
-            'name': 'funkwhale',
-            'version': funkwhale_api.__version__
-        },
-        'protocols': ['activitypub'],
-        'services': {
-            'inbound': [],
-            'outbound': []
-        },
-        'openRegistrations': preferences['users__registration_enabled'],
-        'usage': {
-            'users': {
-                'total': 0,
-            }
-        },
-        'metadata': {
-            'private': preferences['instance__nodeinfo_private'],
-            'shortDescription': preferences['instance__short_description'],
-            'longDescription': preferences['instance__long_description'],
-            'nodeName': preferences['instance__name'],
-            'library': {
-                'federationEnabled': preferences['federation__enabled'],
-                'federationNeedsApproval': preferences['federation__music_needs_approval'],
-                'anonymousCanListen': preferences['common__api_authentication_required'],
+        "version": "2.0",
+        "software": {"name": "funkwhale", "version": funkwhale_api.__version__},
+        "protocols": ["activitypub"],
+        "services": {"inbound": [], "outbound": []},
+        "openRegistrations": preferences["users__registration_enabled"],
+        "usage": {"users": {"total": 0}},
+        "metadata": {
+            "private": preferences["instance__nodeinfo_private"],
+            "shortDescription": preferences["instance__short_description"],
+            "longDescription": preferences["instance__long_description"],
+            "nodeName": preferences["instance__name"],
+            "library": {
+                "federationEnabled": preferences["federation__enabled"],
+                "federationNeedsApproval": preferences[
+                    "federation__music_needs_approval"
+                ],
+                "anonymousCanListen": preferences[
+                    "common__api_authentication_required"
+                ],
             },
-        }
+        },
     }
     assert nodeinfo.get() == expected
diff --git a/api/tests/instance/test_preferences.py b/api/tests/instance/test_preferences.py
index beb8e6d33f810110c35ed14b164a76ed4c8bb019..b465be9d38571bef6513204922001b6c3dd869b1 100644
--- a/api/tests/instance/test_preferences.py
+++ b/api/tests/instance/test_preferences.py
@@ -1,17 +1,15 @@
 import pytest
-
 from django.urls import reverse
 
-from dynamic_preferences.api import serializers
-
 
 def test_can_list_settings_via_api(preferences, api_client):
-    url = reverse('api:v1:instance:settings')
+    url = reverse("api:v1:instance:settings")
     all_preferences = preferences.model.objects.all()
     expected_preferences = {
         p.preference.identifier(): p
         for p in all_preferences
-        if getattr(p.preference, 'show_in_api', False)}
+        if getattr(p.preference, "show_in_api", False)
+    }
 
     assert len(expected_preferences) > 0
 
@@ -20,15 +18,18 @@ def test_can_list_settings_via_api(preferences, api_client):
     assert len(response.data) == len(expected_preferences)
 
     for p in response.data:
-        i = '__'.join([p['section'], p['name']])
+        i = "__".join([p["section"], p["name"]])
         assert i in expected_preferences
 
 
-@pytest.mark.parametrize('pref,value', [
-    ('instance__name', 'My instance'),
-    ('instance__short_description', 'For music lovers'),
-    ('instance__long_description', 'For real music lovers'),
-])
+@pytest.mark.parametrize(
+    "pref,value",
+    [
+        ("instance__name", "My instance"),
+        ("instance__short_description", "For music lovers"),
+        ("instance__long_description", "For real music lovers"),
+    ],
+)
 def test_instance_settings(pref, value, preferences):
     preferences[pref] = value
 
diff --git a/api/tests/instance/test_stats.py b/api/tests/instance/test_stats.py
index 6063e9300512819a9cc3e6d6bebcc477af9a59d7..1d8bcfc0ad29a26992b22d9ca3b5cb93207f47c6 100644
--- a/api/tests/instance/test_stats.py
+++ b/api/tests/instance/test_stats.py
@@ -1,17 +1,14 @@
-from django.urls import reverse
-
 from funkwhale_api.instance import stats
 
 
 def test_get_users(mocker):
-    mocker.patch(
-        'funkwhale_api.users.models.User.objects.count', return_value=42)
+    mocker.patch("funkwhale_api.users.models.User.objects.count", return_value=42)
 
     assert stats.get_users() == 42
 
 
 def test_get_music_duration(factories):
-    factories['music.TrackFile'].create_batch(size=5, duration=360)
+    factories["music.TrackFile"].create_batch(size=5, duration=360)
 
     # duration is in hours
     assert stats.get_music_duration() == 0.5
@@ -19,56 +16,48 @@ def test_get_music_duration(factories):
 
 def test_get_listenings(mocker):
     mocker.patch(
-        'funkwhale_api.history.models.Listening.objects.count',
-         return_value=42)
+        "funkwhale_api.history.models.Listening.objects.count", return_value=42
+    )
     assert stats.get_listenings() == 42
 
 
 def test_get_track_favorites(mocker):
     mocker.patch(
-        'funkwhale_api.favorites.models.TrackFavorite.objects.count',
-         return_value=42)
+        "funkwhale_api.favorites.models.TrackFavorite.objects.count", return_value=42
+    )
     assert stats.get_track_favorites() == 42
 
 
 def test_get_tracks(mocker):
-    mocker.patch(
-        'funkwhale_api.music.models.Track.objects.count',
-         return_value=42)
+    mocker.patch("funkwhale_api.music.models.Track.objects.count", return_value=42)
     assert stats.get_tracks() == 42
 
 
 def test_get_albums(mocker):
-    mocker.patch(
-        'funkwhale_api.music.models.Album.objects.count',
-         return_value=42)
+    mocker.patch("funkwhale_api.music.models.Album.objects.count", return_value=42)
     assert stats.get_albums() == 42
 
 
 def test_get_artists(mocker):
-    mocker.patch(
-        'funkwhale_api.music.models.Artist.objects.count',
-         return_value=42)
+    mocker.patch("funkwhale_api.music.models.Artist.objects.count", return_value=42)
     assert stats.get_artists() == 42
 
 
 def test_get(mocker):
     keys = [
-        'users',
-        'tracks',
-        'albums',
-        'artists',
-        'track_favorites',
-        'listenings',
-        'music_duration',
+        "users",
+        "tracks",
+        "albums",
+        "artists",
+        "track_favorites",
+        "listenings",
+        "music_duration",
     ]
-    mocks = [
-        mocker.patch.object(stats, 'get_{}'.format(k), return_value=i)
+    [
+        mocker.patch.object(stats, "get_{}".format(k), return_value=i)
         for i, k in enumerate(keys)
     ]
 
-    expected = {
-        k: i for i, k in enumerate(keys)
-    }
+    expected = {k: i for i, k in enumerate(keys)}
 
     assert stats.get() == expected
diff --git a/api/tests/instance/test_views.py b/api/tests/instance/test_views.py
index daf54db51cb32181c380fca8719bc951c97404c6..6dd4f6345afebaf91dc001d0b408fb31ae83bbda 100644
--- a/api/tests/instance/test_views.py
+++ b/api/tests/instance/test_views.py
@@ -1,62 +1,54 @@
 import pytest
-
 from django.urls import reverse
 
 from funkwhale_api.instance import views
 
 
-@pytest.mark.parametrize('view,permissions', [
-    (views.AdminSettings, ['settings']),
-])
+@pytest.mark.parametrize("view,permissions", [(views.AdminSettings, ["settings"])])
 def test_permissions(assert_user_permission, view, permissions):
     assert_user_permission(view, permissions)
 
 
 def test_nodeinfo_endpoint(db, api_client, mocker):
-    payload = {
-        'test': 'test'
-    }
-    mocked_nodeinfo = mocker.patch(
-        'funkwhale_api.instance.nodeinfo.get', return_value=payload)
-    url = reverse('api:v1:instance:nodeinfo-2.0')
+    payload = {"test": "test"}
+    mocker.patch("funkwhale_api.instance.nodeinfo.get", return_value=payload)
+    url = reverse("api:v1:instance:nodeinfo-2.0")
     response = api_client.get(url)
-    ct = 'application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8'  # noqa
+    ct = "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"  # noqa
     assert response.status_code == 200
-    assert response['Content-Type'] == ct
+    assert response["Content-Type"] == ct
     assert response.data == payload
 
 
 def test_nodeinfo_endpoint_disabled(db, api_client, preferences):
-    preferences['instance__nodeinfo_enabled'] = False
-    url = reverse('api:v1:instance:nodeinfo-2.0')
+    preferences["instance__nodeinfo_enabled"] = False
+    url = reverse("api:v1:instance:nodeinfo-2.0")
     response = api_client.get(url)
 
     assert response.status_code == 404
 
 
 def test_settings_only_list_public_settings(db, api_client, preferences):
-    url = reverse('api:v1:instance:settings')
+    url = reverse("api:v1:instance:settings")
     response = api_client.get(url)
 
     for conf in response.data:
-        p = preferences.model.objects.get(
-            section=conf['section'], name=conf['name'])
+        p = preferences.model.objects.get(section=conf["section"], name=conf["name"])
         assert p.preference.show_in_api is True
 
 
 def test_admin_settings_restrict_access(db, logged_in_api_client, preferences):
-    url = reverse('api:v1:instance:admin-settings-list')
+    url = reverse("api:v1:instance:admin-settings-list")
     response = logged_in_api_client.get(url)
 
     assert response.status_code == 403
 
 
-def test_admin_settings_correct_permission(
-        db, logged_in_api_client, preferences):
+def test_admin_settings_correct_permission(db, logged_in_api_client, preferences):
     user = logged_in_api_client.user
     user.permission_settings = True
     user.save()
-    url = reverse('api:v1:instance:admin-settings-list')
+    url = reverse("api:v1:instance:admin-settings-list")
     response = logged_in_api_client.get(url)
 
     assert response.status_code == 200
diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py
index 45167722ca2f95ef23eb3c98b8b96491d41207e0..893cfd86e46e5720446c78dcde5457a7358206b1 100644
--- a/api/tests/manage/test_serializers.py
+++ b/api/tests/manage/test_serializers.py
@@ -2,7 +2,7 @@ from funkwhale_api.manage import serializers
 
 
 def test_manage_track_file_action_delete(factories):
-    tfs = factories['music.TrackFile'](size=5)
+    tfs = factories["music.TrackFile"](size=5)
     s = serializers.ManageTrackFileActionSerializer(queryset=None)
 
     s.handle_delete(tfs.__class__.objects.all())
diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py
index db2e0980a8b512e5267dbfe5882fcb87a76a24f3..e2bfbf3a81511dbf313cd630e1d1353840e38b83 100644
--- a/api/tests/manage/test_views.py
+++ b/api/tests/manage/test_views.py
@@ -1,26 +1,25 @@
 import pytest
-
 from django.urls import reverse
 
-from funkwhale_api.manage import serializers
-from funkwhale_api.manage import views
+from funkwhale_api.manage import serializers, views
 
 
-@pytest.mark.parametrize('view,permissions,operator', [
-    (views.ManageTrackFileViewSet, ['library'], 'and'),
-])
+@pytest.mark.parametrize(
+    "view,permissions,operator", [(views.ManageTrackFileViewSet, ["library"], "and")]
+)
 def test_permissions(assert_user_permission, view, permissions, operator):
     assert_user_permission(view, permissions, operator)
 
 
 def test_track_file_view(factories, superuser_api_client):
-    tfs = factories['music.TrackFile'].create_batch(size=5)
-    qs = tfs[0].__class__.objects.order_by('-creation_date')
-    url = reverse('api:v1:manage:library:track-files-list')
+    tfs = factories["music.TrackFile"].create_batch(size=5)
+    qs = tfs[0].__class__.objects.order_by("-creation_date")
+    url = reverse("api:v1:manage:library:track-files-list")
 
-    response = superuser_api_client.get(url, {'sort': '-creation_date'})
+    response = superuser_api_client.get(url, {"sort": "-creation_date"})
     expected = serializers.ManageTrackFileSerializer(
-        qs, many=True, context={'request': response.wsgi_request}).data
+        qs, many=True, context={"request": response.wsgi_request}
+    ).data
 
-    assert response.data['count'] == len(tfs)
-    assert response.data['results'] == expected
+    assert response.data["count"] == len(tfs)
+    assert response.data["results"] == expected
diff --git a/api/tests/music/conftest.py b/api/tests/music/conftest.py
index 4eea8effe173ee5b90bb18dd1400b5dfff39588a..0cb6f4778765365530b45a95716ae0f07a2d906f 100644
--- a/api/tests/music/conftest.py
+++ b/api/tests/music/conftest.py
@@ -1,66 +1,75 @@
 import pytest
 
+_artists = {"search": {}, "get": {}}
 
-_artists = {'search': {}, 'get': {}}
-
-_artists['search']['adhesive_wombat'] = {
-    'artist-list': [
+_artists["search"]["adhesive_wombat"] = {
+    "artist-list": [
         {
-            'type': 'Person',
-            'ext:score': '100',
-            'id': '62c3befb-6366-4585-b256-809472333801',
-            'disambiguation': 'George Shaw',
-            'gender': 'male',
-            'area': {'sort-name': 'Raleigh', 'id': '3f8828b9-ba93-4604-9b92-1f616fa1abd1', 'name': 'Raleigh'},
-            'sort-name': 'Wombat, Adhesive',
-            'life-span': {'ended': 'false'},
-            'name': 'Adhesive Wombat'
+            "type": "Person",
+            "ext:score": "100",
+            "id": "62c3befb-6366-4585-b256-809472333801",
+            "disambiguation": "George Shaw",
+            "gender": "male",
+            "area": {
+                "sort-name": "Raleigh",
+                "id": "3f8828b9-ba93-4604-9b92-1f616fa1abd1",
+                "name": "Raleigh",
+            },
+            "sort-name": "Wombat, Adhesive",
+            "life-span": {"ended": "false"},
+            "name": "Adhesive Wombat",
         },
         {
-            'country': 'SE',
-            'type': 'Group',
-            'ext:score': '42',
-            'id': '61b34e69-7573-4208-bc89-7061bca5a8fc',
-            'area': {'sort-name': 'Sweden', 'id': '23d10872-f5ae-3f0c-bf55-332788a16ecb', 'name': 'Sweden'},
-            'sort-name': 'Adhesive',
-            'life-span': {'end': '2002-07-12', 'begin': '1994', 'ended': 'true'},
-            'name': 'Adhesive',
-            'begin-area': {
-                'sort-name': 'Katrineholm',
-                'id': '02390d96-b5a3-4282-a38f-e64a95d08b7f',
-                'name': 'Katrineholm'
+            "country": "SE",
+            "type": "Group",
+            "ext:score": "42",
+            "id": "61b34e69-7573-4208-bc89-7061bca5a8fc",
+            "area": {
+                "sort-name": "Sweden",
+                "id": "23d10872-f5ae-3f0c-bf55-332788a16ecb",
+                "name": "Sweden",
+            },
+            "sort-name": "Adhesive",
+            "life-span": {"end": "2002-07-12", "begin": "1994", "ended": "true"},
+            "name": "Adhesive",
+            "begin-area": {
+                "sort-name": "Katrineholm",
+                "id": "02390d96-b5a3-4282-a38f-e64a95d08b7f",
+                "name": "Katrineholm",
             },
         },
     ]
 }
-_artists['get']['adhesive_wombat'] = {'artist': _artists['search']['adhesive_wombat']['artist-list'][0]}
+_artists["get"]["adhesive_wombat"] = {
+    "artist": _artists["search"]["adhesive_wombat"]["artist-list"][0]
+}
 
-_artists['get']['soad'] = {
-    'artist': {
-        'country': 'US',
-        'isni-list': ['0000000121055332'],
-        'type': 'Group',
-        'area': {
-            'iso-3166-1-code-list': ['US'],
-            'sort-name': 'United States',
-            'id': '489ce91b-6658-3307-9877-795b68554c98',
-            'name': 'United States'
+_artists["get"]["soad"] = {
+    "artist": {
+        "country": "US",
+        "isni-list": ["0000000121055332"],
+        "type": "Group",
+        "area": {
+            "iso-3166-1-code-list": ["US"],
+            "sort-name": "United States",
+            "id": "489ce91b-6658-3307-9877-795b68554c98",
+            "name": "United States",
         },
-        'begin-area': {
-            'sort-name': 'Glendale',
-            'id': '6db2e45d-d7f3-43da-ac0b-7ba5ca627373',
-            'name': 'Glendale'
+        "begin-area": {
+            "sort-name": "Glendale",
+            "id": "6db2e45d-d7f3-43da-ac0b-7ba5ca627373",
+            "name": "Glendale",
         },
-        'id': 'cc0b7089-c08d-4c10-b6b0-873582c17fd6',
-        'life-span': {'begin': '1994'},
-        'sort-name': 'System of a Down',
-        'name': 'System of a Down'
+        "id": "cc0b7089-c08d-4c10-b6b0-873582c17fd6",
+        "life-span": {"begin": "1994"},
+        "sort-name": "System of a Down",
+        "name": "System of a Down",
     }
 }
 
-_albums = {'search': {}, 'get': {}, 'get_with_includes': {}}
-_albums['search']['hypnotize'] = {
-    'release-list': [
+_albums = {"search": {}, "get": {}, "get_with_includes": {}}
+_albums["search"]["hypnotize"] = {
+    "release-list": [
         {
             "artist-credit": [
                 {
@@ -69,22 +78,22 @@ _albums['search']['hypnotize'] = {
                             {
                                 "alias": "SoaD",
                                 "sort-name": "SoaD",
-                                "type": "Search hint"
+                                "type": "Search hint",
                             },
                             {
                                 "alias": "S.O.A.D.",
                                 "sort-name": "S.O.A.D.",
-                                "type": "Search hint"
+                                "type": "Search hint",
                             },
                             {
                                 "alias": "System Of Down",
                                 "sort-name": "System Of Down",
-                                "type": "Search hint"
-                            }
+                                "type": "Search hint",
+                            },
                         ],
                         "id": "cc0b7089-c08d-4c10-b6b0-873582c17fd6",
                         "name": "System of a Down",
-                        "sort-name": "System of a Down"
+                        "sort-name": "System of a Down",
                     }
                 }
             ],
@@ -99,16 +108,16 @@ _albums['search']['hypnotize'] = {
                     "catalog-number": "8-2796-93871-2",
                     "label": {
                         "id": "f5be9cfe-e1af-405c-a074-caeaed6797c0",
-                        "name": "American Recordings"
-                    }
+                        "name": "American Recordings",
+                    },
                 },
                 {
                     "catalog-number": "D162990",
                     "label": {
                         "id": "9a7d39a4-a887-40f3-a645-a9a136d1f13f",
-                        "name": "BMG Direct Marketing, Inc."
-                    }
-                }
+                        "name": "BMG Direct Marketing, Inc.",
+                    },
+                },
             ],
             "medium-count": 1,
             "medium-list": [
@@ -117,7 +126,7 @@ _albums['search']['hypnotize'] = {
                     "disc-list": [],
                     "format": "CD",
                     "track-count": 12,
-                    "track-list": []
+                    "track-list": [],
                 }
             ],
             "medium-track-count": 12,
@@ -126,26 +135,21 @@ _albums['search']['hypnotize'] = {
                 {
                     "area": {
                         "id": "489ce91b-6658-3307-9877-795b68554c98",
-                        "iso-3166-1-code-list": [
-                            "US"
-                        ],
+                        "iso-3166-1-code-list": ["US"],
                         "name": "United States",
-                        "sort-name": "United States"
+                        "sort-name": "United States",
                     },
-                    "date": "2005"
+                    "date": "2005",
                 }
             ],
             "release-group": {
                 "id": "72035143-d6ec-308b-8ee5-070b8703902a",
                 "primary-type": "Album",
-                "type": "Album"
+                "type": "Album",
             },
             "status": "Official",
-            "text-representation": {
-                "language": "eng",
-                "script": "Latn"
-            },
-            "title": "Hypnotize"
+            "text-representation": {"language": "eng", "script": "Latn"},
+            "title": "Hypnotize",
         },
         {
             "artist-credit": [
@@ -155,22 +159,22 @@ _albums['search']['hypnotize'] = {
                             {
                                 "alias": "SoaD",
                                 "sort-name": "SoaD",
-                                "type": "Search hint"
+                                "type": "Search hint",
                             },
                             {
                                 "alias": "S.O.A.D.",
                                 "sort-name": "S.O.A.D.",
-                                "type": "Search hint"
+                                "type": "Search hint",
                             },
                             {
                                 "alias": "System Of Down",
                                 "sort-name": "System Of Down",
-                                "type": "Search hint"
-                            }
+                                "type": "Search hint",
+                            },
                         ],
                         "id": "cc0b7089-c08d-4c10-b6b0-873582c17fd6",
                         "name": "System of a Down",
-                        "sort-name": "System of a Down"
+                        "sort-name": "System of a Down",
                     }
                 }
             ],
@@ -188,7 +192,7 @@ _albums['search']['hypnotize'] = {
                     "disc-list": [],
                     "format": "Vinyl",
                     "track-count": 12,
-                    "track-list": []
+                    "track-list": [],
                 }
             ],
             "medium-track-count": 12,
@@ -196,167 +200,233 @@ _albums['search']['hypnotize'] = {
                 {
                     "area": {
                         "id": "489ce91b-6658-3307-9877-795b68554c98",
-                        "iso-3166-1-code-list": [
-                            "US"
-                        ],
+                        "iso-3166-1-code-list": ["US"],
                         "name": "United States",
-                        "sort-name": "United States"
+                        "sort-name": "United States",
                     },
-                    "date": "2005-12-20"
+                    "date": "2005-12-20",
                 }
             ],
             "release-group": {
                 "id": "72035143-d6ec-308b-8ee5-070b8703902a",
                 "primary-type": "Album",
-                "type": "Album"
+                "type": "Album",
             },
             "status": "Official",
-            "text-representation": {
-                "language": "eng",
-                "script": "Latn"
-            },
-            "title": "Hypnotize"
+            "text-representation": {"language": "eng", "script": "Latn"},
+            "title": "Hypnotize",
         },
     ]
 }
-_albums['get']['hypnotize'] = {'release': _albums['search']['hypnotize']['release-list'][0]}
-_albums['get_with_includes']['hypnotize'] = {
-  'release': {
-    'artist-credit': [
-        {'artist': {'id': 'cc0b7089-c08d-4c10-b6b0-873582c17fd6',
-            'name': 'System of a Down',
-            'sort-name': 'System of a Down'}}],
-  'artist-credit-phrase': 'System of a Down',
-  'barcode': '',
-  'country': 'US',
-  'cover-art-archive': {'artwork': 'true',
-   'back': 'false',
-   'count': '1',
-   'front': 'true'},
-  'date': '2005',
-  'id': '47ae093f-1607-49a3-be11-a15d335ccc94',
-  'medium-count': 1,
-  'medium-list': [{'format': 'CD',
-    'position': '1',
-    'track-count': 12,
-    'track-list': [{'id': '59f5cf9a-75b2-3aa3-abda-6807a87107b3',
-      'length': '186000',
-      'number': '1',
-      'position': '1',
-      'recording': {'id': '76d03fc5-758c-48d0-a354-a67de086cc68',
-       'length': '186000',
-       'title': 'Attack'},
-      'track_or_recording_length': '186000'},
-     {'id': '3aaa28c1-12b1-3c2a-b90a-82e09e355608',
-      'length': '239000',
-      'number': '2',
-      'position': '2',
-      'recording': {'id': '327543b0-9193-48c5-83c9-01c7b36c8c0a',
-       'length': '239000',
-       'title': 'Dreaming'},
-      'track_or_recording_length': '239000'},
-     {'id': 'a34fef19-e637-3436-b7eb-276ff2814d6f',
-      'length': '147000',
-      'number': '3',
-      'position': '3',
-      'recording': {'id': '6e27866c-07a1-425d-bb4f-9d9e728db344',
-       'length': '147000',
-       'title': 'Kill Rock ’n Roll'},
-      'track_or_recording_length': '147000'},
-     {'id': '72a4e5c0-c150-3ba1-9ceb-3ab82648af25',
-      'length': '189000',
-      'number': '4',
-      'position': '4',
-      'recording': {'id': '7ff8a67d-c8e2-4b3a-a045-7ad3561d0605',
-       'length': '189000',
-       'title': 'Hypnotize'},
-      'track_or_recording_length': '189000'},
-     {'id': 'a748fa6e-b3b7-3b22-89fb-a038ec92ac32',
-      'length': '178000',
-      'number': '5',
-      'position': '5',
-      'recording': {'id': '19b6eb6a-0e76-4ef7-b63f-959339dbd5d2',
-       'length': '178000',
-       'title': 'Stealing Society'},
-      'track_or_recording_length': '178000'},
-     {'id': '5c5a8d4e-e21a-317e-a719-6e2dbdefa5d2',
-      'length': '216000',
-      'number': '6',
-      'position': '6',
-      'recording': {'id': 'c3c2afe1-ee9a-47cb-b3c6-ff8100bc19d5',
-       'length': '216000',
-       'title': 'Tentative'},
-      'track_or_recording_length': '216000'},
-     {'id': '265718ba-787f-3193-947b-3b6fa69ffe96',
-      'length': '175000',
-      'number': '7',
-      'position': '7',
-      'recording': {'id': '96f804e1-f600-4faa-95a6-ce597e7db120',
-       'length': '175000',
-       'title': 'U‐Fig'},
-      'title': 'U-Fig',
-      'track_or_recording_length': '175000'},
-     {'id': 'cdcf8572-3060-31ca-a72c-1ded81ca1f7a',
-      'length': '328000',
-      'number': '8',
-      'position': '8',
-      'recording': {'id': '26ba38f0-b26b-48b7-8e77-226b22a55f79',
-       'length': '328000',
-       'title': 'Holy Mountains'},
-      'track_or_recording_length': '328000'},
-     {'id': 'f9f00cb0-5635-3217-a2a0-bd61917eb0df',
-      'length': '171000',
-      'number': '9',
-      'position': '9',
-      'recording': {'id': '039f3379-3a69-4e75-a882-df1c4e1608aa',
-       'length': '171000',
-       'title': 'Vicinity of Obscenity'},
-      'track_or_recording_length': '171000'},
-     {'id': 'cdd45914-6741-353e-bbb5-d281048ff24f',
-      'length': '164000',
-      'number': '10',
-      'position': '10',
-      'recording': {'id': 'c24d541a-a9a8-4a22-84c6-5e6419459cf8',
-       'length': '164000',
-       'title': 'She’s Like Heroin'},
-      'track_or_recording_length': '164000'},
-     {'id': 'cfcf12ac-6831-3dd6-a2eb-9d0bfeee3f6d',
-      'length': '167000',
-      'number': '11',
-      'position': '11',
-      'recording': {'id': '0aff4799-849f-4f83-84f4-22cabbba2378',
-       'length': '167000',
-       'title': 'Lonely Day'},
-      'track_or_recording_length': '167000'},
-     {'id': '7e38bb38-ff62-3e41-a670-b7d77f578a1f',
-      'length': '220000',
-      'number': '12',
-      'position': '12',
-      'recording': {'id': 'e1b4d90f-2f44-4fe6-a826-362d4e3d9b88',
-       'length': '220000',
-       'title': 'Soldier Side'},
-      'track_or_recording_length': '220000'}]}],
-  'packaging': 'Digipak',
-  'quality': 'normal',
-  'release-event-count': 1,
-  'release-event-list': [{'area': {'id': '489ce91b-6658-3307-9877-795b68554c98',
-     'iso-3166-1-code-list': ['US'],
-     'name': 'United States',
-     'sort-name': 'United States'},
-    'date': '2005'}],
-  'status': 'Official',
-  'text-representation': {'language': 'eng', 'script': 'Latn'},
-  'title': 'Hypnotize'}}
+_albums["get"]["hypnotize"] = {
+    "release": _albums["search"]["hypnotize"]["release-list"][0]
+}
+_albums["get_with_includes"]["hypnotize"] = {
+    "release": {
+        "artist-credit": [
+            {
+                "artist": {
+                    "id": "cc0b7089-c08d-4c10-b6b0-873582c17fd6",
+                    "name": "System of a Down",
+                    "sort-name": "System of a Down",
+                }
+            }
+        ],
+        "artist-credit-phrase": "System of a Down",
+        "barcode": "",
+        "country": "US",
+        "cover-art-archive": {
+            "artwork": "true",
+            "back": "false",
+            "count": "1",
+            "front": "true",
+        },
+        "date": "2005",
+        "id": "47ae093f-1607-49a3-be11-a15d335ccc94",
+        "medium-count": 1,
+        "medium-list": [
+            {
+                "format": "CD",
+                "position": "1",
+                "track-count": 12,
+                "track-list": [
+                    {
+                        "id": "59f5cf9a-75b2-3aa3-abda-6807a87107b3",
+                        "length": "186000",
+                        "number": "1",
+                        "position": "1",
+                        "recording": {
+                            "id": "76d03fc5-758c-48d0-a354-a67de086cc68",
+                            "length": "186000",
+                            "title": "Attack",
+                        },
+                        "track_or_recording_length": "186000",
+                    },
+                    {
+                        "id": "3aaa28c1-12b1-3c2a-b90a-82e09e355608",
+                        "length": "239000",
+                        "number": "2",
+                        "position": "2",
+                        "recording": {
+                            "id": "327543b0-9193-48c5-83c9-01c7b36c8c0a",
+                            "length": "239000",
+                            "title": "Dreaming",
+                        },
+                        "track_or_recording_length": "239000",
+                    },
+                    {
+                        "id": "a34fef19-e637-3436-b7eb-276ff2814d6f",
+                        "length": "147000",
+                        "number": "3",
+                        "position": "3",
+                        "recording": {
+                            "id": "6e27866c-07a1-425d-bb4f-9d9e728db344",
+                            "length": "147000",
+                            "title": "Kill Rock ’n Roll",
+                        },
+                        "track_or_recording_length": "147000",
+                    },
+                    {
+                        "id": "72a4e5c0-c150-3ba1-9ceb-3ab82648af25",
+                        "length": "189000",
+                        "number": "4",
+                        "position": "4",
+                        "recording": {
+                            "id": "7ff8a67d-c8e2-4b3a-a045-7ad3561d0605",
+                            "length": "189000",
+                            "title": "Hypnotize",
+                        },
+                        "track_or_recording_length": "189000",
+                    },
+                    {
+                        "id": "a748fa6e-b3b7-3b22-89fb-a038ec92ac32",
+                        "length": "178000",
+                        "number": "5",
+                        "position": "5",
+                        "recording": {
+                            "id": "19b6eb6a-0e76-4ef7-b63f-959339dbd5d2",
+                            "length": "178000",
+                            "title": "Stealing Society",
+                        },
+                        "track_or_recording_length": "178000",
+                    },
+                    {
+                        "id": "5c5a8d4e-e21a-317e-a719-6e2dbdefa5d2",
+                        "length": "216000",
+                        "number": "6",
+                        "position": "6",
+                        "recording": {
+                            "id": "c3c2afe1-ee9a-47cb-b3c6-ff8100bc19d5",
+                            "length": "216000",
+                            "title": "Tentative",
+                        },
+                        "track_or_recording_length": "216000",
+                    },
+                    {
+                        "id": "265718ba-787f-3193-947b-3b6fa69ffe96",
+                        "length": "175000",
+                        "number": "7",
+                        "position": "7",
+                        "recording": {
+                            "id": "96f804e1-f600-4faa-95a6-ce597e7db120",
+                            "length": "175000",
+                            "title": "U‐Fig",
+                        },
+                        "title": "U-Fig",
+                        "track_or_recording_length": "175000",
+                    },
+                    {
+                        "id": "cdcf8572-3060-31ca-a72c-1ded81ca1f7a",
+                        "length": "328000",
+                        "number": "8",
+                        "position": "8",
+                        "recording": {
+                            "id": "26ba38f0-b26b-48b7-8e77-226b22a55f79",
+                            "length": "328000",
+                            "title": "Holy Mountains",
+                        },
+                        "track_or_recording_length": "328000",
+                    },
+                    {
+                        "id": "f9f00cb0-5635-3217-a2a0-bd61917eb0df",
+                        "length": "171000",
+                        "number": "9",
+                        "position": "9",
+                        "recording": {
+                            "id": "039f3379-3a69-4e75-a882-df1c4e1608aa",
+                            "length": "171000",
+                            "title": "Vicinity of Obscenity",
+                        },
+                        "track_or_recording_length": "171000",
+                    },
+                    {
+                        "id": "cdd45914-6741-353e-bbb5-d281048ff24f",
+                        "length": "164000",
+                        "number": "10",
+                        "position": "10",
+                        "recording": {
+                            "id": "c24d541a-a9a8-4a22-84c6-5e6419459cf8",
+                            "length": "164000",
+                            "title": "She’s Like Heroin",
+                        },
+                        "track_or_recording_length": "164000",
+                    },
+                    {
+                        "id": "cfcf12ac-6831-3dd6-a2eb-9d0bfeee3f6d",
+                        "length": "167000",
+                        "number": "11",
+                        "position": "11",
+                        "recording": {
+                            "id": "0aff4799-849f-4f83-84f4-22cabbba2378",
+                            "length": "167000",
+                            "title": "Lonely Day",
+                        },
+                        "track_or_recording_length": "167000",
+                    },
+                    {
+                        "id": "7e38bb38-ff62-3e41-a670-b7d77f578a1f",
+                        "length": "220000",
+                        "number": "12",
+                        "position": "12",
+                        "recording": {
+                            "id": "e1b4d90f-2f44-4fe6-a826-362d4e3d9b88",
+                            "length": "220000",
+                            "title": "Soldier Side",
+                        },
+                        "track_or_recording_length": "220000",
+                    },
+                ],
+            }
+        ],
+        "packaging": "Digipak",
+        "quality": "normal",
+        "release-event-count": 1,
+        "release-event-list": [
+            {
+                "area": {
+                    "id": "489ce91b-6658-3307-9877-795b68554c98",
+                    "iso-3166-1-code-list": ["US"],
+                    "name": "United States",
+                    "sort-name": "United States",
+                },
+                "date": "2005",
+            }
+        ],
+        "status": "Official",
+        "text-representation": {"language": "eng", "script": "Latn"},
+        "title": "Hypnotize",
+    }
+}
 
-_albums['get']['marsupial'] = {
-    'release': {
+_albums["get"]["marsupial"] = {
+    "release": {
         "artist-credit": [
             {
                 "artist": {
                     "disambiguation": "George Shaw",
                     "id": "62c3befb-6366-4585-b256-809472333801",
                     "name": "Adhesive Wombat",
-                    "sort-name": "Wombat, Adhesive"
+                    "sort-name": "Wombat, Adhesive",
                 }
             }
         ],
@@ -366,7 +436,7 @@ _albums['get']['marsupial'] = {
             "artwork": "true",
             "back": "false",
             "count": "1",
-            "front": "true"
+            "front": "true",
         },
         "date": "2013-06-05",
         "id": "a50d2a81-2a50-484d-9cb4-b9f6833f583e",
@@ -377,28 +447,23 @@ _albums['get']['marsupial'] = {
             {
                 "area": {
                     "id": "525d4e18-3d00-31b9-a58b-a146a916de8f",
-                    "iso-3166-1-code-list": [
-                        "XW"
-                    ],
+                    "iso-3166-1-code-list": ["XW"],
                     "name": "[Worldwide]",
-                    "sort-name": "[Worldwide]"
+                    "sort-name": "[Worldwide]",
                 },
-                "date": "2013-06-05"
+                "date": "2013-06-05",
             }
         ],
         "status": "Official",
-        "text-representation": {
-            "language": "eng",
-            "script": "Latn"
-        },
-        "title": "Marsupial Madness"
+        "text-representation": {"language": "eng", "script": "Latn"},
+        "title": "Marsupial Madness",
     }
 }
 
-_tracks = {'search': {}, 'get': {}}
+_tracks = {"search": {}, "get": {}}
 
-_tracks['search']['8bitadventures'] = {
-    'recording-list': [
+_tracks["search"]["8bitadventures"] = {
+    "recording-list": [
         {
             "artist-credit": [
                 {
@@ -406,7 +471,7 @@ _tracks['search']['8bitadventures'] = {
                         "disambiguation": "George Shaw",
                         "id": "62c3befb-6366-4585-b256-809472333801",
                         "name": "Adhesive Wombat",
-                        "sort-name": "Wombat, Adhesive"
+                        "sort-name": "Wombat, Adhesive",
                     }
                 }
             ],
@@ -430,9 +495,9 @@ _tracks['search']['8bitadventures'] = {
                                     "length": "271000",
                                     "number": "1",
                                     "title": "8-Bit Adventure",
-                                    "track_or_recording_length": "271000"
+                                    "track_or_recording_length": "271000",
                                 }
-                            ]
+                            ],
                         }
                     ],
                     "medium-track-count": 11,
@@ -440,70 +505,85 @@ _tracks['search']['8bitadventures'] = {
                         {
                             "area": {
                                 "id": "525d4e18-3d00-31b9-a58b-a146a916de8f",
-                                "iso-3166-1-code-list": [
-                                    "XW"
-                                ],
+                                "iso-3166-1-code-list": ["XW"],
                                 "name": "[Worldwide]",
-                                "sort-name": "[Worldwide]"
+                                "sort-name": "[Worldwide]",
                             },
-                            "date": "2013-06-05"
+                            "date": "2013-06-05",
                         }
                     ],
                     "release-group": {
                         "id": "447b4979-2178-405c-bfe6-46bf0b09e6c7",
                         "primary-type": "Album",
-                        "type": "Album"
+                        "type": "Album",
                     },
                     "status": "Official",
-                    "title": "Marsupial Madness"
+                    "title": "Marsupial Madness",
                 }
             ],
             "title": "8-Bit Adventure",
             "tag-list": [
-                {
-                    "count": "2",
-                    "name": "techno"
-                },
-                {
-                    "count": "2",
-                    "name": "good-music"
-                },
+                {"count": "2", "name": "techno"},
+                {"count": "2", "name": "good-music"},
             ],
-        },
+        }
     ]
 }
 
-_tracks['get']['8bitadventures'] = {'recording': _tracks['search']['8bitadventures']['recording-list'][0]}
-_tracks['get']['chop_suey'] = {
-    'recording': {
-        'id': '46c7368a-013a-47b6-97cc-e55e7ab25213',
-        'length': '210240',
-        'title': 'Chop Suey!',
-        'work-relation-list': [{'target': 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5',
-        'type': 'performance',
-        'type-id': 'a3005666-a872-32c3-ad06-98af558e99b0',
-        'work': {'id': 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5',
-            'language': 'eng',
-            'title': 'Chop Suey!'}}]}}
+_tracks["get"]["8bitadventures"] = {
+    "recording": _tracks["search"]["8bitadventures"]["recording-list"][0]
+}
+_tracks["get"]["chop_suey"] = {
+    "recording": {
+        "id": "46c7368a-013a-47b6-97cc-e55e7ab25213",
+        "length": "210240",
+        "title": "Chop Suey!",
+        "work-relation-list": [
+            {
+                "target": "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5",
+                "type": "performance",
+                "type-id": "a3005666-a872-32c3-ad06-98af558e99b0",
+                "work": {
+                    "id": "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5",
+                    "language": "eng",
+                    "title": "Chop Suey!",
+                },
+            }
+        ],
+    }
+}
 
-_works = {'search': {}, 'get': {}}
-_works['get']['chop_suey'] = {'work': {'id': 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5',
-  'language': 'eng',
-  'recording-relation-list': [{'direction': 'backward',
-    'recording': {'disambiguation': 'edit',
-     'id': '07ca77cf-f513-4e9c-b190-d7e24bbad448',
-     'length': '170893',
-     'title': 'Chop Suey!'},
-    'target': '07ca77cf-f513-4e9c-b190-d7e24bbad448',
-    'type': 'performance',
-    'type-id': 'a3005666-a872-32c3-ad06-98af558e99b0'},
-  ],
-  'title': 'Chop Suey!',
-  'type': 'Song',
-  'url-relation-list': [{'direction': 'backward',
-    'target': 'http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!',
-    'type': 'lyrics',
-    'type-id': 'e38e65aa-75e0-42ba-ace0-072aeb91a538'}]}}
+_works = {"search": {}, "get": {}}
+_works["get"]["chop_suey"] = {
+    "work": {
+        "id": "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5",
+        "language": "eng",
+        "recording-relation-list": [
+            {
+                "direction": "backward",
+                "recording": {
+                    "disambiguation": "edit",
+                    "id": "07ca77cf-f513-4e9c-b190-d7e24bbad448",
+                    "length": "170893",
+                    "title": "Chop Suey!",
+                },
+                "target": "07ca77cf-f513-4e9c-b190-d7e24bbad448",
+                "type": "performance",
+                "type-id": "a3005666-a872-32c3-ad06-98af558e99b0",
+            }
+        ],
+        "title": "Chop Suey!",
+        "type": "Song",
+        "url-relation-list": [
+            {
+                "direction": "backward",
+                "target": "http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!",
+                "type": "lyrics",
+                "type-id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
+            }
+        ],
+    }
+}
 
 
 @pytest.fixture()
@@ -537,7 +617,7 @@ def lyricswiki_content():
 <head>
 
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-	<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
 <meta name="generator" content="MediaWiki 1.19.24" />
 <meta name="keywords" content="Chop Suey! lyrics,System Of A Down Chop Suey! lyrics,Chop Suey! by System Of A Down lyrics,lyrics,LyricWiki,LyricWikia,lyricwiki,System Of A Down:Chop Suey!,System Of A Down,System Of A Down:Toxicity (2001),Enter Shikari,Enter Shikari:Chop Suey!,&quot;Weird Al&quot; Yankovic,&quot;Weird Al&quot; Yankovic:Angry White Boy Polka,Renard,Renard:Physicality,System Of A Down:Chop Suey!/pt,Daron Malakian" />
 <meta name="description" content="Chop Suey! This song is by System of a Down and appears on the album Toxicity (2001)." />
@@ -570,4 +650,4 @@ def binary_cover():
     """
     Return an album cover image in form of a binary string
     """
-    return b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x02\x01\x00H\x00H\x00\x00\xff\xed\x08\xaePhotoshop 3.0\x008BIM\x03\xe9\x00\x00\x00\x00\x00x\x00\x03\x00\x00\x00H\x00H\x00\x00\x00\x00\x02\xd8\x02(\xff\xe1\xff\xe2\x02\xf9\x02F\x03G\x05(\x03\xfc\x00\x02\x00\x00\x00H\x00H\x00\x00\x00\x00\x02\xd8\x02(\x00\x01\x00\x00\x00d\x00\x00\x00\x01\x00\x03\x03\x03\x00\x00\x00\x01\'\x0f\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\x08\x00\x19\x01\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x008BIM\x03\xed\x00\x00\x00\x00\x00\x10\x00H\x00\x00\x00\x01\x00\x01\x00H\x00\x00\x00\x01\x00\x018BIM\x03\xf3\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x008BIM\x04\n\x00\x00\x00\x00\x00\x01\x00\x008BIM\'\x10\x00\x00\x00\x00\x00\n\x00\x01\x00\x00\x00\x00\x00\x00\x00\x028BIM\x03\xf5\x00\x00\x00\x00\x00H\x00/ff\x00\x01\x00lff\x00\x06\x00\x00\x00\x00\x00\x01\x00/ff\x00\x01\x00\xa1\x99\x9a\x00\x06\x00\x00\x00\x00\x00\x01\x002\x00\x00\x00\x01\x00Z\x00\x00\x00\x06\x00\x00\x00\x00\x00\x01\x005\x00\x00\x00\x01\x00-\x00\x00\x00\x06\x00\x00\x00\x00\x00\x018BIM\x03\xf8\x00\x00\x00\x00\x00p\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x008BIM\x04\x00\x00\x00\x00\x00\x00\x02\x00\x018BIM\x04\x02\x00\x00\x00\x00\x00\x04\x00\x00\x00\x008BIM\x04\x08\x00\x00\x00\x00\x00\x10\x00\x00\x00\x01\x00\x00\x02@\x00\x00\x02@\x00\x00\x00\x008BIM\x04\t\x00\x00\x00\x00\x06\x9b\x00\x00\x00\x01\x00\x00\x00\x80\x00\x00\x00\x80\x00\x00\x01\x80\x00\x00\xc0\x00\x00\x00\x06\x7f\x00\x18\x00\x01\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x02\x01\x00H\x00H\x00\x00\xff\xfe\x00\'File written by Adobe Photoshop\xa8 4.0\x00\xff\xee\x00\x0eAdobe\x00d\x80\x00\x00\x00\x01\xff\xdb\x00\x84\x00\x0c\x08\x08\x08\t\x08\x0c\t\t\x0c\x11\x0b\n\x0b\x11\x15\x0f\x0c\x0c\x0f\x15\x18\x13\x13\x15\x13\x13\x18\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x01\r\x0b\x0b\r\x0e\r\x10\x0e\x0e\x10\x14\x0e\x0e\x0e\x14\x14\x0e\x0e\x0e\x0e\x14\x11\x0c\x0c\x0c\x0c\x0c\x11\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\xff\xc0\x00\x11\x08\x00\x80\x00\x80\x03\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xdd\x00\x04\x00\x08\xff\xc4\x01?\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x01\x02\x04\x05\x06\x07\x08\t\n\x0b\x01\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x10\x00\x01\x04\x01\x03\x02\x04\x02\x05\x07\x06\x08\x05\x03\x0c3\x01\x00\x02\x11\x03\x04!\x121\x05AQa\x13"q\x812\x06\x14\x91\xa1\xb1B#$\x15R\xc1b34r\x82\xd1C\x07%\x92S\xf0\xe1\xf1cs5\x16\xa2\xb2\x83&D\x93TdE\xc2\xa3t6\x17\xd2U\xe2e\xf2\xb3\x84\xc3\xd3u\xe3\xf3F\'\x94\xa4\x85\xb4\x95\xc4\xd4\xe4\xf4\xa5\xb5\xc5\xd5\xe5\xf5Vfv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf67GWgw\x87\x97\xa7\xb7\xc7\xd7\xe7\xf7\x11\x00\x02\x02\x01\x02\x04\x04\x03\x04\x05\x06\x07\x07\x06\x055\x01\x00\x02\x11\x03!1\x12\x04AQaq"\x13\x052\x81\x91\x14\xa1\xb1B#\xc1R\xd1\xf03$b\xe1r\x82\x92CS\x15cs4\xf1%\x06\x16\xa2\xb2\x83\x07&5\xc2\xd2D\x93T\xa3\x17dEU6te\xe2\xf2\xb3\x84\xc3\xd3u\xe3\xf3F\x94\xa4\x85\xb4\x95\xc4\xd4\xe4\xf4\xa5\xb5\xc5\xd5\xe5\xf5Vfv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf6\'7GWgw\x87\x97\xa7\xb7\xc7\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\xf5T\x92I%)$\x92IJI$\x92R\x92I$\x94\xa4\x92I%)$\x92IJI$\x92R\x92I$\x94\xff\x00\xff\xd0\xf5T\x92I%)$\x92IJI%\xe7\xff\x00Z\x7f\xc6\xbf\xfc\xde\xeb\xb9]\x1f\xf6_\xda~\xcd\xe9\xfe\x9b\xed\x1e\x9e\xefR\xba\xef\xfeo\xec\xf6\xed\xdb\xea\xec\xfeq%>\x80\x92\xf2\xaf\xfc}?\xf3I\xff\x00\xb3_\xfb\xe8\x97\xfe>\x9f\xf9\xa4\xff\x00\xd9\xaf\xfd\xf4IO\xaa\xa4\xbc\xab\xff\x00\x1fO\xfc\xd2\x7f\xec\xd7\xfe\xfa%\xff\x00\x8f\xa7\xfei?\xf6k\xff\x00}\x12S\xea\xa9.+\xeaW\xf8\xc8\xff\x00\x9d}V\xde\x9d\xfb;\xec~\x96;\xb2=O[\xd5\x9d\xaf\xaa\xad\x9b=\n\x7f\xd3}-\xeb\xb5IJI$\x92R\x92I$\x94\xff\x00\xff\xd1\xf5T\x92I%)$\x97\x9f\xff\x00\x8d\x7f\xad=w\xea\xf7\xec\xbf\xd8\xf9_f\xfbO\xda=o\xd1\xd7f\xefO\xec\xfe\x9f\xf3\xf5\xdb\xb7o\xabg\xd0IO\xa0/\x9f\xff\x00\xc6\x97\xfe.\xfa\x9f\xfdc\xff\x00m\xf1\xd2\xff\x00\xc7K\xeb\xdf\xfeY\xff\x00\xe0\x18\xff\x00\xfb\xce\xb9\xfe\xa9\xd53\xfa\xbe}\xbdG\xa8\xdb\xeb\xe5\xdf\xb7\xd4\xb3kY;\x1a\xda\x99\xec\xa9\xac\xaf\xf9\xb63\xf3\x12SU$\x92IJI$\x92S\xdf\xff\x00\x89O\xfcUe\x7f\xe1\x0b?\xf3\xf6*\xf6\xb5\xf3/D\xeb\xfd[\xa0\xe5?3\xa4\xdf\xf6l\x8b+59\xfb\x18\xf9a-\xb1\xcd\xdb{-g\xd3\xa9\x8bk\xff\x00\x1d/\xaf\x7f\xf9g\xff\x00\x80c\xff\x00\xef:J~\x80Iq\xff\x00\xe2\xbf\xaf\xf5n\xbd\xd023:\xb5\xff\x00i\xc8\xaf-\xf55\xfb\x18\xc8`\xae\x8b\x1a\xdd\xb42\xa6};^\xbb\x04\x94\xa4\x92I%?\xff\xd2\xf5T\x92I%)yW\xf8\xf4\xff\x00\xbcO\xfd\n\xff\x00\xddE\xea\xab\xca\xbf\xc7\xa7\xfd\xe2\x7f\xe8W\xfe\xea$\xa7\xca\x92I$\x94\xa4\x92I%)$\x92IJI$\x92S\xed_\xe2S\xff\x00\x12\xb9_\xf8~\xcf\xfc\xf3\x8a\xbd\x01y\xff\x00\xf8\x94\xff\x00\xc4\xaeW\xfe\x1f\xb3\xff\x00<\xe2\xaf@IJI$\x92S\xff\xd3\xf5T\x92I%)yW\xf8\xf4\xff\x00\xbcO\xfd\n\xff\x00\xddE\xea\xab\xca\xbf\xc7\xa7\xfd\xe2\x7f\xe8W\xfe\xea$\xa7\xca\x92I$\x94\xa4\x92I%)$\x92IJI$\x92S\xed_\xe2S\xff\x00\x12\xb9_\xf8~\xcf\xfc\xf3\x8a\xbd\x01y\xff\x00\xf8\x94\xff\x00\xc4\xaeW\xfe\x1f\xb3\xff\x00<\xe2\xaf@IJI$\x92S\xff\xd4\xf5T\x92I%)q_\xe3#\xeaWU\xfa\xd7\xfb;\xf6u\xb8\xf5}\x8f\xd6\xf5>\xd0\xe7\xb6}_Cf\xcfJ\xab\xbf\xd0\xbfr\xedRIO\x8a\x7f\xe3)\xf5\xab\xfe\xe5`\x7f\xdb\x97\x7f\xef*\xe4:\xff\x00D\xca\xe8=Z\xfe\x93\x98\xfa\xec\xc8\xc6\xd9\xbd\xd5\x12Xw\xb1\x97\xb7k\xacmO\xfa\x16\xfe\xe2\xfai|\xff\x00\xfe4\xbf\xf1w\xd4\xff\x00\xeb\x1f\xfbo\x8e\x92\x9eU$\x92IJI$\x92S\xb1\xf5_\xea\xbfP\xfa\xd1\xd4,\xc0\xc0\xb2\x9a\xad\xaa\x93{\x9dys[\xb5\xae\xae\xa2\x01\xaa\xbb\x9d\xbfu\xcd\xfc\xd5\xd3\xff\x00\xe3)\xf5\xab\xfe\xe5`\x7f\xdb\x97\x7f\xef*_\xe2S\xff\x00\x15Y_\xf8B\xcf\xfc\xfd\x8a\xbd\xad%<\xbf\xf8\xbc\xfa\xaf\xd4>\xab\xf4[\xb03\xec\xa6\xdbm\xc9u\xedu\x05\xcen\xd7WM@\x13mt\xbb~\xea]\xf9\xab\xa8I$\x94\xa4\x92I%?\xff\xd5\xf5T\x92I%)$\x92IJ\\\x7f_\xff\x00\x15\xfd\x03\xafuk\xfa\xb6fF]y\x19;7\xb6\xa7\xd6\x1861\x947kl\xa2\xd7\xfd\n\xbf}v\t$\xa7\xcf\xff\x00\xf1\x94\xfa\xab\xff\x00r\xb3\xff\x00\xed\xca\x7f\xf7\x95/\xfce>\xaa\xff\x00\xdc\xac\xff\x00\xfbr\x9f\xfd\xe5^\x80\x92J|\xff\x00\xff\x00\x19O\xaa\xbf\xf7+?\xfe\xdc\xa7\xff\x00yR\xff\x00\xc6S\xea\xaf\xfd\xca\xcf\xff\x00\xb7)\xff\x00\xdeU\xe8\t$\xa7\x97\xfa\xaf\xfe/:/\xd5~\xa1f~\x05\xd96\xdbm&\x876\xf7V\xe6\xeds\xab\xb4\x90*\xa6\x97o\xddK\x7f9u\t$\x92\x94\x92I$\xa5$\x92I)\xff\xd6\xf5T\x92I%)$\x92IJI$\x92R\x92I$\x94\xa4\x92I%)$\x92IJI$\x92R\x92I$\x94\xff\x00\xff\xd9\x008BIM\x04\x06\x00\x00\x00\x00\x00\x07\x00\x03\x00\x00\x00\x01\x01\x00\xff\xfe\x00\'File written by Adobe Photoshop\xa8 4.0\x00\xff\xee\x00\x0eAdobe\x00d\x00\x00\x00\x00\x01\xff\xdb\x00\x84\x00\n\x07\x07\x07\x08\x07\n\x08\x08\n\x0f\n\x08\n\x0f\x12\r\n\n\r\x12\x14\x10\x10\x12\x10\x10\x14\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x01\x0b\x0c\x0c\x15\x13\x15"\x18\x18"\x14\x0e\x0e\x0e\x14\x14\x0e\x0e\x0e\x0e\x14\x11\x0c\x0c\x0c\x0c\x0c\x11\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\xff\xc0\x00\x11\x08\x00\t\x00\t\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xdd\x00\x04\x00\x02\xff\xc4\x01\xa2\x00\x00\x00\x07\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x03\x02\x06\x01\x00\x07\x08\t\n\x0b\x01\x00\x02\x02\x03\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x10\x00\x02\x01\x03\x03\x02\x04\x02\x06\x07\x03\x04\x02\x06\x02s\x01\x02\x03\x11\x04\x00\x05!\x121AQ\x06\x13a"q\x81\x142\x91\xa1\x07\x15\xb1B#\xc1R\xd1\xe13\x16b\xf0$r\x82\xf1%C4S\x92\xa2\xb2cs\xc25D\'\x93\xa3\xb36\x17Tdt\xc3\xd2\xe2\x08&\x83\t\n\x18\x19\x84\x94EF\xa4\xb4V\xd3U(\x1a\xf2\xe3\xf3\xc4\xd4\xe4\xf4eu\x85\x95\xa5\xb5\xc5\xd5\xe5\xf5fv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf67GWgw\x87\x97\xa7\xb7\xc7\xd7\xe7\xf78HXhx\x88\x98\xa8\xb8\xc8\xd8\xe8\xf8)9IYiy\x89\x99\xa9\xb9\xc9\xd9\xe9\xf9*:JZjz\x8a\x9a\xaa\xba\xca\xda\xea\xfa\x11\x00\x02\x02\x01\x02\x03\x05\x05\x04\x05\x06\x04\x08\x03\x03m\x01\x00\x02\x11\x03\x04!\x121A\x05Q\x13a"\x06q\x81\x912\xa1\xb1\xf0\x14\xc1\xd1\xe1#B\x15Rbr\xf13$4C\x82\x16\x92S%\xa2c\xb2\xc2\x07s\xd25\xe2D\x83\x17T\x93\x08\t\n\x18\x19&6E\x1a\'dtU7\xf2\xa3\xb3\xc3()\xd3\xe3\xf3\x84\x94\xa4\xb4\xc4\xd4\xe4\xf4eu\x85\x95\xa5\xb5\xc5\xd5\xe5\xf5FVfv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf6GWgw\x87\x97\xa7\xb7\xc7\xd7\xe7\xf78HXhx\x88\x98\xa8\xb8\xc8\xd8\xe8\xf89IYiy\x89\x99\xa9\xb9\xc9\xd9\xe9\xf9*:JZjz\x8a\x9a\xaa\xba\xca\xda\xea\xfa\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\x91\xea\xfa\xbf\xe6D_\x99\x16\x96\x16\x16\x8c\xdeWf\x84;\x88U\xa1hY\x7f\xd3\'\x9e\xf3\xedCq\x0bz\xfe\x94^\xbc?\xdc\xdb\xff\x00\xa3\xcd\xeb\x7f\xa4\xaa\xf4<U\xff\xd0\xec\xd8\xab\xb1W\xff\xd9'
+    return b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x02\x01\x00H\x00H\x00\x00\xff\xed\x08\xaePhotoshop 3.0\x008BIM\x03\xe9\x00\x00\x00\x00\x00x\x00\x03\x00\x00\x00H\x00H\x00\x00\x00\x00\x02\xd8\x02(\xff\xe1\xff\xe2\x02\xf9\x02F\x03G\x05(\x03\xfc\x00\x02\x00\x00\x00H\x00H\x00\x00\x00\x00\x02\xd8\x02(\x00\x01\x00\x00\x00d\x00\x00\x00\x01\x00\x03\x03\x03\x00\x00\x00\x01'\x0f\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\x08\x00\x19\x01\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x008BIM\x03\xed\x00\x00\x00\x00\x00\x10\x00H\x00\x00\x00\x01\x00\x01\x00H\x00\x00\x00\x01\x00\x018BIM\x03\xf3\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x008BIM\x04\n\x00\x00\x00\x00\x00\x01\x00\x008BIM'\x10\x00\x00\x00\x00\x00\n\x00\x01\x00\x00\x00\x00\x00\x00\x00\x028BIM\x03\xf5\x00\x00\x00\x00\x00H\x00/ff\x00\x01\x00lff\x00\x06\x00\x00\x00\x00\x00\x01\x00/ff\x00\x01\x00\xa1\x99\x9a\x00\x06\x00\x00\x00\x00\x00\x01\x002\x00\x00\x00\x01\x00Z\x00\x00\x00\x06\x00\x00\x00\x00\x00\x01\x005\x00\x00\x00\x01\x00-\x00\x00\x00\x06\x00\x00\x00\x00\x00\x018BIM\x03\xf8\x00\x00\x00\x00\x00p\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x008BIM\x04\x00\x00\x00\x00\x00\x00\x02\x00\x018BIM\x04\x02\x00\x00\x00\x00\x00\x04\x00\x00\x00\x008BIM\x04\x08\x00\x00\x00\x00\x00\x10\x00\x00\x00\x01\x00\x00\x02@\x00\x00\x02@\x00\x00\x00\x008BIM\x04\t\x00\x00\x00\x00\x06\x9b\x00\x00\x00\x01\x00\x00\x00\x80\x00\x00\x00\x80\x00\x00\x01\x80\x00\x00\xc0\x00\x00\x00\x06\x7f\x00\x18\x00\x01\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x02\x01\x00H\x00H\x00\x00\xff\xfe\x00'File written by Adobe Photoshop\xa8 4.0\x00\xff\xee\x00\x0eAdobe\x00d\x80\x00\x00\x00\x01\xff\xdb\x00\x84\x00\x0c\x08\x08\x08\t\x08\x0c\t\t\x0c\x11\x0b\n\x0b\x11\x15\x0f\x0c\x0c\x0f\x15\x18\x13\x13\x15\x13\x13\x18\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x01\r\x0b\x0b\r\x0e\r\x10\x0e\x0e\x10\x14\x0e\x0e\x0e\x14\x14\x0e\x0e\x0e\x0e\x14\x11\x0c\x0c\x0c\x0c\x0c\x11\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\xff\xc0\x00\x11\x08\x00\x80\x00\x80\x03\x01\"\x00\x02\x11\x01\x03\x11\x01\xff\xdd\x00\x04\x00\x08\xff\xc4\x01?\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x01\x02\x04\x05\x06\x07\x08\t\n\x0b\x01\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x10\x00\x01\x04\x01\x03\x02\x04\x02\x05\x07\x06\x08\x05\x03\x0c3\x01\x00\x02\x11\x03\x04!\x121\x05AQa\x13\"q\x812\x06\x14\x91\xa1\xb1B#$\x15R\xc1b34r\x82\xd1C\x07%\x92S\xf0\xe1\xf1cs5\x16\xa2\xb2\x83&D\x93TdE\xc2\xa3t6\x17\xd2U\xe2e\xf2\xb3\x84\xc3\xd3u\xe3\xf3F'\x94\xa4\x85\xb4\x95\xc4\xd4\xe4\xf4\xa5\xb5\xc5\xd5\xe5\xf5Vfv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf67GWgw\x87\x97\xa7\xb7\xc7\xd7\xe7\xf7\x11\x00\x02\x02\x01\x02\x04\x04\x03\x04\x05\x06\x07\x07\x06\x055\x01\x00\x02\x11\x03!1\x12\x04AQaq\"\x13\x052\x81\x91\x14\xa1\xb1B#\xc1R\xd1\xf03$b\xe1r\x82\x92CS\x15cs4\xf1%\x06\x16\xa2\xb2\x83\x07&5\xc2\xd2D\x93T\xa3\x17dEU6te\xe2\xf2\xb3\x84\xc3\xd3u\xe3\xf3F\x94\xa4\x85\xb4\x95\xc4\xd4\xe4\xf4\xa5\xb5\xc5\xd5\xe5\xf5Vfv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf6'7GWgw\x87\x97\xa7\xb7\xc7\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\xf5T\x92I%)$\x92IJI$\x92R\x92I$\x94\xa4\x92I%)$\x92IJI$\x92R\x92I$\x94\xff\x00\xff\xd0\xf5T\x92I%)$\x92IJI%\xe7\xff\x00Z\x7f\xc6\xbf\xfc\xde\xeb\xb9]\x1f\xf6_\xda~\xcd\xe9\xfe\x9b\xed\x1e\x9e\xefR\xba\xef\xfeo\xec\xf6\xed\xdb\xea\xec\xfeq%>\x80\x92\xf2\xaf\xfc}?\xf3I\xff\x00\xb3_\xfb\xe8\x97\xfe>\x9f\xf9\xa4\xff\x00\xd9\xaf\xfd\xf4IO\xaa\xa4\xbc\xab\xff\x00\x1fO\xfc\xd2\x7f\xec\xd7\xfe\xfa%\xff\x00\x8f\xa7\xfei?\xf6k\xff\x00}\x12S\xea\xa9.+\xeaW\xf8\xc8\xff\x00\x9d}V\xde\x9d\xfb;\xec~\x96;\xb2=O[\xd5\x9d\xaf\xaa\xad\x9b=\n\x7f\xd3}-\xeb\xb5IJI$\x92R\x92I$\x94\xff\x00\xff\xd1\xf5T\x92I%)$\x97\x9f\xff\x00\x8d\x7f\xad=w\xea\xf7\xec\xbf\xd8\xf9_f\xfbO\xda=o\xd1\xd7f\xefO\xec\xfe\x9f\xf3\xf5\xdb\xb7o\xabg\xd0IO\xa0/\x9f\xff\x00\xc6\x97\xfe.\xfa\x9f\xfdc\xff\x00m\xf1\xd2\xff\x00\xc7K\xeb\xdf\xfeY\xff\x00\xe0\x18\xff\x00\xfb\xce\xb9\xfe\xa9\xd53\xfa\xbe}\xbdG\xa8\xdb\xeb\xe5\xdf\xb7\xd4\xb3kY;\x1a\xda\x99\xec\xa9\xac\xaf\xf9\xb63\xf3\x12SU$\x92IJI$\x92S\xdf\xff\x00\x89O\xfcUe\x7f\xe1\x0b?\xf3\xf6*\xf6\xb5\xf3/D\xeb\xfd[\xa0\xe5?3\xa4\xdf\xf6l\x8b+59\xfb\x18\xf9a-\xb1\xcd\xdb{-g\xd3\xa9\x8bk\xff\x00\x1d/\xaf\x7f\xf9g\xff\x00\x80c\xff\x00\xef:J~\x80Iq\xff\x00\xe2\xbf\xaf\xf5n\xbd\xd023:\xb5\xff\x00i\xc8\xaf-\xf55\xfb\x18\xc8`\xae\x8b\x1a\xdd\xb42\xa6};^\xbb\x04\x94\xa4\x92I%?\xff\xd2\xf5T\x92I%)yW\xf8\xf4\xff\x00\xbcO\xfd\n\xff\x00\xddE\xea\xab\xca\xbf\xc7\xa7\xfd\xe2\x7f\xe8W\xfe\xea$\xa7\xca\x92I$\x94\xa4\x92I%)$\x92IJI$\x92S\xed_\xe2S\xff\x00\x12\xb9_\xf8~\xcf\xfc\xf3\x8a\xbd\x01y\xff\x00\xf8\x94\xff\x00\xc4\xaeW\xfe\x1f\xb3\xff\x00<\xe2\xaf@IJI$\x92S\xff\xd3\xf5T\x92I%)yW\xf8\xf4\xff\x00\xbcO\xfd\n\xff\x00\xddE\xea\xab\xca\xbf\xc7\xa7\xfd\xe2\x7f\xe8W\xfe\xea$\xa7\xca\x92I$\x94\xa4\x92I%)$\x92IJI$\x92S\xed_\xe2S\xff\x00\x12\xb9_\xf8~\xcf\xfc\xf3\x8a\xbd\x01y\xff\x00\xf8\x94\xff\x00\xc4\xaeW\xfe\x1f\xb3\xff\x00<\xe2\xaf@IJI$\x92S\xff\xd4\xf5T\x92I%)q_\xe3#\xeaWU\xfa\xd7\xfb;\xf6u\xb8\xf5}\x8f\xd6\xf5>\xd0\xe7\xb6}_Cf\xcfJ\xab\xbf\xd0\xbfr\xedRIO\x8a\x7f\xe3)\xf5\xab\xfe\xe5`\x7f\xdb\x97\x7f\xef*\xe4:\xff\x00D\xca\xe8=Z\xfe\x93\x98\xfa\xec\xc8\xc6\xd9\xbd\xd5\x12Xw\xb1\x97\xb7k\xacmO\xfa\x16\xfe\xe2\xfai|\xff\x00\xfe4\xbf\xf1w\xd4\xff\x00\xeb\x1f\xfbo\x8e\x92\x9eU$\x92IJI$\x92S\xb1\xf5_\xea\xbfP\xfa\xd1\xd4,\xc0\xc0\xb2\x9a\xad\xaa\x93{\x9dys[\xb5\xae\xae\xa2\x01\xaa\xbb\x9d\xbfu\xcd\xfc\xd5\xd3\xff\x00\xe3)\xf5\xab\xfe\xe5`\x7f\xdb\x97\x7f\xef*_\xe2S\xff\x00\x15Y_\xf8B\xcf\xfc\xfd\x8a\xbd\xad%<\xbf\xf8\xbc\xfa\xaf\xd4>\xab\xf4[\xb03\xec\xa6\xdbm\xc9u\xedu\x05\xcen\xd7WM@\x13mt\xbb~\xea]\xf9\xab\xa8I$\x94\xa4\x92I%?\xff\xd5\xf5T\x92I%)$\x92IJ\\\x7f_\xff\x00\x15\xfd\x03\xafuk\xfa\xb6fF]y\x19;7\xb6\xa7\xd6\x1861\x947kl\xa2\xd7\xfd\n\xbf}v\t$\xa7\xcf\xff\x00\xf1\x94\xfa\xab\xff\x00r\xb3\xff\x00\xed\xca\x7f\xf7\x95/\xfce>\xaa\xff\x00\xdc\xac\xff\x00\xfbr\x9f\xfd\xe5^\x80\x92J|\xff\x00\xff\x00\x19O\xaa\xbf\xf7+?\xfe\xdc\xa7\xff\x00yR\xff\x00\xc6S\xea\xaf\xfd\xca\xcf\xff\x00\xb7)\xff\x00\xdeU\xe8\t$\xa7\x97\xfa\xaf\xfe/:/\xd5~\xa1f~\x05\xd96\xdbm&\x876\xf7V\xe6\xeds\xab\xb4\x90*\xa6\x97o\xddK\x7f9u\t$\x92\x94\x92I$\xa5$\x92I)\xff\xd6\xf5T\x92I%)$\x92IJI$\x92R\x92I$\x94\xa4\x92I%)$\x92IJI$\x92R\x92I$\x94\xff\x00\xff\xd9\x008BIM\x04\x06\x00\x00\x00\x00\x00\x07\x00\x03\x00\x00\x00\x01\x01\x00\xff\xfe\x00'File written by Adobe Photoshop\xa8 4.0\x00\xff\xee\x00\x0eAdobe\x00d\x00\x00\x00\x00\x01\xff\xdb\x00\x84\x00\n\x07\x07\x07\x08\x07\n\x08\x08\n\x0f\n\x08\n\x0f\x12\r\n\n\r\x12\x14\x10\x10\x12\x10\x10\x14\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x01\x0b\x0c\x0c\x15\x13\x15\"\x18\x18\"\x14\x0e\x0e\x0e\x14\x14\x0e\x0e\x0e\x0e\x14\x11\x0c\x0c\x0c\x0c\x0c\x11\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\xff\xc0\x00\x11\x08\x00\t\x00\t\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xdd\x00\x04\x00\x02\xff\xc4\x01\xa2\x00\x00\x00\x07\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x03\x02\x06\x01\x00\x07\x08\t\n\x0b\x01\x00\x02\x02\x03\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x10\x00\x02\x01\x03\x03\x02\x04\x02\x06\x07\x03\x04\x02\x06\x02s\x01\x02\x03\x11\x04\x00\x05!\x121AQ\x06\x13a\"q\x81\x142\x91\xa1\x07\x15\xb1B#\xc1R\xd1\xe13\x16b\xf0$r\x82\xf1%C4S\x92\xa2\xb2cs\xc25D'\x93\xa3\xb36\x17Tdt\xc3\xd2\xe2\x08&\x83\t\n\x18\x19\x84\x94EF\xa4\xb4V\xd3U(\x1a\xf2\xe3\xf3\xc4\xd4\xe4\xf4eu\x85\x95\xa5\xb5\xc5\xd5\xe5\xf5fv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf67GWgw\x87\x97\xa7\xb7\xc7\xd7\xe7\xf78HXhx\x88\x98\xa8\xb8\xc8\xd8\xe8\xf8)9IYiy\x89\x99\xa9\xb9\xc9\xd9\xe9\xf9*:JZjz\x8a\x9a\xaa\xba\xca\xda\xea\xfa\x11\x00\x02\x02\x01\x02\x03\x05\x05\x04\x05\x06\x04\x08\x03\x03m\x01\x00\x02\x11\x03\x04!\x121A\x05Q\x13a\"\x06q\x81\x912\xa1\xb1\xf0\x14\xc1\xd1\xe1#B\x15Rbr\xf13$4C\x82\x16\x92S%\xa2c\xb2\xc2\x07s\xd25\xe2D\x83\x17T\x93\x08\t\n\x18\x19&6E\x1a'dtU7\xf2\xa3\xb3\xc3()\xd3\xe3\xf3\x84\x94\xa4\xb4\xc4\xd4\xe4\xf4eu\x85\x95\xa5\xb5\xc5\xd5\xe5\xf5FVfv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf6GWgw\x87\x97\xa7\xb7\xc7\xd7\xe7\xf78HXhx\x88\x98\xa8\xb8\xc8\xd8\xe8\xf89IYiy\x89\x99\xa9\xb9\xc9\xd9\xe9\xf9*:JZjz\x8a\x9a\xaa\xba\xca\xda\xea\xfa\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\x91\xea\xfa\xbf\xe6D_\x99\x16\x96\x16\x16\x8c\xdeWf\x84;\x88U\xa1hY\x7f\xd3'\x9e\xf3\xedCq\x0bz\xfe\x94^\xbc?\xdc\xdb\xff\x00\xa3\xcd\xeb\x7f\xa4\xaa\xf4<U\xff\xd0\xec\xd8\xab\xb1W\xff\xd9"
diff --git a/api/tests/music/test_activity.py b/api/tests/music/test_activity.py
index f604874c14e75442b9ba50170107a4aa127497a2..8c119394d620b28eac7a594a89bae552e968c3ad 100644
--- a/api/tests/music/test_activity.py
+++ b/api/tests/music/test_activity.py
@@ -1,17 +1,10 @@
-from funkwhale_api.users.serializers import UserActivitySerializer
-from funkwhale_api.favorites import serializers
-
-
-
 def test_get_track_activity_url_mbid(factories):
-    track = factories['music.Track']()
-    expected = 'https://musicbrainz.org/recording/{}'.format(
-        track.mbid)
+    track = factories["music.Track"]()
+    expected = "https://musicbrainz.org/recording/{}".format(track.mbid)
     assert track.get_activity_url() == expected
 
 
 def test_get_track_activity_url_no_mbid(settings, factories):
-    track = factories['music.Track'](mbid=None)
-    expected = settings.FUNKWHALE_URL + '/tracks/{}'.format(
-        track.pk)
+    track = factories["music.Track"](mbid=None)
+    expected = settings.FUNKWHALE_URL + "/tracks/{}".format(track.pk)
     assert track.get_activity_url() == expected
diff --git a/api/tests/music/test_api.py b/api/tests/music/test_api.py
index 7aa20e6262dd42f204054cf58b4d1f69cd2795a5..29a712ce6218db293947e98897dc287710b22d58 100644
--- a/api/tests/music/test_api.py
+++ b/api/tests/music/test_api.py
@@ -1,246 +1,240 @@
 import json
 import os
+
 import pytest
 from django.urls import reverse
 
-from funkwhale_api.music import models
-from funkwhale_api.musicbrainz import api
-from funkwhale_api.music import serializers
-from funkwhale_api.music import tasks
-
+from funkwhale_api.music import models, tasks
 
 DATA_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
 def test_can_submit_youtube_url_for_track_import(
-        settings, artists, albums, tracks, mocker, superuser_client):
-    mocker.patch('funkwhale_api.music.tasks.import_job_run.delay')
+    settings, artists, albums, tracks, mocker, superuser_client
+):
+    mocker.patch("funkwhale_api.music.tasks.import_job_run.delay")
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.artists.get',
-        return_value=artists['get']['adhesive_wombat'])
+        "funkwhale_api.musicbrainz.api.artists.get",
+        return_value=artists["get"]["adhesive_wombat"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.get',
-        return_value=albums['get']['marsupial'])
+        "funkwhale_api.musicbrainz.api.releases.get",
+        return_value=albums["get"]["marsupial"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.recordings.get',
-        return_value=tracks['get']['8bitadventures'])
+        "funkwhale_api.musicbrainz.api.recordings.get",
+        return_value=tracks["get"]["8bitadventures"],
+    )
     mocker.patch(
-        'funkwhale_api.music.models.TrackFile.download_file',
-        return_value=None)
-    mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
-    video_id = 'tPEE9ZwTmy0'
-    url = reverse('api:v1:submit-single')
-    video_url = 'https://www.youtube.com/watch?v={0}'.format(video_id)
-    response = superuser_client.post(
-        url,
-        {'import_url': video_url,
-         'mbid': mbid})
+        "funkwhale_api.music.models.TrackFile.download_file", return_value=None
+    )
+    mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed"
+    video_id = "tPEE9ZwTmy0"
+    url = reverse("api:v1:submit-single")
+    video_url = "https://www.youtube.com/watch?v={0}".format(video_id)
+    response = superuser_client.post(url, {"import_url": video_url, "mbid": mbid})
 
     assert response.status_code == 201
-    batch = superuser_client.user.imports.latest('id')
-    job = batch.jobs.latest('id')
-    assert job.status == 'pending'
+    batch = superuser_client.user.imports.latest("id")
+    job = batch.jobs.latest("id")
+    assert job.status == "pending"
     assert str(job.mbid) == mbid
     assert job.source == video_url
 
 
 def test_import_creates_an_import_with_correct_data(mocker, superuser_client):
-    mocker.patch('funkwhale_api.music.tasks.import_job_run')
-    mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
-    video_id = 'tPEE9ZwTmy0'
-    url = reverse('api:v1:submit-single')
-    response = superuser_client.post(
+    mocker.patch("funkwhale_api.music.tasks.import_job_run")
+    mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed"
+    video_id = "tPEE9ZwTmy0"
+    url = reverse("api:v1:submit-single")
+    superuser_client.post(
         url,
-        {'import_url': 'https://www.youtube.com/watch?v={0}'.format(video_id),
-         'mbid': mbid})
+        {
+            "import_url": "https://www.youtube.com/watch?v={0}".format(video_id),
+            "mbid": mbid,
+        },
+    )
 
-    batch = models.ImportBatch.objects.latest('id')
+    batch = models.ImportBatch.objects.latest("id")
     assert batch.jobs.count() == 1
     assert batch.submitted_by == superuser_client.user
-    assert batch.status == 'pending'
+    assert batch.status == "pending"
     job = batch.jobs.first()
     assert str(job.mbid) == mbid
-    assert job.status == 'pending'
-    assert job.source == 'https://www.youtube.com/watch?v={0}'.format(video_id)
+    assert job.status == "pending"
+    assert job.source == "https://www.youtube.com/watch?v={0}".format(video_id)
 
 
-def test_can_import_whole_album(
-        artists, albums, mocker, superuser_client):
-    mocker.patch('funkwhale_api.music.tasks.import_job_run')
+def test_can_import_whole_album(artists, albums, mocker, superuser_client):
+    mocker.patch("funkwhale_api.music.tasks.import_job_run")
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.artists.get',
-        return_value=artists['get']['soad'])
+        "funkwhale_api.musicbrainz.api.artists.get", return_value=artists["get"]["soad"]
+    )
+    mocker.patch("funkwhale_api.musicbrainz.api.images.get_front", return_value=b"")
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.images.get_front',
-        return_value=b'')
-    mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.get',
-        return_value=albums['get_with_includes']['hypnotize'])
+        "funkwhale_api.musicbrainz.api.releases.get",
+        return_value=albums["get_with_includes"]["hypnotize"],
+    )
     payload = {
-        'releaseId': '47ae093f-1607-49a3-be11-a15d335ccc94',
-        'tracks': [
+        "releaseId": "47ae093f-1607-49a3-be11-a15d335ccc94",
+        "tracks": [
             {
-            'mbid': '1968a9d6-8d92-4051-8f76-674e157b6eed',
-            'source': 'https://www.youtube.com/watch?v=1111111111',
+                "mbid": "1968a9d6-8d92-4051-8f76-674e157b6eed",
+                "source": "https://www.youtube.com/watch?v=1111111111",
             },
             {
-            'mbid': '2968a9d6-8d92-4051-8f76-674e157b6eed',
-            'source': 'https://www.youtube.com/watch?v=2222222222',
+                "mbid": "2968a9d6-8d92-4051-8f76-674e157b6eed",
+                "source": "https://www.youtube.com/watch?v=2222222222",
             },
             {
-            'mbid': '3968a9d6-8d92-4051-8f76-674e157b6eed',
-            'source': 'https://www.youtube.com/watch?v=3333333333',
+                "mbid": "3968a9d6-8d92-4051-8f76-674e157b6eed",
+                "source": "https://www.youtube.com/watch?v=3333333333",
             },
-        ]
+        ],
     }
-    url = reverse('api:v1:submit-album')
-    response = superuser_client.post(
-        url, json.dumps(payload), content_type="application/json")
+    url = reverse("api:v1:submit-album")
+    superuser_client.post(url, json.dumps(payload), content_type="application/json")
 
-    batch = models.ImportBatch.objects.latest('id')
+    batch = models.ImportBatch.objects.latest("id")
     assert batch.jobs.count() == 3
     assert batch.submitted_by == superuser_client.user
-    assert batch.status == 'pending'
+    assert batch.status == "pending"
 
-    album = models.Album.objects.latest('id')
-    assert str(album.mbid) == '47ae093f-1607-49a3-be11-a15d335ccc94'
-    medium_data = albums['get_with_includes']['hypnotize']['release']['medium-list'][0]
-    assert int(medium_data['track-count']) == album.tracks.all().count()
+    album = models.Album.objects.latest("id")
+    assert str(album.mbid) == "47ae093f-1607-49a3-be11-a15d335ccc94"
+    medium_data = albums["get_with_includes"]["hypnotize"]["release"]["medium-list"][0]
+    assert int(medium_data["track-count"]) == album.tracks.all().count()
 
-    for track in medium_data['track-list']:
-        instance = models.Track.objects.get(mbid=track['recording']['id'])
-        assert instance.title == track['recording']['title']
-        assert instance.position == int(track['position'])
-        assert instance.title == track['recording']['title']
+    for track in medium_data["track-list"]:
+        instance = models.Track.objects.get(mbid=track["recording"]["id"])
+        assert instance.title == track["recording"]["title"]
+        assert instance.position == int(track["position"])
+        assert instance.title == track["recording"]["title"]
 
-    for row in payload['tracks']:
-        job = models.ImportJob.objects.get(mbid=row['mbid'])
-        assert str(job.mbid) == row['mbid']
-        assert job.status == 'pending'
-        assert job.source == row['source']
+    for row in payload["tracks"]:
+        job = models.ImportJob.objects.get(mbid=row["mbid"])
+        assert str(job.mbid) == row["mbid"]
+        assert job.status == "pending"
+        assert job.source == row["source"]
 
 
-def test_can_import_whole_artist(
-        artists, albums, mocker, superuser_client):
-    mocker.patch('funkwhale_api.music.tasks.import_job_run')
-    mocker.patch(
-        'funkwhale_api.musicbrainz.api.artists.get',
-        return_value=artists['get']['soad'])
+def test_can_import_whole_artist(artists, albums, mocker, superuser_client):
+    mocker.patch("funkwhale_api.music.tasks.import_job_run")
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.images.get_front',
-        return_value=b'')
+        "funkwhale_api.musicbrainz.api.artists.get", return_value=artists["get"]["soad"]
+    )
+    mocker.patch("funkwhale_api.musicbrainz.api.images.get_front", return_value=b"")
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.get',
-        return_value=albums['get_with_includes']['hypnotize'])
+        "funkwhale_api.musicbrainz.api.releases.get",
+        return_value=albums["get_with_includes"]["hypnotize"],
+    )
     payload = {
-        'artistId': 'mbid',
-        'albums': [
+        "artistId": "mbid",
+        "albums": [
             {
-                'releaseId': '47ae093f-1607-49a3-be11-a15d335ccc94',
-                'tracks': [
+                "releaseId": "47ae093f-1607-49a3-be11-a15d335ccc94",
+                "tracks": [
                     {
-                    'mbid': '1968a9d6-8d92-4051-8f76-674e157b6eed',
-                    'source': 'https://www.youtube.com/watch?v=1111111111',
+                        "mbid": "1968a9d6-8d92-4051-8f76-674e157b6eed",
+                        "source": "https://www.youtube.com/watch?v=1111111111",
                     },
                     {
-                    'mbid': '2968a9d6-8d92-4051-8f76-674e157b6eed',
-                    'source': 'https://www.youtube.com/watch?v=2222222222',
+                        "mbid": "2968a9d6-8d92-4051-8f76-674e157b6eed",
+                        "source": "https://www.youtube.com/watch?v=2222222222",
                     },
                     {
-                    'mbid': '3968a9d6-8d92-4051-8f76-674e157b6eed',
-                    'source': 'https://www.youtube.com/watch?v=3333333333',
+                        "mbid": "3968a9d6-8d92-4051-8f76-674e157b6eed",
+                        "source": "https://www.youtube.com/watch?v=3333333333",
                     },
-                ]
+                ],
             }
-        ]
+        ],
     }
-    url = reverse('api:v1:submit-artist')
-    response = superuser_client.post(
-        url, json.dumps(payload), content_type="application/json")
+    url = reverse("api:v1:submit-artist")
+    superuser_client.post(url, json.dumps(payload), content_type="application/json")
 
-    batch = models.ImportBatch.objects.latest('id')
+    batch = models.ImportBatch.objects.latest("id")
     assert batch.jobs.count() == 3
     assert batch.submitted_by == superuser_client.user
-    assert batch.status == 'pending'
+    assert batch.status == "pending"
 
-    album = models.Album.objects.latest('id')
-    assert str(album.mbid) == '47ae093f-1607-49a3-be11-a15d335ccc94'
-    medium_data = albums['get_with_includes']['hypnotize']['release']['medium-list'][0]
-    assert int(medium_data['track-count']) == album.tracks.all().count()
+    album = models.Album.objects.latest("id")
+    assert str(album.mbid) == "47ae093f-1607-49a3-be11-a15d335ccc94"
+    medium_data = albums["get_with_includes"]["hypnotize"]["release"]["medium-list"][0]
+    assert int(medium_data["track-count"]) == album.tracks.all().count()
 
-    for track in medium_data['track-list']:
-        instance = models.Track.objects.get(mbid=track['recording']['id'])
-        assert instance.title == track['recording']['title']
-        assert instance.position == int(track['position'])
-        assert instance.title == track['recording']['title']
+    for track in medium_data["track-list"]:
+        instance = models.Track.objects.get(mbid=track["recording"]["id"])
+        assert instance.title == track["recording"]["title"]
+        assert instance.position == int(track["position"])
+        assert instance.title == track["recording"]["title"]
 
-    for row in payload['albums'][0]['tracks']:
-        job = models.ImportJob.objects.get(mbid=row['mbid'])
-        assert str(job.mbid) == row['mbid']
-        assert job.status == 'pending'
-        assert job.source == row['source']
+    for row in payload["albums"][0]["tracks"]:
+        job = models.ImportJob.objects.get(mbid=row["mbid"])
+        assert str(job.mbid) == row["mbid"]
+        assert job.status == "pending"
+        assert job.source == row["source"]
 
 
 def test_user_can_create_an_empty_batch(superuser_api_client, factories):
-    url = reverse('api:v1:import-batches-list')
+    url = reverse("api:v1:import-batches-list")
     response = superuser_api_client.post(url)
 
     assert response.status_code == 201
 
-    batch = superuser_api_client.user.imports.latest('id')
+    batch = superuser_api_client.user.imports.latest("id")
 
     assert batch.submitted_by == superuser_api_client.user
-    assert batch.source == 'api'
+    assert batch.source == "api"
 
 
-def test_user_can_create_import_job_with_file(
-        superuser_api_client, factories, mocker):
-    path = os.path.join(DATA_DIR, 'test.ogg')
-    m = mocker.patch('funkwhale_api.common.utils.on_commit')
-    batch = factories['music.ImportBatch'](
-        submitted_by=superuser_api_client.user)
-    url = reverse('api:v1:import-jobs-list')
-    with open(path, 'rb') as f:
+def test_user_can_create_import_job_with_file(superuser_api_client, factories, mocker):
+    path = os.path.join(DATA_DIR, "test.ogg")
+    m = mocker.patch("funkwhale_api.common.utils.on_commit")
+    batch = factories["music.ImportBatch"](submitted_by=superuser_api_client.user)
+    url = reverse("api:v1:import-jobs-list")
+    with open(path, "rb") as f:
         content = f.read()
         f.seek(0)
-        response = superuser_api_client.post(url, {
-            'batch': batch.pk,
-            'audio_file': f,
-            'source': 'file://'
-        })
+        response = superuser_api_client.post(
+            url, {"batch": batch.pk, "audio_file": f, "source": "file://"}
+        )
 
     assert response.status_code == 201
 
-    job = batch.jobs.latest('id')
+    job = batch.jobs.latest("id")
 
-    assert job.status == 'pending'
-    assert job.source.startswith('file://')
-    assert 'test.ogg' in job.source
+    assert job.status == "pending"
+    assert job.source.startswith("file://")
+    assert "test.ogg" in job.source
     assert job.audio_file.read() == content
 
-    m.assert_called_once_with(
-        tasks.import_job_run.delay,
-        import_job_id=job.pk)
+    m.assert_called_once_with(tasks.import_job_run.delay, import_job_id=job.pk)
 
 
-@pytest.mark.parametrize('route,method', [
-    ('api:v1:tags-list', 'get'),
-    ('api:v1:tracks-list', 'get'),
-    ('api:v1:artists-list', 'get'),
-    ('api:v1:albums-list', 'get'),
-])
+@pytest.mark.parametrize(
+    "route,method",
+    [
+        ("api:v1:tags-list", "get"),
+        ("api:v1:tracks-list", "get"),
+        ("api:v1:artists-list", "get"),
+        ("api:v1:albums-list", "get"),
+    ],
+)
 def test_can_restrict_api_views_to_authenticated_users(
-        db, route, method, preferences, client):
+    db, route, method, preferences, client
+):
     url = reverse(route)
-    preferences['common__api_authentication_required'] = True
+    preferences["common__api_authentication_required"] = True
     response = getattr(client, method)(url)
     assert response.status_code == 401
 
 
 def test_track_file_url_is_restricted_to_authenticated_users(
-        api_client, factories, preferences):
-    preferences['common__api_authentication_required'] = True
-    f = factories['music.TrackFile']()
+    api_client, factories, preferences
+):
+    preferences["common__api_authentication_required"] = True
+    f = factories["music.TrackFile"]()
     assert f.audio_file is not None
     url = f.path
     response = api_client.get(url)
@@ -248,12 +242,13 @@ def test_track_file_url_is_restricted_to_authenticated_users(
 
 
 def test_track_file_url_is_accessible_to_authenticated_users(
-        logged_in_api_client, factories, preferences):
-    preferences['common__api_authentication_required'] = True
-    f = factories['music.TrackFile']()
+    logged_in_api_client, factories, preferences
+):
+    preferences["common__api_authentication_required"] = True
+    f = factories["music.TrackFile"]()
     assert f.audio_file is not None
     url = f.path
     response = logged_in_api_client.get(url)
 
     assert response.status_code == 200
-    assert response['X-Accel-Redirect'] == '/_protected{}'.format(f.audio_file.url)
+    assert response["X-Accel-Redirect"] == "/_protected{}".format(f.audio_file.url)
diff --git a/api/tests/music/test_commands.py b/api/tests/music/test_commands.py
index 6f03f6b8ad33e3f6a974ff2da8aa91f053d7ad09..03a9420dc77e598231fe0ac9056bea67868a3897 100644
--- a/api/tests/music/test_commands.py
+++ b/api/tests/music/test_commands.py
@@ -6,13 +6,14 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
 def test_fix_track_files_bitrate_length(factories, mocker):
-    tf1 = factories['music.TrackFile'](bitrate=1, duration=2)
-    tf2 = factories['music.TrackFile'](bitrate=None, duration=None)
+    tf1 = factories["music.TrackFile"](bitrate=1, duration=2)
+    tf2 = factories["music.TrackFile"](bitrate=None, duration=None)
     c = fix_track_files.Command()
 
     mocker.patch(
-        'funkwhale_api.music.utils.get_audio_file_data',
-        return_value={'bitrate': 42, 'length': 43})
+        "funkwhale_api.music.utils.get_audio_file_data",
+        return_value={"bitrate": 42, "length": 43},
+    )
 
     c.fix_file_data(dry_run=False)
 
@@ -29,13 +30,11 @@ def test_fix_track_files_bitrate_length(factories, mocker):
 
 
 def test_fix_track_files_size(factories, mocker):
-    tf1 = factories['music.TrackFile'](size=1)
-    tf2 = factories['music.TrackFile'](size=None)
+    tf1 = factories["music.TrackFile"](size=1)
+    tf2 = factories["music.TrackFile"](size=None)
     c = fix_track_files.Command()
 
-    mocker.patch(
-        'funkwhale_api.music.models.TrackFile.get_file_size',
-        return_value=2)
+    mocker.patch("funkwhale_api.music.models.TrackFile.get_file_size", return_value=2)
 
     c.fix_file_size(dry_run=False)
 
@@ -50,24 +49,25 @@ def test_fix_track_files_size(factories, mocker):
 
 
 def test_fix_track_files_mimetype(factories, mocker):
-    name = 'test.mp3'
-    mp3_path = os.path.join(DATA_DIR, 'test.mp3')
-    ogg_path = os.path.join(DATA_DIR, 'test.ogg')
-    tf1 = factories['music.TrackFile'](
+    mp3_path = os.path.join(DATA_DIR, "test.mp3")
+    ogg_path = os.path.join(DATA_DIR, "test.ogg")
+    tf1 = factories["music.TrackFile"](
         audio_file__from_path=mp3_path,
-        source='file://{}'.format(mp3_path),
-        mimetype='application/x-empty')
+        source="file://{}".format(mp3_path),
+        mimetype="application/x-empty",
+    )
 
     # this one already has a mimetype set, to it should not be updated
-    tf2 = factories['music.TrackFile'](
+    tf2 = factories["music.TrackFile"](
         audio_file__from_path=ogg_path,
-        source='file://{}'.format(ogg_path),
-        mimetype='audio/something')
+        source="file://{}".format(ogg_path),
+        mimetype="audio/something",
+    )
     c = fix_track_files.Command()
     c.fix_mimetypes(dry_run=False)
 
     tf1.refresh_from_db()
     tf2.refresh_from_db()
 
-    assert tf1.mimetype == 'audio/mpeg'
-    assert tf2.mimetype == 'audio/something'
+    assert tf1.mimetype == "audio/mpeg"
+    assert tf2.mimetype == "audio/something"
diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py
index 8453dca8407ad551f70930a00d5e16494560ab38..2be267cffd28f9d9ab1b85fbe2b5f8e5834ed5a7 100644
--- a/api/tests/music/test_import.py
+++ b/api/tests/music/test_import.py
@@ -1,6 +1,5 @@
 import json
 import os
-import pytest
 
 from django.urls import reverse
 
@@ -12,56 +11,51 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
 def test_create_import_can_bind_to_request(
-        artists, albums, mocker, factories, superuser_api_client):
-    request = factories['requests.ImportRequest']()
+    artists, albums, mocker, factories, superuser_api_client
+):
+    request = factories["requests.ImportRequest"]()
 
-    mocker.patch('funkwhale_api.music.tasks.import_job_run')
+    mocker.patch("funkwhale_api.music.tasks.import_job_run")
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.artists.get',
-        return_value=artists['get']['soad'])
-    mocker.patch(
-        'funkwhale_api.musicbrainz.api.images.get_front',
-        return_value=b'')
+        "funkwhale_api.musicbrainz.api.artists.get", return_value=artists["get"]["soad"]
+    )
+    mocker.patch("funkwhale_api.musicbrainz.api.images.get_front", return_value=b"")
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.get',
-        return_value=albums['get_with_includes']['hypnotize'])
+        "funkwhale_api.musicbrainz.api.releases.get",
+        return_value=albums["get_with_includes"]["hypnotize"],
+    )
     payload = {
-        'releaseId': '47ae093f-1607-49a3-be11-a15d335ccc94',
-        'importRequest': request.pk,
-        'tracks': [
+        "releaseId": "47ae093f-1607-49a3-be11-a15d335ccc94",
+        "importRequest": request.pk,
+        "tracks": [
             {
-                'mbid': '1968a9d6-8d92-4051-8f76-674e157b6eed',
-                'source': 'https://www.youtube.com/watch?v=1111111111',
+                "mbid": "1968a9d6-8d92-4051-8f76-674e157b6eed",
+                "source": "https://www.youtube.com/watch?v=1111111111",
             }
-        ]
+        ],
     }
-    url = reverse('api:v1:submit-album')
-    response = superuser_api_client.post(
-        url, json.dumps(payload), content_type='application/json')
-    batch = request.import_batches.latest('id')
+    url = reverse("api:v1:submit-album")
+    superuser_api_client.post(url, json.dumps(payload), content_type="application/json")
+    batch = request.import_batches.latest("id")
 
     assert batch.import_request == request
 
 
 def test_import_job_from_federation_no_musicbrainz(factories, mocker):
     mocker.patch(
-        'funkwhale_api.music.utils.get_audio_file_data',
-        return_value={'bitrate': 24, 'length': 666})
-    mocker.patch(
-        'funkwhale_api.music.models.TrackFile.get_file_size',
-        return_value=42)
-    lt = factories['federation.LibraryTrack'](
-        artist_name='Hello',
-        album_title='World',
-        title='Ping',
+        "funkwhale_api.music.utils.get_audio_file_data",
+        return_value={"bitrate": 24, "length": 666},
+    )
+    mocker.patch("funkwhale_api.music.models.TrackFile.get_file_size", return_value=42)
+    lt = factories["federation.LibraryTrack"](
+        artist_name="Hello",
+        album_title="World",
+        title="Ping",
         metadata__length=42,
         metadata__bitrate=43,
         metadata__size=44,
     )
-    job = factories['music.ImportJob'](
-        federation=True,
-        library_track=lt,
-    )
+    job = factories["music.ImportJob"](federation=True, library_track=lt)
 
     tasks.import_job_run(import_job_id=job.pk)
     job.refresh_from_db()
@@ -72,25 +66,21 @@ def test_import_job_from_federation_no_musicbrainz(factories, mocker):
     assert tf.bitrate == 43
     assert tf.size == 44
     assert tf.library_track == job.library_track
-    assert tf.track.title == 'Ping'
-    assert tf.track.artist.name == 'Hello'
-    assert tf.track.album.title == 'World'
+    assert tf.track.title == "Ping"
+    assert tf.track.artist.name == "Hello"
+    assert tf.track.album.title == "World"
 
 
 def test_import_job_from_federation_musicbrainz_recording(factories, mocker):
-    t = factories['music.Track']()
+    t = factories["music.Track"]()
     track_from_api = mocker.patch(
-        'funkwhale_api.music.models.Track.get_or_create_from_api',
-        return_value=(t, True))
-    lt = factories['federation.LibraryTrack'](
-        metadata__recording__musicbrainz=True,
-        artist_name='Hello',
-        album_title='World',
+        "funkwhale_api.music.models.Track.get_or_create_from_api",
+        return_value=(t, True),
     )
-    job = factories['music.ImportJob'](
-        federation=True,
-        library_track=lt,
+    lt = factories["federation.LibraryTrack"](
+        metadata__recording__musicbrainz=True, artist_name="Hello", album_title="World"
     )
+    job = factories["music.ImportJob"](federation=True, library_track=lt)
 
     tasks.import_job_run(import_job_id=job.pk)
     job.refresh_from_db()
@@ -100,23 +90,20 @@ def test_import_job_from_federation_musicbrainz_recording(factories, mocker):
     assert tf.library_track == job.library_track
     assert tf.track == t
     track_from_api.assert_called_once_with(
-        mbid=lt.metadata['recording']['musicbrainz_id'])
+        mbid=lt.metadata["recording"]["musicbrainz_id"]
+    )
 
 
 def test_import_job_from_federation_musicbrainz_release(factories, mocker):
-    a = factories['music.Album']()
+    a = factories["music.Album"]()
     album_from_api = mocker.patch(
-        'funkwhale_api.music.models.Album.get_or_create_from_api',
-        return_value=(a, True))
-    lt = factories['federation.LibraryTrack'](
-        metadata__release__musicbrainz=True,
-        artist_name='Hello',
-        title='Ping',
+        "funkwhale_api.music.models.Album.get_or_create_from_api",
+        return_value=(a, True),
     )
-    job = factories['music.ImportJob'](
-        federation=True,
-        library_track=lt,
+    lt = factories["federation.LibraryTrack"](
+        metadata__release__musicbrainz=True, artist_name="Hello", title="Ping"
     )
+    job = factories["music.ImportJob"](federation=True, library_track=lt)
 
     tasks.import_job_run(import_job_id=job.pk)
     job.refresh_from_db()
@@ -124,28 +111,25 @@ def test_import_job_from_federation_musicbrainz_release(factories, mocker):
     tf = job.track_file
     assert tf.mimetype == lt.audio_mimetype
     assert tf.library_track == job.library_track
-    assert tf.track.title == 'Ping'
+    assert tf.track.title == "Ping"
     assert tf.track.artist == a.artist
     assert tf.track.album == a
 
     album_from_api.assert_called_once_with(
-        mbid=lt.metadata['release']['musicbrainz_id'])
+        mbid=lt.metadata["release"]["musicbrainz_id"]
+    )
 
 
 def test_import_job_from_federation_musicbrainz_artist(factories, mocker):
-    a = factories['music.Artist']()
+    a = factories["music.Artist"]()
     artist_from_api = mocker.patch(
-        'funkwhale_api.music.models.Artist.get_or_create_from_api',
-        return_value=(a, True))
-    lt = factories['federation.LibraryTrack'](
-        metadata__artist__musicbrainz=True,
-        album_title='World',
-        title='Ping',
+        "funkwhale_api.music.models.Artist.get_or_create_from_api",
+        return_value=(a, True),
     )
-    job = factories['music.ImportJob'](
-        federation=True,
-        library_track=lt,
+    lt = factories["federation.LibraryTrack"](
+        metadata__artist__musicbrainz=True, album_title="World", title="Ping"
     )
+    job = factories["music.ImportJob"](federation=True, library_track=lt)
 
     tasks.import_job_run(import_job_id=job.pk)
     job.refresh_from_db()
@@ -154,108 +138,102 @@ def test_import_job_from_federation_musicbrainz_artist(factories, mocker):
     assert tf.mimetype == lt.audio_mimetype
     assert tf.library_track == job.library_track
 
-    assert tf.track.title == 'Ping'
+    assert tf.track.title == "Ping"
     assert tf.track.artist == a
     assert tf.track.album.artist == a
-    assert tf.track.album.title == 'World'
+    assert tf.track.album.title == "World"
 
     artist_from_api.assert_called_once_with(
-        mbid=lt.metadata['artist']['musicbrainz_id'])
+        mbid=lt.metadata["artist"]["musicbrainz_id"]
+    )
 
 
-def test_import_job_run_triggers_notifies_followers(
-        factories, mocker, tmpfile):
+def test_import_job_run_triggers_notifies_followers(factories, mocker, tmpfile):
     mocker.patch(
-        'funkwhale_api.downloader.download',
-        return_value={'audio_file_path': tmpfile.name})
+        "funkwhale_api.downloader.download",
+        return_value={"audio_file_path": tmpfile.name},
+    )
     mocked_notify = mocker.patch(
-        'funkwhale_api.music.tasks.import_batch_notify_followers.delay')
-    batch = factories['music.ImportBatch']()
-    job = factories['music.ImportJob'](
-        finished=True, batch=batch)
-    track = factories['music.Track'](mbid=job.mbid)
+        "funkwhale_api.music.tasks.import_batch_notify_followers.delay"
+    )
+    batch = factories["music.ImportBatch"]()
+    job = factories["music.ImportJob"](finished=True, batch=batch)
+    factories["music.Track"](mbid=job.mbid)
 
     batch.update_status()
     batch.refresh_from_db()
 
-    assert batch.status == 'finished'
+    assert batch.status == "finished"
 
     mocked_notify.assert_called_once_with(import_batch_id=batch.pk)
 
 
 def test_import_batch_notifies_followers_skip_on_disabled_federation(
-        preferences, factories, mocker):
-    mocked_deliver = mocker.patch('funkwhale_api.federation.activity.deliver')
-    batch = factories['music.ImportBatch'](finished=True)
-    preferences['federation__enabled'] = False
+    preferences, factories, mocker
+):
+    mocked_deliver = mocker.patch("funkwhale_api.federation.activity.deliver")
+    batch = factories["music.ImportBatch"](finished=True)
+    preferences["federation__enabled"] = False
     tasks.import_batch_notify_followers(import_batch_id=batch.pk)
 
     mocked_deliver.assert_not_called()
 
 
-def test_import_batch_notifies_followers_skip_on_federation_import(
-        factories, mocker):
-    mocked_deliver = mocker.patch('funkwhale_api.federation.activity.deliver')
-    batch = factories['music.ImportBatch'](finished=True, federation=True)
+def test_import_batch_notifies_followers_skip_on_federation_import(factories, mocker):
+    mocked_deliver = mocker.patch("funkwhale_api.federation.activity.deliver")
+    batch = factories["music.ImportBatch"](finished=True, federation=True)
     tasks.import_batch_notify_followers(import_batch_id=batch.pk)
 
     mocked_deliver.assert_not_called()
 
 
-def test_import_batch_notifies_followers(
-        factories, mocker):
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
+def test_import_batch_notifies_followers(factories, mocker):
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
 
-    f1 = factories['federation.Follow'](approved=True, target=library_actor)
-    f2 = factories['federation.Follow'](approved=False, target=library_actor)
-    f3 = factories['federation.Follow']()
+    f1 = factories["federation.Follow"](approved=True, target=library_actor)
+    factories["federation.Follow"](approved=False, target=library_actor)
+    factories["federation.Follow"]()
 
-    mocked_deliver = mocker.patch('funkwhale_api.federation.activity.deliver')
-    batch = factories['music.ImportBatch']()
-    job1 = factories['music.ImportJob'](
-        finished=True, batch=batch)
-    job2 = factories['music.ImportJob'](
-        finished=True, federation=True, batch=batch)
-    job3 = factories['music.ImportJob'](
-        status='pending', batch=batch)
+    mocked_deliver = mocker.patch("funkwhale_api.federation.activity.deliver")
+    batch = factories["music.ImportBatch"]()
+    job1 = factories["music.ImportJob"](finished=True, batch=batch)
+    factories["music.ImportJob"](finished=True, federation=True, batch=batch)
+    factories["music.ImportJob"](status="pending", batch=batch)
 
-    batch.status = 'finished'
+    batch.status = "finished"
     batch.save()
     tasks.import_batch_notify_followers(import_batch_id=batch.pk)
 
     # only f1 match the requirements to be notified
     # and only job1 is a non federated track with finished import
     expected = {
-        '@context': federation_serializers.AP_CONTEXT,
-        'actor': library_actor.url,
-        'type': 'Create',
-        'id': batch.get_federation_url(),
-        'to': [f1.actor.url],
-        'object': federation_serializers.CollectionSerializer(
+        "@context": federation_serializers.AP_CONTEXT,
+        "actor": library_actor.url,
+        "type": "Create",
+        "id": batch.get_federation_url(),
+        "to": [f1.actor.url],
+        "object": federation_serializers.CollectionSerializer(
             {
-                'id': batch.get_federation_url(),
-                'items': [job1.track_file],
-                'actor': library_actor,
-                'item_serializer': federation_serializers.AudioSerializer
+                "id": batch.get_federation_url(),
+                "items": [job1.track_file],
+                "actor": library_actor,
+                "item_serializer": federation_serializers.AudioSerializer,
             }
-        ).data
+        ).data,
     }
 
     mocked_deliver.assert_called_once_with(
-        expected,
-        on_behalf_of=library_actor,
-        to=[f1.actor.url]
+        expected, on_behalf_of=library_actor, to=[f1.actor.url]
     )
 
 
 def test__do_import_in_place_mbid(factories, tmpfile):
-    path = os.path.join(DATA_DIR, 'test.ogg')
-    job = factories['music.ImportJob'](
-        in_place=True, source='file://{}'.format(path))
+    path = os.path.join(DATA_DIR, "test.ogg")
+    job = factories["music.ImportJob"](in_place=True, source="file://{}".format(path))
 
-    track = factories['music.Track'](mbid=job.mbid)
+    factories["music.Track"](mbid=job.mbid)
     tf = tasks._do_import(job, use_acoustid=False)
 
     assert bool(tf.audio_file) is False
-    assert tf.source == 'file://{}'.format(path)
-    assert tf.mimetype == 'audio/ogg'
+    assert tf.source == "file://{}".format(path)
+    assert tf.mimetype == "audio/ogg"
diff --git a/api/tests/music/test_lyrics.py b/api/tests/music/test_lyrics.py
index 3aee368c0e9c98a0da626b05f427191b4f8372d4..c8ce92b6a26418e9600e725b93bd1daa53ee72a4 100644
--- a/api/tests/music/test_lyrics.py
+++ b/api/tests/music/test_lyrics.py
@@ -1,27 +1,20 @@
-import json
 from django.urls import reverse
 
-from funkwhale_api.music import models
-from funkwhale_api.musicbrainz import api
-from funkwhale_api.music import serializers
-from funkwhale_api.music import tasks
 from funkwhale_api.music import lyrics as lyrics_utils
+from funkwhale_api.music import models, tasks
 
 
-def test_works_import_lyrics_if_any(
-        lyricswiki_content, mocker, factories):
+def test_lyrics_tasks(lyricswiki_content, mocker, factories):
     mocker.patch(
-        'funkwhale_api.music.lyrics._get_html',
-        return_value=lyricswiki_content)
-    lyrics = factories['music.Lyrics'](
-        url='http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!')
+        "funkwhale_api.music.lyrics._get_html", return_value=lyricswiki_content
+    )
+    lyrics = factories["music.Lyrics"](
+        url="http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!"
+    )
 
     tasks.fetch_content(lyrics_id=lyrics.pk)
     lyrics.refresh_from_db()
-    self.assertIn(
-        'Grab a brush and put on a little makeup',
-        lyrics.content,
-    )
+    assert "Grab a brush and put on a little makeup" in lyrics.content
 
 
 def test_clean_content():
@@ -39,40 +32,38 @@ def test_markdown_rendering(factories):
     content = """Hello
 Is it me you're looking for?"""
 
-    l = factories['music.Lyrics'](content=content)
+    lyrics = factories["music.Lyrics"](content=content)
 
     expected = "<p>Hello<br />\nIs it me you're looking for?</p>"
-    assert expected == l.content_rendered
+    assert expected == lyrics.content_rendered
 
 
 def test_works_import_lyrics_if_any(
-        lyricswiki_content,
-        works,
-        tracks,
-        mocker,
-        factories,
-        logged_in_client):
+    lyricswiki_content, works, tracks, mocker, factories, logged_in_client
+):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.works.get',
-        return_value=works['get']['chop_suey'])
+        "funkwhale_api.musicbrainz.api.works.get",
+        return_value=works["get"]["chop_suey"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.recordings.get',
-        return_value=tracks['get']['chop_suey'])
+        "funkwhale_api.musicbrainz.api.recordings.get",
+        return_value=tracks["get"]["chop_suey"],
+    )
     mocker.patch(
-        'funkwhale_api.music.lyrics._get_html',
-        return_value=lyricswiki_content)
-    track = factories['music.Track'](
-        work=None,
-        mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
+        "funkwhale_api.music.lyrics._get_html", return_value=lyricswiki_content
+    )
+    track = factories["music.Track"](
+        work=None, mbid="07ca77cf-f513-4e9c-b190-d7e24bbad448"
+    )
 
-    url = reverse('api:v1:tracks-lyrics', kwargs={'pk': track.pk})
+    url = reverse("api:v1:tracks-lyrics", kwargs={"pk": track.pk})
     response = logged_in_client.get(url)
 
     assert response.status_code == 200
 
     track.refresh_from_db()
-    lyrics = models.Lyrics.objects.latest('id')
-    work = models.Work.objects.latest('id')
+    lyrics = models.Lyrics.objects.latest("id")
+    work = models.Work.objects.latest("id")
 
     assert track.work == work
     assert lyrics.work == work
diff --git a/api/tests/music/test_metadata.py b/api/tests/music/test_metadata.py
index a4f15b3355f9e2299e682675fd1622d16ab19353..fbdf5b81fcbca1532c6716466a5bade62c776d0a 100644
--- a/api/tests/music/test_metadata.py
+++ b/api/tests/music/test_metadata.py
@@ -1,111 +1,124 @@
 import datetime
 import os
-import pytest
 import uuid
 
+import pytest
+
 from funkwhale_api.music import metadata
 
 DATA_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
-@pytest.mark.parametrize('field,value', [
-    ('title', 'Peer Gynt Suite no. 1, op. 46: I. Morning'),
-    ('artist', 'Edvard Grieg'),
-    ('album', 'Peer Gynt Suite no. 1, op. 46'),
-    ('date', datetime.date(2012, 8, 15)),
-    ('track_number', 1),
-    ('musicbrainz_albumid', uuid.UUID('a766da8b-8336-47aa-a3ee-371cc41ccc75')),
-    ('musicbrainz_recordingid', uuid.UUID('bd21ac48-46d8-4e78-925f-d9cc2a294656')),
-    ('musicbrainz_artistid', uuid.UUID('013c8e5b-d72a-4cd3-8dee-6c64d6125823')),
-])
+@pytest.mark.parametrize(
+    "field,value",
+    [
+        ("title", "Peer Gynt Suite no. 1, op. 46: I. Morning"),
+        ("artist", "Edvard Grieg"),
+        ("album", "Peer Gynt Suite no. 1, op. 46"),
+        ("date", datetime.date(2012, 8, 15)),
+        ("track_number", 1),
+        ("musicbrainz_albumid", uuid.UUID("a766da8b-8336-47aa-a3ee-371cc41ccc75")),
+        ("musicbrainz_recordingid", uuid.UUID("bd21ac48-46d8-4e78-925f-d9cc2a294656")),
+        ("musicbrainz_artistid", uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823")),
+    ],
+)
 def test_can_get_metadata_from_ogg_file(field, value):
-    path = os.path.join(DATA_DIR, 'test.ogg')
+    path = os.path.join(DATA_DIR, "test.ogg")
     data = metadata.Metadata(path)
 
     assert data.get(field) == value
 
-@pytest.mark.parametrize('field,value', [
-    ('title', 'Drei Kreuze (dass wir hier sind)'),
-    ('artist', 'Die Toten Hosen'),
-    ('album', 'Ballast der Republik'),
-    ('date', datetime.date(2012, 5, 4)),
-    ('track_number', 1),
-    ('musicbrainz_albumid', uuid.UUID('1f0441ad-e609-446d-b355-809c445773cf')),
-    ('musicbrainz_recordingid', uuid.UUID('124d0150-8627-46bc-bc14-789a3bc960c8')),
-    ('musicbrainz_artistid', uuid.UUID('c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1')),
-])
+
+@pytest.mark.parametrize(
+    "field,value",
+    [
+        ("title", "Drei Kreuze (dass wir hier sind)"),
+        ("artist", "Die Toten Hosen"),
+        ("album", "Ballast der Republik"),
+        ("date", datetime.date(2012, 5, 4)),
+        ("track_number", 1),
+        ("musicbrainz_albumid", uuid.UUID("1f0441ad-e609-446d-b355-809c445773cf")),
+        ("musicbrainz_recordingid", uuid.UUID("124d0150-8627-46bc-bc14-789a3bc960c8")),
+        ("musicbrainz_artistid", uuid.UUID("c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1")),
+    ],
+)
 def test_can_get_metadata_from_ogg_theora_file(field, value):
-    path = os.path.join(DATA_DIR, 'test_theora.ogg')
+    path = os.path.join(DATA_DIR, "test_theora.ogg")
     data = metadata.Metadata(path)
 
     assert data.get(field) == value
 
 
-@pytest.mark.parametrize('field,value', [
-    ('title', 'Bend'),
-    ('artist', 'Bindrpilot'),
-    ('album', 'You Can\'t Stop Da Funk'),
-    ('date', datetime.date(2006, 2, 7)),
-    ('track_number', 2),
-    ('musicbrainz_albumid', uuid.UUID('ce40cdb1-a562-4fd8-a269-9269f98d4124')),
-    ('musicbrainz_recordingid', uuid.UUID('f269d497-1cc0-4ae4-a0c4-157ec7d73fcb')),
-    ('musicbrainz_artistid', uuid.UUID('9c6bddde-6228-4d9f-ad0d-03f6fcb19e13')),
-])
+@pytest.mark.parametrize(
+    "field,value",
+    [
+        ("title", "Bend"),
+        ("artist", "Bindrpilot"),
+        ("album", "You Can't Stop Da Funk"),
+        ("date", datetime.date(2006, 2, 7)),
+        ("track_number", 2),
+        ("musicbrainz_albumid", uuid.UUID("ce40cdb1-a562-4fd8-a269-9269f98d4124")),
+        ("musicbrainz_recordingid", uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcb")),
+        ("musicbrainz_artistid", uuid.UUID("9c6bddde-6228-4d9f-ad0d-03f6fcb19e13")),
+    ],
+)
 def test_can_get_metadata_from_id3_mp3_file(field, value):
-    path = os.path.join(DATA_DIR, 'test.mp3')
+    path = os.path.join(DATA_DIR, "test.mp3")
     data = metadata.Metadata(path)
 
     assert data.get(field) == value
 
 
-@pytest.mark.parametrize('name', ['test.mp3', 'sample.flac'])
+@pytest.mark.parametrize("name", ["test.mp3", "sample.flac"])
 def test_can_get_pictures(name):
     path = os.path.join(DATA_DIR, name)
     data = metadata.Metadata(path)
 
-    pictures = data.get('pictures')
+    pictures = data.get("pictures")
     assert len(pictures) == 1
-    cover_data = data.get_picture('cover_front')
-    assert cover_data['mimetype'].startswith('image/')
-    assert len(cover_data['content']) > 0
-    assert type(cover_data['content']) == bytes
-    assert type(cover_data['description']) == str
-
-
-@pytest.mark.parametrize('field,value', [
-    ('title', '999,999'),
-    ('artist', 'Nine Inch Nails'),
-    ('album', 'The Slip'),
-    ('date', datetime.date(2008, 5, 5)),
-    ('track_number', 1),
-    ('musicbrainz_albumid', uuid.UUID('12b57d46-a192-499e-a91f-7da66790a1c1')),
-    ('musicbrainz_recordingid', uuid.UUID('30f3f33e-8d0c-4e69-8539-cbd701d18f28')),
-    ('musicbrainz_artistid', uuid.UUID('b7ffd2af-418f-4be2-bdd1-22f8b48613da')),
-])
+    cover_data = data.get_picture("cover_front")
+    assert cover_data["mimetype"].startswith("image/")
+    assert len(cover_data["content"]) > 0
+    assert type(cover_data["content"]) == bytes
+    assert type(cover_data["description"]) == str
+
+
+@pytest.mark.parametrize(
+    "field,value",
+    [
+        ("title", "999,999"),
+        ("artist", "Nine Inch Nails"),
+        ("album", "The Slip"),
+        ("date", datetime.date(2008, 5, 5)),
+        ("track_number", 1),
+        ("musicbrainz_albumid", uuid.UUID("12b57d46-a192-499e-a91f-7da66790a1c1")),
+        ("musicbrainz_recordingid", uuid.UUID("30f3f33e-8d0c-4e69-8539-cbd701d18f28")),
+        ("musicbrainz_artistid", uuid.UUID("b7ffd2af-418f-4be2-bdd1-22f8b48613da")),
+    ],
+)
 def test_can_get_metadata_from_flac_file(field, value):
-    path = os.path.join(DATA_DIR, 'sample.flac')
+    path = os.path.join(DATA_DIR, "sample.flac")
     data = metadata.Metadata(path)
 
     assert data.get(field) == value
 
 
 def test_can_get_metadata_from_flac_file_not_crash_if_empty():
-    path = os.path.join(DATA_DIR, 'sample.flac')
+    path = os.path.join(DATA_DIR, "sample.flac")
     data = metadata.Metadata(path)
 
     with pytest.raises(metadata.TagNotFound):
-        data.get('test')
+        data.get("test")
 
 
-@pytest.mark.parametrize('field_name', [
-    'musicbrainz_artistid',
-    'musicbrainz_albumid',
-    'musicbrainz_recordingid',
-])
+@pytest.mark.parametrize(
+    "field_name",
+    ["musicbrainz_artistid", "musicbrainz_albumid", "musicbrainz_recordingid"],
+)
 def test_mbid_clean_keeps_only_first(field_name):
     u1 = str(uuid.uuid4())
     u2 = str(uuid.uuid4())
     field = metadata.VALIDATION[field_name]
-    result = field.to_python('/'.join([u1, u2]))
+    result = field.to_python("/".join([u1, u2]))
 
     assert str(result) == u1
diff --git a/api/tests/music/test_models.py b/api/tests/music/test_models.py
index 0ef54eb668014462e57649129115b2719e56e2f8..df18a09096fc147454f4fbf1905a84ff185213b2 100644
--- a/api/tests/music/test_models.py
+++ b/api/tests/music/test_models.py
@@ -1,15 +1,14 @@
 import os
+
 import pytest
 
-from funkwhale_api.music import models
-from funkwhale_api.music import importers
-from funkwhale_api.music import tasks
+from funkwhale_api.music import importers, models, tasks
 
 DATA_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
 def test_can_store_release_group_id_on_album(factories):
-    album = factories['music.Album']()
+    album = factories["music.Album"]()
     assert album.release_group_id is not None
 
 
@@ -21,7 +20,7 @@ def test_import_album_stores_release_group(factories):
                     "disambiguation": "George Shaw",
                     "id": "62c3befb-6366-4585-b256-809472333801",
                     "name": "Adhesive Wombat",
-                    "sort-name": "Wombat, Adhesive"
+                    "sort-name": "Wombat, Adhesive",
                 }
             }
         ],
@@ -31,137 +30,134 @@ def test_import_album_stores_release_group(factories):
         "id": "a50d2a81-2a50-484d-9cb4-b9f6833f583e",
         "status": "Official",
         "title": "Marsupial Madness",
-        'release-group': {'id': '447b4979-2178-405c-bfe6-46bf0b09e6c7'}
+        "release-group": {"id": "447b4979-2178-405c-bfe6-46bf0b09e6c7"},
     }
-    artist = factories['music.Artist'](
-        mbid=album_data['artist-credit'][0]['artist']['id']
+    artist = factories["music.Artist"](
+        mbid=album_data["artist-credit"][0]["artist"]["id"]
     )
     cleaned_data = models.Album.clean_musicbrainz_data(album_data)
     album = importers.load(models.Album, cleaned_data, album_data, import_hooks=[])
 
-    assert album.release_group_id == album_data['release-group']['id']
+    assert album.release_group_id == album_data["release-group"]["id"]
     assert album.artist == artist
 
 
 def test_import_track_from_release(factories, mocker):
-    album = factories['music.Album'](
-        mbid='430347cb-0879-3113-9fde-c75b658c298e')
+    album = factories["music.Album"](mbid="430347cb-0879-3113-9fde-c75b658c298e")
     album_data = {
-        'release': {
-            'id': album.mbid,
-            'title': 'Daydream Nation',
-            'status': 'Official',
-            'medium-count': 1,
-            'medium-list': [
+        "release": {
+            "id": album.mbid,
+            "title": "Daydream Nation",
+            "status": "Official",
+            "medium-count": 1,
+            "medium-list": [
                 {
-                    'position': '1',
-                    'format': 'CD',
-                    'track-list': [
+                    "position": "1",
+                    "format": "CD",
+                    "track-list": [
                         {
-                            'id': '03baca8b-855a-3c05-8f3d-d3235287d84d',
-                            'position': '4',
-                            'number': '4',
-                            'length': '417973',
-                            'recording': {
-                                'id': '2109e376-132b-40ad-b993-2bb6812e19d4',
-                                'title': 'Teen Age Riot',
-                                'length': '417973'},
-                            'track_or_recording_length': '417973'
+                            "id": "03baca8b-855a-3c05-8f3d-d3235287d84d",
+                            "position": "4",
+                            "number": "4",
+                            "length": "417973",
+                            "recording": {
+                                "id": "2109e376-132b-40ad-b993-2bb6812e19d4",
+                                "title": "Teen Age Riot",
+                                "length": "417973",
+                            },
+                            "track_or_recording_length": "417973",
                         }
                     ],
-                    'track-count': 1
+                    "track-count": 1,
                 }
             ],
         }
     }
     mocked_get = mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.get',
-        return_value=album_data)
-    track_data = album_data['release']['medium-list'][0]['track-list'][0]
+        "funkwhale_api.musicbrainz.api.releases.get", return_value=album_data
+    )
+    track_data = album_data["release"]["medium-list"][0]["track-list"][0]
     track = models.Track.get_or_create_from_release(
-        '430347cb-0879-3113-9fde-c75b658c298e',
-        track_data['recording']['id'],
+        "430347cb-0879-3113-9fde-c75b658c298e", track_data["recording"]["id"]
     )[0]
-    mocked_get.assert_called_once_with(
-        album.mbid, includes=models.Album.api_includes)
-    assert track.title == track_data['recording']['title']
-    assert track.mbid == track_data['recording']['id']
+    mocked_get.assert_called_once_with(album.mbid, includes=models.Album.api_includes)
+    assert track.title == track_data["recording"]["title"]
+    assert track.mbid == track_data["recording"]["id"]
     assert track.album == album
     assert track.artist == album.artist
-    assert track.position == int(track_data['position'])
+    assert track.position == int(track_data["position"])
+
 
 def test_import_job_is_bound_to_track_file(factories, mocker):
-    track = factories['music.Track']()
-    job = factories['music.ImportJob'](mbid=track.mbid)
+    track = factories["music.Track"]()
+    job = factories["music.ImportJob"](mbid=track.mbid)
 
-    mocker.patch('funkwhale_api.music.models.TrackFile.download_file')
+    mocker.patch("funkwhale_api.music.models.TrackFile.download_file")
     tasks.import_job_run(import_job_id=job.pk)
     job.refresh_from_db()
     assert job.track_file.track == track
 
 
-@pytest.mark.parametrize('status', ['pending', 'errored', 'finished'])
-def test_saving_job_updates_batch_status(status,factories, mocker):
-    batch = factories['music.ImportBatch']()
+@pytest.mark.parametrize("status", ["pending", "errored", "finished"])
+def test_saving_job_updates_batch_status(status, factories, mocker):
+    batch = factories["music.ImportBatch"]()
 
-    assert batch.status == 'pending'
+    assert batch.status == "pending"
 
-    job = factories['music.ImportJob'](batch=batch, status=status)
+    factories["music.ImportJob"](batch=batch, status=status)
 
     batch.refresh_from_db()
 
     assert batch.status == status
 
 
-@pytest.mark.parametrize('extention,mimetype', [
-    ('ogg', 'audio/ogg'),
-    ('mp3', 'audio/mpeg'),
-])
+@pytest.mark.parametrize(
+    "extention,mimetype", [("ogg", "audio/ogg"), ("mp3", "audio/mpeg")]
+)
 def test_audio_track_mime_type(extention, mimetype, factories):
 
-    name = '.'.join(['test', extention])
+    name = ".".join(["test", extention])
     path = os.path.join(DATA_DIR, name)
-    tf = factories['music.TrackFile'](audio_file__from_path=path)
+    tf = factories["music.TrackFile"](audio_file__from_path=path)
 
     assert tf.mimetype == mimetype
 
 
 def test_track_file_file_name(factories):
-    name = 'test.mp3'
+    name = "test.mp3"
     path = os.path.join(DATA_DIR, name)
-    tf = factories['music.TrackFile'](audio_file__from_path=path)
+    tf = factories["music.TrackFile"](audio_file__from_path=path)
 
-    assert tf.filename == tf.track.full_name + '.mp3'
+    assert tf.filename == tf.track.full_name + ".mp3"
 
 
 def test_track_get_file_size(factories):
-    name = 'test.mp3'
+    name = "test.mp3"
     path = os.path.join(DATA_DIR, name)
-    tf = factories['music.TrackFile'](audio_file__from_path=path)
+    tf = factories["music.TrackFile"](audio_file__from_path=path)
 
     assert tf.get_file_size() == 297745
 
 
 def test_track_get_file_size_federation(factories):
-    tf = factories['music.TrackFile'](
-        federation=True,
-        library_track__with_audio_file=True)
+    tf = factories["music.TrackFile"](
+        federation=True, library_track__with_audio_file=True
+    )
 
     assert tf.get_file_size() == tf.library_track.audio_file.size
 
 
 def test_track_get_file_size_in_place(factories):
-    name = 'test.mp3'
+    name = "test.mp3"
     path = os.path.join(DATA_DIR, name)
-    tf = factories['music.TrackFile'](
-        in_place=True, source='file://{}'.format(path))
+    tf = factories["music.TrackFile"](in_place=True, source="file://{}".format(path))
 
     assert tf.get_file_size() == 297745
 
 
 def test_album_get_image_content(factories):
-    album = factories['music.Album']()
-    album.get_image(data={'content': b'test', 'mimetype':'image/jpeg'})
+    album = factories["music.Album"]()
+    album.get_image(data={"content": b"test", "mimetype": "image/jpeg"})
     album.refresh_from_db()
 
-    assert album.cover.read() == b'test'
+    assert album.cover.read() == b"test"
diff --git a/api/tests/music/test_music.py b/api/tests/music/test_music.py
index 4162912e4fdee2e13d192ce14ed2a1fc4dcd2a23..387cebb2c365947a0e987778b363b50ad809adb2 100644
--- a/api/tests/music/test_music.py
+++ b/api/tests/music/test_music.py
@@ -1,125 +1,142 @@
+import datetime
+
 import pytest
+
 from funkwhale_api.music import models
-import datetime
 
 
 def test_can_create_artist_from_api(artists, mocker, db):
     mocker.patch(
-        'musicbrainzngs.search_artists',
-        return_value=artists['search']['adhesive_wombat'])
+        "musicbrainzngs.search_artists",
+        return_value=artists["search"]["adhesive_wombat"],
+    )
     artist = models.Artist.create_from_api(query="Adhesive wombat")
-    data = models.Artist.api.search(query='Adhesive wombat')['artist-list'][0]
+    data = models.Artist.api.search(query="Adhesive wombat")["artist-list"][0]
 
-    assert int(data['ext:score']), 100
-    assert data['id'], '62c3befb-6366-4585-b256-809472333801'
-    assert artist.mbid, data['id']
-    assert artist.name, 'Adhesive Wombat'
+    assert int(data["ext:score"]), 100
+    assert data["id"], "62c3befb-6366-4585-b256-809472333801"
+    assert artist.mbid, data["id"]
+    assert artist.name, "Adhesive Wombat"
 
 
 def test_can_create_album_from_api(artists, albums, mocker, db):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.search',
-        return_value=albums['search']['hypnotize'])
+        "funkwhale_api.musicbrainz.api.releases.search",
+        return_value=albums["search"]["hypnotize"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.artists.get',
-        return_value=artists['get']['soad'])
-    album = models.Album.create_from_api(query="Hypnotize", artist='system of a down', type='album')
-    data = models.Album.api.search(query='Hypnotize', artist='system of a down', type='album')['release-list'][0]
-
-    assert album.mbid, data['id']
-    assert album.title, 'Hypnotize'
+        "funkwhale_api.musicbrainz.api.artists.get", return_value=artists["get"]["soad"]
+    )
+    album = models.Album.create_from_api(
+        query="Hypnotize", artist="system of a down", type="album"
+    )
+    data = models.Album.api.search(
+        query="Hypnotize", artist="system of a down", type="album"
+    )["release-list"][0]
+
+    assert album.mbid, data["id"]
+    assert album.title, "Hypnotize"
     with pytest.raises(ValueError):
         assert album.cover.path is not None
     assert album.release_date, datetime.date(2005, 1, 1)
-    assert album.artist.name, 'System of a Down'
-    assert album.artist.mbid, data['artist-credit'][0]['artist']['id']
+    assert album.artist.name, "System of a Down"
+    assert album.artist.mbid, data["artist-credit"][0]["artist"]["id"]
 
 
 def test_can_create_track_from_api(artists, albums, tracks, mocker, db):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.artists.get',
-        return_value=artists['get']['adhesive_wombat'])
+        "funkwhale_api.musicbrainz.api.artists.get",
+        return_value=artists["get"]["adhesive_wombat"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.get',
-        return_value=albums['get']['marsupial'])
+        "funkwhale_api.musicbrainz.api.releases.get",
+        return_value=albums["get"]["marsupial"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.recordings.search',
-        return_value=tracks['search']['8bitadventures'])
+        "funkwhale_api.musicbrainz.api.recordings.search",
+        return_value=tracks["search"]["8bitadventures"],
+    )
     track = models.Track.create_from_api(query="8-bit adventure")
-    data = models.Track.api.search(query='8-bit adventure')['recording-list'][0]
-    assert int(data['ext:score']) == 100
-    assert data['id'] == '9968a9d6-8d92-4051-8f76-674e157b6eed'
-    assert track.mbid == data['id']
+    data = models.Track.api.search(query="8-bit adventure")["recording-list"][0]
+    assert int(data["ext:score"]) == 100
+    assert data["id"] == "9968a9d6-8d92-4051-8f76-674e157b6eed"
+    assert track.mbid == data["id"]
     assert track.artist.pk is not None
-    assert str(track.artist.mbid) == '62c3befb-6366-4585-b256-809472333801'
-    assert track.artist.name == 'Adhesive Wombat'
-    assert str(track.album.mbid) == 'a50d2a81-2a50-484d-9cb4-b9f6833f583e'
-    assert track.album.title == 'Marsupial Madness'
+    assert str(track.artist.mbid) == "62c3befb-6366-4585-b256-809472333801"
+    assert track.artist.name == "Adhesive Wombat"
+    assert str(track.album.mbid) == "a50d2a81-2a50-484d-9cb4-b9f6833f583e"
+    assert track.album.title == "Marsupial Madness"
 
 
 def test_can_create_track_from_api_with_corresponding_tags(
-        artists, albums, tracks, mocker, db):
+    artists, albums, tracks, mocker, db
+):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.artists.get',
-        return_value=artists['get']['adhesive_wombat'])
+        "funkwhale_api.musicbrainz.api.artists.get",
+        return_value=artists["get"]["adhesive_wombat"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.get',
-        return_value=albums['get']['marsupial'])
+        "funkwhale_api.musicbrainz.api.releases.get",
+        return_value=albums["get"]["marsupial"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.recordings.get',
-        return_value=tracks['get']['8bitadventures'])
-    track = models.Track.create_from_api(id='9968a9d6-8d92-4051-8f76-674e157b6eed')
-    expected_tags = ['techno', 'good-music']
+        "funkwhale_api.musicbrainz.api.recordings.get",
+        return_value=tracks["get"]["8bitadventures"],
+    )
+    track = models.Track.create_from_api(id="9968a9d6-8d92-4051-8f76-674e157b6eed")
+    expected_tags = ["techno", "good-music"]
     track_tags = [tag.slug for tag in track.tags.all()]
     for tag in expected_tags:
         assert tag in track_tags
 
 
-def test_can_get_or_create_track_from_api(
-        artists, albums, tracks, mocker, db):
+def test_can_get_or_create_track_from_api(artists, albums, tracks, mocker, db):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.artists.get',
-        return_value=artists['get']['adhesive_wombat'])
+        "funkwhale_api.musicbrainz.api.artists.get",
+        return_value=artists["get"]["adhesive_wombat"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.get',
-        return_value=albums['get']['marsupial'])
+        "funkwhale_api.musicbrainz.api.releases.get",
+        return_value=albums["get"]["marsupial"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.recordings.search',
-        return_value=tracks['search']['8bitadventures'])
+        "funkwhale_api.musicbrainz.api.recordings.search",
+        return_value=tracks["search"]["8bitadventures"],
+    )
     track = models.Track.create_from_api(query="8-bit adventure")
-    data = models.Track.api.search(query='8-bit adventure')['recording-list'][0]
-    assert int(data['ext:score']) == 100
-    assert data['id'] == '9968a9d6-8d92-4051-8f76-674e157b6eed'
-    assert track.mbid == data['id']
+    data = models.Track.api.search(query="8-bit adventure")["recording-list"][0]
+    assert int(data["ext:score"]) == 100
+    assert data["id"] == "9968a9d6-8d92-4051-8f76-674e157b6eed"
+    assert track.mbid == data["id"]
     assert track.artist.pk is not None
-    assert str(track.artist.mbid) == '62c3befb-6366-4585-b256-809472333801'
-    assert track.artist.name == 'Adhesive Wombat'
+    assert str(track.artist.mbid) == "62c3befb-6366-4585-b256-809472333801"
+    assert track.artist.name == "Adhesive Wombat"
 
-    track2, created = models.Track.get_or_create_from_api(mbid=data['id'])
+    track2, created = models.Track.get_or_create_from_api(mbid=data["id"])
     assert not created
     assert track == track2
 
 
 def test_album_tags_deduced_from_tracks_tags(factories, django_assert_num_queries):
-    tag = factories['taggit.Tag']()
-    album = factories['music.Album']()
-    tracks = factories['music.Track'].create_batch(
-        5, album=album, tags=[tag])
+    tag = factories["taggit.Tag"]()
+    album = factories["music.Album"]()
+    factories["music.Track"].create_batch(5, album=album, tags=[tag])
 
-    album = models.Album.objects.prefetch_related('tracks__tags').get(pk=album.pk)
+    album = models.Album.objects.prefetch_related("tracks__tags").get(pk=album.pk)
 
     with django_assert_num_queries(0):
         assert tag in album.tags
 
 
 def test_artist_tags_deduced_from_album_tags(factories, django_assert_num_queries):
-    tag = factories['taggit.Tag']()
-    album = factories['music.Album']()
+    tag = factories["taggit.Tag"]()
+    album = factories["music.Album"]()
     artist = album.artist
-    tracks = factories['music.Track'].create_batch(
-        5, album=album, tags=[tag])
+    factories["music.Track"].create_batch(5, album=album, tags=[tag])
 
-    artist = models.Artist.objects.prefetch_related('albums__tracks__tags').get(pk=artist.pk)
+    artist = models.Artist.objects.prefetch_related("albums__tracks__tags").get(
+        pk=artist.pk
+    )
 
     with django_assert_num_queries(0):
         assert tag in artist.tags
@@ -127,10 +144,10 @@ def test_artist_tags_deduced_from_album_tags(factories, django_assert_num_querie
 
 def test_can_download_image_file_for_album(binary_cover, mocker, factories):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.images.get_front',
-        return_value=binary_cover)
+        "funkwhale_api.musicbrainz.api.images.get_front", return_value=binary_cover
+    )
     # client._api.get_image_front('55ea4f82-b42b-423e-a0e5-290ccdf443ed')
-    album = factories['music.Album'](mbid='55ea4f82-b42b-423e-a0e5-290ccdf443ed')
+    album = factories["music.Album"](mbid="55ea4f82-b42b-423e-a0e5-290ccdf443ed")
     album.get_image()
     album.save()
 
diff --git a/api/tests/music/test_permissions.py b/api/tests/music/test_permissions.py
index 825d1731ddae4704e00f47507f85a38144de7fcc..5f73a361e05bef73a51685f351a4d6a4fcb264d9 100644
--- a/api/tests/music/test_permissions.py
+++ b/api/tests/music/test_permissions.py
@@ -5,58 +5,56 @@ from funkwhale_api.music import permissions
 
 
 def test_list_permission_no_protect(preferences, anonymous_user, api_request):
-    preferences['common__api_authentication_required'] = False
+    preferences["common__api_authentication_required"] = False
     view = APIView.as_view()
     permission = permissions.Listen()
-    request = api_request.get('/')
+    request = api_request.get("/")
     assert permission.has_permission(request, view) is True
 
 
-def test_list_permission_protect_authenticated(
-        factories, api_request, preferences):
-    preferences['common__api_authentication_required'] = True
-    user = factories['users.User']()
+def test_list_permission_protect_authenticated(factories, api_request, preferences):
+    preferences["common__api_authentication_required"] = True
+    user = factories["users.User"]()
     view = APIView.as_view()
     permission = permissions.Listen()
-    request = api_request.get('/')
-    setattr(request, 'user', user)
+    request = api_request.get("/")
+    setattr(request, "user", user)
     assert permission.has_permission(request, view) is True
 
 
 def test_list_permission_protect_not_following_actor(
-        factories, api_request, preferences):
-    preferences['common__api_authentication_required'] = True
-    actor = factories['federation.Actor']()
+    factories, api_request, preferences
+):
+    preferences["common__api_authentication_required"] = True
+    actor = factories["federation.Actor"]()
     view = APIView.as_view()
     permission = permissions.Listen()
-    request = api_request.get('/')
-    setattr(request, 'actor', actor)
+    request = api_request.get("/")
+    setattr(request, "actor", actor)
     assert permission.has_permission(request, view) is False
 
 
-def test_list_permission_protect_following_actor(
-        factories, api_request, preferences):
-    preferences['common__api_authentication_required'] = True
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    follow = factories['federation.Follow'](
-        approved=True, target=library_actor)
+def test_list_permission_protect_following_actor(factories, api_request, preferences):
+    preferences["common__api_authentication_required"] = True
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    follow = factories["federation.Follow"](approved=True, target=library_actor)
     view = APIView.as_view()
     permission = permissions.Listen()
-    request = api_request.get('/')
-    setattr(request, 'actor', follow.actor)
+    request = api_request.get("/")
+    setattr(request, "actor", follow.actor)
 
     assert permission.has_permission(request, view) is True
 
 
 def test_list_permission_protect_following_actor_not_approved(
-        factories, api_request, preferences):
-    preferences['common__api_authentication_required'] = True
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    follow = factories['federation.Follow'](
-        approved=False, target=library_actor)
+    factories, api_request, preferences
+):
+    preferences["common__api_authentication_required"] = True
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    follow = factories["federation.Follow"](approved=False, target=library_actor)
     view = APIView.as_view()
     permission = permissions.Listen()
-    request = api_request.get('/')
-    setattr(request, 'actor', follow.actor)
+    request = api_request.get("/")
+    setattr(request, "actor", follow.actor)
 
     assert permission.has_permission(request, view) is False
diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py
index fa22ceceeb79199b40ca71297fb4bc0f849dfc7e..51ca96b5d713af1c1e39f2c1872a9bbea9809216 100644
--- a/api/tests/music/test_serializers.py
+++ b/api/tests/music/test_serializers.py
@@ -2,18 +2,18 @@ from funkwhale_api.music import serializers
 
 
 def test_artist_album_serializer(factories, to_api_date):
-    track = factories['music.Track']()
+    track = factories["music.Track"]()
     album = track.album
     album = album.__class__.objects.with_tracks_count().get(pk=album.pk)
     expected = {
-        'id': album.id,
-        'mbid': str(album.mbid),
-        'title': album.title,
-        'artist': album.artist.id,
-        'creation_date': to_api_date(album.creation_date),
-        'tracks_count': 1,
-        'cover': album.cover.url,
-        'release_date': to_api_date(album.release_date),
+        "id": album.id,
+        "mbid": str(album.mbid),
+        "title": album.title,
+        "artist": album.artist.id,
+        "creation_date": to_api_date(album.creation_date),
+        "tracks_count": 1,
+        "cover": album.cover.url,
+        "release_date": to_api_date(album.release_date),
     }
     serializer = serializers.ArtistAlbumSerializer(album)
 
@@ -21,79 +21,71 @@ def test_artist_album_serializer(factories, to_api_date):
 
 
 def test_artist_with_albums_serializer(factories, to_api_date):
-    track = factories['music.Track']()
+    track = factories["music.Track"]()
     artist = track.artist
     artist = artist.__class__.objects.with_albums().get(pk=artist.pk)
     album = list(artist.albums.all())[0]
 
     expected = {
-        'id': artist.id,
-        'mbid': str(artist.mbid),
-        'name': artist.name,
-        'creation_date': to_api_date(artist.creation_date),
-        'albums': [
-            serializers.ArtistAlbumSerializer(album).data
-        ]
+        "id": artist.id,
+        "mbid": str(artist.mbid),
+        "name": artist.name,
+        "creation_date": to_api_date(artist.creation_date),
+        "albums": [serializers.ArtistAlbumSerializer(album).data],
     }
     serializer = serializers.ArtistWithAlbumsSerializer(artist)
     assert serializer.data == expected
 
 
 def test_album_track_serializer(factories, to_api_date):
-    tf = factories['music.TrackFile']()
+    tf = factories["music.TrackFile"]()
     track = tf.track
 
     expected = {
-        'id': track.id,
-        'artist': track.artist.id,
-        'album': track.album.id,
-        'mbid': str(track.mbid),
-        'title': track.title,
-        'position': track.position,
-        'creation_date': to_api_date(track.creation_date),
-        'files': [
-            serializers.TrackFileSerializer(tf).data
-        ]
+        "id": track.id,
+        "artist": track.artist.id,
+        "album": track.album.id,
+        "mbid": str(track.mbid),
+        "title": track.title,
+        "position": track.position,
+        "creation_date": to_api_date(track.creation_date),
+        "files": [serializers.TrackFileSerializer(tf).data],
     }
     serializer = serializers.AlbumTrackSerializer(track)
     assert serializer.data == expected
 
 
 def test_track_file_serializer(factories, to_api_date):
-    tf = factories['music.TrackFile']()
+    tf = factories["music.TrackFile"]()
 
     expected = {
-        'id': tf.id,
-        'path': tf.path,
-        'source': tf.source,
-        'filename': tf.filename,
-        'mimetype': tf.mimetype,
-        'track': tf.track.pk,
-        'duration': tf.duration,
-        'mimetype': tf.mimetype,
-        'bitrate': tf.bitrate,
-        'size': tf.size,
+        "id": tf.id,
+        "path": tf.path,
+        "source": tf.source,
+        "filename": tf.filename,
+        "track": tf.track.pk,
+        "duration": tf.duration,
+        "mimetype": tf.mimetype,
+        "bitrate": tf.bitrate,
+        "size": tf.size,
     }
     serializer = serializers.TrackFileSerializer(tf)
     assert serializer.data == expected
 
 
 def test_album_serializer(factories, to_api_date):
-    track1 = factories['music.Track'](position=2)
-    track2 = factories['music.Track'](position=1, album=track1.album)
+    track1 = factories["music.Track"](position=2)
+    track2 = factories["music.Track"](position=1, album=track1.album)
     album = track1.album
     expected = {
-        'id': album.id,
-        'mbid': str(album.mbid),
-        'title': album.title,
-        'artist': serializers.ArtistSimpleSerializer(album.artist).data,
-        'creation_date': to_api_date(album.creation_date),
-        'cover': album.cover.url,
-        'release_date': to_api_date(album.release_date),
-        'tracks': serializers.AlbumTrackSerializer(
-            [track2, track1],
-            many=True
-        ).data
+        "id": album.id,
+        "mbid": str(album.mbid),
+        "title": album.title,
+        "artist": serializers.ArtistSimpleSerializer(album.artist).data,
+        "creation_date": to_api_date(album.creation_date),
+        "cover": album.cover.url,
+        "release_date": to_api_date(album.release_date),
+        "tracks": serializers.AlbumTrackSerializer([track2, track1], many=True).data,
     }
     serializer = serializers.AlbumSerializer(album)
 
@@ -101,21 +93,19 @@ def test_album_serializer(factories, to_api_date):
 
 
 def test_track_serializer(factories, to_api_date):
-    tf = factories['music.TrackFile']()
+    tf = factories["music.TrackFile"]()
     track = tf.track
 
     expected = {
-        'id': track.id,
-        'artist': serializers.ArtistSimpleSerializer(track.artist).data,
-        'album': serializers.TrackAlbumSerializer(track.album).data,
-        'mbid': str(track.mbid),
-        'title': track.title,
-        'position': track.position,
-        'creation_date': to_api_date(track.creation_date),
-        'lyrics': track.get_lyrics_url(),
-        'files': [
-            serializers.TrackFileSerializer(tf).data
-        ]
+        "id": track.id,
+        "artist": serializers.ArtistSimpleSerializer(track.artist).data,
+        "album": serializers.TrackAlbumSerializer(track.album).data,
+        "mbid": str(track.mbid),
+        "title": track.title,
+        "position": track.position,
+        "creation_date": to_api_date(track.creation_date),
+        "lyrics": track.get_lyrics_url(),
+        "files": [serializers.TrackFileSerializer(tf).data],
     }
     serializer = serializers.TrackSerializer(track)
     assert serializer.data == expected
diff --git a/api/tests/music/test_tasks.py b/api/tests/music/test_tasks.py
index 77245e204e364627cd9667b6fa3b5e1a09faf4bf..71d605b2b3768ab7b5dafa73738c0d4fb5af8c2d 100644
--- a/api/tests/music/test_tasks.py
+++ b/api/tests/music/test_tasks.py
@@ -1,181 +1,183 @@
 import os
+
 import pytest
 
-from funkwhale_api.providers.acoustid import get_acoustid_client
 from funkwhale_api.music import tasks
 
 DATA_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
 def test_set_acoustid_on_track_file(factories, mocker, preferences):
-    preferences['providers_acoustid__api_key'] = 'test'
-    track_file = factories['music.TrackFile'](acoustid_track_id=None)
-    id = 'e475bf79-c1ce-4441-bed7-1e33f226c0a2'
+    preferences["providers_acoustid__api_key"] = "test"
+    track_file = factories["music.TrackFile"](acoustid_track_id=None)
+    id = "e475bf79-c1ce-4441-bed7-1e33f226c0a2"
     payload = {
-        'results': [
-            {'id': id,
-             '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'
+        "results": [
+            {
+                "id": id,
+                "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)
+    m = mocker.patch("acoustid.match", return_value=payload)
     r = tasks.set_acoustid_on_track_file(track_file_id=track_file.pk)
     track_file.refresh_from_db()
 
     assert str(track_file.acoustid_track_id) == id
     assert r == id
-    m.assert_called_once_with('test', track_file.audio_file.path, parse=False)
+    m.assert_called_once_with("test", track_file.audio_file.path, parse=False)
 
 
 def test_set_acoustid_on_track_file_required_high_score(factories, mocker):
-    track_file = factories['music.TrackFile'](acoustid_track_id=None)
-    id = 'e475bf79-c1ce-4441-bed7-1e33f226c0a2'
-    payload = {
-        'results': [{'score': 0.79}],
-        'status': 'ok'
-    }
-    m = mocker.patch('acoustid.match', return_value=payload)
-    r = tasks.set_acoustid_on_track_file(track_file_id=track_file.pk)
+    track_file = factories["music.TrackFile"](acoustid_track_id=None)
+    payload = {"results": [{"score": 0.79}], "status": "ok"}
+    mocker.patch("acoustid.match", return_value=payload)
+    tasks.set_acoustid_on_track_file(track_file_id=track_file.pk)
     track_file.refresh_from_db()
 
     assert track_file.acoustid_track_id is None
 
 
 def test_import_batch_run(factories, mocker):
-    job = factories['music.ImportJob']()
-    mocked_job_run = mocker.patch(
-        'funkwhale_api.music.tasks.import_job_run.delay')
+    job = factories["music.ImportJob"]()
+    mocked_job_run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay")
     tasks.import_batch_run(import_batch_id=job.batch.pk)
 
     mocked_job_run.assert_called_once_with(import_job_id=job.pk)
 
 
-@pytest.mark.skip('Acoustid is disabled')
+@pytest.mark.skip("Acoustid is disabled")
 def test_import_job_can_run_with_file_and_acoustid(
-        artists, albums, tracks, preferences, factories, mocker):
-    preferences['providers_acoustid__api_key'] = 'test'
-    path = os.path.join(DATA_DIR, 'test.ogg')
-    mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
+    artists, albums, tracks, preferences, factories, mocker
+):
+    preferences["providers_acoustid__api_key"] = "test"
+    path = os.path.join(DATA_DIR, "test.ogg")
+    mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed"
     acoustid_payload = {
-        'results': [
-            {'id': 'e475bf79-c1ce-4441-bed7-1e33f226c0a2',
-             'recordings': [
-                {
-                 'duration': 268,
-                 'id': mbid}],
-            'score': 0.860825}],
-        'status': 'ok'
+        "results": [
+            {
+                "id": "e475bf79-c1ce-4441-bed7-1e33f226c0a2",
+                "recordings": [{"duration": 268, "id": mbid}],
+                "score": 0.860825,
+            }
+        ],
+        "status": "ok",
     }
     mocker.patch(
-        'funkwhale_api.music.utils.get_audio_file_data',
-        return_value={'bitrate': 42, 'length': 43})
+        "funkwhale_api.music.utils.get_audio_file_data",
+        return_value={"bitrate": 42, "length": 43},
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.artists.get',
-        return_value=artists['get']['adhesive_wombat'])
+        "funkwhale_api.musicbrainz.api.artists.get",
+        return_value=artists["get"]["adhesive_wombat"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.get',
-        return_value=albums['get']['marsupial'])
+        "funkwhale_api.musicbrainz.api.releases.get",
+        return_value=albums["get"]["marsupial"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.recordings.search',
-        return_value=tracks['search']['8bitadventures'])
-    mocker.patch('acoustid.match', return_value=acoustid_payload)
+        "funkwhale_api.musicbrainz.api.recordings.search",
+        return_value=tracks["search"]["8bitadventures"],
+    )
+    mocker.patch("acoustid.match", return_value=acoustid_payload)
 
-    job = factories['music.FileImportJob'](audio_file__path=path)
+    job = factories["music.FileImportJob"](audio_file__path=path)
     f = job.audio_file
     tasks.import_job_run(import_job_id=job.pk)
     job.refresh_from_db()
 
     track_file = job.track_file
 
-    with open(path, 'rb') as f:
+    with open(path, "rb") as f:
         assert track_file.audio_file.read() == f.read()
     assert track_file.bitrate == 42
     assert track_file.duration == 43
     assert track_file.size == os.path.getsize(path)
     # audio file is deleted from import job once persisted to audio file
     assert not job.audio_file
-    assert job.status == 'finished'
-    assert job.source == 'file://'
+    assert job.status == "finished"
+    assert job.source == "file://"
 
 
 def test_run_import_skipping_accoustid(factories, mocker):
-    m = mocker.patch('funkwhale_api.music.tasks._do_import')
-    path = os.path.join(DATA_DIR, 'test.ogg')
-    job = factories['music.FileImportJob'](audio_file__path=path)
+    m = mocker.patch("funkwhale_api.music.tasks._do_import")
+    path = os.path.join(DATA_DIR, "test.ogg")
+    job = factories["music.FileImportJob"](audio_file__path=path)
     tasks.import_job_run(import_job_id=job.pk, use_acoustid=False)
     m.assert_called_once_with(job, False, use_acoustid=False)
 
 
 def test__do_import_skipping_accoustid(factories, mocker):
-    t = factories['music.Track']()
+    t = factories["music.Track"]()
     m = mocker.patch(
-        'funkwhale_api.providers.audiofile.tasks.import_track_data_from_path',
-        return_value=t)
-    path = os.path.join(DATA_DIR, 'test.ogg')
-    job = factories['music.FileImportJob'](
-        mbid=None,
-        audio_file__path=path)
+        "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path",
+        return_value=t,
+    )
+    path = os.path.join(DATA_DIR, "test.ogg")
+    job = factories["music.FileImportJob"](mbid=None, audio_file__path=path)
     p = job.audio_file.path
     tasks._do_import(job, replace=False, use_acoustid=False)
     m.assert_called_once_with(p)
 
 
-def test__do_import_skipping_accoustid_if_no_key(
-        factories, mocker, preferences):
-    preferences['providers_acoustid__api_key'] = ''
-    t = factories['music.Track']()
+def test__do_import_skipping_accoustid_if_no_key(factories, mocker, preferences):
+    preferences["providers_acoustid__api_key"] = ""
+    t = factories["music.Track"]()
     m = mocker.patch(
-        'funkwhale_api.providers.audiofile.tasks.import_track_data_from_path',
-        return_value=t)
-    path = os.path.join(DATA_DIR, 'test.ogg')
-    job = factories['music.FileImportJob'](
-        mbid=None,
-        audio_file__path=path)
+        "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path",
+        return_value=t,
+    )
+    path = os.path.join(DATA_DIR, "test.ogg")
+    job = factories["music.FileImportJob"](mbid=None, audio_file__path=path)
     p = job.audio_file.path
     tasks._do_import(job, replace=False, use_acoustid=False)
     m.assert_called_once_with(p)
 
 
-def test_import_job_skip_if_already_exists(
-        artists, albums, tracks, factories, mocker):
-    path = os.path.join(DATA_DIR, 'test.ogg')
-    mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
-    track_file = factories['music.TrackFile'](track__mbid=mbid)
+def test_import_job_skip_if_already_exists(artists, albums, tracks, factories, mocker):
+    path = os.path.join(DATA_DIR, "test.ogg")
+    mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed"
+    track_file = factories["music.TrackFile"](track__mbid=mbid)
     mocker.patch(
-        'funkwhale_api.providers.audiofile.tasks.import_track_data_from_path',
-        return_value=track_file.track)
+        "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path",
+        return_value=track_file.track,
+    )
 
-    job = factories['music.FileImportJob'](audio_file__path=path)
-    f = job.audio_file
+    job = factories["music.FileImportJob"](audio_file__path=path)
     tasks.import_job_run(import_job_id=job.pk)
     job.refresh_from_db()
 
     assert job.track_file is None
     # audio file is deleted from import job once persisted to audio file
     assert not job.audio_file
-    assert job.status == 'skipped'
+    assert job.status == "skipped"
 
 
 def test_import_job_can_be_errored(factories, mocker, preferences):
-    path = os.path.join(DATA_DIR, 'test.ogg')
-    mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed'
-    track_file = factories['music.TrackFile'](track__mbid=mbid)
+    path = os.path.join(DATA_DIR, "test.ogg")
+    mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed"
+    factories["music.TrackFile"](track__mbid=mbid)
 
     class MyException(Exception):
         pass
 
-    mocker.patch(
-        'funkwhale_api.music.tasks._do_import',
-        side_effect=MyException())
+    mocker.patch("funkwhale_api.music.tasks._do_import", side_effect=MyException())
 
-    job = factories['music.FileImportJob'](
-        audio_file__path=path, track_file=None)
+    job = factories["music.FileImportJob"](audio_file__path=path, track_file=None)
 
     with pytest.raises(MyException):
         tasks.import_job_run(import_job_id=job.pk)
@@ -183,23 +185,22 @@ def test_import_job_can_be_errored(factories, mocker, preferences):
     job.refresh_from_db()
 
     assert job.track_file is None
-    assert job.status == 'errored'
+    assert job.status == "errored"
 
 
 def test__do_import_calls_update_album_cover_if_no_cover(factories, mocker):
-    path = os.path.join(DATA_DIR, 'test.ogg')
-    album = factories['music.Album'](cover='')
-    track = factories['music.Track'](album=album)
+    path = os.path.join(DATA_DIR, "test.ogg")
+    album = factories["music.Album"](cover="")
+    track = factories["music.Track"](album=album)
 
     mocker.patch(
-        'funkwhale_api.providers.audiofile.tasks.import_track_data_from_path',
-        return_value=track)
+        "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path",
+        return_value=track,
+    )
 
-    mocked_update = mocker.patch(
-        'funkwhale_api.music.tasks.update_album_cover')
+    mocked_update = mocker.patch("funkwhale_api.music.tasks.update_album_cover")
 
-    job = factories['music.FileImportJob'](
-        audio_file__path=path, track_file=None)
+    job = factories["music.FileImportJob"](audio_file__path=path, track_file=None)
 
     tasks.import_job_run(import_job_id=job.pk)
 
@@ -207,50 +208,41 @@ def test__do_import_calls_update_album_cover_if_no_cover(factories, mocker):
 
 
 def test_update_album_cover_mbid(factories, mocker):
-    album = factories['music.Album'](cover='')
+    album = factories["music.Album"](cover="")
 
-    mocked_get = mocker.patch('funkwhale_api.music.models.Album.get_image')
+    mocked_get = mocker.patch("funkwhale_api.music.models.Album.get_image")
     tasks.update_album_cover(album=album, track_file=None)
 
     mocked_get.assert_called_once_with()
 
 
 def test_update_album_cover_file_data(factories, mocker):
-    path = os.path.join(DATA_DIR, 'test.mp3')
-    album = factories['music.Album'](cover='', mbid=None)
-    tf = factories['music.TrackFile'](track__album=album)
+    album = factories["music.Album"](cover="", mbid=None)
+    tf = factories["music.TrackFile"](track__album=album)
 
-    mocked_get = mocker.patch('funkwhale_api.music.models.Album.get_image')
+    mocked_get = mocker.patch("funkwhale_api.music.models.Album.get_image")
     mocker.patch(
-        'funkwhale_api.music.metadata.Metadata.get_picture',
-        return_value={'hello': 'world'})
+        "funkwhale_api.music.metadata.Metadata.get_picture",
+        return_value={"hello": "world"},
+    )
     tasks.update_album_cover(album=album, track_file=tf)
-    md = data = tf.get_metadata()
-    mocked_get.assert_called_once_with(
-        data={'hello': 'world'})
-
-
-@pytest.mark.parametrize('ext,mimetype', [
-    ('jpg', 'image/jpeg'),
-    ('png', 'image/png'),
-])
-def test_update_album_cover_file_cover_separate_file(
-        ext, mimetype, factories, mocker):
-    mocker.patch('funkwhale_api.music.tasks.IMAGE_TYPES', [(ext, mimetype)])
-    path = os.path.join(DATA_DIR, 'test.mp3')
-    image_path = os.path.join(DATA_DIR, 'cover.{}'.format(ext))
-    with open(image_path, 'rb') as f:
+    tf.get_metadata()
+    mocked_get.assert_called_once_with(data={"hello": "world"})
+
+
+@pytest.mark.parametrize("ext,mimetype", [("jpg", "image/jpeg"), ("png", "image/png")])
+def test_update_album_cover_file_cover_separate_file(ext, mimetype, factories, mocker):
+    mocker.patch("funkwhale_api.music.tasks.IMAGE_TYPES", [(ext, mimetype)])
+    image_path = os.path.join(DATA_DIR, "cover.{}".format(ext))
+    with open(image_path, "rb") as f:
         image_content = f.read()
-    album = factories['music.Album'](cover='', mbid=None)
-    tf = factories['music.TrackFile'](
-        track__album=album,
-        source='file://' + image_path)
+    album = factories["music.Album"](cover="", mbid=None)
+    tf = factories["music.TrackFile"](track__album=album, source="file://" + image_path)
 
-    mocked_get = mocker.patch('funkwhale_api.music.models.Album.get_image')
-    mocker.patch(
-        'funkwhale_api.music.metadata.Metadata.get_picture',
-        return_value=None)
+    mocked_get = mocker.patch("funkwhale_api.music.models.Album.get_image")
+    mocker.patch("funkwhale_api.music.metadata.Metadata.get_picture", return_value=None)
     tasks.update_album_cover(album=album, track_file=tf)
-    md = data = tf.get_metadata()
+    tf.get_metadata()
     mocked_get.assert_called_once_with(
-        data={'mimetype': mimetype, 'content': image_content})
+        data={"mimetype": mimetype, "content": image_content}
+    )
diff --git a/api/tests/music/test_utils.py b/api/tests/music/test_utils.py
index 7b967dbbcceac9d6aade23d2dfb97d8f2a7d696b..4019e47b4537d44b07677725bc4b0d3cb11ae7aa 100644
--- a/api/tests/music/test_utils.py
+++ b/api/tests/music/test_utils.py
@@ -1,4 +1,5 @@
 import os
+
 import pytest
 
 from funkwhale_api.music import utils
@@ -7,35 +8,31 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
 def test_guess_mimetype_try_using_extension(factories, mocker):
-    mocker.patch(
-        'magic.from_buffer', return_value='audio/mpeg')
-    f = factories['music.TrackFile'].build(
-        audio_file__filename='test.ogg')
+    mocker.patch("magic.from_buffer", return_value="audio/mpeg")
+    f = factories["music.TrackFile"].build(audio_file__filename="test.ogg")
 
-    assert utils.guess_mimetype(f.audio_file) == 'audio/mpeg'
+    assert utils.guess_mimetype(f.audio_file) == "audio/mpeg"
 
 
-@pytest.mark.parametrize('wrong', [
-    'application/octet-stream',
-    'application/x-empty',
-])
+@pytest.mark.parametrize("wrong", ["application/octet-stream", "application/x-empty"])
 def test_guess_mimetype_try_using_extension_if_fail(wrong, factories, mocker):
-    mocker.patch(
-        'magic.from_buffer', return_value=wrong)
-    f = factories['music.TrackFile'].build(
-        audio_file__filename='test.mp3')
+    mocker.patch("magic.from_buffer", return_value=wrong)
+    f = factories["music.TrackFile"].build(audio_file__filename="test.mp3")
 
-    assert utils.guess_mimetype(f.audio_file) == 'audio/mpeg'
+    assert utils.guess_mimetype(f.audio_file) == "audio/mpeg"
 
 
-@pytest.mark.parametrize('name, expected', [
-    ('sample.flac', {'bitrate': 1608000, 'length': 0.001}),
-    ('test.mp3', {'bitrate': 8000, 'length': 267.70285714285717}),
-    ('test.ogg', {'bitrate': 128000, 'length': 229.18304166666667}),
-])
+@pytest.mark.parametrize(
+    "name, expected",
+    [
+        ("sample.flac", {"bitrate": 1608000, "length": 0.001}),
+        ("test.mp3", {"bitrate": 8000, "length": 267.70285714285717}),
+        ("test.ogg", {"bitrate": 128000, "length": 229.18304166666667}),
+    ],
+)
 def test_get_audio_file_data(name, expected):
     path = os.path.join(DATA_DIR, name)
-    with open(path, 'rb') as f:
+    with open(path, "rb") as f:
         result = utils.get_audio_file_data(f)
 
     assert result == expected
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index 91fef13f2f3d411bc65397a86d882ab293dd352b..aa04521cb571583ba8ce26511c12fe6bb5f2b518 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -1,36 +1,34 @@
 import io
-import pytest
 
+import pytest
 from django.urls import reverse
 from django.utils import timezone
 
-from funkwhale_api.music import serializers
-from funkwhale_api.music import views
 from funkwhale_api.federation import actors
+from funkwhale_api.music import serializers, views
 
 
-@pytest.mark.parametrize('view,permissions,operator', [
-    (views.ImportBatchViewSet, ['library', 'upload'], 'or'),
-    (views.ImportJobViewSet, ['library', 'upload'], 'or'),
-])
+@pytest.mark.parametrize(
+    "view,permissions,operator",
+    [
+        (views.ImportBatchViewSet, ["library", "upload"], "or"),
+        (views.ImportJobViewSet, ["library", "upload"], "or"),
+    ],
+)
 def test_permissions(assert_user_permission, view, permissions, operator):
     assert_user_permission(view, permissions, operator)
 
 
 def test_artist_list_serializer(api_request, factories, logged_in_api_client):
-    track = factories['music.Track']()
+    track = factories["music.Track"]()
     artist = track.artist
-    request = api_request.get('/')
+    request = api_request.get("/")
     qs = artist.__class__.objects.with_albums()
     serializer = serializers.ArtistWithAlbumsSerializer(
-        qs, many=True, context={'request': request})
-    expected = {
-        'count': 1,
-        'next': None,
-        'previous': None,
-        'results': serializer.data
-    }
-    url = reverse('api:v1:artists-list')
+        qs, many=True, context={"request": request}
+    )
+    expected = {"count": 1, "next": None, "previous": None, "results": serializer.data}
+    url = reverse("api:v1:artists-list")
     response = logged_in_api_client.get(url)
 
     assert response.status_code == 200
@@ -38,19 +36,15 @@ def test_artist_list_serializer(api_request, factories, logged_in_api_client):
 
 
 def test_album_list_serializer(api_request, factories, logged_in_api_client):
-    track = factories['music.Track']()
+    track = factories["music.Track"]()
     album = track.album
-    request = api_request.get('/')
+    request = api_request.get("/")
     qs = album.__class__.objects.all()
     serializer = serializers.AlbumSerializer(
-        qs, many=True, context={'request': request})
-    expected = {
-        'count': 1,
-        'next': None,
-        'previous': None,
-        'results': serializer.data
-    }
-    url = reverse('api:v1:albums-list')
+        qs, many=True, context={"request": request}
+    )
+    expected = {"count": 1, "next": None, "previous": None, "results": serializer.data}
+    url = reverse("api:v1:albums-list")
     response = logged_in_api_client.get(url)
 
     assert response.status_code == 200
@@ -58,38 +52,30 @@ def test_album_list_serializer(api_request, factories, logged_in_api_client):
 
 
 def test_track_list_serializer(api_request, factories, logged_in_api_client):
-    track = factories['music.Track']()
-    request = api_request.get('/')
+    track = factories["music.Track"]()
+    request = api_request.get("/")
     qs = track.__class__.objects.all()
     serializer = serializers.TrackSerializer(
-        qs, many=True, context={'request': request})
-    expected = {
-        'count': 1,
-        'next': None,
-        'previous': None,
-        'results': serializer.data
-    }
-    url = reverse('api:v1:tracks-list')
+        qs, many=True, context={"request": request}
+    )
+    expected = {"count": 1, "next": None, "previous": None, "results": serializer.data}
+    url = reverse("api:v1:tracks-list")
     response = logged_in_api_client.get(url)
 
     assert response.status_code == 200
     assert response.data == expected
 
 
-@pytest.mark.parametrize('param,expected', [
-    ('true', 'full'),
-    ('false', 'empty'),
-])
-def test_artist_view_filter_listenable(
-        param, expected, factories, api_request):
+@pytest.mark.parametrize("param,expected", [("true", "full"), ("false", "empty")])
+def test_artist_view_filter_listenable(param, expected, factories, api_request):
     artists = {
-        'empty': factories['music.Artist'](),
-        'full': factories['music.TrackFile']().track.artist,
+        "empty": factories["music.Artist"](),
+        "full": factories["music.TrackFile"]().track.artist,
     }
 
-    request = api_request.get('/', {'listenable': param})
+    request = api_request.get("/", {"listenable": param})
     view = views.ArtistViewSet()
-    view.action_map = {'get': 'list'}
+    view.action_map = {"get": "list"}
     expected = [artists[expected]]
     view.request = view.initialize_request(request)
     queryset = view.filter_queryset(view.get_queryset())
@@ -97,20 +83,16 @@ def test_artist_view_filter_listenable(
     assert list(queryset) == expected
 
 
-@pytest.mark.parametrize('param,expected', [
-    ('true', 'full'),
-    ('false', 'empty'),
-])
-def test_album_view_filter_listenable(
-        param, expected, factories, api_request):
+@pytest.mark.parametrize("param,expected", [("true", "full"), ("false", "empty")])
+def test_album_view_filter_listenable(param, expected, factories, api_request):
     artists = {
-        'empty': factories['music.Album'](),
-        'full': factories['music.TrackFile']().track.album,
+        "empty": factories["music.Album"](),
+        "full": factories["music.TrackFile"]().track.album,
     }
 
-    request = api_request.get('/', {'listenable': param})
+    request = api_request.get("/", {"listenable": param})
     view = views.AlbumViewSet()
-    view.action_map = {'get': 'list'}
+    view.action_map = {"get": "list"}
     expected = [artists[expected]]
     view.request = view.initialize_request(request)
     queryset = view.filter_queryset(view.get_queryset())
@@ -119,58 +101,53 @@ def test_album_view_filter_listenable(
 
 
 def test_can_serve_track_file_as_remote_library(
-        factories, authenticated_actor, api_client, settings, preferences):
-    preferences['common__api_authentication_required'] = True
-    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
-    follow = factories['federation.Follow'](
-        approved=True,
-        actor=authenticated_actor,
-        target=library_actor)
-
-    track_file = factories['music.TrackFile']()
+    factories, authenticated_actor, api_client, settings, preferences
+):
+    preferences["common__api_authentication_required"] = True
+    library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
+    factories["federation.Follow"](
+        approved=True, actor=authenticated_actor, target=library_actor
+    )
+
+    track_file = factories["music.TrackFile"]()
     response = api_client.get(track_file.path)
 
     assert response.status_code == 200
-    assert response['X-Accel-Redirect'] == "{}{}".format(
-        settings.PROTECT_FILES_PATH,
-        track_file.audio_file.url)
+    assert response["X-Accel-Redirect"] == "{}{}".format(
+        settings.PROTECT_FILES_PATH, track_file.audio_file.url
+    )
 
 
 def test_can_serve_track_file_as_remote_library_deny_not_following(
-        factories, authenticated_actor, settings, api_client, preferences):
-    preferences['common__api_authentication_required'] = True
-    track_file = factories['music.TrackFile']()
+    factories, authenticated_actor, settings, api_client, preferences
+):
+    preferences["common__api_authentication_required"] = True
+    track_file = factories["music.TrackFile"]()
     response = api_client.get(track_file.path)
 
     assert response.status_code == 403
 
 
-@pytest.mark.parametrize('proxy,serve_path,expected', [
-    ('apache2', '/host/music', '/host/music/hello/world.mp3'),
-    ('apache2', '/app/music', '/app/music/hello/world.mp3'),
-    ('nginx', '/host/music', '/_protected/music/hello/world.mp3'),
-    ('nginx', '/app/music', '/_protected/music/hello/world.mp3'),
-])
+@pytest.mark.parametrize(
+    "proxy,serve_path,expected",
+    [
+        ("apache2", "/host/music", "/host/music/hello/world.mp3"),
+        ("apache2", "/app/music", "/app/music/hello/world.mp3"),
+        ("nginx", "/host/music", "/_protected/music/hello/world.mp3"),
+        ("nginx", "/app/music", "/_protected/music/hello/world.mp3"),
+    ],
+)
 def test_serve_file_in_place(
-        proxy,
-        serve_path,
-        expected,
-        factories,
-        api_client,
-        preferences,
-        settings):
-    headers = {
-        'apache2': 'X-Sendfile',
-        'nginx': 'X-Accel-Redirect',
-    }
-    preferences['common__api_authentication_required'] = False
-    settings.PROTECT_FILE_PATH = '/_protected/music'
+    proxy, serve_path, expected, factories, api_client, preferences, settings
+):
+    headers = {"apache2": "X-Sendfile", "nginx": "X-Accel-Redirect"}
+    preferences["common__api_authentication_required"] = False
+    settings.PROTECT_FILE_PATH = "/_protected/music"
     settings.REVERSE_PROXY_TYPE = proxy
-    settings.MUSIC_DIRECTORY_PATH = '/app/music'
+    settings.MUSIC_DIRECTORY_PATH = "/app/music"
     settings.MUSIC_DIRECTORY_SERVE_PATH = serve_path
-    tf = factories['music.TrackFile'](
-        in_place=True,
-        source='file:///app/music/hello/world.mp3'
+    tf = factories["music.TrackFile"](
+        in_place=True, source="file:///app/music/hello/world.mp3"
     )
     response = api_client.get(tf.path)
 
@@ -178,86 +155,76 @@ def test_serve_file_in_place(
     assert response[headers[proxy]] == expected
 
 
-@pytest.mark.parametrize('proxy,serve_path,expected', [
-    ('apache2', '/host/music', '/host/music/hello/worldéà.mp3'),
-    ('apache2', '/app/music', '/app/music/hello/worldéà.mp3'),
-    ('nginx', '/host/music', '/_protected/music/hello/worldéà.mp3'),
-    ('nginx', '/app/music', '/_protected/music/hello/worldéà.mp3'),
-])
+@pytest.mark.parametrize(
+    "proxy,serve_path,expected",
+    [
+        ("apache2", "/host/music", "/host/music/hello/worldéà.mp3"),
+        ("apache2", "/app/music", "/app/music/hello/worldéà.mp3"),
+        ("nginx", "/host/music", "/_protected/music/hello/worldéà.mp3"),
+        ("nginx", "/app/music", "/_protected/music/hello/worldéà.mp3"),
+    ],
+)
 def test_serve_file_in_place_utf8(
-        proxy,
-        serve_path,
-        expected,
-        factories,
-        api_client,
-        settings,
-        preferences):
-    preferences['common__api_authentication_required'] = False
-    settings.PROTECT_FILE_PATH = '/_protected/music'
+    proxy, serve_path, expected, factories, api_client, settings, preferences
+):
+    preferences["common__api_authentication_required"] = False
+    settings.PROTECT_FILE_PATH = "/_protected/music"
     settings.REVERSE_PROXY_TYPE = proxy
-    settings.MUSIC_DIRECTORY_PATH = '/app/music'
+    settings.MUSIC_DIRECTORY_PATH = "/app/music"
     settings.MUSIC_DIRECTORY_SERVE_PATH = serve_path
-    path = views.get_file_path('/app/music/hello/worldéà.mp3')
+    path = views.get_file_path("/app/music/hello/worldéà.mp3")
 
-    assert path == expected.encode('utf-8')
+    assert path == expected.encode("utf-8")
 
 
-@pytest.mark.parametrize('proxy,serve_path,expected', [
-    ('apache2', '/host/music', '/host/media/tracks/hello/world.mp3'),
-    # apache with container not supported yet
-    # ('apache2', '/app/music', '/app/music/tracks/hello/world.mp3'),
-    ('nginx', '/host/music', '/_protected/media/tracks/hello/world.mp3'),
-    ('nginx', '/app/music', '/_protected/media/tracks/hello/world.mp3'),
-])
+@pytest.mark.parametrize(
+    "proxy,serve_path,expected",
+    [
+        ("apache2", "/host/music", "/host/media/tracks/hello/world.mp3"),
+        # apache with container not supported yet
+        # ('apache2', '/app/music', '/app/music/tracks/hello/world.mp3'),
+        ("nginx", "/host/music", "/_protected/media/tracks/hello/world.mp3"),
+        ("nginx", "/app/music", "/_protected/media/tracks/hello/world.mp3"),
+    ],
+)
 def test_serve_file_media(
-        proxy,
-        serve_path,
-        expected,
-        factories,
-        api_client,
-        settings,
-        preferences):
-    headers = {
-        'apache2': 'X-Sendfile',
-        'nginx': 'X-Accel-Redirect',
-    }
-    preferences['common__api_authentication_required'] = False
-    settings.MEDIA_ROOT = '/host/media'
-    settings.PROTECT_FILE_PATH = '/_protected/music'
+    proxy, serve_path, expected, factories, api_client, settings, preferences
+):
+    headers = {"apache2": "X-Sendfile", "nginx": "X-Accel-Redirect"}
+    preferences["common__api_authentication_required"] = False
+    settings.MEDIA_ROOT = "/host/media"
+    settings.PROTECT_FILE_PATH = "/_protected/music"
     settings.REVERSE_PROXY_TYPE = proxy
-    settings.MUSIC_DIRECTORY_PATH = '/app/music'
+    settings.MUSIC_DIRECTORY_PATH = "/app/music"
     settings.MUSIC_DIRECTORY_SERVE_PATH = serve_path
 
-    tf = factories['music.TrackFile']()
-    tf.__class__.objects.filter(pk=tf.pk).update(
-        audio_file='tracks/hello/world.mp3')
+    tf = factories["music.TrackFile"]()
+    tf.__class__.objects.filter(pk=tf.pk).update(audio_file="tracks/hello/world.mp3")
     response = api_client.get(tf.path)
 
     assert response.status_code == 200
     assert response[headers[proxy]] == expected
 
 
-def test_can_proxy_remote_track(
-        factories, settings, api_client, r_mock, preferences):
-    preferences['common__api_authentication_required'] = False
-    track_file = factories['music.TrackFile'](federation=True)
+def test_can_proxy_remote_track(factories, settings, api_client, r_mock, preferences):
+    preferences["common__api_authentication_required"] = False
+    track_file = factories["music.TrackFile"](federation=True)
 
-    r_mock.get(track_file.library_track.audio_url, body=io.BytesIO(b'test'))
+    r_mock.get(track_file.library_track.audio_url, body=io.BytesIO(b"test"))
     response = api_client.get(track_file.path)
 
     library_track = track_file.library_track
     library_track.refresh_from_db()
     assert response.status_code == 200
-    assert response['X-Accel-Redirect'] == "{}{}".format(
-        settings.PROTECT_FILES_PATH,
-        library_track.audio_file.url)
-    assert library_track.audio_file.read() == b'test'
+    assert response["X-Accel-Redirect"] == "{}{}".format(
+        settings.PROTECT_FILES_PATH, library_track.audio_file.url
+    )
+    assert library_track.audio_file.read() == b"test"
 
 
-def test_serve_updates_access_date(
-        factories, settings, api_client, preferences):
-    preferences['common__api_authentication_required'] = False
-    track_file = factories['music.TrackFile']()
+def test_serve_updates_access_date(factories, settings, api_client, preferences):
+    preferences["common__api_authentication_required"] = False
+    track_file = factories["music.TrackFile"]()
     now = timezone.now()
     assert track_file.accessed_date is None
 
@@ -269,128 +236,118 @@ def test_serve_updates_access_date(
 
 
 def test_can_list_import_jobs(factories, superuser_api_client):
-    job = factories['music.ImportJob']()
-    url = reverse('api:v1:import-jobs-list')
+    job = factories["music.ImportJob"]()
+    url = reverse("api:v1:import-jobs-list")
     response = superuser_api_client.get(url)
 
     assert response.status_code == 200
-    assert response.data['results'][0]['id'] == job.pk
+    assert response.data["results"][0]["id"] == job.pk
 
 
 def test_import_job_stats(factories, superuser_api_client):
-    job1 = factories['music.ImportJob'](status='pending')
-    job2 = factories['music.ImportJob'](status='errored')
+    factories["music.ImportJob"](status="pending")
+    factories["music.ImportJob"](status="errored")
 
-    url = reverse('api:v1:import-jobs-stats')
+    url = reverse("api:v1:import-jobs-stats")
     response = superuser_api_client.get(url)
-    expected = {
-        'errored': 1,
-        'pending': 1,
-        'finished': 0,
-        'skipped': 0,
-        'count': 2,
-    }
+    expected = {"errored": 1, "pending": 1, "finished": 0, "skipped": 0, "count": 2}
     assert response.status_code == 200
     assert response.data == expected
 
 
 def test_import_job_stats_filter(factories, superuser_api_client):
-    job1 = factories['music.ImportJob'](status='pending')
-    job2 = factories['music.ImportJob'](status='errored')
-
-    url = reverse('api:v1:import-jobs-stats')
-    response = superuser_api_client.get(url, {'batch': job1.batch.pk})
-    expected = {
-        'errored': 0,
-        'pending': 1,
-        'finished': 0,
-        'skipped': 0,
-        'count': 1,
-    }
+    job1 = factories["music.ImportJob"](status="pending")
+    factories["music.ImportJob"](status="errored")
+
+    url = reverse("api:v1:import-jobs-stats")
+    response = superuser_api_client.get(url, {"batch": job1.batch.pk})
+    expected = {"errored": 0, "pending": 1, "finished": 0, "skipped": 0, "count": 1}
     assert response.status_code == 200
     assert response.data == expected
 
 
 def test_import_job_run_via_api(factories, superuser_api_client, mocker):
-    run = mocker.patch('funkwhale_api.music.tasks.import_job_run.delay')
-    job1 = factories['music.ImportJob'](status='errored')
-    job2 = factories['music.ImportJob'](status='pending')
+    run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay")
+    job1 = factories["music.ImportJob"](status="errored")
+    job2 = factories["music.ImportJob"](status="pending")
 
-    url = reverse('api:v1:import-jobs-run')
-    response = superuser_api_client.post(url, {'jobs': [job2.pk, job1.pk]})
+    url = reverse("api:v1:import-jobs-run")
+    response = superuser_api_client.post(url, {"jobs": [job2.pk, job1.pk]})
 
     job1.refresh_from_db()
     job2.refresh_from_db()
     assert response.status_code == 200
-    assert response.data == {'jobs': [job1.pk, job2.pk]}
-    assert job1.status == 'pending'
-    assert job2.status == 'pending'
+    assert response.data == {"jobs": [job1.pk, job2.pk]}
+    assert job1.status == "pending"
+    assert job2.status == "pending"
 
     run.assert_any_call(import_job_id=job1.pk)
     run.assert_any_call(import_job_id=job2.pk)
 
 
 def test_import_batch_run_via_api(factories, superuser_api_client, mocker):
-    run = mocker.patch('funkwhale_api.music.tasks.import_job_run.delay')
+    run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay")
 
-    batch = factories['music.ImportBatch']()
-    job1 = factories['music.ImportJob'](batch=batch, status='errored')
-    job2 = factories['music.ImportJob'](batch=batch, status='pending')
+    batch = factories["music.ImportBatch"]()
+    job1 = factories["music.ImportJob"](batch=batch, status="errored")
+    job2 = factories["music.ImportJob"](batch=batch, status="pending")
 
-    url = reverse('api:v1:import-jobs-run')
-    response = superuser_api_client.post(url, {'batches': [batch.pk]})
+    url = reverse("api:v1:import-jobs-run")
+    response = superuser_api_client.post(url, {"batches": [batch.pk]})
 
     job1.refresh_from_db()
     job2.refresh_from_db()
     assert response.status_code == 200
-    assert job1.status == 'pending'
-    assert job2.status == 'pending'
+    assert job1.status == "pending"
+    assert job2.status == "pending"
 
     run.assert_any_call(import_job_id=job1.pk)
     run.assert_any_call(import_job_id=job2.pk)
 
 
-def test_import_batch_and_job_run_via_api(
-        factories, superuser_api_client, mocker):
-    run = mocker.patch('funkwhale_api.music.tasks.import_job_run.delay')
+def test_import_batch_and_job_run_via_api(factories, superuser_api_client, mocker):
+    run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay")
 
-    batch = factories['music.ImportBatch']()
-    job1 = factories['music.ImportJob'](batch=batch, status='errored')
-    job2 = factories['music.ImportJob'](status='pending')
+    batch = factories["music.ImportBatch"]()
+    job1 = factories["music.ImportJob"](batch=batch, status="errored")
+    job2 = factories["music.ImportJob"](status="pending")
 
-    url = reverse('api:v1:import-jobs-run')
+    url = reverse("api:v1:import-jobs-run")
     response = superuser_api_client.post(
-        url, {'batches': [batch.pk], 'jobs': [job2.pk]})
+        url, {"batches": [batch.pk], "jobs": [job2.pk]}
+    )
 
     job1.refresh_from_db()
     job2.refresh_from_db()
     assert response.status_code == 200
-    assert job1.status == 'pending'
-    assert job2.status == 'pending'
+    assert job1.status == "pending"
+    assert job2.status == "pending"
 
     run.assert_any_call(import_job_id=job1.pk)
     run.assert_any_call(import_job_id=job2.pk)
 
 
 def test_import_job_viewset_get_queryset_upload_filters_user(
-        factories, logged_in_api_client):
+    factories, logged_in_api_client
+):
     logged_in_api_client.user.permission_upload = True
     logged_in_api_client.user.save()
 
-    job = factories['music.ImportJob']()
-    url = reverse('api:v1:import-jobs-list')
+    factories["music.ImportJob"]()
+    url = reverse("api:v1:import-jobs-list")
     response = logged_in_api_client.get(url)
 
-    assert response.data['count'] == 0
+    assert response.data["count"] == 0
 
 
 def test_import_batch_viewset_get_queryset_upload_filters_user(
-        factories, logged_in_api_client):
+    factories, logged_in_api_client
+):
     logged_in_api_client.user.permission_upload = True
     logged_in_api_client.user.save()
 
-    job = factories['music.ImportBatch']()
-    url = reverse('api:v1:import-batches-list')
+    factories["music.ImportBatch"]()
+    url = reverse("api:v1:import-batches-list")
     response = logged_in_api_client.get(url)
 
-    assert response.data['count'] == 0
+    assert response.data["count"] == 0
diff --git a/api/tests/music/test_works.py b/api/tests/music/test_works.py
index 13f6447bec60f54797f890f441412494f557b7c7..96b537ca297a5b9e8c81528349b42d5404fbe9f6 100644
--- a/api/tests/music/test_works.py
+++ b/api/tests/music/test_works.py
@@ -1,23 +1,18 @@
-import json
-from django.urls import reverse
-
 from funkwhale_api.music import models
-from funkwhale_api.musicbrainz import api
-from funkwhale_api.music import serializers
 
 
 def test_can_import_work(factories, mocker, works):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.works.get',
-        return_value=works['get']['chop_suey'])
-    recording = factories['music.Track'](
-        mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
-    mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
+        "funkwhale_api.musicbrainz.api.works.get",
+        return_value=works["get"]["chop_suey"],
+    )
+    recording = factories["music.Track"](mbid="07ca77cf-f513-4e9c-b190-d7e24bbad448")
+    mbid = "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5"
     work = models.Work.create_from_api(id=mbid)
 
-    assert work.title == 'Chop Suey!'
-    assert work.nature == 'song'
-    assert work.language == 'eng'
+    assert work.title == "Chop Suey!"
+    assert work.nature == "song"
+    assert work.language == "eng"
     assert work.mbid == mbid
 
     # a imported work should also be linked to corresponding recordings
@@ -28,23 +23,25 @@ def test_can_import_work(factories, mocker, works):
 
 def test_can_get_work_from_recording(factories, mocker, works, tracks):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.works.get',
-        return_value=works['get']['chop_suey'])
+        "funkwhale_api.musicbrainz.api.works.get",
+        return_value=works["get"]["chop_suey"],
+    )
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.recordings.get',
-        return_value=tracks['get']['chop_suey'])
-    recording = factories['music.Track'](
-        work=None,
-        mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
-    mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
+        "funkwhale_api.musicbrainz.api.recordings.get",
+        return_value=tracks["get"]["chop_suey"],
+    )
+    recording = factories["music.Track"](
+        work=None, mbid="07ca77cf-f513-4e9c-b190-d7e24bbad448"
+    )
+    mbid = "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5"
 
-    assert recording.work == None
+    assert recording.work is None
 
     work = recording.get_work()
 
-    assert work.title == 'Chop Suey!'
-    assert work.nature == 'song'
-    assert work.language == 'eng'
+    assert work.title == "Chop Suey!"
+    assert work.nature == "song"
+    assert work.language == "eng"
     assert work.mbid == mbid
 
     recording.refresh_from_db()
@@ -53,11 +50,12 @@ def test_can_get_work_from_recording(factories, mocker, works, tracks):
 
 def test_works_import_lyrics_if_any(db, mocker, works):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.works.get',
-        return_value=works['get']['chop_suey'])
-    mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
+        "funkwhale_api.musicbrainz.api.works.get",
+        return_value=works["get"]["chop_suey"],
+    )
+    mbid = "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5"
     work = models.Work.create_from_api(id=mbid)
 
-    lyrics = models.Lyrics.objects.latest('id')
+    lyrics = models.Lyrics.objects.latest("id")
     assert lyrics.work == work
-    assert lyrics.url == 'http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!'
+    assert lyrics.url == "http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!"
diff --git a/api/tests/musicbrainz/conftest.py b/api/tests/musicbrainz/conftest.py
index 505d6e5537ab367090a4ead483e56105f561080c..3e3ebfa481fc47a5df9d6c63b40d5e21a57c27f1 100644
--- a/api/tests/musicbrainz/conftest.py
+++ b/api/tests/musicbrainz/conftest.py
@@ -1,33 +1,28 @@
 import pytest
 
-_artists = {'search': {}, 'get': {}}
-_artists['search']['lost fingers'] = {
-    'artist-count': 696,
-    'artist-list': [
+_artists = {"search": {}, "get": {}}
+_artists["search"]["lost fingers"] = {
+    "artist-count": 696,
+    "artist-list": [
         {
-            'country': 'CA',
-            'sort-name': 'Lost Fingers, The',
-            'id': 'ac16bbc0-aded-4477-a3c3-1d81693d58c9',
-            'type': 'Group',
-            'life-span': {
-                'ended': 'false',
-                'begin': '2008'
-            },
-            'area': {
-                'sort-name': 'Canada',
-                'id': '71bbafaa-e825-3e15-8ca9-017dcad1748b',
-                'name': 'Canada'
+            "country": "CA",
+            "sort-name": "Lost Fingers, The",
+            "id": "ac16bbc0-aded-4477-a3c3-1d81693d58c9",
+            "type": "Group",
+            "life-span": {"ended": "false", "begin": "2008"},
+            "area": {
+                "sort-name": "Canada",
+                "id": "71bbafaa-e825-3e15-8ca9-017dcad1748b",
+                "name": "Canada",
             },
-            'ext:score': '100',
-            'name': 'The Lost Fingers'
-        },
-    ]
+            "ext:score": "100",
+            "name": "The Lost Fingers",
+        }
+    ],
 }
-_artists['get']['lost fingers'] = {
+_artists["get"]["lost fingers"] = {
     "artist": {
-        "life-span": {
-            "begin": "2008"
-        },
+        "life-span": {"begin": "2008"},
         "type": "Group",
         "id": "ac16bbc0-aded-4477-a3c3-1d81693d58c9",
         "release-group-count": 8,
@@ -38,137 +33,135 @@ _artists['get']['lost fingers'] = {
                 "first-release-date": "2010",
                 "type": "Album",
                 "id": "03d3f1d4-e2b0-40d3-8314-05f1896e93a0",
-                "primary-type": "Album"
+                "primary-type": "Album",
             },
             {
                 "title": "Gitan Kameleon",
                 "first-release-date": "2011-11-11",
                 "type": "Album",
                 "id": "243c0cd2-2492-4f5d-bf37-c7c76bed05b7",
-                "primary-type": "Album"
+                "primary-type": "Album",
             },
             {
                 "title": "Pump Up the Jam \u2013 Do Not Cover, Pt. 3",
                 "first-release-date": "2014-03-17",
                 "type": "Single",
                 "id": "4429befd-ff45-48eb-a8f4-cdf7bf007f3f",
-                "primary-type": "Single"
+                "primary-type": "Single",
             },
             {
                 "title": "La Marquise",
                 "first-release-date": "2012-03-27",
                 "type": "Album",
                 "id": "4dab4b96-0a6b-4507-a31e-2189e3e7bad1",
-                "primary-type": "Album"
+                "primary-type": "Album",
             },
             {
                 "title": "Christmas Caravan",
                 "first-release-date": "2016-11-11",
                 "type": "Album",
                 "id": "ca0a506d-6ba9-47c3-a712-de5ce9ae6b1f",
-                "primary-type": "Album"
+                "primary-type": "Album",
             },
             {
                 "title": "Rendez-vous rose",
                 "first-release-date": "2009-06-16",
                 "type": "Album",
                 "id": "d002f1a8-5890-4188-be58-1caadbbd767f",
-                "primary-type": "Album"
+                "primary-type": "Album",
             },
             {
                 "title": "Wonders of the World",
                 "first-release-date": "2014-05-06",
                 "type": "Album",
                 "id": "eeb644c2-5000-42fb-b959-e5e9cc2901c5",
-                "primary-type": "Album"
+                "primary-type": "Album",
             },
             {
                 "title": "Lost in the 80s",
                 "first-release-date": "2008-05-06",
                 "type": "Album",
                 "id": "f04ed607-11b7-3843-957e-503ecdd485d1",
-                "primary-type": "Album"
-            }
+                "primary-type": "Album",
+            },
         ],
         "area": {
-            "iso-3166-1-code-list": [
-                "CA"
-            ],
+            "iso-3166-1-code-list": ["CA"],
             "name": "Canada",
             "id": "71bbafaa-e825-3e15-8ca9-017dcad1748b",
-            "sort-name": "Canada"
+            "sort-name": "Canada",
         },
         "sort-name": "Lost Fingers, The",
-        "country": "CA"
+        "country": "CA",
     }
 }
 
 
-_release_groups = {'browse': {}}
-_release_groups['browse']["lost fingers"] = {
+_release_groups = {"browse": {}}
+_release_groups["browse"]["lost fingers"] = {
     "release-group-list": [
         {
             "first-release-date": "2010",
             "type": "Album",
             "primary-type": "Album",
             "title": "Gypsy Kameleon",
-            "id": "03d3f1d4-e2b0-40d3-8314-05f1896e93a0"
+            "id": "03d3f1d4-e2b0-40d3-8314-05f1896e93a0",
         },
         {
             "first-release-date": "2011-11-11",
             "type": "Album",
             "primary-type": "Album",
             "title": "Gitan Kameleon",
-            "id": "243c0cd2-2492-4f5d-bf37-c7c76bed05b7"
+            "id": "243c0cd2-2492-4f5d-bf37-c7c76bed05b7",
         },
         {
             "first-release-date": "2014-03-17",
             "type": "Single",
             "primary-type": "Single",
             "title": "Pump Up the Jam \u2013 Do Not Cover, Pt. 3",
-            "id": "4429befd-ff45-48eb-a8f4-cdf7bf007f3f"
+            "id": "4429befd-ff45-48eb-a8f4-cdf7bf007f3f",
         },
         {
             "first-release-date": "2012-03-27",
             "type": "Album",
             "primary-type": "Album",
             "title": "La Marquise",
-            "id": "4dab4b96-0a6b-4507-a31e-2189e3e7bad1"
+            "id": "4dab4b96-0a6b-4507-a31e-2189e3e7bad1",
         },
         {
             "first-release-date": "2016-11-11",
             "type": "Album",
             "primary-type": "Album",
             "title": "Christmas Caravan",
-            "id": "ca0a506d-6ba9-47c3-a712-de5ce9ae6b1f"
+            "id": "ca0a506d-6ba9-47c3-a712-de5ce9ae6b1f",
         },
         {
             "first-release-date": "2009-06-16",
             "type": "Album",
             "primary-type": "Album",
             "title": "Rendez-vous rose",
-            "id": "d002f1a8-5890-4188-be58-1caadbbd767f"
+            "id": "d002f1a8-5890-4188-be58-1caadbbd767f",
         },
         {
             "first-release-date": "2014-05-06",
             "type": "Album",
             "primary-type": "Album",
             "title": "Wonders of the World",
-            "id": "eeb644c2-5000-42fb-b959-e5e9cc2901c5"
+            "id": "eeb644c2-5000-42fb-b959-e5e9cc2901c5",
         },
         {
             "first-release-date": "2008-05-06",
             "type": "Album",
             "primary-type": "Album",
             "title": "Lost in the 80s",
-            "id": "f04ed607-11b7-3843-957e-503ecdd485d1"
-        }
+            "id": "f04ed607-11b7-3843-957e-503ecdd485d1",
+        },
     ],
-    "release-group-count": 8
+    "release-group-count": 8,
 }
 
-_recordings = {'search': {}, 'get': {}}
-_recordings['search']['brontide matador'] = {
+_recordings = {"search": {}, "get": {}}
+_recordings["search"]["brontide matador"] = {
     "recording-count": 1044,
     "recording-list": [
         {
@@ -184,9 +177,9 @@ _recordings['search']['brontide matador'] = {
                                 "name": "United Kingdom",
                                 "sort-name": "United Kingdom",
                                 "id": "8a754a16-0027-3a29-b6d7-2b40ea0481ed",
-                                "iso-3166-1-code-list": ["GB"]
+                                "iso-3166-1-code-list": ["GB"],
                             },
-                            "date": "2011-05-30"
+                            "date": "2011-05-30",
                         }
                     ],
                     "country": "GB",
@@ -196,7 +189,7 @@ _recordings['search']['brontide matador'] = {
                     "release-group": {
                         "type": "Album",
                         "id": "113ab958-cfb8-4782-99af-639d4d9eae8d",
-                        "primary-type": "Album"
+                        "primary-type": "Album",
                     },
                     "medium-list": [
                         {
@@ -206,22 +199,24 @@ _recordings['search']['brontide matador'] = {
                                     "track_or_recording_length": "366280",
                                     "id": "fe506782-a5cb-3d89-9b3e-86287be05768",
                                     "length": "366280",
-                                    "title": "Matador", "number": "1"
+                                    "title": "Matador",
+                                    "number": "1",
                                 }
                             ],
                             "position": "1",
-                            "track-count": 8
+                            "track-count": 8,
                         }
-                    ]
-                },
-            ]
+                    ],
+                }
+            ],
         }
-    ]
+    ],
 }
 
-_releases = {'search': {}, 'get': {}, 'browse': {}}
-_releases['search']['brontide matador'] = {
-    "release-count": 116, "release-list": [
+_releases = {"search": {}, "get": {}, "browse": {}}
+_releases["search"]["brontide matador"] = {
+    "release-count": 116,
+    "release-list": [
         {
             "ext:score": "100",
             "date": "2009-04-02",
@@ -231,16 +226,16 @@ _releases['search']['brontide matador'] = {
                         "name": "[Worldwide]",
                         "sort-name": "[Worldwide]",
                         "id": "525d4e18-3d00-31b9-a58b-a146a916de8f",
-                        "iso-3166-1-code-list": ["XW"]
+                        "iso-3166-1-code-list": ["XW"],
                     },
-                    "date": "2009-04-02"
+                    "date": "2009-04-02",
                 }
             ],
             "label-info-list": [
                 {
                     "label": {
                         "name": "Holy Roar",
-                        "id": "6e940f35-961d-4ac3-bc2a-569fc211c2e3"
+                        "id": "6e940f35-961d-4ac3-bc2a-569fc211c2e3",
                     }
                 }
             ],
@@ -251,7 +246,7 @@ _releases['search']['brontide matador'] = {
                     "artist": {
                         "name": "Brontide",
                         "sort-name": "Brontide",
-                        "id": "2179fbd2-3c88-4b94-a778-eb3daf1e81a1"
+                        "id": "2179fbd2-3c88-4b94-a778-eb3daf1e81a1",
                     }
                 }
             ],
@@ -265,7 +260,7 @@ _releases['search']['brontide matador'] = {
                 "type": "EP",
                 "secondary-type-list": ["Demo"],
                 "id": "b9207129-2d03-4a68-8a53-3c46fe7d2810",
-                "primary-type": "EP"
+                "primary-type": "EP",
             },
             "medium-list": [
                 {
@@ -273,28 +268,22 @@ _releases['search']['brontide matador'] = {
                     "format": "Digital Media",
                     "disc-count": 0,
                     "track-count": 3,
-                    "track-list": []
+                    "track-list": [],
                 }
             ],
             "medium-count": 1,
-            "text-representation": {
-                "script": "Latn",
-                "language": "eng"
-            }
-        },
-    ]
+            "text-representation": {"script": "Latn", "language": "eng"},
+        }
+    ],
 }
 
-_releases['browse']['Lost in the 80s'] = {
+_releases["browse"]["Lost in the 80s"] = {
     "release-count": 3,
     "release-list": [
         {
             "quality": "normal",
             "status": "Official",
-            "text-representation": {
-                "script": "Latn",
-                "language": "eng"
-            },
+            "text-representation": {"script": "Latn", "language": "eng"},
             "title": "Lost in the 80s",
             "date": "2008-05-06",
             "release-event-count": 1,
@@ -304,14 +293,12 @@ _releases['browse']['Lost in the 80s'] = {
             "release-event-list": [
                 {
                     "area": {
-                        "iso-3166-1-code-list": [
-                            "CA"
-                        ],
+                        "iso-3166-1-code-list": ["CA"],
                         "id": "71bbafaa-e825-3e15-8ca9-017dcad1748b",
                         "name": "Canada",
-                        "sort-name": "Canada"
+                        "sort-name": "Canada",
                     },
-                    "date": "2008-05-06"
+                    "date": "2008-05-06",
                 }
             ],
             "country": "CA",
@@ -319,7 +306,7 @@ _releases['browse']['Lost in the 80s'] = {
                 "back": "false",
                 "artwork": "false",
                 "front": "false",
-                "count": "0"
+                "count": "0",
             },
             "medium-list": [
                 {
@@ -333,11 +320,11 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "2e0dbf37-65af-4408-8def-7b0b3cb8426b",
                                 "length": "228000",
-                                "title": "Pump Up the Jam"
+                                "title": "Pump Up the Jam",
                             },
                             "track_or_recording_length": "228000",
                             "position": "1",
-                            "number": "1"
+                            "number": "1",
                         },
                         {
                             "id": "01a8cf99-2170-3d3f-96ef-5e4ef7a015a4",
@@ -345,11 +332,11 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "57017e2e-625d-4e7b-a445-47cdb0224dd2",
                                 "length": "231000",
-                                "title": "You Give Love a Bad Name"
+                                "title": "You Give Love a Bad Name",
                             },
                             "track_or_recording_length": "231000",
                             "position": "2",
-                            "number": "2"
+                            "number": "2",
                         },
                         {
                             "id": "375a7ce7-5a41-3fbf-9809-96d491401034",
@@ -357,11 +344,11 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "a948672b-b42d-44a5-89b0-7e9ab6a7e11d",
                                 "length": "189000",
-                                "title": "You Shook Me All Night Long"
+                                "title": "You Shook Me All Night Long",
                             },
                             "track_or_recording_length": "189000",
                             "position": "3",
-                            "number": "3"
+                            "number": "3",
                         },
                         {
                             "id": "ed7d823e-76da-31be-82a8-770288e27d32",
@@ -369,11 +356,11 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "6e097e31-f37b-4fae-8ad0-ada57f3091a7",
                                 "length": "253000",
-                                "title": "Incognito"
+                                "title": "Incognito",
                             },
                             "track_or_recording_length": "253000",
                             "position": "4",
-                            "number": "4"
+                            "number": "4",
                         },
                         {
                             "id": "76ac8c77-6a99-34d9-ae4d-be8f056d50e0",
@@ -381,11 +368,11 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "faa922e6-e834-44ee-8125-79e640a690e3",
                                 "length": "221000",
-                                "title": "Touch Me"
+                                "title": "Touch Me",
                             },
                             "track_or_recording_length": "221000",
                             "position": "5",
-                            "number": "5"
+                            "number": "5",
                         },
                         {
                             "id": "d0a87409-2be6-3ab7-8526-4313e7134be1",
@@ -393,11 +380,11 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "02da8148-60d8-4c79-ab31-8d90d233d711",
                                 "length": "228000",
-                                "title": "Part-Time Lover"
+                                "title": "Part-Time Lover",
                             },
                             "track_or_recording_length": "228000",
                             "position": "6",
-                            "number": "6"
+                            "number": "6",
                         },
                         {
                             "id": "02c5384b-5ca9-38e9-8b7c-c08dce608deb",
@@ -405,11 +392,11 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "40085704-d6ab-44f6-a4d8-b27c9ca25b31",
                                 "length": "248000",
-                                "title": "Fresh"
+                                "title": "Fresh",
                             },
                             "track_or_recording_length": "248000",
                             "position": "7",
-                            "number": "7"
+                            "number": "7",
                         },
                         {
                             "id": "ab389542-53d5-346a-b168-1d915ecf0ef6",
@@ -417,11 +404,11 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "77edd338-eeaf-4157-9e2a-5cc3bcee8abd",
                                 "length": "257000",
-                                "title": "Billie Jean"
+                                "title": "Billie Jean",
                             },
                             "track_or_recording_length": "257000",
                             "position": "8",
-                            "number": "8"
+                            "number": "8",
                         },
                         {
                             "id": "6d9e722b-7408-350e-bb7c-2de1e329ae84",
@@ -429,11 +416,11 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "040aaffa-7206-40ff-9930-469413fe2420",
                                 "length": "293000",
-                                "title": "Careless Whisper"
+                                "title": "Careless Whisper",
                             },
                             "track_or_recording_length": "293000",
                             "position": "9",
-                            "number": "9"
+                            "number": "9",
                         },
                         {
                             "id": "63b4e67c-7536-3cd0-8c47-0310c1e40866",
@@ -441,11 +428,11 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "054942f0-4c0f-4e92-a606-d590976b1cff",
                                 "length": "211000",
-                                "title": "Tainted Love"
+                                "title": "Tainted Love",
                             },
                             "track_or_recording_length": "211000",
                             "position": "10",
-                            "number": "10"
+                            "number": "10",
                         },
                         {
                             "id": "a07f4ca3-dbf0-3337-a247-afcd0509334a",
@@ -453,11 +440,11 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "8023b5ad-649a-4c67-b7a2-e12358606f6e",
                                 "length": "245000",
-                                "title": "Straight Up"
+                                "title": "Straight Up",
                             },
                             "track_or_recording_length": "245000",
                             "position": "11",
-                            "number": "11"
+                            "number": "11",
                         },
                         {
                             "id": "73d47f16-b18d-36ff-b0bb-1fa1fd32ebf7",
@@ -465,18 +452,18 @@ _releases['browse']['Lost in the 80s'] = {
                             "recording": {
                                 "id": "95a8c8a1-fcb6-4cbb-a853-be86d816b357",
                                 "length": "322000",
-                                "title": "Black Velvet"
+                                "title": "Black Velvet",
                             },
                             "track_or_recording_length": "322000",
                             "position": "12",
-                            "number": "12"
-                        }
-                    ]
+                            "number": "12",
+                        },
+                    ],
                 }
             ],
-            "asin": "B0017M8YTO"
-        },
-    ]
+            "asin": "B0017M8YTO",
+        }
+    ],
 }
 
 
diff --git a/api/tests/musicbrainz/test_api.py b/api/tests/musicbrainz/test_api.py
index fdd1dbdb03b74769588ef6c66c23a49cc1053b29..0fdaf7ab6cadf4ee67ed4d7c9783fa69f58f4e89 100644
--- a/api/tests/musicbrainz/test_api.py
+++ b/api/tests/musicbrainz/test_api.py
@@ -1,92 +1,95 @@
-import json
 from django.urls import reverse
 
-from funkwhale_api.musicbrainz import api
-
-
 
 def test_can_search_recording_in_musicbrainz_api(
-        recordings, db, mocker, logged_in_api_client):
+    recordings, db, mocker, logged_in_api_client
+):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.recordings.search',
-        return_value=recordings['search']['brontide matador'])
-    query = 'brontide matador'
-    url = reverse('api:v1:providers:musicbrainz:search-recordings')
-    expected = recordings['search']['brontide matador']
-    response = logged_in_api_client.get(url, data={'query': query})
+        "funkwhale_api.musicbrainz.api.recordings.search",
+        return_value=recordings["search"]["brontide matador"],
+    )
+    query = "brontide matador"
+    url = reverse("api:v1:providers:musicbrainz:search-recordings")
+    expected = recordings["search"]["brontide matador"]
+    response = logged_in_api_client.get(url, data={"query": query})
 
     assert expected == response.data
 
 
-def test_can_search_release_in_musicbrainz_api(releases, db, mocker, logged_in_api_client):
+def test_can_search_release_in_musicbrainz_api(
+    releases, db, mocker, logged_in_api_client
+):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.search',
-        return_value=releases['search']['brontide matador'])
-    query = 'brontide matador'
-    url = reverse('api:v1:providers:musicbrainz:search-releases')
-    expected = releases['search']['brontide matador']
-    response = logged_in_api_client.get(url, data={'query': query})
+        "funkwhale_api.musicbrainz.api.releases.search",
+        return_value=releases["search"]["brontide matador"],
+    )
+    query = "brontide matador"
+    url = reverse("api:v1:providers:musicbrainz:search-releases")
+    expected = releases["search"]["brontide matador"]
+    response = logged_in_api_client.get(url, data={"query": query})
 
     assert expected == response.data
 
 
-def test_can_search_artists_in_musicbrainz_api(artists, db, mocker, logged_in_api_client):
+def test_can_search_artists_in_musicbrainz_api(
+    artists, db, mocker, logged_in_api_client
+):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.artists.search',
-        return_value=artists['search']['lost fingers'])
-    query = 'lost fingers'
-    url = reverse('api:v1:providers:musicbrainz:search-artists')
-    expected = artists['search']['lost fingers']
-    response = logged_in_api_client.get(url, data={'query': query})
+        "funkwhale_api.musicbrainz.api.artists.search",
+        return_value=artists["search"]["lost fingers"],
+    )
+    query = "lost fingers"
+    url = reverse("api:v1:providers:musicbrainz:search-artists")
+    expected = artists["search"]["lost fingers"]
+    response = logged_in_api_client.get(url, data={"query": query})
 
     assert expected == response.data
 
 
 def test_can_get_artist_in_musicbrainz_api(artists, db, mocker, logged_in_api_client):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.artists.get',
-        return_value=artists['get']['lost fingers'])
-    uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9'
-    url = reverse('api:v1:providers:musicbrainz:artist-detail', kwargs={
-        'uuid': uuid,
-    })
+        "funkwhale_api.musicbrainz.api.artists.get",
+        return_value=artists["get"]["lost fingers"],
+    )
+    uuid = "ac16bbc0-aded-4477-a3c3-1d81693d58c9"
+    url = reverse("api:v1:providers:musicbrainz:artist-detail", kwargs={"uuid": uuid})
     response = logged_in_api_client.get(url)
-    expected = artists['get']['lost fingers']
+    expected = artists["get"]["lost fingers"]
 
     assert expected == response.data
 
 
 def test_can_broswe_release_group_using_musicbrainz_api(
-        release_groups, db, mocker, logged_in_api_client):
+    release_groups, db, mocker, logged_in_api_client
+):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.release_groups.browse',
-        return_value=release_groups['browse']['lost fingers'])
-    uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9'
+        "funkwhale_api.musicbrainz.api.release_groups.browse",
+        return_value=release_groups["browse"]["lost fingers"],
+    )
+    uuid = "ac16bbc0-aded-4477-a3c3-1d81693d58c9"
     url = reverse(
-        'api:v1:providers:musicbrainz:release-group-browse',
-        kwargs={
-            'artist_uuid': uuid,
-        }
+        "api:v1:providers:musicbrainz:release-group-browse",
+        kwargs={"artist_uuid": uuid},
     )
     response = logged_in_api_client.get(url)
-    expected = release_groups['browse']['lost fingers']
+    expected = release_groups["browse"]["lost fingers"]
 
     assert expected == response.data
 
 
 def test_can_broswe_releases_using_musicbrainz_api(
-        releases, db, mocker, logged_in_api_client):
+    releases, db, mocker, logged_in_api_client
+):
     mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.browse',
-        return_value=releases['browse']['Lost in the 80s'])
-    uuid = 'f04ed607-11b7-3843-957e-503ecdd485d1'
+        "funkwhale_api.musicbrainz.api.releases.browse",
+        return_value=releases["browse"]["Lost in the 80s"],
+    )
+    uuid = "f04ed607-11b7-3843-957e-503ecdd485d1"
     url = reverse(
-        'api:v1:providers:musicbrainz:release-browse',
-        kwargs={
-            'release_group_uuid': uuid,
-        }
+        "api:v1:providers:musicbrainz:release-browse",
+        kwargs={"release_group_uuid": uuid},
     )
     response = logged_in_api_client.get(url)
-    expected = releases['browse']['Lost in the 80s']
+    expected = releases["browse"]["Lost in the 80s"]
 
     assert expected == response.data
diff --git a/api/tests/musicbrainz/test_cache.py b/api/tests/musicbrainz/test_cache.py
index fe0d5677302b08bb3f75c45a31f23f3238d034ff..3a326ff24ea715ff61fdf1816de74f96217e55d8 100644
--- a/api/tests/musicbrainz/test_cache.py
+++ b/api/tests/musicbrainz/test_cache.py
@@ -2,12 +2,12 @@ from funkwhale_api.musicbrainz import client
 
 
 def test_can_search_recording_in_musicbrainz_api(mocker):
-    r = {'hello': 'world'}
+    r = {"hello": "world"}
     m = mocker.patch(
-        'funkwhale_api.musicbrainz.client._api.search_artists',
-        return_value=r)
-    assert client.api.artists.search('test') == r
+        "funkwhale_api.musicbrainz.client._api.search_artists", return_value=r
+    )
+    assert client.api.artists.search("test") == r
     # now call from cache
-    assert client.api.artists.search('test') == r
-    assert client.api.artists.search('test') == r
+    assert client.api.artists.search("test") == r
+    assert client.api.artists.search("test") == r
     assert m.call_count == 1
diff --git a/api/tests/playlists/test_models.py b/api/tests/playlists/test_models.py
index fe5dd40a8518aa6507f980fed06e2e7b45012d17..25c40d5573ea9c67e4a3926ddfd7024032c8ab78 100644
--- a/api/tests/playlists/test_models.py
+++ b/api/tests/playlists/test_models.py
@@ -1,10 +1,9 @@
 import pytest
-
 from rest_framework import exceptions
 
 
 def test_can_insert_plt(factories):
-    plt = factories['playlists.PlaylistTrack']()
+    plt = factories["playlists.PlaylistTrack"]()
     modification_date = plt.playlist.modification_date
 
     assert plt.index is None
@@ -17,9 +16,8 @@ def test_can_insert_plt(factories):
 
 
 def test_insert_use_last_idx_by_default(factories):
-    playlist = factories['playlists.Playlist']()
-    plts = factories['playlists.PlaylistTrack'].create_batch(
-        size=3, playlist=playlist)
+    playlist = factories["playlists.Playlist"]()
+    plts = factories["playlists.PlaylistTrack"].create_batch(size=3, playlist=playlist)
 
     for i, plt in enumerate(plts):
         index = playlist.insert(plt)
@@ -28,11 +26,12 @@ def test_insert_use_last_idx_by_default(factories):
         assert index == i
         assert plt.index == i
 
+
 def test_can_insert_at_index(factories):
-    playlist = factories['playlists.Playlist']()
-    first = factories['playlists.PlaylistTrack'](playlist=playlist)
+    playlist = factories["playlists.Playlist"]()
+    first = factories["playlists.PlaylistTrack"](playlist=playlist)
     playlist.insert(first)
-    new_first = factories['playlists.PlaylistTrack'](playlist=playlist)
+    new_first = factories["playlists.PlaylistTrack"](playlist=playlist)
     index = playlist.insert(new_first, index=0)
     first.refresh_from_db()
     new_first.refresh_from_db()
@@ -43,10 +42,10 @@ def test_can_insert_at_index(factories):
 
 
 def test_can_insert_and_move(factories):
-    playlist = factories['playlists.Playlist']()
-    first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0)
-    second = factories['playlists.PlaylistTrack'](playlist=playlist, index=1)
-    third = factories['playlists.PlaylistTrack'](playlist=playlist, index=2)
+    playlist = factories["playlists.Playlist"]()
+    first = factories["playlists.PlaylistTrack"](playlist=playlist, index=0)
+    second = factories["playlists.PlaylistTrack"](playlist=playlist, index=1)
+    third = factories["playlists.PlaylistTrack"](playlist=playlist, index=2)
 
     playlist.insert(second, index=0)
 
@@ -60,10 +59,10 @@ def test_can_insert_and_move(factories):
 
 
 def test_can_insert_and_move_last_to_0(factories):
-    playlist = factories['playlists.Playlist']()
-    first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0)
-    second = factories['playlists.PlaylistTrack'](playlist=playlist, index=1)
-    third = factories['playlists.PlaylistTrack'](playlist=playlist, index=2)
+    playlist = factories["playlists.Playlist"]()
+    first = factories["playlists.PlaylistTrack"](playlist=playlist, index=0)
+    second = factories["playlists.PlaylistTrack"](playlist=playlist, index=1)
+    third = factories["playlists.PlaylistTrack"](playlist=playlist, index=2)
 
     playlist.insert(third, index=0)
 
@@ -77,24 +76,24 @@ def test_can_insert_and_move_last_to_0(factories):
 
 
 def test_cannot_insert_at_wrong_index(factories):
-    plt = factories['playlists.PlaylistTrack']()
-    new = factories['playlists.PlaylistTrack'](playlist=plt.playlist)
+    plt = factories["playlists.PlaylistTrack"]()
+    new = factories["playlists.PlaylistTrack"](playlist=plt.playlist)
     with pytest.raises(exceptions.ValidationError):
         plt.playlist.insert(new, 2)
 
 
 def test_cannot_insert_at_negative_index(factories):
-    plt = factories['playlists.PlaylistTrack']()
-    new = factories['playlists.PlaylistTrack'](playlist=plt.playlist)
+    plt = factories["playlists.PlaylistTrack"]()
+    new = factories["playlists.PlaylistTrack"](playlist=plt.playlist)
     with pytest.raises(exceptions.ValidationError):
         plt.playlist.insert(new, -1)
 
 
 def test_remove_update_indexes(factories):
-    playlist = factories['playlists.Playlist']()
-    first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0)
-    second = factories['playlists.PlaylistTrack'](playlist=playlist, index=1)
-    third = factories['playlists.PlaylistTrack'](playlist=playlist, index=2)
+    playlist = factories["playlists.Playlist"]()
+    first = factories["playlists.PlaylistTrack"](playlist=playlist, index=0)
+    second = factories["playlists.PlaylistTrack"](playlist=playlist, index=1)
+    third = factories["playlists.PlaylistTrack"](playlist=playlist, index=2)
 
     second.delete(update_indexes=True)
 
@@ -106,9 +105,9 @@ def test_remove_update_indexes(factories):
 
 
 def test_can_insert_many(factories):
-    playlist = factories['playlists.Playlist']()
-    existing = factories['playlists.PlaylistTrack'](playlist=playlist, index=0)
-    tracks = factories['music.Track'].create_batch(size=3)
+    playlist = factories["playlists.Playlist"]()
+    factories["playlists.PlaylistTrack"](playlist=playlist, index=0)
+    tracks = factories["music.Track"].create_batch(size=3)
     plts = playlist.insert_many(tracks)
     for i, plt in enumerate(plts):
         assert plt.index == i + 1
@@ -117,10 +116,9 @@ def test_can_insert_many(factories):
 
 
 def test_insert_many_honor_max_tracks(preferences, factories):
-    preferences['playlists__max_tracks'] = 4
-    playlist = factories['playlists.Playlist']()
-    plts = factories['playlists.PlaylistTrack'].create_batch(
-        size=2, playlist=playlist)
-    track = factories['music.Track']()
+    preferences["playlists__max_tracks"] = 4
+    playlist = factories["playlists.Playlist"]()
+    factories["playlists.PlaylistTrack"].create_batch(size=2, playlist=playlist)
+    track = factories["music.Track"]()
     with pytest.raises(exceptions.ValidationError):
         playlist.insert_many([track, track, track])
diff --git a/api/tests/playlists/test_serializers.py b/api/tests/playlists/test_serializers.py
index 908c1c79644d0ec44bcef36fe986f160f7e06cd5..677288070082872453325d2bf7ad8a8fd5072cca 100644
--- a/api/tests/playlists/test_serializers.py
+++ b/api/tests/playlists/test_serializers.py
@@ -1,31 +1,26 @@
-from funkwhale_api.playlists import models
-from funkwhale_api.playlists import serializers
+from funkwhale_api.playlists import models, serializers
 
 
 def test_cannot_max_500_tracks_per_playlist(factories, preferences):
-    preferences['playlists__max_tracks'] = 2
-    playlist = factories['playlists.Playlist']()
-    plts = factories['playlists.PlaylistTrack'].create_batch(
-        size=2, playlist=playlist)
-    track = factories['music.Track']()
-    serializer = serializers.PlaylistTrackWriteSerializer(data={
-        'playlist': playlist.pk,
-        'track': track.pk,
-    })
+    preferences["playlists__max_tracks"] = 2
+    playlist = factories["playlists.Playlist"]()
+    factories["playlists.PlaylistTrack"].create_batch(size=2, playlist=playlist)
+    track = factories["music.Track"]()
+    serializer = serializers.PlaylistTrackWriteSerializer(
+        data={"playlist": playlist.pk, "track": track.pk}
+    )
 
     assert serializer.is_valid() is False
-    assert 'playlist' in serializer.errors
+    assert "playlist" in serializer.errors
 
 
 def test_create_insert_is_called_when_index_is_None(factories, mocker):
-    insert = mocker.spy(models.Playlist, 'insert')
-    playlist = factories['playlists.Playlist']()
-    track = factories['music.Track']()
-    serializer = serializers.PlaylistTrackWriteSerializer(data={
-        'playlist': playlist.pk,
-        'track': track.pk,
-        'index': None,
-    })
+    insert = mocker.spy(models.Playlist, "insert")
+    playlist = factories["playlists.Playlist"]()
+    track = factories["music.Track"]()
+    serializer = serializers.PlaylistTrackWriteSerializer(
+        data={"playlist": playlist.pk, "track": track.pk, "index": None}
+    )
     assert serializer.is_valid() is True
 
     plt = serializer.save()
@@ -34,16 +29,14 @@ def test_create_insert_is_called_when_index_is_None(factories, mocker):
 
 
 def test_create_insert_is_called_when_index_is_provided(factories, mocker):
-    playlist = factories['playlists.Playlist']()
-    first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0)
-    insert = mocker.spy(models.Playlist, 'insert')
-    factories['playlists.Playlist']()
-    track = factories['music.Track']()
-    serializer = serializers.PlaylistTrackWriteSerializer(data={
-        'playlist': playlist.pk,
-        'track': track.pk,
-        'index': 0,
-    })
+    playlist = factories["playlists.Playlist"]()
+    first = factories["playlists.PlaylistTrack"](playlist=playlist, index=0)
+    insert = mocker.spy(models.Playlist, "insert")
+    factories["playlists.Playlist"]()
+    track = factories["music.Track"]()
+    serializer = serializers.PlaylistTrackWriteSerializer(
+        data={"playlist": playlist.pk, "track": track.pk, "index": 0}
+    )
     assert serializer.is_valid() is True
 
     plt = serializer.save()
@@ -54,17 +47,15 @@ def test_create_insert_is_called_when_index_is_provided(factories, mocker):
 
 
 def test_update_insert_is_called_when_index_is_provided(factories, mocker):
-    playlist = factories['playlists.Playlist']()
-    first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0)
-    second = factories['playlists.PlaylistTrack'](playlist=playlist, index=1)
-    insert = mocker.spy(models.Playlist, 'insert')
-    factories['playlists.Playlist']()
-    track = factories['music.Track']()
-    serializer = serializers.PlaylistTrackWriteSerializer(second, data={
-        'playlist': playlist.pk,
-        'track': second.track.pk,
-        'index': 0,
-    })
+    playlist = factories["playlists.Playlist"]()
+    first = factories["playlists.PlaylistTrack"](playlist=playlist, index=0)
+    second = factories["playlists.PlaylistTrack"](playlist=playlist, index=1)
+    insert = mocker.spy(models.Playlist, "insert")
+    factories["playlists.Playlist"]()
+    factories["music.Track"]()
+    serializer = serializers.PlaylistTrackWriteSerializer(
+        second, data={"playlist": playlist.pk, "track": second.track.pk, "index": 0}
+    )
     assert serializer.is_valid() is True
 
     plt = serializer.save()
diff --git a/api/tests/playlists/test_views.py b/api/tests/playlists/test_views.py
index 44d0608210d170b9df6b1e830cc1400ef148b7b5..e7b47c7a2d924032e55d324b4bdc05d2e8f8341d 100644
--- a/api/tests/playlists/test_views.py
+++ b/api/tests/playlists/test_views.py
@@ -1,72 +1,59 @@
-import json
 import pytest
-
 from django.urls import reverse
-from django.core.exceptions import ValidationError
-from django.utils import timezone
 
-from funkwhale_api.playlists import models
-from funkwhale_api.playlists import serializers
+from funkwhale_api.playlists import models, serializers
 
 
 def test_can_create_playlist_via_api(logged_in_api_client):
-    url = reverse('api:v1:playlists-list')
-    data = {
-        'name': 'test',
-        'privacy_level': 'everyone'
-    }
+    url = reverse("api:v1:playlists-list")
+    data = {"name": "test", "privacy_level": "everyone"}
 
-    response = logged_in_api_client.post(url, data)
+    logged_in_api_client.post(url, data)
 
-    playlist = logged_in_api_client.user.playlists.latest('id')
-    assert playlist.name == 'test'
-    assert playlist.privacy_level == 'everyone'
+    playlist = logged_in_api_client.user.playlists.latest("id")
+    assert playlist.name == "test"
+    assert playlist.privacy_level == "everyone"
 
 
 def test_serializer_includes_tracks_count(factories, logged_in_api_client):
-    playlist = factories['playlists.Playlist']()
-    plt = factories['playlists.PlaylistTrack'](playlist=playlist)
+    playlist = factories["playlists.Playlist"]()
+    factories["playlists.PlaylistTrack"](playlist=playlist)
 
-    url = reverse('api:v1:playlists-detail', kwargs={'pk': playlist.pk})
+    url = reverse("api:v1:playlists-detail", kwargs={"pk": playlist.pk})
     response = logged_in_api_client.get(url)
 
-    assert response.data['tracks_count'] == 1
+    assert response.data["tracks_count"] == 1
 
 
 def test_playlist_inherits_user_privacy(logged_in_api_client):
-    url = reverse('api:v1:playlists-list')
+    url = reverse("api:v1:playlists-list")
     user = logged_in_api_client.user
-    user.privacy_level = 'me'
+    user.privacy_level = "me"
     user.save()
 
-    data = {
-        'name': 'test',
-    }
+    data = {"name": "test"}
 
-    response = logged_in_api_client.post(url, data)
-    playlist = user.playlists.latest('id')
+    logged_in_api_client.post(url, data)
+    playlist = user.playlists.latest("id")
     assert playlist.privacy_level == user.privacy_level
 
 
 def test_can_add_playlist_track_via_api(factories, logged_in_api_client):
-    tracks = factories['music.Track'].create_batch(5)
-    playlist = factories['playlists.Playlist'](user=logged_in_api_client.user)
-    url = reverse('api:v1:playlist-tracks-list')
-    data = {
-        'playlist': playlist.pk,
-        'track': tracks[0].pk
-    }
+    tracks = factories["music.Track"].create_batch(5)
+    playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
+    url = reverse("api:v1:playlist-tracks-list")
+    data = {"playlist": playlist.pk, "track": tracks[0].pk}
 
     response = logged_in_api_client.post(url, data)
     assert response.status_code == 201
-    plts = logged_in_api_client.user.playlists.latest('id').playlist_tracks.all()
+    plts = logged_in_api_client.user.playlists.latest("id").playlist_tracks.all()
     assert plts.first().track == tracks[0]
 
 
-@pytest.mark.parametrize('name,method', [
-    ('api:v1:playlist-tracks-list', 'post'),
-    ('api:v1:playlists-list', 'post'),
-])
+@pytest.mark.parametrize(
+    "name,method",
+    [("api:v1:playlist-tracks-list", "post"), ("api:v1:playlists-list", "post")],
+)
 def test_url_requires_login(name, method, factories, api_client):
     url = reverse(name)
 
@@ -75,29 +62,24 @@ def test_url_requires_login(name, method, factories, api_client):
     assert response.status_code == 401
 
 
-def test_only_can_add_track_on_own_playlist_via_api(
-        factories, logged_in_api_client):
-    track = factories['music.Track']()
-    playlist = factories['playlists.Playlist']()
-    url = reverse('api:v1:playlist-tracks-list')
-    data = {
-        'playlist': playlist.pk,
-        'track': track.pk
-    }
+def test_only_can_add_track_on_own_playlist_via_api(factories, logged_in_api_client):
+    track = factories["music.Track"]()
+    playlist = factories["playlists.Playlist"]()
+    url = reverse("api:v1:playlist-tracks-list")
+    data = {"playlist": playlist.pk, "track": track.pk}
 
     response = logged_in_api_client.post(url, data)
     assert response.status_code == 400
     assert playlist.playlist_tracks.count() == 0
 
 
-def test_deleting_plt_updates_indexes(
-        mocker, factories, logged_in_api_client):
-    remove = mocker.spy(models.Playlist, 'remove')
-    track = factories['music.Track']()
-    plt = factories['playlists.PlaylistTrack'](
-        index=0,
-        playlist__user=logged_in_api_client.user)
-    url = reverse('api:v1:playlist-tracks-detail', kwargs={'pk': plt.pk})
+def test_deleting_plt_updates_indexes(mocker, factories, logged_in_api_client):
+    remove = mocker.spy(models.Playlist, "remove")
+    factories["music.Track"]()
+    plt = factories["playlists.PlaylistTrack"](
+        index=0, playlist__user=logged_in_api_client.user
+    )
+    url = reverse("api:v1:playlist-tracks-detail", kwargs={"pk": plt.pk})
 
     response = logged_in_api_client.delete(url)
 
@@ -105,97 +87,93 @@ def test_deleting_plt_updates_indexes(
     remove.assert_called_once_with(plt.playlist, 0)
 
 
-@pytest.mark.parametrize('level', ['instance', 'me', 'followers'])
+@pytest.mark.parametrize("level", ["instance", "me", "followers"])
 def test_playlist_privacy_respected_in_list_anon(
-        preferences, level, factories, api_client):
-    preferences['common__api_authentication_required'] = False
-    factories['playlists.Playlist'](privacy_level=level)
-    url = reverse('api:v1:playlists-list')
+    preferences, level, factories, api_client
+):
+    preferences["common__api_authentication_required"] = False
+    factories["playlists.Playlist"](privacy_level=level)
+    url = reverse("api:v1:playlists-list")
     response = api_client.get(url)
 
-    assert response.data['count'] == 0
+    assert response.data["count"] == 0
 
 
-@pytest.mark.parametrize('method', ['PUT', 'PATCH', 'DELETE'])
+@pytest.mark.parametrize("method", ["PUT", "PATCH", "DELETE"])
 def test_only_owner_can_edit_playlist(method, factories, logged_in_api_client):
-    playlist = factories['playlists.Playlist']()
-    url = reverse('api:v1:playlists-detail', kwargs={'pk': playlist.pk})
+    playlist = factories["playlists.Playlist"]()
+    url = reverse("api:v1:playlists-detail", kwargs={"pk": playlist.pk})
     response = getattr(logged_in_api_client, method.lower())(url)
 
     assert response.status_code == 404
 
 
-@pytest.mark.parametrize('method', ['PUT', 'PATCH', 'DELETE'])
-def test_only_owner_can_edit_playlist_track(
-        method, factories, logged_in_api_client):
-    plt = factories['playlists.PlaylistTrack']()
-    url = reverse('api:v1:playlist-tracks-detail', kwargs={'pk': plt.pk})
+@pytest.mark.parametrize("method", ["PUT", "PATCH", "DELETE"])
+def test_only_owner_can_edit_playlist_track(method, factories, logged_in_api_client):
+    plt = factories["playlists.PlaylistTrack"]()
+    url = reverse("api:v1:playlist-tracks-detail", kwargs={"pk": plt.pk})
     response = getattr(logged_in_api_client, method.lower())(url)
 
     assert response.status_code == 404
 
 
-@pytest.mark.parametrize('level', ['instance', 'me', 'followers'])
+@pytest.mark.parametrize("level", ["instance", "me", "followers"])
 def test_playlist_track_privacy_respected_in_list_anon(
-        level, factories, api_client, preferences):
-    preferences['common__api_authentication_required'] = False
-    factories['playlists.PlaylistTrack'](playlist__privacy_level=level)
-    url = reverse('api:v1:playlist-tracks-list')
+    level, factories, api_client, preferences
+):
+    preferences["common__api_authentication_required"] = False
+    factories["playlists.PlaylistTrack"](playlist__privacy_level=level)
+    url = reverse("api:v1:playlist-tracks-list")
     response = api_client.get(url)
 
-    assert response.data['count'] == 0
+    assert response.data["count"] == 0
 
 
-@pytest.mark.parametrize('level', ['instance', 'me', 'followers'])
-def test_can_list_tracks_from_playlist(
-        level, factories, logged_in_api_client):
-    plt = factories['playlists.PlaylistTrack'](
-        playlist__user=logged_in_api_client.user)
-    url = reverse('api:v1:playlists-tracks', kwargs={'pk': plt.playlist.pk})
+@pytest.mark.parametrize("level", ["instance", "me", "followers"])
+def test_can_list_tracks_from_playlist(level, factories, logged_in_api_client):
+    plt = factories["playlists.PlaylistTrack"](playlist__user=logged_in_api_client.user)
+    url = reverse("api:v1:playlists-tracks", kwargs={"pk": plt.playlist.pk})
     response = logged_in_api_client.get(url)
     serialized_plt = serializers.PlaylistTrackSerializer(plt).data
 
-    assert response.data['count'] == 1
-    assert response.data['results'][0] == serialized_plt
+    assert response.data["count"] == 1
+    assert response.data["results"][0] == serialized_plt
 
 
 def test_can_add_multiple_tracks_at_once_via_api(
-        factories, mocker, logged_in_api_client):
-    playlist = factories['playlists.Playlist'](user=logged_in_api_client.user)
-    tracks = factories['music.Track'].create_batch(size=5)
+    factories, mocker, logged_in_api_client
+):
+    playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
+    tracks = factories["music.Track"].create_batch(size=5)
     track_ids = [t.id for t in tracks]
-    mocker.spy(playlist, 'insert_many')
-    url = reverse('api:v1:playlists-add', kwargs={'pk': playlist.pk})
-    response = logged_in_api_client.post(url, {'tracks': track_ids})
+    mocker.spy(playlist, "insert_many")
+    url = reverse("api:v1:playlists-add", kwargs={"pk": playlist.pk})
+    response = logged_in_api_client.post(url, {"tracks": track_ids})
 
     assert response.status_code == 201
     assert playlist.playlist_tracks.count() == len(track_ids)
 
-    for plt in playlist.playlist_tracks.order_by('index'):
-        assert response.data['results'][plt.index]['id'] == plt.id
+    for plt in playlist.playlist_tracks.order_by("index"):
+        assert response.data["results"][plt.index]["id"] == plt.id
         assert plt.track == tracks[plt.index]
 
 
-def test_can_clear_playlist_from_api(
-        factories, mocker, logged_in_api_client):
-    playlist = factories['playlists.Playlist'](user=logged_in_api_client.user)
-    plts = factories['playlists.PlaylistTrack'].create_batch(
-        size=5, playlist=playlist)
-    url = reverse('api:v1:playlists-clear', kwargs={'pk': playlist.pk})
+def test_can_clear_playlist_from_api(factories, mocker, logged_in_api_client):
+    playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
+    factories["playlists.PlaylistTrack"].create_batch(size=5, playlist=playlist)
+    url = reverse("api:v1:playlists-clear", kwargs={"pk": playlist.pk})
     response = logged_in_api_client.delete(url)
 
     assert response.status_code == 204
     assert playlist.playlist_tracks.count() == 0
 
 
-def test_update_playlist_from_api(
-        factories, mocker, logged_in_api_client):
-    playlist = factories['playlists.Playlist'](user=logged_in_api_client.user)
-    plts = factories['playlists.PlaylistTrack'].create_batch(
-        size=5, playlist=playlist)
-    url = reverse('api:v1:playlists-detail', kwargs={'pk': playlist.pk})
-    response = logged_in_api_client.patch(url, {'name': 'test'})
+def test_update_playlist_from_api(factories, mocker, logged_in_api_client):
+    playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
+    factories["playlists.PlaylistTrack"].create_batch(size=5, playlist=playlist)
+    url = reverse("api:v1:playlists-detail", kwargs={"pk": playlist.pk})
+    response = logged_in_api_client.patch(url, {"name": "test"})
     playlist.refresh_from_db()
 
     assert response.status_code == 200
-    assert response.data['user']['username'] == playlist.user.username
+    assert response.data["user"]["username"] == playlist.user.username
diff --git a/api/tests/radios/test_api.py b/api/tests/radios/test_api.py
index 66bf6052d539ef1745c45c277385b099a18c51d3..0ddebe3878051113f4bbdfd2776839a58fbf3530 100644
--- a/api/tests/radios/test_api.py
+++ b/api/tests/radios/test_api.py
@@ -1,159 +1,131 @@
-import json
-import pytest
-
 from django.urls import reverse
 
 from funkwhale_api.music.serializers import TrackSerializer
-from funkwhale_api.radios import filters
-from funkwhale_api.radios import serializers
+from funkwhale_api.radios import filters, serializers
 
 
-def test_can_list_config_options(logged_in_client):
-    url = reverse('api:v1:radios:radios-filters')
-    response = logged_in_client.get(url)
+def test_can_list_config_options(logged_in_api_client):
+    url = reverse("api:v1:radios:radios-filters")
+    response = logged_in_api_client.get(url)
 
     assert response.status_code == 200
 
-    payload = json.loads(response.content.decode('utf-8'))
+    payload = response.data
 
     expected = [f for f in filters.registry.values() if f.expose_in_api]
     assert len(payload) == len(expected)
 
 
-def test_can_validate_config(logged_in_client, factories):
-    artist1 = factories['music.Artist']()
-    artist2 = factories['music.Artist']()
-    factories['music.Track'].create_batch(3, artist=artist1)
-    factories['music.Track'].create_batch(3, artist=artist2)
-    candidates = artist1.tracks.order_by('pk')
-    f = {
-        'filters': [
-            {'type': 'artist', 'ids': [artist1.pk]}
-        ]
-    }
-    url = reverse('api:v1:radios:radios-validate')
-    response = logged_in_client.post(
-        url,
-        json.dumps(f),
-        content_type="application/json")
+def test_can_validate_config(logged_in_api_client, factories):
+    artist1 = factories["music.Artist"]()
+    artist2 = factories["music.Artist"]()
+    factories["music.Track"].create_batch(3, artist=artist1)
+    factories["music.Track"].create_batch(3, artist=artist2)
+    candidates = artist1.tracks.order_by("pk")
+    f = {"filters": [{"type": "artist", "ids": [artist1.pk]}]}
+    url = reverse("api:v1:radios:radios-validate")
+    response = logged_in_api_client.post(url, f, format="json")
 
     assert response.status_code == 200
 
-    payload = json.loads(response.content.decode('utf-8'))
+    payload = response.data
 
     expected = {
-        'count': candidates.count(),
-        'sample': TrackSerializer(candidates, many=True).data
+        "count": candidates.count(),
+        "sample": TrackSerializer(candidates, many=True).data,
     }
-    assert payload['filters'][0]['candidates'] == expected
-    assert payload['filters'][0]['errors'] == []
+    assert payload["filters"][0]["candidates"] == expected
+    assert payload["filters"][0]["errors"] == []
 
 
-def test_can_validate_config_with_wrong_config(logged_in_client, factories):
-    f = {
-        'filters': [
-            {'type': 'artist', 'ids': [999]}
-        ]
-    }
-    url = reverse('api:v1:radios:radios-validate')
-    response = logged_in_client.post(
-        url,
-        json.dumps(f),
-        content_type="application/json")
+def test_can_validate_config_with_wrong_config(logged_in_api_client, factories):
+    f = {"filters": [{"type": "artist", "ids": [999]}]}
+    url = reverse("api:v1:radios:radios-validate")
+    response = logged_in_api_client.post(url, f, format="json")
 
     assert response.status_code == 200
 
-    payload = json.loads(response.content.decode('utf-8'))
+    payload = response.data
 
-    expected = {
-        'count': None,
-        'sample': None
-    }
-    assert payload['filters'][0]['candidates'] == expected
-    assert len(payload['filters'][0]['errors']) == 1
+    expected = {"count": None, "sample": None}
+    assert payload["filters"][0]["candidates"] == expected
+    assert len(payload["filters"][0]["errors"]) == 1
 
 
-def test_saving_radio_sets_user(logged_in_client, factories):
-    artist = factories['music.Artist']()
-    f = {
-        'name': 'Test',
-        'config': [
-            {'type': 'artist', 'ids': [artist.pk]}
-        ]
-    }
-    url = reverse('api:v1:radios:radios-list')
-    response = logged_in_client.post(
-        url,
-        json.dumps(f),
-        content_type="application/json")
+def test_saving_radio_sets_user(logged_in_api_client, factories):
+    artist = factories["music.Artist"]()
+    f = {"name": "Test", "config": [{"type": "artist", "ids": [artist.pk]}]}
+    url = reverse("api:v1:radios:radios-list")
+    response = logged_in_api_client.post(url, f, format="json")
 
     assert response.status_code == 201
 
-    radio = logged_in_client.user.radios.latest('id')
-    assert radio.name == 'Test'
-    assert radio.user == logged_in_client.user
+    radio = logged_in_api_client.user.radios.latest("id")
+    assert radio.name == "Test"
+    assert radio.user == logged_in_api_client.user
 
 
-def test_user_can_detail_his_radio(logged_in_client, factories):
-    radio = factories['radios.Radio'](user=logged_in_client.user)
-    url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk})
-    response = logged_in_client.get(url)
+def test_user_can_detail_his_radio(logged_in_api_client, factories):
+    radio = factories["radios.Radio"](user=logged_in_api_client.user)
+    url = reverse("api:v1:radios:radios-detail", kwargs={"pk": radio.pk})
+    response = logged_in_api_client.get(url)
 
     assert response.status_code == 200
 
 
-def test_user_can_detail_public_radio(logged_in_client, factories):
-    radio = factories['radios.Radio'](is_public=True)
-    url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk})
-    response = logged_in_client.get(url)
+def test_user_can_detail_public_radio(logged_in_api_client, factories):
+    radio = factories["radios.Radio"](is_public=True)
+    url = reverse("api:v1:radios:radios-detail", kwargs={"pk": radio.pk})
+    response = logged_in_api_client.get(url)
 
     assert response.status_code == 200
 
 
-def test_user_cannot_detail_someone_else_radio(logged_in_client, factories):
-    radio = factories['radios.Radio'](is_public=False)
-    url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk})
-    response = logged_in_client.get(url)
+def test_user_cannot_detail_someone_else_radio(logged_in_api_client, factories):
+    radio = factories["radios.Radio"](is_public=False)
+    url = reverse("api:v1:radios:radios-detail", kwargs={"pk": radio.pk})
+    response = logged_in_api_client.get(url)
 
     assert response.status_code == 404
 
 
-def test_user_can_edit_his_radio(logged_in_client, factories):
-    radio = factories['radios.Radio'](user=logged_in_client.user)
-    url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk})
-    response = logged_in_client.put(
-        url,
-        json.dumps({'name': 'new', 'config': []}),
-        content_type="application/json")
+def test_user_can_edit_his_radio(logged_in_api_client, factories):
+    radio = factories["radios.Radio"](user=logged_in_api_client.user)
+    url = reverse("api:v1:radios:radios-detail", kwargs={"pk": radio.pk})
+    response = logged_in_api_client.put(
+        url, {"name": "new", "config": []}, format="json"
+    )
 
     radio.refresh_from_db()
     assert response.status_code == 200
-    assert radio.name == 'new'
+    assert radio.name == "new"
 
 
-def test_user_cannot_edit_someone_else_radio(logged_in_client, factories):
-    radio = factories['radios.Radio']()
-    url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk})
-    response = logged_in_client.put(
-        url,
-        json.dumps({'name': 'new', 'config': []}),
-        content_type="application/json")
+def test_user_cannot_edit_someone_else_radio(logged_in_api_client, factories):
+    radio = factories["radios.Radio"](is_public=True)
+    url = reverse("api:v1:radios:radios-detail", kwargs={"pk": radio.pk})
+    response = logged_in_api_client.put(
+        url, {"name": "new", "config": []}, format="json"
+    )
+
+    assert response.status_code == 404
+
+
+def test_user_cannot_delete_someone_else_radio(logged_in_api_client, factories):
+    radio = factories["radios.Radio"](is_public=True)
+    url = reverse("api:v1:radios:radios-detail", kwargs={"pk": radio.pk})
+    response = logged_in_api_client.delete(url)
 
     assert response.status_code == 404
 
 
 def test_clean_config_is_called_on_serializer_save(mocker, factories):
-    user = factories['users.User']()
-    artist = factories['music.Artist']()
-    data= {
-        'name': 'Test',
-        'config': [
-            {'type': 'artist', 'ids': [artist.pk]}
-        ]
-    }
-    spied = mocker.spy(filters.registry['artist'], 'clean_config')
+    user = factories["users.User"]()
+    artist = factories["music.Artist"]()
+    data = {"name": "Test", "config": [{"type": "artist", "ids": [artist.pk]}]}
+    spied = mocker.spy(filters.registry["artist"], "clean_config")
     serializer = serializers.RadioSerializer(data=data)
     assert serializer.is_valid()
     instance = serializer.save(user=user)
-    spied.assert_called_once_with(data['config'][0])
-    assert instance.config[0]['names'] == [artist.name]
+    spied.assert_called_once_with(data["config"][0])
+    assert instance.config[0]["names"] == [artist.name]
diff --git a/api/tests/radios/test_filters.py b/api/tests/radios/test_filters.py
index 27166b4ab27d9f6e00dad4da69cea75253844e4e..89bb726aff643c7365b759b510f763b738c738d0 100644
--- a/api/tests/radios/test_filters.py
+++ b/api/tests/radios/test_filters.py
@@ -1,5 +1,4 @@
 import pytest
-
 from django.core.exceptions import ValidationError
 
 from funkwhale_api.music.models import Track
@@ -8,154 +7,147 @@ from funkwhale_api.radios import filters
 
 @filters.registry.register
 class NoopFilter(filters.RadioFilter):
-    code = 'noop'
+    code = "noop"
+
     def get_query(self, candidates, **kwargs):
         return
 
 
 def test_most_simple_radio_does_not_filter_anything(factories):
-    tracks = factories['music.Track'].create_batch(3)
-    radio = factories['radios.Radio'](config=[{'type': 'noop'}])
+    factories["music.Track"].create_batch(3)
+    radio = factories["radios.Radio"](config=[{"type": "noop"}])
 
     assert radio.version == 0
     assert radio.get_candidates().count() == 3
 
 
-
 def test_filter_can_use_custom_queryset(factories):
-    tracks = factories['music.Track'].create_batch(3)
+    tracks = factories["music.Track"].create_batch(3)
     candidates = Track.objects.filter(pk=tracks[0].pk)
 
-    qs = filters.run([{'type': 'noop'}], candidates=candidates)
+    qs = filters.run([{"type": "noop"}], candidates=candidates)
     assert qs.count() == 1
     assert qs.first() == tracks[0]
 
 
 def test_filter_on_tag(factories):
-    tracks = factories['music.Track'].create_batch(3, tags=['metal'])
-    factories['music.Track'].create_batch(3, tags=['pop'])
+    tracks = factories["music.Track"].create_batch(3, tags=["metal"])
+    factories["music.Track"].create_batch(3, tags=["pop"])
     expected = tracks
-    f = [
-        {'type': 'tag', 'names': ['metal']}
-    ]
+    f = [{"type": "tag", "names": ["metal"]}]
 
     candidates = filters.run(f)
-    assert list(candidates.order_by('pk')) == expected
+    assert list(candidates.order_by("pk")) == expected
 
 
 def test_filter_on_artist(factories):
-    artist1 = factories['music.Artist']()
-    artist2 = factories['music.Artist']()
-    factories['music.Track'].create_batch(3, artist=artist1)
-    factories['music.Track'].create_batch(3, artist=artist2)
-    expected = list(artist1.tracks.order_by('pk'))
-    f = [
-        {'type': 'artist', 'ids': [artist1.pk]}
-    ]
+    artist1 = factories["music.Artist"]()
+    artist2 = factories["music.Artist"]()
+    factories["music.Track"].create_batch(3, artist=artist1)
+    factories["music.Track"].create_batch(3, artist=artist2)
+    expected = list(artist1.tracks.order_by("pk"))
+    f = [{"type": "artist", "ids": [artist1.pk]}]
 
     candidates = filters.run(f)
-    assert list(candidates.order_by('pk')) == expected
+    assert list(candidates.order_by("pk")) == expected
 
 
 def test_can_combine_with_or(factories):
-    artist1 = factories['music.Artist']()
-    artist2 = factories['music.Artist']()
-    artist3 = factories['music.Artist']()
-    factories['music.Track'].create_batch(3, artist=artist1)
-    factories['music.Track'].create_batch(3, artist=artist2)
-    factories['music.Track'].create_batch(3, artist=artist3)
-    expected = Track.objects.exclude(artist=artist3).order_by('pk')
+    artist1 = factories["music.Artist"]()
+    artist2 = factories["music.Artist"]()
+    artist3 = factories["music.Artist"]()
+    factories["music.Track"].create_batch(3, artist=artist1)
+    factories["music.Track"].create_batch(3, artist=artist2)
+    factories["music.Track"].create_batch(3, artist=artist3)
+    expected = Track.objects.exclude(artist=artist3).order_by("pk")
     f = [
-        {'type': 'artist', 'ids': [artist1.pk]},
-        {'type': 'artist', 'ids': [artist2.pk], 'operator': 'or'},
+        {"type": "artist", "ids": [artist1.pk]},
+        {"type": "artist", "ids": [artist2.pk], "operator": "or"},
     ]
 
     candidates = filters.run(f)
-    assert list(candidates.order_by('pk')) == list(expected)
+    assert list(candidates.order_by("pk")) == list(expected)
 
 
 def test_can_combine_with_and(factories):
-    artist1 = factories['music.Artist']()
-    artist2 = factories['music.Artist']()
-    metal_tracks = factories['music.Track'].create_batch(
-        2, artist=artist1, tags=['metal'])
-    factories['music.Track'].create_batch(2, artist=artist1, tags=['pop'])
-    factories['music.Track'].create_batch(3, artist=artist2)
+    artist1 = factories["music.Artist"]()
+    artist2 = factories["music.Artist"]()
+    metal_tracks = factories["music.Track"].create_batch(
+        2, artist=artist1, tags=["metal"]
+    )
+    factories["music.Track"].create_batch(2, artist=artist1, tags=["pop"])
+    factories["music.Track"].create_batch(3, artist=artist2)
     expected = metal_tracks
     f = [
-        {'type': 'artist', 'ids': [artist1.pk]},
-        {'type': 'tag', 'names': ['metal'], 'operator': 'and'},
+        {"type": "artist", "ids": [artist1.pk]},
+        {"type": "tag", "names": ["metal"], "operator": "and"},
     ]
 
     candidates = filters.run(f)
-    assert list(candidates.order_by('pk')) == list(expected)
+    assert list(candidates.order_by("pk")) == list(expected)
 
 
 def test_can_negate(factories):
-    artist1 = factories['music.Artist']()
-    artist2 = factories['music.Artist']()
-    factories['music.Track'].create_batch(3, artist=artist1)
-    factories['music.Track'].create_batch(3, artist=artist2)
-    expected = artist2.tracks.order_by('pk')
-    f = [
-        {'type': 'artist', 'ids': [artist1.pk], 'not': True},
-    ]
+    artist1 = factories["music.Artist"]()
+    artist2 = factories["music.Artist"]()
+    factories["music.Track"].create_batch(3, artist=artist1)
+    factories["music.Track"].create_batch(3, artist=artist2)
+    expected = artist2.tracks.order_by("pk")
+    f = [{"type": "artist", "ids": [artist1.pk], "not": True}]
 
     candidates = filters.run(f)
-    assert list(candidates.order_by('pk')) == list(expected)
+    assert list(candidates.order_by("pk")) == list(expected)
 
 
 def test_can_group(factories):
-    artist1 = factories['music.Artist']()
-    artist2 = factories['music.Artist']()
-    factories['music.Track'].create_batch(2, artist=artist1)
-    t1 = factories['music.Track'].create_batch(
-        2, artist=artist1, tags=['metal'])
-    factories['music.Track'].create_batch(2, artist=artist2)
-    t2 = factories['music.Track'].create_batch(
-        2, artist=artist2, tags=['metal'])
-    factories['music.Track'].create_batch(2, tags=['metal'])
+    artist1 = factories["music.Artist"]()
+    artist2 = factories["music.Artist"]()
+    factories["music.Track"].create_batch(2, artist=artist1)
+    t1 = factories["music.Track"].create_batch(2, artist=artist1, tags=["metal"])
+    factories["music.Track"].create_batch(2, artist=artist2)
+    t2 = factories["music.Track"].create_batch(2, artist=artist2, tags=["metal"])
+    factories["music.Track"].create_batch(2, tags=["metal"])
     expected = t1 + t2
     f = [
-        {'type': 'tag', 'names': ['metal']},
-        {'type': 'group', 'operator': 'and', 'filters': [
-            {'type': 'artist', 'ids': [artist1.pk], 'operator': 'or'},
-            {'type': 'artist', 'ids': [artist2.pk], 'operator': 'or'},
-        ]}
+        {"type": "tag", "names": ["metal"]},
+        {
+            "type": "group",
+            "operator": "and",
+            "filters": [
+                {"type": "artist", "ids": [artist1.pk], "operator": "or"},
+                {"type": "artist", "ids": [artist2.pk], "operator": "or"},
+            ],
+        },
     ]
 
     candidates = filters.run(f)
-    assert list(candidates.order_by('pk')) == list(expected)
+    assert list(candidates.order_by("pk")) == list(expected)
 
 
 def test_artist_filter_clean_config(factories):
-    artist1 = factories['music.Artist']()
-    artist2 = factories['music.Artist']()
+    artist1 = factories["music.Artist"]()
+    artist2 = factories["music.Artist"]()
 
-    config = filters.clean_config(
-        {'type': 'artist', 'ids': [artist2.pk, artist1.pk]})
+    config = filters.clean_config({"type": "artist", "ids": [artist2.pk, artist1.pk]})
 
     expected = {
-        'type': 'artist',
-        'ids': [artist1.pk, artist2.pk],
-        'names': [artist1.name, artist2.name]
+        "type": "artist",
+        "ids": [artist1.pk, artist2.pk],
+        "names": [artist1.name, artist2.name],
     }
     assert filters.clean_config(config) == expected
 
 
 def test_can_check_artist_filter(factories):
-    artist = factories['music.Artist']()
+    artist = factories["music.Artist"]()
 
-    assert filters.validate({'type': 'artist', 'ids': [artist.pk]})
+    assert filters.validate({"type": "artist", "ids": [artist.pk]})
     with pytest.raises(ValidationError):
-        filters.validate({'type': 'artist', 'ids': [artist.pk + 1]})
+        filters.validate({"type": "artist", "ids": [artist.pk + 1]})
 
 
 def test_can_check_operator():
-    assert filters.validate(
-        {'type': 'group', 'operator': 'or', 'filters': []})
-    assert filters.validate(
-        {'type': 'group', 'operator': 'and', 'filters': []})
+    assert filters.validate({"type": "group", "operator": "or", "filters": []})
+    assert filters.validate({"type": "group", "operator": "and", "filters": []})
     with pytest.raises(ValidationError):
-        assert filters.validate(
-            {'type': 'group', 'operator': 'nope', 'filters': []})
+        assert filters.validate({"type": "group", "operator": "nope", "filters": []})
diff --git a/api/tests/radios/test_radios.py b/api/tests/radios/test_radios.py
index b166b648c574f8050f574151aa08e9f3282b49ab..e218ced907de421873bcc22e80a16b4fbf7e228e 100644
--- a/api/tests/radios/test_radios.py
+++ b/api/tests/radios/test_radios.py
@@ -1,15 +1,12 @@
 import json
 import random
-import pytest
 
-from django.urls import reverse
+import pytest
 from django.core.exceptions import ValidationError
+from django.urls import reverse
 
-
-from funkwhale_api.radios import radios
-from funkwhale_api.radios import models
-from funkwhale_api.radios import serializers
 from funkwhale_api.favorites.models import TrackFavorite
+from funkwhale_api.radios import models, radios, serializers
 
 
 def test_can_pick_track_from_choices():
@@ -51,9 +48,9 @@ def test_can_pick_by_weight():
 
 
 def test_can_get_choices_for_favorites_radio(factories):
-    files = factories['music.TrackFile'].create_batch(10)
+    files = factories["music.TrackFile"].create_batch(10)
     tracks = [f.track for f in files]
-    user = factories['users.User']()
+    user = factories["users.User"]()
     for i in range(5):
         TrackFavorite.add(track=random.choice(tracks), user=user)
 
@@ -71,71 +68,65 @@ def test_can_get_choices_for_favorites_radio(factories):
 
 
 def test_can_get_choices_for_custom_radio(factories):
-    artist = factories['music.Artist']()
-    files = factories['music.TrackFile'].create_batch(
-        5, track__artist=artist)
+    artist = factories["music.Artist"]()
+    files = factories["music.TrackFile"].create_batch(5, track__artist=artist)
     tracks = [f.track for f in files]
-    wrong_files = factories['music.TrackFile'].create_batch(5)
-    wrong_tracks = [f.track for f in wrong_files]
+    factories["music.TrackFile"].create_batch(5)
 
-    session = factories['radios.CustomRadioSession'](
-        custom_radio__config=[{'type': 'artist', 'ids': [artist.pk]}]
+    session = factories["radios.CustomRadioSession"](
+        custom_radio__config=[{"type": "artist", "ids": [artist.pk]}]
     )
     choices = session.radio.get_choices()
 
     expected = [t.pk for t in tracks]
-    assert list(choices.values_list('id', flat=True)) == expected
+    assert list(choices.values_list("id", flat=True)) == expected
 
 
 def test_cannot_start_custom_radio_if_not_owner_or_not_public(factories):
-    user = factories['users.User']()
-    artist = factories['music.Artist']()
-    radio = factories['radios.Radio'](
-        config=[{'type': 'artist', 'ids': [artist.pk]}]
-    )
+    user = factories["users.User"]()
+    artist = factories["music.Artist"]()
+    radio = factories["radios.Radio"](config=[{"type": "artist", "ids": [artist.pk]}])
     serializer = serializers.RadioSessionSerializer(
-        data={
-            'radio_type': 'custom', 'custom_radio': radio.pk, 'user': user.pk}
+        data={"radio_type": "custom", "custom_radio": radio.pk, "user": user.pk}
     )
     message = "You don't have access to this radio"
     assert not serializer.is_valid()
-    assert message in serializer.errors['non_field_errors']
+    assert message in serializer.errors["non_field_errors"]
 
 
 def test_can_start_custom_radio_from_api(logged_in_client, factories):
-    artist = factories['music.Artist']()
-    radio = factories['radios.Radio'](
-        config=[{'type': 'artist', 'ids': [artist.pk]}],
-        user=logged_in_client.user
+    artist = factories["music.Artist"]()
+    radio = factories["radios.Radio"](
+        config=[{"type": "artist", "ids": [artist.pk]}], user=logged_in_client.user
     )
-    url = reverse('api:v1:radios:sessions-list')
+    url = reverse("api:v1:radios:sessions-list")
     response = logged_in_client.post(
-        url, {'radio_type': 'custom', 'custom_radio': radio.pk})
+        url, {"radio_type": "custom", "custom_radio": radio.pk}
+    )
     assert response.status_code == 201
-    session = radio.sessions.latest('id')
-    assert session.radio_type == 'custom'
+    session = radio.sessions.latest("id")
+    assert session.radio_type == "custom"
     assert session.user == logged_in_client.user
 
 
 def test_can_use_radio_session_to_filter_choices(factories):
-    files = factories['music.TrackFile'].create_batch(30)
-    tracks = [f.track for f in files]
-    user = factories['users.User']()
+    factories["music.TrackFile"].create_batch(30)
+    user = factories["users.User"]()
     radio = radios.RandomRadio()
     session = radio.start_session(user)
 
     for i in range(30):
-        p = radio.pick()
+        radio.pick()
 
     # ensure 30 differents tracks have been suggested
     tracks_id = [
-        session_track.track.pk
-        for session_track in session.session_tracks.all()]
+        session_track.track.pk for session_track in session.session_tracks.all()
+    ]
     assert len(set(tracks_id)) == 30
 
 
 def test_can_restore_radio_from_previous_session(factories):
-    user = factories['users.User']()
+    user = factories["users.User"]()
     radio = radios.RandomRadio()
     session = radio.start_session(user)
 
@@ -144,37 +135,37 @@ def test_can_restore_radio_from_previous_session(factories):
 
 
 def test_can_start_radio_for_logged_in_user(logged_in_client):
-    url = reverse('api:v1:radios:sessions-list')
-    response = logged_in_client.post(url, {'radio_type': 'random'})
-    session = models.RadioSession.objects.latest('id')
-    assert session.radio_type == 'random'
+    url = reverse("api:v1:radios:sessions-list")
+    logged_in_client.post(url, {"radio_type": "random"})
+    session = models.RadioSession.objects.latest("id")
+    assert session.radio_type == "random"
     assert session.user == logged_in_client.user
 
 
 def test_can_get_track_for_session_from_api(factories, logged_in_client):
-    files = factories['music.TrackFile'].create_batch(1)
+    files = factories["music.TrackFile"].create_batch(1)
     tracks = [f.track for f in files]
-    url = reverse('api:v1:radios:sessions-list')
-    response = logged_in_client.post(url, {'radio_type': 'random'})
-    session = models.RadioSession.objects.latest('id')
+    url = reverse("api:v1:radios:sessions-list")
+    response = logged_in_client.post(url, {"radio_type": "random"})
+    session = models.RadioSession.objects.latest("id")
 
-    url = reverse('api:v1:radios:tracks-list')
-    response = logged_in_client.post(url, {'session': session.pk})
-    data = json.loads(response.content.decode('utf-8'))
+    url = reverse("api:v1:radios:tracks-list")
+    response = logged_in_client.post(url, {"session": session.pk})
+    data = json.loads(response.content.decode("utf-8"))
 
-    assert data['track']['id'] == tracks[0].id
-    assert data['position'] == 1
+    assert data["track"]["id"] == tracks[0].id
+    assert data["position"] == 1
 
-    next_track = factories['music.TrackFile']().track
-    response = logged_in_client.post(url, {'session': session.pk})
-    data = json.loads(response.content.decode('utf-8'))
+    next_track = factories["music.TrackFile"]().track
+    response = logged_in_client.post(url, {"session": session.pk})
+    data = json.loads(response.content.decode("utf-8"))
 
-    assert data['track']['id'] == next_track.id
-    assert data['position'] == 2
+    assert data["track"]["id"] == next_track.id
+    assert data["position"] == 2
 
 
 def test_related_object_radio_validate_related_object(factories):
-    user = factories['users.User']()
+    user = factories["users.User"]()
     # cannot start without related object
     radio = radios.ArtistRadio()
     with pytest.raises(ValidationError):
@@ -187,62 +178,58 @@ def test_related_object_radio_validate_related_object(factories):
 
 
 def test_can_start_artist_radio(factories):
-    user = factories['users.User']()
-    artist = factories['music.Artist']()
-    wrong_files = factories['music.TrackFile'].create_batch(5)
-    wrong_tracks = [f.track for f in wrong_files]
-    good_files = factories['music.TrackFile'].create_batch(
-        5, track__artist=artist)
+    user = factories["users.User"]()
+    artist = factories["music.Artist"]()
+    factories["music.TrackFile"].create_batch(5)
+    good_files = factories["music.TrackFile"].create_batch(5, track__artist=artist)
     good_tracks = [f.track for f in good_files]
 
     radio = radios.ArtistRadio()
     session = radio.start_session(user, related_object=artist)
-    assert session.radio_type == 'artist'
+    assert session.radio_type == "artist"
     for i in range(5):
         assert radio.pick() in good_tracks
 
 
 def test_can_start_tag_radio(factories):
-    user = factories['users.User']()
-    tag = factories['taggit.Tag']()
-    wrong_files = factories['music.TrackFile'].create_batch(5)
-    wrong_tracks = [f.track for f in wrong_files]
-    good_files = factories['music.TrackFile'].create_batch(
-        5, track__tags=[tag])
+    user = factories["users.User"]()
+    tag = factories["taggit.Tag"]()
+    factories["music.TrackFile"].create_batch(5)
+    good_files = factories["music.TrackFile"].create_batch(5, track__tags=[tag])
     good_tracks = [f.track for f in good_files]
 
     radio = radios.TagRadio()
     session = radio.start_session(user, related_object=tag)
-    assert session.radio_type == 'tag'
+    assert session.radio_type == "tag"
     for i in range(5):
         assert radio.pick() in good_tracks
 
 
-def test_can_start_artist_radio_from_api(
-        logged_in_api_client, preferences, factories):
-    artist = factories['music.Artist']()
-    url = reverse('api:v1:radios:sessions-list')
+def test_can_start_artist_radio_from_api(logged_in_api_client, preferences, factories):
+    artist = factories["music.Artist"]()
+    url = reverse("api:v1:radios:sessions-list")
 
     response = logged_in_api_client.post(
-        url, {'radio_type': 'artist', 'related_object_id': artist.id})
+        url, {"radio_type": "artist", "related_object_id": artist.id}
+    )
 
     assert response.status_code == 201
 
-    session = models.RadioSession.objects.latest('id')
+    session = models.RadioSession.objects.latest("id")
 
-    assert session.radio_type == 'artist'
+    assert session.radio_type == "artist"
     assert session.related_object == artist
 
 
 def test_can_start_less_listened_radio(factories):
-    user = factories['users.User']()
-    wrong_files = factories['music.TrackFile'].create_batch(5)
+    user = factories["users.User"]()
+    wrong_files = factories["music.TrackFile"].create_batch(5)
     for f in wrong_files:
-        factories['history.Listening'](track=f.track, user=user)
-    good_files = factories['music.TrackFile'].create_batch(5)
+        factories["history.Listening"](track=f.track, user=user)
+    good_files = factories["music.TrackFile"].create_batch(5)
     good_tracks = [f.track for f in good_files]
     radio = radios.LessListenedRadio()
-    session = radio.start_session(user)
+    radio.start_session(user)
 
     for i in range(5):
         assert radio.pick() in good_tracks
diff --git a/api/tests/requests/test_models.py b/api/tests/requests/test_models.py
index 797656bd70c5ab916c650c589bd70ea5fc41be87..3ac8a534207acc5152b18ed0eedc8c429463f345 100644
--- a/api/tests/requests/test_models.py
+++ b/api/tests/requests/test_models.py
@@ -1,23 +1,18 @@
-import pytest
-
-from django.forms import ValidationError
-
-
 def test_can_bind_import_batch_to_request(factories):
-    request = factories['requests.ImportRequest']()
+    request = factories["requests.ImportRequest"]()
 
-    assert request.status == 'pending'
+    assert request.status == "pending"
 
     # when we create the import, we consider the request as accepted
-    batch = factories['music.ImportBatch'](import_request=request)
+    batch = factories["music.ImportBatch"](import_request=request)
     request.refresh_from_db()
 
-    assert request.status == 'accepted'
+    assert request.status == "accepted"
 
     # now, the batch is finished, therefore the request status should be
     # imported
-    batch.status = 'finished'
-    batch.save(update_fields=['status'])
+    batch.status = "finished"
+    batch.save(update_fields=["status"])
     request.refresh_from_db()
 
-    assert request.status == 'imported'
+    assert request.status == "imported"
diff --git a/api/tests/requests/test_views.py b/api/tests/requests/test_views.py
index 6c34f9ad19fcc8ae7ec10ba6563cb44d45cb3461..0d64336720aaa23abdc6b7ad4cd6d48b604d6ef2 100644
--- a/api/tests/requests/test_views.py
+++ b/api/tests/requests/test_views.py
@@ -2,25 +2,25 @@ from django.urls import reverse
 
 
 def test_request_viewset_requires_auth(db, api_client):
-    url = reverse('api:v1:requests:import-requests-list')
+    url = reverse("api:v1:requests:import-requests-list")
     response = api_client.get(url)
     assert response.status_code == 401
 
 
 def test_user_can_create_request(logged_in_api_client):
-    url = reverse('api:v1:requests:import-requests-list')
+    url = reverse("api:v1:requests:import-requests-list")
     user = logged_in_api_client.user
     data = {
-        'artist_name': 'System of a Down',
-        'albums': 'All please!',
-        'comment': 'Please, they rock!',
+        "artist_name": "System of a Down",
+        "albums": "All please!",
+        "comment": "Please, they rock!",
     }
     response = logged_in_api_client.post(url, data)
 
     assert response.status_code == 201
 
-    ir = user.import_requests.latest('id')
-    assert ir.status == 'pending'
+    ir = user.import_requests.latest("id")
+    assert ir.status == "pending"
     assert ir.creation_date is not None
     for field, value in data.items():
         assert getattr(ir, field) == value
diff --git a/api/tests/subsonic/test_authentication.py b/api/tests/subsonic/test_authentication.py
index 656f8c44d8af605aa603dcec23fbdff41037f088..b2d2c04001d694bacff2e4e2f816240944e4d2fa 100644
--- a/api/tests/subsonic/test_authentication.py
+++ b/api/tests/subsonic/test_authentication.py
@@ -1,22 +1,18 @@
 import binascii
-import pytest
 
+import pytest
 from rest_framework import exceptions
 
 from funkwhale_api.subsonic import authentication
 
 
 def test_auth_with_salt(api_request, factories):
-    salt = 'salt'
-    user = factories['users.User']()
-    user.subsonic_api_token = 'password'
+    salt = "salt"
+    user = factories["users.User"]()
+    user.subsonic_api_token = "password"
     user.save()
-    token = authentication.get_token(salt, 'password')
-    request = api_request.get('/', {
-        't': token,
-        's': salt,
-        'u': user.username
-    })
+    token = authentication.get_token(salt, "password")
+    request = api_request.get("/", {"t": token, "s": salt, "u": user.username})
 
     authenticator = authentication.SubsonicAuthentication()
     u, _ = authenticator.authenticate(request)
@@ -25,16 +21,20 @@ def test_auth_with_salt(api_request, factories):
 
 
 def test_auth_with_password_hex(api_request, factories):
-    salt = 'salt'
-    user = factories['users.User']()
-    user.subsonic_api_token = 'password'
+    user = factories["users.User"]()
+    user.subsonic_api_token = "password"
     user.save()
-    token = authentication.get_token(salt, 'password')
-    request = api_request.get('/', {
-        'u': user.username,
-        'p': 'enc:{}'.format(binascii.hexlify(
-            user.subsonic_api_token.encode('utf-8')).decode('utf-8'))
-    })
+    request = api_request.get(
+        "/",
+        {
+            "u": user.username,
+            "p": "enc:{}".format(
+                binascii.hexlify(user.subsonic_api_token.encode("utf-8")).decode(
+                    "utf-8"
+                )
+            ),
+        },
+    )
 
     authenticator = authentication.SubsonicAuthentication()
     u, _ = authenticator.authenticate(request)
@@ -43,15 +43,10 @@ def test_auth_with_password_hex(api_request, factories):
 
 
 def test_auth_with_password_cleartext(api_request, factories):
-    salt = 'salt'
-    user = factories['users.User']()
-    user.subsonic_api_token = 'password'
+    user = factories["users.User"]()
+    user.subsonic_api_token = "password"
     user.save()
-    token = authentication.get_token(salt, 'password')
-    request = api_request.get('/', {
-        'u': user.username,
-        'p': 'password',
-    })
+    request = api_request.get("/", {"u": user.username, "p": "password"})
 
     authenticator = authentication.SubsonicAuthentication()
     u, _ = authenticator.authenticate(request)
@@ -60,15 +55,10 @@ def test_auth_with_password_cleartext(api_request, factories):
 
 
 def test_auth_with_inactive_users(api_request, factories):
-    salt = 'salt'
-    user = factories['users.User'](is_active=False)
-    user.subsonic_api_token = 'password'
+    user = factories["users.User"](is_active=False)
+    user.subsonic_api_token = "password"
     user.save()
-    token = authentication.get_token(salt, 'password')
-    request = api_request.get('/', {
-        'u': user.username,
-        'p': 'password',
-    })
+    request = api_request.get("/", {"u": user.username, "p": "password"})
 
     authenticator = authentication.SubsonicAuthentication()
     with pytest.raises(exceptions.AuthenticationFailed):
diff --git a/api/tests/subsonic/test_renderers.py b/api/tests/subsonic/test_renderers.py
index 8e2ea3f85ab80bc45661eb07e4ec69f542b8f963..301fee8b5d2e67673d7cc829fcdeb399fb1b0b76 100644
--- a/api/tests/subsonic/test_renderers.py
+++ b/api/tests/subsonic/test_renderers.py
@@ -5,38 +5,26 @@ from funkwhale_api.subsonic import renderers
 
 
 def test_json_renderer():
-    data = {'hello': 'world'}
+    data = {"hello": "world"}
     expected = {
-       'subsonic-response': {
-          'status': 'ok',
-          'version': '1.16.0',
-          'hello': 'world'
-       }
+        "subsonic-response": {"status": "ok", "version": "1.16.0", "hello": "world"}
     }
     renderer = renderers.SubsonicJSONRenderer()
     assert json.loads(renderer.render(data)) == expected
 
 
 def test_xml_renderer_dict_to_xml():
-    payload = {
-        'hello': 'world',
-        'item': [
-            {'this': 1},
-            {'some': 'node'},
-        ]
-    }
+    payload = {"hello": "world", "item": [{"this": 1}, {"some": "node"}]}
     expected = """<?xml version="1.0" encoding="UTF-8"?>
 <key hello="world"><item this="1" /><item some="node" /></key>"""
-    result = renderers.dict_to_xml_tree('key', payload)
+    result = renderers.dict_to_xml_tree("key", payload)
     exp = ET.fromstring(expected)
     assert ET.tostring(result) == ET.tostring(exp)
 
 
 def test_xml_renderer():
-    payload = {
-        'hello': 'world',
-    }
-    expected = b'<?xml version="1.0" encoding="UTF-8"?>\n<subsonic-response hello="world" status="ok" version="1.16.0" xmlns="http://subsonic.org/restapi" />'
+    payload = {"hello": "world"}
+    expected = b'<?xml version="1.0" encoding="UTF-8"?>\n<subsonic-response hello="world" status="ok" version="1.16.0" xmlns="http://subsonic.org/restapi" />'  # noqa
 
     renderer = renderers.SubsonicXMLRenderer()
     rendered = renderer.render(payload)
diff --git a/api/tests/subsonic/test_serializers.py b/api/tests/subsonic/test_serializers.py
index 6b9ec232da9e8789fd3bdbd60147c2d706b3e1ad..6fdf02e2d11bd56b1d523935a1ecd4863d77b806 100644
--- a/api/tests/subsonic/test_serializers.py
+++ b/api/tests/subsonic/test_serializers.py
@@ -3,136 +3,121 @@ from funkwhale_api.subsonic import serializers
 
 
 def test_get_artists_serializer(factories):
-    artist1 = factories['music.Artist'](name='eliot')
-    artist2 = factories['music.Artist'](name='Ellena')
-    artist3 = factories['music.Artist'](name='Rilay')
+    artist1 = factories["music.Artist"](name="eliot")
+    artist2 = factories["music.Artist"](name="Ellena")
+    artist3 = factories["music.Artist"](name="Rilay")
 
-    factories['music.Album'].create_batch(size=3, artist=artist1)
-    factories['music.Album'].create_batch(size=2, artist=artist2)
+    factories["music.Album"].create_batch(size=3, artist=artist1)
+    factories["music.Album"].create_batch(size=2, artist=artist2)
 
     expected = {
-        'ignoredArticles': '',
-        'index': [
+        "ignoredArticles": "",
+        "index": [
             {
-                'name': 'E',
-                'artist': [
-                    {
-                        'id': artist1.pk,
-                        'name': artist1.name,
-                        'albumCount': 3,
-                    },
-                    {
-                        'id': artist2.pk,
-                        'name': artist2.name,
-                        'albumCount': 2,
-                    },
-                ]
+                "name": "E",
+                "artist": [
+                    {"id": artist1.pk, "name": artist1.name, "albumCount": 3},
+                    {"id": artist2.pk, "name": artist2.name, "albumCount": 2},
+                ],
             },
             {
-                'name': 'R',
-                'artist': [
-                    {
-                        'id': artist3.pk,
-                        'name': artist3.name,
-                        'albumCount': 0,
-                    },
-                ]
+                "name": "R",
+                "artist": [{"id": artist3.pk, "name": artist3.name, "albumCount": 0}],
             },
-        ]
+        ],
     }
 
-    queryset = artist1.__class__.objects.filter(pk__in=[
-        artist1.pk, artist2.pk, artist3.pk
-    ])
+    queryset = artist1.__class__.objects.filter(
+        pk__in=[artist1.pk, artist2.pk, artist3.pk]
+    )
 
     assert serializers.GetArtistsSerializer(queryset).data == expected
 
 
 def test_get_artist_serializer(factories):
-    artist = factories['music.Artist']()
-    album = factories['music.Album'](artist=artist)
-    tracks = factories['music.Track'].create_batch(size=3, album=album)
+    artist = factories["music.Artist"]()
+    album = factories["music.Album"](artist=artist)
+    tracks = factories["music.Track"].create_batch(size=3, album=album)
 
     expected = {
-        'id': artist.pk,
-        'name': artist.name,
-        'albumCount': 1,
-        'album': [
+        "id": artist.pk,
+        "name": artist.name,
+        "albumCount": 1,
+        "album": [
             {
-                'id': album.pk,
-                'coverArt': 'al-{}'.format(album.id),
-                'artistId': artist.pk,
-                'name': album.title,
-                'artist': artist.name,
-                'songCount': len(tracks),
-                'created': album.creation_date,
-                'year': album.release_date.year,
+                "id": album.pk,
+                "coverArt": "al-{}".format(album.id),
+                "artistId": artist.pk,
+                "name": album.title,
+                "artist": artist.name,
+                "songCount": len(tracks),
+                "created": album.creation_date,
+                "year": album.release_date.year,
             }
-        ]
+        ],
     }
 
     assert serializers.GetArtistSerializer(artist).data == expected
 
 
 def test_get_album_serializer(factories):
-    artist = factories['music.Artist']()
-    album = factories['music.Album'](artist=artist)
-    track = factories['music.Track'](album=album)
-    tf = factories['music.TrackFile'](
-        track=track, bitrate=42000, duration=43, size=44)
+    artist = factories["music.Artist"]()
+    album = factories["music.Album"](artist=artist)
+    track = factories["music.Track"](album=album)
+    tf = factories["music.TrackFile"](track=track, bitrate=42000, duration=43, size=44)
 
     expected = {
-        'id': album.pk,
-        'artistId': artist.pk,
-        'name': album.title,
-        'artist': artist.name,
-        'songCount': 1,
-        'created': album.creation_date,
-        'year': album.release_date.year,
-        'coverArt': 'al-{}'.format(album.id),
-        'song': [
+        "id": album.pk,
+        "artistId": artist.pk,
+        "name": album.title,
+        "artist": artist.name,
+        "songCount": 1,
+        "created": album.creation_date,
+        "year": album.release_date.year,
+        "coverArt": "al-{}".format(album.id),
+        "song": [
             {
-                'id': track.pk,
-                'isDir': 'false',
-                'title': track.title,
-                'coverArt': 'al-{}'.format(album.id),
-                'album': album.title,
-                'artist': artist.name,
-                'track': track.position,
-                'year': track.album.release_date.year,
-                'contentType': tf.mimetype,
-                'suffix': tf.extension or '',
-                'bitrate': 42,
-                'duration': 43,
-                'size': 44,
-                'created': track.creation_date,
-                'albumId': album.pk,
-                'artistId': artist.pk,
-                'type': 'music',
+                "id": track.pk,
+                "isDir": "false",
+                "title": track.title,
+                "coverArt": "al-{}".format(album.id),
+                "album": album.title,
+                "artist": artist.name,
+                "track": track.position,
+                "year": track.album.release_date.year,
+                "contentType": tf.mimetype,
+                "suffix": tf.extension or "",
+                "bitrate": 42,
+                "duration": 43,
+                "size": 44,
+                "created": track.creation_date,
+                "albumId": album.pk,
+                "artistId": artist.pk,
+                "type": "music",
             }
-        ]
+        ],
     }
 
     assert serializers.GetAlbumSerializer(album).data == expected
 
 
 def test_starred_tracks2_serializer(factories):
-    artist = factories['music.Artist']()
-    album = factories['music.Album'](artist=artist)
-    track = factories['music.Track'](album=album)
-    tf = factories['music.TrackFile'](track=track)
-    favorite = factories['favorites.TrackFavorite'](track=track)
+    artist = factories["music.Artist"]()
+    album = factories["music.Album"](artist=artist)
+    track = factories["music.Track"](album=album)
+    tf = factories["music.TrackFile"](track=track)
+    favorite = factories["favorites.TrackFavorite"](track=track)
     expected = [serializers.get_track_data(album, track, tf)]
-    expected[0]['starred'] = favorite.creation_date
+    expected[0]["starred"] = favorite.creation_date
     data = serializers.get_starred_tracks_data([favorite])
     assert data == expected
 
 
 def test_get_album_list2_serializer(factories):
-    album1 = factories['music.Album']()
-    album2 = factories['music.Album']()
+    album1 = factories["music.Album"]()
+    album2 = factories["music.Album"]()
 
-    qs = music_models.Album.objects.with_tracks_count().order_by('pk')
+    qs = music_models.Album.objects.with_tracks_count().order_by("pk")
     expected = [
         serializers.get_album2_data(album1),
         serializers.get_album2_data(album2),
@@ -142,17 +127,17 @@ def test_get_album_list2_serializer(factories):
 
 
 def test_playlist_serializer(factories):
-    plt = factories['playlists.PlaylistTrack']()
+    plt = factories["playlists.PlaylistTrack"]()
     playlist = plt.playlist
-    qs = music_models.Album.objects.with_tracks_count().order_by('pk')
+    qs = music_models.Album.objects.with_tracks_count().order_by("pk")
     expected = {
-        'id': playlist.pk,
-        'name': playlist.name,
-        'owner': playlist.user.username,
-        'public': 'false',
-        'songCount': 1,
-        'duration': 0,
-        'created': playlist.creation_date,
+        "id": playlist.pk,
+        "name": playlist.name,
+        "owner": playlist.user.username,
+        "public": "false",
+        "songCount": 1,
+        "duration": 0,
+        "created": playlist.creation_date,
     }
     qs = playlist.__class__.objects.with_tracks_count()
     data = serializers.get_playlist_data(qs.first())
@@ -160,21 +145,19 @@ def test_playlist_serializer(factories):
 
 
 def test_playlist_detail_serializer(factories):
-    plt = factories['playlists.PlaylistTrack']()
-    tf = factories['music.TrackFile'](track=plt.track)
+    plt = factories["playlists.PlaylistTrack"]()
+    tf = factories["music.TrackFile"](track=plt.track)
     playlist = plt.playlist
-    qs = music_models.Album.objects.with_tracks_count().order_by('pk')
+    qs = music_models.Album.objects.with_tracks_count().order_by("pk")
     expected = {
-        'id': playlist.pk,
-        'name': playlist.name,
-        'owner': playlist.user.username,
-        'public': 'false',
-        'songCount': 1,
-        'duration': 0,
-        'created': playlist.creation_date,
-        'entry': [
-            serializers.get_track_data(plt.track.album, plt.track, tf)
-        ]
+        "id": playlist.pk,
+        "name": playlist.name,
+        "owner": playlist.user.username,
+        "public": "false",
+        "songCount": 1,
+        "duration": 0,
+        "created": playlist.creation_date,
+        "entry": [serializers.get_track_data(plt.track.album, plt.track, tf)],
     }
     qs = playlist.__class__.objects.with_tracks_count()
     data = serializers.get_playlist_detail_data(qs.first())
@@ -182,50 +165,47 @@ def test_playlist_detail_serializer(factories):
 
 
 def test_directory_serializer_artist(factories):
-    track = factories['music.Track']()
-    tf = factories['music.TrackFile'](
-        track=track, bitrate=42000, duration=43, size=44)
+    track = factories["music.Track"]()
+    tf = factories["music.TrackFile"](track=track, bitrate=42000, duration=43, size=44)
     album = track.album
     artist = track.artist
 
     expected = {
-        'id': artist.pk,
-        'parent': 1,
-        'name': artist.name,
-        'child': [{
-            'id': track.pk,
-            'isDir': 'false',
-            'title': track.title,
-            'album': album.title,
-            'artist': artist.name,
-            'track': track.position,
-            'year': track.album.release_date.year,
-            'contentType': tf.mimetype,
-            'suffix': tf.extension or '',
-            'bitrate': 42,
-            'duration': 43,
-            'size': 44,
-            'created': track.creation_date,
-            'albumId': album.pk,
-            'artistId': artist.pk,
-            'parent': artist.pk,
-            'type': 'music',
-        }]
+        "id": artist.pk,
+        "parent": 1,
+        "name": artist.name,
+        "child": [
+            {
+                "id": track.pk,
+                "isDir": "false",
+                "title": track.title,
+                "album": album.title,
+                "artist": artist.name,
+                "track": track.position,
+                "year": track.album.release_date.year,
+                "contentType": tf.mimetype,
+                "suffix": tf.extension or "",
+                "bitrate": 42,
+                "duration": 43,
+                "size": 44,
+                "created": track.creation_date,
+                "albumId": album.pk,
+                "artistId": artist.pk,
+                "parent": artist.pk,
+                "type": "music",
+            }
+        ],
     }
     data = serializers.get_music_directory_data(artist)
     assert data == expected
 
 
 def test_scrobble_serializer(factories):
-    tf = factories['music.TrackFile']()
+    tf = factories["music.TrackFile"]()
     track = tf.track
-    user = factories['users.User']()
-    payload = {
-        'id': track.pk,
-        'submission': True,
-    }
-    serializer = serializers.ScrobbleSerializer(
-        data=payload, context={'user': user})
+    user = factories["users.User"]()
+    payload = {"id": track.pk, "submission": True}
+    serializer = serializers.ScrobbleSerializer(data=payload, context={"user": user})
 
     assert serializer.is_valid(raise_exception=True)
 
diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py
index 52e410e52b4c516b58ecc05a45df595316748553..b7431efab4a05962fa6ed5d34ba0c963336ad9a3 100644
--- a/api/tests/subsonic/test_views.py
+++ b/api/tests/subsonic/test_views.py
@@ -1,16 +1,13 @@
 import datetime
 import json
-import pytest
 
-from django.utils import timezone
+import pytest
 from django.urls import reverse
-
-from rest_framework.response import Response
+from django.utils import timezone
 
 from funkwhale_api.music import models as music_models
 from funkwhale_api.music import views as music_views
-from funkwhale_api.subsonic import renderers
-from funkwhale_api.subsonic import serializers
+from funkwhale_api.subsonic import renderers, serializers
 
 
 def render_json(data):
@@ -18,372 +15,355 @@ def render_json(data):
 
 
 def test_render_content_json(db, api_client):
-    url = reverse('api:subsonic-ping')
-    response = api_client.get(url, {'f': 'json'})
+    url = reverse("api:subsonic-ping")
+    response = api_client.get(url, {"f": "json"})
 
-    expected = {
-        'status': 'ok',
-        'version': '1.16.0'
-    }
+    expected = {"status": "ok", "version": "1.16.0"}
     assert response.status_code == 200
     assert json.loads(response.content) == render_json(expected)
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_exception_wrong_credentials(f, db, api_client):
-    url = reverse('api:subsonic-ping')
-    response = api_client.get(url, {'f': f, 'u': 'yolo'})
+    url = reverse("api:subsonic-ping")
+    response = api_client.get(url, {"f": f, "u": "yolo"})
 
     expected = {
-        'status': 'failed',
-        'error': {
-            'code': 40,
-            'message': 'Wrong username or password.'
-        }
+        "status": "failed",
+        "error": {"code": 40, "message": "Wrong username or password."},
     }
     assert response.status_code == 200
     assert response.data == expected
 
 
 def test_disabled_subsonic(preferences, api_client):
-    preferences['subsonic__enabled'] = False
-    url = reverse('api:subsonic-ping')
+    preferences["subsonic__enabled"] = False
+    url = reverse("api:subsonic-ping")
     response = api_client.get(url)
     assert response.status_code == 405
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_license(f, db, logged_in_api_client, mocker):
-    url = reverse('api:subsonic-get-license')
-    assert url.endswith('getLicense') is True
+    url = reverse("api:subsonic-get-license")
+    assert url.endswith("getLicense") is True
     now = timezone.now()
-    mocker.patch('django.utils.timezone.now', return_value=now)
-    response = logged_in_api_client.get(url, {'f': f})
+    mocker.patch("django.utils.timezone.now", return_value=now)
+    response = logged_in_api_client.get(url, {"f": f})
     expected = {
-        'status': 'ok',
-        'version': '1.16.0',
-        'license': {
-            'valid': 'true',
-            'email': 'valid@valid.license',
-            'licenseExpires': now + datetime.timedelta(days=365)
-        }
+        "status": "ok",
+        "version": "1.16.0",
+        "license": {
+            "valid": "true",
+            "email": "valid@valid.license",
+            "licenseExpires": now + datetime.timedelta(days=365),
+        },
     }
     assert response.status_code == 200
     assert response.data == expected
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_ping(f, db, api_client):
-    url = reverse('api:subsonic-ping')
-    response = api_client.get(url, {'f': f})
+    url = reverse("api:subsonic-ping")
+    response = api_client.get(url, {"f": f})
 
-    expected = {
-        'status': 'ok',
-        'version': '1.16.0',
-    }
+    expected = {"status": "ok", "version": "1.16.0"}
     assert response.status_code == 200
     assert response.data == expected
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_artists(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-get-artists')
-    assert url.endswith('getArtists') is True
-    artists = factories['music.Artist'].create_batch(size=10)
+    url = reverse("api:subsonic-get-artists")
+    assert url.endswith("getArtists") is True
+    factories["music.Artist"].create_batch(size=10)
     expected = {
-        'artists': serializers.GetArtistsSerializer(
+        "artists": serializers.GetArtistsSerializer(
             music_models.Artist.objects.all()
         ).data
     }
-    response = logged_in_api_client.get(url, {'f': f})
+    response = logged_in_api_client.get(url, {"f": f})
 
     assert response.status_code == 200
     assert response.data == expected
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_artist(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-get-artist')
-    assert url.endswith('getArtist') is True
-    artist = factories['music.Artist']()
-    albums = factories['music.Album'].create_batch(size=3, artist=artist)
-    expected = {
-        'artist': serializers.GetArtistSerializer(artist).data
-    }
-    response = logged_in_api_client.get(url, {'id': artist.pk})
+    url = reverse("api:subsonic-get-artist")
+    assert url.endswith("getArtist") is True
+    artist = factories["music.Artist"]()
+    factories["music.Album"].create_batch(size=3, artist=artist)
+    expected = {"artist": serializers.GetArtistSerializer(artist).data}
+    response = logged_in_api_client.get(url, {"id": artist.pk})
 
     assert response.status_code == 200
     assert response.data == expected
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_artist_info2(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-get-artist-info2')
-    assert url.endswith('getArtistInfo2') is True
-    artist = factories['music.Artist']()
+    url = reverse("api:subsonic-get-artist-info2")
+    assert url.endswith("getArtistInfo2") is True
+    artist = factories["music.Artist"]()
 
-    expected = {
-        'artist-info2': {}
-    }
-    response = logged_in_api_client.get(url, {'id': artist.pk})
+    expected = {"artist-info2": {}}
+    response = logged_in_api_client.get(url, {"id": artist.pk})
 
     assert response.status_code == 200
     assert response.data == expected
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_album(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-get-album')
-    assert url.endswith('getAlbum') is True
-    artist = factories['music.Artist']()
-    album = factories['music.Album'](artist=artist)
-    tracks = factories['music.Track'].create_batch(size=3, album=album)
-    expected = {
-        'album': serializers.GetAlbumSerializer(album).data
-    }
-    response = logged_in_api_client.get(url, {'f': f, 'id': album.pk})
+    url = reverse("api:subsonic-get-album")
+    assert url.endswith("getAlbum") is True
+    artist = factories["music.Artist"]()
+    album = factories["music.Album"](artist=artist)
+    factories["music.Track"].create_batch(size=3, album=album)
+    expected = {"album": serializers.GetAlbumSerializer(album).data}
+    response = logged_in_api_client.get(url, {"f": f, "id": album.pk})
 
     assert response.status_code == 200
     assert response.data == expected
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_stream(f, db, logged_in_api_client, factories, mocker):
-    url = reverse('api:subsonic-stream')
-    mocked_serve = mocker.spy(
-        music_views, 'handle_serve')
-    assert url.endswith('stream') is True
-    artist = factories['music.Artist']()
-    album = factories['music.Album'](artist=artist)
-    track = factories['music.Track'](album=album)
-    tf = factories['music.TrackFile'](track=track)
-    response = logged_in_api_client.get(url, {'f': f, 'id': track.pk})
-
-    mocked_serve.assert_called_once_with(
-        track_file=tf
-    )
+    url = reverse("api:subsonic-stream")
+    mocked_serve = mocker.spy(music_views, "handle_serve")
+    assert url.endswith("stream") is True
+    artist = factories["music.Artist"]()
+    album = factories["music.Album"](artist=artist)
+    track = factories["music.Track"](album=album)
+    tf = factories["music.TrackFile"](track=track)
+    response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
+
+    mocked_serve.assert_called_once_with(track_file=tf)
     assert response.status_code == 200
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_star(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-star')
-    assert url.endswith('star') is True
-    track = factories['music.Track']()
-    response = logged_in_api_client.get(url, {'f': f, 'id': track.pk})
+    url = reverse("api:subsonic-star")
+    assert url.endswith("star") is True
+    track = factories["music.Track"]()
+    response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
 
     assert response.status_code == 200
-    assert response.data == {'status': 'ok'}
+    assert response.data == {"status": "ok"}
 
-    favorite = logged_in_api_client.user.track_favorites.latest('id')
+    favorite = logged_in_api_client.user.track_favorites.latest("id")
     assert favorite.track == track
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_unstar(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-unstar')
-    assert url.endswith('unstar') is True
-    track = factories['music.Track']()
-    favorite = factories['favorites.TrackFavorite'](
-        track=track, user=logged_in_api_client.user)
-    response = logged_in_api_client.get(url, {'f': f, 'id': track.pk})
+    url = reverse("api:subsonic-unstar")
+    assert url.endswith("unstar") is True
+    track = factories["music.Track"]()
+    factories["favorites.TrackFavorite"](track=track, user=logged_in_api_client.user)
+    response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
 
     assert response.status_code == 200
-    assert response.data == {'status': 'ok'}
+    assert response.data == {"status": "ok"}
     assert logged_in_api_client.user.track_favorites.count() == 0
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_starred2(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-get-starred2')
-    assert url.endswith('getStarred2') is True
-    track = factories['music.Track']()
-    favorite = factories['favorites.TrackFavorite'](
-        track=track, user=logged_in_api_client.user)
-    response = logged_in_api_client.get(url, {'f': f, 'id': track.pk})
+    url = reverse("api:subsonic-get-starred2")
+    assert url.endswith("getStarred2") is True
+    track = factories["music.Track"]()
+    favorite = factories["favorites.TrackFavorite"](
+        track=track, user=logged_in_api_client.user
+    )
+    response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
 
     assert response.status_code == 200
     assert response.data == {
-        'starred2': {
-            'song': serializers.get_starred_tracks_data([favorite])
-        }
+        "starred2": {"song": serializers.get_starred_tracks_data([favorite])}
     }
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_starred(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-get-starred')
-    assert url.endswith('getStarred') is True
-    track = factories['music.Track']()
-    favorite = factories['favorites.TrackFavorite'](
-        track=track, user=logged_in_api_client.user)
-    response = logged_in_api_client.get(url, {'f': f, 'id': track.pk})
+    url = reverse("api:subsonic-get-starred")
+    assert url.endswith("getStarred") is True
+    track = factories["music.Track"]()
+    favorite = factories["favorites.TrackFavorite"](
+        track=track, user=logged_in_api_client.user
+    )
+    response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
 
     assert response.status_code == 200
     assert response.data == {
-        'starred': {
-            'song': serializers.get_starred_tracks_data([favorite])
-        }
+        "starred": {"song": serializers.get_starred_tracks_data([favorite])}
     }
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_album_list2(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-get-album-list2')
-    assert url.endswith('getAlbumList2') is True
-    album1 = factories['music.Album']()
-    album2 = factories['music.Album']()
-    response = logged_in_api_client.get(url, {'f': f, 'type': 'newest'})
+    url = reverse("api:subsonic-get-album-list2")
+    assert url.endswith("getAlbumList2") is True
+    album1 = factories["music.Album"]()
+    album2 = factories["music.Album"]()
+    response = logged_in_api_client.get(url, {"f": f, "type": "newest"})
 
     assert response.status_code == 200
     assert response.data == {
-        'albumList2': {
-            'album': serializers.get_album_list2_data([album2, album1])
-        }
+        "albumList2": {"album": serializers.get_album_list2_data([album2, album1])}
     }
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
+def test_get_album_list2_pagination(f, db, logged_in_api_client, factories):
+    url = reverse("api:subsonic-get-album-list2")
+    assert url.endswith("getAlbumList2") is True
+    album1 = factories["music.Album"]()
+    factories["music.Album"]()
+    response = logged_in_api_client.get(
+        url, {"f": f, "type": "newest", "size": 1, "offset": 1}
+    )
+
+    assert response.status_code == 200
+    assert response.data == {
+        "albumList2": {"album": serializers.get_album_list2_data([album1])}
+    }
+
+
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_search3(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-search3')
-    assert url.endswith('search3') is True
-    artist = factories['music.Artist'](name='testvalue')
-    factories['music.Artist'](name='nope')
-    album = factories['music.Album'](title='testvalue')
-    factories['music.Album'](title='nope')
-    track = factories['music.Track'](title='testvalue')
-    factories['music.Track'](title='nope')
-
-    response = logged_in_api_client.get(url, {'f': f, 'query': 'testval'})
-
-    artist_qs = music_models.Artist.objects.with_albums_count().filter(
-        pk=artist.pk).values('_albums_count', 'id', 'name')
+    url = reverse("api:subsonic-search3")
+    assert url.endswith("search3") is True
+    artist = factories["music.Artist"](name="testvalue")
+    factories["music.Artist"](name="nope")
+    album = factories["music.Album"](title="testvalue")
+    factories["music.Album"](title="nope")
+    track = factories["music.Track"](title="testvalue")
+    factories["music.Track"](title="nope")
+
+    response = logged_in_api_client.get(url, {"f": f, "query": "testval"})
+
+    artist_qs = (
+        music_models.Artist.objects.with_albums_count()
+        .filter(pk=artist.pk)
+        .values("_albums_count", "id", "name")
+    )
     assert response.status_code == 200
     assert response.data == {
-        'searchResult3': {
-            'artist': [serializers.get_artist_data(a) for a in artist_qs],
-            'album': serializers.get_album_list2_data([album]),
-            'song': serializers.get_song_list_data([track]),
+        "searchResult3": {
+            "artist": [serializers.get_artist_data(a) for a in artist_qs],
+            "album": serializers.get_album_list2_data([album]),
+            "song": serializers.get_song_list_data([track]),
         }
     }
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_playlists(f, db, logged_in_api_client, factories):
-    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})
+    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})
 
     qs = playlist.__class__.objects.with_tracks_count()
     assert response.status_code == 200
     assert response.data == {
-        'playlists': {
-            'playlist': [serializers.get_playlist_data(qs.first())],
-        }
+        "playlists": {"playlist": [serializers.get_playlist_data(qs.first())]}
     }
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_playlist(f, db, logged_in_api_client, factories):
-    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})
+    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})
 
     qs = playlist.__class__.objects.with_tracks_count()
     assert response.status_code == 200
     assert response.data == {
-        'playlist': serializers.get_playlist_detail_data(qs.first())
+        "playlist": serializers.get_playlist_detail_data(qs.first())
     }
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_update_playlist(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-update-playlist')
-    assert url.endswith('updatePlaylist') is True
-    playlist = factories['playlists.Playlist'](
-        user=logged_in_api_client.user
-    )
-    plt = factories['playlists.PlaylistTrack'](
-        index=0, playlist=playlist)
-    new_track = factories['music.Track']()
+    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)
+    new_track = factories["music.Track"]()
     response = logged_in_api_client.get(
-        url, {
-            'f': f,
-            'name': 'new_name',
-            'playlistId': playlist.pk,
-            'songIdToAdd': new_track.pk,
-            'songIndexToRemove': 0})
+        url,
+        {
+            "f": f,
+            "name": "new_name",
+            "playlistId": playlist.pk,
+            "songIdToAdd": new_track.pk,
+            "songIndexToRemove": 0,
+        },
+    )
     playlist.refresh_from_db()
     assert response.status_code == 200
-    assert playlist.name == 'new_name'
+    assert playlist.name == "new_name"
     assert playlist.playlist_tracks.count() == 1
     assert playlist.playlist_tracks.first().track_id == new_track.pk
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_delete_playlist(f, db, logged_in_api_client, factories):
-    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})
+    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})
     assert response.status_code == 200
     with pytest.raises(playlist.__class__.DoesNotExist):
         playlist.refresh_from_db()
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_create_playlist(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-create-playlist')
-    assert url.endswith('createPlaylist') is True
-    track1 = factories['music.Track']()
-    track2 = factories['music.Track']()
+    url = reverse("api:subsonic-create-playlist")
+    assert url.endswith("createPlaylist") is True
+    track1 = factories["music.Track"]()
+    track2 = factories["music.Track"]()
     response = logged_in_api_client.get(
-        url, {'f': f, 'name': 'hello', 'songId': [track1.pk, track2.pk]})
+        url, {"f": f, "name": "hello", "songId": [track1.pk, track2.pk]}
+    )
     assert response.status_code == 200
-    playlist = logged_in_api_client.user.playlists.latest('id')
+    playlist = logged_in_api_client.user.playlists.latest("id")
     assert playlist.playlist_tracks.count() == 2
     for i, t in enumerate([track1, track2]):
         plt = playlist.playlist_tracks.get(track=t)
         assert plt.index == i
-    assert playlist.name == 'hello'
+    assert playlist.name == "hello"
     qs = playlist.__class__.objects.with_tracks_count()
     assert response.data == {
-        'playlist': serializers.get_playlist_detail_data(qs.first())
+        "playlist": serializers.get_playlist_detail_data(qs.first())
     }
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_music_folders(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-get-music-folders')
-    assert url.endswith('getMusicFolders') is True
-    response = logged_in_api_client.get(url, {'f': f})
+    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
     assert response.data == {
-        'musicFolders': {
-            'musicFolder': [{
-                'id': 1,
-                'name': 'Music'
-            }]
-        }
+        "musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]}
     }
 
 
-@pytest.mark.parametrize('f', ['xml', 'json'])
+@pytest.mark.parametrize("f", ["xml", "json"])
 def test_get_indexes(f, db, logged_in_api_client, factories):
-    url = reverse('api:subsonic-get-indexes')
-    assert url.endswith('getIndexes') is True
-    artists = factories['music.Artist'].create_batch(size=10)
+    url = reverse("api:subsonic-get-indexes")
+    assert url.endswith("getIndexes") is True
+    factories["music.Artist"].create_batch(size=10)
     expected = {
-        'indexes': serializers.GetArtistsSerializer(
+        "indexes": serializers.GetArtistsSerializer(
             music_models.Artist.objects.all()
         ).data
     }
@@ -394,27 +374,26 @@ def test_get_indexes(f, db, logged_in_api_client, factories):
 
 
 def test_get_cover_art_album(factories, logged_in_api_client):
-    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)})
+    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)})
 
     assert response.status_code == 200
-    assert response['Content-Type'] == ''
-    assert response['X-Accel-Redirect'] == music_views.get_file_path(
+    assert response["Content-Type"] == ""
+    assert response["X-Accel-Redirect"] == music_views.get_file_path(
         album.cover
-    ).decode('utf-8')
+    ).decode("utf-8")
 
 
 def test_scrobble(factories, logged_in_api_client):
-    tf = factories['music.TrackFile']()
+    tf = factories["music.TrackFile"]()
     track = tf.track
-    url = reverse('api:subsonic-scrobble')
-    assert url.endswith('scrobble') is True
-    response = logged_in_api_client.get(
-        url, {'id': track.pk, 'submission': True})
+    url = reverse("api:subsonic-scrobble")
+    assert url.endswith("scrobble") is True
+    response = logged_in_api_client.get(url, {"id": track.pk, "submission": True})
 
     assert response.status_code == 200
 
-    l = logged_in_api_client.user.listenings.latest('id')
-    assert l.track == track
+    listening = logged_in_api_client.user.listenings.latest("id")
+    assert listening.track == track
diff --git a/api/tests/test_acoustid.py b/api/tests/test_acoustid.py
index 1f7de9247e226a13971641624b0a436b5718d743..ab3dfd1d87c9a8c47e5e6d1b357f27f01f19a3c2 100644
--- a/api/tests/test_acoustid.py
+++ b/api/tests/test_acoustid.py
@@ -2,33 +2,42 @@ 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
+    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
+    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'
+        "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)
+    m = mocker.patch("acoustid.match", return_value=payload)
     client = get_acoustid_client()
-    response = client.match('/tmp/noopfile.mp3')
+    response = client.match("/tmp/noopfile.mp3")
 
     assert response == payload
-    m.assert_called_once_with('test', '/tmp/noopfile.mp3', parse=False)
+    m.assert_called_once_with("test", "/tmp/noopfile.mp3", parse=False)
diff --git a/api/tests/test_downloader.py b/api/tests/test_downloader.py
index ede7bb16cc9571607f0b3c73114010f02e411349..0a41343935590fe370ccf34286787f2314a58982 100644
--- a/api/tests/test_downloader.py
+++ b/api/tests/test_downloader.py
@@ -5,7 +5,7 @@ from funkwhale_api import downloader
 
 def test_can_download_audio_from_youtube_url_to_vorbis(tmpdir):
     data = downloader.download(
-        'https://www.youtube.com/watch?v=tPEE9ZwTmy0',
-        target_directory=tmpdir)
-    assert data['audio_file_path'] == os.path.join(tmpdir, 'tPEE9ZwTmy0.ogg')
-    assert os.path.exists(data['audio_file_path'])
+        "https://www.youtube.com/watch?v=tPEE9ZwTmy0", target_directory=tmpdir
+    )
+    assert data["audio_file_path"] == os.path.join(tmpdir, "tPEE9ZwTmy0.ogg")
+    assert os.path.exists(data["audio_file_path"])
diff --git a/api/tests/test_import_audio_file.py b/api/tests/test_import_audio_file.py
index da3d1959cbb076ccadf9ad5cff81ca2affb6dcb0..67f6c489d9304c13d1721a529ebc870eb35dca98 100644
--- a/api/tests/test_import_audio_file.py
+++ b/api/tests/test_import_audio_file.py
@@ -1,201 +1,166 @@
-import pytest
 import datetime
 import os
-import uuid
 
+import pytest
 from django.core.management import call_command
 from django.core.management.base import CommandError
 
 from funkwhale_api.providers.audiofile import tasks
-from funkwhale_api.music import tasks as music_tasks
 
-DATA_DIR = os.path.join(
-    os.path.dirname(os.path.abspath(__file__)),
-    'files'
-)
+DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "files")
 
 
 def test_can_create_track_from_file_metadata_no_mbid(db, mocker):
     metadata = {
-        'artist': ['Test artist'],
-        'album': ['Test album'],
-        'title': ['Test track'],
-        'TRACKNUMBER': ['4'],
-        'date': ['2012-08-15'],
+        "artist": ["Test artist"],
+        "album": ["Test album"],
+        "title": ["Test track"],
+        "TRACKNUMBER": ["4"],
+        "date": ["2012-08-15"],
     }
-    m1 = mocker.patch('mutagen.File', return_value=metadata)
-    m2 = mocker.patch(
-        'funkwhale_api.music.metadata.Metadata.get_file_type',
-        return_value='OggVorbis',
+    mocker.patch("mutagen.File", return_value=metadata)
+    mocker.patch(
+        "funkwhale_api.music.metadata.Metadata.get_file_type", return_value="OggVorbis"
     )
-    track = tasks.import_track_data_from_path(
-        os.path.join(DATA_DIR, 'dummy_file.ogg'))
+    track = tasks.import_track_data_from_path(os.path.join(DATA_DIR, "dummy_file.ogg"))
 
-    assert track.title == metadata['title'][0]
+    assert track.title == metadata["title"][0]
     assert track.mbid is None
     assert track.position == 4
-    assert track.album.title == metadata['album'][0]
+    assert track.album.title == metadata["album"][0]
     assert track.album.mbid is None
     assert track.album.release_date == datetime.date(2012, 8, 15)
-    assert track.artist.name == metadata['artist'][0]
+    assert track.artist.name == metadata["artist"][0]
     assert track.artist.mbid is None
 
 
 def test_can_create_track_from_file_metadata_mbid(factories, mocker):
-    album = factories['music.Album']()
+    album = factories["music.Album"]()
     mocker.patch(
-        'funkwhale_api.music.models.Album.get_or_create_from_api',
+        "funkwhale_api.music.models.Album.get_or_create_from_api",
         return_value=(album, True),
     )
 
     album_data = {
-        'release': {
-            'id': album.mbid,
-            'medium-list': [
+        "release": {
+            "id": album.mbid,
+            "medium-list": [
                 {
-                    'track-list': [
+                    "track-list": [
                         {
-                            'id': '03baca8b-855a-3c05-8f3d-d3235287d84d',
-                            'position': '4',
-                            'number': '4',
-                            'recording': {
-                                'id': '2109e376-132b-40ad-b993-2bb6812e19d4',
-                                'title': 'Teen Age Riot',
+                            "id": "03baca8b-855a-3c05-8f3d-d3235287d84d",
+                            "position": "4",
+                            "number": "4",
+                            "recording": {
+                                "id": "2109e376-132b-40ad-b993-2bb6812e19d4",
+                                "title": "Teen Age Riot",
                             },
                         }
                     ],
-                    'track-count': 1
+                    "track-count": 1,
                 }
             ],
         }
     }
-    mocker.patch(
-        'funkwhale_api.musicbrainz.api.releases.get',
-        return_value=album_data)
-    track_data = album_data['release']['medium-list'][0]['track-list'][0]
+    mocker.patch("funkwhale_api.musicbrainz.api.releases.get", return_value=album_data)
+    track_data = album_data["release"]["medium-list"][0]["track-list"][0]
     metadata = {
-        'musicbrainz_albumid': [album.mbid],
-        'musicbrainz_trackid': [track_data['recording']['id']],
+        "musicbrainz_albumid": [album.mbid],
+        "musicbrainz_trackid": [track_data["recording"]["id"]],
     }
-    m1 = mocker.patch('mutagen.File', return_value=metadata)
-    m2 = mocker.patch(
-        'funkwhale_api.music.metadata.Metadata.get_file_type',
-        return_value='OggVorbis',
+    mocker.patch("mutagen.File", return_value=metadata)
+    mocker.patch(
+        "funkwhale_api.music.metadata.Metadata.get_file_type", return_value="OggVorbis"
     )
-    track = tasks.import_track_data_from_path(
-        os.path.join(DATA_DIR, 'dummy_file.ogg'))
+    track = tasks.import_track_data_from_path(os.path.join(DATA_DIR, "dummy_file.ogg"))
 
-    assert track.title == track_data['recording']['title']
-    assert track.mbid == track_data['recording']['id']
+    assert track.title == track_data["recording"]["title"]
+    assert track.mbid == track_data["recording"]["id"]
     assert track.position == 4
     assert track.album == album
     assert track.artist == album.artist
 
 
 def test_management_command_requires_a_valid_username(factories, mocker):
-    path = os.path.join(DATA_DIR, 'dummy_file.ogg')
-    user = factories['users.User'](username='me')
+    path = os.path.join(DATA_DIR, "dummy_file.ogg")
+    factories["users.User"](username="me")
     mocker.patch(
-        'funkwhale_api.providers.audiofile.management.commands.import_files.Command.do_import',  # noqa
-        return_value=(mocker.MagicMock(), []))
+        "funkwhale_api.providers.audiofile.management.commands.import_files.Command.do_import",  # noqa
+        return_value=(mocker.MagicMock(), []),
+    )
     with pytest.raises(CommandError):
-        call_command('import_files', path, username='not_me', interactive=False)
-    call_command('import_files', path, username='me', interactive=False)
+        call_command("import_files", path, username="not_me", interactive=False)
+    call_command("import_files", path, username="me", interactive=False)
 
 
 def test_in_place_import_only_from_music_dir(factories, settings):
-    user = factories['users.User'](username='me')
-    settings.MUSIC_DIRECTORY_PATH = '/nope'
-    path = os.path.join(DATA_DIR, 'dummy_file.ogg')
+    factories["users.User"](username="me")
+    settings.MUSIC_DIRECTORY_PATH = "/nope"
+    path = os.path.join(DATA_DIR, "dummy_file.ogg")
     with pytest.raises(CommandError):
         call_command(
-            'import_files',
-            path,
-            in_place=True,
-            username='me',
-            interactive=False
+            "import_files", path, in_place=True, username="me", interactive=False
         )
 
 
 def test_import_files_creates_a_batch_and_job(factories, mocker):
-    m = mocker.patch('funkwhale_api.music.tasks.import_job_run')
-    user = factories['users.User'](username='me')
-    path = os.path.join(DATA_DIR, 'dummy_file.ogg')
-    call_command(
-        'import_files',
-        path,
-        username='me',
-        async=False,
-        interactive=False)
+    m = mocker.patch("funkwhale_api.music.tasks.import_job_run")
+    user = factories["users.User"](username="me")
+    path = os.path.join(DATA_DIR, "dummy_file.ogg")
+    call_command("import_files", path, username="me", async=False, interactive=False)
 
-    batch = user.imports.latest('id')
-    assert batch.source == 'shell'
+    batch = user.imports.latest("id")
+    assert batch.source == "shell"
     assert batch.jobs.count() == 1
 
     job = batch.jobs.first()
 
-    assert job.status == 'pending'
-    with open(path, 'rb') as f:
+    assert job.status == "pending"
+    with open(path, "rb") as f:
         assert job.audio_file.read() == f.read()
 
-    assert job.source == 'file://' + path
-    m.assert_called_once_with(
-        import_job_id=job.pk,
-        use_acoustid=False)
+    assert job.source == "file://" + path
+    m.assert_called_once_with(import_job_id=job.pk, use_acoustid=False)
 
 
 def test_import_files_skip_if_path_already_imported(factories, mocker):
-    user = factories['users.User'](username='me')
-    path = os.path.join(DATA_DIR, 'dummy_file.ogg')
-    existing = factories['music.TrackFile'](
-        source='file://{}'.format(path))
+    user = factories["users.User"](username="me")
+    path = os.path.join(DATA_DIR, "dummy_file.ogg")
+    factories["music.TrackFile"](source="file://{}".format(path))
 
-    call_command(
-        'import_files',
-        path,
-        username='me',
-        async=False,
-        interactive=False)
+    call_command("import_files", path, username="me", async=False, interactive=False)
     assert user.imports.count() == 0
 
 
 def test_import_files_works_with_utf8_file_name(factories, mocker):
-    m = mocker.patch('funkwhale_api.music.tasks.import_job_run')
-    user = factories['users.User'](username='me')
-    path = os.path.join(DATA_DIR, 'utf8-éà◌.ogg')
-    call_command(
-        'import_files',
-        path,
-        username='me',
-        async=False,
-        interactive=False)
-    batch = user.imports.latest('id')
+    m = mocker.patch("funkwhale_api.music.tasks.import_job_run")
+    user = factories["users.User"](username="me")
+    path = os.path.join(DATA_DIR, "utf8-éà◌.ogg")
+    call_command("import_files", path, username="me", async=False, interactive=False)
+    batch = user.imports.latest("id")
     job = batch.jobs.first()
-    m.assert_called_once_with(
-        import_job_id=job.pk,
-        use_acoustid=False)
+    m.assert_called_once_with(import_job_id=job.pk, use_acoustid=False)
 
 
 def test_import_files_in_place(factories, mocker, settings):
     settings.MUSIC_DIRECTORY_PATH = DATA_DIR
-    m = mocker.patch('funkwhale_api.music.tasks.import_job_run')
-    user = factories['users.User'](username='me')
-    path = os.path.join(DATA_DIR, 'utf8-éà◌.ogg')
+    m = mocker.patch("funkwhale_api.music.tasks.import_job_run")
+    user = factories["users.User"](username="me")
+    path = os.path.join(DATA_DIR, "utf8-éà◌.ogg")
     call_command(
-        'import_files',
+        "import_files",
         path,
-        username='me',
+        username="me",
         async=False,
         in_place=True,
-        interactive=False)
-    batch = user.imports.latest('id')
+        interactive=False,
+    )
+    batch = user.imports.latest("id")
     job = batch.jobs.first()
     assert bool(job.audio_file) is False
-    m.assert_called_once_with(
-        import_job_id=job.pk,
-        use_acoustid=False)
+    m.assert_called_once_with(import_job_id=job.pk, use_acoustid=False)
 
 
 def test_storage_rename_utf_8_files(factories):
-    tf = factories['music.TrackFile'](audio_file__filename='été.ogg')
-    assert tf.audio_file.name.endswith('ete.ogg')
+    tf = factories["music.TrackFile"](audio_file__filename="été.ogg")
+    assert tf.audio_file.name.endswith("ete.ogg")
diff --git a/api/tests/test_jwt_querystring.py b/api/tests/test_jwt_querystring.py
index f18e6b7292839617cd61d6357725f6bd493462c4..18a673fb480d71fdea41dc60557dfd56b6c34aaa 100644
--- a/api/tests/test_jwt_querystring.py
+++ b/api/tests/test_jwt_querystring.py
@@ -5,18 +5,15 @@ jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
 jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
 
 
-def test_can_authenticate_using_token_param_in_url(
-        factories, preferences, client):
-    user = factories['users.User']()
-    preferences['common__api_authentication_required'] = True
-    url = reverse('api:v1:tracks-list')
+def test_can_authenticate_using_token_param_in_url(factories, preferences, client):
+    user = factories["users.User"]()
+    preferences["common__api_authentication_required"] = True
+    url = reverse("api:v1:tracks-list")
     response = client.get(url)
 
     assert response.status_code == 401
 
     payload = jwt_payload_handler(user)
     token = jwt_encode_handler(payload)
-    response = client.get(url, data={
-        'jwt': token
-    })
+    response = client.get(url, data={"jwt": token})
     assert response.status_code == 200
diff --git a/api/tests/test_tasks.py b/api/tests/test_tasks.py
index 16088d53ff5e1ceb75307d62bdaa178dc90d3778..d46c3a3fb36128cf673499ff6b05f4269d683da8 100644
--- a/api/tests/test_tasks.py
+++ b/api/tests/test_tasks.py
@@ -10,24 +10,25 @@ class Dummy:
 
 
 def test_require_instance_decorator(factories, mocker):
-    user = factories['users.User']()
+    user = factories["users.User"]()
 
-    @celery.require_instance(user.__class__, 'user')
+    @celery.require_instance(user.__class__, "user")
     def t(user):
         Dummy.noop(user)
 
-    m = mocker.patch.object(Dummy, 'noop')
+    m = mocker.patch.object(Dummy, "noop")
     t(user_id=user.pk)
 
     m.assert_called_once_with(user)
 
 
 def test_require_instance_decorator_accepts_qs(factories, mocker):
-    user = factories['users.User'](is_active=False)
+    user = factories["users.User"](is_active=False)
     qs = user.__class__.objects.filter(is_active=True)
 
-    @celery.require_instance(qs, 'user')
+    @celery.require_instance(qs, "user")
     def t(user):
         pass
+
     with pytest.raises(user.__class__.DoesNotExist):
         t(user_id=user.pk)
diff --git a/api/tests/test_youtube.py b/api/tests/test_youtube.py
index 7ab6256daf73352542acc2a8e62ca7860cd58df4..cb5559ce1c2a24fc16e5bdbc326fa3e6c717110d 100644
--- a/api/tests/test_youtube.py
+++ b/api/tests/test_youtube.py
@@ -1,6 +1,7 @@
-import json
 from collections import OrderedDict
+
 from django.urls import reverse
+
 from funkwhale_api.providers.youtube.client import client
 
 from .data import youtube as api_data
@@ -8,35 +9,36 @@ from .data import youtube as api_data
 
 def test_can_get_search_results_from_youtube(mocker):
     mocker.patch(
-        'funkwhale_api.providers.youtube.client._do_search',
-        return_value=api_data.search['8 bit adventure'])
-    query = '8 bit adventure'
+        "funkwhale_api.providers.youtube.client._do_search",
+        return_value=api_data.search["8 bit adventure"],
+    )
+    query = "8 bit adventure"
     results = client.search(query)
-    assert results[0]['id']['videoId'] == '0HxZn6CzOIo'
-    assert results[0]['snippet']['title'] == 'AdhesiveWombat - 8 Bit Adventure'
-    assert results[0]['full_url'] == 'https://www.youtube.com/watch?v=0HxZn6CzOIo'
+    assert results[0]["id"]["videoId"] == "0HxZn6CzOIo"
+    assert results[0]["snippet"]["title"] == "AdhesiveWombat - 8 Bit Adventure"
+    assert results[0]["full_url"] == "https://www.youtube.com/watch?v=0HxZn6CzOIo"
 
 
-def test_can_get_search_results_from_funkwhale(
-        preferences, mocker, api_client, db):
-    preferences['common__api_authentication_required'] = False
+def test_can_get_search_results_from_funkwhale(preferences, mocker, api_client, db):
+    preferences["common__api_authentication_required"] = False
     mocker.patch(
-        'funkwhale_api.providers.youtube.client._do_search',
-        return_value=api_data.search['8 bit adventure'])
-    query = '8 bit adventure'
-    url = reverse('api:v1:providers:youtube:search')
-    response = api_client.get(url, {'query': query})
+        "funkwhale_api.providers.youtube.client._do_search",
+        return_value=api_data.search["8 bit adventure"],
+    )
+    query = "8 bit adventure"
+    url = reverse("api:v1:providers:youtube:search")
+    response = api_client.get(url, {"query": query})
     # we should cast the youtube result to something more generic
     expected = {
         "id": "0HxZn6CzOIo",
         "url": "https://www.youtube.com/watch?v=0HxZn6CzOIo",
         "type": "youtube#video",
-        "description": "Make sure to apply adhesive evenly before use. GET IT HERE: http://adhesivewombat.bandcamp.com/album/marsupial-madness Facebook: ...",
+        "description": "Description",
         "channelId": "UCps63j3krzAG4OyXeEyuhFw",
         "title": "AdhesiveWombat - 8 Bit Adventure",
         "channelTitle": "AdhesiveWombat",
         "publishedAt": "2012-08-22T18:41:03.000Z",
-        "cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg"
+        "cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg",
     }
 
     assert response.data[0] == expected
@@ -44,55 +46,51 @@ def test_can_get_search_results_from_funkwhale(
 
 def test_can_send_multiple_queries_at_once(mocker):
     mocker.patch(
-        'funkwhale_api.providers.youtube.client._do_search',
+        "funkwhale_api.providers.youtube.client._do_search",
         side_effect=[
-            api_data.search['8 bit adventure'],
-            api_data.search['system of a down toxicity'],
-        ]
+            api_data.search["8 bit adventure"],
+            api_data.search["system of a down toxicity"],
+        ],
     )
 
     queries = OrderedDict()
-    queries['1'] = {
-        'q': '8 bit adventure',
-    }
-    queries['2'] = {
-        'q': 'system of a down toxicity',
-    }
+    queries["1"] = {"q": "8 bit adventure"}
+    queries["2"] = {"q": "system of a down toxicity"}
 
     results = client.search_multiple(queries)
 
-    assert results['1'][0]['id']['videoId'] == '0HxZn6CzOIo'
-    assert results['1'][0]['snippet']['title'] == 'AdhesiveWombat - 8 Bit Adventure'
-    assert results['1'][0]['full_url'] == 'https://www.youtube.com/watch?v=0HxZn6CzOIo'
-    assert results['2'][0]['id']['videoId'] == 'BorYwGi2SJc'
-    assert results['2'][0]['snippet']['title'] == 'System of a Down: Toxicity'
-    assert results['2'][0]['full_url'] == 'https://www.youtube.com/watch?v=BorYwGi2SJc'
+    assert results["1"][0]["id"]["videoId"] == "0HxZn6CzOIo"
+    assert results["1"][0]["snippet"]["title"] == "AdhesiveWombat - 8 Bit Adventure"
+    assert results["1"][0]["full_url"] == "https://www.youtube.com/watch?v=0HxZn6CzOIo"
+    assert results["2"][0]["id"]["videoId"] == "BorYwGi2SJc"
+    assert results["2"][0]["snippet"]["title"] == "System of a Down: Toxicity"
+    assert results["2"][0]["full_url"] == "https://www.youtube.com/watch?v=BorYwGi2SJc"
 
 
 def test_can_send_multiple_queries_at_once_from_funwkhale(
-        preferences, mocker, db, api_client):
-    preferences['common__api_authentication_required'] = False
+    preferences, mocker, db, api_client
+):
+    preferences["common__api_authentication_required"] = False
     mocker.patch(
-        'funkwhale_api.providers.youtube.client._do_search',
-        return_value=api_data.search['8 bit adventure'])
+        "funkwhale_api.providers.youtube.client._do_search",
+        return_value=api_data.search["8 bit adventure"],
+    )
     queries = OrderedDict()
-    queries['1'] = {
-        'q': '8 bit adventure',
-    }
+    queries["1"] = {"q": "8 bit adventure"}
 
     expected = {
         "id": "0HxZn6CzOIo",
         "url": "https://www.youtube.com/watch?v=0HxZn6CzOIo",
         "type": "youtube#video",
-        "description": "Make sure to apply adhesive evenly before use. GET IT HERE: http://adhesivewombat.bandcamp.com/album/marsupial-madness Facebook: ...",
+        "description": "Description",
         "channelId": "UCps63j3krzAG4OyXeEyuhFw",
         "title": "AdhesiveWombat - 8 Bit Adventure",
         "channelTitle": "AdhesiveWombat",
         "publishedAt": "2012-08-22T18:41:03.000Z",
-        "cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg"
+        "cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg",
     }
 
-    url = reverse('api:v1:providers:youtube:searchs')
-    response = api_client.post(url, queries, format='json')
+    url = reverse("api:v1:providers:youtube:searchs")
+    response = api_client.post(url, queries, format="json")
 
-    assert expected == response.data['1'][0]
+    assert expected == response.data["1"][0]
diff --git a/api/tests/users/test_activity.py b/api/tests/users/test_activity.py
index 26d0b11f8ab42ff7644e688a92e4693f755ff054..cfacff9975e9dbe35c81cbbb90f684bbd1bdcdfa 100644
--- a/api/tests/users/test_activity.py
+++ b/api/tests/users/test_activity.py
@@ -2,13 +2,14 @@ from funkwhale_api.users import serializers
 
 
 def test_get_user_activity_url(settings, factories):
-    user = factories['users.User']()
-    assert user.get_activity_url() == '{}/@{}'.format(
-        settings.FUNKWHALE_URL, user.username)
+    user = factories["users.User"]()
+    assert user.get_activity_url() == "{}/@{}".format(
+        settings.FUNKWHALE_URL, user.username
+    )
 
 
 def test_activity_user_serializer(factories):
-    user = factories['users.User']()
+    user = factories["users.User"]()
 
     expected = {
         "type": "Person",
diff --git a/api/tests/users/test_admin.py b/api/tests/users/test_admin.py
index 7645a02953b1caa44b6002a741fadf48c406449f..03b316eb0d9418422dc68690821a535b5389daac 100644
--- a/api/tests/users/test_admin.py
+++ b/api/tests/users/test_admin.py
@@ -3,28 +3,24 @@ from funkwhale_api.users.admin import MyUserCreationForm
 
 def test_clean_username_success(db):
     # Instantiate the form with a new username
-    form = MyUserCreationForm({
-        'username': 'alamode',
-        'password1': '123456',
-        'password2': '123456',
-    })
+    form = MyUserCreationForm(
+        {"username": "alamode", "password1": "123456", "password2": "123456"}
+    )
     # Run is_valid() to trigger the validation
     valid = form.is_valid()
     assert valid
 
     # Run the actual clean_username method
     username = form.clean_username()
-    assert 'alamode' == username
+    assert "alamode" == username
 
 
 def test_clean_username_false(factories):
-    user = factories['users.User']()
+    user = factories["users.User"]()
     # Instantiate the form with the same username as self.user
-    form = MyUserCreationForm({
-        'username': user.username,
-        'password1': '123456',
-        'password2': '123456',
-    })
+    form = MyUserCreationForm(
+        {"username": user.username, "password1": "123456", "password2": "123456"}
+    )
     # Run is_valid() to trigger the validation, which is going to fail
     # because the username is already taken
     valid = form.is_valid()
@@ -32,4 +28,4 @@ def test_clean_username_false(factories):
 
     # The form.errors dict should contain a single error called 'username'
     assert len(form.errors) == 1
-    assert 'username' in form.errors
+    assert "username" in form.errors
diff --git a/api/tests/users/test_jwt.py b/api/tests/users/test_jwt.py
index d264494e59bfb06f358e7dd83225cf4eef187c0f..83de757c8b1d49570f680f2673aebefb3e0803e5 100644
--- a/api/tests/users/test_jwt.py
+++ b/api/tests/users/test_jwt.py
@@ -1,13 +1,10 @@
 import pytest
-import uuid
-
 from jwt.exceptions import DecodeError
 from rest_framework_jwt.settings import api_settings
 
-from funkwhale_api.users.models import User
 
 def test_can_invalidate_token_when_changing_user_secret_key(factories):
-    user = factories['users.User']()
+    user = factories["users.User"]()
     u1 = user.secret_key
     jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
     jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py
index 42123b5e866eac282bb8f20b14bdc96ec3d16fe7..c73a4a1b1a4b4cd351278d7757fb2b0307e3b193 100644
--- a/api/tests/users/test_models.py
+++ b/api/tests/users/test_models.py
@@ -4,26 +4,26 @@ from funkwhale_api.users import models
 
 
 def test__str__(factories):
-    user = factories['users.User'](username='hello')
-    assert user.__str__() == 'hello'
+    user = factories["users.User"](username="hello")
+    assert user.__str__() == "hello"
 
 
 def test_changing_password_updates_subsonic_api_token_no_token(factories):
-    user = factories['users.User'](subsonic_api_token=None)
-    user.set_password('new')
+    user = factories["users.User"](subsonic_api_token=None)
+    user.set_password("new")
     assert user.subsonic_api_token is None
 
 
 def test_changing_password_updates_subsonic_api_token(factories):
-    user = factories['users.User'](subsonic_api_token='test')
-    user.set_password('new')
+    user = factories["users.User"](subsonic_api_token="test")
+    user.set_password("new")
 
     assert user.subsonic_api_token is not None
-    assert user.subsonic_api_token != 'test'
+    assert user.subsonic_api_token != "test"
 
 
 def test_get_permissions_superuser(factories):
-    user = factories['users.User'](is_superuser=True)
+    user = factories["users.User"](is_superuser=True)
 
     perms = user.get_permissions()
     for p in models.PERMISSIONS:
@@ -31,44 +31,50 @@ def test_get_permissions_superuser(factories):
 
 
 def test_get_permissions_regular(factories):
-    user = factories['users.User'](permission_library=True)
+    user = factories["users.User"](permission_library=True)
 
     perms = user.get_permissions()
     for p in models.PERMISSIONS:
-        if p == 'library':
+        if p == "library":
             assert perms[p] is True
         else:
             assert perms[p] is False
 
 
 def test_get_permissions_default(factories, preferences):
-    preferences['users__default_permissions'] = ['upload', 'federation']
-    user = factories['users.User']()
+    preferences["users__default_permissions"] = ["upload", "federation"]
+    user = factories["users.User"]()
 
     perms = user.get_permissions()
-    assert perms['upload'] is True
-    assert perms['federation'] is True
-    assert perms['library'] is False
-    assert perms['settings'] is False
-
-
-@pytest.mark.parametrize('args,perms,expected', [
-    ({'is_superuser': True}, ['federation', 'library'], True),
-    ({'is_superuser': False}, ['federation'], False),
-    ({'permission_library': True}, ['library'], True),
-    ({'permission_library': True}, ['library', 'federation'], False),
-])
+    assert perms["upload"] is True
+    assert perms["federation"] is True
+    assert perms["library"] is False
+    assert perms["settings"] is False
+
+
+@pytest.mark.parametrize(
+    "args,perms,expected",
+    [
+        ({"is_superuser": True}, ["federation", "library"], True),
+        ({"is_superuser": False}, ["federation"], False),
+        ({"permission_library": True}, ["library"], True),
+        ({"permission_library": True}, ["library", "federation"], False),
+    ],
+)
 def test_has_permissions_and(args, perms, expected, factories):
-    user = factories['users.User'](**args)
-    assert user.has_permissions(*perms, operator='and') is expected
-
-
-@pytest.mark.parametrize('args,perms,expected', [
-    ({'is_superuser': True}, ['federation', 'library'], True),
-    ({'is_superuser': False}, ['federation'], False),
-    ({'permission_library': True}, ['library', 'federation'], True),
-    ({'permission_library': True}, ['federation'], False),
-])
+    user = factories["users.User"](**args)
+    assert user.has_permissions(*perms, operator="and") is expected
+
+
+@pytest.mark.parametrize(
+    "args,perms,expected",
+    [
+        ({"is_superuser": True}, ["federation", "library"], True),
+        ({"is_superuser": False}, ["federation"], False),
+        ({"permission_library": True}, ["library", "federation"], True),
+        ({"permission_library": True}, ["federation"], False),
+    ],
+)
 def test_has_permissions_or(args, perms, expected, factories):
-    user = factories['users.User'](**args)
-    assert user.has_permissions(*perms, operator='or') is expected
+    user = factories["users.User"](**args)
+    assert user.has_permissions(*perms, operator="or") is expected
diff --git a/api/tests/users/test_permissions.py b/api/tests/users/test_permissions.py
index 518ccd1c803038cf0ab5c38a1438b44ef53c1180..7f72138f4a14394e3a65d53deda3ab1728f32473 100644
--- a/api/tests/users/test_permissions.py
+++ b/api/tests/users/test_permissions.py
@@ -7,76 +7,86 @@ from funkwhale_api.users import permissions
 def test_has_user_permission_no_user(api_request):
     view = APIView.as_view()
     permission = permissions.HasUserPermission()
-    request = api_request.get('/')
+    request = api_request.get("/")
     assert permission.has_permission(request, view) is False
 
 
 def test_has_user_permission_anonymous(anonymous_user, api_request):
     view = APIView.as_view()
     permission = permissions.HasUserPermission()
-    request = api_request.get('/')
-    setattr(request, 'user', anonymous_user)
+    request = api_request.get("/")
+    setattr(request, "user", anonymous_user)
     assert permission.has_permission(request, view) is False
 
 
-@pytest.mark.parametrize('value', [True, False])
+@pytest.mark.parametrize("value", [True, False])
 def test_has_user_permission_logged_in_single(value, factories, api_request):
-    user = factories['users.User'](permission_federation=value)
+    user = factories["users.User"](permission_federation=value)
 
     class View(APIView):
-        required_permissions = ['federation']
+        required_permissions = ["federation"]
+
     view = View()
     permission = permissions.HasUserPermission()
-    request = api_request.get('/')
-    setattr(request, 'user', user)
+    request = api_request.get("/")
+    setattr(request, "user", user)
     result = permission.has_permission(request, view)
-    assert result == user.has_permissions('federation') == value
+    assert result == user.has_permissions("federation") == value
 
 
-@pytest.mark.parametrize('federation,library,expected', [
-    (True, False, False),
-    (False, True, False),
-    (False, False, False),
-    (True, True, True),
-])
+@pytest.mark.parametrize(
+    "federation,library,expected",
+    [
+        (True, False, False),
+        (False, True, False),
+        (False, False, False),
+        (True, True, True),
+    ],
+)
 def test_has_user_permission_logged_in_multiple_and(
-        federation, library, expected, factories, api_request):
-    user = factories['users.User'](
-        permission_federation=federation,
-        permission_library=library,
+    federation, library, expected, factories, api_request
+):
+    user = factories["users.User"](
+        permission_federation=federation, permission_library=library
     )
 
     class View(APIView):
-        required_permissions = ['federation', 'library']
-        permission_operator = 'and'
+        required_permissions = ["federation", "library"]
+        permission_operator = "and"
+
     view = View()
     permission = permissions.HasUserPermission()
-    request = api_request.get('/')
-    setattr(request, 'user', user)
+    request = api_request.get("/")
+    setattr(request, "user", user)
     result = permission.has_permission(request, view)
-    assert result == user.has_permissions('federation', 'library') == expected
+    assert result == user.has_permissions("federation", "library") == expected
 
 
-@pytest.mark.parametrize('federation,library,expected', [
-    (True, False, True),
-    (False, True, True),
-    (False, False, False),
-    (True, True, True),
-])
+@pytest.mark.parametrize(
+    "federation,library,expected",
+    [
+        (True, False, True),
+        (False, True, True),
+        (False, False, False),
+        (True, True, True),
+    ],
+)
 def test_has_user_permission_logged_in_multiple_or(
-        federation, library, expected, factories, api_request):
-    user = factories['users.User'](
-        permission_federation=federation,
-        permission_library=library,
+    federation, library, expected, factories, api_request
+):
+    user = factories["users.User"](
+        permission_federation=federation, permission_library=library
     )
 
     class View(APIView):
-        required_permissions = ['federation', 'library']
-        permission_operator = 'or'
+        required_permissions = ["federation", "library"]
+        permission_operator = "or"
+
     view = View()
     permission = permissions.HasUserPermission()
-    request = api_request.get('/')
-    setattr(request, 'user', user)
+    request = api_request.get("/")
+    setattr(request, "user", user)
     result = permission.has_permission(request, view)
-    assert result == user.has_permissions(
-        'federation', 'library', operator='or') == expected
+    has_permission_result = user.has_permissions("federation", "library", operator="or")
+
+    assert result == has_permission_result == expected
diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py
index 6418889ce4f0903480f7583cf85c6542338b58cb..00272c2aea76026c3df2b125c39fdee852acf1ec 100644
--- a/api/tests/users/test_views.py
+++ b/api/tests/users/test_views.py
@@ -1,98 +1,87 @@
-import json
 import pytest
-
-from django.test import RequestFactory
 from django.urls import reverse
 
 from funkwhale_api.users.models import User
 
 
 def test_can_create_user_via_api(preferences, api_client, db):
-    url = reverse('rest_register')
+    url = reverse("rest_register")
     data = {
-        'username': 'test1',
-        'email': 'test1@test.com',
-        'password1': 'testtest',
-        'password2': 'testtest',
+        "username": "test1",
+        "email": "test1@test.com",
+        "password1": "testtest",
+        "password2": "testtest",
     }
-    preferences['users__registration_enabled'] = True
+    preferences["users__registration_enabled"] = True
     response = api_client.post(url, data)
     assert response.status_code == 201
 
-    u = User.objects.get(email='test1@test.com')
-    assert u.username == 'test1'
+    u = User.objects.get(email="test1@test.com")
+    assert u.username == "test1"
 
 
 def test_can_restrict_usernames(settings, preferences, db, api_client):
-    url = reverse('rest_register')
-    preferences['users__registration_enabled'] = True
-    settings.USERNAME_BLACKLIST = ['funkwhale']
+    url = reverse("rest_register")
+    preferences["users__registration_enabled"] = True
+    settings.USERNAME_BLACKLIST = ["funkwhale"]
     data = {
-        'username': 'funkwhale',
-        'email': 'contact@funkwhale.io',
-        'password1': 'testtest',
-        'password2': 'testtest',
+        "username": "funkwhale",
+        "email": "contact@funkwhale.io",
+        "password1": "testtest",
+        "password2": "testtest",
     }
 
     response = api_client.post(url, data)
 
     assert response.status_code == 400
-    assert 'username' in response.data
+    assert "username" in response.data
 
 
 def test_can_disable_registration_view(preferences, api_client, db):
-    url = reverse('rest_register')
+    url = reverse("rest_register")
     data = {
-        'username': 'test1',
-        'email': 'test1@test.com',
-        'password1': 'testtest',
-        'password2': 'testtest',
+        "username": "test1",
+        "email": "test1@test.com",
+        "password1": "testtest",
+        "password2": "testtest",
     }
-    preferences['users__registration_enabled'] = False
+    preferences["users__registration_enabled"] = False
     response = api_client.post(url, data)
     assert response.status_code == 403
 
 
 def test_can_fetch_data_from_api(api_client, factories):
-    url = reverse('api:v1:users:users-me')
+    url = reverse("api:v1:users:users-me")
     response = api_client.get(url)
     # login required
     assert response.status_code == 401
 
-    user = factories['users.User'](
-        permission_library=True
-    )
-    api_client.login(username=user.username, password='test')
+    user = factories["users.User"](permission_library=True)
+    api_client.login(username=user.username, password="test")
     response = api_client.get(url)
     assert response.status_code == 200
-    assert response.data['username'] == user.username
-    assert response.data['is_staff'] == user.is_staff
-    assert response.data['is_superuser'] == user.is_superuser
-    assert response.data['email'] == user.email
-    assert response.data['name'] == user.name
-    assert response.data['permissions'] == user.get_permissions()
+    assert response.data["username"] == user.username
+    assert response.data["is_staff"] == user.is_staff
+    assert response.data["is_superuser"] == user.is_superuser
+    assert response.data["email"] == user.email
+    assert response.data["name"] == user.name
+    assert response.data["permissions"] == user.get_permissions()
 
 
 def test_can_get_token_via_api(api_client, factories):
-    user = factories['users.User']()
-    url = reverse('api:v1:token')
-    payload = {
-        'username': user.username,
-        'password': 'test'
-    }
+    user = factories["users.User"]()
+    url = reverse("api:v1:token")
+    payload = {"username": user.username, "password": "test"}
 
     response = api_client.post(url, payload)
     assert response.status_code == 200
-    assert 'token' in response.data
+    assert "token" in response.data
 
 
 def test_can_get_token_via_api_inactive(api_client, factories):
-    user = factories['users.User'](is_active=False)
-    url = reverse('api:v1:token')
-    payload = {
-        'username': user.username,
-        'password': 'test'
-    }
+    user = factories["users.User"](is_active=False)
+    url = reverse("api:v1:token")
+    payload = {"username": user.username, "password": "test"}
 
     response = api_client.post(url, payload)
     assert response.status_code == 400
@@ -100,36 +89,29 @@ def test_can_get_token_via_api_inactive(api_client, factories):
 
 def test_can_refresh_token_via_api(api_client, factories, mocker):
     # first, we get a token
-    user = factories['users.User']()
-    url = reverse('api:v1:token')
-    payload = {
-        'username': user.username,
-        'password': 'test'
-    }
+    user = factories["users.User"]()
+    url = reverse("api:v1:token")
+    payload = {"username": user.username, "password": "test"}
 
     response = api_client.post(url, payload)
     assert response.status_code == 200
 
-    token = response.data['token']
-    url = reverse('api:v1:token_refresh')
-    response = api_client.post(url, {'token': token})
+    token = response.data["token"]
+    url = reverse("api:v1:token_refresh")
+    response = api_client.post(url, {"token": token})
 
     assert response.status_code == 200
-    assert 'token' in response.data
+    assert "token" in response.data
 
 
 def test_changing_password_updates_secret_key(logged_in_api_client):
     user = logged_in_api_client.user
     password = user.password
     secret_key = user.secret_key
-    payload = {
-        'old_password': 'test',
-        'new_password1': 'new',
-        'new_password2': 'new',
-    }
-    url = reverse('change_password')
+    payload = {"old_password": "test", "new_password1": "new", "new_password2": "new"}
+    url = reverse("change_password")
 
-    response = logged_in_api_client.post(url, payload)
+    logged_in_api_client.post(url, payload)
 
     user.refresh_from_db()
 
@@ -137,14 +119,11 @@ def test_changing_password_updates_secret_key(logged_in_api_client):
     assert user.password != password
 
 
-def test_can_request_password_reset(
-        factories, api_client, mailoutbox):
-    user = factories['users.User']()
-    payload = {
-        'email': user.email,
-    }
+def test_can_request_password_reset(factories, api_client, mailoutbox):
+    user = factories["users.User"]()
+    payload = {"email": user.email}
     emails = len(mailoutbox)
-    url = reverse('rest_password_reset')
+    url = reverse("rest_password_reset")
 
     response = api_client.post(url, payload)
     assert response.status_code == 200
@@ -153,86 +132,58 @@ def test_can_request_password_reset(
 
 def test_user_can_patch_his_own_settings(logged_in_api_client):
     user = logged_in_api_client.user
-    payload = {
-        'privacy_level': 'me',
-    }
-    url = reverse(
-        'api:v1:users:users-detail',
-        kwargs={'username': user.username})
+    payload = {"privacy_level": "me"}
+    url = reverse("api:v1:users:users-detail", kwargs={"username": user.username})
 
     response = logged_in_api_client.patch(url, payload)
 
     assert response.status_code == 200
     user.refresh_from_db()
 
-    assert user.privacy_level == 'me'
+    assert user.privacy_level == "me"
 
 
 def test_user_can_request_new_subsonic_token(logged_in_api_client):
     user = logged_in_api_client.user
-    user.subsonic_api_token = 'test'
+    user.subsonic_api_token = "test"
     user.save()
 
     url = reverse(
-        'api:v1:users:users-subsonic-token',
-        kwargs={'username': user.username})
+        "api:v1:users:users-subsonic-token", kwargs={"username": user.username}
+    )
 
     response = logged_in_api_client.post(url)
 
     assert response.status_code == 200
     user.refresh_from_db()
-    assert user.subsonic_api_token != 'test'
+    assert user.subsonic_api_token != "test"
     assert user.subsonic_api_token is not None
-    assert response.data == {
-        'subsonic_api_token': user.subsonic_api_token
-    }
+    assert response.data == {"subsonic_api_token": user.subsonic_api_token}
 
 
-def test_user_can_get_new_subsonic_token(logged_in_api_client):
+def test_user_can_get_subsonic_token(logged_in_api_client):
     user = logged_in_api_client.user
-    user.subsonic_api_token = 'test'
+    user.subsonic_api_token = "test"
     user.save()
 
     url = reverse(
-        'api:v1:users:users-subsonic-token',
-        kwargs={'username': user.username})
+        "api:v1:users:users-subsonic-token", kwargs={"username": user.username}
+    )
 
     response = logged_in_api_client.get(url)
 
     assert response.status_code == 200
-    assert response.data == {
-        'subsonic_api_token': 'test'
-    }
-
-
-def test_user_can_request_new_subsonic_token(logged_in_api_client):
-    user = logged_in_api_client.user
-    user.subsonic_api_token = 'test'
-    user.save()
-
-    url = reverse(
-        'api:v1:users:users-subsonic-token',
-        kwargs={'username': user.username})
-
-    response = logged_in_api_client.post(url)
-
-    assert response.status_code == 200
-    user.refresh_from_db()
-    assert user.subsonic_api_token != 'test'
-    assert user.subsonic_api_token is not None
-    assert response.data == {
-        'subsonic_api_token': user.subsonic_api_token
-    }
+    assert response.data == {"subsonic_api_token": "test"}
 
 
 def test_user_can_delete_subsonic_token(logged_in_api_client):
     user = logged_in_api_client.user
-    user.subsonic_api_token = 'test'
+    user.subsonic_api_token = "test"
     user.save()
 
     url = reverse(
-        'api:v1:users:users-subsonic-token',
-        kwargs={'username': user.username})
+        "api:v1:users:users-subsonic-token", kwargs={"username": user.username}
+    )
 
     response = logged_in_api_client.delete(url)
 
@@ -241,16 +192,11 @@ def test_user_can_delete_subsonic_token(logged_in_api_client):
     assert user.subsonic_api_token is None
 
 
-@pytest.mark.parametrize('method', ['put', 'patch'])
-def test_user_cannot_patch_another_user(
-        method, logged_in_api_client, factories):
-    user = factories['users.User']()
-    payload = {
-        'privacy_level': 'me',
-    }
-    url = reverse(
-        'api:v1:users:users-detail',
-        kwargs={'username': user.username})
+@pytest.mark.parametrize("method", ["put", "patch"])
+def test_user_cannot_patch_another_user(method, logged_in_api_client, factories):
+    user = factories["users.User"]()
+    payload = {"privacy_level": "me"}
+    url = reverse("api:v1:users:users-detail", kwargs={"username": user.username})
 
     handler = getattr(logged_in_api_client, method)
     response = handler(url, payload)
diff --git a/deploy/apache.conf b/deploy/apache.conf
index 5b74efecdc029594856ac95f9811daa6cb313fae..5f86db7bd533846577dc6ae140249b1239a4387e 100644
--- a/deploy/apache.conf
+++ b/deploy/apache.conf
@@ -4,12 +4,10 @@ Define funkwhale-sn funkwhale.yourdomain.com
 # Following variables should be modified according to your setup and if you
 # use different configuration than what is described in our installation guide.
 Define funkwhale-api http://localhost:5000
+Define funkwhale-api-ws ws://localhost:5000
 Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music
-# websockets are not working yet
-# Define funkwhale-api-ws ws://localhost:5000
 
-
-# HTTP request redirected to HTTPS
+# HTTP requests redirected to HTTPS
 <VirtualHost *:80>
    ServerName ${funkwhale-sn}
 
@@ -22,7 +20,6 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music
       Options None
       Require all granted
    </Location>
-
 </VirtualHost>
 
 
@@ -39,13 +36,15 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music
    # lines and move the configuration to the previous server block if you
    # don't want to run funkwhale behind https (this is not recommanded)
    # have a look here for let's encrypt configuration:
-   # https://certbot.eff.org/all-instructions/#debian-9-stretch-nginx
+   # https://certbot.eff.org/lets-encrypt/debianstretch-apache.html
    SSLEngine on
    SSLProxyEngine On
    SSLCertificateFile /etc/letsencrypt/live/${funkwhale-sn}/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/${funkwhale-sn}/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf
 
+   # Tell the api that the client is using https
+   RequestHeader set X-Forwarded-Proto "https"
 
    DocumentRoot /srv/funkwhale/front/dist
 
@@ -69,8 +68,8 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music
       Allow from all
    </Proxy>
 
-   # Activating WebSockets (not working)
-   # ProxyPass "/api/v1/instance/activity"  "ws://localhost:5000/api/v1/instance/activity"
+   # Activating WebSockets
+   ProxyPass "/api/v1/instance/activity"  ${funkwhale-api-ws}/api/v1/instance/activity
 
    <Location "/api">
       # similar to nginx 'client_max_body_size 30M;'
@@ -112,6 +111,12 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music
       Require all granted
    </Directory>
 
+   <Directory /srv/funkwhale/data/media/albums>
+      Options FollowSymLinks
+      AllowOverride None
+      Require all granted
+   </Directory>
+
    # XSendFile is serving audio files
    # WARNING : permissions on paths specified below overrides previous definition,
    # everything under those paths is potentially exposed.
@@ -123,6 +128,5 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music
       XSendFilePath ${MUSIC_DIRECTORY_PATH}
       SetEnv MOD_X_SENDFILE_ENABLED 1
    </IfModule>
-
 </VirtualHost>
 </IfModule>
diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample
index 42659a0dabc33cf2da439b21f06f77163c544722..64ff3d4dfb7c1bdeaf7841a24c19aa668f9ad2f3 100644
--- a/deploy/env.prod.sample
+++ b/deploy/env.prod.sample
@@ -113,4 +113,4 @@ RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f
 # You can safely leave those settings uncommented if you don't plan to use
 # in place imports.
 # MUSIC_DIRECTORY_PATH=
-# MUSIC_DIRECTORY_SERVE_PATH=
+# MUSIC_DIRECTORY_SERVE_PATH=  # docker-only
diff --git a/deploy/nginx.conf b/deploy/nginx.conf
index 66851321fb43017af8b893319940562309b5b604..b403f4388f9bf69399ed3479715c778fc1b837e6 100644
--- a/deploy/nginx.conf
+++ b/deploy/nginx.conf
@@ -79,18 +79,11 @@ server {
         alias /srv/funkwhale/data/media/;
     }
 
-    location /_protected/media {
-        # this is an internal location that is used to serve
-        # audio files once correct permission / authentication
-        # has been checked on API side
-        internal;
-        alias   /srv/funkwhale/data/media;
-    }
-
     location /_protected/music {
         # this is an internal location that is used to serve
         # audio files once correct permission / authentication
         # has been checked on API side
+        # Set this to the same value as your MUSIC_DIRECTORY_PATH setting
         internal;
         alias   /srv/funkwhale/data/music;
     }
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 46756bb266ccf918314f57b8ef8b01dcb68409ce..7b7751fc61ee9827fb2f1267060e114682f32907 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -97,6 +97,12 @@ for this value. For non-docker installation, you can use any absolute path.
 
 .. note:: This path should not include any trailing slash
 
+.. warning::
+
+   You need to adapt your :ref:`reverse-proxy configuration<reverse-proxy-setup>` to
+   serve the directory pointed by ``MUSIC_DIRECTORY_PATH`` on
+   ``/_protected/music`` URL.
+
 .. _setting-MUSIC_DIRECTORY_SERVE_PATH:
 
 ``MUSIC_DIRECTORY_SERVE_PATH``
@@ -112,7 +118,7 @@ in your :file:`docker-compose.yml` file::
       - /srv/funkwhale/data/music:/music:ro
 
 Then, the value of :ref:`setting-MUSIC_DIRECTORY_SERVE_PATH` should be
-``/srv/funkwhale/data``. This must be readable by the webserver.
+``/srv/funkwhale/data/music``. This must be readable by the webserver.
 
 On non-docker setup, you don't need to configure this setting.
 
diff --git a/docs/installation/debian.rst b/docs/installation/debian.rst
index eb0c3f0eaca309b0992bb876a5652e4a3f1011a5..be17ab1e07aabe944389180fcef8771dfb2f3ca7 100644
--- a/docs/installation/debian.rst
+++ b/docs/installation/debian.rst
@@ -3,8 +3,7 @@ Debian installation
 
 .. note::
 
-    this guide targets Debian 9, which is the latest debian, but should work
-    similarly on Debian 8.
+    This guide targets Debian 9 (Stretch), which is the latest Debian.
 
 External dependencies
 ---------------------
@@ -23,7 +22,7 @@ default on system. You can install them using:
 .. code-block:: shell
 
     sudo apt-get update
-    sudo apt-get install curl python3-venv git unzip
+    sudo apt-get install curl python3-pip python3-venv git unzip
 
 
 Layout
@@ -90,7 +89,7 @@ First, we'll download the latest api release.
     curl -L -o "api-|version|.zip" "https://code.eliotberriot.com/funkwhale/funkwhale/-/jobs/artifacts/|version|/download?job=build_api"
     unzip "api-|version|.zip" -d extracted
     mv extracted/api/* api/
-    rmdir extracted
+    rm -rf extracted
 
 
 Then we'll download the frontend files:
@@ -240,6 +239,14 @@ This will create the required tables and rows.
     You can safely execute this command any time you want, this will only
     run unapplied migrations.
 
+.. warning::
+
+    You may sometimes get the following warning while applying migrations::
+
+        "Your models have changes that are not yet reflected in a migration, and so won't be applied."
+
+    This is a warning, not an error, and it can be safely ignored.
+    Never run the ``makemigrations`` command yourself.
 
 Create an admin account
 -----------------------
diff --git a/docs/installation/docker.rst b/docs/installation/docker.rst
index dc031caed91b8c169ff0bd68827865c432f3504d..e0520180064473d9362c85e0107f2dbdba23dad3 100644
--- a/docs/installation/docker.rst
+++ b/docs/installation/docker.rst
@@ -36,6 +36,15 @@ Run the database container and the initial migrations:
     docker-compose up -d postgres
     docker-compose run --rm api python manage.py migrate
 
+.. warning::
+
+    You may sometimes get the following warning while applying migrations::
+
+        "Your models have changes that are not yet reflected in a migration, and so won't be applied."
+
+    This is a warning, not an error, and it can be safely ignored.
+    Never run the ``makemigrations`` command yourself.
+
 Create your admin user:
 
 .. code-block:: bash
diff --git a/docs/installation/external_dependencies.rst b/docs/installation/external_dependencies.rst
index 39d32b38fdaf2c3e071665341164b12e77dc9309..adfb90e72e9f2011dc5d3334a6c3fdfa2a3aef9e 100644
--- a/docs/installation/external_dependencies.rst
+++ b/docs/installation/external_dependencies.rst
@@ -32,10 +32,8 @@ Create the project database and user:
 
 .. code-block:: shell
 
-    CREATE DATABASE "scratch"
-      WITH ENCODING 'utf8'
-      LC_COLLATE = 'en_US.utf8'
-      LC_CTYPE = 'en_US.utf8';
+    CREATE DATABASE "funkwhale"
+      WITH ENCODING 'utf8';
     CREATE USER funkwhale;
     GRANT ALL PRIVILEGES ON DATABASE funkwhale TO funkwhale;
 
@@ -58,7 +56,7 @@ for funkwhale to work properly:
 
 .. code-block:: shell
 
-    sudo -u postgres psql -c 'CREATE EXTENSION "unaccent";'
+    sudo -u postgres psql funkwhale -c 'CREATE EXTENSION "unaccent";'
 
 
 Cache setup (Redis)
diff --git a/docs/installation/index.rst b/docs/installation/index.rst
index 0628fe17f5c6eac184427dab7d5d8ddb00a3883f..034f8e9ba30d9f3b22c19ba0fd475c80f5acac2e 100644
--- a/docs/installation/index.rst
+++ b/docs/installation/index.rst
@@ -124,15 +124,6 @@ If everything is fine, you can restart your nginx server with ``service nginx re
 Apache2
 ^^^^^^^
 
-.. note::
-
-    Apache2 support is still very recent and the following features
-    are not working yet:
-
-    - Websocket (used for real-time updates on Instance timeline)
-
-    Those features are not necessary to use your Funkwhale instance.
-
 Ensure you have a recent version of apache2 installed on your server.
 You'll also need the following dependencies::
 
diff --git a/docs/upgrading.rst b/docs/upgrading.rst
index bd3d5578f3904ecf4edd73d9360cbcaa46d768bf..1b092d74706995c335e73aa2d4346c24ec0e4300 100644
--- a/docs/upgrading.rst
+++ b/docs/upgrading.rst
@@ -37,6 +37,14 @@ easy:
     # Relaunch the containers
     docker-compose up -d
 
+.. warning::
+
+    You may sometimes get the following warning while applying migrations::
+
+        "Your models have changes that are not yet reflected in a migration, and so won't be applied."
+
+    This is a warning, not an error, and it can be safely ignored.
+    Never run the ``makemigrations`` command yourself.
 
 
 Non-docker setup
@@ -95,3 +103,12 @@ match what is described in :doc:`debian`:
 
     # restart the services
     sudo systemctl restart funkwhale.target
+
+.. warning::
+
+    You may sometimes get the following warning while applying migrations::
+
+        "Your models have changes that are not yet reflected in a migration, and so won't be applied."
+
+    This is a warning, not an error, and it can be safely ignored.
+    Never run the ``makemigrations`` command yourself.
diff --git a/front/src/App.vue b/front/src/App.vue
index 673f8386460ecba32737c129e3421adc06881f04..2eb673ab4bf1800920111616091aa80cffba1c08 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -1,6 +1,7 @@
 <template>
   <div id="app">
     <sidebar></sidebar>
+    <service-messages v-if="messages.length > 0" />
     <router-view :key="$route.fullPath"></router-view>
     <div class="ui fitted divider"></div>
     <div id="footer" class="ui vertical footer segment">
@@ -44,9 +45,11 @@
 <script>
 import axios from 'axios'
 import _ from 'lodash'
+import {mapState} from 'vuex'
 
 import Sidebar from '@/components/Sidebar'
 import Raven from '@/components/Raven'
+import ServiceMessages from '@/components/ServiceMessages'
 
 import PlaylistModal from '@/components/playlists/PlaylistModal'
 
@@ -55,7 +58,8 @@ export default {
   components: {
     Sidebar,
     Raven,
-    PlaylistModal
+    PlaylistModal,
+    ServiceMessages
   },
   data () {
     return {
@@ -80,6 +84,9 @@ export default {
     }
   },
   computed: {
+    ...mapState({
+      messages: state => state.ui.messages
+    }),
     version () {
       if (!this.nodeinfo) {
         return null
@@ -115,6 +122,14 @@ html, body {
   }
   transform: none !important;
 }
+.service-messages {
+  position: fixed;
+  bottom: 1em;
+  left: 1em;
+  @include media(">desktop") {
+    left: 350px;
+  }
+}
 .main-pusher {
   padding: 1.5rem 0;
 }
@@ -154,4 +169,15 @@ html, body {
 .floated.buttons .button ~ .dropdown {
   border-left: none;
 }
+
+.ui.icon.header .circular.icon {
+  display: flex;
+  justify-content: center;
+  
+}
+
+.segment-content .button{
+  margin:  0.5em;
+}
+
 </style>
diff --git a/front/src/components/ServiceMessages.vue b/front/src/components/ServiceMessages.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0a3be99e5becb1f0efd13ec60d212a5f32233068
--- /dev/null
+++ b/front/src/components/ServiceMessages.vue
@@ -0,0 +1,82 @@
+<template>
+  <div class="service-messages">
+    <message v-for="message in displayedMessages" :key="String(message.date)" :class="['large', getLevel(message)]">
+      <p>{{ message.content }}</p>
+    </message>
+  </div>
+</template>
+
+<script>
+import {mapState} from 'vuex'
+
+export default {
+  data () {
+    return {
+      date: new Date(),
+      interval: null
+    }
+  },
+  created () {
+    this.setupInterval()
+  },
+  destroyed () {
+    if (this.interval) {
+      clearInterval(this.interval)
+    }
+  },
+  computed: {
+    ...mapState({
+      messages: state => state.ui.messages,
+      displayDuration: state => state.ui.messageDisplayDuration
+    }),
+    displayedMessages () {
+      let now = this.date
+      let interval = this.displayDuration
+      let toDisplay = this.messages.filter(m => {
+        return now - m.date <= interval
+      })
+      return toDisplay.slice(0, 3)
+    }
+  },
+  methods: {
+    setupInterval () {
+      if (this.interval) {
+        return
+      }
+      let self = this
+      this.interval = setInterval(() => {
+        if (self.displayedMessages.length === 0) {
+          clearInterval(self.interval)
+          this.interval = null
+        }
+        self.date = new Date()
+      }, 1000)
+    },
+    getLevel (message) {
+      return message.level || 'info'
+    }
+  },
+  watch: {
+    messages: {
+      handler (v) {
+        if (v.length > 0 && !this.interval) {
+          this.setupInterval()
+        }
+      },
+      deep: true
+    }
+  }
+}
+</script>
+
+<style>
+.service-messages {
+  z-index: 9999;
+  margin-left: 1em;
+  min-width: 20em;
+  max-width: 40em;
+}
+.service-messages .message:last-child {
+  margin-bottom: 0;
+}
+</style>
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 72c55847fa3ed09c1cd428cba0347495f3608903..d46fb846cf1d76fd23265b18f1fd34a6e361dfe7 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -272,7 +272,7 @@ export default {
         this.scrollToCurrent()
       }
     },
-    '$store.state.availablePermissions': {
+    '$store.state.auth.availablePermissions': {
       handler () {
         this.fetchNotificationsCount()
       },
diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue
index 28a8900841afc29fdefe844b3b8c473420f7809c..9777fa83ca2d52605e39d37219e25694e49b614c 100644
--- a/front/src/components/audio/PlayButton.vue
+++ b/front/src/components/audio/PlayButton.vue
@@ -124,19 +124,28 @@ export default {
     add () {
       let self = this
       this.getPlayableTracks().then((tracks) => {
-        self.$store.dispatch('queue/appendMany', {tracks: tracks})
+        self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => self.addMessage(tracks))
       })
     },
     addNext (next) {
       let self = this
       let wasEmpty = this.$store.state.queue.tracks.length === 0
       this.getPlayableTracks().then((tracks) => {
-        self.$store.dispatch('queue/appendMany', {tracks: tracks, index: self.$store.state.queue.currentIndex + 1})
+        self.$store.dispatch('queue/appendMany', {tracks: tracks, index: self.$store.state.queue.currentIndex + 1}).then(() => self.addMessage(tracks))
         let goNext = next && !wasEmpty
         if (goNext) {
           self.$store.dispatch('queue/next')
         }
       })
+    },
+    addMessage (tracks) {
+      if (tracks.length < 1) {
+        return
+      }
+      this.$store.commit('ui/addMessage', {
+        content: this.$t('{% tracks %} tracks were added to your queue.', {tracks: tracks.length}),
+        date: new Date()
+      })
     }
   }
 }
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index c475ec684a008ec46392361523eefc77ab38b0e6..3c922e14ad323d75413d969ba7c033add19e92e0 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -113,7 +113,8 @@
           :disabled="queue.tracks.length === 0"
           :title="$t('Shuffle your queue')"
           class="two wide column control">
-          <i @click="shuffle()" :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
+          <div v-if="isShuffling" class="ui inline shuffling inverted small active loader"></div>
+          <i v-else @click="shuffle()" :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
         </div>
         <div class="one wide column"></div>
         <div
@@ -158,6 +159,7 @@ export default {
   data () {
     let defaultAmbiantColors = [[46, 46, 46], [46, 46, 46], [46, 46, 46], [46, 46, 46]]
     return {
+      isShuffling: false,
       renderAudio: true,
       sliderVolume: this.volume,
       Track: Track,
@@ -173,9 +175,24 @@ export default {
     ...mapActions({
       togglePlay: 'player/togglePlay',
       clean: 'queue/clean',
-      shuffle: 'queue/shuffle',
       updateProgress: 'player/updateProgress'
     }),
+    shuffle () {
+      if (this.isShuffling) {
+        return
+      }
+      let self = this
+      this.isShuffling = true
+      setTimeout(() => {
+        self.$store.dispatch('queue/shuffle', () => {
+          self.isShuffling = false
+          self.$store.commit('ui/addMessage', {
+            content: self.$t('Queue shuffled!'),
+            date: new Date()
+          })
+        })
+      }, 100)
+    },
     next () {
       let self = this
       this.$store.dispatch('queue/next').then(() => {
@@ -402,5 +419,8 @@ export default {
 .ui.feed.icon {
   margin: 0;
 }
+.shuffling.loader.inline {
+  margin: 0;
+}
 
 </style>
diff --git a/front/src/components/auth/Login.vue b/front/src/components/auth/Login.vue
index f3add57b1cccd736c82eb4f749764a9c0229ab2f..8286860bcaa051fdf2ee844dc878805c3402dcfd 100644
--- a/front/src/components/auth/Login.vue
+++ b/front/src/components/auth/Login.vue
@@ -79,8 +79,6 @@ export default {
         username: this.credentials.username,
         password: this.credentials.password
       }
-      // We need to pass the component's this context
-      // to properly make use of http in the auth service
       this.$store.dispatch('auth/login', {
         credentials,
         next: '/library',
diff --git a/front/src/components/common/Message.vue b/front/src/components/common/Message.vue
new file mode 100644
index 0000000000000000000000000000000000000000..772071db78c98881dab84405b2bc0903beff8106
--- /dev/null
+++ b/front/src/components/common/Message.vue
@@ -0,0 +1,36 @@
+<template>
+  <div class="ui message">
+    <div class="content">
+      <slot></slot>
+    </div>
+    <i class="close icon"></i>
+  </div>
+</template>
+<script>
+import $ from 'jquery'
+
+export default {
+  mounted () {
+    let self = this
+    $(this.$el).find('.close.icon').on('click', function () {
+      $(self.$el).transition('fade', 125)
+    })
+    $(this.$el).on('click', function () {
+      $(self.$el).transition('fade', 125)
+    })
+  }
+}
+</script>
+<style scoped>
+.ui.message .content {
+  padding-right: 0.5em;
+  cursor: pointer;
+}
+.ui.message .content :first-child {
+  margin-top: 0;
+}
+
+.ui.message .content :last-child {
+  margin-bottom: 0;
+}
+</style>
diff --git a/front/src/components/globals.js b/front/src/components/globals.js
index 79bbcf1b93a4a74ecf3c3b3d3d4e724870d7120c..4ad09f70425a987fd99e86f6e4277cd07d797605 100644
--- a/front/src/components/globals.js
+++ b/front/src/components/globals.js
@@ -12,4 +12,8 @@ import DangerousButton from '@/components/common/DangerousButton'
 
 Vue.component('dangerous-button', DangerousButton)
 
+import Message from '@/components/common/Message'
+
+Vue.component('message', Message)
+
 export default {}
diff --git a/front/src/components/library/import/FileUpload.vue b/front/src/components/library/import/FileUpload.vue
index 48ca0ad84adf41744303ed363d070e34daaed2bd..fb88c006d3a53a33116d64a1044ee1c0b7802dac 100644
--- a/front/src/components/library/import/FileUpload.vue
+++ b/front/src/components/library/import/FileUpload.vue
@@ -50,7 +50,7 @@
       <tbody>
         <tr v-for="(file, index) in files" :key="file.id">
           <td>{{ file.name }}</td>
-          <td>{{ file.size }}</td>
+          <td>{{ file.size | humanSize }}</td>
           <td>
             <span v-if="file.error" class="ui red label">
               {{ file.error }}
diff --git a/front/src/store/auth.js b/front/src/store/auth.js
index 87af081d26fdfd09d2d02b16db6196f91d6cee95..d36366996c5ea49b546e1c7394d00f00bd28858e 100644
--- a/front/src/store/auth.js
+++ b/front/src/store/auth.js
@@ -25,7 +25,11 @@ export default {
       state.username = ''
       state.token = ''
       state.tokenData = {}
-      state.availablePermissions = {}
+      state.availablePermissions = {
+        federation: false,
+        library: false,
+        upload: false
+      }
     },
     profile: (state, value) => {
       state.profile = value
@@ -108,8 +112,8 @@ export default {
         commit('authenticated', true)
         commit('profile', data)
         commit('username', data.username)
-        dispatch('favorites/fetch', null, {root: true})
-        dispatch('playlists/fetchOwn', null, {root: true})
+        dispatch('favorites/fetch', null, { root: true })
+        dispatch('playlists/fetchOwn', null, { root: true })
         Object.keys(data.permissions).forEach(function (key) {
           // this makes it easier to check for permissions in templates
           commit('permission', {key, status: data.permissions[String(key)]})
diff --git a/front/src/store/queue.js b/front/src/store/queue.js
index 23e074a80c36fb849eb306ea59da68756a05282b..2d6c667b29ba5e87623f3cc60e7be31e0d5d3fc4 100644
--- a/front/src/store/queue.js
+++ b/front/src/store/queue.js
@@ -72,16 +72,20 @@ export default {
       }
     },
 
-    appendMany ({state, dispatch}, {tracks, index}) {
+    appendMany ({state, dispatch}, {tracks, index, callback}) {
       logger.default.info('Appending many tracks to the queue', tracks.map(e => { return e.title }))
       if (state.tracks.length === 0) {
         index = 0
       } else {
         index = index || state.tracks.length
       }
-      tracks.forEach((t) => {
-        dispatch('append', {track: t, index: index, skipPlay: true})
+      let total = tracks.length
+      tracks.forEach((t, i) => {
+        let p = dispatch('append', {track: t, index: index, skipPlay: true})
         index += 1
+        if (callback && i + 1 === total) {
+          p.then(callback)
+        }
       })
       dispatch('resume')
     },
@@ -148,13 +152,17 @@ export default {
       // so we replay automatically on next track append
       commit('ended', true)
     },
-    shuffle ({dispatch, commit, state}) {
+    shuffle ({dispatch, commit, state}, callback) {
       let toKeep = state.tracks.slice(0, state.currentIndex + 1)
       let toShuffle = state.tracks.slice(state.currentIndex + 1)
       let shuffled = toKeep.concat(_.shuffle(toShuffle))
       commit('player/currentTime', 0, {root: true})
       commit('tracks', [])
-      dispatch('appendMany', {tracks: shuffled})
+      let params = {tracks: shuffled}
+      if (callback) {
+        params.callback = callback
+      }
+      dispatch('appendMany', params)
     }
   }
 }
diff --git a/front/src/store/ui.js b/front/src/store/ui.js
index f0935e491bb1a48abc5f90d3482e3d5eab7ee9a5..be744afe51ad954a4bae722f9442a9d71ad85730 100644
--- a/front/src/store/ui.js
+++ b/front/src/store/ui.js
@@ -2,11 +2,20 @@
 export default {
   namespaced: true,
   state: {
-    lastDate: new Date()
+    lastDate: new Date(),
+    maxMessages: 100,
+    messageDisplayDuration: 10000,
+    messages: []
   },
   mutations: {
     computeLastDate: (state) => {
       state.lastDate = new Date()
+    },
+    addMessage (state, message) {
+      state.messages.push(message)
+      if (state.messages.length > state.maxMessages) {
+        state.messages.shift()
+      }
     }
   }
 }
diff --git a/front/src/views/instance/Timeline.vue b/front/src/views/instance/Timeline.vue
index 2ab8b708cd1ae140d31c7ca3c5e1cc3f56631f51..03bd5a53758373257239d054f6ffbead3569d9fe 100644
--- a/front/src/views/instance/Timeline.vue
+++ b/front/src/views/instance/Timeline.vue
@@ -34,6 +34,7 @@ export default {
   data () {
     return {
       isLoading: false,
+      bridge: null,
       components: {
         'Like': Like,
         'Listen': Listen
@@ -44,6 +45,9 @@ export default {
     this.openWebsocket()
     this.fetchEvents()
   },
+  destroyed () {
+    this.disconnect()
+  },
   computed: {
     ...mapState({
       events: state => state.instance.events
@@ -58,14 +62,22 @@ export default {
         self.$store.commit('instance/events', response.data.results)
       })
     },
+    disconnect () {
+      if (!this.bridge) {
+        return
+      }
+      this.bridge.socket.close(1000, 'goodbye', {keepClosed: true})
+    },
     openWebsocket () {
       if (!this.$store.state.auth.authenticated) {
         return
       }
+      this.disconnect()
       let self = this
       let token = this.$store.state.auth.token
       // let token = 'test'
       const bridge = new WebSocketBridge()
+      this.bridge = bridge
       bridge.connect(
         `/api/v1/instance/activity?token=${token}`,
         null,
diff --git a/front/src/views/radios/Detail.vue b/front/src/views/radios/Detail.vue
index 26d8a4d83ce5709bab1f9e641782812e76a10435..0975398b54237577332a672d3c3f80e1a5c9b169 100644
--- a/front/src/views/radios/Detail.vue
+++ b/front/src/views/radios/Detail.vue
@@ -17,16 +17,18 @@
         </h2>
         <div class="ui hidden divider"></div>
         <radio-button type="custom" :custom-radio-id="radio.id"></radio-button>
-        <router-link class="ui icon button" :to="{name: 'library.radios.edit', params: {id: radio.id}}" exact>
-          <i class="pencil icon"></i>
-          Edit…
-        </router-link>
-        <dangerous-button class="labeled icon" :action="deleteRadio">
-          <i class="trash icon"></i> Delete
-          <p slot="modal-header">Do you want to delete the radio "{{ radio.name }}"?</p>
-          <p slot="modal-content">This will completely delete this radio and cannot be undone.</p>
-          <p slot="modal-confirm">Delete radio</p>
-        </dangerous-button>
+        <template v-if="$store.state.auth.username === radio.user.username">
+          <router-link class="ui icon button" :to="{name: 'library.radios.edit', params: {id: radio.id}}" exact>
+            <i class="pencil icon"></i>
+            Edit…
+          </router-link>
+          <dangerous-button class="labeled icon" :action="deleteRadio">
+            <i class="trash icon"></i> Delete
+            <p slot="modal-header">Do you want to delete the radio "{{ radio.name }}"?</p>
+            <p slot="modal-content">This will completely delete this radio and cannot be undone.</p>
+            <p slot="modal-confirm">Delete radio</p>
+          </dangerous-button>
+        </template>
       </div>
     </div>
     <div class="ui vertical stripe segment">
@@ -82,7 +84,7 @@ export default {
       let url = 'radios/radios/' + this.id + '/'
       axios.get(url).then((response) => {
         self.radio = response.data
-        axios.get(url + 'tracks', {params: {page: this.page}}).then((response) => {
+        axios.get(url + 'tracks/', {params: {page: this.page}}).then((response) => {
           this.totalTracks = response.data.count
           this.tracks = response.data.results
         }).then(() => {
diff --git a/front/test/unit/specs/store/ui.spec.js b/front/test/unit/specs/store/ui.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..adcfa87d8f34bd600f113fc4100c0c6318bc730e
--- /dev/null
+++ b/front/test/unit/specs/store/ui.spec.js
@@ -0,0 +1,18 @@
+import store from '@/store/ui'
+
+import { testAction } from '../../utils'
+
+describe('store/ui', () => {
+  describe('mutations', () => {
+    it('addMessage', () => {
+      const state = {maxMessages: 100, messages: []}
+      store.mutations.addMessage(state, 'hello')
+      expect(state.messages).to.deep.equal(['hello'])
+    })
+    it('addMessage', () => {
+      const state = {maxMessages: 1, messages: ['hello']}
+      store.mutations.addMessage(state, 'world')
+      expect(state.messages).to.deep.equal(['world'])
+    })
+  })
+})