Admin UI for libraries and uploads
This commit is contained in:
parent
9aee135c2f
commit
a605bcbe76
|
@ -5,9 +5,10 @@ from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
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 import Lookup
|
||||||
from django.db.models.fields import Field
|
from django.db.models.fields import Field
|
||||||
|
from django.db.models.sql.compiler import SQLCompiler
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
@ -25,6 +26,41 @@ class NotEqual(Lookup):
|
||||||
return "%s <> %s" % (lhs, rhs), params
|
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:
|
class LocalFromFidQuerySet:
|
||||||
def local(self, include=True):
|
def local(self, include=True):
|
||||||
host = settings.FEDERATION_HOSTNAME
|
host = settings.FEDERATION_HOSTNAME
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import urllib.parse
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -43,6 +44,18 @@ class FederationMixin(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
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):
|
class ActorQuerySet(models.QuerySet):
|
||||||
def local(self, include=True):
|
def local(self, include=True):
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
from django import forms
|
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 django_filters import rest_framework as filters
|
||||||
|
|
||||||
from funkwhale_api.common import fields
|
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
|
from funkwhale_api.users import models as users_models
|
||||||
|
|
||||||
|
|
||||||
class ManageUploadFilterSet(filters.FilterSet):
|
class ActorField(forms.CharField):
|
||||||
q = fields.SearchFilter(
|
def clean(self, value):
|
||||||
search_fields=[
|
value = super().clean(value)
|
||||||
"track__title",
|
if not value:
|
||||||
"track__album__title",
|
return value
|
||||||
"track__artist__name",
|
|
||||||
"source",
|
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"],
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
return {"field": ActorField(), "handler": handler}
|
||||||
model = music_models.Upload
|
|
||||||
fields = ["q", "track__album", "track__artist", "track"]
|
|
||||||
|
|
||||||
|
|
||||||
class ManageArtistFilterSet(filters.FilterSet):
|
class ManageArtistFilterSet(filters.FilterSet):
|
||||||
|
@ -37,7 +54,11 @@ class ManageArtistFilterSet(filters.FilterSet):
|
||||||
filter_fields={
|
filter_fields={
|
||||||
"domain": {
|
"domain": {
|
||||||
"handler": lambda v: federation_utils.get_domain_query_from_url(v)
|
"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": {
|
"domain": {
|
||||||
"handler": lambda v: federation_utils.get_domain_query_from_url(v)
|
"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": {
|
"domain": {
|
||||||
"handler": lambda v: federation_utils.get_domain_query_from_url(v)
|
"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"]
|
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):
|
class ManageDomainFilterSet(filters.FilterSet):
|
||||||
q = fields.SearchFilter(search_fields=["name"])
|
q = fields.SearchFilter(search_fields=["name"])
|
||||||
|
|
||||||
|
|
|
@ -15,67 +15,6 @@ from funkwhale_api.users import models as users_models
|
||||||
from . import filters
|
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):
|
class PermissionsSerializer(serializers.Serializer):
|
||||||
def to_representation(self, o):
|
def to_representation(self, o):
|
||||||
return o.get_permissions(defaults=self.context.get("default_permissions"))
|
return o.get_permissions(defaults=self.context.get("default_permissions"))
|
||||||
|
@ -493,3 +432,111 @@ class ManageArtistActionSerializer(common_serializers.ActionSerializer):
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def handle_delete(self, objects):
|
def handle_delete(self, objects):
|
||||||
return objects.delete()
|
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",
|
||||||
|
)
|
||||||
|
|
|
@ -7,10 +7,11 @@ federation_router = routers.SimpleRouter()
|
||||||
federation_router.register(r"domains", views.ManageDomainViewSet, "domains")
|
federation_router.register(r"domains", views.ManageDomainViewSet, "domains")
|
||||||
|
|
||||||
library_router = routers.SimpleRouter()
|
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"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"tracks", views.ManageTrackViewSet, "tracks")
|
||||||
|
library_router.register(r"uploads", views.ManageUploadViewSet, "uploads")
|
||||||
|
|
||||||
moderation_router = routers.SimpleRouter()
|
moderation_router = routers.SimpleRouter()
|
||||||
moderation_router.register(
|
moderation_router.register(
|
||||||
|
|
|
@ -19,38 +19,6 @@ from funkwhale_api.users import models as users_models
|
||||||
from . import filters, serializers
|
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):
|
def get_stats(tracks, target):
|
||||||
data = {}
|
data = {}
|
||||||
tracks = list(tracks.values_list("pk", flat=True))
|
tracks = list(tracks.values_list("pk", flat=True))
|
||||||
|
@ -70,6 +38,12 @@ def get_stats(tracks, target):
|
||||||
).count()
|
).count()
|
||||||
data["libraries"] = uploads.values_list("library", flat=True).distinct().count()
|
data["libraries"] = uploads.values_list("library", flat=True).distinct().count()
|
||||||
data["uploads"] = uploads.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_total_size"] = uploads.aggregate(v=Sum("size"))["v"] or 0
|
||||||
data["media_downloaded_size"] = (
|
data["media_downloaded_size"] = (
|
||||||
uploads.with_file().aggregate(v=Sum("size"))["v"] or 0
|
uploads.with_file().aggregate(v=Sum("size"))["v"] or 0
|
||||||
|
@ -85,6 +59,7 @@ class ManageArtistViewSet(
|
||||||
):
|
):
|
||||||
queryset = (
|
queryset = (
|
||||||
music_models.Artist.objects.all()
|
music_models.Artist.objects.all()
|
||||||
|
.distinct()
|
||||||
.order_by("-id")
|
.order_by("-id")
|
||||||
.select_related("attributed_to")
|
.select_related("attributed_to")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
|
@ -130,6 +105,7 @@ class ManageAlbumViewSet(
|
||||||
):
|
):
|
||||||
queryset = (
|
queryset = (
|
||||||
music_models.Album.objects.all()
|
music_models.Album.objects.all()
|
||||||
|
.distinct()
|
||||||
.order_by("-id")
|
.order_by("-id")
|
||||||
.select_related("attributed_to", "artist")
|
.select_related("attributed_to", "artist")
|
||||||
.prefetch_related("tracks")
|
.prefetch_related("tracks")
|
||||||
|
@ -164,6 +140,7 @@ class ManageTrackViewSet(
|
||||||
):
|
):
|
||||||
queryset = (
|
queryset = (
|
||||||
music_models.Track.objects.all()
|
music_models.Track.objects.all()
|
||||||
|
.distinct()
|
||||||
.order_by("-id")
|
.order_by("-id")
|
||||||
.select_related("attributed_to", "artist", "album__artist")
|
.select_related("attributed_to", "artist", "album__artist")
|
||||||
.annotate(uploads_count=Count("uploads"))
|
.annotate(uploads_count=Count("uploads"))
|
||||||
|
@ -196,6 +173,96 @@ class ManageTrackViewSet(
|
||||||
return response.Response(result, status=200)
|
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(
|
class ManageUserViewSet(
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
|
|
|
@ -649,7 +649,7 @@ class Track(APIModelMixin):
|
||||||
return licenses.LICENSES_BY_ID.get(self.license_id)
|
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):
|
def playable_by(self, actor, include=True):
|
||||||
libraries = Library.objects.viewable_by(actor)
|
libraries = Library.objects.viewable_by(actor)
|
||||||
|
|
||||||
|
@ -746,6 +746,18 @@ class Upload(models.Model):
|
||||||
|
|
||||||
objects = UploadQuerySet.as_manager()
|
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):
|
def download_audio_from_remote(self, actor):
|
||||||
from funkwhale_api.common import session
|
from funkwhale_api.common import session
|
||||||
from funkwhale_api.federation import signing
|
from funkwhale_api.federation import signing
|
||||||
|
|
|
@ -440,8 +440,6 @@ class UploadViewSet(
|
||||||
"artist__name",
|
"artist__name",
|
||||||
)
|
)
|
||||||
|
|
||||||
fetches = federation_decorators.fetches_route()
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
return qs.filter(library__actor=self.request.user.actor)
|
return qs.filter(library__actor=self.request.user.actor)
|
||||||
|
|
|
@ -399,12 +399,73 @@ def test_manage_track_serializer(factories, now):
|
||||||
assert s.data == expected
|
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(
|
@pytest.mark.parametrize(
|
||||||
"factory, serializer_class",
|
"factory, serializer_class",
|
||||||
[
|
[
|
||||||
("music.Track", serializers.ManageTrackActionSerializer),
|
("music.Track", serializers.ManageTrackActionSerializer),
|
||||||
("music.Album", serializers.ManageAlbumActionSerializer),
|
("music.Album", serializers.ManageAlbumActionSerializer),
|
||||||
("music.Artist", serializers.ManageArtistActionSerializer),
|
("music.Artist", serializers.ManageArtistActionSerializer),
|
||||||
|
("music.Library", serializers.ManageLibraryActionSerializer),
|
||||||
|
("music.Upload", serializers.ManageUploadActionSerializer),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_action_serializer_delete(factory, serializer_class, factories):
|
def test_action_serializer_delete(factory, serializer_class, factories):
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import pytest
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from funkwhale_api.federation import models as federation_models
|
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
|
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):
|
def test_user_view(factories, superuser_api_client, mocker):
|
||||||
mocker.patch("funkwhale_api.users.models.User.record_activity")
|
mocker.patch("funkwhale_api.users.models.User.record_activity")
|
||||||
users = factories["users.User"].create_batch(size=5) + [superuser_api_client.user]
|
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)
|
response = superuser_api_client.delete(url)
|
||||||
|
|
||||||
assert response.status_code == 204
|
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
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<modal :show.sync="showModal">
|
||||||
|
<div class="header">
|
||||||
|
<translate translate-context="Popup/Import/Title">Import detail</translate>
|
||||||
|
</div>
|
||||||
|
<div class="content" v-if="upload">
|
||||||
|
<div class="description">
|
||||||
|
<div class="ui message" v-if="upload.import_status === 'pending'">
|
||||||
|
<translate translate-context="Popup/Import/Message">Upload is still pending and will soon be processed by the server.</translate>
|
||||||
|
</div>
|
||||||
|
<div class="ui success message" v-if="upload.import_status === 'finished'">
|
||||||
|
<translate translate-context="Popup/Import/Message">Upload was successfully processed by the server.</translate>
|
||||||
|
</div>
|
||||||
|
<div class="ui warning message" v-if="upload.import_status === 'skipped'">
|
||||||
|
<translate translate-context="Popup/Import/Message">Upload was skipped because a similar one is already available in one of your libraries.</translate>
|
||||||
|
</div>
|
||||||
|
<div class="ui error message" v-if="upload.import_status === 'errored'">
|
||||||
|
<translate translate-context="Popup/Import/Message">An error occured during upload processing. You will find more information below.</translate>
|
||||||
|
</div>
|
||||||
|
<template v-if="upload.import_status === 'errored'">
|
||||||
|
<table class="ui very basic collapsing celled table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Popup/Import/Table.Label/Noun">Error type</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ getErrorData(upload).label }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Popup/Import/Table.Label/Noun">Error detail</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ getErrorData(upload).detail }}
|
||||||
|
<ul v-if="getErrorData(upload).errorRows.length > 0">
|
||||||
|
<li v-for="row in getErrorData(upload).errorRows">
|
||||||
|
{{ row.key}}: {{ row.value}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Popup/Import/Table.Label/Noun">Getting help</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a :href="getErrorData(upload).documentationUrl" target="_blank">
|
||||||
|
<translate translate-context="Popup/Import/Table.Label/Value">Read our documentation for this error</translate>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a :href="getErrorData(upload).supportUrl" target="_blank">
|
||||||
|
<translate translate-context="Popup/Import/Table.Label/Value">Open a support thread (include the debug information below in your message)</translate>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Popup/Import/Table.Label/Noun">Debug information</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="ui form">
|
||||||
|
<textarea class="ui textarea" rows="10" :value="getErrorData(upload).debugInfo"></textarea>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<div class="ui deny button">
|
||||||
|
<translate translate-context="*/*/Button.Label/Verb">Close</translate>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Modal from '@/components/semantic/Modal'
|
||||||
|
|
||||||
|
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 {
|
||||||
|
props: ['upload', "show"],
|
||||||
|
components: {
|
||||||
|
Modal
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
showModal: this.show
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
showModal (v) {
|
||||||
|
this.$emit('update:show', v)
|
||||||
|
},
|
||||||
|
show (v) {
|
||||||
|
this.showModal = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,235 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="ui inline form">
|
||||||
|
<div class="fields">
|
||||||
|
<div class="ui six wide field">
|
||||||
|
<label><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
||||||
|
<form @submit.prevent="search.query = $refs.search.value">
|
||||||
|
<input name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label><translate translate-context="*/*/*">Visibility</translate></label>
|
||||||
|
<select class="ui dropdown" @change="addSearchToken('privacy_level', $event.target.value)" :value="getTokenValue('privacy_level', '')">
|
||||||
|
<option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
|
||||||
|
<option value="me">{{ sharedLabels.fields.privacy_level.shortChoices.me }}</option>
|
||||||
|
<option value="instance">{{ sharedLabels.fields.privacy_level.shortChoices.instance }}</option>
|
||||||
|
<option value="everyone">{{ sharedLabels.fields.privacy_level.shortChoices.everyone }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
|
||||||
|
<select class="ui dropdown" v-model="ordering">
|
||||||
|
<option v-for="option in orderingOptions" :value="option[0]">
|
||||||
|
{{ sharedLabels.filters[option[1]] }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
|
||||||
|
<select class="ui dropdown" v-model="orderingDirection">
|
||||||
|
<option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
|
||||||
|
<option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dimmable">
|
||||||
|
<div v-if="isLoading" class="ui active inverted dimmer">
|
||||||
|
<div class="ui loader"></div>
|
||||||
|
</div>
|
||||||
|
<action-table
|
||||||
|
v-if="result"
|
||||||
|
@action-launched="fetchData"
|
||||||
|
:objects-data="result"
|
||||||
|
:actions="actions"
|
||||||
|
action-url="manage/library/libraries/action/"
|
||||||
|
:filters="actionFilters">
|
||||||
|
<template slot="header-cells">
|
||||||
|
<th><translate translate-context="*/*/*">Name</translate></th>
|
||||||
|
<th><translate translate-context="*/*/*">Account</translate></th>
|
||||||
|
<th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th>
|
||||||
|
<th><translate translate-context="*/*/*">Visibility</translate></th>
|
||||||
|
<th><translate translate-context="Content/*/*/Noun">Uploads</translate></th>
|
||||||
|
<th><translate translate-context="Content/*/*/Noun">Followers</translate></th>
|
||||||
|
<th><translate translate-context="Content/*/*/Noun">Creation date</translate></th>
|
||||||
|
</template>
|
||||||
|
<template slot="row-cells" slot-scope="scope">
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.libraries.detail', params: {id: scope.obj.uuid }}">{{ scope.obj.name }}</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.actor.full_username }}">
|
||||||
|
<i class="wrench icon"></i>
|
||||||
|
</router-link>
|
||||||
|
<span role="button" class="discrete link" @click="addSearchToken('account', scope.obj.actor.full_username)" :title="scope.obj.actor.full_username">{{ scope.obj.actor.preferred_username }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<template v-if="!scope.obj.is_local">
|
||||||
|
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.domain }}">
|
||||||
|
<i class="wrench icon"></i>
|
||||||
|
</router-link>
|
||||||
|
<span role="button" class="discrete link" @click="addSearchToken('domain', scope.obj.domain)" :title="scope.obj.domain">{{ scope.obj.domain }}</span>
|
||||||
|
</template>
|
||||||
|
<span role="button" v-else class="ui tiny teal icon link label" @click="addSearchToken('domain', scope.obj.domain)">
|
||||||
|
<i class="home icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
class="discrete link"
|
||||||
|
@click="addSearchToken('privacy_level', scope.obj.privacy_level)"
|
||||||
|
:title="sharedLabels.fields.privacy_level.shortChoices[scope.obj.privacy_level]">
|
||||||
|
{{ sharedLabels.fields.privacy_level.shortChoices[scope.obj.privacy_level] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ scope.obj.uploads_count }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ scope.obj.followers_count }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date :date="scope.obj.creation_date"></human-date>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</action-table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<pagination
|
||||||
|
v-if="result && result.count > paginateBy"
|
||||||
|
@page-changed="selectPage"
|
||||||
|
:compact="true"
|
||||||
|
:current="page"
|
||||||
|
:paginate-by="paginateBy"
|
||||||
|
:total="result.count"
|
||||||
|
></pagination>
|
||||||
|
|
||||||
|
<span v-if="result && result.results.length > 0">
|
||||||
|
<translate translate-context="Content/*/Paragraph"
|
||||||
|
:translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
|
||||||
|
Showing results %{ start }-%{ end } on %{ total }
|
||||||
|
</translate>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
import _ from '@/lodash'
|
||||||
|
import time from '@/utils/time'
|
||||||
|
import {normalizeQuery, parseTokens} from '@/search'
|
||||||
|
import Pagination from '@/components/Pagination'
|
||||||
|
import ActionTable from '@/components/common/ActionTable'
|
||||||
|
import OrderingMixin from '@/components/mixins/Ordering'
|
||||||
|
import TranslationsMixin from '@/components/mixins/Translations'
|
||||||
|
import SmartSearchMixin from '@/components/mixins/SmartSearch'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
|
||||||
|
props: {
|
||||||
|
filters: {type: Object, required: false},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Pagination,
|
||||||
|
ActionTable
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
|
||||||
|
return {
|
||||||
|
time,
|
||||||
|
isLoading: false,
|
||||||
|
result: null,
|
||||||
|
page: 1,
|
||||||
|
paginateBy: 50,
|
||||||
|
search: {
|
||||||
|
query: this.defaultQuery,
|
||||||
|
tokens: parseTokens(normalizeQuery(this.defaultQuery))
|
||||||
|
},
|
||||||
|
orderingDirection: defaultOrdering.direction || '+',
|
||||||
|
ordering: defaultOrdering.field,
|
||||||
|
orderingOptions: [
|
||||||
|
['creation_date', 'creation_date'],
|
||||||
|
['followers_count', 'followers'],
|
||||||
|
['uploads_count', 'uploads'],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData () {
|
||||||
|
let params = _.merge({
|
||||||
|
'page': this.page,
|
||||||
|
'page_size': this.paginateBy,
|
||||||
|
'q': this.search.query,
|
||||||
|
'ordering': this.getOrderingAsString()
|
||||||
|
}, this.filters)
|
||||||
|
let self = this
|
||||||
|
self.isLoading = true
|
||||||
|
self.checked = []
|
||||||
|
axios.get('/manage/library/libraries/', {params: params}).then((response) => {
|
||||||
|
self.result = response.data
|
||||||
|
self.isLoading = false
|
||||||
|
}, error => {
|
||||||
|
self.isLoading = false
|
||||||
|
self.errors = error.backendErrors
|
||||||
|
})
|
||||||
|
},
|
||||||
|
selectPage: function (page) {
|
||||||
|
this.page = page
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labels () {
|
||||||
|
return {
|
||||||
|
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by domain, actor, name, description…')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actionFilters () {
|
||||||
|
var currentFilters = {
|
||||||
|
q: this.search.query
|
||||||
|
}
|
||||||
|
if (this.filters) {
|
||||||
|
return _.merge(currentFilters, this.filters)
|
||||||
|
} else {
|
||||||
|
return currentFilters
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions () {
|
||||||
|
let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete')
|
||||||
|
let confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected library will be removed, as well as associated uploads and follows. This action is irreversible.')
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
label: deleteLabel,
|
||||||
|
confirmationMessage: confirmationMessage,
|
||||||
|
isDangerous: true,
|
||||||
|
allowAll: false,
|
||||||
|
confirmColor: 'red',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
search (newValue) {
|
||||||
|
this.page = 1
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
page () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
ordering () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
orderingDirection () {
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,285 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="ui inline form">
|
||||||
|
<div class="fields">
|
||||||
|
<div class="ui six wide field">
|
||||||
|
<label><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
||||||
|
<form @submit.prevent="search.query = $refs.search.value">
|
||||||
|
<input name="search" ref="search" type="text" :value="search.query" :placeholder="labels.searchPlaceholder" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label><translate translate-context="*/*/*">Visibility</translate></label>
|
||||||
|
<select class="ui dropdown" @change="addSearchToken('privacy_level', $event.target.value)" :value="getTokenValue('privacy_level', '')">
|
||||||
|
<option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
|
||||||
|
<option value="me">{{ sharedLabels.fields.privacy_level.shortChoices.me }}</option>
|
||||||
|
<option value="instance">{{ sharedLabels.fields.privacy_level.shortChoices.instance }}</option>
|
||||||
|
<option value="everyone">{{ sharedLabels.fields.privacy_level.shortChoices.everyone }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label><translate translate-context="Content/Library/*/Noun">Import status</translate></label>
|
||||||
|
<select class="ui dropdown" @change="addSearchToken('status', $event.target.value)" :value="getTokenValue('status', '')">
|
||||||
|
<option value=""><translate translate-context="Content/*/Dropdown">All</translate></option>
|
||||||
|
<option value="pending"><translate translate-context="Content/Library/*/Short">Pending</translate></option>
|
||||||
|
<option value="skipped"><translate translate-context="Content/Library/*">Skipped</translate></option>
|
||||||
|
<option value="errored"><translate translate-context="Content/Library/Dropdown">Failed</translate></option>
|
||||||
|
<option value="finished"><translate translate-context="Content/Library/*">Finished</translate></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
|
||||||
|
<select class="ui dropdown" v-model="ordering">
|
||||||
|
<option v-for="option in orderingOptions" :value="option[0]">
|
||||||
|
{{ sharedLabels.filters[option[1]] }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering direction</translate></label>
|
||||||
|
<select class="ui dropdown" v-model="orderingDirection">
|
||||||
|
<option value="+"><translate translate-context="Content/Search/Dropdown">Ascending</translate></option>
|
||||||
|
<option value="-"><translate translate-context="Content/Search/Dropdown">Descending</translate></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<import-status-modal :upload="detailedUpload" :show.sync="showUploadDetailModal" />
|
||||||
|
<div class="dimmable">
|
||||||
|
<div v-if="isLoading" class="ui active inverted dimmer">
|
||||||
|
<div class="ui loader"></div>
|
||||||
|
</div>
|
||||||
|
<action-table
|
||||||
|
v-if="result"
|
||||||
|
@action-launched="fetchData"
|
||||||
|
:objects-data="result"
|
||||||
|
:actions="actions"
|
||||||
|
action-url="manage/library/uploads/action/"
|
||||||
|
:filters="actionFilters">
|
||||||
|
<template slot="header-cells">
|
||||||
|
<th><translate translate-context="*/*/*">Name</translate></th>
|
||||||
|
<th><translate translate-context="*/*/*">Library</translate></th>
|
||||||
|
<th><translate translate-context="*/*/*">Account</translate></th>
|
||||||
|
<th><translate translate-context="Content/Moderation/*/Noun">Domain</translate></th>
|
||||||
|
<th><translate translate-context="*/*/*">Visibility</translate></th>
|
||||||
|
<th><translate translate-context="Content/*/*/Noun">Import status</translate></th>
|
||||||
|
<th><translate translate-context="Content/*/*/Noun">Size</translate></th>
|
||||||
|
<th><translate translate-context="Content/*/*/Noun">Creation date</translate></th>
|
||||||
|
<th><translate translate-context="Content/*/*/Noun">Accessed date</translate></th>
|
||||||
|
</template>
|
||||||
|
<template slot="row-cells" slot-scope="scope">
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.uploads.detail', params: {id: scope.obj.uuid }}" :title="displayName(scope.obj)">
|
||||||
|
{{ displayName(scope.obj)|truncate(30, "…", true) }}
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.libraries.detail', params: {id: scope.obj.library.uuid }}">
|
||||||
|
<i class="wrench icon"></i>
|
||||||
|
</router-link>
|
||||||
|
<span role="button" class="discrete link"
|
||||||
|
@click="addSearchToken('library_id', scope.obj.library.id)"
|
||||||
|
:title="scope.obj.library.name">
|
||||||
|
{{ scope.obj.library.name | truncate(20) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: scope.obj.library.actor.full_username }}">
|
||||||
|
</router-link>
|
||||||
|
<span role="button" class="discrete link" @click="addSearchToken('account', scope.obj.library.actor.full_username)" :title="scope.obj.library.actor.full_username">{{ scope.obj.library.actor.preferred_username }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<template v-if="!scope.obj.is_local">
|
||||||
|
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: scope.obj.domain }}">
|
||||||
|
<i class="wrench icon"></i>
|
||||||
|
</router-link>
|
||||||
|
<span role="button" class="discrete link" @click="addSearchToken('domain', scope.obj.domain)" :title="scope.obj.domain">{{ scope.obj.domain }}</span>
|
||||||
|
</template>
|
||||||
|
<span role="button" v-else class="ui tiny teal icon link label" @click="addSearchToken('domain', scope.obj.domain)">
|
||||||
|
<i class="home icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
class="discrete link"
|
||||||
|
@click="addSearchToken('privacy_level', scope.obj.library.privacy_level)"
|
||||||
|
:title="sharedLabels.fields.privacy_level.shortChoices[scope.obj.library.privacy_level]">
|
||||||
|
{{ sharedLabels.fields.privacy_level.shortChoices[scope.obj.library.privacy_level] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="discrete link" @click="addSearchToken('status', scope.obj.import_status)" :title="sharedLabels.fields.import_status.choices[scope.obj.import_status].help">
|
||||||
|
{{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }}
|
||||||
|
</span>
|
||||||
|
<button class="ui tiny basic icon button" :title="sharedLabels.fields.import_status.detailTitle" @click="detailedUpload = scope.obj; showUploadDetailModal = true">
|
||||||
|
<i class="question circle outline icon"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="scope.obj.size">{{ scope.obj.size | humanSize }}</span>
|
||||||
|
<translate v-else translate-context="*/*/*">N/A</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date :date="scope.obj.creation_date"></human-date>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date v-if="scope.obj.accessed_date" :date="scope.obj.accessed_date"></human-date>
|
||||||
|
<translate v-else translate-context="*/*/*">N/A</translate>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</action-table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<pagination
|
||||||
|
v-if="result && result.count > paginateBy"
|
||||||
|
@page-changed="selectPage"
|
||||||
|
:compact="true"
|
||||||
|
:current="page"
|
||||||
|
:paginate-by="paginateBy"
|
||||||
|
:total="result.count"
|
||||||
|
></pagination>
|
||||||
|
|
||||||
|
<span v-if="result && result.results.length > 0">
|
||||||
|
<translate translate-context="Content/*/Paragraph"
|
||||||
|
:translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
|
||||||
|
Showing results %{ start }-%{ end } on %{ total }
|
||||||
|
</translate>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
import _ from '@/lodash'
|
||||||
|
import time from '@/utils/time'
|
||||||
|
import {normalizeQuery, parseTokens} from '@/search'
|
||||||
|
import Pagination from '@/components/Pagination'
|
||||||
|
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 ImportStatusModal from '@/components/library/ImportStatusModal'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
|
||||||
|
props: {
|
||||||
|
filters: {type: Object, required: false},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Pagination,
|
||||||
|
ActionTable,
|
||||||
|
ImportStatusModal
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
|
||||||
|
return {
|
||||||
|
detailedUpload: null,
|
||||||
|
showUploadDetailModal: false,
|
||||||
|
time,
|
||||||
|
isLoading: false,
|
||||||
|
result: null,
|
||||||
|
page: 1,
|
||||||
|
paginateBy: 50,
|
||||||
|
search: {
|
||||||
|
query: this.defaultQuery,
|
||||||
|
tokens: parseTokens(normalizeQuery(this.defaultQuery))
|
||||||
|
},
|
||||||
|
orderingDirection: defaultOrdering.direction || '+',
|
||||||
|
ordering: defaultOrdering.field,
|
||||||
|
orderingOptions: [
|
||||||
|
['creation_date', 'creation_date'],
|
||||||
|
['modification_date', 'modification_date'],
|
||||||
|
['accessed_date', 'accessed_date'],
|
||||||
|
['size', 'size'],
|
||||||
|
['bitrate', 'bitrate'],
|
||||||
|
['duration', 'duration'],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData () {
|
||||||
|
let params = _.merge({
|
||||||
|
'page': this.page,
|
||||||
|
'page_size': this.paginateBy,
|
||||||
|
'q': this.search.query,
|
||||||
|
'ordering': this.getOrderingAsString()
|
||||||
|
}, this.filters)
|
||||||
|
let self = this
|
||||||
|
self.isLoading = true
|
||||||
|
self.checked = []
|
||||||
|
axios.get('/manage/library/uploads/', {params: params}).then((response) => {
|
||||||
|
self.result = response.data
|
||||||
|
self.isLoading = false
|
||||||
|
}, error => {
|
||||||
|
self.isLoading = false
|
||||||
|
self.errors = error.backendErrors
|
||||||
|
})
|
||||||
|
},
|
||||||
|
selectPage: function (page) {
|
||||||
|
this.page = page
|
||||||
|
},
|
||||||
|
displayName (upload) {
|
||||||
|
if (upload.filename) {
|
||||||
|
return upload.filename
|
||||||
|
}
|
||||||
|
if (upload.source) {
|
||||||
|
return upload.source
|
||||||
|
}
|
||||||
|
return upload.uuid
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labels () {
|
||||||
|
return {
|
||||||
|
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by domain, actor, name, reference, source…')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actionFilters () {
|
||||||
|
var currentFilters = {
|
||||||
|
q: this.search.query
|
||||||
|
}
|
||||||
|
if (this.filters) {
|
||||||
|
return _.merge(currentFilters, this.filters)
|
||||||
|
} else {
|
||||||
|
return currentFilters
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions () {
|
||||||
|
let deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete')
|
||||||
|
let confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected upload will be removed. This action is irreversible.')
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
label: deleteLabel,
|
||||||
|
confirmationMessage: confirmationMessage,
|
||||||
|
isDangerous: true,
|
||||||
|
allowAll: false,
|
||||||
|
confirmColor: 'red',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
search (newValue) {
|
||||||
|
this.page = 1
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
page () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
ordering () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
orderingDirection () {
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -11,12 +11,39 @@ export default {
|
||||||
me: this.$pgettext('Content/Settings/Dropdown', 'Nobody except me'),
|
me: this.$pgettext('Content/Settings/Dropdown', 'Nobody except me'),
|
||||||
instance: this.$pgettext('Content/Settings/Dropdown', 'Everyone on this instance'),
|
instance: this.$pgettext('Content/Settings/Dropdown', 'Everyone on this instance'),
|
||||||
everyone: this.$pgettext('Content/Settings/Dropdown', 'Everyone, across all instances'),
|
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: {
|
filters: {
|
||||||
creation_date: this.$pgettext('Content/*/*/Noun', 'Creation date'),
|
creation_date: this.$pgettext('Content/*/*/Noun', 'Creation date'),
|
||||||
release_date: this.$pgettext('Content/*/*/Noun', 'Release 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'),
|
first_seen: this.$pgettext('Content/Moderation/Dropdown/Noun', 'First seen date'),
|
||||||
last_seen: this.$pgettext('Content/Moderation/Dropdown/Noun', 'Last seen date'),
|
last_seen: this.$pgettext('Content/Moderation/Dropdown/Noun', 'Last seen date'),
|
||||||
modification_date: this.$pgettext('Content/Playlist/Dropdown/Noun', 'Modification date'),
|
modification_date: this.$pgettext('Content/Playlist/Dropdown/Noun', 'Modification date'),
|
||||||
|
|
|
@ -2,14 +2,25 @@ import Vue from 'vue'
|
||||||
|
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
|
||||||
export function truncate (str, max, ellipsis) {
|
export function truncate (str, max, ellipsis, middle) {
|
||||||
max = max || 100
|
max = max || 100
|
||||||
ellipsis = ellipsis || '…'
|
ellipsis = ellipsis || '…'
|
||||||
if (str.length <= max) {
|
if (str.length <= max) {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
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
|
return str.slice(0, max) + ellipsis
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Vue.filter('truncate', truncate)
|
Vue.filter('truncate', truncate)
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,10 @@ import AdminLibraryAlbumsList from '@/views/admin/library/AlbumsList'
|
||||||
import AdminLibraryAlbumDetail from '@/views/admin/library/AlbumDetail'
|
import AdminLibraryAlbumDetail from '@/views/admin/library/AlbumDetail'
|
||||||
import AdminLibraryTracksList from '@/views/admin/library/TracksList'
|
import AdminLibraryTracksList from '@/views/admin/library/TracksList'
|
||||||
import AdminLibraryTrackDetail from '@/views/admin/library/TrackDetail'
|
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 AdminUsersBase from '@/views/admin/users/Base'
|
||||||
import AdminUsersList from '@/views/admin/users/UsersList'
|
import AdminUsersList from '@/views/admin/users/UsersList'
|
||||||
import AdminInvitationsList from '@/views/admin/users/InvitationsList'
|
import AdminInvitationsList from '@/views/admin/users/InvitationsList'
|
||||||
|
@ -303,6 +307,38 @@ export default new Router({
|
||||||
component: AdminLibraryTrackDetail,
|
component: AdminLibraryTrackDetail,
|
||||||
props: true
|
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
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="object">
|
<template v-if="object">
|
||||||
<section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.name">
|
<section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.title">
|
||||||
<div class="ui stackable one column grid">
|
<div class="ui stackable one column grid">
|
||||||
<div class="ui column">
|
<div class="ui column">
|
||||||
<div class="segment-content">
|
<div class="segment-content">
|
||||||
|
@ -113,22 +113,14 @@
|
||||||
{{ object.artist.name }}
|
{{ object.artist.name }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<human-date :date="object.creation_date"></human-date>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="!object.is_local">
|
<tr v-if="!object.is_local">
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
|
||||||
<translate translate-context="Content/Moderation/*/Noun">Domain</translate>
|
<translate translate-context="Content/Moderation/*/Noun">Domain</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
|
|
||||||
{{ object.domain }}
|
{{ object.domain }}
|
||||||
</router-link>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -153,6 +145,14 @@
|
||||||
</div>
|
</div>
|
||||||
<table v-else class="ui very basic table">
|
<table v-else class="ui very basic table">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date :date="object.creation_date"></human-date>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<translate translate-context="*/*/*/Noun">Listenings</translate>
|
<translate translate-context="*/*/*/Noun">Listenings</translate>
|
||||||
|
@ -229,7 +229,9 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('album_id', object.id) }}">
|
||||||
<translate translate-context="*/*/*/Noun">Libraries</translate>
|
<translate translate-context="*/*/*/Noun">Libraries</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.libraries }}
|
{{ stats.libraries }}
|
||||||
|
@ -237,7 +239,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('album_id', object.id) }}">
|
||||||
<translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
|
<translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.uploads }}
|
{{ stats.uploads }}
|
||||||
|
|
|
@ -102,22 +102,14 @@
|
||||||
{{ object.name }}
|
{{ object.name }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<human-date :date="object.creation_date"></human-date>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="!object.is_local">
|
<tr v-if="!object.is_local">
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
|
||||||
<translate translate-context="Content/Moderation/*/Noun">Domain</translate>
|
<translate translate-context="Content/Moderation/*/Noun">Domain</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
|
|
||||||
{{ object.domain }}
|
{{ object.domain }}
|
||||||
</router-link>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -142,6 +134,14 @@
|
||||||
</div>
|
</div>
|
||||||
<table v-else class="ui very basic table">
|
<table v-else class="ui very basic table">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date :date="object.creation_date"></human-date>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<translate translate-context="*/*/*/Noun">Listenings</translate>
|
<translate translate-context="*/*/*/Noun">Listenings</translate>
|
||||||
|
@ -218,7 +218,9 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('artist_id', object.id) }}">
|
||||||
<translate translate-context="*/*/*/Noun">Libraries</translate>
|
<translate translate-context="*/*/*/Noun">Libraries</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.libraries }}
|
{{ stats.libraries }}
|
||||||
|
@ -226,7 +228,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('artist_id', object.id) }}">
|
||||||
<translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
|
<translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.uploads }}
|
{{ stats.uploads }}
|
||||||
|
|
|
@ -13,6 +13,12 @@
|
||||||
<router-link
|
<router-link
|
||||||
class="ui item"
|
class="ui item"
|
||||||
:to="{name: 'manage.library.tracks'}"><translate translate-context="*/*/*">Tracks</translate></router-link>
|
:to="{name: 'manage.library.tracks'}"><translate translate-context="*/*/*">Tracks</translate></router-link>
|
||||||
|
<router-link
|
||||||
|
class="ui item"
|
||||||
|
:to="{name: 'manage.library.libraries'}"><translate translate-context="*/*/*">Libraries</translate></router-link>
|
||||||
|
<router-link
|
||||||
|
class="ui item"
|
||||||
|
:to="{name: 'manage.library.uploads'}"><translate translate-context="*/*/*">Uploads</translate></router-link>
|
||||||
</nav>
|
</nav>
|
||||||
<router-view :key="$route.fullPath"></router-view>
|
<router-view :key="$route.fullPath"></router-view>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<main v-title="labels.title">
|
||||||
|
<section class="ui vertical stripe segment">
|
||||||
|
<h2 class="ui header">{{ labels.title }}</h2>
|
||||||
|
<div class="ui hidden divider"></div>
|
||||||
|
<libraries-table :update-url="true" :default-query="defaultQuery"></libraries-table>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LibrariesTable from "@/components/manage/library/LibrariesTable"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LibrariesTable
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
defaultQuery: {type: String, required: false},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labels() {
|
||||||
|
return {
|
||||||
|
title: this.$pgettext('*/*/*', 'Libraries')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,321 @@
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<div v-if="isLoading" class="ui vertical segment">
|
||||||
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||||
|
</div>
|
||||||
|
<template v-if="object">
|
||||||
|
<section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.name">
|
||||||
|
<div class="ui stackable one column grid">
|
||||||
|
<div class="ui column">
|
||||||
|
<div class="segment-content">
|
||||||
|
<h2 class="ui header">
|
||||||
|
<i class="circular inverted book icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
{{ object.name | truncate(100) }}
|
||||||
|
<div class="sub header">
|
||||||
|
<template v-if="object.is_local">
|
||||||
|
<span class="ui tiny teal label">
|
||||||
|
<i class="home icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
<div class="header-buttons">
|
||||||
|
|
||||||
|
<div class="ui icon buttons">
|
||||||
|
<a
|
||||||
|
v-if="$store.state.auth.profile.is_superuser"
|
||||||
|
class="ui labeled icon button"
|
||||||
|
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/library/${object.id}`)"
|
||||||
|
target="_blank" rel="noopener noreferrer">
|
||||||
|
<i class="wrench icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
||||||
|
</a>
|
||||||
|
<div class="ui floating dropdown icon button" v-dropdown>
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
<div class="menu">
|
||||||
|
<a
|
||||||
|
v-if="$store.state.auth.profile.is_superuser"
|
||||||
|
class="basic item"
|
||||||
|
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/library/${object.id}`)"
|
||||||
|
target="_blank" rel="noopener noreferrer">
|
||||||
|
<i class="wrench icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
||||||
|
</a>
|
||||||
|
<a class="basic item" :href="object.url || object.fid" target="_blank" rel="noopener noreferrer">
|
||||||
|
<i class="external icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui buttons">
|
||||||
|
<dangerous-button
|
||||||
|
:class="['ui', {loading: isLoading}, 'basic button']"
|
||||||
|
:action="remove">
|
||||||
|
<translate translate-context="*/*/*/Verb">Delete</translate>
|
||||||
|
<p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this library?</translate></p>
|
||||||
|
<div slot="modal-content">
|
||||||
|
<p><translate translate-context="Content/Moderation/Paragraph">The library will be removed, as well as associated uploads, and follows. This action is irreversible.</translate></p>
|
||||||
|
</div>
|
||||||
|
<p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p>
|
||||||
|
</dangerous-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="ui vertical stripe segment">
|
||||||
|
<div class="ui stackable three column grid">
|
||||||
|
<div class="column">
|
||||||
|
<section>
|
||||||
|
<h3 class="ui header">
|
||||||
|
<i class="info icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
<translate translate-context="Content/Moderation/Title">Library data</translate>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<table class="ui very basic table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="*/*/*/Noun">Name</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ object.name }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('privacy_level', object.privacy_level) }}">
|
||||||
|
<translate translate-context="*/*/*">Visibility</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ sharedLabels.fields.privacy_level.shortChoices[object.privacy_level] }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: object.actor.full_username }}">
|
||||||
|
<translate translate-context="*/*/*/Noun">Account</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ object.actor.preferred_username }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!object.is_local">
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
|
||||||
|
<translate translate-context="Content/Moderation/*/Noun">Domain</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ object.domain }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="*/*/*/Noun">Description</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ object.description }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<section>
|
||||||
|
<h3 class="ui header">
|
||||||
|
<i class="feed icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
<translate translate-context="Content/Moderation/Title">Activity</translate>
|
||||||
|
<span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<div v-if="isLoadingStats" class="ui placeholder">
|
||||||
|
<div class="full line"></div>
|
||||||
|
<div class="short line"></div>
|
||||||
|
<div class="medium line"></div>
|
||||||
|
<div class="long line"></div>
|
||||||
|
</div>
|
||||||
|
<table v-else class="ui very basic table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date :date="object.creation_date"></human-date>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Federation/*/Noun">Followers</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ stats.followers }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<section>
|
||||||
|
<h3 class="ui header">
|
||||||
|
<i class="music icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
<translate translate-context="Content/Moderation/Title">Audio content</translate>
|
||||||
|
<span :data-tooltip="labels.statsWarning"><i class="question circle icon"></i></span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<div v-if="isLoadingStats" class="ui placeholder">
|
||||||
|
<div class="full line"></div>
|
||||||
|
<div class="short line"></div>
|
||||||
|
<div class="medium line"></div>
|
||||||
|
<div class="long line"></div>
|
||||||
|
</div>
|
||||||
|
<table v-else class="ui very basic table">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ stats.media_downloaded_size | humanSize }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Moderation/Table.Label">Total size</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ stats.media_total_size | humanSize }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.artists', query: {q: getQuery('library_id', object.id) }}">
|
||||||
|
<translate translate-context="*/*/*">Artists</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ stats.artists }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.albums', query: {q: getQuery('library_id', object.id) }}">
|
||||||
|
<translate translate-context="*/*/*">Albums</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ stats.albums }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('library_id', object.id) }}">
|
||||||
|
<translate translate-context="*/*/*">Tracks</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ stats.tracks }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('library_id', object.id) }}">
|
||||||
|
<translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ stats.uploads }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from "axios"
|
||||||
|
import logger from "@/logging"
|
||||||
|
import TranslationsMixin from "@/components/mixins/Translations"
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["id"],
|
||||||
|
mixins: [
|
||||||
|
TranslationsMixin
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
isLoadingStats: false,
|
||||||
|
object: null,
|
||||||
|
stats: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchData()
|
||||||
|
this.fetchStats()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData() {
|
||||||
|
var self = this
|
||||||
|
this.isLoading = true
|
||||||
|
let url = `manage/library/libraries/${this.id}/`
|
||||||
|
axios.get(url).then(response => {
|
||||||
|
self.object = response.data
|
||||||
|
self.isLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fetchStats() {
|
||||||
|
var self = this
|
||||||
|
this.isLoadingStats = true
|
||||||
|
let url = `manage/library/libraries/${this.id}/stats/`
|
||||||
|
axios.get(url).then(response => {
|
||||||
|
self.stats = response.data
|
||||||
|
self.isLoadingStats = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
remove () {
|
||||||
|
var self = this
|
||||||
|
this.isLoading = true
|
||||||
|
let url = `manage/library/libraries/${this.id}/`
|
||||||
|
axios.delete(url).then(response => {
|
||||||
|
self.$router.push({name: 'manage.library.libraries'})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getQuery (field, value) {
|
||||||
|
return `${field}:"${value}"`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labels() {
|
||||||
|
return {
|
||||||
|
statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -4,7 +4,7 @@
|
||||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="object">
|
<template v-if="object">
|
||||||
<section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.name">
|
<section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="object.title">
|
||||||
<div class="ui stackable one column grid">
|
<div class="ui stackable one column grid">
|
||||||
<div class="ui column">
|
<div class="ui column">
|
||||||
<div class="segment-content">
|
<div class="segment-content">
|
||||||
|
@ -133,14 +133,6 @@
|
||||||
{{ object.album.artist.name }}
|
{{ object.album.artist.name }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<human-date :date="object.creation_date"></human-date>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<translate translate-context="*/*/*/Noun">Position</translate>
|
<translate translate-context="*/*/*/Noun">Position</translate>
|
||||||
|
@ -175,12 +167,12 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!object.is_local">
|
<tr v-if="!object.is_local">
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
|
||||||
<translate translate-context="Content/Moderation/*/Noun">Domain</translate>
|
<translate translate-context="Content/Moderation/*/Noun">Domain</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
|
|
||||||
{{ object.domain }}
|
{{ object.domain }}
|
||||||
</router-link>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -205,6 +197,14 @@
|
||||||
</div>
|
</div>
|
||||||
<table v-else class="ui very basic table">
|
<table v-else class="ui very basic table">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date :date="object.creation_date"></human-date>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<translate translate-context="*/*/*/Noun">Listenings</translate>
|
<translate translate-context="*/*/*/Noun">Listenings</translate>
|
||||||
|
@ -281,7 +281,9 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('track_id', object.id) }}">
|
||||||
<translate translate-context="*/*/*/Noun">Libraries</translate>
|
<translate translate-context="*/*/*/Noun">Libraries</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.libraries }}
|
{{ stats.libraries }}
|
||||||
|
@ -289,7 +291,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('track_id', object.id) }}">
|
||||||
<translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
|
<translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.uploads }}
|
{{ stats.uploads }}
|
||||||
|
|
|
@ -0,0 +1,340 @@
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<div v-if="isLoading" class="ui vertical segment">
|
||||||
|
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||||
|
</div>
|
||||||
|
<template v-if="object">
|
||||||
|
<import-status-modal :upload="object" :show.sync="showUploadDetailModal" />
|
||||||
|
<section :class="['ui', 'head', 'vertical', 'stripe', 'segment']" v-title="displayName(object)">
|
||||||
|
<div class="ui stackable one column grid">
|
||||||
|
<div class="ui column">
|
||||||
|
<div class="segment-content">
|
||||||
|
<h2 class="ui header">
|
||||||
|
<i class="circular inverted file icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
{{ displayName(object) | truncate(100) }}
|
||||||
|
<div class="sub header">
|
||||||
|
<template v-if="object.is_local">
|
||||||
|
<span class="ui tiny teal label">
|
||||||
|
<i class="home icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
<div class="header-buttons">
|
||||||
|
|
||||||
|
<div class="ui icon buttons">
|
||||||
|
<a
|
||||||
|
v-if="$store.state.auth.profile.is_superuser"
|
||||||
|
class="ui labeled icon button"
|
||||||
|
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/upload/${object.id}`)"
|
||||||
|
target="_blank" rel="noopener noreferrer">
|
||||||
|
<i class="wrench icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
||||||
|
</a>
|
||||||
|
<div class="ui floating dropdown icon button" v-dropdown>
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
<div class="menu">
|
||||||
|
<a
|
||||||
|
v-if="$store.state.auth.profile.is_superuser"
|
||||||
|
class="basic item"
|
||||||
|
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/upload/${object.id}`)"
|
||||||
|
target="_blank" rel="noopener noreferrer">
|
||||||
|
<i class="wrench icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/Link/Verb">View in Django's admin</translate>
|
||||||
|
</a>
|
||||||
|
<a class="basic item" :href="object.url || object.fid" target="_blank" rel="noopener noreferrer">
|
||||||
|
<i class="external icon"></i>
|
||||||
|
<translate translate-context="Content/Moderation/Link/Verb">Open remote profile</translate>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui buttons">
|
||||||
|
<a class="ui labeled icon button" v-if="object.audio_file" :href="$store.getters['instance/absoluteUrl'](object.audio_file)" target="_blank" rel="noopener noreferrer">
|
||||||
|
<i class="download icon"></i>
|
||||||
|
<translate translate-context="Content/Track/Link/Verb">Download</translate>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="ui buttons">
|
||||||
|
<dangerous-button
|
||||||
|
:class="['ui', {loading: isLoading}, 'basic button']"
|
||||||
|
:action="remove">
|
||||||
|
<translate translate-context="*/*/*/Verb">Delete</translate>
|
||||||
|
<p slot="modal-header"><translate translate-context="Popup/Library/Title">Delete this upload?</translate></p>
|
||||||
|
<div slot="modal-content">
|
||||||
|
<p><translate translate-context="Content/Moderation/Paragraph">The upload will be removed. This action is irreversible.</translate></p>
|
||||||
|
</div>
|
||||||
|
<p slot="modal-confirm"><translate translate-context="*/*/*/Verb">Delete</translate></p>
|
||||||
|
</dangerous-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="ui vertical stripe segment">
|
||||||
|
<div class="ui stackable three column grid">
|
||||||
|
<div class="column">
|
||||||
|
<section>
|
||||||
|
<h3 class="ui header">
|
||||||
|
<i class="info icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
<translate translate-context="Content/Moderation/Title">Upload data</translate>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<table class="ui very basic table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="*/*/*/Noun">Name</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ displayName(object) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('privacy_level', object.library.privacy_level) }}">
|
||||||
|
<translate translate-context="*/*/*">Visibility</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ sharedLabels.fields.privacy_level.shortChoices[object.library.privacy_level] }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.moderation.accounts.detail', params: {id: object.library.actor.full_username }}">
|
||||||
|
<translate translate-context="*/*/*/Noun">Account</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ object.library.actor.preferred_username }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!object.is_local">
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
|
||||||
|
<translate translate-context="Content/Moderation/*/Noun">Domain</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ object.domain }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('status', object.import_status) }}">
|
||||||
|
<translate translate-context="Content/*/*/Noun">Import status</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ sharedLabels.fields.import_status.choices[object.import_status].label }}
|
||||||
|
<button class="ui tiny basic icon button" :title="sharedLabels.fields.import_status.detailTitle" @click="detailedUpload = object; showUploadDetailModal = true">
|
||||||
|
<i class="question circle outline icon"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.libraries.detail', params: {id: object.library.uuid }}">
|
||||||
|
<translate translate-context="*/*/*">Library</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ object.library.name }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<section>
|
||||||
|
<h3 class="ui header">
|
||||||
|
<i class="feed icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
<translate translate-context="Content/Moderation/Title">Activity</translate>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<table class="ui very basic table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date :date="object.creation_date"></human-date>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/*/*/Noun">Accessed date</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date v-if="object.accessed_date" :date="object.accessed_date"></human-date>
|
||||||
|
<translate v-else translate-context="*/*/*">N/A</translate>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<section>
|
||||||
|
<h3 class="ui header">
|
||||||
|
<i class="music icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
<translate translate-context="Content/Moderation/Title">Audio content</translate>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<table class="ui very basic table">
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="object.track">
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.tracks.detail', params: {id: object.track.id }}">
|
||||||
|
<translate translate-context="*/*/*">Track</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ object.track.title }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Moderation/Table.Label/Noun">Cached size</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<template v-if="object.audio_file">
|
||||||
|
{{ object.size | humanSize }}
|
||||||
|
</template>
|
||||||
|
<translate v-else translate-context="*/*/*">N/A</translate>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/*/*/Noun">Size</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ object.size | humanSize }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Track/*/Noun">Bitrate</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<template v-if="object.bitrate">
|
||||||
|
{{ object.bitrate | humanSize }}/s
|
||||||
|
</template>
|
||||||
|
<translate v-else translate-context="*/*/*">N/A</translate>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/*/*">Duration</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<template v-if="object.duration">
|
||||||
|
{{ time.parse(object.duration) }}
|
||||||
|
</template>
|
||||||
|
<translate v-else translate-context="*/*/*">N/A</translate>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('type', object.mimetype) }}">
|
||||||
|
<translate translate-context="Content/Track/Table.Label/Noun">Type</translate>
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<template v-if="object.mimetype">
|
||||||
|
{{ object.mimetype }}
|
||||||
|
</template>
|
||||||
|
<translate v-else translate-context="*/*/*">N/A</translate>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from "axios"
|
||||||
|
import logger from "@/logging"
|
||||||
|
import TranslationsMixin from "@/components/mixins/Translations"
|
||||||
|
import ImportStatusModal from '@/components/library/ImportStatusModal'
|
||||||
|
import time from '@/utils/time'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["id"],
|
||||||
|
mixins: [
|
||||||
|
TranslationsMixin,
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
ImportStatusModal
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
time,
|
||||||
|
detailedUpload: null,
|
||||||
|
showUploadDetailModal: false,
|
||||||
|
isLoading: true,
|
||||||
|
object: null,
|
||||||
|
stats: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData() {
|
||||||
|
var self = this
|
||||||
|
this.isLoading = true
|
||||||
|
let url = `manage/library/uploads/${this.id}/`
|
||||||
|
axios.get(url).then(response => {
|
||||||
|
self.object = response.data
|
||||||
|
self.isLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
remove () {
|
||||||
|
var self = this
|
||||||
|
this.isLoading = true
|
||||||
|
let url = `manage/library/uploads/${this.id}/`
|
||||||
|
axios.delete(url).then(response => {
|
||||||
|
self.$router.push({name: 'manage.library.uploads'})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getQuery (field, value) {
|
||||||
|
return `${field}:"${value}"`
|
||||||
|
},
|
||||||
|
displayName (upload) {
|
||||||
|
if (upload.filename) {
|
||||||
|
return upload.filename
|
||||||
|
}
|
||||||
|
if (upload.source) {
|
||||||
|
return upload.source
|
||||||
|
}
|
||||||
|
return upload.uuid
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labels() {
|
||||||
|
return {
|
||||||
|
statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<main v-title="labels.title">
|
||||||
|
<section class="ui vertical stripe segment">
|
||||||
|
<h2 class="ui header">{{ labels.title }}</h2>
|
||||||
|
<div class="ui hidden divider"></div>
|
||||||
|
<uploads-table :update-url="true" :default-query="defaultQuery"></uploads-table>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import UploadsTable from "@/components/manage/library/UploadsTable"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
UploadsTable
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
defaultQuery: {type: String, required: false},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labels() {
|
||||||
|
return {
|
||||||
|
title: this.$pgettext('*/*/*', 'Uploads')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -91,12 +91,12 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!object.user">
|
<tr v-if="!object.user">
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
|
||||||
<translate translate-context="Content/Moderation/*/Noun">Domain</translate>
|
<translate translate-context="Content/Moderation/*/Noun">Domain</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
|
|
||||||
{{ object.domain }}
|
{{ object.domain }}
|
||||||
</router-link>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -155,14 +155,6 @@
|
||||||
{{ object.type }}
|
{{ object.type }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!object.user">
|
|
||||||
<td>
|
|
||||||
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<human-date :date="object.creation_date"></human-date>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="!object.user">
|
<tr v-if="!object.user">
|
||||||
<td>
|
<td>
|
||||||
<translate translate-context="Content/*/Table.Label">Last checked</translate>
|
<translate translate-context="Content/*/Table.Label">Last checked</translate>
|
||||||
|
@ -210,6 +202,14 @@
|
||||||
</div>
|
</div>
|
||||||
<table v-else class="ui very basic table">
|
<table v-else class="ui very basic table">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr v-if="!object.user">
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date :date="object.creation_date"></human-date>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<translate translate-context="Content/Moderation/Table.Label/Noun">Emitted messages</translate>
|
<translate translate-context="Content/Moderation/Table.Label/Noun">Emitted messages</translate>
|
||||||
|
@ -295,7 +295,9 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('account', object.full_username) }}">
|
||||||
<translate translate-context="*/*/*/Noun">Libraries</translate>
|
<translate translate-context="*/*/*/Noun">Libraries</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.libraries }}
|
{{ stats.libraries }}
|
||||||
|
@ -303,7 +305,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('account', object.full_username) }}">
|
||||||
<translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
|
<translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.uploads }}
|
{{ stats.uploads }}
|
||||||
|
@ -446,6 +450,9 @@ export default {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
getQuery (field, value) {
|
||||||
|
return `${field}:"${value}"`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -74,14 +74,6 @@
|
||||||
</h3>
|
</h3>
|
||||||
<table class="ui very basic table">
|
<table class="ui very basic table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<human-date :date="object.creation_date"></human-date>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<translate translate-context="Content/*/Table.Label">Last checked</translate>
|
<translate translate-context="Content/*/Table.Label">Last checked</translate>
|
||||||
|
@ -155,6 +147,14 @@
|
||||||
</div>
|
</div>
|
||||||
<table v-else class="ui very basic table">
|
<table v-else class="ui very basic table">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<translate translate-context="Content/Moderation/Table.Label/Short (Value is a date)">First seen</translate>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<human-date :date="object.creation_date"></human-date>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<router-link
|
<router-link
|
||||||
|
@ -231,7 +231,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('domain', object.name) }}">
|
||||||
<translate translate-context="*/*/*/Noun">Libraries</translate>
|
<translate translate-context="*/*/*/Noun">Libraries</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.libraries }}
|
{{ stats.libraries }}
|
||||||
|
@ -239,7 +241,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('domain', object.name) }}">
|
||||||
<translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
|
<translate translate-context="Content/Moderation/Table.Label/Noun">Uploads</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.uploads }}
|
{{ stats.uploads }}
|
||||||
|
@ -247,7 +251,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.artists', query: {q: getQuery('domain', object.name) }}">
|
||||||
<translate translate-context="*/*/*/Noun">Artists</translate>
|
<translate translate-context="*/*/*/Noun">Artists</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.artists }}
|
{{ stats.artists }}
|
||||||
|
@ -255,7 +261,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.albums', query: {q: getQuery('domain', object.name) }}">
|
||||||
<translate translate-context="*/*/*">Albums</translate>
|
<translate translate-context="*/*/*">Albums</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.albums}}
|
{{ stats.albums}}
|
||||||
|
@ -263,7 +271,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('domain', object.name) }}">
|
||||||
<translate translate-context="*/*/*/Noun">Tracks</translate>
|
<translate translate-context="*/*/*/Noun">Tracks</translate>
|
||||||
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ stats.tracks }}
|
{{ stats.tracks }}
|
||||||
|
@ -350,6 +360,9 @@ export default {
|
||||||
updatePolicy (policy) {
|
updatePolicy (policy) {
|
||||||
this.policy = policy
|
this.policy = policy
|
||||||
this.showPolicyForm = false
|
this.showPolicyForm = false
|
||||||
|
},
|
||||||
|
getQuery (field, value) {
|
||||||
|
return `${field}:"${value}"`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -35,88 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<modal :show.sync="showUploadDetailModal">
|
<import-status-modal :upload="detailedUpload" :show.sync="showUploadDetailModal" />
|
||||||
<div class="header">
|
|
||||||
<translate translate-context="Popup/Import/Title">Import detail</translate>
|
|
||||||
</div>
|
|
||||||
<div class="content" v-if="detailedUpload">
|
|
||||||
<div class="description">
|
|
||||||
<div class="ui message" v-if="detailedUpload.import_status === 'pending'">
|
|
||||||
<translate translate-context="Popup/Import/Message">Upload is still pending and will soon be processed by the server.</translate>
|
|
||||||
</div>
|
|
||||||
<div class="ui success message" v-if="detailedUpload.import_status === 'finished'">
|
|
||||||
<translate translate-context="Popup/Import/Message">Upload was successfully processed by the server.</translate>
|
|
||||||
</div>
|
|
||||||
<div class="ui warning message" v-if="detailedUpload.import_status === 'skipped'">
|
|
||||||
<translate translate-context="Popup/Import/Message">Upload was skipped because a similar one is already available in one of your libraries.</translate>
|
|
||||||
</div>
|
|
||||||
<div class="ui error message" v-if="detailedUpload.import_status === 'errored'">
|
|
||||||
<translate translate-context="Popup/Import/Message">An error occured during upload processing. You will find more information below.</translate>
|
|
||||||
</div>
|
|
||||||
<template v-if="detailedUpload.import_status === 'errored'">
|
|
||||||
<table class="ui very basic collapsing celled table">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<translate translate-context="Popup/Import/Table.Label/Noun">Error type</translate>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ getErrorData(detailedUpload).label }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<translate translate-context="Popup/Import/Table.Label/Noun">Error detail</translate>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ getErrorData(detailedUpload).detail }}
|
|
||||||
<ul v-if="getErrorData(detailedUpload).errorRows.length > 0">
|
|
||||||
<li v-for="row in getErrorData(detailedUpload).errorRows">
|
|
||||||
{{ row.key}}: {{ row.value}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<translate translate-context="Popup/Import/Table.Label/Noun">Getting help</translate>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a :href="getErrorData(detailedUpload).documentationUrl" target="_blank">
|
|
||||||
<translate translate-context="Popup/Import/Table.Label/Value">Read our documentation for this error</translate>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a :href="getErrorData(detailedUpload).supportUrl" target="_blank">
|
|
||||||
<translate translate-context="Popup/Import/Table.Label/Value">Open a support thread (include the debug information below in your message)</translate>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<translate translate-context="Popup/Import/Table.Label/Noun">Debug information</translate>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="ui form">
|
|
||||||
<textarea class="ui textarea" rows="10" :value="getErrorData(detailedUpload).debugInfo"></textarea>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<div class="ui deny button">
|
|
||||||
<translate translate-context="*/*/Button.Label/Verb">Close</translate>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
<div class="dimmable">
|
<div class="dimmable">
|
||||||
<div v-if="isLoading" class="ui active inverted dimmer">
|
<div v-if="isLoading" class="ui active inverted dimmer">
|
||||||
<div class="ui loader"></div>
|
<div class="ui loader"></div>
|
||||||
|
@ -163,10 +82,10 @@
|
||||||
<human-date :date="scope.obj.creation_date"></human-date>
|
<human-date :date="scope.obj.creation_date"></human-date>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="discrete link" @click="addSearchToken('status', scope.obj.import_status)" :title="labels.importStatuses[scope.obj.import_status].help">
|
<span class="discrete link" @click="addSearchToken('status', scope.obj.import_status)" :title="sharedLabels.fields.import_status.choices[scope.obj.import_status].help">
|
||||||
{{ labels.importStatuses[scope.obj.import_status].label }}
|
{{ sharedLabels.fields.import_status.choices[scope.obj.import_status].label }}
|
||||||
</span>
|
</span>
|
||||||
<button class="ui tiny basic icon button" :title="labels.statusDetailTitle" @click="detailedUpload = scope.obj; showUploadDetailModal = true">
|
<button class="ui tiny basic icon button" :title="sharedLabels.fields.import_status.detailTitle" @click="detailedUpload = scope.obj; showUploadDetailModal = true">
|
||||||
<i class="question circle outline icon"></i>
|
<i class="question circle outline icon"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
@ -216,33 +135,8 @@ import ActionTable from '@/components/common/ActionTable'
|
||||||
import OrderingMixin from '@/components/mixins/Ordering'
|
import OrderingMixin from '@/components/mixins/Ordering'
|
||||||
import TranslationsMixin from '@/components/mixins/Translations'
|
import TranslationsMixin from '@/components/mixins/Translations'
|
||||||
import SmartSearchMixin from '@/components/mixins/SmartSearch'
|
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 {
|
export default {
|
||||||
mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
|
mixins: [OrderingMixin, TranslationsMixin, SmartSearchMixin],
|
||||||
props: {
|
props: {
|
||||||
|
@ -253,7 +147,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
Pagination,
|
Pagination,
|
||||||
ActionTable,
|
ActionTable,
|
||||||
Modal
|
ImportStatusModal
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -307,58 +201,11 @@ export default {
|
||||||
selectPage: function (page) {
|
selectPage: function (page) {
|
||||||
this.page = 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: {
|
computed: {
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
searchPlaceholder: this.$pgettext('Content/Library/Input.Placeholder', 'Search by title, artist, album…'),
|
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 () {
|
actionFilters () {
|
||||||
|
|
Loading…
Reference in New Issue