diff --git a/back/src/Kyoo.Abstractions/Models/INews.cs b/back/src/Kyoo.Abstractions/Models/INews.cs
index 5b25eaca..b5642f5d 100644
--- a/back/src/Kyoo.Abstractions/Models/INews.cs
+++ b/back/src/Kyoo.Abstractions/Models/INews.cs
@@ -24,8 +24,8 @@ namespace Kyoo.Abstractions.Models;
///
/// A show, a movie or a collection.
///
-[OneOf(Types = new[] { typeof(Episode), typeof(Movie) })]
-public interface INews : IResource, IThumbnails, IMetadata, IAddedDate, IQuery
+[OneOf(Types = [typeof(Episode), typeof(Movie)])]
+public interface INews : IResource, IThumbnails, IAddedDate, IQuery
{
static Sort IQuery.DefaultSort => new Sort.By(nameof(AddedDate), true);
}
diff --git a/back/src/Kyoo.Abstractions/Models/MetadataID.cs b/back/src/Kyoo.Abstractions/Models/MetadataID.cs
index 37919c10..ec384ec8 100644
--- a/back/src/Kyoo.Abstractions/Models/MetadataID.cs
+++ b/back/src/Kyoo.Abstractions/Models/MetadataID.cs
@@ -33,3 +33,29 @@ public class MetadataId
///
public string? Link { get; set; }
}
+
+///
+/// ID informations about an episode.
+///
+public class EpisodeId
+{
+ ///
+ /// The Id of the show on the metadata database.
+ ///
+ public string ShowId { get; set; }
+
+ ///
+ /// The season number or null if absolute numbering is used in this database.
+ ///
+ public int? Season { get; set; }
+
+ ///
+ /// The episode number or absolute number if Season is null.
+ ///
+ public int Episode { get; set; }
+
+ ///
+ /// The URL of the resource on the external provider.
+ ///
+ public string? Link { get; set; }
+}
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs b/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs
index eb1bda94..f2539a55 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Collection.cs
@@ -28,7 +28,7 @@ namespace Kyoo.Abstractions.Models;
///
/// A class representing collections of .
///
-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.By(nameof(Collection.Name));
@@ -76,6 +76,9 @@ public class Collection : IQuery, IResource, IMetadata, IThumbnails, IAddedDate,
///
public Dictionary ExternalId { get; set; } = new();
+ ///
+ public DateTime? NextMetadataRefresh { get; set; }
+
public Collection() { }
[JsonConstructor]
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
index 536decaf..f32bf741 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
@@ -31,7 +31,7 @@ namespace Kyoo.Abstractions.Models;
///
/// A class to represent a single show's episode.
///
-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.
public static Sort DefaultSort =>
@@ -166,7 +166,10 @@ public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, IN
public Image? Logo { get; set; }
///
- public Dictionary ExternalId { get; set; } = new();
+ public Dictionary ExternalId { get; set; } = [];
+
+ ///
+ public DateTime? NextMetadataRefresh { get; set; }
///
/// The previous episode that should be seen before viewing this one.
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IRefreshable.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IRefreshable.cs
new file mode 100644
index 00000000..cef2b663
--- /dev/null
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IRefreshable.cs
@@ -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 .
+
+using System;
+
+namespace Kyoo.Abstractions.Models;
+
+public interface IRefreshable
+{
+ ///
+ /// The date of the next metadata refresh. Null if auto-refresh is disabled.
+ ///
+ public DateTime? NextMetadataRefresh { get; set; }
+}
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
index 4e1d0282..18d49946 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
@@ -38,6 +38,7 @@ public class Movie
IMetadata,
IThumbnails,
IAddedDate,
+ IRefreshable,
ILibraryItem,
INews,
IWatchlist
@@ -134,6 +135,9 @@ public class Movie
///
public Dictionary ExternalId { get; set; } = new();
+ ///
+ public DateTime? NextMetadataRefresh { get; set; }
+
///
/// The ID of the Studio that made this show.
///
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs
index 8d3e0489..d94e6514 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs
@@ -31,7 +31,7 @@ namespace Kyoo.Abstractions.Models;
///
/// A season of a .
///
-public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate
+public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, IRefreshable
{
public static Sort DefaultSort => new Sort.By(x => x.SeasonNumber);
@@ -119,6 +119,9 @@ public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate
///
public Dictionary ExternalId { get; set; } = new();
+ ///
+ public DateTime? NextMetadataRefresh { get; set; }
+
///
/// The list of episodes that this season contains.
///
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
index edee8866..b3af184f 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
@@ -39,6 +39,7 @@ public class Show
IOnMerge,
IThumbnails,
IAddedDate,
+ IRefreshable,
ILibraryItem,
IWatchlist
{
@@ -126,6 +127,9 @@ public class Show
///
public Dictionary ExternalId { get; set; } = new();
+ ///
+ public DateTime? NextMetadataRefresh { get; set; }
+
///
/// The ID of the Studio that made this show.
///
diff --git a/back/src/Kyoo.Postgresql/DatabaseContext.cs b/back/src/Kyoo.Postgresql/DatabaseContext.cs
index db5de9c2..6f655a4b 100644
--- a/back/src/Kyoo.Postgresql/DatabaseContext.cs
+++ b/back/src/Kyoo.Postgresql/DatabaseContext.cs
@@ -157,21 +157,11 @@ public abstract class DatabaseContext : DbContext
optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
- private static ValueComparer> _GetComparer()
- {
- return new(
- (c1, c2) => c1!.SequenceEqual(c2!),
- c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode()))
- );
- }
-
- ///
- /// Build the metadata model for the given type.
- ///
- /// The database model builder
- /// The type to add metadata to.
- private static void _HasMetadata(ModelBuilder modelBuilder)
- where T : class, IMetadata
+ private static void _HasJson(
+ ModelBuilder builder,
+ Expression>> property
+ )
+ where T : class
{
// TODO: Waiting for https://github.com/dotnet/efcore/issues/29825
// modelBuilder.Entity()
@@ -179,22 +169,33 @@ public abstract class DatabaseContext : DbContext
// {
// x.ToJson();
// });
- modelBuilder
+ builder
.Entity()
- .Property(x => x.ExternalId)
+ .Property(property)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v =>
- JsonSerializer.Deserialize>(
+ JsonSerializer.Deserialize>(
v,
(JsonSerializerOptions?)null
)!
)
.HasColumnType("json");
- modelBuilder
+ builder
.Entity()
- .Property(x => x.ExternalId)
- .Metadata.SetValueComparer(_GetComparer());
+ .Property(property)
+ .Metadata.SetValueComparer(
+ new ValueComparer>(
+ (c1, c2) => c1!.SequenceEqual(c2!),
+ c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode()))
+ )
+ );
+ }
+
+ private static void _HasMetadata(ModelBuilder modelBuilder)
+ where T : class, IMetadata
+ {
+ _HasJson(modelBuilder, x => x.ExternalId);
}
private static void _HasImages(ModelBuilder modelBuilder)
@@ -215,6 +216,16 @@ public abstract class DatabaseContext : DbContext
.ValueGeneratedOnAdd();
}
+ private static void _HasRefreshDate(ModelBuilder builder)
+ where T : class, IRefreshable
+ {
+ // schedule a refresh soon since metadata can change frequently for recently added items ond online databases
+ builder.Entity()
+ .Property(x => x.NextMetadataRefresh)
+ .HasDefaultValueSql("now() at time zone 'utc' + interval '2 hours'")
+ .ValueGeneratedOnAdd();
+ }
+
///
/// Create a many to many relationship between the two entities.
/// The resulting relationship will have an available method.
@@ -296,8 +307,8 @@ public abstract class DatabaseContext : DbContext
_HasMetadata(modelBuilder);
_HasMetadata(modelBuilder);
_HasMetadata(modelBuilder);
- _HasMetadata(modelBuilder);
_HasMetadata(modelBuilder);
+ _HasJson(modelBuilder, x => x.ExternalId);
_HasImages(modelBuilder);
_HasImages(modelBuilder);
@@ -313,6 +324,12 @@ public abstract class DatabaseContext : DbContext
_HasAddedDate(modelBuilder);
_HasAddedDate(modelBuilder);
+ _HasRefreshDate(modelBuilder);
+ _HasRefreshDate(modelBuilder);
+ _HasRefreshDate(modelBuilder);
+ _HasRefreshDate(modelBuilder);
+ _HasRefreshDate(modelBuilder);
+
modelBuilder
.Entity()
.HasKey(x => new { User = x.UserId, Movie = x.MovieId });
@@ -389,62 +406,9 @@ public abstract class DatabaseContext : DbContext
modelBuilder.Entity().HasKey(x => new { x.Domain, x.Cause });
- // TODO: Waiting for https://github.com/dotnet/efcore/issues/29825
- // modelBuilder.Entity()
- // .OwnsOne(x => x.ExternalId, x =>
- // {
- // x.ToJson();
- // });
- modelBuilder
- .Entity()
- .Property(x => x.Settings)
- .HasConversion(
- v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
- v =>
- JsonSerializer.Deserialize>(
- v,
- (JsonSerializerOptions?)null
- )!
- )
- .HasColumnType("json");
- modelBuilder
- .Entity()
- .Property(x => x.Settings)
- .Metadata.SetValueComparer(_GetComparer());
-
- modelBuilder
- .Entity()
- .Property(x => x.ExternalId)
- .HasConversion(
- v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
- v =>
- JsonSerializer.Deserialize>(
- v,
- (JsonSerializerOptions?)null
- )!
- )
- .HasColumnType("json");
- modelBuilder
- .Entity()
- .Property(x => x.ExternalId)
- .Metadata.SetValueComparer(_GetComparer());
-
- modelBuilder
- .Entity()
- .Property(x => x.Extra)
- .HasConversion(
- v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
- v =>
- JsonSerializer.Deserialize>(
- v,
- (JsonSerializerOptions?)null
- )!
- )
- .HasColumnType("json");
- modelBuilder
- .Entity()
- .Property(x => x.Extra)
- .Metadata.SetValueComparer(_GetComparer