diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index 76dbfd1ad..e841b6394 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -163,3 +163,10 @@ class LibraryTrack(models.Model):
title = models.CharField(max_length=500)
metadata = JSONField(
default={}, max_length=10000, encoder=DjangoJSONEncoder)
+
+ @property
+ def mbid(self):
+ try:
+ return self.metadata['recording']['musicbrainz_id']
+ except KeyError:
+ pass
diff --git a/api/funkwhale_api/music/forms.py b/api/funkwhale_api/music/forms.py
index 04e4bfe05..e68ab73cc 100644
--- a/api/funkwhale_api/music/forms.py
+++ b/api/funkwhale_api/music/forms.py
@@ -19,5 +19,5 @@ class TranscodeForm(forms.Form):
choices=BITRATE_CHOICES, required=False)
track_file = forms.ModelChoiceField(
- queryset=models.TrackFile.objects.all()
+ queryset=models.TrackFile.objects.exclude(audio_file__isnull=True)
)
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index 42795dbea..b5f69eb1d 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -3,8 +3,9 @@ from rest_framework import serializers
from taggit.models import Tag
from funkwhale_api.activity import serializers as activity_serializers
-from funkwhale_api.federation.serializers import AP_CONTEXT
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 . import models
@@ -153,3 +154,25 @@ class TrackActivitySerializer(activity_serializers.ModelSerializer):
def get_type(self, obj):
return 'Audio'
+
+
+class SubmitFederationTracksSerializer(serializers.Serializer):
+ library_tracks = serializers.PrimaryKeyRelatedField(
+ many=True,
+ queryset=LibraryTrack.objects.filter(local_track_file__isnull=True),
+ )
+
+ @transaction.atomic
+ def save(self, **kwargs):
+ batch = models.ImportBatch.objects.create(
+ source='federation',
+ **kwargs
+ )
+ for lt in self.validated_data['library_tracks']:
+ models.ImportJob.objects.create(
+ batch=batch,
+ library_track=lt,
+ mbid=lt.mbid,
+ source=lt.url,
+ )
+ return batch
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 98048b41d..d5247fbf6 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -1,6 +1,7 @@
import ffmpeg
import os
import json
+import logging
import subprocess
import unicodedata
import urllib
@@ -40,6 +41,8 @@ from . import serializers
from . import tasks
from . import utils
+logger = logging.getLogger(__name__)
+
class SearchMixin(object):
search_fields = []
@@ -223,6 +226,8 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
headers={
'Content-Type': 'application/activity+json'
})
+ logger.debug(
+ 'Proxying media request to %s', library_track.audio_url)
response = StreamingHttpResponse(remote_response.iter_content())
else:
response = Response()
@@ -249,6 +254,8 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
return Response(form.errors, status=400)
f = form.cleaned_data['track_file']
+ if not f.audio_file:
+ return Response(status=400)
output_kwargs = {
'format': form.cleaned_data['to']
}
@@ -392,6 +399,22 @@ class SubmitViewSet(viewsets.ViewSet):
data, request, batch=None, import_request=import_request)
return Response(import_data)
+ @list_route(methods=['post'])
+ @transaction.non_atomic_requests
+ def federation(self, request, *args, **kwargs):
+ serializer = serializers.SubmitFederationTracksSerializer(
+ data=request.data)
+ serializer.is_valid(raise_exception=True)
+ batch = serializer.save(submitted_by=request.user)
+ for job in batch.jobs.all():
+ funkwhale_utils.on_commit(
+ tasks.import_job_run.delay,
+ import_job_id=job.pk,
+ use_acoustid=False,
+ )
+
+ return Response({'id': batch.id}, status=201)
+
@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
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index 468ea77e3..f18d18c86 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -1,6 +1,8 @@
import io
import pytest
+from django.urls import reverse
+
from funkwhale_api.music import views
from funkwhale_api.federation import actors
@@ -83,3 +85,21 @@ def test_can_proxy_remote_track(
assert response.status_code == 200
assert list(response.streaming_content) == [b't', b'e', b's', b't']
assert response['Content-Type'] == track_file.library_track.audio_mimetype
+
+
+def test_can_create_import_from_federation_tracks(
+ factories, superuser_api_client, mocker):
+ lts = factories['federation.LibraryTrack'].create_batch(size=5)
+ mocker.patch('funkwhale_api.music.tasks.import_job_run')
+
+ payload = {
+ 'library_tracks': [l.pk for l in lts]
+ }
+ url = reverse('api:v1:submit-federation')
+ response = superuser_api_client.post(url, payload)
+
+ assert response.status_code == 201
+ batch = superuser_api_client.user.imports.latest('id')
+ assert batch.jobs.count() == 5
+ for i, job in enumerate(batch.jobs.all()):
+ assert job.library_track == lts[i]
diff --git a/front/src/components/federation/LibraryTrackTable.vue b/front/src/components/federation/LibraryTrackTable.vue
new file mode 100644
index 000000000..dc6eb9d21
--- /dev/null
+++ b/front/src/components/federation/LibraryTrackTable.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+ Title
+ Artist
+ Album
+ Published date
+
+
+
+
+
+
+
+ {{ track.title }}
+
+
+ {{ track.artist_name }}
+
+
+ {{ track.album_title }}
+
+
+
+
+
+
+
+
+
+