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.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 d3162666..e1f2f0bd 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)
{
@@ -96,6 +91,8 @@ public class EpisodeRepository(
.Select(x => x.Id)
.FirstOrDefaultAsync();
}
+
+ resource.NextMetadataRefresh ??= IRefreshable.ComputeNextRefreshDate(resource.ReleaseDate);
await thumbnails.DownloadImages(resource);
}
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;
}
///
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 590d0b10..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;
@@ -31,8 +30,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 +68,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 +75,9 @@ 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;
+ 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);
}
}
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/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.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
///
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..bc9db49d
--- /dev/null
+++ b/back/src/Kyoo.RabbitMq/ScannerProducer.cs
@@ -0,0 +1,75 @@
+// 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;
+ }
+}
diff --git a/front/packages/models/src/resources/user.ts b/front/packages/models/src/resources/user.ts
index f54442bf..f17ea5d5 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/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) && (
+
+ )}
+
+ );
+};
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(
- ,
- )
- }
- />
- >
- )}
-
+
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",
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]