Add metadata refresh (#430)

This commit is contained in:
Zoe Roux 2024-04-22 23:55:15 +02:00 committed by GitHub
commit 7d354249d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 461 additions and 152 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.Threading.Tasks;
namespace Kyoo.Abstractions.Controllers;
public interface IScanner
{
Task SendRefreshRequest(string kind, Guid id);
}

View File

@ -26,4 +26,18 @@ public interface IRefreshable
/// The date of the next metadata refresh. Null if auto-refresh is disabled.
/// </summary>
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)
};
}
}

View File

@ -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);
}

View File

@ -71,14 +71,6 @@ public class EpisodeRepository(
.ToListAsync();
}
/// <inheritdoc />
public override async Task<Episode> 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);
}
/// <inheritdoc />
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);
}

View File

@ -297,6 +297,8 @@ public abstract class GenericRepository<T>(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<T>.OnResourceEdited(edited);
return edited;
@ -305,23 +307,15 @@ public abstract class GenericRepository<T>(DatabaseContext database) : IReposito
/// <inheritdoc/>
public virtual async Task<T> Patch(Guid id, Func<T, T> 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<T>.OnResourceEdited(resource);
return resource;
}
finally
{
Database.ChangeTracker.LazyLoadingEnabled = lazyLoading;
Database.ChangeTracker.Clear();
}
await Database.SaveChangesAsync();
await IRepository<T>.OnResourceEdited(resource);
return resource;
}
/// <exception cref="ValidationException">

View File

@ -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);
}
}

View File

@ -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<Season>(database)
public class SeasonRepository(
DatabaseContext database,
IRepository<Show> shows,
IThumbnailsManager thumbnails
) : GenericRepository<Season>(database)
{
static SeasonRepository()
{
@ -66,16 +68,6 @@ public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumb
.ToListAsync();
}
/// <inheritdoc/>
public override async Task<Season> 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);
}
/// <inheritdoc/>
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);
}
}

View File

@ -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);
}
}

View File

