From 5a6da7441f726fdfd6198b795305aee7412a3243 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 22:51:57 +0200 Subject: [PATCH 01/10] Add missing show/season ids on episode/season refresh --- scanner/matcher/matcher.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/scanner/matcher/matcher.py b/scanner/matcher/matcher.py index 7b0c71e7..6d4db3f0 100644 --- a/scanner/matcher/matcher.py +++ b/scanner/matcher/matcher.py @@ -172,23 +172,36 @@ class Matcher: kind: Literal["collection", "movie", "episode", "show", "season"], kyoo_id: str, ): + async def id_season(season: dict, id: dict): + ret = await self._provider.identify_season( + id["dataId"], season["seasonNumber"] + ) + ret.show_id = season["showId"] + return ret + + async def id_episode(episode: dict, id: dict): + ret = await self._provider.identify_episode( + id["showId"], id["season"], id["episode"], episode["absoluteNumber"] + ) + ret.show_id = episode["showId"] + ret.season_id = episode["seasonId"] + ret.path = episode["path"] + return ret + identify_table = { "collection": lambda _, id: self._provider.identify_collection( id["dataId"] ), "movie": lambda _, id: self._provider.identify_movie(id["dataId"]), "show": lambda _, id: self._provider.identify_show(id["dataId"]), - "season": lambda season, id: self._provider.identify_season( - id["dataId"], season["seasonNumber"] - ), - "episode": lambda episode, id: self._provider.identify_episode( - id["showId"], id["season"], id["episode"], episode["absoluteNumber"] - ), + "season": id_season, + "episode": id_episode, } + current = await self._client.get(kind, kyoo_id) if self._provider.name not in current["externalId"]: logger.error( - f"Could not refresh metadata of {kind}/{kyoo_id}. Missisg provider id." + f"Could not refresh metadata of {kind}/{kyoo_id}. Missing provider id." ) return False provider_id = current["externalId"][self._provider.name] From 4688868f04e6d37a9b98490c2025f3e1a2c3a07e Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 22:58:19 +0200 Subject: [PATCH 02/10] Fix season and episode slugs on update --- .../Repositories/EpisodeRepository.cs | 11 +++-------- .../Repositories/SeasonRepository.cs | 19 +++++++------------ 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index d3162666..62e587ba 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -71,14 +71,6 @@ public class EpisodeRepository( .ToListAsync(); } - /// - public override async Task Create(Episode obj) - { - // Set it for the OnResourceCreated event and the return value. - obj.ShowSlug = obj.Show?.Slug ?? (await shows.Get(obj.ShowId)).Slug; - return await base.Create(obj); - } - /// protected override async Task Validate(Episode resource) { @@ -86,6 +78,9 @@ public class EpisodeRepository( resource.Show = null; if (resource.ShowId == Guid.Empty) throw new ValidationException("Missing show id"); + // This is storred in db so it needs to be set before every create/edit (and before events) + resource.ShowSlug = (await shows.Get(resource.ShowId)).Slug; + resource.Season = null; if (resource.SeasonId == null && resource.SeasonNumber != null) { diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs index 590d0b10..aa314440 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs @@ -31,8 +31,11 @@ using Microsoft.Extensions.DependencyInjection; namespace Kyoo.Core.Controllers; -public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumbnails) - : GenericRepository(database) +public class SeasonRepository( + DatabaseContext database, + IRepository shows, + IThumbnailsManager thumbnails +) : GenericRepository(database) { static SeasonRepository() { @@ -66,16 +69,6 @@ public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumb .ToListAsync(); } - /// - public override async Task Create(Season obj) - { - // Set it for the OnResourceCreated event and the return value. - obj.ShowSlug = - (await Database.Shows.FirstOrDefaultAsync(x => x.Id == obj.ShowId))?.Slug - ?? throw new ItemNotFoundException($"No show found with ID {obj.ShowId}"); - return await base.Create(obj); - } - /// protected override async Task Validate(Season resource) { @@ -83,6 +76,8 @@ public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumb resource.Show = null; if (resource.ShowId == Guid.Empty) throw new ValidationException("Missing show id"); + // This is storred in db so it needs to be set before every create/edit (and before events) + resource.ShowSlug = (await shows.Get(resource.ShowId)).Slug; await thumbnails.DownloadImages(resource); } } From 274d2987d2e2d461f08cb75949233f1a93e4b165 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 21 Apr 2024 22:58:36 +0200 Subject: [PATCH 03/10] Prevent AddedDate edits --- .../Repositories/GenericRepository.cs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs index dbe199f2..36019ffd 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/GenericRepository.cs @@ -297,6 +297,8 @@ public abstract class GenericRepository(DatabaseContext database) : IReposito { await Validate(edited); Database.Update(edited); + if (edited is IAddedDate date) + Database.Entry(date).Property(p => p.AddedDate).IsModified = false; await Database.SaveChangesAsync(); await IRepository.OnResourceEdited(edited); return edited; @@ -305,23 +307,15 @@ public abstract class GenericRepository(DatabaseContext database) : IReposito /// public virtual async Task Patch(Guid id, Func patch) { - bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled; - Database.ChangeTracker.LazyLoadingEnabled = false; - try - { - T resource = await GetWithTracking(id); + T resource = await GetWithTracking(id); - resource = patch(resource); + resource = patch(resource); + if (resource is IAddedDate date) + Database.Entry(date).Property(p => p.AddedDate).IsModified = false; - await Database.SaveChangesAsync(); - await IRepository.OnResourceEdited(resource); - return resource; - } - finally - { - Database.ChangeTracker.LazyLoadingEnabled = lazyLoading; - Database.ChangeTracker.Clear(); - } + await Database.SaveChangesAsync(); + await IRepository.OnResourceEdited(resource); + return resource; } /// From 5c6bcee763caa6e4f5b89dfd9dc44af31da1c98c Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 22 Apr 2024 14:47:53 +0200 Subject: [PATCH 04/10] Auto calculate next refresh dates for episodes --- .../Controllers/Repositories/EpisodeRepository.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index 62e587ba..2561ad89 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -91,6 +91,16 @@ public class EpisodeRepository( .Select(x => x.Id) .FirstOrDefaultAsync(); } + + // Refresh metadata every day if the item aired this week, refresh every two mounts otherwise + if ( + resource.ReleaseDate is not DateOnly date + || (date.DayNumber - DateOnly.FromDateTime(DateTime.UtcNow).DayNumber) < 7 + ) + resource.NextMetadataRefresh = DateTime.UtcNow.AddDays(1); + else + resource.NextMetadataRefresh = DateTime.UtcNow.AddMonths(2); + await thumbnails.DownloadImages(resource); } From 526ac1b8ab2838f6b28b1c3be984527bb1d7a6c8 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 22 Apr 2024 16:29:36 +0200 Subject: [PATCH 05/10] Compute next refresh date automatically for all resources --- .../Models/Resources/Interfaces/IRefreshable.cs | 14 ++++++++++++++ .../Repositories/CollectionRepository.cs | 1 + .../Controllers/Repositories/EpisodeRepository.cs | 10 +--------- .../Controllers/Repositories/MovieRepository.cs | 1 + .../Controllers/Repositories/SeasonRepository.cs | 2 +- .../Controllers/Repositories/ShowRepository.cs | 1 + 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IRefreshable.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IRefreshable.cs index cef2b663..ad7acc73 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IRefreshable.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IRefreshable.cs @@ -26,4 +26,18 @@ public interface IRefreshable /// The date of the next metadata refresh. Null if auto-refresh is disabled. /// public DateTime? NextMetadataRefresh { get; set; } + + public static DateTime ComputeNextRefreshDate(DateOnly? airDate) + { + if (airDate is null) + return DateTime.UtcNow.AddDays(1); + + int days = airDate.Value.DayNumber - DateOnly.FromDateTime(DateTime.UtcNow).DayNumber; + return days switch + { + <= 7 => DateTime.UtcNow.AddDays(1), + <= 21 => DateTime.UtcNow.AddDays(5), + _ => DateTime.UtcNow.AddMonths(2) + }; + } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs index d9f8499b..0a81a29b 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/CollectionRepository.cs @@ -53,6 +53,7 @@ public class CollectionRepository(DatabaseContext database, IThumbnailsManager t if (string.IsNullOrEmpty(resource.Name)) throw new ArgumentException("The collection's name must be set and not empty"); + resource.NextMetadataRefresh ??= DateTime.UtcNow.AddMonths(2); await thumbnails.DownloadImages(resource); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs index 2561ad89..e1f2f0bd 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/EpisodeRepository.cs @@ -92,15 +92,7 @@ public class EpisodeRepository( .FirstOrDefaultAsync(); } - // Refresh metadata every day if the item aired this week, refresh every two mounts otherwise - if ( - resource.ReleaseDate is not DateOnly date - || (date.DayNumber - DateOnly.FromDateTime(DateTime.UtcNow).DayNumber) < 7 - ) - resource.NextMetadataRefresh = DateTime.UtcNow.AddDays(1); - else - resource.NextMetadataRefresh = DateTime.UtcNow.AddMonths(2); - + resource.NextMetadataRefresh ??= IRefreshable.ComputeNextRefreshDate(resource.ReleaseDate); await thumbnails.DownloadImages(resource); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs index c04a72cb..2dec59c8 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/MovieRepository.cs @@ -74,6 +74,7 @@ public class MovieRepository( resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id; resource.Studio = null; } + resource.NextMetadataRefresh ??= IRefreshable.ComputeNextRefreshDate(resource.AirDate); await thumbnails.DownloadImages(resource); } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs index aa314440..61297f4a 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/SeasonRepository.cs @@ -23,7 +23,6 @@ using System.Linq; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Postgresql; using Microsoft.EntityFrameworkCore; @@ -78,6 +77,7 @@ public class SeasonRepository( throw new ValidationException("Missing show id"); // This is storred in db so it needs to be set before every create/edit (and before events) resource.ShowSlug = (await shows.Get(resource.ShowId)).Slug; + resource.NextMetadataRefresh ??= IRefreshable.ComputeNextRefreshDate(resource.StartDate); await thumbnails.DownloadImages(resource); } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs index 17ee8251..13caad26 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/ShowRepository.cs @@ -74,6 +74,7 @@ public class ShowRepository( resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id; resource.Studio = null; } + resource.NextMetadataRefresh ??= IRefreshable.ComputeNextRefreshDate(resource.StartAir); await thumbnails.DownloadImages(resource); } } From 2273e990741df1730b3e93173559c97ee7600f1f Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 22 Apr 2024 17:45:51 +0200 Subject: [PATCH 06/10] Add refresh route --- .../Kyoo.Abstractions/Controllers/IScanner.cs | 27 +++++++ .../Kyoo.Core/Views/Resources/EpisodeApi.cs | 23 ++++++ back/src/Kyoo.RabbitMq/RabbitMqModule.cs | 2 + back/src/Kyoo.RabbitMq/ScannerProducer.cs | 71 +++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 back/src/Kyoo.Abstractions/Controllers/IScanner.cs create mode 100644 back/src/Kyoo.RabbitMq/ScannerProducer.cs diff --git a/back/src/Kyoo.Abstractions/Controllers/IScanner.cs b/back/src/Kyoo.Abstractions/Controllers/IScanner.cs new file mode 100644 index 00000000..62f14f9f --- /dev/null +++ b/back/src/Kyoo.Abstractions/Controllers/IScanner.cs @@ -0,0 +1,27 @@ +// 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 . + +using System; +using System.Threading.Tasks; + +namespace Kyoo.Abstractions.Controllers; + +public interface IScanner +{ + Task SendRefreshRequest(string kind, Guid id); +} diff --git a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs index 93b3063c..9fb268a0 100644 --- a/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/EpisodeApi.cs @@ -41,6 +41,29 @@ namespace Kyoo.Core.Api; public class EpisodeApi(ILibraryManager libraryManager) : TranscoderApi(libraryManager.Episodes) { + /// + /// Refresh + /// + /// + /// Ask a metadata refresh. + /// + /// The ID or slug of the . + /// Nothing + /// No episode with the given ID or slug could be found. + [HttpPost("{identifier:id}/refresh")] + [PartialPermission(Kind.Write)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Refresh(Identifier identifier, [FromServices] IScanner scanner) + { + Guid id = await identifier.Match( + id => Task.FromResult(id), + async slug => (await libraryManager.Episodes.Get(slug)).Id + ); + await scanner.SendRefreshRequest(nameof(Episode), id); + return NoContent(); + } + /// /// Get episode's show /// diff --git a/back/src/Kyoo.RabbitMq/RabbitMqModule.cs b/back/src/Kyoo.RabbitMq/RabbitMqModule.cs index aba89bb2..45ff8c39 100644 --- a/back/src/Kyoo.RabbitMq/RabbitMqModule.cs +++ b/back/src/Kyoo.RabbitMq/RabbitMqModule.cs @@ -16,6 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . +using Kyoo.Abstractions.Controllers; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -41,5 +42,6 @@ public static class RabbitMqModule return factory.CreateConnection(); }); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); } } diff --git a/back/src/Kyoo.RabbitMq/ScannerProducer.cs b/back/src/Kyoo.RabbitMq/ScannerProducer.cs new file mode 100644 index 00000000..71c8f97a --- /dev/null +++ b/back/src/Kyoo.RabbitMq/ScannerProducer.cs @@ -0,0 +1,71 @@ +// 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 . + +using System.Text; +using System.Text.Json; +using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models; +using Kyoo.Utils; +using RabbitMQ.Client; + +namespace Kyoo.RabbitMq; + +public class ScannerProducer : IScanner +{ + private readonly IModel _channel; + + public ScannerProducer(IConnection rabbitConnection) + { + _channel = rabbitConnection.CreateModel(); + _channel.QueueDeclare("scanner", exclusive: false, autoDelete: false); + } + + private IRepository.ResourceEventHandler _Publish( + string exchange, + string type, + string action + ) + where T : IResource, IQuery + { + return (T resource) => + { + Message message = + new() + { + Action = action, + Type = type, + Value = resource, + }; + _channel.BasicPublish( + exchange, + routingKey: message.AsRoutingKey(), + body: message.AsBytes() + ); + return Task.CompletedTask; + }; + } + + public Task SendRefreshRequest(string kind, Guid id) + { + var message = new { Action = "refresh", Kind = kind.ToLowerInvariant(), Id = id }; + var body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(message, Utility.JsonOptions)); + _channel.BasicPublish("", routingKey: "scanner", body: body); + return Task.CompletedTask; + } +} + From 525da02fce1f56fcee29bacae40a8cf170504d30 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 22 Apr 2024 21:47:58 +0200 Subject: [PATCH 07/10] Add episode refresh button on the front --- front/packages/models/src/resources/user.ts | 1 + front/packages/ui/src/admin/users.tsx | 2 +- .../ui/src/components/context-menus.tsx | 37 ++++++++++++++----- front/translations/en.json | 1 + front/translations/fr.json | 1 + 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/front/packages/models/src/resources/user.ts b/front/packages/models/src/resources/user.ts index f54442bf..9f7927c8 100644 --- a/front/packages/models/src/resources/user.ts +++ b/front/packages/models/src/resources/user.ts @@ -82,6 +82,7 @@ export const UserP = ResourceP("user") ...x, logo: imageFn(`/user/${x.slug}/logo`), isVerified: x.permissions.length > 0, + isAdmin: x.permissions?.includes("admin.write") })); export type User = z.infer; diff --git a/front/packages/ui/src/admin/users.tsx b/front/packages/ui/src/admin/users.tsx index 43e3c7af..fd54da41 100644 --- a/front/packages/ui/src/admin/users.tsx +++ b/front/packages/ui/src/admin/users.tsx @@ -178,7 +178,7 @@ export const UserList = () => { id={user.id} username={user.username} avatar={user.logo} - isAdmin={user.permissions?.includes("admin.write")} + isAdmin={user.isAdmin} isVerified={user.isVerified} /> )} diff --git a/front/packages/ui/src/components/context-menus.tsx b/front/packages/ui/src/components/context-menus.tsx index 71ad0eac..fd0f2422 100644 --- a/front/packages/ui/src/components/context-menus.tsx +++ b/front/packages/ui/src/components/context-menus.tsx @@ -18,20 +18,21 @@ * along with Kyoo. If not, see . */ -import { IconButton, Menu, tooltip, usePopup } from "@kyoo/primitives"; -import { ComponentProps, ReactElement, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg"; -import Info from "@material-symbols/svg-400/rounded/info.svg"; -import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg"; -import Download from "@material-symbols/svg-400/rounded/download.svg"; import { WatchStatusV, queryFn, useAccount } from "@kyoo/models"; +import { HR, IconButton, Menu, tooltip, usePopup } from "@kyoo/primitives"; +import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg"; +import Download from "@material-symbols/svg-400/rounded/download.svg"; +import Info from "@material-symbols/svg-400/rounded/info.svg"; +import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg"; +import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { watchListIcon } from "./watchlist-info"; -import { useDownloader } from "../downloads"; +import { ComponentProps } from "react"; +import { useTranslation } from "react-i18next"; import { Platform } from "react-native"; import { useYoshiki } from "yoshiki/native"; +import { useDownloader } from "../downloads"; import { MediaInfoPopup } from "./media-info"; +import { watchListIcon } from "./watchlist-info"; export const EpisodesContext = ({ type = "episode", @@ -63,6 +64,14 @@ export const EpisodesContext = ({ onSettled: async () => await queryClient.invalidateQueries({ queryKey: [type, slug] }), }); + const metadataRefreshMutation = useMutation({ + mutationFn: () => + queryFn({ + path: [type, slug, "refresh"], + method: "POST", + }), + }); + return ( <> )} + {account?.isAdmin === true && ( + <> +
+ metadataRefreshMutation.mutate()} + /> + + )}
); diff --git a/front/translations/en.json b/front/translations/en.json index 0281f8fe..6d157aa7 100644 --- a/front/translations/en.json +++ b/front/translations/en.json @@ -6,6 +6,7 @@ "info": "See more", "none": "No episodes", "watchlistLogin": "To keep track of what you watched or plan to watch, you need to login.", + "refreshMetadata": "Refresh metadata", "episodeMore": { "goToShow": "Go to show", "download": "Download", diff --git a/front/translations/fr.json b/front/translations/fr.json index 016f7163..a154d214 100644 --- a/front/translations/fr.json +++ b/front/translations/fr.json @@ -6,6 +6,7 @@ "info": "Voir plus", "none": "Aucun episode", "watchlistLogin": "Pour suivre ce que vous avez regardé ou prévoyez de regarder, vous devez vous connecter.", + "refreshMetadata": "Actualiser les métadonnées", "episodeMore": { "goToShow": "Aller a la serie", "download": "Télécharger", From 12aa73376a1d25f3581298ddf36b6519a8bd5cc2 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 22 Apr 2024 22:08:03 +0200 Subject: [PATCH 08/10] Add refresh routes for seasons/shows/collections/movies --- .../Views/Resources/CollectionApi.cs | 23 ++++++++++++++++ .../src/Kyoo.Core/Views/Resources/MovieApi.cs | 27 +++++++++++++++++-- .../Kyoo.Core/Views/Resources/SeasonApi.cs | 24 +++++++++++++++++ back/src/Kyoo.Core/Views/Resources/ShowApi.cs | 23 ++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs b/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs index 24acee4a..a8a7a73c 100644 --- a/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/CollectionApi.cs @@ -47,6 +47,29 @@ public class CollectionApi( LibraryItemRepository items ) : CrudThumbsApi(collections) { + /// + /// Refresh + /// + /// + /// Ask a metadata refresh. + /// + /// The ID or slug of the . + /// Nothing + /// No episode with the given ID or slug could be found. + [HttpPost("{identifier:id}/refresh")] + [PartialPermission(Kind.Write)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Refresh(Identifier identifier, [FromServices] IScanner scanner) + { + Guid id = await identifier.Match( + id => Task.FromResult(id), + async slug => (await collections.Get(slug)).Id + ); + await scanner.SendRefreshRequest(nameof(Collection), id); + return NoContent(); + } + /// /// Add a movie /// diff --git a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs index 629c3d0f..1d95ab6f 100644 --- a/back/src/Kyoo.Core/Views/Resources/MovieApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/MovieApi.cs @@ -38,10 +38,33 @@ namespace Kyoo.Core.Api; [Route("movies")] [Route("movie", Order = AlternativeRoute)] [ApiController] -[PartialPermission(nameof(Show))] -[ApiDefinition("Shows", Group = ResourcesGroup)] +[PartialPermission(nameof(Movie))] +[ApiDefinition("Movie", Group = ResourcesGroup)] public class MovieApi(ILibraryManager libraryManager) : TranscoderApi(libraryManager.Movies) { + /// + /// Refresh + /// + /// + /// Ask a metadata refresh. + /// + /// The ID or slug of the . + /// Nothing + /// No episode with the given ID or slug could be found. + [HttpPost("{identifier:id}/refresh")] + [PartialPermission(Kind.Write)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Refresh(Identifier identifier, [FromServices] IScanner scanner) + { + Guid id = await identifier.Match( + id => Task.FromResult(id), + async slug => (await libraryManager.Movies.Get(slug)).Id + ); + await scanner.SendRefreshRequest(nameof(Movie), id); + return NoContent(); + } + /// /// Get studio that made the show /// diff --git a/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs b/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs index 8df85291..dcbdae8c 100644 --- a/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/SeasonApi.cs @@ -16,6 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -41,6 +42,29 @@ namespace Kyoo.Core.Api; public class SeasonApi(ILibraryManager libraryManager) : CrudThumbsApi(libraryManager.Seasons) { + /// + /// Refresh + /// + /// + /// Ask a metadata refresh. + /// + /// The ID or slug of the . + /// Nothing + /// No episode with the given ID or slug could be found. + [HttpPost("{identifier:id}/refresh")] + [PartialPermission(Kind.Write)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Refresh(Identifier identifier, [FromServices] IScanner scanner) + { + Guid id = await identifier.Match( + id => Task.FromResult(id), + async slug => (await libraryManager.Seasons.Get(slug)).Id + ); + await scanner.SendRefreshRequest(nameof(Season), id); + return NoContent(); + } + /// /// Get episodes in the season /// diff --git a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs index 54138ac9..fb23e4f3 100644 --- a/back/src/Kyoo.Core/Views/Resources/ShowApi.cs +++ b/back/src/Kyoo.Core/Views/Resources/ShowApi.cs @@ -42,6 +42,29 @@ namespace Kyoo.Core.Api; [ApiDefinition("Shows", Group = ResourcesGroup)] public class ShowApi(ILibraryManager libraryManager) : CrudThumbsApi(libraryManager.Shows) { + /// + /// Refresh + /// + /// + /// Ask a metadata refresh. + /// + /// The ID or slug of the . + /// Nothing + /// No episode with the given ID or slug could be found. + [HttpPost("{identifier:id}/refresh")] + [PartialPermission(Kind.Write)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Refresh(Identifier identifier, [FromServices] IScanner scanner) + { + Guid id = await identifier.Match( + id => Task.FromResult(id), + async slug => (await libraryManager.Shows.Get(slug)).Id + ); + await scanner.SendRefreshRequest(nameof(Show), id); + return NoContent(); + } + /// /// Get seasons of this show /// From b2c30ee059b1cb4b067f3f4864e401c6a28c430d Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 22 Apr 2024 22:54:17 +0200 Subject: [PATCH 09/10] Add metadata refresh button on show/movie details page --- front/packages/ui/src/details/header.tsx | 245 ++++++++++++++--------- 1 file changed, 148 insertions(+), 97 deletions(-) diff --git a/front/packages/ui/src/details/header.tsx b/front/packages/ui/src/details/header.tsx index 80268029..c4f5dc9a 100644 --- a/front/packages/ui/src/details/header.tsx +++ b/front/packages/ui/src/details/header.tsx @@ -19,65 +19,165 @@ */ import { + Genre, + KyooImage, Movie, QueryIdentifier, Show, - getDisplayDate, - Genre, Studio, - KyooImage, + getDisplayDate, + queryFn, + useAccount, } from "@kyoo/models"; +import { WatchStatusV } from "@kyoo/models/src/resources/watch-status"; import { + A, + Chip, Container, + DottedSeparator, H1, - ImageBackground, - Skeleton, - Poster, - P, - tooltip, - Link, + H2, + HR, + Head, IconButton, IconFab, - Head, - HR, - H2, - UL, + ImageBackground, LI, - A, + Link, + Menu, + P, + Poster, + Skeleton, + UL, + capitalize, + tooltip, ts, - Chip, - DottedSeparator, usePopup, } from "@kyoo/primitives"; -import { Fragment } from "react"; -import { useTranslation } from "react-i18next"; +import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg"; +import Download from "@material-symbols/svg-400/rounded/download.svg"; +import MoreHoriz from "@material-symbols/svg-400/rounded/more_horiz.svg"; import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg"; -import { ImageStyle, Platform, View } from "react-native"; -import { - Theme, - md, - px, - min, - max, - em, - percent, - rem, - vh, - useYoshiki, - Stylable, -} from "yoshiki/native"; -import { Fetch } from "../fetch"; import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg"; import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg"; -import { Rating } from "../components/rating"; -import { displayRuntime } from "./episode"; -import { WatchListInfo } from "../components/watchlist-info"; -import { WatchStatusV } from "@kyoo/models/src/resources/watch-status"; -import { capitalize } from "@kyoo/primitives"; -import { ShowWatchStatusCard } from "./show"; -import Download from "@material-symbols/svg-400/rounded/download.svg"; -import { useDownloader } from "../downloads"; +import { useMutation } from "@tanstack/react-query"; +import { Fragment } from "react"; +import { useTranslation } from "react-i18next"; +import { ImageStyle, Platform, View } from "react-native"; +import { + Stylable, + Theme, + em, + max, + md, + min, + percent, + px, + rem, + useYoshiki, + vh, +} from "yoshiki/native"; import { MediaInfoPopup } from "../components/media-info"; +import { Rating } from "../components/rating"; +import { WatchListInfo } from "../components/watchlist-info"; +import { useDownloader } from "../downloads"; +import { Fetch } from "../fetch"; +import { displayRuntime } from "./episode"; +import { ShowWatchStatusCard } from "./show"; + +const ButtonList = ({ + playHref, + trailerUrl, + watchStatus, + type, + slug, +}: { + type: "movie" | "show" | "collection"; + slug?: string; + playHref?: string | null; + trailerUrl?: string | null; + watchStatus?: WatchStatusV | null; +}) => { + const account = useAccount(); + const { css, theme } = useYoshiki(); + const { t } = useTranslation(); + const downloader = useDownloader(); + const [setPopup, close] = usePopup(); + + const metadataRefreshMutation = useMutation({ + mutationFn: () => + queryFn({ + path: [type, slug, "refresh"], + method: "POST", + }), + }); + + return ( + + {playHref !== null && ( + + )} + {trailerUrl && ( + + )} + {watchStatus !== undefined && type !== "collection" && slug && ( + + )} + {((type === "movie" && slug) || account?.isAdmin === true) && ( + + {type === "movie" && slug && ( + <> + downloader(type, slug)} + label={t("home.episodeMore.download")} + /> + + setPopup() + } + /> + + )} + {account?.isAdmin === true && ( + <> + {type === "movie" &&
} + metadataRefreshMutation.mutate()} + /> + + )} +
+ )} +
+ ); +}; export const TitleLine = ({ isLoading, @@ -111,8 +211,6 @@ export const TitleLine = ({ } & Stylable) => { const { css, theme } = useYoshiki(); const { t } = useTranslation(); - const downloader = useDownloader(); - const [setPopup, close] = usePopup(); return ( - - {playHref !== null && ( - - )} - {trailerUrl && ( - - )} - {watchStatus !== undefined && type !== "collection" && slug && ( - - )} - {type === "movie" && slug && ( - <> - downloader(type, slug)} - color={{ xs: theme.user.contrast, md: theme.colors.white }} - {...tooltip(t("home.episodeMore.download"))} - /> - - setPopup( - , - ) - } - /> - - )} - + From 7c79c37d8c35a34493daab5daa23f682a5e8794a Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 22 Apr 2024 23:24:38 +0200 Subject: [PATCH 10/10] Format code --- back/src/Kyoo.RabbitMq/ScannerProducer.cs | 8 ++++++-- front/packages/models/src/resources/user.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/back/src/Kyoo.RabbitMq/ScannerProducer.cs b/back/src/Kyoo.RabbitMq/ScannerProducer.cs index 71c8f97a..bc9db49d 100644 --- a/back/src/Kyoo.RabbitMq/ScannerProducer.cs +++ b/back/src/Kyoo.RabbitMq/ScannerProducer.cs @@ -62,10 +62,14 @@ public class ScannerProducer : IScanner public Task SendRefreshRequest(string kind, Guid id) { - var message = new { Action = "refresh", Kind = kind.ToLowerInvariant(), Id = id }; + var message = new + { + Action = "refresh", + Kind = kind.ToLowerInvariant(), + Id = id + }; var body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(message, Utility.JsonOptions)); _channel.BasicPublish("", routingKey: "scanner", body: body); return Task.CompletedTask; } } - diff --git a/front/packages/models/src/resources/user.ts b/front/packages/models/src/resources/user.ts index 9f7927c8..f17ea5d5 100644 --- a/front/packages/models/src/resources/user.ts +++ b/front/packages/models/src/resources/user.ts @@ -82,7 +82,7 @@ export const UserP = ResourceP("user") ...x, logo: imageFn(`/user/${x.slug}/logo`), isVerified: x.permissions.length > 0, - isAdmin: x.permissions?.includes("admin.write") + isAdmin: x.permissions?.includes("admin.write"), })); export type User = z.infer;