diff --git a/back/src/Kyoo.Core/Views/Resources/SearchApi.cs b/back/src/Kyoo.Core/Views/Resources/SearchApi.cs
index d4d4a982..59c4a081 100644
--- a/back/src/Kyoo.Core/Views/Resources/SearchApi.cs
+++ b/back/src/Kyoo.Core/Views/Resources/SearchApi.cs
@@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see .
-using System.Collections.Generic;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
@@ -33,7 +32,7 @@ namespace Kyoo.Core.Api
/// An endpoint to search for every resources of kyoo. Searching for only a specific type of resource
/// is available on the said endpoint.
///
- [Route("search/{query?}")]
+ [Route("search")]
[ApiController]
[ResourceView]
[ApiDefinition("Search", Group = ResourcesGroup)]
@@ -54,7 +53,7 @@ namespace Kyoo.Core.Api
///
/// Search for collections
///
- /// The query to search for.
+ /// The query to search for.
/// Sort information about the query (sort by, sort order).
/// How many items per page should be returned, where should the page start...
/// The aditional fields to include in the result.
@@ -64,12 +63,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Collection), Kind.Read)]
[ApiDefinition("Collections")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> SearchCollections(string? query,
+ public async Task> SearchCollections(
+ [FromQuery] string? q,
[FromQuery] Sort sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include fields)
{
- return SearchPage(await _searchManager.SearchCollections(query, sortBy, pagination, fields));
+ return SearchPage(await _searchManager.SearchCollections(q, sortBy, pagination, fields));
}
///
@@ -78,7 +78,7 @@ namespace Kyoo.Core.Api
///
/// Search for shows
///
- /// The query to search for.
+ /// The query to search for.
/// Sort information about the query (sort by, sort order).
/// How many items per page should be returned, where should the page start...
/// The aditional fields to include in the result.
@@ -88,12 +88,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Show), Kind.Read)]
[ApiDefinition("Show")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> SearchShows(string? query,
+ public async Task> SearchShows(
+ [FromQuery] string? q,
[FromQuery] Sort sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include fields)
{
- return SearchPage(await _searchManager.SearchShows(query, sortBy, pagination, fields));
+ return SearchPage(await _searchManager.SearchShows(q, sortBy, pagination, fields));
}
///
@@ -102,7 +103,7 @@ namespace Kyoo.Core.Api
///
/// Search for movie
///
- /// The query to search for.
+ /// The query to search for.
/// Sort information about the query (sort by, sort order).
/// How many items per page should be returned, where should the page start...
/// The aditional fields to include in the result.
@@ -112,12 +113,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Movie), Kind.Read)]
[ApiDefinition("Movie")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> SearchMovies(string? query,
+ public async Task> SearchMovies(
+ [FromQuery] string? q,
[FromQuery] Sort sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include fields)
{
- return SearchPage(await _searchManager.SearchMovies(query, sortBy, pagination, fields));
+ return SearchPage(await _searchManager.SearchMovies(q, sortBy, pagination, fields));
}
///
@@ -126,7 +128,7 @@ namespace Kyoo.Core.Api
///
/// Search for items
///
- /// The query to search for.
+ /// The query to search for.
/// Sort information about the query (sort by, sort order).
/// How many items per page should be returned, where should the page start...
/// The aditional fields to include in the result.
@@ -136,12 +138,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(LibraryItem), Kind.Read)]
[ApiDefinition("Item")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> SearchItems(string? query,
+ public async Task> SearchItems(
+ [FromQuery] string? q,
[FromQuery] Sort sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include fields)
{
- return SearchPage(await _searchManager.SearchItems(query, sortBy, pagination, fields));
+ return SearchPage(await _searchManager.SearchItems(q, sortBy, pagination, fields));
}
///
@@ -150,7 +153,7 @@ namespace Kyoo.Core.Api
///
/// Search for episodes
///
- /// The query to search for.
+ /// The query to search for.
/// Sort information about the query (sort by, sort order).
/// How many items per page should be returned, where should the page start...
/// The aditional fields to include in the result.
@@ -160,12 +163,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Episode), Kind.Read)]
[ApiDefinition("Episodes")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> SearchEpisodes(string? query,
+ public async Task> SearchEpisodes(
+ [FromQuery] string? q,
[FromQuery] Sort sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include fields)
{
- return SearchPage(await _searchManager.SearchEpisodes(query, sortBy, pagination, fields));
+ return SearchPage(await _searchManager.SearchEpisodes(q, sortBy, pagination, fields));
}
///
@@ -174,7 +178,7 @@ namespace Kyoo.Core.Api
///
/// Search for studios
///
- /// The query to search for.
+ /// The query to search for.
/// Sort information about the query (sort by, sort order).
/// How many items per page should be returned, where should the page start...
/// The aditional fields to include in the result.
@@ -184,12 +188,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Studio), Kind.Read)]
[ApiDefinition("Studios")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> SearchStudios(string? query,
+ public async Task> SearchStudios(
+ [FromQuery] string? q,
[FromQuery] Sort sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include fields)
{
- return SearchPage(await _searchManager.SearchStudios(query, sortBy, pagination, fields));
+ return SearchPage(await _searchManager.SearchStudios(q, sortBy, pagination, fields));
}
}
}
diff --git a/back/src/Kyoo.Meilisearch/MeilisearchModule.cs b/back/src/Kyoo.Meilisearch/MeilisearchModule.cs
index 5dedf474..642d9617 100644
--- a/back/src/Kyoo.Meilisearch/MeilisearchModule.cs
+++ b/back/src/Kyoo.Meilisearch/MeilisearchModule.cs
@@ -33,6 +33,93 @@ namespace Kyoo.Meiliseach
private readonly IConfiguration _configuration;
+ public static Dictionary IndexSettings => new()
+ {
+ {
+ "items",
+ new Settings()
+ {
+ SearchableAttributes = new[]
+ {
+ CamelCase.ConvertName(nameof(LibraryItem.Name)),
+ CamelCase.ConvertName(nameof(LibraryItem.Slug)),
+ CamelCase.ConvertName(nameof(LibraryItem.Aliases)),
+ CamelCase.ConvertName(nameof(LibraryItem.Path)),
+ CamelCase.ConvertName(nameof(LibraryItem.Tags)),
+ CamelCase.ConvertName(nameof(LibraryItem.Overview)),
+ },
+ FilterableAttributes = new[]
+ {
+ CamelCase.ConvertName(nameof(LibraryItem.Genres)),
+ CamelCase.ConvertName(nameof(LibraryItem.Status)),
+ CamelCase.ConvertName(nameof(LibraryItem.AirDate)),
+ CamelCase.ConvertName(nameof(Movie.StudioID)),
+ CamelCase.ConvertName(nameof(LibraryItem.Kind)),
+ },
+ SortableAttributes = new[]
+ {
+ CamelCase.ConvertName(nameof(LibraryItem.AirDate)),
+ CamelCase.ConvertName(nameof(LibraryItem.AddedDate)),
+ },
+ DisplayedAttributes = new[]
+ {
+ CamelCase.ConvertName(nameof(LibraryItem.Id)),
+ CamelCase.ConvertName(nameof(LibraryItem.Kind)),
+ },
+ // TODO: Add stopwords
+ // TODO: Extend default ranking to add ratings.
+ }
+ },
+ {
+ nameof(Episode),
+ new Settings()
+ {
+ SearchableAttributes = new[]
+ {
+ CamelCase.ConvertName(nameof(Episode.Name)),
+ CamelCase.ConvertName(nameof(Episode.Overview)),
+ CamelCase.ConvertName(nameof(Episode.Slug)),
+ CamelCase.ConvertName(nameof(Episode.Path)),
+ },
+ FilterableAttributes = new[]
+ {
+ CamelCase.ConvertName(nameof(Episode.SeasonNumber)),
+ },
+ SortableAttributes = new[]
+ {
+ CamelCase.ConvertName(nameof(Episode.ReleaseDate)),
+ CamelCase.ConvertName(nameof(Episode.AddedDate)),
+ CamelCase.ConvertName(nameof(Episode.SeasonNumber)),
+ CamelCase.ConvertName(nameof(Episode.EpisodeNumber)),
+ CamelCase.ConvertName(nameof(Episode.AbsoluteNumber)),
+ },
+ DisplayedAttributes = new[]
+ {
+ CamelCase.ConvertName(nameof(Episode.Id)),
+ },
+ // TODO: Add stopwords
+ }
+ },
+ {
+ nameof(Studio),
+ new Settings()
+ {
+ SearchableAttributes = new[]
+ {
+ CamelCase.ConvertName(nameof(Studio.Name)),
+ CamelCase.ConvertName(nameof(Studio.Slug)),
+ },
+ FilterableAttributes = Array.Empty(),
+ SortableAttributes = Array.Empty(),
+ DisplayedAttributes = new[]
+ {
+ CamelCase.ConvertName(nameof(Studio.Id)),
+ },
+ // TODO: Add stopwords
+ }
+ },
+ };
+
public MeilisearchModule(IConfiguration configuration)
{
_configuration = configuration;
@@ -47,89 +134,16 @@ namespace Kyoo.Meiliseach
{
MeilisearchClient client = provider.GetRequiredService();
- await _CreateIndex(client, "items", true, new Settings()
- {
- SearchableAttributes = new[]
- {
- CamelCase.ConvertName(nameof(LibraryItem.Name)),
- CamelCase.ConvertName(nameof(LibraryItem.Slug)),
- CamelCase.ConvertName(nameof(LibraryItem.Aliases)),
- CamelCase.ConvertName(nameof(LibraryItem.Path)),
- CamelCase.ConvertName(nameof(LibraryItem.Tags)),
- // Overview could be included as well but I think it would be better without.
- },
- FilterableAttributes = new[]
- {
- CamelCase.ConvertName(nameof(LibraryItem.Genres)),
- CamelCase.ConvertName(nameof(LibraryItem.Status)),
- CamelCase.ConvertName(nameof(LibraryItem.AirDate)),
- CamelCase.ConvertName(nameof(Movie.StudioID)),
- CamelCase.ConvertName(nameof(LibraryItem.Kind)),
- },
- SortableAttributes = new[]
- {
- CamelCase.ConvertName(nameof(LibraryItem.AirDate)),
- CamelCase.ConvertName(nameof(LibraryItem.AddedDate)),
- },
- DisplayedAttributes = new[]
- {
- CamelCase.ConvertName(nameof(LibraryItem.Id)),
- CamelCase.ConvertName(nameof(LibraryItem.Kind)),
- },
- // TODO: Add stopwords
- // TODO: Extend default ranking to add ratings.
- });
-
- await _CreateIndex(client, nameof(Episode), false, new Settings()
- {
- SearchableAttributes = new[]
- {
- CamelCase.ConvertName(nameof(Episode.Name)),
- CamelCase.ConvertName(nameof(Episode.Overview)),
- CamelCase.ConvertName(nameof(Episode.Slug)),
- CamelCase.ConvertName(nameof(Episode.Path)),
- },
- FilterableAttributes = new[]
- {
- CamelCase.ConvertName(nameof(Episode.SeasonNumber)),
- },
- SortableAttributes = new[]
- {
- CamelCase.ConvertName(nameof(Episode.ReleaseDate)),
- CamelCase.ConvertName(nameof(Episode.AddedDate)),
- CamelCase.ConvertName(nameof(Episode.SeasonNumber)),
- CamelCase.ConvertName(nameof(Episode.EpisodeNumber)),
- CamelCase.ConvertName(nameof(Episode.AbsoluteNumber)),
- },
- DisplayedAttributes = new[]
- {
- CamelCase.ConvertName(nameof(Episode.Id)),
- },
- // TODO: Add stopwords
- });
-
- await _CreateIndex(client, nameof(Studio), false, new Settings()
- {
- SearchableAttributes = new[]
- {
- CamelCase.ConvertName(nameof(Studio.Name)),
- CamelCase.ConvertName(nameof(Studio.Slug)),
- },
- FilterableAttributes = Array.Empty(),
- SortableAttributes = Array.Empty(),
- DisplayedAttributes = new[]
- {
- CamelCase.ConvertName(nameof(Studio.Id)),
- },
- // TODO: Add stopwords
- });
+ await _CreateIndex(client, "items", true);
+ await _CreateIndex(client, nameof(Episode), false);
+ await _CreateIndex(client, nameof(Studio), false);
}
- private static async Task _CreateIndex(MeilisearchClient client, string index, bool hasKind, Settings opts)
+ private static async Task _CreateIndex(MeilisearchClient client, string index, bool hasKind)
{
TaskInfo task = await client.CreateIndexAsync(index, hasKind ? "ref" : CamelCase.ConvertName(nameof(IResource.Id)));
await client.WaitForTaskAsync(task.TaskUid);
- await client.Index(index).UpdateSettingsAsync(opts);
+ await client.Index(index).UpdateSettingsAsync(IndexSettings[index]);
}
///
diff --git a/back/src/Kyoo.Meilisearch/SearchManager.cs b/back/src/Kyoo.Meilisearch/SearchManager.cs
index 0e2ddbe5..22c18d09 100644
--- a/back/src/Kyoo.Meilisearch/SearchManager.cs
+++ b/back/src/Kyoo.Meilisearch/SearchManager.cs
@@ -32,13 +32,15 @@ public class SearchManager : ISearchManager
private readonly MeilisearchClient _client;
private readonly ILibraryManager _libraryManager;
- private static IEnumerable _GetSortsBy(Sort? sort)
+ private static IEnumerable _GetSortsBy(string index, Sort? sort)
{
return sort switch
{
Sort.Default => Array.Empty(),
- Sort.By @sortBy => new[] { $"{sortBy.Key}:{(sortBy.Desendant ? "desc" : "asc")}" },
- Sort.Conglomerate(var list) => list.SelectMany(_GetSortsBy),
+ Sort.By @sortBy => MeilisearchModule.IndexSettings[index].SortableAttributes.Contains(sortBy.Key, StringComparer.InvariantCultureIgnoreCase)
+ ? new[] { $"{CamelCase.ConvertName(sortBy.Key)}:{(sortBy.Desendant ? "desc" : "asc")}" }
+ : throw new ValidationException($"Invalid sorting mode: {sortBy.Key}"),
+ Sort.Conglomerate(var list) => list.SelectMany(x => _GetSortsBy(index, x)),
Sort.Random => throw new ValidationException("Random sorting is not supported while searching."),
_ => Array.Empty(),
};
@@ -106,7 +108,7 @@ public class SearchManager : ISearchManager
ISearchable res = await _client.Index(index).SearchAsync(query, new SearchQuery()
{
Filter = where,
- Sort = _GetSortsBy(sortBy),
+ Sort = _GetSortsBy(index, sortBy),
Limit = pagination?.Limit ?? 50,
Offset = pagination?.Skip ?? 0,
});
@@ -126,7 +128,7 @@ public class SearchManager : ISearchManager
// TODO: add filters and facets
ISearchable res = await _client.Index("items").SearchAsync(query, new SearchQuery()
{
- Sort = _GetSortsBy(sortBy),
+ Sort = _GetSortsBy("items", sortBy),
Limit = pagination?.Limit ?? 50,
Offset = pagination?.Skip ?? 0,
});
diff --git a/front/apps/web/src/pages/browse/[slug].tsx b/front/apps/web/src/pages/browse/[slug].tsx
deleted file mode 100644
index e756ef4e..00000000
--- a/front/apps/web/src/pages/browse/[slug].tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Kyoo - A portable and vast media library solution.
- * Copyright (c) Kyoo.
- *
- * See AUTHORS.md and LICENSE file in the project root for full license information.
- *
- * Kyoo is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * any later version.
- *
- * Kyoo is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Kyoo. If not, see .
- */
-
-import { BrowsePage } from "@kyoo/ui";
-import { withRoute } from "~/router";
-
-export default withRoute(BrowsePage);
diff --git a/front/packages/ui/src/browse/header.tsx b/front/packages/ui/src/browse/header.tsx
index 2f2dbd44..aeb6ccaa 100644
--- a/front/packages/ui/src/browse/header.tsx
+++ b/front/packages/ui/src/browse/header.tsx
@@ -37,11 +37,11 @@ import ViewList from "@material-symbols/svg-400/rounded/view_list.svg";
import Sort from "@material-symbols/svg-400/rounded/sort.svg";
import ArrowUpward from "@material-symbols/svg-400/rounded/arrow_upward.svg";
import ArrowDownward from "@material-symbols/svg-400/rounded/arrow_downward.svg";
-import { Layout, SortBy, SortOrd } from "./types";
+import { Layout, SearchSort, SortOrd } from "./types";
import { forwardRef } from "react";
import { View, PressableProps } from "react-native";
-const SortTrigger = forwardRef(function SortTrigger(
+const SortTrigger = forwardRef(function SortTrigger(
{ sortKey, ...props },
ref,
) {
@@ -61,15 +61,17 @@ const SortTrigger = forwardRef(funct
});
export const BrowseSettings = ({
+ availableSorts,
sortKey,
sortOrd,
setSort,
layout,
setLayout,
}: {
- sortKey: SortBy;
+ availableSorts: string[];
+ sortKey: string;
sortOrd: SortOrd;
- setSort: (sort: SortBy, ord: SortOrd) => void;
+ setSort: (sort: string, ord: SortOrd) => void;
layout: Layout;
setLayout: (layout: Layout) => void;
}) => {
@@ -97,12 +99,18 @@ export const BrowseSettings = ({
)}