Merge branch 'cleanup-017' into 'develop'

Cleanup  unused code

See merge request funkwhale/funkwhale!426
This commit is contained in:
Eliot Berriot 2018-09-28 20:55:37 +00:00
commit 42933fa138
17 changed files with 13 additions and 661 deletions

View File

@ -125,7 +125,6 @@ LOCAL_APPS = (
"funkwhale_api.radios", "funkwhale_api.radios",
"funkwhale_api.history", "funkwhale_api.history",
"funkwhale_api.playlists", "funkwhale_api.playlists",
"funkwhale_api.providers.audiofile",
"funkwhale_api.providers.acoustid", "funkwhale_api.providers.acoustid",
"funkwhale_api.subsonic", "funkwhale_api.subsonic",
) )

View File

@ -1,26 +1,16 @@
import datetime import datetime
import logging import logging
import xml
from django.conf import settings from django.conf import settings
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from rest_framework.exceptions import PermissionDenied
from funkwhale_api.common import preferences, session from funkwhale_api.common import preferences, session
from . import activity, keys, models, serializers, signing, utils from . import models, serializers
logger = logging.getLogger(__name__) 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()
)
def get_actor_data(actor_url): def get_actor_data(actor_url):
response = session.get_session().get( response = session.get_session().get(
actor_url, actor_url,
@ -51,247 +41,3 @@ def get_actor(fid):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
return serializer.save(last_fetch_date=timezone.now()) 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()}

View File

@ -1,74 +1,9 @@
import json
import requests import requests
from django.conf import settings from django.conf import settings
from funkwhale_api.common import session from funkwhale_api.common import session
from . import actors, models, serializers, signing, webfinger from . import serializers, signing
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
def get_library_data(library_url, actor): def get_library_data(library_url, actor):

View File

@ -5,9 +5,7 @@ from . import views
router = routers.SimpleRouter(trailing_slash=False) router = routers.SimpleRouter(trailing_slash=False)
music_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/shared", views.SharedViewSet, "shared")
router.register(r"federation/actors", views.ActorViewSet, "actors") router.register(r"federation/actors", views.ActorViewSet, "actors")
router.register(r".well-known", views.WellKnownViewSet, "well-known") router.register(r".well-known", views.WellKnownViewSet, "well-known")

View File

@ -1,6 +1,6 @@
from django import forms from django import forms
from django.core import paginator from django.core import paginator
from django.http import HttpResponse, Http404 from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
from rest_framework import exceptions, mixins, response, viewsets from rest_framework import exceptions, mixins, response, viewsets
from rest_framework.decorators import detail_route, list_route 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.common import preferences
from funkwhale_api.music import models as music_models from funkwhale_api.music import models as music_models
from . import ( from . import activity, authentication, models, renderers, serializers, utils, webfinger
activity,
actors,
authentication,
models,
renderers,
serializers,
utils,
webfinger,
)
class FederationMixin(object): class FederationMixin(object):
@ -78,47 +69,6 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV
return response.Response({}) 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): class WellKnownViewSet(viewsets.GenericViewSet):
authentication_classes = [] authentication_classes = []
permission_classes = [] permission_classes = []
@ -160,13 +110,10 @@ class WellKnownViewSet(viewsets.GenericViewSet):
def handler_acct(self, clean_result): def handler_acct(self, clean_result):
username, hostname = clean_result username, hostname = clean_result
if username in actors.SYSTEM_ACTORS: try:
actor = actors.SYSTEM_ACTORS[username].get_actor_instance() actor = models.Actor.objects.local().get(preferred_username=username)
else: except models.Actor.DoesNotExist:
try: raise forms.ValidationError("Invalid username")
actor = models.Actor.objects.local().get(preferred_username=username)
except models.Actor.DoesNotExist:
raise forms.ValidationError("Invalid username")
return serializers.ActorWebfingerSerializer(actor).data return serializers.ActorWebfingerSerializer(actor).data

View File

@ -11,10 +11,8 @@ from musicbrainzngs import ResponseError
from requests.exceptions import RequestException from requests.exceptions import RequestException
from funkwhale_api.common import channels from funkwhale_api.common import channels
from funkwhale_api.common import preferences from funkwhale_api.federation import routes
from funkwhale_api.federation import activity, actors, routes
from funkwhale_api.federation import library as lb from funkwhale_api.federation import library as lb
from funkwhale_api.federation import library as federation_serializers
from funkwhale_api.taskapp import celery from funkwhale_api.taskapp import celery
from . import lyrics as lyrics_utils from . import lyrics as lyrics_utils
@ -78,45 +76,6 @@ def fetch_content(lyrics):
lyrics.save(update_fields=["content"]) 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.app.task(name="music.start_library_scan")
@celery.require_instance( @celery.require_instance(
models.LibraryScan.objects.select_related().filter(status="pending"), "library_scan" models.LibraryScan.objects.select_related().filter(status="pending"), "library_scan"

View File

@ -1,4 +0,0 @@
"""
This module is responsible from importing existing audiofiles from the
filesystem into funkwhale.
"""

View File

@ -1,8 +1,4 @@
import pytest from funkwhale_api.federation import actors, serializers
from django.urls import reverse
from rest_framework import exceptions
from funkwhale_api.federation import actors, models, serializers, utils
def test_actor_fetching(r_mock): 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 == actor
assert new_actor.last_fetch_date > actor.last_fetch_date 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_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)

View File

@ -2,20 +2,7 @@ import pytest
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.urls import reverse from django.urls import reverse
from funkwhale_api.federation import actors, serializers, webfinger from funkwhale_api.federation import 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
def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker): 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") 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): def test_wellknown_nodeinfo(db, preferences, api_client, settings):
expected = { expected = {
"links": [ "links": [

View File

@ -452,11 +452,6 @@ def test_get_audio_data(factories):
assert result == {"duration": 229, "bitrate": 128000, "size": 3459481} 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): def test_library_queryset_with_follows(factories):
library1 = factories["music.Library"]() library1 = factories["music.Library"]()
library2 = factories["music.Library"]() library2 = factories["music.Library"]()

View File

@ -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"

View File

@ -33,7 +33,7 @@ def test_import_with_multiple_argument(factories, mocker):
path1 = os.path.join(DATA_DIR, "dummy_file.ogg") path1 = os.path.join(DATA_DIR, "dummy_file.ogg")
path2 = os.path.join(DATA_DIR, "utf8-éà◌.ogg") path2 = os.path.join(DATA_DIR, "utf8-éà◌.ogg")
mocked_filter = mocker.patch( 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": []}), return_value=({"new": [], "skipped": []}),
) )
call_command("import_files", str(library.uuid), path1, path2, interactive=False) call_command("import_files", str(library.uuid), path1, path2, interactive=False)

View File

@ -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