See #228: now use our new action logic for library track import
This commit is contained in:
parent
f1a1b93ee5
commit
ba4b6f6ba6
|
@ -49,17 +49,19 @@ class ActionSerializer(serializers.Serializer):
|
||||||
'list of identifiers or the string "all".'.format(value))
|
'list of identifiers or the string "all".'.format(value))
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if not self.filterset_class or 'filters' not in data:
|
if self.filterset_class and 'filters' in data:
|
||||||
# no additional filters to apply, we just skip
|
qs_filterset = self.filterset_class(
|
||||||
return data
|
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
|
||||||
|
|
||||||
qs_filterset = self.filterset_class(
|
data['count'] = data['objects'].count()
|
||||||
data['filters'], queryset=data['objects'])
|
if data['count'] < 1:
|
||||||
try:
|
raise serializers.ValidationError(
|
||||||
assert qs_filterset.form.is_valid()
|
'No object matching your request')
|
||||||
except (AssertionError, TypeError):
|
|
||||||
raise serializers.ValidationError('Invalid filters')
|
|
||||||
data['objects'] = qs_filterset.qs
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
@ -67,7 +69,7 @@ class ActionSerializer(serializers.Serializer):
|
||||||
handler = getattr(self, handler_name)
|
handler = getattr(self, handler_name)
|
||||||
result = handler(self.validated_data['objects'])
|
result = handler(self.validated_data['objects'])
|
||||||
payload = {
|
payload = {
|
||||||
'updated': self.validated_data['objects'].count(),
|
'updated': self.validated_data['count'],
|
||||||
'action': self.validated_data['action'],
|
'action': self.validated_data['action'],
|
||||||
'result': result,
|
'result': result,
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,11 @@ from rest_framework import serializers
|
||||||
from dynamic_preferences.registries import global_preferences_registry
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
from funkwhale_api.common import utils as funkwhale_utils
|
from funkwhale_api.common import utils as funkwhale_utils
|
||||||
|
from funkwhale_api.common import serializers as common_serializers
|
||||||
|
from funkwhale_api.music import models as music_models
|
||||||
|
from funkwhale_api.music import tasks as music_tasks
|
||||||
from . import activity
|
from . import activity
|
||||||
|
from . import filters
|
||||||
from . import models
|
from . import models
|
||||||
from . import utils
|
from . import utils
|
||||||
|
|
||||||
|
@ -806,3 +809,29 @@ class CollectionSerializer(serializers.Serializer):
|
||||||
if self.context.get('include_ap_context', True):
|
if self.context.get('include_ap_context', True):
|
||||||
d['@context'] = AP_CONTEXT
|
d['@context'] = AP_CONTEXT
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryTrackActionSerializer(common_serializers.ActionSerializer):
|
||||||
|
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']
|
||||||
|
)
|
||||||
|
for lt in objects:
|
||||||
|
job = music_models.ImportJob.objects.create(
|
||||||
|
batch=batch,
|
||||||
|
library_track=lt,
|
||||||
|
mbid=lt.mbid,
|
||||||
|
source=lt.url,
|
||||||
|
)
|
||||||
|
funkwhale_utils.on_commit(
|
||||||
|
music_tasks.import_job_run.delay,
|
||||||
|
import_job_id=job.pk,
|
||||||
|
use_acoustid=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {'batch': {'id': batch.pk}}
|
||||||
|
|
|
@ -15,7 +15,7 @@ from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
from funkwhale_api.common import utils as funkwhale_utils
|
from funkwhale_api.common import utils as funkwhale_utils
|
||||||
from funkwhale_api.music.models import TrackFile
|
from funkwhale_api.music import models as music_models
|
||||||
from funkwhale_api.users.permissions import HasUserPermission
|
from funkwhale_api.users.permissions import HasUserPermission
|
||||||
|
|
||||||
from . import activity
|
from . import activity
|
||||||
|
@ -148,7 +148,9 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
page = request.GET.get('page')
|
page = request.GET.get('page')
|
||||||
library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
library = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
||||||
qs = TrackFile.objects.order_by('-creation_date').select_related(
|
qs = music_models.TrackFile.objects.order_by(
|
||||||
|
'-creation_date'
|
||||||
|
).select_related(
|
||||||
'track__artist',
|
'track__artist',
|
||||||
'track__album__artist'
|
'track__album__artist'
|
||||||
).filter(library_track__isnull=True)
|
).filter(library_track__isnull=True)
|
||||||
|
@ -307,3 +309,16 @@ class LibraryTrackViewSet(
|
||||||
'fetched_date',
|
'fetched_date',
|
||||||
'published_date',
|
'published_date',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@list_route(methods=['post'])
|
||||||
|
def action(self, request, *args, **kwargs):
|
||||||
|
queryset = models.LibraryTrack.objects.filter(
|
||||||
|
local_track_file__isnull=True)
|
||||||
|
serializer = serializers.LibraryTrackActionSerializer(
|
||||||
|
request.data,
|
||||||
|
queryset=queryset,
|
||||||
|
context={'submitted_by': request.user}
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
result = serializer.save()
|
||||||
|
return response.Response(result, status=200)
|
||||||
|
|
|
@ -250,28 +250,6 @@ class TrackActivitySerializer(activity_serializers.ModelSerializer):
|
||||||
return 'Audio'
|
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
|
|
||||||
|
|
||||||
|
|
||||||
class ImportJobRunSerializer(serializers.Serializer):
|
class ImportJobRunSerializer(serializers.Serializer):
|
||||||
jobs = serializers.PrimaryKeyRelatedField(
|
jobs = serializers.PrimaryKeyRelatedField(
|
||||||
many=True,
|
many=True,
|
||||||
|
|
|
@ -449,22 +449,6 @@ class SubmitViewSet(viewsets.ViewSet):
|
||||||
data, request, batch=None, import_request=import_request)
|
data, request, batch=None, import_request=import_request)
|
||||||
return Response(import_data)
|
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
|
@transaction.atomic
|
||||||
def _import_album(self, data, request, batch=None, import_request=None):
|
def _import_album(self, data, request, batch=None, import_request=None):
|
||||||
# we import the whole album here to prevent race conditions that occurs
|
# we import the whole album here to prevent race conditions that occurs
|
||||||
|
|
|
@ -87,3 +87,14 @@ def test_action_serializers_filterset(factories):
|
||||||
|
|
||||||
assert serializer.is_valid() is True
|
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',
|
||||||
|
}
|
||||||
|
serializer = TestSerializer(data, queryset=models.User.objects.none())
|
||||||
|
|
||||||
|
assert serializer.is_valid() is False
|
||||||
|
assert 'non_field_errors' in serializer.errors
|
||||||
|
|
|
@ -418,3 +418,37 @@ def test_can_filter_pending_follows(factories, superuser_api_client):
|
||||||
|
|
||||||
assert response.status_code == 200
|
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)
|
||||||
|
mocker.patch('funkwhale_api.music.tasks.import_job_run')
|
||||||
|
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imported_lts = [lt1, lt2]
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == expected
|
||||||
|
assert batch.jobs.count() == 2
|
||||||
|
for i, job in enumerate(batch.jobs.all()):
|
||||||
|
assert job.library_track == imported_lts[i]
|
||||||
|
|
|
@ -249,24 +249,6 @@ def test_serve_updates_access_date(factories, settings, api_client):
|
||||||
assert track_file.accessed_date > now
|
assert track_file.accessed_date > now
|
||||||
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_list_import_jobs(factories, superuser_api_client):
|
def test_can_list_import_jobs(factories, superuser_api_client):
|
||||||
job = factories['music.ImportJob']()
|
job = factories['music.ImportJob']()
|
||||||
url = reverse('api:v1:import-jobs-list')
|
url = reverse('api:v1:import-jobs-list')
|
||||||
|
|
|
@ -157,10 +157,11 @@ export default {
|
||||||
let self = this
|
let self = this
|
||||||
self.isImporting = true
|
self.isImporting = true
|
||||||
let payload = {
|
let payload = {
|
||||||
library_tracks: this.checked
|
objects: this.checked,
|
||||||
|
action: 'import'
|
||||||
}
|
}
|
||||||
axios.post('/submit/federation/', payload).then((response) => {
|
axios.post('/federation/library-tracks/action/', payload).then((response) => {
|
||||||
self.importBatch = response.data
|
self.importBatch = response.data.result.batch
|
||||||
self.isImporting = false
|
self.isImporting = false
|
||||||
self.fetchData()
|
self.fetchData()
|
||||||
}, error => {
|
}, error => {
|
||||||
|
|
Loading…
Reference in New Issue