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.
|
/// The date of the next metadata refresh. Null if auto-refresh is disabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? NextMetadataRefresh { get; set; }
|
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))
|
if (string.IsNullOrEmpty(resource.Name))
|
||||||
throw new ArgumentException("The collection's name must be set and not empty");
|
throw new ArgumentException("The collection's name must be set and not empty");
|
||||||
|
resource.NextMetadataRefresh ??= DateTime.UtcNow.AddMonths(2);
|
||||||
await thumbnails.DownloadImages(resource);
|
await thumbnails.DownloadImages(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,14 +71,6 @@ public class EpisodeRepository(
|
|||||||
.ToListAsync();
|
.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 />
|
/// <inheritdoc />
|
||||||
protected override async Task Validate(Episode resource)
|
protected override async Task Validate(Episode resource)
|
||||||
{
|
{
|
||||||
@ -86,6 +78,9 @@ public class EpisodeRepository(
|
|||||||
resource.Show = null;
|
resource.Show = null;
|
||||||
if (resource.ShowId == Guid.Empty)
|
if (resource.ShowId == Guid.Empty)
|
||||||
throw new ValidationException("Missing show id");
|
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;
|
resource.Season = null;
|
||||||
if (resource.SeasonId == null && resource.SeasonNumber != null)
|
if (resource.SeasonId == null && resource.SeasonNumber != null)
|
||||||
{
|
{
|
||||||
@ -96,6 +91,8 @@ public class EpisodeRepository(
|
|||||||
.Select(x => x.Id)
|
.Select(x => x.Id)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource.NextMetadataRefresh ??= IRefreshable.ComputeNextRefreshDate(resource.ReleaseDate);
|
||||||
await thumbnails.DownloadImages(resource);
|
await thumbnails.DownloadImages(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,6 +297,8 @@ public abstract class GenericRepository<T>(DatabaseContext database) : IReposito
|
|||||||
{
|
{
|
||||||
await Validate(edited);
|
await Validate(edited);
|
||||||
Database.Update(edited);
|
Database.Update(edited);
|
||||||
|
if (edited is IAddedDate date)
|
||||||
|
Database.Entry(date).Property(p => p.AddedDate).IsModified = false;
|
||||||
await Database.SaveChangesAsync();
|
await Database.SaveChangesAsync();
|
||||||
await IRepository<T>.OnResourceEdited(edited);
|
await IRepository<T>.OnResourceEdited(edited);
|
||||||
return edited;
|
return edited;
|
||||||
@ -304,25 +306,17 @@ public abstract class GenericRepository<T>(DatabaseContext database) : IReposito
|
|||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public virtual async Task<T> Patch(Guid id, Func<T, T> patch)
|
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 Database.SaveChangesAsync();
|
||||||
await IRepository<T>.OnResourceEdited(resource);
|
await IRepository<T>.OnResourceEdited(resource);
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
Database.ChangeTracker.LazyLoadingEnabled = lazyLoading;
|
|
||||||
Database.ChangeTracker.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <exception cref="ValidationException">
|
/// <exception cref="ValidationException">
|
||||||
/// You can throw this if the resource is illegal and should not be saved.
|
/// You can throw this if the resource is illegal and should not be saved.
|
||||||
|
@ -74,6 +74,7 @@ public class MovieRepository(
|
|||||||
resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id;
|
resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id;
|
||||||
resource.Studio = null;
|
resource.Studio = null;
|
||||||
}
|
}
|
||||||
|
resource.NextMetadataRefresh ??= IRefreshable.ComputeNextRefreshDate(resource.AirDate);
|
||||||
await thumbnails.DownloadImages(resource);
|
await thumbnails.DownloadImages(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Abstractions.Controllers;
|
using Kyoo.Abstractions.Controllers;
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
using Kyoo.Abstractions.Models.Exceptions;
|
|
||||||
using Kyoo.Abstractions.Models.Utils;
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
using Kyoo.Postgresql;
|
using Kyoo.Postgresql;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -31,8 +30,11 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace Kyoo.Core.Controllers;
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumbnails)
|
public class SeasonRepository(
|
||||||
: GenericRepository<Season>(database)
|
DatabaseContext database,
|
||||||
|
IRepository<Show> shows,
|
||||||
|
IThumbnailsManager thumbnails
|
||||||
|
) : GenericRepository<Season>(database)
|
||||||
{
|
{
|
||||||
static SeasonRepository()
|
static SeasonRepository()
|
||||||
{
|
{
|
||||||
@ -66,16 +68,6 @@ public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumb
|
|||||||
.ToListAsync();
|
.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/>
|
/// <inheritdoc/>
|
||||||
protected override async Task Validate(Season resource)
|
protected override async Task Validate(Season resource)
|
||||||
{
|
{
|
||||||
@ -83,6 +75,9 @@ public class SeasonRepository(DatabaseContext database, IThumbnailsManager thumb
|
|||||||
resource.Show = null;
|
resource.Show = null;
|
||||||
if (resource.ShowId == Guid.Empty)
|
if (resource.ShowId == Guid.Empty)
|
||||||
throw new ValidationException("Missing show id");
|
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);
|
await thumbnails.DownloadImages(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,7 @@ public class ShowRepository(
|
|||||||
resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id;
|
resource.StudioId = (await studios.CreateIfNotExists(resource.Studio)).Id;
|
||||||
resource.Studio = null;
|
resource.Studio = null;
|
||||||
}
|
}
|
||||||
|
resource.NextMetadataRefresh ??= IRefreshable.ComputeNextRefreshDate(resource.StartAir);
|
||||||
await thumbnails.DownloadImages(resource);
|
await thumbnails.DownloadImages(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,29 @@ public class CollectionApi(
|
|||||||
LibraryItemRepository items
|
LibraryItemRepository items
|
||||||
) : CrudThumbsApi<Collection>(collections)
|
) : 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>
|
/// <summary>
|
||||||
/// Add a movie
|
/// Add a movie
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -41,6 +41,29 @@ namespace Kyoo.Core.Api;
|
|||||||
public class EpisodeApi(ILibraryManager libraryManager)
|
public class EpisodeApi(ILibraryManager libraryManager)
|
||||||
: TranscoderApi<Episode>(libraryManager.Episodes)
|
: 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>
|
/// <summary>
|
||||||
/// Get episode's show
|
/// Get episode's show
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -38,10 +38,33 @@ namespace Kyoo.Core.Api;
|
|||||||
[Route("movies")]
|
[Route("movies")]
|
||||||
[Route("movie", Order = AlternativeRoute)]
|
[Route("movie", Order = AlternativeRoute)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[PartialPermission(nameof(Show))]
|
[PartialPermission(nameof(Movie))]
|
||||||
[ApiDefinition("Shows", Group = ResourcesGroup)]
|
[ApiDefinition("Movie", Group = ResourcesGroup)]
|
||||||
public class MovieApi(ILibraryManager libraryManager) : TranscoderApi<Movie>(libraryManager.Movies)
|
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>
|
/// <summary>
|
||||||
/// Get studio that made the show
|
/// Get studio that made the show
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -41,6 +42,29 @@ namespace Kyoo.Core.Api;
|
|||||||
public class SeasonApi(ILibraryManager libraryManager)
|
public class SeasonApi(ILibraryManager libraryManager)
|
||||||
: CrudThumbsApi<Season>(libraryManager.Seasons)
|
: 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>
|
/// <summary>
|
||||||
/// Get episodes in the season
|
/// Get episodes in the season
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -42,6 +42,29 @@ namespace Kyoo.Core.Api;
|
|||||||
[ApiDefinition("Shows", Group = ResourcesGroup)]
|
[ApiDefinition("Shows", Group = ResourcesGroup)]
|
||||||
public class ShowApi(ILibraryManager libraryManager) : CrudThumbsApi<Show>(libraryManager.Shows)
|
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>
|
/// <summary>
|
||||||
/// Get seasons of this show
|
/// Get seasons of this show
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -41,5 +42,6 @@ public static class RabbitMqModule
|
|||||||
return factory.CreateConnection();
|
return factory.CreateConnection();
|
||||||
});
|
});
|
||||||
builder.Services.AddSingleton<RabbitProducer>();
|
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,
|
...x,
|
||||||
logo: imageFn(`/user/${x.slug}/logo`),
|
logo: imageFn(`/user/${x.slug}/logo`),
|
||||||
isVerified: x.permissions.length > 0,
|
isVerified: x.permissions.length > 0,
|
||||||
|
isAdmin: x.permissions?.includes("admin.write"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type User = z.infer<typeof UserP>;
|
export type User = z.infer<typeof UserP>;
|
||||||
|
@ -178,7 +178,7 @@ export const UserList = () => {
|
|||||||
id={user.id}
|
id={user.id}
|
||||||
username={user.username}
|
username={user.username}
|
||||||
avatar={user.logo}
|
avatar={user.logo}
|
||||||
isAdmin={user.permissions?.includes("admin.write")}
|
isAdmin={user.isAdmin}
|
||||||
isVerified={user.isVerified}
|
isVerified={user.isVerified}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -18,20 +18,21 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* 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 { 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 { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { watchListIcon } from "./watchlist-info";
|
import { ComponentProps } from "react";
|
||||||
import { useDownloader } from "../downloads";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
|
import { useDownloader } from "../downloads";
|
||||||
import { MediaInfoPopup } from "./media-info";
|
import { MediaInfoPopup } from "./media-info";
|
||||||
|
import { watchListIcon } from "./watchlist-info";
|
||||||
|
|
||||||
export const EpisodesContext = ({
|
export const EpisodesContext = ({
|
||||||
type = "episode",
|
type = "episode",
|
||||||
@ -63,6 +64,14 @@ export const EpisodesContext = ({
|
|||||||
onSettled: async () => await queryClient.invalidateQueries({ queryKey: [type, slug] }),
|
onSettled: async () => await queryClient.invalidateQueries({ queryKey: [type, slug] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const metadataRefreshMutation = useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
queryFn({
|
||||||
|
path: [type, slug, "refresh"],
|
||||||
|
method: "POST",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Menu
|
<Menu
|
||||||
@ -114,6 +123,16 @@ export const EpisodesContext = ({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{account?.isAdmin === true && (
|
||||||
|
<>
|
||||||
|
<HR />
|
||||||
|
<Menu.Item
|
||||||
|
label={t("home.refreshMetadata")}
|
||||||
|
icon={Refresh}
|
||||||
|
onSelect={() => metadataRefreshMutation.mutate()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -19,65 +19,165 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Genre,
|
||||||
|
KyooImage,
|
||||||
Movie,
|
Movie,
|
||||||
QueryIdentifier,
|
QueryIdentifier,
|
||||||
Show,
|
Show,
|
||||||
getDisplayDate,
|
|
||||||
Genre,
|
|
||||||
Studio,
|
Studio,
|
||||||
KyooImage,
|
getDisplayDate,
|
||||||
|
queryFn,
|
||||||
|
useAccount,
|
||||||
} from "@kyoo/models";
|
} from "@kyoo/models";
|
||||||
|
import { WatchStatusV } from "@kyoo/models/src/resources/watch-status";
|
||||||
import {
|
import {
|
||||||
|
A,
|
||||||
|
Chip,
|
||||||
Container,
|
Container,
|
||||||
|
DottedSeparator,
|
||||||
H1,
|
H1,
|
||||||
ImageBackground,
|
H2,
|
||||||
Skeleton,
|
HR,
|
||||||
Poster,
|
Head,
|
||||||
P,
|
|
||||||
tooltip,
|
|
||||||
Link,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
IconFab,
|
IconFab,
|
||||||
Head,
|
ImageBackground,
|
||||||
HR,
|
|
||||||
H2,
|
|
||||||
UL,
|
|
||||||
LI,
|
LI,
|
||||||
A,
|
Link,
|
||||||
|
Menu,
|
||||||
|
P,
|
||||||
|
Poster,
|
||||||
|
Skeleton,
|
||||||
|
UL,
|
||||||
|
capitalize,
|
||||||
|
tooltip,
|
||||||
ts,
|
ts,
|
||||||
Chip,
|
|
||||||
DottedSeparator,
|
|
||||||
usePopup,
|
usePopup,
|
||||||
} from "@kyoo/primitives";
|
} from "@kyoo/primitives";
|
||||||
import { Fragment } from "react";
|
import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg";
|
||||||
import { useTranslation } from "react-i18next";
|
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 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 PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||||
import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg";
|
import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg";
|
||||||
import { Rating } from "../components/rating";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { displayRuntime } from "./episode";
|
import { Fragment } from "react";
|
||||||
import { WatchListInfo } from "../components/watchlist-info";
|
import { useTranslation } from "react-i18next";
|
||||||
import { WatchStatusV } from "@kyoo/models/src/resources/watch-status";
|
import { ImageStyle, Platform, View } from "react-native";
|
||||||
import { capitalize } from "@kyoo/primitives";
|
import {
|
||||||
import { ShowWatchStatusCard } from "./show";
|
Stylable,
|
||||||
import Download from "@material-symbols/svg-400/rounded/download.svg";
|
Theme,
|
||||||
import { useDownloader } from "../downloads";
|
em,
|
||||||
|
max,
|
||||||
|
md,
|
||||||
|
min,
|
||||||
|
percent,
|
||||||
|
px,
|
||||||
|
rem,
|
||||||
|
useYoshiki,
|
||||||
|
vh,
|
||||||
|
} from "yoshiki/native";
|
||||||
import { MediaInfoPopup } from "../components/media-info";
|
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 = ({
|
export const TitleLine = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -111,8 +211,6 @@ export const TitleLine = ({
|
|||||||
} & Stylable) => {
|
} & Stylable) => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const downloader = useDownloader();
|
|
||||||
const [setPopup, close] = usePopup();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
@ -221,60 +319,13 @@ export const TitleLine = ({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<View
|
<ButtonList
|
||||||
{...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}
|
type={type}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
status={watchStatus}
|
playHref={playHref}
|
||||||
color={{ xs: theme.user.contrast, md: theme.colors.white }}
|
trailerUrl={trailerUrl}
|
||||||
|
watchStatus={watchStatus}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{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>
|
|
||||||
<View
|
<View
|
||||||
{...css({ flexDirection: "row", alignItems: "center", justifyContent: "center" })}
|
{...css({ flexDirection: "row", alignItems: "center", justifyContent: "center" })}
|
||||||
>
|
>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"info": "See more",
|
"info": "See more",
|
||||||
"none": "No episodes",
|
"none": "No episodes",
|
||||||
"watchlistLogin": "To keep track of what you watched or plan to watch, you need to login.",
|
"watchlistLogin": "To keep track of what you watched or plan to watch, you need to login.",
|
||||||
|
"refreshMetadata": "Refresh metadata",
|
||||||
"episodeMore": {
|
"episodeMore": {
|
||||||
"goToShow": "Go to show",
|
"goToShow": "Go to show",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"info": "Voir plus",
|
"info": "Voir plus",
|
||||||
"none": "Aucun episode",
|
"none": "Aucun episode",
|
||||||
"watchlistLogin": "Pour suivre ce que vous avez regardé ou prévoyez de regarder, vous devez vous connecter.",
|
"watchlistLogin": "Pour suivre ce que vous avez regardé ou prévoyez de regarder, vous devez vous connecter.",
|
||||||
|
"refreshMetadata": "Actualiser les métadonnées",
|
||||||
"episodeMore": {
|
"episodeMore": {
|
||||||
"goToShow": "Aller a la serie",
|
"goToShow": "Aller a la serie",
|
||||||
"download": "Télécharger",
|
"download": "Télécharger",
|
||||||
|
@ -172,23 +172,36 @@ class Matcher:
|
|||||||
kind: Literal["collection", "movie", "episode", "show", "season"],
|
kind: Literal["collection", "movie", "episode", "show", "season"],
|
||||||
kyoo_id: str,
|
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 = {
|
identify_table = {
|
||||||
"collection": lambda _, id: self._provider.identify_collection(
|
"collection": lambda _, id: self._provider.identify_collection(
|
||||||
id["dataId"]
|
id["dataId"]
|
||||||
),
|
),
|
||||||
"movie": lambda _, id: self._provider.identify_movie(id["dataId"]),
|
"movie": lambda _, id: self._provider.identify_movie(id["dataId"]),
|
||||||
"show": lambda _, id: self._provider.identify_show(id["dataId"]),
|
"show": lambda _, id: self._provider.identify_show(id["dataId"]),
|
||||||
"season": lambda season, id: self._provider.identify_season(
|
"season": id_season,
|
||||||
id["dataId"], season["seasonNumber"]
|
"episode": id_episode,
|
||||||
),
|
|
||||||
"episode": lambda episode, id: self._provider.identify_episode(
|
|
||||||
id["showId"], id["season"], id["episode"], episode["absoluteNumber"]
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current = await self._client.get(kind, kyoo_id)
|
current = await self._client.get(kind, kyoo_id)
|
||||||
if self._provider.name not in current["externalId"]:
|
if self._provider.name not in current["externalId"]:
|
||||||
logger.error(
|
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
|
return False
|
||||||
provider_id = current["externalId"][self._provider.name]
|
provider_id = current["externalId"][self._provider.name]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user