Add next update date in db

This commit is contained in:
Zoe Roux 2024-04-14 23:04:56 +02:00
parent c1ecdad916
commit 5a9b846f7c
No known key found for this signature in database
9 changed files with 120 additions and 84 deletions

View File

@ -24,8 +24,8 @@ namespace Kyoo.Abstractions.Models;
/// <summary> /// <summary>
/// A show, a movie or a collection. /// A show, a movie or a collection.
/// </summary> /// </summary>
[OneOf(Types = new[] { typeof(Episode), typeof(Movie) })] [OneOf(Types = [typeof(Episode), typeof(Movie)])]
public interface INews : IResource, IThumbnails, IMetadata, IAddedDate, IQuery public interface INews : IResource, IThumbnails, IAddedDate, IQuery
{ {
static Sort IQuery.DefaultSort => new Sort<INews>.By(nameof(AddedDate), true); static Sort IQuery.DefaultSort => new Sort<INews>.By(nameof(AddedDate), true);
} }

View File

@ -33,3 +33,29 @@ public class MetadataId
/// </summary> /// </summary>
public string? Link { get; set; } public string? Link { get; set; }
} }
/// <summary>
/// ID informations about an episode.
/// </summary>
public class EpisodeId
{
/// <summary>
/// The Id of the show on the metadata database.
/// </summary>
public string ShowId { get; set; }
/// <summary>
/// The season number or null if absolute numbering is used in this database.
/// </summary>
public int? Season { get; set; }
/// <summary>
/// The episode number or absolute number if Season is null.
/// </summary>
public int Episode { get; set; }
/// <summary>
/// The URL of the resource on the external provider.
/// </summary>
public string? Link { get; set; }
}

View File