@ -47,6 +47,29 @@ public class CollectionApi(
LibraryItemRepository items
) : CrudThumbsApi<Collection>(collections)
{
/// <summary>
/// Refresh
/// </summary>
/// <remarks>
/// Ask a metadata refresh.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param>
/// <returns>Nothing</returns>
/// <response code="404">No episode with the given ID or slug could be found.</response>
[HttpPost("{identifier:id}/refresh")]
[PartialPermission(Kind.Write)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> 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();
}
/// <summary>
/// Add a movie
/// </summary>

View File

@ -41,6 +41,29 @@ namespace Kyoo.Core.Api;
public class EpisodeApi(ILibraryManager libraryManager)
: TranscoderApi<Episode>(libraryManager.Episodes)
{
/// <summary>
/// Refresh
/// </summary>
/// <remarks>
/// Ask a metadata refresh.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Episode"/>.</param>
/// <returns>Nothing</returns>
/// <response code="404">No episode with the given ID or slug could be found.</response>
[HttpPost("{identifier:id}/refresh")]
[PartialPermission(Kind.Write)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> 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();
}
/// <summary>
/// Get episode's show
/// </summary>

View File

@ -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<Movie>(libraryManager.Movies)
{
/// <summary>
/// Refresh
/// </summary>
/// <remarks>
/// Ask a metadata refresh.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Movie"/>.</param>
/// <returns>Nothing</returns>
/// <response code="404">No episode with the given ID or slug could be found.</response>
[HttpPost("{identifier:id}/refresh")]
[PartialPermission(Kind.Write)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> 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();
}
/// <summary>
/// Get studio that made the show
/// </summary>

View File

@ -16,6 +16,7 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
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<Season>(libraryManager.Seasons)
{
/// <summary>
/// Refresh
/// </summary>
/// <remarks>
/// Ask a metadata refresh.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Season"/>.</param>
/// <returns>Nothing</returns>
/// <response code="404">No episode with the given ID or slug could be found.</response>
[HttpPost("{identifier:id}/refresh")]
[PartialPermission(Kind.Write)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> 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();
}
/// <summary>
/// Get episodes in the season
/// </summary>

View File

@ -42,6 +42,29 @@ namespace Kyoo.Core.Api;
[ApiDefinition("Shows", Group = ResourcesGroup)]
public class ShowApi(ILibraryManager libraryManager) : CrudThumbsApi<Show>(libraryManager.Shows)
{
/// <summary>
/// Refresh
/// </summary>
/// <remarks>
/// Ask a metadata refresh.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <returns>Nothing</returns>
/// <response code="404">No episode with the given ID or slug could be found.</response>
[HttpPost("{identifier:id}/refresh")]
[PartialPermission(Kind.Write)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> 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();
}
/// <summary>
/// Get seasons of this show
/// </summary>

View File

@ -16,6 +16,7 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
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<RabbitProducer>();
builder.Services.AddSingleton<IScanner, ScannerProducer>();
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<T>.ResourceEventHandler _Publish<T>(
string exchange,
string type,
string action
)
where T : IResource, IQuery
{
return (T resource) =>
{
Message<T> 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;
}
}

View File

@ -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<typeof UserP>;

View File

@ -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}
/>
)}

View File

@ -18,20 +18,21 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
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 (
<>
<Menu
@ -114,6 +123,16 @@ export const EpisodesContext = ({
/>
</>
)}
{account?.isAdmin === true && (
<>
<HR />
<Menu.Item
label={t("home.refreshMetadata")}
icon={Refresh}
onSelect={() => metadataRefreshMutation.mutate()}
/>
</>
)}
</Menu>
</>
);

View File

@ -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 (
<View {...css({ flexDirection: "row", alignItems: "center", justifyContent: "center" })}>
{playHref !== null && (
<IconFab
icon={PlayArrow}
as={Link}
href={playHref}
color={{ xs: theme.user.colors.black, md: theme.colors.black }}
{...css({
bg: theme.user.accent,
fover: { self: { bg: theme.user.accent } },
})}
{...tooltip(t("show.play"))}
/>
)}
{trailerUrl && (
<IconButton
icon={Theaters}
as={Link}
href={trailerUrl}
target="_blank"
color={{ xs: theme.user.contrast, md: theme.colors.white }}
{...tooltip(t("show.trailer"))}
/>
)}
{watchStatus !== undefined && type !== "collection" && slug && (
<WatchListInfo
type={type}
slug={slug}
status={watchStatus}
color={{ xs: theme.user.contrast, md: theme.colors.white }}
/>
)}
{((type === "movie" && slug) || account?.isAdmin === true) && (
<Menu Trigger={IconButton} icon={MoreHoriz} {...tooltip(t("misc.more"))}>
{type === "movie" && slug && (
<>
<Menu.Item
icon={Download}
onSelect={() => downloader(type, slug)}
label={t("home.episodeMore.download")}
/>
<Menu.Item
icon={MovieInfo}
label={t("home.episodeMore.mediainfo")}
onSelect={() =>
setPopup(<MediaInfoPopup mediaType={"movie"} mediaSlug={slug!} close={close} />)
}
/>
</>
)}
{account?.isAdmin === true && (
<>
{type === "movie" && <HR />}
<Menu.Item
label={t("home.refreshMetadata")}
icon={Refresh}
onSelect={() => metadataRefreshMutation.mutate()}
/>
</>
)}
</Menu>
)}
</View>
);
};
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 (
<Container
@ -221,60 +319,13 @@ export const TitleLine = ({
justifyContent: "center",
})}
>
<View
{...css({ flexDirection: "row", alignItems: "center", justifyContent: "center" })}
>
{playHref !== null && (
<IconFab
icon={PlayArrow}
as={Link}
href={playHref}
color={{ xs: theme.user.colors.black, md: theme.colors.black }}
{...css({
bg: theme.user.accent,
fover: { self: { bg: theme.user.accent } },
})}
{...tooltip(t("show.play"))}
/>
)}
{trailerUrl && (
<IconButton
icon={Theaters}
as={Link}
href={trailerUrl}
target="_blank"
color={{ xs: theme.user.contrast, md: theme.colors.white }}
{...tooltip(t("show.trailer"))}
/>
)}
{watchStatus !== undefined && type !== "collection" && slug && (
<WatchListInfo
type={type}
slug={slug}
status={watchStatus}
color={{ xs: theme.user.contrast, md: theme.colors.white }}
/>
)}
{type === "movie" && slug && (
<>
<IconButton
icon={Download}
onPress={() => downloader(type, slug)}
color={{ xs: theme.user.contrast, md: theme.colors.white }}
{...tooltip(t("home.episodeMore.download"))}
/>
<IconButton
icon={MovieInfo}
color={{ xs: theme.user.contrast, md: theme.colors.white }}
onPress={() =>
setPopup(
<MediaInfoPopup mediaType={"movie"} mediaSlug={slug!} close={close} />,
)
}
/>
</>
)}
</View>
<ButtonList
type={type}
slug={slug}
playHref={playHref}
trailerUrl={trailerUrl}
watchStatus={watchStatus}
/>
<View
{...css({ flexDirection: "row", alignItems: "center", justifyContent: "center" })}
>

View File

@ -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",

View File

@ -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",

View File

@ -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]