Fix frontend federation search
This commit is contained in:
parent
40cb7aadb3
commit
f6faed0691
|
@ -20659,18 +20659,32 @@ components:
|
|||
format: date-time
|
||||
readOnly: true
|
||||
nullable: true
|
||||
type:
|
||||
type: string
|
||||
readOnly: true
|
||||
object:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Artist'
|
||||
- $ref: '#/components/schemas/Album'
|
||||
- $ref: '#/components/schemas/Track'
|
||||
- $ref: '#/components/schemas/APIActor'
|
||||
- $ref: '#/components/schemas/Channel'
|
||||
- $ref: '#/components/schemas/Playlist'
|
||||
readOnly: true
|
||||
required:
|
||||
- actor
|
||||
- creation_date
|
||||
- detail
|
||||
- fetch_date
|
||||
- id
|
||||
- object
|
||||
- status
|
||||
- type
|
||||
- url
|
||||
FetchRequest:
|
||||
type: object
|
||||
properties:
|
||||
object:
|
||||
object_uri:
|
||||
type: string
|
||||
writeOnly: true
|
||||
minLength: 1
|
||||
|
@ -20679,7 +20693,7 @@ components:
|
|||
writeOnly: true
|
||||
default: false
|
||||
required:
|
||||
- object
|
||||
- object_uri
|
||||
FetchStatusEnum:
|
||||
enum:
|
||||
- pending
|
||||
|
|
|
@ -10,9 +10,10 @@ from drf_spectacular.utils import extend_schema_field
|
|||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.audio import models as audio_models
|
||||
from funkwhale_api.common import fields as common_fields
|
||||
from funkwhale_api.audio import serializers as audio_serializers
|
||||
from funkwhale_api.common import serializers as common_serializers
|
||||
from funkwhale_api.music import models as music_models
|
||||
from funkwhale_api.playlists import models as playlists_models
|
||||
from funkwhale_api.users import serializers as users_serializers
|
||||
|
||||
from . import filters, models
|
||||
|
@ -192,19 +193,17 @@ class InboxItemActionSerializer(common_serializers.ActionSerializer):
|
|||
return objects.update(is_read=True)
|
||||
|
||||
|
||||
FETCH_OBJECT_CONFIG = {
|
||||
"artist": {"queryset": music_models.Artist.objects.all()},
|
||||
"album": {"queryset": music_models.Album.objects.all()},
|
||||
"track": {"queryset": music_models.Track.objects.all()},
|
||||
"library": {"queryset": music_models.Library.objects.all(), "id_attr": "uuid"},
|
||||
"upload": {"queryset": music_models.Upload.objects.all(), "id_attr": "uuid"},
|
||||
"account": {"queryset": models.Actor.objects.all(), "id_attr": "full_username"},
|
||||
"channel": {"queryset": audio_models.Channel.objects.all(), "id_attr": "uuid"},
|
||||
OBJECT_SERIALIZER_MAPPING = {
|
||||
music_models.Artist: federation_serializers.ArtistSerializer,
|
||||
music_models.Album: federation_serializers.AlbumSerializer,
|
||||
music_models.Track: federation_serializers.TrackSerializer,
|
||||
models.Actor: federation_serializers.APIActorSerializer,
|
||||
audio_models.Channel: audio_serializers.ChannelSerializer,
|
||||
playlists_models.Playlist: federation_serializers.PlaylistSerializer,
|
||||
}
|
||||
FETCH_OBJECT_FIELD = common_fields.GenericRelation(FETCH_OBJECT_CONFIG)
|
||||
|
||||
|
||||
def convert_url_to_webginfer(url):
|
||||
def convert_url_to_webfinger(url):
|
||||
parsed_url = urlparse(url)
|
||||
domain = parsed_url.netloc # e.g., "node1.funkwhale.test"
|
||||
path_parts = parsed_url.path.strip("/").split("/")
|
||||
|
@ -217,7 +216,9 @@ def convert_url_to_webginfer(url):
|
|||
|
||||
class FetchSerializer(serializers.ModelSerializer):
|
||||
actor = federation_serializers.APIActorSerializer(read_only=True)
|
||||
object = serializers.CharField(write_only=True)
|
||||
object_uri = serializers.CharField(required=True, write_only=True)
|
||||
object = serializers.SerializerMethodField(read_only=True)
|
||||
type = serializers.SerializerMethodField(read_only=True)
|
||||
force = serializers.BooleanField(default=False, required=False, write_only=True)
|
||||
|
||||
class Meta:
|
||||
|
@ -230,8 +231,10 @@ class FetchSerializer(serializers.ModelSerializer):
|
|||
"detail",
|
||||
"creation_date",
|
||||
"fetch_date",
|
||||
"object",
|
||||
"object_uri",
|
||||
"force",
|
||||
"type",
|
||||
"object",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
|
@ -241,14 +244,36 @@ class FetchSerializer(serializers.ModelSerializer):
|
|||
"detail",
|
||||
"creation_date",
|
||||
"fetch_date",
|
||||
"type",
|
||||
"object",
|
||||
]
|
||||
|
||||
def validate_object(self, value):
|
||||
def get_type(self, fetch):
|
||||
obj = fetch.object
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
# Return the type as a string
|
||||
if isinstance(obj, music_models.Artist):
|
||||
return "artist"
|
||||
elif isinstance(obj, music_models.Album):
|
||||
return "album"
|
||||
elif isinstance(obj, music_models.Track):
|
||||
return "track"
|
||||
elif isinstance(obj, models.Actor):
|
||||
return "account"
|
||||
elif isinstance(obj, audio_models.Channel):
|
||||
return "channel"
|
||||
elif isinstance(obj, playlists_models.Playlist):
|
||||
return "playlist"
|
||||
else:
|
||||
return None
|
||||
|
||||
def validate_object_uri(self, value):
|
||||
if value.startswith("https://"):
|
||||
converted = convert_url_to_webginfer(value)
|
||||
converted = convert_url_to_webfinger(value)
|
||||
if converted:
|
||||
value = converted
|
||||
# if value is a webginfer lookup, we craft a special url
|
||||
if value.startswith("@"):
|
||||
value = value.lstrip("@")
|
||||
validator = validators.EmailValidator()
|
||||
|
@ -256,9 +281,30 @@ class FetchSerializer(serializers.ModelSerializer):
|
|||
validator(value)
|
||||
except validators.ValidationError:
|
||||
return value
|
||||
|
||||
return f"webfinger://{value}"
|
||||
|
||||
@extend_schema_field(
|
||||
{
|
||||
"oneOf": [
|
||||
{"$ref": "#/components/schemas/Artist"},
|
||||
{"$ref": "#/components/schemas/Album"},
|
||||
{"$ref": "#/components/schemas/Track"},
|
||||
{"$ref": "#/components/schemas/APIActor"},
|
||||
{"$ref": "#/components/schemas/Channel"},
|
||||
{"$ref": "#/components/schemas/Playlist"},
|
||||
]
|
||||
}
|
||||
)
|
||||
def get_object(self, fetch):
|
||||
obj = fetch.object
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
serializer_class = OBJECT_SERIALIZER_MAPPING.get(type(obj))
|
||||
if serializer_class:
|
||||
return serializer_class(obj).data
|
||||
return None
|
||||
|
||||
def create(self, validated_data):
|
||||
check_duplicates = not validated_data.get("force", False)
|
||||
if check_duplicates:
|
||||
|
@ -267,7 +313,7 @@ class FetchSerializer(serializers.ModelSerializer):
|
|||
validated_data["actor"]
|
||||
.fetches.filter(
|
||||
status="finished",
|
||||
url=validated_data["object"],
|
||||
url=validated_data["object_uri"],
|
||||
creation_date__gte=timezone.now()
|
||||
- datetime.timedelta(
|
||||
seconds=settings.FEDERATION_DUPLICATE_FETCH_DELAY
|
||||
|
@ -280,18 +326,10 @@ class FetchSerializer(serializers.ModelSerializer):
|
|||
return duplicate
|
||||
|
||||
fetch = models.Fetch.objects.create(
|
||||
actor=validated_data["actor"], url=validated_data["object"]
|
||||
actor=validated_data["actor"], url=validated_data["object_uri"]
|
||||
)
|
||||
return fetch
|
||||
|
||||
def to_representation(self, obj):
|
||||
repr = super().to_representation(obj)
|
||||
object_data = None
|
||||
if obj.object:
|
||||
object_data = FETCH_OBJECT_FIELD.to_representation(obj.object)
|
||||
repr["object"] = object_data
|
||||
return repr
|
||||
|
||||
|
||||
class FullActorSerializer(serializers.Serializer):
|
||||
fid = serializers.URLField()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import pytest
|
||||
|
||||
from funkwhale_api.audio.serializers import ChannelSerializer
|
||||
from funkwhale_api.common import serializers as common_serializers
|
||||
from funkwhale_api.federation import api_serializers, serializers
|
||||
from funkwhale_api.users import serializers as users_serializers
|
||||
|
@ -128,6 +129,7 @@ def test_fetch_serializer_no_obj(factories, to_api_date):
|
|||
"status": fetch.status,
|
||||
"detail": fetch.detail,
|
||||
"object": None,
|
||||
"type": None,
|
||||
"actor": serializers.APIActorSerializer(fetch.actor).data,
|
||||
}
|
||||
|
||||
|
@ -135,22 +137,28 @@ def test_fetch_serializer_no_obj(factories, to_api_date):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"object_factory, expected_type, expected_id",
|
||||
"object_factory, expected_type, serializer_class",
|
||||
[
|
||||
("music.Album", "album", "id"),
|
||||
("music.Artist", "artist", "id"),
|
||||
("music.Track", "track", "id"),
|
||||
("music.Library", "library", "uuid"),
|
||||
("music.Upload", "upload", "uuid"),
|
||||
("audio.Channel", "channel", "uuid"),
|
||||
("federation.Actor", "account", "full_username"),
|
||||
("music.Album", "album", serializers.AlbumSerializer),
|
||||
("music.Artist", "artist", serializers.ArtistSerializer),
|
||||
("music.Track", "track", serializers.TrackSerializer),
|
||||
("audio.Channel", "channel", ChannelSerializer),
|
||||
("federation.Actor", "account", serializers.APIActorSerializer),
|
||||
("playlists.Playlist", "playlist", serializers.PlaylistSerializer),
|
||||
],
|
||||
)
|
||||
def test_fetch_serializer_with_object(
|
||||
object_factory, expected_type, expected_id, factories, to_api_date
|
||||
object_factory, expected_type, serializer_class, factories, to_api_date
|
||||
):
|
||||
obj = factories[object_factory]()
|
||||
fetch = factories["federation.Fetch"](object=obj)
|
||||
|
||||
# Serialize the object
|
||||
if serializer_class:
|
||||
object_data = serializer_class(obj).data
|
||||
else:
|
||||
object_data = {"uuid": getattr(obj, "uuid", None)}
|
||||
|
||||
expected = {
|
||||
"id": fetch.pk,
|
||||
"url": fetch.url,
|
||||
|
@ -158,7 +166,10 @@ def test_fetch_serializer_with_object(
|
|||
"fetch_date": None,
|
||||
"status": fetch.status,
|
||||
"detail": fetch.detail,
|
||||
"object": {"type": expected_type, expected_id: getattr(obj, expected_id)},
|
||||
"object": {
|
||||
**object_data,
|
||||
},
|
||||
"type": expected_type,
|
||||
"actor": serializers.APIActorSerializer(fetch.actor).data,
|
||||
}
|
||||
|
||||
|
@ -175,6 +186,7 @@ def test_fetch_serializer_unhandled_obj(factories, to_api_date):
|
|||
"status": fetch.status,
|
||||
"detail": fetch.detail,
|
||||
"object": None,
|
||||
"type": None,
|
||||
"actor": serializers.APIActorSerializer(fetch.actor).data,
|
||||
}
|
||||
|
||||
|
|
|
@ -243,7 +243,7 @@ def test_can_fetch_using_url_synchronous(
|
|||
fetch_task = mocker.patch.object(tasks, "fetch", side_effect=fake_task)
|
||||
|
||||
url = reverse("api:v1:federation:fetches-list")
|
||||
data = {"object": object_id}
|
||||
data = {"object_uri": object_id}
|
||||
response = logged_in_api_client.post(url, data)
|
||||
assert response.status_code == 201
|
||||
|
||||
|
@ -266,7 +266,7 @@ def test_fetch_duplicate(factories, logged_in_api_client, settings, now):
|
|||
creation_date=now - datetime.timedelta(seconds=59),
|
||||
)
|
||||
url = reverse("api:v1:federation:fetches-list")
|
||||
data = {"object": object_id}
|
||||
data = {"object_uri": object_id}
|
||||
response = logged_in_api_client.post(url, data)
|
||||
assert response.status_code == 201
|
||||
assert response.data == api_serializers.FetchSerializer(duplicate).data
|
||||
|
@ -286,7 +286,7 @@ def test_fetch_duplicate_bypass_with_force(
|
|||
creation_date=now - datetime.timedelta(seconds=59),
|
||||
)
|
||||
url = reverse("api:v1:federation:fetches-list")
|
||||
data = {"object": object_id, "force": True}
|
||||
data = {"object_uri": object_id, "force": True}
|
||||
response = logged_in_api_client.post(url, data)
|
||||
|
||||
fetch = actor.fetches.latest("id")
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Entering an input in the global search in the sidebar opens up a modal that show all possible results at once with collapseable sections for each category, including federated searches for other users, channels and rss feeds. (#2910)
|
|
@ -6638,9 +6638,11 @@ export interface components {
|
|||
readonly creation_date: string;
|
||||
/** Format: date-time */
|
||||
readonly fetch_date: string | null;
|
||||
readonly type: string;
|
||||
readonly object: components["schemas"]["Artist"] | components["schemas"]["Album"] | components["schemas"]["Track"] | components["schemas"]["APIActor"] | components["schemas"]["Channel"] | components["schemas"]["Playlist"];
|
||||
};
|
||||
FetchRequest: {
|
||||
object: string;
|
||||
object_uri: string;
|
||||
/** @default false */
|
||||
force: boolean;
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
},
|
||||
"vui": {
|
||||
"tracks": "{n}টি গান | {n}টি গান",
|
||||
"by-user": "{'@'}{username} থেকে",
|
||||
"by-user": "{username} থেকে",
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"gotoPage": "{n} পৃষ্ঠায় যাও",
|
||||
|
|
|
@ -2,6 +2,27 @@
|
|||
"App": {
|
||||
"loading": "Carregant…"
|
||||
},
|
||||
"vui": {
|
||||
"radio": "Ràdio",
|
||||
"albums": "{n} àlbum | {n} àlbums",
|
||||
"tracks": "{n} pista | {n} pistes",
|
||||
"episodes": "{n} episodi | {n} episodis",
|
||||
"by-user": "per {username}",
|
||||
"go-to": "Anar a",
|
||||
"pagination": {
|
||||
"previous": "Prèvia",
|
||||
"next": "Següent"
|
||||
},
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"nav": "Navegació per Paginació",
|
||||
"gotoPage": "Anar a Pàgina {n}",
|
||||
"gotoPrevious": "Anar a la Pàgina Prèvia",
|
||||
"gotoNext": "Anar a la Pàgina Següent",
|
||||
"currentPage": "Pàgina actual, Pàgina {n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"About": {
|
||||
"description": {
|
||||
|
@ -4629,26 +4650,5 @@
|
|||
"title": "Ràdio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vui": {
|
||||
"albums": "{n} àlbum | {n} àlbums",
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"currentPage": "Pàgina actual, Pàgina {n}",
|
||||
"gotoNext": "Anar a la Pàgina Següent",
|
||||
"gotoPage": "Anar a Pàgina {n}",
|
||||
"gotoPrevious": "Anar a la Pàgina Prèvia",
|
||||
"nav": "Navegació per Paginació"
|
||||
}
|
||||
},
|
||||
"by-user": "per {'@'}{username}",
|
||||
"episodes": "{n} episodi | {n} episodis",
|
||||
"go-to": "Anar a",
|
||||
"pagination": {
|
||||
"next": "Següent",
|
||||
"previous": "Prèvia"
|
||||
},
|
||||
"radio": "Ràdio",
|
||||
"tracks": "{n} pista | {n} pistes"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"radio": "Ràdio",
|
||||
"albums": "{n} àlbum | {n} àlbums",
|
||||
"episodes": "{n} episodi | {n} episodis",
|
||||
"by-user": "per {'@'}{username}",
|
||||
"by-user": "per {username}",
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"gotoPrevious": "Retornar a la pàgina anterior",
|
||||
|
|
|
@ -4623,7 +4623,7 @@
|
|||
"nav": "Nummerierte Navigation"
|
||||
}
|
||||
},
|
||||
"by-user": "von {'@'}{username}",
|
||||
"by-user": "von {username}",
|
||||
"episodes": "{n} Episode | {n} Episoden",
|
||||
"go-to": "Gehe zu",
|
||||
"pagination": {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"albums": "{n} album | {n} albums",
|
||||
"tracks": "{n} track | {n} tracks",
|
||||
"episodes": "{n} episode | {n} episodes",
|
||||
"by-user": "by {'@'}{username}",
|
||||
"by-user": "by {username}",
|
||||
"go-to": "Go to",
|
||||
"pagination": {
|
||||
"previous": "Previous",
|
||||
|
|
|
@ -2,6 +2,27 @@
|
|||
"App": {
|
||||
"loading": "Cargando…"
|
||||
},
|
||||
"vui": {
|
||||
"radio": "Radio",
|
||||
"albums": "{n} álbum | {n} álbumes",
|
||||
"tracks": "{n} pista | {n} pistas",
|
||||
"episodes": "{n} episodio | {n} episodios",
|
||||
"by-user": "de {username}",
|
||||
"go-to": "Vaya a",
|
||||
"pagination": {
|
||||
"previous": "Anterior",
|
||||
"next": "Siguiente"
|
||||
},
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"nav": "Paginación Navegación",
|
||||
"currentPage": "Página corriente, Página {n}",
|
||||
"gotoPage": "Ir a la página {n}",
|
||||
"gotoPrevious": "Ir a la página anterior",
|
||||
"gotoNext": "Ir a la página siguiente"
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"About": {
|
||||
"description": {
|
||||
|
@ -4422,26 +4443,5 @@
|
|||
"title": "Radio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vui": {
|
||||
"albums": "{n} álbum | {n} álbumes",
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"currentPage": "Página corriente, Página {n}",
|
||||
"gotoNext": "Ir a la página siguiente",
|
||||
"gotoPage": "Ir a la página {n}",
|
||||
"gotoPrevious": "Ir a la página anterior",
|
||||
"nav": "Paginación Navegación"
|
||||
}
|
||||
},
|
||||
"by-user": "de {'@'}{username}",
|
||||
"episodes": "{n} episodio | {n} episodios",
|
||||
"go-to": "Vaya a",
|
||||
"pagination": {
|
||||
"next": "Siguiente",
|
||||
"previous": "Anterior"
|
||||
},
|
||||
"radio": "Radio",
|
||||
"tracks": "{n} pista | {n} pistas"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,27 @@
|
|||
"App": {
|
||||
"loading": "Kargatzen…"
|
||||
},
|
||||
"vui": {
|
||||
"tracks": "Pista {n} | {n} pista",
|
||||
"episodes": "Atal {n} | {n} atal",
|
||||
"by-user": "honen eskutik: {username}",
|
||||
"go-to": "Joan hona:",
|
||||
"pagination": {
|
||||
"previous": "Aurrekoa",
|
||||
"next": "Hurrengoa"
|
||||
},
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"nav": "Orrikatzearen nabigazioa",
|
||||
"gotoPage": "Joan orri honetara: {n}",
|
||||
"gotoPrevious": "Joan aurreko orrira",
|
||||
"gotoNext": "Joan hurrengo orrira",
|
||||
"currentPage": "Uneko orria: {n}"
|
||||
}
|
||||
},
|
||||
"albums": "Album {n} | {n} album",
|
||||
"radio": "Irratia"
|
||||
},
|
||||
"components": {
|
||||
"About": {
|
||||
"description": {
|
||||
|
@ -4611,26 +4632,5 @@
|
|||
"title": "Irratia"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vui": {
|
||||
"albums": "Album {n} | {n} album",
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"currentPage": "Uneko orria: {n}",
|
||||
"gotoNext": "Joan hurrengo orrira",
|
||||
"gotoPage": "Joan orri honetara: {n}",
|
||||
"gotoPrevious": "Joan aurreko orrira",
|
||||
"nav": "Orrikatzearen nabigazioa"
|
||||
}
|
||||
},
|
||||
"by-user": "honen eskutik: {'@'}{username}",
|
||||
"episodes": "Atal {n} | {n} atal",
|
||||
"go-to": "Joan hona:",
|
||||
"pagination": {
|
||||
"next": "Hurrengoa",
|
||||
"previous": "Aurrekoa"
|
||||
},
|
||||
"radio": "Irratia",
|
||||
"tracks": "Pista {n} | {n} pista"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,32 @@
|
|||
"App": {
|
||||
"loading": "Laden…"
|
||||
},
|
||||
"vui": {
|
||||
"radio": "Radio",
|
||||
"albums": "{n} album | {n} albums",
|
||||
"tracks": "{n} nummer | {n} nummers",
|
||||
"episodes": "{n} aflevering | {n} afleveringen",
|
||||
"by-user": "door {username}",
|
||||
"go-to": "Ga naar",
|
||||
"pagination": {
|
||||
"previous": "Vorige",
|
||||
"next": "Volgende"
|
||||
},
|
||||
"privacy-level": {
|
||||
"private": "Privé",
|
||||
"public": "Openbaar",
|
||||
"pod": "pod"
|
||||
},
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"nav": "Pagina navigatie",
|
||||
"gotoPage": "Ga naar pagina {n}",
|
||||
"gotoPrevious": "Ga naar Vorige Pagina",
|
||||
"gotoNext": "Ga naar Volgende Pagina",
|
||||
"currentPage": "Huidige pagina, pagina {n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"About": {
|
||||
"description": {
|
||||
|
@ -4611,31 +4637,5 @@
|
|||
"title": "Radio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vui": {
|
||||
"albums": "{n} album | {n} albums",
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"currentPage": "Huidige pagina, pagina {n}",
|
||||
"gotoNext": "Ga naar Volgende Pagina",
|
||||
"gotoPage": "Ga naar pagina {n}",
|
||||
"gotoPrevious": "Ga naar Vorige Pagina",
|
||||
"nav": "Pagina navigatie"
|
||||
}
|
||||
},
|
||||
"by-user": "door {'@'}{username}",
|
||||
"episodes": "{n} aflevering | {n} afleveringen",
|
||||
"go-to": "Ga naar",
|
||||
"pagination": {
|
||||
"next": "Volgende",
|
||||
"previous": "Vorige"
|
||||
},
|
||||
"privacy-level": {
|
||||
"pod": "pod",
|
||||
"private": "Privé",
|
||||
"public": "Openbaar"
|
||||
},
|
||||
"radio": "Radio",
|
||||
"tracks": "{n} nummer | {n} nummers"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,27 @@
|
|||
"App": {
|
||||
"loading": "Yükleniyor…"
|
||||
},
|
||||
"vui": {
|
||||
"radio": "Radyo",
|
||||
"albums": "{n} albüm | {n} albümler",
|
||||
"tracks": "{n} parça | {n} parçalar",
|
||||
"episodes": "{n} bölüm | {n} bölümler",
|
||||
"by-user": "by {username}",
|
||||
"go-to": "Git",
|
||||
"pagination": {
|
||||
"previous": "Önceki",
|
||||
"next": "Sonraki"
|
||||
},
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"nav": "Sayfalandırma Navigasyonu",
|
||||
"gotoPage": "Sayfaya Git {n}",
|
||||
"gotoPrevious": "Önceki Sayfaya Git",
|
||||
"gotoNext": "Sonraki Sayfaya Git",
|
||||
"currentPage": "Geçerli Sayfa, Sayfa {n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"About": {
|
||||
"description": {
|
||||
|
@ -2236,26 +2257,5 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vui": {
|
||||
"albums": "{n} albüm | {n} albümler",
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"currentPage": "Geçerli Sayfa, Sayfa {n}",
|
||||
"gotoNext": "Sonraki Sayfaya Git",
|
||||
"gotoPage": "Sayfaya Git {n}",
|
||||
"gotoPrevious": "Önceki Sayfaya Git",
|
||||
"nav": "Sayfalandırma Navigasyonu"
|
||||
}
|
||||
},
|
||||
"by-user": "by {'@'}{username}",
|
||||
"episodes": "{n} bölüm | {n} bölümler",
|
||||
"go-to": "Git",
|
||||
"pagination": {
|
||||
"next": "Sonraki",
|
||||
"previous": "Önceki"
|
||||
},
|
||||
"radio": "Radyo",
|
||||
"tracks": "{n} parça | {n} parçalar"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,27 @@
|
|||
"App": {
|
||||
"loading": "加载中…"
|
||||
},
|
||||
"vui": {
|
||||
"tracks": "{n} 歌曲 | {n} 歌曲",
|
||||
"episodes": "{n} 节目 | {n} 节目",
|
||||
"by-user": "由 {username}",
|
||||
"go-to": "去",
|
||||
"pagination": {
|
||||
"previous": "以前的",
|
||||
"next": "下一个"
|
||||
},
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"nav": "分页导航",
|
||||
"gotoPage": "转到页面 {n}",
|
||||
"gotoPrevious": "转到上一页",
|
||||
"gotoNext": "转到下一页",
|
||||
"currentPage": "当前页面,第 {n} 页"
|
||||
}
|
||||
},
|
||||
"radio": "电台",
|
||||
"albums": "{n} 专辑 | {n} 专辑"
|
||||
},
|
||||
"components": {
|
||||
"About": {
|
||||
"description": {
|
||||
|
@ -4517,26 +4538,5 @@
|
|||
"title": "电台"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vui": {
|
||||
"albums": "{n} 专辑 | {n} 专辑",
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"currentPage": "当前页面,第 {n} 页",
|
||||
"gotoNext": "转到下一页",
|
||||
"gotoPage": "转到页面 {n}",
|
||||
"gotoPrevious": "转到上一页",
|
||||
"nav": "分页导航"
|
||||
}
|
||||
},
|
||||
"by-user": "由 {'@'}{username}",
|
||||
"episodes": "{n} 节目 | {n} 节目",
|
||||
"go-to": "去",
|
||||
"pagination": {
|
||||
"next": "下一个",
|
||||
"previous": "以前的"
|
||||
},
|
||||
"radio": "电台",
|
||||
"tracks": "{n} 歌曲 | {n} 歌曲"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import type { paths } from '~/generated/types.ts'
|
||||
import type { paths, components } from '~/generated/types.ts'
|
||||
import type { RadioConfig } from '~/store/radios'
|
||||
import axios from 'axios'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
|
@ -9,9 +9,12 @@ import { trim, uniqBy } from 'lodash-es'
|
|||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useModal } from '~/ui/composables/useModal.ts'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import ArtistCard from '~/components/artist/Card.vue'
|
||||
import PlaylistCard from '~/components/playlists/Card.vue'
|
||||
import ChannelCard from '~/components/audio/ChannelCard.vue'
|
||||
import ActorLink from '~/components/common/ActorLink.vue'
|
||||
import TrackTable from '~/components/audio/track/Table.vue'
|
||||
import AlbumCard from '~/components/album/Card.vue'
|
||||
import RadioCard from '~/components/radios/Card.vue'
|
||||
|
@ -98,7 +101,8 @@ type Results = {
|
|||
podcasts: Response['podcasts']['results'],
|
||||
series: Response['series']['results'],
|
||||
rss: [Response['rss']],
|
||||
federation: [Response['federation']]
|
||||
federation: [Response['federation']],
|
||||
type: Category
|
||||
}
|
||||
|
||||
const responses = ref<Partial<Response>>({})
|
||||
|
@ -201,7 +205,7 @@ const categories = computed(() => [
|
|||
endpoint: '/federation/fetches/',
|
||||
post: true,
|
||||
params: {
|
||||
object: trimmedQuery.value
|
||||
object_uri: trimmedQuery.value
|
||||
}
|
||||
}
|
||||
] as const satisfies {
|
||||
|
@ -222,7 +226,8 @@ const availableCategories = computed(() =>
|
|||
isFetch.value ? type === 'federation'
|
||||
: isRss.value ? type === 'rss'
|
||||
: type !== 'federation' && type !== 'rss'
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
// Whenever available categories change, if there is exactly one, open it
|
||||
watch(availableCategories, () => {
|
||||
|
@ -278,6 +283,26 @@ watch(results, () => {
|
|||
openSections.value = new Set(categoriesWithResults.map(({ type }) => type))
|
||||
})
|
||||
|
||||
// Subscribe to an RSS feed
|
||||
|
||||
const store = useStore()
|
||||
|
||||
/**
|
||||
* Subscribe to an RSS feed and return the route for the subscribed channel
|
||||
* @param url The RSS feed URL
|
||||
* @returns The route object for the subscribed channel
|
||||
*/
|
||||
const rssSubscribe = async (url: string) => {
|
||||
try {
|
||||
const response = await axios.post('channels/rss-subscribe/', { url })
|
||||
store.commit('channels/subscriptions', { uuid: response.data.channel.uuid, value: true })
|
||||
return response.data.channel
|
||||
} catch (error) {
|
||||
useErrorHandler(error as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Search
|
||||
|
||||
const search = async () => {
|
||||
|
@ -321,18 +346,18 @@ const search = async () => {
|
|||
}
|
||||
responses.value[category.type] = response.data
|
||||
} else {
|
||||
// TODO: add (@)type key to Response type
|
||||
if (category.type === 'rss') {
|
||||
const response = await axios.post<Response['rss']>(
|
||||
category.endpoint,
|
||||
{ url: trimmedQuery.value }
|
||||
)
|
||||
results.value.rss = [response.data]
|
||||
responses.value[category.type] = response.data
|
||||
const channel = await rssSubscribe(trimmedQuery.value)
|
||||
if (channel) {
|
||||
results.value.rss = [channel] // Store the subscribed channel
|
||||
}
|
||||
} else if (category.type === 'federation') {
|
||||
const response = await axios.post<Response['federation']>(
|
||||
category.endpoint,
|
||||
{ params }
|
||||
{ object_uri: trimmedQuery.value }
|
||||
)
|
||||
results.value.type = category.type
|
||||
results.value.federation = [response.data]
|
||||
responses.value[category.type] = response.data
|
||||
} else if (category.type === 'playlists') {
|
||||
|
@ -340,6 +365,7 @@ const search = async () => {
|
|||
category.endpoint,
|
||||
{ params }
|
||||
)
|
||||
results.value.type = category.type
|
||||
results.value.playlists = response.data.results
|
||||
responses.value[category.type] = response.data
|
||||
} else if (category.type === 'podcasts') {
|
||||
|
@ -347,6 +373,7 @@ const search = async () => {
|
|||
category.endpoint,
|
||||
{ params }
|
||||
)
|
||||
results.value.type = category.type
|
||||
results.value.podcasts = response.data.results
|
||||
responses.value[category.type] = response.data
|
||||
} else if (category.type === 'radios') {
|
||||
|
@ -354,6 +381,7 @@ const search = async () => {
|
|||
category.endpoint,
|
||||
{ params }
|
||||
)
|
||||
results.value.type = category.type
|
||||
results.value.radios = response.data.results
|
||||
responses.value[category.type] = response.data
|
||||
} else if (category.type === 'series') {
|
||||
|
@ -361,6 +389,7 @@ const search = async () => {
|
|||
category.endpoint,
|
||||
{ params }
|
||||
)
|
||||
results.value.type = category.type
|
||||
results.value.series = response.data.results
|
||||
responses.value[category.type] = response.data
|
||||
}
|
||||
|
@ -489,23 +518,50 @@ watch(queryDebounced, search, { immediate: true })
|
|||
/>
|
||||
</template>
|
||||
|
||||
<!-- If response has "url": "webfinger://node1@node1.funkwhale.test" -> Link to go directly to the federation page -->
|
||||
|
||||
<span v-if="category.type === 'rss' && count(category) > 0">
|
||||
<Alert>{{ t('modals.search.tryAgain') }}</Alert>
|
||||
<Link
|
||||
v-for="channel in resultsPerCategory(category)"
|
||||
:key="channel.artist.fid"
|
||||
:to="channel.artist.fid"
|
||||
autofocus
|
||||
<template v-if="category.type === 'rss' && count(category) > 0">
|
||||
<Alert
|
||||
blue
|
||||
style="grid-column: 1 / -1"
|
||||
>
|
||||
{{ channel.artist.name }}
|
||||
</Link>
|
||||
</span>
|
||||
{{ t('modals.search.tryAgain') }}
|
||||
</Alert>
|
||||
<channel-card
|
||||
v-if="results.rss && results.rss[0]"
|
||||
:key="results.rss[0].uuid"
|
||||
:object="results.rss[0]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<span v-else-if="category.type === 'federation'">
|
||||
<!-- TODO: Federation search: backend adapter + display, fix results_per_category query -->
|
||||
<!-- {{ resultsPerCategory(category) }} -->
|
||||
<span v-else-if="category.type === 'federation' && count(category) > 0">
|
||||
<template
|
||||
v-for="result in resultsPerCategory(category)"
|
||||
:key="result.id"
|
||||
>
|
||||
<ActorLink
|
||||
v-if="result.object && result.type === 'account'"
|
||||
:actor="result.object as components['schemas']['APIActor']"
|
||||
/>
|
||||
<ChannelCard
|
||||
v-else-if="result.object && result.type === 'channel'"
|
||||
:object="result.object as components['schemas']['Channel']"
|
||||
/>
|
||||
<ArtistCard
|
||||
v-else-if="result.object && result.type === 'artist'"
|
||||
:artist="result.object as components['schemas']['Artist']"
|
||||
/>
|
||||
<AlbumCard
|
||||
v-else-if="result.object && result.type === 'album'"
|
||||
:album="result.object as components['schemas']['Album']"
|
||||
/>
|
||||
<PlaylistCard
|
||||
v-else-if="result.object && result.type === 'playlist'"
|
||||
:playlist="result.object as components['schemas']['Playlist']"
|
||||
/>
|
||||
<TrackTable
|
||||
v-else-if="result.object && result.type === 'track'"
|
||||
:tracks="[result.object] as components['schemas']['Track'][]"
|
||||
/>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
<EmptyState
|
||||
|
|
Loading…
Reference in New Issue