diff --git a/back/src/Kyoo.Abstractions/Controllers/ISearchManager.cs b/back/src/Kyoo.Abstractions/Controllers/ISearchManager.cs index 68c23b2a..695b44f9 100644 --- a/back/src/Kyoo.Abstractions/Controllers/ISearchManager.cs +++ b/back/src/Kyoo.Abstractions/Controllers/ISearchManager.cs @@ -78,4 +78,30 @@ public interface ISearchManager Sort sortBy, SearchPagination pagination, Include? include = default); + + /// + /// Search for episodes. + /// + /// The seach query. + /// Sort information about the query (sort by, sort order) + /// How pagination should be done (where to start and how many to return) + /// The related fields to include. + /// A list of resources that match every filters + public Task.SearchResult> SearchEpisodes(string? query, + Sort sortBy, + SearchPagination pagination, + Include? include = default); + + /// + /// Search for studios. + /// + /// The seach query. + /// Sort information about the query (sort by, sort order) + /// How pagination should be done (where to start and how many to return) + /// The related fields to include. + /// A list of resources that match every filters + public Task.SearchResult> SearchStudios(string? query, + Sort sortBy, + SearchPagination pagination, + Include? include = default); } diff --git a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs index d88e88a6..a334e9ba 100644 --- a/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs +++ b/back/src/Kyoo.Core/Controllers/ThumbnailsManager.cs @@ -75,6 +75,12 @@ namespace Kyoo.Core.Controllers response.EnsureSuccessStatusCode(); await using Stream reader = await response.Content.ReadAsStreamAsync(); using SKCodec codec = SKCodec.Create(reader); + if (codec == null) + { + _logger.LogError("Unsupported codec for {What}", what); + return; + } + SKImageInfo info = codec.Info; info.ColorType = SKColorType.Rgba8888; using SKBitmap original = SKBitmap.Decode(codec, info); diff --git a/back/src/Kyoo.Core/Views/Resources/SearchApi.cs b/back/src/Kyoo.Core/Views/Resources/SearchApi.cs index 4c30c8f1..d4d4a982 100644 --- a/back/src/Kyoo.Core/Views/Resources/SearchApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/SearchApi.cs @@ -39,7 +39,6 @@ namespace Kyoo.Core.Api [ApiDefinition("Search", Group = ResourcesGroup)] public class SearchApi : BaseApi { - private readonly ILibraryManager _libraryManager; private readonly ISearchManager _searchManager; public SearchApi(ISearchManager searchManager) @@ -56,6 +55,8 @@ namespace Kyoo.Core.Api /// Search for collections /// /// 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. /// A list of collections found for the specified query. [HttpGet("collections")] @@ -63,9 +64,12 @@ namespace Kyoo.Core.Api [Permission(nameof(Collection), Kind.Read)] [ApiDefinition("Collections")] [ProducesResponseType(StatusCodes.Status200OK)] - public Task> SearchCollections(string query, [FromQuery] Include fields) + public async Task> SearchCollections(string? query, + [FromQuery] Sort sortBy, + [FromQuery] SearchPagination pagination, + [FromQuery] Include fields) { - return _libraryManager.Collections.Search(query); + return SearchPage(await _searchManager.SearchCollections(query, sortBy, pagination, fields)); } /// @@ -75,16 +79,21 @@ namespace Kyoo.Core.Api /// Search for shows /// /// 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. /// A list of shows found for the specified query. [HttpGet("shows")] [HttpGet("show", Order = AlternativeRoute)] [Permission(nameof(Show), Kind.Read)] - [ApiDefinition("Shows")] + [ApiDefinition("Show")] [ProducesResponseType(StatusCodes.Status200OK)] - public Task> SearchShows(string query, [FromQuery] Include fields) + public async Task> SearchShows(string? query, + [FromQuery] Sort sortBy, + [FromQuery] SearchPagination pagination, + [FromQuery] Include fields) { - return _libraryManager.Shows.Search(query); + return SearchPage(await _searchManager.SearchShows(query, sortBy, pagination, fields)); } /// @@ -118,16 +127,21 @@ namespace Kyoo.Core.Api /// Search for items /// /// 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. /// A list of items found for the specified query. [HttpGet("items")] [HttpGet("item", Order = AlternativeRoute)] - [Permission(nameof(Show), Kind.Read)] - [ApiDefinition("Items")] + [Permission(nameof(LibraryItem), Kind.Read)] + [ApiDefinition("Item")] [ProducesResponseType(StatusCodes.Status200OK)] - public Task> SearchItems(string query, [FromQuery] Include fields) + public async Task> SearchItems(string? query, + [FromQuery] Sort sortBy, + [FromQuery] SearchPagination pagination, + [FromQuery] Include fields) { - return _libraryManager.LibraryItems.Search(query); + return SearchPage(await _searchManager.SearchItems(query, sortBy, pagination, fields)); } /// @@ -137,6 +151,8 @@ namespace Kyoo.Core.Api /// Search for episodes /// /// 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. /// A list of episodes found for the specified query. [HttpGet("episodes")] @@ -144,29 +160,12 @@ namespace Kyoo.Core.Api [Permission(nameof(Episode), Kind.Read)] [ApiDefinition("Episodes")] [ProducesResponseType(StatusCodes.Status200OK)] - public Task> SearchEpisodes(string query, [FromQuery] Include fields) + public async Task> SearchEpisodes(string? query, + [FromQuery] Sort sortBy, + [FromQuery] SearchPagination pagination, + [FromQuery] Include fields) { - return _libraryManager.Episodes.Search(query); - } - - /// - /// Search staff - /// - /// - /// Search for staff - /// - /// The query to search for. - /// The aditional fields to include in the result. - /// A list of staff members found for the specified query. - [HttpGet("staff")] - [HttpGet("person", Order = AlternativeRoute)] - [HttpGet("people", Order = AlternativeRoute)] - [Permission(nameof(People), Kind.Read)] - [ApiDefinition("Staff")] - [ProducesResponseType(StatusCodes.Status200OK)] - public Task> SearchPeople(string query, [FromQuery] Include fields) - { - return _libraryManager.People.Search(query); + return SearchPage(await _searchManager.SearchEpisodes(query, sortBy, pagination, fields)); } /// @@ -176,6 +175,8 @@ namespace Kyoo.Core.Api /// Search for studios /// /// 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. /// A list of studios found for the specified query. [HttpGet("studios")] @@ -183,9 +184,12 @@ namespace Kyoo.Core.Api [Permission(nameof(Studio), Kind.Read)] [ApiDefinition("Studios")] [ProducesResponseType(StatusCodes.Status200OK)] - public Task> SearchStudios(string query, [FromQuery] Include fields) + public async Task> SearchStudios(string? query, + [FromQuery] Sort sortBy, + [FromQuery] SearchPagination pagination, + [FromQuery] Include fields) { - return _libraryManager.Studios.Search(query); + return SearchPage(await _searchManager.SearchStudios(query, sortBy, pagination, fields)); } } } diff --git a/back/src/Kyoo.Meilisearch/MeilisearchModule.cs b/back/src/Kyoo.Meilisearch/MeilisearchModule.cs index d32a7f5c..5dedf474 100644 --- a/back/src/Kyoo.Meilisearch/MeilisearchModule.cs +++ b/back/src/Kyoo.Meilisearch/MeilisearchModule.cs @@ -22,6 +22,7 @@ using Kyoo.Abstractions.Models; using Meilisearch; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using static System.Text.Json.JsonNamingPolicy; namespace Kyoo.Meiliseach { @@ -50,39 +51,83 @@ namespace Kyoo.Meiliseach { SearchableAttributes = new[] { - nameof(LibraryItem.Name), - nameof(LibraryItem.Slug), - nameof(LibraryItem.Aliases), - nameof(LibraryItem.Path), - nameof(LibraryItem.Tags), + 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[] { - nameof(LibraryItem.Genres), - nameof(LibraryItem.Status), - nameof(LibraryItem.AirDate), - nameof(Movie.StudioID), - nameof(LibraryItem.Kind), + 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[] { - nameof(LibraryItem.AirDate), - nameof(LibraryItem.AddedDate), + CamelCase.ConvertName(nameof(LibraryItem.AirDate)), + CamelCase.ConvertName(nameof(LibraryItem.AddedDate)), }, DisplayedAttributes = new[] { - nameof(LibraryItem.Id), - nameof(LibraryItem.Kind), + 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 + }); } private static async Task _CreateIndex(MeilisearchClient client, string index, bool hasKind, Settings opts) { - TaskInfo task = await client.CreateIndexAsync(index, hasKind ? "Ref" : nameof(IResource.Id)); + TaskInfo task = await client.CreateIndexAsync(index, hasKind ? "ref" : CamelCase.ConvertName(nameof(IResource.Id))); await client.WaitForTaskAsync(task.TaskUid); await client.Index(index).UpdateSettingsAsync(opts); } diff --git a/back/src/Kyoo.Meilisearch/SearchManager.cs b/back/src/Kyoo.Meilisearch/SearchManager.cs index a3072c7c..0e2ddbe5 100644 --- a/back/src/Kyoo.Meilisearch/SearchManager.cs +++ b/back/src/Kyoo.Meilisearch/SearchManager.cs @@ -23,6 +23,7 @@ using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Utils; using Meilisearch; +using static System.Text.Json.JsonNamingPolicy; namespace Kyoo.Meiliseach; @@ -57,9 +58,17 @@ public class SearchManager : ISearchManager IRepository.OnCreated += (x) => _CreateOrUpdate("items", x, nameof(Collection)); IRepository.OnEdited += (x) => _CreateOrUpdate("items", x, nameof(Collection)); IRepository.OnDeleted += (x) => _Delete("items", x.Id, nameof(Collection)); + + IRepository.OnCreated += (x) => _CreateOrUpdate(nameof(Episode), x); + IRepository.OnEdited += (x) => _CreateOrUpdate(nameof(Episode), x); + IRepository.OnDeleted += (x) => _Delete(nameof(Episode), x.Id); + + IRepository.OnCreated += (x) => _CreateOrUpdate(nameof(Studio), x); + IRepository.OnEdited += (x) => _CreateOrUpdate(nameof(Studio), x); + IRepository.OnDeleted += (x) => _Delete(nameof(Studio), x.Id); } - private Task _CreateOrUpdate(string index, IResource item, string? kind = null) + private async Task _CreateOrUpdate(string index, IResource item, string? kind = null) { if (kind != null) { @@ -67,12 +76,14 @@ public class SearchManager : ISearchManager var dictionary = (IDictionary)expando; foreach (PropertyInfo property in item.GetType().GetProperties()) - dictionary.Add(property.Name, property.GetValue(item)); - expando.Ref = $"{kind}/{item.Id}"; - expando.Kind = kind; - return _client.Index(index).AddDocumentsAsync(new[] { item }); + dictionary.Add(CamelCase.ConvertName(property.Name), property.GetValue(item)); + dictionary.Add("ref", $"{kind}-{item.Id}"); + expando.kind = kind; + var task = await _client.Index(index).AddDocumentsAsync(new[] { expando }); + var ret = await _client.WaitForTaskAsync(task.TaskUid); + Console.WriteLine(ret.Error); } - return _client.Index(index).AddDocumentsAsync(new[] { item }); + await _client.Index(index).AddDocumentsAsync(new[] { item }); } private Task _Delete(string index, int id, string? kind = null) @@ -144,7 +155,7 @@ public class SearchManager : ISearchManager SearchPagination pagination, Include? include = default) { - return _Search("items", query, $"Kind = {nameof(Movie)}", sortBy, pagination, include); + return _Search("items", query, $"kind = {nameof(Movie)}", sortBy, pagination, include); } /// @@ -153,7 +164,7 @@ public class SearchManager : ISearchManager SearchPagination pagination, Include? include = default) { - return _Search("items", query, $"Kind = {nameof(Show)}", sortBy, pagination, include); + return _Search("items", query, $"kind = {nameof(Show)}", sortBy, pagination, include); } /// @@ -162,7 +173,25 @@ public class SearchManager : ISearchManager SearchPagination pagination, Include? include = default) { - return _Search("items", query, $"Kind = {nameof(Collection)}", sortBy, pagination, include); + return _Search("items", query, $"kind = {nameof(Collection)}", sortBy, pagination, include); + } + + /// + public Task.SearchResult> SearchEpisodes(string? query, + Sort sortBy, + SearchPagination pagination, + Include? include = default) + { + return _Search(nameof(Episode), query, null, sortBy, pagination, include); + } + + /// + public Task.SearchResult> SearchStudios(string? query, + Sort sortBy, + SearchPagination pagination, + Include? include = default) + { + return _Search(nameof(Studio), query, null, sortBy, pagination, include); } private class IdResource diff --git a/scanner/scanner/scanner.py b/scanner/scanner/scanner.py index d803c003..060093a8 100644 --- a/scanner/scanner/scanner.py +++ b/scanner/scanner/scanner.py @@ -40,7 +40,7 @@ class Scanner: if len(deleted) != len(self.registered): for x in deleted: await self.delete(x) - else: + elif len(deleted) > 0: logging.warning("All video files are unavailable. Check your disks.") # We batch videos by 20 because too mutch at once kinda DDOS everything.