From f6faed0691d5b0c299b15b9b36fe91b675765ed0 Mon Sep 17 00:00:00 2001 From: jon r Date: Sat, 3 May 2025 11:32:24 +0000 Subject: [PATCH] Fix frontend federation search --- api/funkwhale_api/common/schema.yml | 18 ++- .../federation/api_serializers.py | 92 +++++++++---- api/tests/federation/test_api_serializers.py | 32 +++-- api/tests/federation/test_api_views.py | 6 +- changes/changelog.d/2910.feature | 1 + front/src/generated/types.ts | 4 +- front/src/locales/bn.json | 2 +- front/src/locales/ca.json | 42 +++--- front/src/locales/ca@valencia.json | 2 +- front/src/locales/de.json | 2 +- front/src/locales/en_US.json | 2 +- front/src/locales/es.json | 42 +++--- front/src/locales/eu.json | 42 +++--- front/src/locales/nl.json | 52 +++---- front/src/locales/tr.json | 42 +++--- front/src/locales/zh_Hans.json | 42 +++--- front/src/ui/modals/Search.vue | 130 +++++++++++++----- 17 files changed, 338 insertions(+), 215 deletions(-) create mode 100644 changes/changelog.d/2910.feature diff --git a/api/funkwhale_api/common/schema.yml b/api/funkwhale_api/common/schema.yml index b2ec199cf..81bee930d 100644 --- a/api/funkwhale_api/common/schema.yml +++ b/api/funkwhale_api/common/schema.yml @@ -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 diff --git a/api/funkwhale_api/federation/api_serializers.py b/api/funkwhale_api/federation/api_serializers.py index 2c1d5ae49..c3e103af7 100644 --- a/api/funkwhale_api/federation/api_serializers.py +++ b/api/funkwhale_api/federation/api_serializers.py @@ -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() diff --git a/api/tests/federation/test_api_serializers.py b/api/tests/federation/test_api_serializers.py index f60799508..25339b345 100644 --- a/api/tests/federation/test_api_serializers.py +++ b/api/tests/federation/test_api_serializers.py @@ -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, } diff --git a/api/tests/federation/test_api_views.py b/api/tests/federation/test_api_views.py index 9e9b8fb10..b996db837 100644 --- a/api/tests/federation/test_api_views.py +++ b/api/tests/federation/test_api_views.py @@ -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") diff --git a/changes/changelog.d/2910.feature b/changes/changelog.d/2910.feature new file mode 100644 index 000000000..0a7f4db34 --- /dev/null +++ b/changes/changelog.d/2910.feature @@ -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) diff --git a/front/src/generated/types.ts b/front/src/generated/types.ts index 71186acda..359eac503 100644 --- a/front/src/generated/types.ts +++ b/front/src/generated/types.ts @@ -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; }; diff --git a/front/src/locales/bn.json b/front/src/locales/bn.json index 6871ba0af..34d235aa4 100644 --- a/front/src/locales/bn.json +++ b/front/src/locales/bn.json @@ -4,7 +4,7 @@ }, "vui": { "tracks": "{n}টি গান | {n}টি গান", - "by-user": "{'@'}{username} থেকে", + "by-user": "{username} থেকে", "aria": { "pagination": { "gotoPage": "{n} পৃষ্ঠায় যাও", diff --git a/front/src/locales/ca.json b/front/src/locales/ca.json index 16624603d..407f89812 100644 --- a/front/src/locales/ca.json +++ b/front/src/locales/ca.json @@ -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" } } diff --git a/front/src/locales/ca@valencia.json b/front/src/locales/ca@valencia.json index a3713c35c..7d1522607 100644 --- a/front/src/locales/ca@valencia.json +++ b/front/src/locales/ca@valencia.json @@ -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", diff --git a/front/src/locales/de.json b/front/src/locales/de.json index 8c6c9963b..69f77213b 100644 --- a/front/src/locales/de.json +++ b/front/src/locales/de.json @@ -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": { diff --git a/front/src/locales/en_US.json b/front/src/locales/en_US.json index e564c3548..5907f43c5 100644 --- a/front/src/locales/en_US.json +++ b/front/src/locales/en_US.json @@ -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", diff --git a/front/src/locales/es.json b/front/src/locales/es.json index 125e0fccb..552e2ed3a 100644 --- a/front/src/locales/es.json +++ b/front/src/locales/es.json @@ -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" } } diff --git a/front/src/locales/eu.json b/front/src/locales/eu.json index 8d0c4e8eb..604b7b020 100644 --- a/front/src/locales/eu.json +++ b/front/src/locales/eu.json @@ -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" } } diff --git a/front/src/locales/nl.json b/front/src/locales/nl.json index 6edf74e0e..e055f7774 100644 --- a/front/src/locales/nl.json +++ b/front/src/locales/nl.json @@ -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" } } diff --git a/front/src/locales/tr.json b/front/src/locales/tr.json index 8deb96638..390fb7038 100644 --- a/front/src/locales/tr.json +++ b/front/src/locales/tr.json @@ -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" } } diff --git a/front/src/locales/zh_Hans.json b/front/src/locales/zh_Hans.json index d4f150d26..77df70f83 100644 --- a/front/src/locales/zh_Hans.json +++ b/front/src/locales/zh_Hans.json @@ -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} 歌曲" } } diff --git a/front/src/ui/modals/Search.vue b/front/src/ui/modals/Search.vue index e0683d5df..1fbc4d95e 100644 --- a/front/src/ui/modals/Search.vue +++ b/front/src/ui/modals/Search.vue @@ -1,5 +1,5 @@