diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 60446e370..4759d7aab 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -125,7 +125,6 @@ LOCAL_APPS = ( "funkwhale_api.radios", "funkwhale_api.history", "funkwhale_api.playlists", - "funkwhale_api.providers.audiofile", "funkwhale_api.providers.acoustid", "funkwhale_api.subsonic", ) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index a91e9a5e9..f1b94809d 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -1,26 +1,16 @@ import datetime import logging -import xml from django.conf import settings -from django.urls import reverse from django.utils import timezone -from rest_framework.exceptions import PermissionDenied from funkwhale_api.common import preferences, session -from . import activity, keys, models, serializers, signing, utils +from . import models, serializers logger = logging.getLogger(__name__) -def remove_tags(text): - logger.debug("Removing tags from %s", text) - return "".join( - xml.etree.ElementTree.fromstring("
{}
".format(text)).itertext() - ) - - def get_actor_data(actor_url): response = session.get_session().get( actor_url, @@ -51,247 +41,3 @@ def get_actor(fid): serializer.is_valid(raise_exception=True) return serializer.save(last_fetch_date=timezone.now()) - - -class SystemActor(object): - additional_attributes = {} - manually_approves_followers = False - - def get_request_auth(self): - actor = self.get_actor_instance() - return signing.get_auth(actor.private_key, actor.private_key_id) - - def serialize(self): - actor = self.get_actor_instance() - serializer = serializers.ActorSerializer(actor) - return serializer.data - - def get_actor_instance(self): - try: - return models.Actor.objects.get(fid=self.get_actor_id()) - except models.Actor.DoesNotExist: - pass - private, public = keys.get_key_pair() - args = self.get_instance_argument( - 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") - return models.Actor.objects.create(**args) - - def get_actor_id(self): - return utils.full_url( - 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, - "fid": self.get_actor_id(), - "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 - - def get_inbox(self, data, actor=None): - raise NotImplementedError - - def post_inbox(self, data, actor=None): - return self.handle(data, actor=actor) - - def get_outbox(self, data, actor=None): - raise NotImplementedError - - def post_outbox(self, data, actor=None): - raise NotImplementedError - - def handle(self, data, actor=None): - """ - Main entrypoint for handling activities posted to the - actor's inbox - """ - logger.info("Received activity on %s inbox", self.id) - - if actor is None: - raise PermissionDenied("Actor not authenticated") - - 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())) - except (KeyError, AttributeError): - logger.debug("No handler for activity %s", ac["type"]) - return - - return handler(data, actor) - - def handle_follow(self, ac, sender): - serializer = serializers.FollowSerializer( - data=ac, context={"follow_actor": sender} - ) - if not serializer.is_valid(): - return logger.info("Invalid follow payload") - approved = True if not self.manually_approves_followers else None - follow = serializer.save(approved=approved) - if follow.approved: - return activity.accept_follow(follow) - - 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} - ) - if not serializer.is_valid(raise_exception=True): - 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} - ) - if not serializer.is_valid(): - return logger.info("Received invalid payload") - serializer.save() - - def handle_undo(self, ac, sender): - if ac["object"]["type"] != "Follow": - return - - if ac["object"]["actor"] != sender.fid: - # not the same actor, permission issue - return - - self.handle_undo_follow(ac, sender) - - -class TestActor(SystemActor): - id = "test" - name = "{host}'s test account" - summary = ( - "Bot account to test federation with {host}. " - "Send me /ping and I'll answer you." - ) - additional_attributes = {"manually_approves_followers": False} - manually_approves_followers = False - - def get_outbox(self, data, actor=None): - return { - "@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}) - ), - "type": "OrderedCollection", - "totalItems": 0, - "orderedItems": [], - } - - def parse_command(self, message): - """ - Remove any links or fancy markup to extract /command from - a note message. - """ - raw = remove_tags(message) - try: - return raw.split("/")[1] - except IndexError: - return - - def handle_create(self, ac, sender): - 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": - return - - now = timezone.now() - test_actor = self.get_actor_instance() - reply_url = "https://{}/activities/note/{}".format( - settings.FEDERATION_HOSTNAME, now.timestamp() - ) - reply_activity = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {}, - ], - "type": "Create", - "actor": test_actor.fid, - "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.fid, - "cc": [], - "attachment": [], - "tag": [ - { - "type": "Mention", - "href": ac["actor"], - "name": sender.full_username, - } - ], - }, - } - 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 - )[0] - activity.deliver( - serializers.FollowSerializer(follow_back).data, - to=[follow_back.target.fid], - 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) - except models.Follow.DoesNotExist: - return - undo = serializers.UndoFollowSerializer(follow).data - follow.delete() - activity.deliver(undo, to=[sender.fid], on_behalf_of=actor) - - -SYSTEM_ACTORS = {"test": TestActor()} diff --git a/api/funkwhale_api/federation/library.py b/api/funkwhale_api/federation/library.py index 467220799..e7f8373fa 100644 --- a/api/funkwhale_api/federation/library.py +++ b/api/funkwhale_api/federation/library.py @@ -1,74 +1,9 @@ -import json - import requests from django.conf import settings from funkwhale_api.common import session -from . import actors, models, serializers, signing, webfinger - - -def scan_from_account_name(account_name): - """ - Given an account name such as library@test.library, will: - - 1. Perform the webfinger lookup - 2. Perform the actor lookup - 3. Perform the library's collection lookup - - and return corresponding data in a dictionary. - """ - data = {} - try: - 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() - 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 - except models.Follow.DoesNotExist: - pass - - try: - data["webfinger"] = webfinger.get_resource("acct:{}".format(account_name)) - except requests.ConnectionError: - 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) - ] - } - } - except json.JSONDecodeError as e: - return {"webfinger": {"errors": ["Could not process webfinger response"]}} - - try: - data["actor"] = actors.get_actor_data(data["webfinger"]["actor_url"]) - except requests.ConnectionError: - 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)] - } - return data - - serializer = serializers.LibraryActorSerializer(data=data["actor"]) - if not serializer.is_valid(): - data["actor"] = {"errors": ["Invalid ActivityPub actor"]} - return data - data["library"] = get_library_data(serializer.validated_data["library_url"]) - - return data +from . import serializers, signing def get_library_data(library_url, actor): diff --git a/api/funkwhale_api/federation/urls.py b/api/funkwhale_api/federation/urls.py index cfec4a237..f8347d1eb 100644 --- a/api/funkwhale_api/federation/urls.py +++ b/api/funkwhale_api/federation/urls.py @@ -5,9 +5,7 @@ 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"federation/shared", views.SharedViewSet, "shared") router.register(r"federation/actors", views.ActorViewSet, "actors") router.register(r".well-known", views.WellKnownViewSet, "well-known") diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py index 510c672fd..a12d5e5b5 100644 --- a/api/funkwhale_api/federation/views.py +++ b/api/funkwhale_api/federation/views.py @@ -1,6 +1,6 @@ from django import forms from django.core import paginator -from django.http import HttpResponse, Http404 +from django.http import HttpResponse from django.urls import reverse from rest_framework import exceptions, mixins, response, viewsets from rest_framework.decorators import detail_route, list_route @@ -8,16 +8,7 @@ from rest_framework.decorators import detail_route, list_route from funkwhale_api.common import preferences from funkwhale_api.music import models as music_models -from . import ( - activity, - actors, - authentication, - models, - renderers, - serializers, - utils, - webfinger, -) +from . import activity, authentication, models, renderers, serializers, utils, webfinger class FederationMixin(object): @@ -78,47 +69,6 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV return response.Response({}) -class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet): - 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"]] - except KeyError: - raise Http404 - - def retrieve(self, request, *args, **kwargs): - system_actor = self.get_object() - actor = system_actor.get_actor_instance() - data = actor.system_conf.serialize() - return response.Response(data, status=200) - - @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())) - - try: - handler(request.data, actor=request.actor) - except NotImplementedError: - return response.Response(status=405) - return response.Response({}, status=200) - - @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())) - try: - handler(request.data, actor=request.actor) - except NotImplementedError: - return response.Response(status=405) - return response.Response({}, status=200) - - class WellKnownViewSet(viewsets.GenericViewSet): authentication_classes = [] permission_classes = [] @@ -160,13 +110,10 @@ class WellKnownViewSet(viewsets.GenericViewSet): def handler_acct(self, clean_result): username, hostname = clean_result - if username in actors.SYSTEM_ACTORS: - actor = actors.SYSTEM_ACTORS[username].get_actor_instance() - else: - try: - actor = models.Actor.objects.local().get(preferred_username=username) - except models.Actor.DoesNotExist: - raise forms.ValidationError("Invalid username") + try: + actor = models.Actor.objects.local().get(preferred_username=username) + except models.Actor.DoesNotExist: + raise forms.ValidationError("Invalid username") return serializers.ActorWebfingerSerializer(actor).data diff --git a/api/funkwhale_api/providers/audiofile/management/commands/import_files.py b/api/funkwhale_api/music/management/commands/import_files.py similarity index 100% rename from api/funkwhale_api/providers/audiofile/management/commands/import_files.py rename to api/funkwhale_api/music/management/commands/import_files.py diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index b78cabaae..d96471b96 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -11,10 +11,8 @@ from musicbrainzngs import ResponseError from requests.exceptions import RequestException from funkwhale_api.common import channels -from funkwhale_api.common import preferences -from funkwhale_api.federation import activity, actors, routes +from funkwhale_api.federation import routes from funkwhale_api.federation import library as lb -from funkwhale_api.federation import library as federation_serializers from funkwhale_api.taskapp import celery from . import lyrics as lyrics_utils @@ -78,45 +76,6 @@ def fetch_content(lyrics): lyrics.save(update_fields=["content"]) -@celery.app.task(name="music.import_batch_notify_followers") -@celery.require_instance( - models.ImportBatch.objects.filter(status="finished"), "import_batch" -) -def import_batch_notify_followers(import_batch): - if not preferences.get("federation__enabled"): - return - - if import_batch.source == "federation": - return - - 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, upload__isnull=False - ).select_related("upload__track__artist", "upload__track__album__artist") - uploads = [job.upload for job in jobs] - collection = federation_serializers.CollectionSerializer( - { - "actor": library_actor, - "id": import_batch.get_federation_id(), - "items": uploads, - "item_serializer": federation_serializers.AudioSerializer, - } - ).data - for f in followers: - create = federation_serializers.ActivitySerializer( - { - "type": "Create", - "id": collection["id"], - "object": collection, - "actor": library_actor.fid, - "to": [f.url], - } - ).data - - activity.deliver(create, on_behalf_of=library_actor, to=[f.url]) - - @celery.app.task(name="music.start_library_scan") @celery.require_instance( models.LibraryScan.objects.select_related().filter(status="pending"), "library_scan" diff --git a/api/funkwhale_api/providers/audiofile/__init__.py b/api/funkwhale_api/providers/audiofile/__init__.py deleted file mode 100644 index 18e2c469a..000000000 --- a/api/funkwhale_api/providers/audiofile/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -This module is responsible from importing existing audiofiles from the -filesystem into funkwhale. -""" diff --git a/api/funkwhale_api/providers/audiofile/management/__init__.py b/api/funkwhale_api/providers/audiofile/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/funkwhale_api/providers/audiofile/management/commands/__init__.py b/api/funkwhale_api/providers/audiofile/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/funkwhale_api/providers/audiofile/tasks.py b/api/funkwhale_api/providers/audiofile/tasks.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index 71c1b047b..a416cd78f 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -1,8 +1,4 @@ -import pytest -from django.urls import reverse -from rest_framework import exceptions - -from funkwhale_api.federation import actors, models, serializers, utils +from funkwhale_api.federation import actors, serializers def test_actor_fetching(r_mock): @@ -50,120 +46,3 @@ def test_get_actor_refresh(factories, preferences, mocker): assert new_actor == actor assert new_actor.last_fetch_date > actor.last_fetch_date assert new_actor.preferred_username == "New me" - - -def test_get_test(db, mocker, settings): - 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", - "fid": 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() - for key, value in expected.items(): - assert getattr(actor, key) == value - - -def test_test_get_outbox(): - expected = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {}, - ], - "id": utils.full_url( - reverse("federation:instance-actors-outbox", kwargs={"actor": "test"}) - ), - "type": "OrderedCollection", - "totalItems": 0, - "orderedItems": [], - } - - 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) - - -def test_test_post_outbox_validates_actor(nodb_factories): - 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" - assert msg in exc_info.value - - -def test_getting_actor_instance_persists_in_db(db): - test = actors.SYSTEM_ACTORS["test"].get_actor_instance() - from_db = models.Actor.objects.get(fid=test.fid) - - 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): - if not domain: - domain = settings.FEDERATION_HOSTNAME - - 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_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 - ) - assert actor.system_conf == expected - - -@pytest.mark.skip("Refactoring in progress") -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) - serializer = serializers.ActivitySerializer(data=activity) - assert serializer.is_valid() - actors.SYSTEM_ACTORS["test"].handle(activity, actor) - handler.assert_called_once_with(activity, actor) diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py index ac359eac6..2caa7856a 100644 --- a/api/tests/federation/test_views.py +++ b/api/tests/federation/test_views.py @@ -2,20 +2,7 @@ import pytest from django.core.paginator import Paginator from django.urls import reverse -from funkwhale_api.federation import actors, serializers, webfinger - - -@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}) - response = api_client.get(url) - serializer = serializers.ActorSerializer(actor) - - if system_actor == "library": - response.data.pop("url") - assert response.status_code == 200 - assert response.data == serializer.data +from funkwhale_api.federation import serializers, webfinger def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker): @@ -29,22 +16,6 @@ def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker 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): - actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance() - url = reverse("federation:well-known-webfinger") - response = api_client.get( - url, - 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.data == serializer.data - - def test_wellknown_nodeinfo(db, preferences, api_client, settings): expected = { "links": [ diff --git a/api/tests/music/test_models.py b/api/tests/music/test_models.py index aa618aa2d..d045a04c2 100644 --- a/api/tests/music/test_models.py +++ b/api/tests/music/test_models.py @@ -452,11 +452,6 @@ def test_get_audio_data(factories): assert result == {"duration": 229, "bitrate": 128000, "size": 3459481} -@pytest.mark.skip(reason="Refactoring in progress") -def test_library_viewable_by(): - assert False - - def test_library_queryset_with_follows(factories): library1 = factories["music.Library"]() library2 = factories["music.Library"]() diff --git a/api/tests/requests/test_models.py b/api/tests/requests/test_models.py deleted file mode 100644 index 9b9100b5c..000000000 --- a/api/tests/requests/test_models.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest - - -@pytest.mark.skip(reason="Refactoring in progress") -def test_can_bind_import_batch_to_request(factories): - request = factories["requests.ImportRequest"]() - - assert request.status == "pending" - - # when we create the import, we consider the request as accepted - batch = factories["music.ImportBatch"](import_request=request) - request.refresh_from_db() - - assert request.status == "accepted" - - # now, the batch is finished, therefore the request status should be - # imported - batch.status = "finished" - batch.save(update_fields=["status"]) - request.refresh_from_db() - - assert request.status == "imported" diff --git a/api/tests/test_import_audio_file.py b/api/tests/test_import_audio_file.py index ad4b4be0e..ce6aebbc3 100644 --- a/api/tests/test_import_audio_file.py +++ b/api/tests/test_import_audio_file.py @@ -33,7 +33,7 @@ def test_import_with_multiple_argument(factories, mocker): path1 = os.path.join(DATA_DIR, "dummy_file.ogg") path2 = os.path.join(DATA_DIR, "utf8-éà◌.ogg") mocked_filter = mocker.patch( - "funkwhale_api.providers.audiofile.management.commands.import_files.Command.filter_matching", + "funkwhale_api.music.management.commands.import_files.Command.filter_matching", return_value=({"new": [], "skipped": []}), ) call_command("import_files", str(library.uuid), path1, path2, interactive=False) diff --git a/per-user-libraries b/per-user-libraries deleted file mode 100644 index d2905d88a..000000000 --- a/per-user-libraries +++ /dev/null @@ -1,51 +0,0 @@ -Todo: - -- upload utilisateur -- gestion des doublons ? Si piste uploadée deux fois, on fait quoi: - - On rajoute un lien entre la piste existante et la bibliothèque de l'utilisateur - - L'utilisateur peut forcer l'upload de SA piste -- Comment on gère l'affichage : une piste peut ne pas être jouable -- more tests about: - - replacing - - deletion - - permissions - -- un utilisateur envoie une piste: - - pas de problème: au pire les métadonnées ne sont pas bonnes mais ce n'est pas très grave - - on incite à tagguer avec picard / musicbrainz - - en cas de conflit (piste sans tag, mais le nom de l'artiste existe avec un ID musicbrainz, par exemple): - on fournit à l'utilisateur le choix (binder à l'artiste musicbrainz), ou créer un artiste séparé - on peut aussi tenter des trucs plus intelligents à base de matching sur les noms de pistes, mais dans un second temps - -- Un créateur envoie une piste: - - il crée un profil avec son nom d'artiste, des liens vers ses différents profils (youtube, etc). - - on peut lui fournir un snippet à inclure sur ses profils "Funkwhale: http://creator.url" pour servir à valider - - les instances fédérées pourront donc faire la vérification elles-mêmes - - à l'upload, il a un formulaire spécial ou il déclare bien être le créateur des pistes et avoir les droits - - on ne tente pas d'être smart : il faut que les données soient fiables et éditables par le créateur avant publication ! - - -Jour 2: - -- on bind les fichiers aux bibliothèques -- on ne dédoublonne pas, trop compliqué -- en fédé, quand on scanne, on crée les track files, du coup plus besoin d'import manuel -- dans le script de migration, gérer le cas dess trucs importés via la fédé - - -Todo: - -- tester le remplacement -- skipper les tracks qui sont déjà dans une autre bibliothèque -- gestion d'erreur plus poussée -- gérer les radios -- tester permission sur la fédé -- tester qu'on ne sert que les bibliothèques locales -- virer au maximum la logique custom pour les acteurs systèmes -- utiliser le vrai champs durée d'activitystream pour l'audio -- shared inbox url -- vue pour servir les bibliothèques: - -- logique de scan: - - pouvoir lancer, mettre en pause, interrompre un scan - - avoir des infos sur le déroulement du scan