From c6e568692ee14e66e6c844dc1e5481d705a096da Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Fri, 27 Jun 2025 01:50:37 +0200 Subject: [PATCH] Fix modification checks and make sure to use UTC (#14347) --- Emby.Photos/PhotoProvider.cs | 2 +- .../HttpServer/WebSocketConnection.cs | 2 +- .../Images/BaseDynamicImageProvider.cs | 14 +- .../Library/LibraryManager.cs | 28 +- .../Library/ResolverHelper.cs | 2 +- .../Plugins/PluginManager.cs | 2 +- .../Sorting/StartDateComparer.cs | 1 - .../Item/BaseItemRepository.cs | 26 +- .../Migrations/Routines/FixDates.cs | 168 ++ MediaBrowser.Controller/Entities/BaseItem.cs | 15 +- .../Savers/BaseXmlSaver.cs | 4 +- .../Manager/MetadataService.cs | 12 +- .../Manager/ProviderManager.cs | 9 +- .../Providers/BaseNfoProvider.cs | 1 + .../Entities/BaseItemImageInfo.cs | 2 +- .../QueryPartitionHelpers.cs | 8 +- ...mImageInfoDateModifiedNullable.Designer.cs | 1709 +++++++++++++++++ ...2_BaseItemImageInfoDateModifiedNullable.cs | 37 + .../Migrations/JellyfinDbModelSnapshot.cs | 2 +- .../Channels/ChannelManager.cs | 2 +- .../Manager/MetadataServiceTests.cs | 7 +- .../Plugins/PluginManagerTests.cs | 4 +- 22 files changed, 1990 insertions(+), 67 deletions(-) create mode 100644 Jellyfin.Server/Migrations/Routines/FixDates.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.Designer.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index ac6c41ca52..4aca412ba1 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -108,7 +108,7 @@ public class PhotoProvider : ICustomMetadataProvider, IForcedProvider, IH var dateTaken = image.ImageTag.DateTime; if (dateTaken.HasValue) { - item.DateCreated = dateTaken.Value; + item.DateCreated = dateTaken.Value.ToUniversalTime(); item.PremiereDate = dateTaken.Value; item.ProductionYear = dateTaken.Value.Year; } diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index a720c86fb2..373b0994a6 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer RemoteEndPoint = remoteEndPoint; _jsonOptions = JsonDefaults.Options; - LastActivityDate = DateTime.Now; + LastActivityDate = DateTime.UtcNow; } /// diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 8b28691498..4874eca8e6 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -43,13 +43,11 @@ namespace Emby.Server.Implementations.Images protected IImageProcessor ImageProcessor { get; set; } protected virtual IReadOnlyCollection SupportedImages { get; } - = new ImageType[] { ImageType.Primary }; + = [ImageType.Primary]; /// public string Name => "Dynamic Image Provider"; - protected virtual int MaxImageAgeDays => 7; - public int Order => 0; protected virtual bool Supports(BaseItem item) => true; @@ -292,8 +290,14 @@ namespace Emby.Server.Implementations.Images protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image) { - var age = DateTime.UtcNow - image.DateModified; - return age.TotalDays > MaxImageAgeDays; + var path = image.Path; + if (!string.IsNullOrEmpty(path)) + { + var modificationDate = FileSystem.GetLastWriteTimeUtc(path); + return image.DateModified != modificationDate; + } + + return false; } protected string CreateSingleImage(IEnumerable itemsWithImages, string outputPathWithoutExtension, ImageType imageType) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 47d6663a16..6ffe67776b 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2050,13 +2050,17 @@ namespace Emby.Server.Implementations.Library /// public async Task UpdateItemsAsync(IReadOnlyList items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { - _itemRepository.SaveItems(items, cancellationToken); - foreach (var item in items) { + item.DateLastSaved = DateTime.UtcNow; await RunMetadataSavers(item, updateReason).ConfigureAwait(false); + + // Modify again, so saved value is after write time of externally saved metadata + item.DateLastSaved = DateTime.UtcNow; } + _itemRepository.SaveItems(items, cancellationToken); + if (ItemUpdated is not null) { foreach (var item in items) @@ -2097,8 +2101,6 @@ namespace Emby.Server.Implementations.Library await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false); } - item.DateLastSaved = DateTime.UtcNow; - await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); } @@ -2384,12 +2386,13 @@ namespace Emby.Server.Implementations.Library isNew = true; } - var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + var lastRefreshedUtc = item.DateLastRefreshed; + var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc; } if (refresh) @@ -2447,12 +2450,13 @@ namespace Emby.Server.Implementations.Library isNew = true; } - var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + var lastRefreshedUtc = item.DateLastRefreshed; + var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc; } if (refresh) @@ -2522,12 +2526,13 @@ namespace Emby.Server.Implementations.Library item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); } - var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + var lastRefreshedUtc = item.DateLastRefreshed; + var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc; } if (refresh) @@ -2991,13 +2996,12 @@ namespace Emby.Server.Implementations.Library { var path = Person.GetPath(person.Name); var info = Directory.CreateDirectory(path); - var lastWriteTime = info.LastWriteTimeUtc; personEntity = new Person() { Name = person.Name, Id = GetItemByNameId(path), DateCreated = info.CreationTimeUtc, - DateModified = lastWriteTime, + DateModified = info.LastWriteTimeUtc, Path = path }; diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index ab6bc49073..06aa772bd9 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.Library if (fileCreationDate is not null) { var dateCreated = fileCreationDate; - if (dateCreated.Equals(DateTime.MinValue)) + if (dateCreated == DateTime.MinValue) { dateCreated = DateTime.UtcNow; } diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 8eeca3667e..91ccb16ef9 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.Plugins Overview = packageInfo.Overview, Owner = packageInfo.Owner, TargetAbi = versionInfo.TargetAbi ?? string.Empty, - Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture), + Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal), Version = versionInfo.Version, Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state. AutoUpdate = true, diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs index e0b438ef1a..861ca2d3a0 100644 --- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs @@ -5,7 +5,6 @@ using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index e1e7e1a074..52585c996a 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -540,7 +540,7 @@ public sealed class BaseItemRepository } var itemValueMaps = tuples - .Select(e => (Item: e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags))) + .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags))) .ToArray(); var allListedItemValues = itemValueMaps .SelectMany(f => f.Values) @@ -567,7 +567,7 @@ public sealed class BaseItemRepository var itemValuesStore = existingValues.Concat(missingItemValues).ToArray(); var valueMap = itemValueMaps - .Select(f => (Item: f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).ToArray())) + .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).ToArray())) .ToArray(); var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList(); @@ -702,11 +702,11 @@ public sealed class BaseItemRepository dto.ExternalId = entity.ExternalId; dto.Size = entity.Size; dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|'); - dto.DateCreated = entity.DateCreated.GetValueOrDefault(); - dto.DateModified = entity.DateModified.GetValueOrDefault(); + dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); + dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); dto.ChannelId = entity.ChannelId ?? Guid.Empty; - dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault(); - dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault(); + dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); + dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty); dto.Width = entity.Width.GetValueOrDefault(); dto.Height = entity.Height.GetValueOrDefault(); @@ -807,7 +807,7 @@ public sealed class BaseItemRepository if (dto is Folder folder) { - folder.DateLastMediaAdded = entity.DateLastMediaAdded; + folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); } return dto; @@ -867,11 +867,11 @@ public sealed class BaseItemRepository entity.ExternalId = dto.ExternalId; entity.Size = dto.Size; entity.Genres = string.Join('|', dto.Genres); - entity.DateCreated = dto.DateCreated; - entity.DateModified = dto.DateModified; + entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated; + entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified; entity.ChannelId = dto.ChannelId; - entity.DateLastRefreshed = dto.DateLastRefreshed; - entity.DateLastSaved = dto.DateLastSaved; + entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed; + entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved; entity.OwnerId = dto.OwnerId.ToString(); entity.Width = dto.Width; entity.Height = dto.Height; @@ -981,7 +981,7 @@ public sealed class BaseItemRepository if (dto is Folder folder) { - entity.DateLastMediaAdded = folder.DateLastMediaAdded; + entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdded; entity.IsFolder = folder.IsFolder; } @@ -1302,7 +1302,7 @@ public sealed class BaseItemRepository { Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path, BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash), - DateModified = e.DateModified, + DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc), Height = e.Height, Width = e.Width, Type = (ImageType)e.ImageType diff --git a/Jellyfin.Server/Migrations/Routines/FixDates.cs b/Jellyfin.Server/Migrations/Routines/FixDates.cs new file mode 100644 index 0000000000..f112502b9f --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/FixDates.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Database.Implementations; +using Jellyfin.Server.ServerSetupApp; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// +/// Migration to fix dates saved in the database to always be UTC. +/// +[JellyfinMigration("2025-06-20T18:00:00", nameof(FixDates))] +public class FixDates : IAsyncMigrationRoutine +{ + private const int PageSize = 5000; + + private readonly ILogger _logger; + private readonly IDbContextFactory _dbProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The startup logger for Startup UI integration. + /// Instance of the interface. + public FixDates( + ILogger logger, + IStartupLogger startupLogger, + IDbContextFactory dbProvider) + { + _logger = startupLogger.With(logger); + _dbProvider = dbProvider; + } + + /// + public async Task PerformAsync(CancellationToken cancellationToken) + { + if (!TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc)) + { + using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + var sw = Stopwatch.StartNew(); + + await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false); + sw.Reset(); + await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false); + sw.Reset(); + await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false); + } + } + + private async Task FixBaseItemsAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken) + { + int itemCount = 0; + + var baseQuery = context.BaseItems.OrderBy(e => e.Id); + var records = baseQuery.Count(); + _logger.LogInformation("Fixing dates for {Count} BaseItems.", records); + + sw.Start(); + await foreach (var result in context.BaseItems.OrderBy(e => e.Id) + .WithPartitionProgress( + (partition) => + _logger.LogInformation( + "Processing BaseItems batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}", + partition + 1, + Math.Min((partition + 1) * PageSize, records), + records, + sw.Elapsed)) + .PartitionEagerAsync(PageSize, cancellationToken) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + result.DateCreated = ToUniversalTime(result.DateCreated); + result.DateLastMediaAdded = ToUniversalTime(result.DateLastMediaAdded); + result.DateLastRefreshed = ToUniversalTime(result.DateLastRefreshed); + result.DateLastSaved = ToUniversalTime(result.DateLastSaved); + result.DateModified = ToUniversalTime(result.DateModified); + itemCount++; + } + + var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + _logger.LogInformation("BaseItems: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed); + } + + private async Task FixChaptersAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken) + { + int itemCount = 0; + + var baseQuery = context.Chapters; + var records = baseQuery.Count(); + _logger.LogInformation("Fixing dates for {Count} Chapters.", records); + + sw.Start(); + await foreach (var result in context.Chapters.OrderBy(e => e.ItemId) + .WithPartitionProgress( + (partition) => + _logger.LogInformation( + "Processing Chapter batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}", + partition + 1, + Math.Min((partition + 1) * PageSize, records), + records, + sw.Elapsed)) + .PartitionEagerAsync(PageSize, cancellationToken) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + result.ImageDateModified = ToUniversalTime(result.ImageDateModified, true); + itemCount++; + } + + var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Chapters: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed); + } + + private async Task FixBaseItemImageInfos(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken) + { + int itemCount = 0; + + var baseQuery = context.BaseItemImageInfos; + var records = baseQuery.Count(); + _logger.LogInformation("Fixing dates for {Count} BaseItemImageInfos.", records); + + sw.Start(); + await foreach (var result in context.BaseItemImageInfos.OrderBy(e => e.Id) + .WithPartitionProgress( + (partition) => + _logger.LogInformation( + "Processing BaseItemImageInfos batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}", + partition + 1, + Math.Min((partition + 1) * PageSize, records), + records, + sw.Elapsed)) + .PartitionEagerAsync(PageSize, cancellationToken) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + result.DateModified = ToUniversalTime(result.DateModified); + itemCount++; + } + + var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + _logger.LogInformation("BaseItemImageInfos: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed); + } + + private DateTime? ToUniversalTime(DateTime? dateTime, bool isUTC = false) + { + if (dateTime is null) + { + return null; + } + + if (dateTime.Value.Year == 1 && dateTime.Value.Month == 1 && dateTime.Value.Day == 1) + { + return null; + } + + if (dateTime.Value.Kind == DateTimeKind.Utc || isUTC) + { + return dateTime.Value; + } + + return dateTime.Value.ToUniversalTime(); + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 96984758f8..4efa3f410d 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1423,23 +1423,16 @@ namespace MediaBrowser.Controller.Entities public virtual bool RequiresRefresh() { - if (string.IsNullOrEmpty(Path) || DateModified == default) + if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue) { return false; } var info = FileSystem.GetFileSystemInfo(Path); - if (info.Exists) - { - if (info.IsDirectory) - { - return info.LastWriteTimeUtc != DateModified; - } - return info.LastWriteTimeUtc != DateModified; - } - - return false; + return info.Exists + ? info.LastWriteTimeUtc != DateModified + : false; } public virtual List GetUserDataKeys() diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index c25adb7746..025a815247 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -235,11 +235,11 @@ namespace MediaBrowser.LocalMetadata.Savers { if (item is Person) { - await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false); + await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false); } else if (item is not Episode) { - await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false); + await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false); } } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index cd95bf6ef1..5c855328b8 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -73,11 +73,11 @@ namespace MediaBrowser.Providers.Manager public virtual int Order => 0; - private FileSystemMetadata TryGetFile(string path, IDirectoryService directoryService) + private FileSystemMetadata TryGetFileSystemMetadata(string path, IDirectoryService directoryService) { try { - return directoryService.GetFile(path); + return directoryService.GetFileSystemEntry(path); } catch (Exception ex) { @@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.Manager var updateType = ItemUpdateType.None; var libraryOptions = LibraryManager.GetLibraryOptions(item); - var isFirstRefresh = item.DateLastRefreshed.Date == DateTime.MinValue.Date; + var isFirstRefresh = item.DateLastRefreshed == DateTime.MinValue; var hasRefreshedMetadata = true; var hasRefreshedImages = true; @@ -225,7 +225,7 @@ namespace MediaBrowser.Providers.Manager { if (item.IsFileProtocol) { - var file = TryGetFile(item.Path, refreshOptions.DirectoryService); + var file = TryGetFileSystemMetadata(item.Path, refreshOptions.DirectoryService); if (file is not null) { item.DateModified = file.LastWriteTimeUtc; @@ -1180,12 +1180,12 @@ namespace MediaBrowser.Providers.Manager target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray(); } - if (source.DateCreated != default) + if (source.DateCreated != DateTime.MinValue) { target.DateCreated = source.DateCreated; } - if (replaceData || source.DateModified != default) + if (replaceData || source.DateModified != DateTime.MinValue) { target.DateModified = source.DateModified; } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 1a29548f24..87ca728d38 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -669,8 +669,13 @@ namespace MediaBrowser.Providers.Manager private async Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType, IEnumerable savers) { var libraryOptions = _libraryManager.GetLibraryOptions(item); + var applicableSavers = savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, updateType, false)).ToList(); + if (applicableSavers.Count == 0) + { + return; + } - foreach (var saver in savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, updateType, false))) + foreach (var saver in applicableSavers) { _logger.LogDebug("Saving {Item} to {Saver}", item.Path ?? item.Name, saver.Name); @@ -714,6 +719,8 @@ namespace MediaBrowser.Providers.Manager } } } + + _libraryManager.CreateItem(item, null); } /// diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs index 22c065b5d4..c671e7a932 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using System.IO; using System.Threading; using System.Threading.Tasks; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs index 71d60fc25d..cd14764e49 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs @@ -22,7 +22,7 @@ public class BaseItemImageInfo /// /// Gets or Sets the time the image was last modified. /// - public DateTime DateModified { get; set; } + public DateTime? DateModified { get; set; } /// /// Gets or Sets the imagetype. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs index bb66bddca3..c20dfeeb5a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs @@ -82,7 +82,7 @@ public static class QueryPartitionHelpers /// The entity to load. /// The source query. /// The number of elements to load per partition. - /// The cancelation token. + /// The cancellation token. /// A enumerable representing the whole of the query. public static async IAsyncEnumerable PartitionAsync(this ProgressablePartitionReporting partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -98,7 +98,7 @@ public static class QueryPartitionHelpers /// The entity to load. /// The source query. /// The number of elements to load per partition. - /// The cancelation token. + /// The cancellation token. /// A enumerable representing the whole of the query. public static async IAsyncEnumerable PartitionEagerAsync(this ProgressablePartitionReporting partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -115,7 +115,7 @@ public static class QueryPartitionHelpers /// The source query. /// The number of elements to load per partition. /// Reporting helper. - /// The cancelation token. + /// The cancellation token. /// A enumerable representing the whole of the query. public static async IAsyncEnumerable PartitionAsync( this IOrderedQueryable query, @@ -154,7 +154,7 @@ public static class QueryPartitionHelpers /// The source query. /// The number of elements to load per partition. /// Reporting helper. - /// The cancelation token. + /// The cancellation token. /// A enumerable representing the whole of the query. public static async IAsyncEnumerable PartitionEagerAsync( this IOrderedQueryable query, diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.Designer.cs new file mode 100644 index 0000000000..a0622c14d5 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.Designer.cs @@ -0,0 +1,1709 @@ +// +using System; +using Jellyfin.Database.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20250622170802_BaseItemImageInfoDateModifiedNullable")] + partial class BaseItemImageInfoDateModifiedNullable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.6"); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingSubValue") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0000-000000000001"), + IsFolder = false, + IsInMixedFolder = false, + IsLocked = false, + IsMovie = false, + IsRepeat = false, + IsSeries = false, + IsVirtualItem = false, + Name = "This is a placeholder item for UserData that has been detacted from its original item", + Type = "PLACEHOLDER" + }); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue"); + + b.HasIndex("Type", "Value") + .IsUnique(); + + b.ToTable("ItemValues"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("KeyframeTicks") + .HasColumnType("TEXT"); + + b.Property("TotalDuration") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId"); + + b.ToTable("KeyframeData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Hdr10PlusPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingScore") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingSubScore") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("RetentionDate") + .HasColumnType("TEXT"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Parents") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem") + .WithMany("Children") + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Navigation("Chapters"); + + b.Navigation("Children"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("Parents"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs new file mode 100644 index 0000000000..bce6029d5b --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class BaseItemImageInfoDateModifiedNullable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "DateModified", + table: "BaseItemImageInfos", + type: "TEXT", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "TEXT"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "DateModified", + table: "BaseItemImageInfos", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + oldClrType: typeof(DateTime), + oldType: "TEXT", + oldNullable: true); + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs index 706215eeff..fe7870ed37 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs @@ -418,7 +418,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("Blurhash") .HasColumnType("BLOB"); - b.Property("DateModified") + b.Property("DateModified") .HasColumnType("TEXT"); b.Property("Height") diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs index 74182171f5..8ee129a575 100644 --- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs @@ -1166,7 +1166,7 @@ namespace Jellyfin.LiveTv.Channels } } - if (isNew || forceUpdate || item.DateLastRefreshed == default) + if (isNew || forceUpdate || item.DateLastRefreshed == DateTime.MinValue) { _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } diff --git a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs index b32ecf6ec4..b5585f4fd2 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs @@ -22,7 +22,7 @@ namespace Jellyfin.Providers.Tests.Manager { var newLocked = new[] { MetadataField.Genres, MetadataField.Cast }; var newString = "new"; - var newDate = DateTime.Now; + var newDate = DateTime.UtcNow; var oldLocked = new[] { MetadataField.Genres }; var oldString = "old"; @@ -39,6 +39,7 @@ namespace Jellyfin.Providers.Tests.Manager DateCreated = newDate } }; + if (defaultDate) { source.Item.DateCreated = default; @@ -141,8 +142,8 @@ namespace Jellyfin.Providers.Tests.Manager { "ProductionYear", 1, 2 }, { "CommunityRating", 1.0f, 2.0f }, { "CriticRating", 1.0f, 2.0f }, - { "EndDate", DateTime.UnixEpoch, DateTime.Now }, - { "PremiereDate", DateTime.UnixEpoch, DateTime.Now }, + { "EndDate", DateTime.UnixEpoch, DateTime.UtcNow }, + { "PremiereDate", DateTime.UnixEpoch, DateTime.UtcNow }, { "Video3DFormat", Video3DFormat.HalfSideBySide, Video3DFormat.FullSideBySide } }; diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs index b289c763b5..6964c920bc 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs @@ -185,7 +185,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins Description = packageInfo.Description, Overview = packageInfo.Overview, TargetAbi = packageInfo.Versions[0].TargetAbi!, - Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture), + Timestamp = DateTimeOffset.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture).UtcDateTime, Changelog = packageInfo.Versions[0].Changelog!, Version = new Version(1, 0).ToString(), ImagePath = string.Empty @@ -221,7 +221,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins Description = packageInfo.Description, Overview = packageInfo.Overview, TargetAbi = packageInfo.Versions[0].TargetAbi!, - Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture), + Timestamp = DateTimeOffset.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture).UtcDateTime, Changelog = packageInfo.Versions[0].Changelog!, Version = packageInfo.Versions[0].Version, ImagePath = string.Empty