diff --git a/api/funkwhale_api/common/models.py b/api/funkwhale_api/common/models.py
index 87f7dc8e3..52a02cad9 100644
--- a/api/funkwhale_api/common/models.py
+++ b/api/funkwhale_api/common/models.py
@@ -5,9 +5,10 @@ from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
-from django.db import models, transaction
+from django.db import connections, models, transaction
from django.db.models import Lookup
from django.db.models.fields import Field
+from django.db.models.sql.compiler import SQLCompiler
from django.utils import timezone
from django.urls import reverse
@@ -25,6 +26,41 @@ class NotEqual(Lookup):
return "%s <> %s" % (lhs, rhs), params
+class NullsLastSQLCompiler(SQLCompiler):
+ def get_order_by(self):
+ result = super().get_order_by()
+ if result and self.connection.vendor == "postgresql":
+ return [
+ (
+ expr,
+ (
+ sql + " NULLS LAST" if not sql.endswith(" NULLS LAST") else sql,
+ params,
+ is_ref,
+ ),
+ )
+ for (expr, (sql, params, is_ref)) in result
+ ]
+ return result
+
+
+class NullsLastQuery(models.sql.query.Query):
+ """Use a custom compiler to inject 'NULLS LAST' (for PostgreSQL)."""
+
+ def get_compiler(self, using=None, connection=None):
+ if using is None and connection is None:
+ raise ValueError("Need either using or connection")
+ if using:
+ connection = connections[using]
+ return NullsLastSQLCompiler(self, connection, using)
+
+
+class NullsLastQuerySet(models.QuerySet):
+ def __init__(self, model=None, query=None, using=None, hints=None):
+ super().__init__(model, query, using, hints)
+ self.query = query or NullsLastQuery(self.model)
+
+
class LocalFromFidQuerySet:
def local(self, include=True):
host = settings.FEDERATION_HOSTNAME
diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index 7d3d5639d..3e3cb0e52 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -1,4 +1,5 @@
import tempfile
+import urllib.parse
import uuid
from django.conf import settings
@@ -43,6 +44,18 @@ class FederationMixin(models.Model):
class Meta:
abstract = True
+ @property
+ def is_local(self):
+ return federation_utils.is_local(self.fid)
+
+ @property
+ def domain_name(self):
+ if not self.fid:
+ return
+
+ parsed = urllib.parse.urlparse(self.fid)
+ return parsed.hostname
+
class ActorQuerySet(models.QuerySet):
def local(self, include=True):
diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py
index 64a6473e0..c6f5db53a 100644
--- a/api/funkwhale_api/manage/filters.py
+++ b/api/funkwhale_api/manage/filters.py
@@ -1,4 +1,8 @@
from django import forms
+from django.db.models import Q
+from django.conf import settings
+
+import django_filters
from django_filters import rest_framework as filters
from funkwhale_api.common import fields
@@ -11,19 +15,32 @@ from funkwhale_api.music import models as music_models
from funkwhale_api.users import models as users_models
-class ManageUploadFilterSet(filters.FilterSet):
- q = fields.SearchFilter(
- search_fields=[
- "track__title",
- "track__album__title",
- "track__artist__name",
- "source",
- ]
- )
+class ActorField(forms.CharField):
+ def clean(self, value):
+ value = super().clean(value)
+ if not value:
+ return value
- class Meta:
- model = music_models.Upload
- fields = ["q", "track__album", "track__artist", "track"]
+ parts = value.split("@")
+
+ return {
+ "username": parts[0],
+ "domain": parts[1] if len(parts) > 1 else settings.FEDERATION_HOSTNAME,
+ }
+
+
+def get_actor_filter(actor_field):
+ def handler(v):
+ if not v:
+ return Q(**{actor_field: None})
+ return Q(
+ **{
+ "{}__preferred_username__iexact".format(actor_field): v["username"],
+ "{}__domain__name__iexact".format(actor_field): v["domain"],
+ }
+ )
+
+ return {"field": ActorField(), "handler": handler}
class ManageArtistFilterSet(filters.FilterSet):
@@ -37,7 +54,11 @@ class ManageArtistFilterSet(filters.FilterSet):
filter_fields={
"domain": {
"handler": lambda v: federation_utils.get_domain_query_from_url(v)
- }
+ },
+ "library_id": {
+ "to": "tracks__uploads__library_id",
+ "field": forms.IntegerField(),
+ },
},
)
)
@@ -61,6 +82,10 @@ class ManageAlbumFilterSet(filters.FilterSet):
"domain": {
"handler": lambda v: federation_utils.get_domain_query_from_url(v)
},
+ "library_id": {
+ "to": "tracks__uploads__library_id",
+ "field": forms.IntegerField(),
+ },
},
)
)
@@ -93,6 +118,10 @@ class ManageTrackFilterSet(filters.FilterSet):
"domain": {
"handler": lambda v: federation_utils.get_domain_query_from_url(v)
},
+ "library_id": {
+ "to": "uploads__library_id",
+ "field": forms.IntegerField(),
+ },
},
)
)
@@ -102,6 +131,96 @@ class ManageTrackFilterSet(filters.FilterSet):
fields = ["q", "title", "mbid", "fid", "artist", "album", "license"]
+class ManageLibraryFilterSet(filters.FilterSet):
+ ordering = django_filters.OrderingFilter(
+ # tuple-mapping retains order
+ fields=(
+ ("creation_date", "creation_date"),
+ ("_uploads_count", "uploads_count"),
+ ("followers_count", "followers_count"),
+ )
+ )
+ q = fields.SmartSearchFilter(
+ config=search.SearchConfig(
+ search_fields={
+ "name": {"to": "name"},
+ "description": {"to": "description"},
+ "fid": {"to": "fid"},
+ },
+ filter_fields={
+ "artist_id": {
+ "to": "uploads__track__artist_id",
+ "field": forms.IntegerField(),
+ },
+ "album_id": {
+ "to": "uploads__track__album_id",
+ "field": forms.IntegerField(),
+ },
+ "track_id": {"to": "uploads__track__id", "field": forms.IntegerField()},
+ "domain": {"to": "actor__domain_id"},
+ "account": get_actor_filter("actor"),
+ "privacy_level": {"to": "privacy_level"},
+ },
+ )
+ )
+ domain = filters.CharFilter("actor__domain_id")
+
+ class Meta:
+ model = music_models.Library
+ fields = ["q", "name", "fid", "privacy_level", "domain"]
+
+
+class ManageUploadFilterSet(filters.FilterSet):
+ ordering = django_filters.OrderingFilter(
+ # tuple-mapping retains order
+ fields=(
+ ("creation_date", "creation_date"),
+ ("modification_date", "modification_date"),
+ ("accessed_date", "accessed_date"),
+ ("size", "size"),
+ ("bitrate", "bitrate"),
+ ("duration", "duration"),
+ )
+ )
+ q = fields.SmartSearchFilter(
+ config=search.SearchConfig(
+ search_fields={
+ "source": {"to": "source"},
+ "fid": {"to": "fid"},
+ "track": {"to": "track__title"},
+ "album": {"to": "track__album__title"},
+ "artist": {"to": "track__artist__name"},
+ },
+ filter_fields={
+ "library_id": {"to": "library_id", "field": forms.IntegerField()},
+ "artist_id": {"to": "track__artist_id", "field": forms.IntegerField()},
+ "album_id": {"to": "track__album_id", "field": forms.IntegerField()},
+ "track_id": {"to": "track__id", "field": forms.IntegerField()},
+ "domain": {"to": "library__actor__domain_id"},
+ "import_reference": {"to": "import_reference"},
+ "type": {"to": "mimetype"},
+ "status": {"to": "import_status"},
+ "account": get_actor_filter("library__actor"),
+ "privacy_level": {"to": "library__privacy_level"},
+ },
+ )
+ )
+ domain = filters.CharFilter("library__actor__domain_id")
+ privacy_level = filters.CharFilter("library__privacy_level")
+
+ class Meta:
+ model = music_models.Upload
+ fields = [
+ "q",
+ "fid",
+ "privacy_level",
+ "domain",
+ "mimetype",
+ "import_reference",
+ "import_status",
+ ]
+
+
class ManageDomainFilterSet(filters.FilterSet):
q = fields.SearchFilter(search_fields=["name"])
diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py
index cf6a1eab4..add9364e8 100644
--- a/api/funkwhale_api/manage/serializers.py
+++ b/api/funkwhale_api/manage/serializers.py
@@ -15,67 +15,6 @@ from funkwhale_api.users import models as users_models
from . import filters
-class ManageUploadArtistSerializer(serializers.ModelSerializer):
- class Meta:
- model = music_models.Artist
- fields = ["id", "mbid", "creation_date", "name"]
-
-
-class ManageUploadAlbumSerializer(serializers.ModelSerializer):
- artist = ManageUploadArtistSerializer()
-
- class Meta:
- model = music_models.Album
- fields = (
- "id",
- "mbid",
- "title",
- "artist",
- "release_date",
- "cover",
- "creation_date",
- )
-
-
-class ManageUploadTrackSerializer(serializers.ModelSerializer):
- artist = ManageUploadArtistSerializer()
- album = ManageUploadAlbumSerializer()
-
- class Meta:
- model = music_models.Track
- fields = ("id", "mbid", "title", "album", "artist", "creation_date", "position")
-
-
-class ManageUploadSerializer(serializers.ModelSerializer):
- track = ManageUploadTrackSerializer()
-
- class Meta:
- model = music_models.Upload
- fields = (
- "id",
- "path",
- "source",
- "filename",
- "mimetype",
- "track",
- "duration",
- "mimetype",
- "creation_date",
- "bitrate",
- "size",
- "path",
- )
-
-
-class ManageUploadActionSerializer(common_serializers.ActionSerializer):
- actions = [common_serializers.Action("delete", allow_all=False)]
- filterset_class = filters.ManageUploadFilterSet
-
- @transaction.atomic
- def handle_delete(self, objects):
- return objects.delete()
-
-
class PermissionsSerializer(serializers.Serializer):
def to_representation(self, o):
return o.get_permissions(defaults=self.context.get("default_permissions"))
@@ -493,3 +432,111 @@ class ManageArtistActionSerializer(common_serializers.ActionSerializer):
@transaction.atomic
def handle_delete(self, objects):
return objects.delete()
+
+
+class ManageLibraryActionSerializer(common_serializers.ActionSerializer):
+ actions = [common_serializers.Action("delete", allow_all=False)]
+ filterset_class = filters.ManageLibraryFilterSet
+
+ @transaction.atomic
+ def handle_delete(self, objects):
+ return objects.delete()
+
+
+class ManageUploadActionSerializer(common_serializers.ActionSerializer):
+ actions = [common_serializers.Action("delete", allow_all=False)]
+ filterset_class = filters.ManageUploadFilterSet
+
+ @transaction.atomic
+ def handle_delete(self, objects):
+ return objects.delete()
+
+
+class ManageLibrarySerializer(serializers.ModelSerializer):
+ domain = serializers.CharField(source="domain_name")
+ actor = ManageBaseActorSerializer()
+ uploads_count = serializers.SerializerMethodField()
+ followers_count = serializers.SerializerMethodField()
+
+ class Meta:
+ model = music_models.Library
+ fields = [
+ "id",
+ "uuid",
+ "fid",
+ "url",
+ "name",
+ "description",
+ "domain",
+ "is_local",
+ "creation_date",
+ "privacy_level",
+ "uploads_count",
+ "followers_count",
+ "followers_url",
+ "actor",
+ ]
+
+ def get_uploads_count(self, obj):
+ return getattr(obj, "_uploads_count", obj.uploads_count)
+
+ def get_followers_count(self, obj):
+ return getattr(obj, "followers_count", None)
+
+
+class ManageNestedLibrarySerializer(serializers.ModelSerializer):
+ domain = serializers.CharField(source="domain_name")
+ actor = ManageBaseActorSerializer()
+
+ class Meta:
+ model = music_models.Library
+ fields = [
+ "id",
+ "uuid",
+ "fid",
+ "url",
+ "name",
+ "description",
+ "domain",
+ "is_local",
+ "creation_date",
+ "privacy_level",
+ "followers_url",
+ "actor",
+ ]
+
+
+class ManageUploadSerializer(serializers.ModelSerializer):
+ track = ManageNestedTrackSerializer()
+ library = ManageNestedLibrarySerializer()
+ domain = serializers.CharField(source="domain_name")
+
+ class Meta:
+ model = music_models.Upload
+ fields = (
+ "id",
+ "uuid",
+ "fid",
+ "domain",
+ "is_local",
+ "audio_file",
+ "listen_url",
+ "source",
+ "filename",
+ "mimetype",
+ "duration",
+ "mimetype",
+ "bitrate",
+ "size",
+ "creation_date",
+ "accessed_date",
+ "modification_date",
+ "metadata",
+ "import_date",
+ "import_details",
+ "import_status",
+ "import_metadata",
+ "import_reference",
+ "track",
+ "library",
+ )
diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py
index f93667725..2d5da59e3 100644
--- a/api/funkwhale_api/manage/urls.py
+++ b/api/funkwhale_api/manage/urls.py
@@ -7,10 +7,11 @@ federation_router = routers.SimpleRouter()
federation_router.register(r"domains", views.ManageDomainViewSet, "domains")
library_router = routers.SimpleRouter()
-library_router.register(r"uploads", views.ManageUploadViewSet, "uploads")
-library_router.register(r"artists", views.ManageArtistViewSet, "artists")
library_router.register(r"albums", views.ManageAlbumViewSet, "albums")
+library_router.register(r"artists", views.ManageArtistViewSet, "artists")
+library_router.register(r"libraries", views.ManageLibraryViewSet, "libraries")
library_router.register(r"tracks", views.ManageTrackViewSet, "tracks")
+library_router.register(r"uploads", views.ManageUploadViewSet, "uploads")
moderation_router = routers.SimpleRouter()
moderation_router.register(
diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py
index 6fc1a2f1e..48ed62a02 100644
--- a/api/funkwhale_api/manage/views.py
+++ b/api/funkwhale_api/manage/views.py
@@ -19,38 +19,6 @@ from funkwhale_api.users import models as users_models
from . import filters, serializers
-class ManageUploadViewSet(
- mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
-):
- queryset = (
- music_models.Upload.objects.all()
- .select_related("track__artist", "track__album__artist")
- .order_by("-id")
- )
- serializer_class = serializers.ManageUploadSerializer
- filterset_class = filters.ManageUploadFilterSet
- required_scope = "instance:libraries"
- ordering_fields = [
- "accessed_date",
- "modification_date",
- "creation_date",
- "track__artist__name",
- "bitrate",
- "size",
- "duration",
- ]
-
- @rest_decorators.action(methods=["post"], detail=False)
- def action(self, request, *args, **kwargs):
- queryset = self.get_queryset()
- serializer = serializers.ManageUploadActionSerializer(
- request.data, queryset=queryset
- )
- serializer.is_valid(raise_exception=True)
- result = serializer.save()
- return response.Response(result, status=200)
-
-
def get_stats(tracks, target):
data = {}
tracks = list(tracks.values_list("pk", flat=True))
@@ -70,6 +38,12 @@ def get_stats(tracks, target):
).count()
data["libraries"] = uploads.values_list("library", flat=True).distinct().count()
data["uploads"] = uploads.count()
+ data.update(get_media_stats(uploads))
+ return data
+
+
+def get_media_stats(uploads):
+ data = {}
data["media_total_size"] = uploads.aggregate(v=Sum("size"))["v"] or 0
data["media_downloaded_size"] = (
uploads.with_file().aggregate(v=Sum("size"))["v"] or 0
@@ -85,6 +59,7 @@ class ManageArtistViewSet(
):
queryset = (
music_models.Artist.objects.all()
+ .distinct()
.order_by("-id")
.select_related("attributed_to")
.prefetch_related(
@@ -130,6 +105,7 @@ class ManageAlbumViewSet(
):
queryset = (
music_models.Album.objects.all()
+ .distinct()
.order_by("-id")
.select_related("attributed_to", "artist")
.prefetch_related("tracks")
@@ -164,6 +140,7 @@ class ManageTrackViewSet(
):
queryset = (
music_models.Track.objects.all()
+ .distinct()
.order_by("-id")
.select_related("attributed_to", "artist", "album__artist")
.annotate(uploads_count=Count("uploads"))
@@ -196,6 +173,96 @@ class ManageTrackViewSet(
return response.Response(result, status=200)
+class ManageLibraryViewSet(
+ mixins.ListModelMixin,
+ mixins.RetrieveModelMixin,
+ mixins.DestroyModelMixin,
+ viewsets.GenericViewSet,
+):
+ lookup_field = "uuid"
+ queryset = (
+ music_models.Library.objects.all()
+ .distinct()
+ .order_by("-id")
+ .select_related("actor")
+ .annotate(
+ followers_count=Count("received_follows", distinct=True),
+ _uploads_count=Count("uploads", distinct=True),
+ )
+ )
+ serializer_class = serializers.ManageLibrarySerializer
+ filterset_class = filters.ManageLibraryFilterSet
+ required_scope = "instance:libraries"
+
+ @rest_decorators.action(methods=["get"], detail=True)
+ def stats(self, request, *args, **kwargs):
+ library = self.get_object()
+ uploads = library.uploads.all()
+ tracks = uploads.values_list("track", flat=True).distinct()
+ albums = (
+ music_models.Track.objects.filter(pk__in=tracks)
+ .values_list("album", flat=True)
+ .distinct()
+ )
+ artists = set(
+ music_models.Album.objects.filter(pk__in=albums).values_list(
+ "artist", flat=True
+ )
+ ) | set(
+ music_models.Track.objects.filter(pk__in=tracks).values_list(
+ "artist", flat=True
+ )
+ )
+
+ data = {
+ "uploads": uploads.count(),
+ "followers": library.received_follows.count(),
+ "tracks": tracks.count(),
+ "albums": albums.count(),
+ "artists": len(artists),
+ }
+ data.update(get_media_stats(uploads.all()))
+ return response.Response(data, status=200)
+
+ @rest_decorators.action(methods=["post"], detail=False)
+ def action(self, request, *args, **kwargs):
+ queryset = self.get_queryset()
+ serializer = serializers.ManageTrackActionSerializer(
+ request.data, queryset=queryset
+ )
+ serializer.is_valid(raise_exception=True)
+ result = serializer.save()
+ return response.Response(result, status=200)
+
+
+class ManageUploadViewSet(
+ mixins.ListModelMixin,
+ mixins.RetrieveModelMixin,
+ mixins.DestroyModelMixin,
+ viewsets.GenericViewSet,
+):
+ lookup_field = "uuid"
+ queryset = (
+ music_models.Upload.objects.all()
+ .distinct()
+ .order_by("-id")
+ .select_related("library__actor", "track__artist", "track__album__artist")
+ )
+ serializer_class = serializers.ManageUploadSerializer
+ filterset_class = filters.ManageUploadFilterSet
+ required_scope = "instance:libraries"
+
+ @rest_decorators.action(methods=["post"], detail=False)
+ def action(self, request, *args, **kwargs):
+ queryset = self.get_queryset()
+ serializer = serializers.ManageTrackActionSerializer(
+ request.data, queryset=queryset
+ )
+ serializer.is_valid(raise_exception=True)
+ result = serializer.save()
+ return response.Response(result, status=200)
+
+
class ManageUserViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py
index 7ad88d45f..4b004bf15 100644
--- a/api/funkwhale_api/music/models.py
+++ b/api/funkwhale_api/music/models.py
@@ -649,7 +649,7 @@ class Track(APIModelMixin):
return licenses.LICENSES_BY_ID.get(self.license_id)
-class UploadQuerySet(models.QuerySet):
+class UploadQuerySet(common_models.NullsLastQuerySet):
def playable_by(self, actor, include=True):
libraries = Library.objects.viewable_by(actor)
@@ -746,6 +746,18 @@ class Upload(models.Model):
objects = UploadQuerySet.as_manager()
+ @property
+ def is_local(self):
+ return federation_utils.is_local(self.fid)
+
+ @property
+ def domain_name(self):
+ if not self.fid:
+ return
+
+ parsed = urllib.parse.urlparse(self.fid)
+ return parsed.hostname
+
def download_audio_from_remote(self, actor):
from funkwhale_api.common import session
from funkwhale_api.federation import signing
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 336a87ce0..86ea5d406 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -440,8 +440,6 @@ class UploadViewSet(
"artist__name",
)
- fetches = federation_decorators.fetches_route()
-
def get_queryset(self):
qs = super().get_queryset()
return qs.filter(library__actor=self.request.user.actor)
diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py
index 64a26538f..65c75c2c3 100644
--- a/api/tests/manage/test_serializers.py
+++ b/api/tests/manage/test_serializers.py
@@ -399,12 +399,73 @@ def test_manage_track_serializer(factories, now):
assert s.data == expected
+def test_manage_library_serializer(factories, now):
+ library = factories["music.Library"]()
+ setattr(library, "followers_count", 42)
+ setattr(library, "_uploads_count", 44)
+ expected = {
+ "id": library.id,
+ "fid": library.fid,
+ "url": library.url,
+ "uuid": str(library.uuid),
+ "followers_url": library.followers_url,
+ "domain": library.domain_name,
+ "is_local": library.is_local,
+ "name": library.name,
+ "description": library.description,
+ "privacy_level": library.privacy_level,
+ "creation_date": library.creation_date.isoformat().split("+")[0] + "Z",
+ "actor": serializers.ManageBaseActorSerializer(library.actor).data,
+ "uploads_count": 44,
+ "followers_count": 42,
+ }
+ s = serializers.ManageLibrarySerializer(library)
+
+ assert s.data == expected
+
+
+def test_manage_upload_serializer(factories, now):
+ upload = factories["music.Upload"]()
+
+ expected = {
+ "id": upload.id,
+ "fid": upload.fid,
+ "audio_file": upload.audio_file.url,
+ "listen_url": upload.listen_url,
+ "uuid": str(upload.uuid),
+ "domain": upload.domain_name,
+ "is_local": upload.is_local,
+ "duration": upload.duration,
+ "size": upload.size,
+ "bitrate": upload.bitrate,
+ "mimetype": upload.mimetype,
+ "source": upload.source,
+ "filename": upload.filename,
+ "metadata": upload.metadata,
+ "creation_date": upload.creation_date.isoformat().split("+")[0] + "Z",
+ "modification_date": upload.modification_date.isoformat().split("+")[0] + "Z",
+ "accessed_date": None,
+ "import_date": None,
+ "import_metadata": upload.import_metadata,
+ "import_status": upload.import_status,
+ "import_reference": upload.import_reference,
+ "import_details": upload.import_details,
+ "library": serializers.ManageNestedLibrarySerializer(upload.library).data,
+ "track": serializers.ManageNestedTrackSerializer(upload.track).data,
+ }
+ s = serializers.ManageUploadSerializer(upload)
+
+ assert s.data == expected
+
+
@pytest.mark.parametrize(
"factory, serializer_class",
[
("music.Track", serializers.ManageTrackActionSerializer),
("music.Album", serializers.ManageAlbumActionSerializer),
("music.Artist", serializers.ManageArtistActionSerializer),
+ ("music.Library", serializers.ManageLibraryActionSerializer),
+ ("music.Upload", serializers.ManageUploadActionSerializer),
],
)
def test_action_serializer_delete(factory, serializer_class, factories):
diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py
index 923d331d8..e3d136a0e 100644
--- a/api/tests/manage/test_views.py
+++ b/api/tests/manage/test_views.py
@@ -1,4 +1,3 @@
-import pytest
from django.urls import reverse
from funkwhale_api.federation import models as federation_models
@@ -6,21 +5,6 @@ from funkwhale_api.federation import tasks as federation_tasks
from funkwhale_api.manage import serializers
-@pytest.mark.skip(reason="Refactoring in progress")
-def test_upload_view(factories, superuser_api_client):
- uploads = factories["music.Upload"].create_batch(size=5)
- qs = uploads[0].__class__.objects.order_by("-creation_date")
- url = reverse("api:v1:manage:library:uploads-list")
-
- response = superuser_api_client.get(url, {"sort": "-creation_date"})
- expected = serializers.ManageUploadSerializer(
- qs, many=True, context={"request": response.wsgi_request}
- ).data
-
- assert response.data["count"] == len(uploads)
- assert response.data["results"] == expected
-
-
def test_user_view(factories, superuser_api_client, mocker):
mocker.patch("funkwhale_api.users.models.User.record_activity")
users = factories["users.User"].create_batch(size=5) + [superuser_api_client.user]
@@ -289,3 +273,82 @@ def test_track_delete(factories, superuser_api_client):
response = superuser_api_client.delete(url)
assert response.status_code == 204
+
+
+def test_library_list(factories, superuser_api_client, settings):
+ library = factories["music.Library"]()
+ url = reverse("api:v1:manage:library:libraries-list")
+ response = superuser_api_client.get(url)
+
+ assert response.status_code == 200
+
+ assert response.data["count"] == 1
+ assert response.data["results"][0]["id"] == library.id
+
+
+def test_library_detail(factories, superuser_api_client):
+ library = factories["music.Library"]()
+ url = reverse(
+ "api:v1:manage:library:libraries-detail", kwargs={"uuid": library.uuid}
+ )
+ response = superuser_api_client.get(url)
+
+ assert response.status_code == 200
+ assert response.data["id"] == library.id
+
+
+def test_library_detail_stats(factories, superuser_api_client):
+ library = factories["music.Library"]()
+ url = reverse(
+ "api:v1:manage:library:libraries-stats", kwargs={"uuid": library.uuid}
+ )
+ response = superuser_api_client.get(url)
+ expected = {
+ "uploads": 0,
+ "followers": 0,
+ "tracks": 0,
+ "albums": 0,
+ "artists": 0,
+ "media_total_size": 0,
+ "media_downloaded_size": 0,
+ }
+ assert response.status_code == 200
+ assert response.data == expected
+
+
+def test_library_delete(factories, superuser_api_client):
+ library = factories["music.Library"]()
+ url = reverse(
+ "api:v1:manage:library:libraries-detail", kwargs={"uuid": library.uuid}
+ )
+ response = superuser_api_client.delete(url)
+
+ assert response.status_code == 204
+
+
+def test_upload_list(factories, superuser_api_client, settings):
+ upload = factories["music.Upload"]()
+ url = reverse("api:v1:manage:library:uploads-list")
+ response = superuser_api_client.get(url)
+
+ assert response.status_code == 200
+
+ assert response.data["count"] == 1
+ assert response.data["results"][0]["id"] == upload.id
+
+
+def test_upload_detail(factories, superuser_api_client):
+ upload = factories["music.Upload"]()
+ url = reverse("api:v1:manage:library:uploads-detail", kwargs={"uuid": upload.uuid})
+ response = superuser_api_client.get(url)
+
+ assert response.status_code == 200
+ assert response.data["id"] == upload.id
+
+
+def test_upload_delete(factories, superuser_api_client):
+ upload = factories["music.Upload"]()
+ url = reverse("api:v1:manage:library:uploads-detail", kwargs={"uuid": upload.uuid})
+ response = superuser_api_client.delete(url)
+
+ assert response.status_code == 204
diff --git a/front/src/components/library/ImportStatusModal.vue b/front/src/components/library/ImportStatusModal.vue
new file mode 100644
index 000000000..5632e9594
--- /dev/null
+++ b/front/src/components/library/ImportStatusModal.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+ Upload is still pending and will soon be processed by the server.
+
+
+ Upload was successfully processed by the server.
+
+
+ Upload was skipped because a similar one is already available in one of your libraries.
+
+
+ An error occured during upload processing. You will find more information below.
+
+
+
+
+
+
+ Error type
+ |
+
+ {{ getErrorData(upload).label }}
+ |
+
+
+
+ Error detail
+ |
+
+ {{ getErrorData(upload).detail }}
+
+ -
+ {{ row.key}}: {{ row.value}}
+
+
+ |
+
+
+
+ Getting help
+ |
+
+
+ |
+
+
+
+ Debug information
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
diff --git a/front/src/components/manage/library/LibrariesTable.vue b/front/src/components/manage/library/LibrariesTable.vue
new file mode 100644
index 000000000..88c58f311
--- /dev/null
+++ b/front/src/components/manage/library/LibrariesTable.vue
@@ -0,0 +1,235 @@
+
+
+
+
+
+
+
+ Name |
+ Account |
+ Domain |
+ Visibility |
+ Uploads |
+ Followers |
+ Creation date |
+
+
+
+ {{ scope.obj.name }}
+ |
+
+
+
+
+ {{ scope.obj.actor.preferred_username }}
+ |
+
+
+
+
+
+ {{ scope.obj.domain }}
+
+
+
+ Local
+
+ |
+
+
+ {{ sharedLabels.fields.privacy_level.shortChoices[scope.obj.privacy_level] }}
+
+ |
+
+ {{ scope.obj.uploads_count }}
+ |
+
+ {{ scope.obj.followers_count }}
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+ Showing results %{ start }-%{ end } on %{ total }
+
+
+
+
+
+
+
diff --git a/front/src/components/manage/library/UploadsTable.vue b/front/src/components/manage/library/UploadsTable.vue
new file mode 100644
index 000000000..efc4e2394
--- /dev/null
+++ b/front/src/components/manage/library/UploadsTable.vue
@@ -0,0 +1,285 @@
+
+
+
+
+
+
+
+
+ Name |
+ Library |
+ Account |
+ Domain |
+ Visibility |
+ Import status |
+ Size |
+ Creation date |
+ Accessed date |
+
+
+
+
+ {{ displayName(scope.obj)|truncate(30, "…", true) }}
+
+ |
+
+
+
+
+
+ {{ scope.obj.library.name | truncate(20) }}
+
+ |
+
+
+
+ {{ scope.obj.library.actor.preferred_username }}
+ |
+
+
+
+
+
+ {{ scope.obj.domain }}
+
+
+
+ Local
+
+ |
+
+
+ {{ sharedLabels.fields.privacy_level.shortChoices[scope.obj.library.privacy_level] }}
+
+ |
+
+
+ {{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }}
+
+
+ |
+
+ {{ scope.obj.size | humanSize }}
+ N/A
+ |
+
+
+ |
+
+
+ N/A
+ |
+
+
+
+
+
+
+
+
+ Showing results %{ start }-%{ end } on %{ total }
+
+
+
+
+
+
+
diff --git a/front/src/components/mixins/Translations.vue b/front/src/components/mixins/Translations.vue
index b2bd455cc..56ea3ed15 100644
--- a/front/src/components/mixins/Translations.vue
+++ b/front/src/components/mixins/Translations.vue
@@ -11,12 +11,39 @@ export default {
me: this.$pgettext('Content/Settings/Dropdown', 'Nobody except me'),
instance: this.$pgettext('Content/Settings/Dropdown', 'Everyone on this instance'),
everyone: this.$pgettext('Content/Settings/Dropdown', 'Everyone, across all instances'),
+ },
+ shortChoices: {
+ me: this.$pgettext('Content/Settings/Dropdown/Short', 'Private'),
+ instance: this.$pgettext('Content/Settings/Dropdown/Short', 'Instance'),
+ everyone: this.$pgettext('Content/Settings/Dropdown/Short', 'Everyone'),
}
- }
+ },
+ import_status: {
+ detailTitle: this.$pgettext('Content/Library/Link.Title', 'Click to display more information about the import process for this upload'),
+ choices: {
+ skipped: {
+ label: this.$pgettext('Content/Library/*', 'Skipped'),
+ help: this.$pgettext('Content/Library/Help text', 'This track is already present in one of your libraries'),
+ },
+ pending: {
+ label: this.$pgettext('Content/Library/*/Short', 'Pending'),
+ help: this.$pgettext('Content/Library/Help text', 'This track has been uploaded, but hasn\'t been processed by the server yet'),
+ },
+ errored: {
+ label: this.$pgettext('Content/Library/Table/Short', 'Errored'),
+ help: this.$pgettext('Content/Library/Help text', 'This track could not be processed, please it is tagged correctly'),
+ },
+ finished: {
+ label: this.$pgettext('Content/Library/*', 'Finished'),
+ help: this.$pgettext('Content/Library/Help text', 'Imported'),
+ },
+ }
+ },
},
filters: {
creation_date: this.$pgettext('Content/*/*/Noun', 'Creation date'),
release_date: this.$pgettext('Content/*/*/Noun', 'Release date'),
+ accessed_date: this.$pgettext('Content/*/*/Noun', 'Accessed date'),
first_seen: this.$pgettext('Content/Moderation/Dropdown/Noun', 'First seen date'),
last_seen: this.$pgettext('Content/Moderation/Dropdown/Noun', 'Last seen date'),
modification_date: this.$pgettext('Content/Playlist/Dropdown/Noun', 'Modification date'),
diff --git a/front/src/filters.js b/front/src/filters.js
index 1edea76f6..966742619 100644
--- a/front/src/filters.js
+++ b/front/src/filters.js
@@ -2,13 +2,24 @@ import Vue from 'vue'
import moment from 'moment'
-export function truncate (str, max, ellipsis) {
+export function truncate (str, max, ellipsis, middle) {
max = max || 100
ellipsis = ellipsis || '…'
if (str.length <= max) {
return str
}
- return str.slice(0, max) + ellipsis
+ if (middle) {
+ var sepLen = 1,
+ charsToShow = max - sepLen,
+ frontChars = Math.ceil(charsToShow/2),
+ backChars = Math.floor(charsToShow/2);
+
+ return str.substr(0, frontChars) +
+ ellipsis +
+ str.substr(str.length - backChars);
+ } else {
+ return str.slice(0, max) + ellipsis
+ }
}
Vue.filter('truncate', truncate)
diff --git a/front/src/router/index.js b/front/src/router/index.js
index f9332f5f5..4b59deacc 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -43,6 +43,10 @@ import AdminLibraryAlbumsList from '@/views/admin/library/AlbumsList'
import AdminLibraryAlbumDetail from '@/views/admin/library/AlbumDetail'
import AdminLibraryTracksList from '@/views/admin/library/TracksList'
import AdminLibraryTrackDetail from '@/views/admin/library/TrackDetail'
+import AdminLibraryLibrariesList from '@/views/admin/library/LibrariesList'
+import AdminLibraryLibraryDetail from '@/views/admin/library/LibraryDetail'
+import AdminLibraryUploadsList from '@/views/admin/library/UploadsList'
+import AdminLibraryUploadDetail from '@/views/admin/library/UploadDetail'
import AdminUsersBase from '@/views/admin/users/Base'
import AdminUsersList from '@/views/admin/users/UsersList'
import AdminInvitationsList from '@/views/admin/users/InvitationsList'
@@ -303,6 +307,38 @@ export default new Router({
component: AdminLibraryTrackDetail,
props: true
},
+ {
+ path: 'libraries',
+ name: 'manage.library.libraries',
+ component: AdminLibraryLibrariesList,
+ props: (route) => {
+ return {
+ defaultQuery: route.query.q,
+ }
+ }
+ },
+ {
+ path: 'libraries/:id',
+ name: 'manage.library.libraries.detail',
+ component: AdminLibraryLibraryDetail,
+ props: true
+ },
+ {
+ path: 'uploads',
+ name: 'manage.library.uploads',
+ component: AdminLibraryUploadsList,
+ props: (route) => {
+ return {
+ defaultQuery: route.query.q,
+ }
+ }
+ },
+ {
+ path: 'uploads/:id',
+ name: 'manage.library.uploads.detail',
+ component: AdminLibraryUploadDetail,
+ props: true
+ },
]
},
{
diff --git a/front/src/views/admin/library/AlbumDetail.vue b/front/src/views/admin/library/AlbumDetail.vue
index b5d802d98..8de907a72 100644
--- a/front/src/views/admin/library/AlbumDetail.vue
+++ b/front/src/views/admin/library/AlbumDetail.vue
@@ -4,7 +4,7 @@
-
+
@@ -113,22 +113,14 @@
{{ object.artist.name }}
-
-
- First seen
- |
-
-
- |
-
- Domain
+
+ Domain
+
|
-
- {{ object.domain }}
-
+ {{ object.domain }}
|
@@ -153,6 +145,14 @@
+
+
+ First seen
+ |
+
+
+ |
+
Listenings
@@ -229,7 +229,9 @@
|
- Libraries
+
+ Libraries
+
|
{{ stats.libraries }}
@@ -237,7 +239,9 @@
|
- Uploads
+
+ Uploads
+
|
{{ stats.uploads }}
diff --git a/front/src/views/admin/library/ArtistDetail.vue b/front/src/views/admin/library/ArtistDetail.vue
index d509f7394..91b3542b2 100644
--- a/front/src/views/admin/library/ArtistDetail.vue
+++ b/front/src/views/admin/library/ArtistDetail.vue
@@ -102,22 +102,14 @@
{{ object.name }}
|
-
-
- First seen
- |
-
-
- |
-
- Domain
+
+ Domain
+
|
-
- {{ object.domain }}
-
+ {{ object.domain }}
|
@@ -142,6 +134,14 @@
+
+
+ First seen
+ |
+
+
+ |
+
Listenings
@@ -218,7 +218,9 @@
|
- Libraries
+
+ Libraries
+
|
{{ stats.libraries }}
@@ -226,7 +228,9 @@
|
- Uploads
+
+ Uploads
+
|
{{ stats.uploads }}
diff --git a/front/src/views/admin/library/Base.vue b/front/src/views/admin/library/Base.vue
index 4b7048a17..009e1ca95 100644
--- a/front/src/views/admin/library/Base.vue
+++ b/front/src/views/admin/library/Base.vue
@@ -13,6 +13,12 @@
Tracks
+ Libraries
+ Uploads
diff --git a/front/src/views/admin/library/LibrariesList.vue b/front/src/views/admin/library/LibrariesList.vue
new file mode 100644
index 000000000..495a660c1
--- /dev/null
+++ b/front/src/views/admin/library/LibrariesList.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/front/src/views/admin/library/LibraryDetail.vue b/front/src/views/admin/library/LibraryDetail.vue
new file mode 100644
index 000000000..a4df048da
--- /dev/null
+++ b/front/src/views/admin/library/LibraryDetail.vue
@@ -0,0 +1,321 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+ |
+
+ {{ object.name }}
+ |
+
+
+
+
+ Visibility
+
+ |
+
+ {{ sharedLabels.fields.privacy_level.shortChoices[object.privacy_level] }}
+ |
+
+
+
+
+ Account
+
+ |
+
+ {{ object.actor.preferred_username }}
+ |
+
+
+
+
+ Domain
+
+ |
+
+ {{ object.domain }}
+ |
+
+
+
+ Description
+ |
+
+ {{ object.description }}
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ First seen
+ |
+
+
+ |
+
+
+
+ Followers
+ |
+
+ {{ stats.followers }}
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cached size
+ |
+
+ {{ stats.media_downloaded_size | humanSize }}
+ |
+
+
+
+ Total size
+ |
+
+ {{ stats.media_total_size | humanSize }}
+ |
+
+
+
+
+ Artists
+
+ |
+
+ {{ stats.artists }}
+ |
+
+
+
+
+ Albums
+
+ |
+
+ {{ stats.albums }}
+ |
+
+
+
+
+ Tracks
+
+ |
+
+ {{ stats.tracks }}
+ |
+
+
+
+
+ Uploads
+
+ |
+
+ {{ stats.uploads }}
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front/src/views/admin/library/TrackDetail.vue b/front/src/views/admin/library/TrackDetail.vue
index 15e08f7a8..3256d0d63 100644
--- a/front/src/views/admin/library/TrackDetail.vue
+++ b/front/src/views/admin/library/TrackDetail.vue
@@ -4,7 +4,7 @@
-
+
@@ -133,14 +133,6 @@
{{ object.album.artist.name }}
|
-
-
- First seen
- |
-
-
- |
-
Position
@@ -175,12 +167,12 @@
|
- Domain
+
+ Domain
+
|
-
- {{ object.domain }}
-
+ {{ object.domain }}
|
@@ -205,6 +197,14 @@
+
+
+ First seen
+ |
+
+
+ |
+
Listenings
@@ -281,7 +281,9 @@
|
- Libraries
+
+ Libraries
+
|
{{ stats.libraries }}
@@ -289,7 +291,9 @@
|
- Uploads
+
+ Uploads
+
|
{{ stats.uploads }}
diff --git a/front/src/views/admin/library/UploadDetail.vue b/front/src/views/admin/library/UploadDetail.vue
new file mode 100644
index 000000000..604d0af30
--- /dev/null
+++ b/front/src/views/admin/library/UploadDetail.vue
@@ -0,0 +1,340 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+ |
+
+ {{ displayName(object) }}
+ |
+
+
+
+
+ Visibility
+
+ |
+
+ {{ sharedLabels.fields.privacy_level.shortChoices[object.library.privacy_level] }}
+ |
+
+
+
+
+ Account
+
+ |
+
+ {{ object.library.actor.preferred_username }}
+ |
+
+
+
+
+ Domain
+
+ |
+
+ {{ object.domain }}
+ |
+
+
+
+
+ Import status
+
+ |
+
+ {{ sharedLabels.fields.import_status.choices[object.import_status].label }}
+
+ |
+
+
+
+
+ Library
+
+ |
+
+ {{ object.library.name }}
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ First seen
+ |
+
+
+ |
+
+
+
+ Accessed date
+ |
+
+
+ N/A
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Track
+
+ |
+
+ {{ object.track.title }}
+ |
+
+
+
+ Cached size
+ |
+
+
+ {{ object.size | humanSize }}
+
+ N/A
+ |
+
+
+
+ Size
+ |
+
+ {{ object.size | humanSize }}
+ |
+
+
+
+ Bitrate
+ |
+
+
+ {{ object.bitrate | humanSize }}/s
+
+ N/A
+ |
+
+
+
+ Duration
+ |
+
+
+ {{ time.parse(object.duration) }}
+
+ N/A
+ |
+
+
+
+
+ Type
+
+ |
+
+
+ {{ object.mimetype }}
+
+ N/A
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front/src/views/admin/library/UploadsList.vue b/front/src/views/admin/library/UploadsList.vue
new file mode 100644
index 000000000..0d4d7b5e3
--- /dev/null
+++ b/front/src/views/admin/library/UploadsList.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/front/src/views/admin/moderation/AccountsDetail.vue b/front/src/views/admin/moderation/AccountsDetail.vue
index ec02f7e11..d863af52d 100644
--- a/front/src/views/admin/moderation/AccountsDetail.vue
+++ b/front/src/views/admin/moderation/AccountsDetail.vue
@@ -91,12 +91,12 @@
|
- Domain
+
+ Domain
+
|
-
- {{ object.domain }}
-
+ {{ object.domain }}
|
@@ -155,14 +155,6 @@
{{ object.type }}
-
-
- First seen
- |
-
-
- |
-
Last checked
@@ -210,6 +202,14 @@
+
+
+ First seen
+ |
+
+
+ |
+
Emitted messages
@@ -295,7 +295,9 @@
|
- Libraries
+
+ Libraries
+
|
{{ stats.libraries }}
@@ -303,7 +305,9 @@
|
- Uploads
+
+ Uploads
+
|
{{ stats.uploads }}
@@ -446,6 +450,9 @@ export default {
)
}
)
+ },
+ getQuery (field, value) {
+ return `${field}:"${value}"`
}
},
computed: {
diff --git a/front/src/views/admin/moderation/DomainsDetail.vue b/front/src/views/admin/moderation/DomainsDetail.vue
index e0e2b5680..575b15f93 100644
--- a/front/src/views/admin/moderation/DomainsDetail.vue
+++ b/front/src/views/admin/moderation/DomainsDetail.vue
@@ -74,14 +74,6 @@
-
-
- First seen
- |
-
-
- |
-
Last checked
@@ -155,6 +147,14 @@
+
+
+ First seen
+ |
+
+
+ |
+
- Libraries
+
+ Libraries
+
|
{{ stats.libraries }}
@@ -239,7 +241,9 @@
|
- Uploads
+
+ Uploads
+
|
{{ stats.uploads }}
@@ -247,7 +251,9 @@
|
- Artists
+
+ Artists
+
|
{{ stats.artists }}
@@ -255,7 +261,9 @@
|
- Albums
+
+ Albums
+
|
{{ stats.albums}}
@@ -263,7 +271,9 @@
|
- Tracks
+
+ Tracks
+
|
{{ stats.tracks }}
@@ -350,6 +360,9 @@ export default {
updatePolicy (policy) {
this.policy = policy
this.showPolicyForm = false
+ },
+ getQuery (field, value) {
+ return `${field}:"${value}"`
}
},
computed: {
diff --git a/front/src/views/content/libraries/FilesTable.vue b/front/src/views/content/libraries/FilesTable.vue
index f6c4e69ef..9ea4634ad 100644
--- a/front/src/views/content/libraries/FilesTable.vue
+++ b/front/src/views/content/libraries/FilesTable.vue
@@ -35,88 +35,7 @@
-
-
-
-
-
- Upload is still pending and will soon be processed by the server.
-
-
- Upload was successfully processed by the server.
-
-
- Upload was skipped because a similar one is already available in one of your libraries.
-
-
- An error occured during upload processing. You will find more information below.
-
-
-
-
-
-
- Error type
- |
-
- {{ getErrorData(detailedUpload).label }}
- |
-
-
-
- Error detail
- |
-
- {{ getErrorData(detailedUpload).detail }}
-
- -
- {{ row.key}}: {{ row.value}}
-
-
- |
-
-
-
- Getting help
- |
-
-
- |
-
-
-
- Debug information
- |
-
-
-
-
- |
-
-
-
-
-
-
-
-
+
|
-
- {{ labels.importStatuses[scope.obj.import_status].label }}
+
+ {{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }}
- |
@@ -216,33 +135,8 @@ import ActionTable from '@/components/common/ActionTable'
import OrderingMixin from '@/components/mixins/Ordering'
import TranslationsMixin from '@/components/mixins/Translations'
import SmartSearchMixin from '@/components/mixins/SmartSearch'
-import Modal from '@/components/semantic/Modal'
+import ImportStatusModal from '@/components/library/ImportStatusModal'
-function getErrors(payload) {
- let errors = []
- for (var k in payload) {
- if (payload.hasOwnProperty(k)) {
- let value = payload[k]
- if (Array.isArray(value)) {
- errors.push({
- key: k,
- value: value.join(', ')
- })
- } else {
- // possibly artists, so nested errors
- if (typeof value === 'object') {
- getErrors(value).forEach((e) => {
- errors.push({
- key: `${k} / ${e.key}`,
- value: e.value
- })
- })
- }
- }
- }
- }
- return errors
-}
export default {
mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
props: {
@@ -253,7 +147,7 @@ export default {
components: {
Pagination,
ActionTable,
- Modal
+ ImportStatusModal
},
data () {
return {
@@ -307,58 +201,11 @@ export default {
selectPage: function (page) {
this.page = page
},
- getErrorData (upload) {
- let payload = upload.import_details || {}
- let d = {
- supportUrl: 'https://governance.funkwhale.audio/g/246YOJ1m/funkwhale-support',
- errorRows: []
- }
- if (!payload.error_code) {
- d.errorCode = 'unknown_error'
- } else {
- d.errorCode = payload.error_code
- }
- d.documentationUrl = `https://docs.funkwhale.audio/users/upload.html#${d.errorCode}`
- if (d.errorCode === 'invalid_metadata') {
- d.label = this.$pgettext('Popup/Import/Error.Label', 'Invalid metadata')
- d.detail = this.$pgettext('Popup/Import/Error.Label', 'The metadata included in the file is invalid or some mandatory fields are missing.')
- let detail = payload.detail || {}
- d.errorRows = getErrors(detail)
- } else {
- d.label = this.$pgettext('Popup/Import/Error.Label', 'Unkwown error')
- d.detail = this.$pgettext('Popup/Import/Error.Label', 'An unkwown error occured')
- }
- let debugInfo = {
- source: upload.source,
- ...payload,
- }
- d.debugInfo = JSON.stringify(debugInfo, null, 4)
- return d
- }
},
computed: {
labels () {
return {
searchPlaceholder: this.$pgettext('Content/Library/Input.Placeholder', 'Search by title, artist, album…'),
- statusDetailTitle: this.$pgettext('Content/Library/Link.Title', 'Click to display more information about the import process for this upload'),
- importStatuses: {
- skipped: {
- label: this.$pgettext('Content/Library/*', 'Skipped'),
- help: this.$pgettext('Content/Library/Help text', 'This track is already present in one of your libraries'),
- },
- pending: {
- label: this.$pgettext('Content/Library/*/Short', 'Pending'),
- help: this.$pgettext('Content/Library/Help text', 'This track has been uploaded, but hasn\'t been processed by the server yet'),
- },
- errored: {
- label: this.$pgettext('Content/Library/Table/Short', 'Errored'),
- help: this.$pgettext('Content/Library/Help text', 'This track could not be processed, please it is tagged correctly'),
- },
- finished: {
- label: this.$pgettext('Content/Library/*', 'Finished'),
- help: this.$pgettext('Content/Library/Help text', 'Imported'),
- },
- }
}
},
actionFilters () {
| | | |