@ -28,7 +28,7 @@ namespace Kyoo.Abstractions.Models;
/// <summary> /// <summary>
/// A class representing collections of <see cref="Show"/>. /// A class representing collections of <see cref="Show"/>.
/// </summary> /// </summary>
public class Collection : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, ILibraryItem public class Collection : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, IRefreshable, ILibraryItem
{ {
public static Sort DefaultSort => new Sort<Collection>.By(nameof(Collection.Name)); public static Sort DefaultSort => new Sort<Collection>.By(nameof(Collection.Name));
@ -76,6 +76,9 @@ public class Collection : IQuery, IResource, IMetadata, IThumbnails, IAddedDate,
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new(); public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <inheritdoc />
public DateTime? NextMetadataRefresh { get; set; }
public Collection() { } public Collection() { }
[JsonConstructor] [JsonConstructor]

View File

@ -31,7 +31,7 @@ namespace Kyoo.Abstractions.Models;
/// <summary> /// <summary>
/// A class to represent a single show's episode. /// A class to represent a single show's episode.
/// </summary> /// </summary>
public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, INews public class Episode : IQuery, IResource, IThumbnails, IAddedDate, IRefreshable, INews
{ {
// Use absolute numbers by default and fallback to season/episodes if it does not exists. // Use absolute numbers by default and fallback to season/episodes if it does not exists.
public static Sort DefaultSort => public static Sort DefaultSort =>
@ -166,7 +166,10 @@ public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, IN
public Image? Logo { get; set; } public Image? Logo { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new(); public Dictionary<string, EpisodeId> ExternalId { get; set; } = [];
/// <inheritdoc />
public DateTime? NextMetadataRefresh { get; set; }
/// <summary> /// <summary>
/// The previous episode that should be seen before viewing this one. /// The previous episode that should be seen before viewing this one.

View File

@ -0,0 +1,29 @@
// 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;
namespace Kyoo.Abstractions.Models;
public interface IRefreshable
{
/// <summary>
/// The date of the next metadata refresh. Null if auto-refresh is disabled.
/// </summary>
public DateTime? NextMetadataRefresh { get; set; }
}

View File

@ -38,6 +38,7 @@ public class Movie
IMetadata, IMetadata,
IThumbnails, IThumbnails,
IAddedDate, IAddedDate,
IRefreshable,
ILibraryItem, ILibraryItem,
INews, INews,
IWatchlist IWatchlist
@ -134,6 +135,9 @@ public class Movie
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new(); public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <inheritdoc />
public DateTime? NextMetadataRefresh { get; set; }
/// <summary> /// <summary>
/// The ID of the Studio that made this show. /// The ID of the Studio that made this show.
/// </summary> /// </summary>

View File

@ -31,7 +31,7 @@ namespace Kyoo.Abstractions.Models;
/// <summary> /// <summary>
/// A season of a <see cref="Show"/>. /// A season of a <see cref="Show"/>.
/// </summary> /// </summary>
public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, IRefreshable
{ {
public static Sort DefaultSort => new Sort<Season>.By(x => x.SeasonNumber); public static Sort DefaultSort => new Sort<Season>.By(x => x.SeasonNumber);
@ -119,6 +119,9 @@ public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new(); public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <inheritdoc />
public DateTime? NextMetadataRefresh { get; set; }
/// <summary> /// <summary>
/// The list of episodes that this season contains. /// The list of episodes that this season contains.
/// </summary> /// </summary>

View File

@ -39,6 +39,7 @@ public class Show
IOnMerge, IOnMerge,
IThumbnails, IThumbnails,
IAddedDate, IAddedDate,
IRefreshable,
ILibraryItem, ILibraryItem,
IWatchlist IWatchlist
{ {
@ -126,6 +127,9 @@ public class Show
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new(); public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
/// <inheritdoc />
public DateTime? NextMetadataRefresh { get; set; }
/// <summary> /// <summary>
/// The ID of the Studio that made this show. /// The ID of the Studio that made this show.
/// </summary> /// </summary>

View File

@ -157,21 +157,11 @@ public abstract class DatabaseContext : DbContext
optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
} }
private static ValueComparer<Dictionary<string, T>> _GetComparer<T>() private static void _HasJson<T, TVal>(
{ ModelBuilder builder,
return new( Expression<Func<T, Dictionary<string, TVal>>> property
(c1, c2) => c1!.SequenceEqual(c2!), )
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())) where T : class
);
}
/// <summary>
/// Build the metadata model for the given type.
/// </summary>
/// <param name="modelBuilder">The database model builder</param>
/// <typeparam name="T">The type to add metadata to.</typeparam>
private static void _HasMetadata<T>(ModelBuilder modelBuilder)
where T : class, IMetadata
{ {
// TODO: Waiting for https://github.com/dotnet/efcore/issues/29825 // TODO: Waiting for https://github.com/dotnet/efcore/issues/29825
// modelBuilder.Entity<T>() // modelBuilder.Entity<T>()
@ -179,22 +169,33 @@ public abstract class DatabaseContext : DbContext
// { // {
// x.ToJson(); // x.ToJson();
// }); // });
modelBuilder builder
.Entity<T>() .Entity<T>()
.Property(x => x.ExternalId) .Property(property)
.HasConversion( .HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v => v =>
JsonSerializer.Deserialize<Dictionary<string, MetadataId>>( JsonSerializer.Deserialize<Dictionary<string, TVal>>(
v, v,
(JsonSerializerOptions?)null (JsonSerializerOptions?)null
)! )!
) )
.HasColumnType("json"); .HasColumnType("json");
modelBuilder builder
.Entity<T>() .Entity<T>()
.Property(x => x.ExternalId) .Property(property)
.Metadata.SetValueComparer(_GetComparer<MetadataId>()); .Metadata.SetValueComparer(
new ValueComparer<Dictionary<string, TVal>>(
(c1, c2) => c1!.SequenceEqual(c2!),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode()))
)
);
}
private static void _HasMetadata<T>(ModelBuilder modelBuilder)
where T : class, IMetadata
{
_HasJson<T, MetadataId>(modelBuilder, x => x.ExternalId);
} }
private static void _HasImages<T>(ModelBuilder modelBuilder) private static void _HasImages<T>(ModelBuilder modelBuilder)
@ -215,6 +216,16 @@ public abstract class DatabaseContext : DbContext
.ValueGeneratedOnAdd(); .ValueGeneratedOnAdd();
} }
private static void _HasRefreshDate<T>(ModelBuilder builder)
where T : class, IRefreshable
{
// schedule a refresh soon since metadata can change frequently for recently added items ond online databases
builder.Entity<T>()
.Property(x => x.NextMetadataRefresh)
.HasDefaultValueSql("now() at time zone 'utc' + interval '2 hours'")
.ValueGeneratedOnAdd();
}
/// <summary> /// <summary>
/// Create a many to many relationship between the two entities. /// Create a many to many relationship between the two entities.
/// The resulting relationship will have an available <see cref="AddLinks{T1,T2}"/> method. /// The resulting relationship will have an available <see cref="AddLinks{T1,T2}"/> method.
@ -296,8 +307,8 @@ public abstract class DatabaseContext : DbContext
_HasMetadata<Movie>(modelBuilder); _HasMetadata<Movie>(modelBuilder);
_HasMetadata<Show>(modelBuilder); _HasMetadata<Show>(modelBuilder);
_HasMetadata<Season>(modelBuilder); _HasMetadata<Season>(modelBuilder);
_HasMetadata<Episode>(modelBuilder);
_HasMetadata<Studio>(modelBuilder); _HasMetadata<Studio>(modelBuilder);
_HasJson<Episode, EpisodeId>(modelBuilder, x => x.ExternalId);
_HasImages<Collection>(modelBuilder); _HasImages<Collection>(modelBuilder);
_HasImages<Movie>(modelBuilder); _HasImages<Movie>(modelBuilder);
@ -313,6 +324,12 @@ public abstract class DatabaseContext : DbContext
_HasAddedDate<User>(modelBuilder); _HasAddedDate<User>(modelBuilder);
_HasAddedDate<Issue>(modelBuilder); _HasAddedDate<Issue>(modelBuilder);
_HasRefreshDate<Collection>(modelBuilder);
_HasRefreshDate<Movie>(modelBuilder);
_HasRefreshDate<Show>(modelBuilder);
_HasRefreshDate<Season>(modelBuilder);
_HasRefreshDate<Episode>(modelBuilder);
modelBuilder modelBuilder
.Entity<MovieWatchStatus>() .Entity<MovieWatchStatus>()
.HasKey(x => new { User = x.UserId, Movie = x.MovieId }); .HasKey(x => new { User = x.UserId, Movie = x.MovieId });
@ -389,62 +406,9 @@ public abstract class DatabaseContext : DbContext
modelBuilder.Entity<Issue>().HasKey(x => new { x.Domain, x.Cause }); modelBuilder.Entity<Issue>().HasKey(x => new { x.Domain, x.Cause });
// TODO: Waiting for https://github.com/dotnet/efcore/issues/29825 _HasJson<User, string>(modelBuilder, x => x.Settings);
// modelBuilder.Entity<T>() _HasJson<User, ExternalToken>(modelBuilder, x => x.ExternalId);
// .OwnsOne(x => x.ExternalId, x => _HasJson<Issue, object>(modelBuilder, x => x.Extra);
// {
// x.ToJson();
// });
modelBuilder
.Entity<User>()
.Property(x => x.Settings)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v =>
JsonSerializer.Deserialize<Dictionary<string, string>>(
v,
(JsonSerializerOptions?)null
)!
)
.HasColumnType("json");
modelBuilder
.Entity<User>()
.Property(x => x.Settings)
.Metadata.SetValueComparer(_GetComparer<string>());
modelBuilder
.Entity<User>()
.Property(x => x.ExternalId)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v =>
JsonSerializer.Deserialize<Dictionary<string, ExternalToken>>(
v,
(JsonSerializerOptions?)null
)!
)
.HasColumnType("json");
modelBuilder
.Entity<User>()
.Property(x => x.ExternalId)
.Metadata.SetValueComparer(_GetComparer<ExternalToken>());
modelBuilder
.Entity<Issue>()
.Property(x => x.Extra)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v =>
JsonSerializer.Deserialize<Dictionary<string, object>>(
v,
(JsonSerializerOptions?)null
)!
)
.HasColumnType("json");
modelBuilder
.Entity<Issue>()
.Property(x => x.Extra)
.Metadata.SetValueComparer(_GetComparer<object>());
} }
/// <summary> /// <summary>