mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add metadata refresh (#430)
This commit is contained in:
commit
7d354249d4
27
back/src/Kyoo.Abstractions/Controllers/IScanner.cs
Normal file
27
back/src/Kyoo.Abstractions/Controllers/IScanner.cs
Normal 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);
|
||||
}
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
75
back/src/Kyoo.RabbitMq/ScannerProducer.cs
Normal file
75
back/src/Kyoo.RabbitMq/ScannerProducer.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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>;
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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" })}
|
||||
>
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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]
|
||||
|
Loading…
x
Reference in New Issue
Block a user