Fix modification checks and make sure to use UTC (#14347)

This commit is contained in:
Tim Eisele 2025-06-27 01:50:37 +02:00 committed by GitHub
parent d5a76bdff8
commit c6e568692e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1990 additions and 67 deletions

View file

@ -108,7 +108,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
var dateTaken = image.ImageTag.DateTime; var dateTaken = image.ImageTag.DateTime;
if (dateTaken.HasValue) if (dateTaken.HasValue)
{ {
item.DateCreated = dateTaken.Value; item.DateCreated = dateTaken.Value.ToUniversalTime();
item.PremiereDate = dateTaken.Value; item.PremiereDate = dateTaken.Value;
item.ProductionYear = dateTaken.Value.Year; item.ProductionYear = dateTaken.Value.Year;
} }

View file

@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer
RemoteEndPoint = remoteEndPoint; RemoteEndPoint = remoteEndPoint;
_jsonOptions = JsonDefaults.Options; _jsonOptions = JsonDefaults.Options;
LastActivityDate = DateTime.Now; LastActivityDate = DateTime.UtcNow;
} }
/// <inheritdoc /> /// <inheritdoc />

View file

@ -43,13 +43,11 @@ namespace Emby.Server.Implementations.Images
protected IImageProcessor ImageProcessor { get; set; } protected IImageProcessor ImageProcessor { get; set; }
protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; } protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
= new ImageType[] { ImageType.Primary }; = [ImageType.Primary];
/// <inheritdoc /> /// <inheritdoc />
public string Name => "Dynamic Image Provider"; public string Name => "Dynamic Image Provider";
protected virtual int MaxImageAgeDays => 7;
public int Order => 0; public int Order => 0;
protected virtual bool Supports(BaseItem item) => true; protected virtual bool Supports(BaseItem item) => true;
@ -292,8 +290,14 @@ namespace Emby.Server.Implementations.Images
protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image) protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image)
{ {
var age = DateTime.UtcNow - image.DateModified; var path = image.Path;
return age.TotalDays > MaxImageAgeDays; if (!string.IsNullOrEmpty(path))
{
var modificationDate = FileSystem.GetLastWriteTimeUtc(path);
return image.DateModified != modificationDate;
}
return false;
} }
protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType) protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)

View file

@ -2050,13 +2050,17 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc /> /// <inheritdoc />
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{ {
_itemRepository.SaveItems(items, cancellationToken);
foreach (var item in items) foreach (var item in items)
{ {
item.DateLastSaved = DateTime.UtcNow;
await RunMetadataSavers(item, updateReason).ConfigureAwait(false); 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) if (ItemUpdated is not null)
{ {
foreach (var item in items) foreach (var item in items)
@ -2097,8 +2101,6 @@ namespace Emby.Server.Implementations.Library
await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false); await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
} }
item.DateLastSaved = DateTime.UtcNow;
await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
} }
@ -2384,12 +2386,13 @@ namespace Emby.Server.Implementations.Library
isNew = true; 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()) if (!refresh && !item.DisplayParentId.IsEmpty())
{ {
var displayParent = GetItemById(item.DisplayParentId); var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
} }
if (refresh) if (refresh)
@ -2447,12 +2450,13 @@ namespace Emby.Server.Implementations.Library
isNew = true; 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()) if (!refresh && !item.DisplayParentId.IsEmpty())
{ {
var displayParent = GetItemById(item.DisplayParentId); var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
} }
if (refresh) if (refresh)
@ -2522,12 +2526,13 @@ namespace Emby.Server.Implementations.Library
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); 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()) if (!refresh && !item.DisplayParentId.IsEmpty())
{ {
var displayParent = GetItemById(item.DisplayParentId); var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
} }
if (refresh) if (refresh)
@ -2991,13 +2996,12 @@ namespace Emby.Server.Implementations.Library
{ {
var path = Person.GetPath(person.Name); var path = Person.GetPath(person.Name);
var info = Directory.CreateDirectory(path); var info = Directory.CreateDirectory(path);
var lastWriteTime = info.LastWriteTimeUtc;
personEntity = new Person() personEntity = new Person()
{ {
Name = person.Name, Name = person.Name,
Id = GetItemByNameId<Person>(path), Id = GetItemByNameId<Person>(path),
DateCreated = info.CreationTimeUtc, DateCreated = info.CreationTimeUtc,
DateModified = lastWriteTime, DateModified = info.LastWriteTimeUtc,
Path = path Path = path
}; };

View file

@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.Library
if (fileCreationDate is not null) if (fileCreationDate is not null)
{ {
var dateCreated = fileCreationDate; var dateCreated = fileCreationDate;
if (dateCreated.Equals(DateTime.MinValue)) if (dateCreated == DateTime.MinValue)
{ {
dateCreated = DateTime.UtcNow; dateCreated = DateTime.UtcNow;
} }

View file

@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.Plugins
Overview = packageInfo.Overview, Overview = packageInfo.Overview,
Owner = packageInfo.Owner, Owner = packageInfo.Owner,
TargetAbi = versionInfo.TargetAbi ?? string.Empty, 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, Version = versionInfo.Version,
Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state. Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
AutoUpdate = true, AutoUpdate = true,

View file

@ -5,7 +5,6 @@ using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting namespace Emby.Server.Implementations.Sorting
{ {

View file

@ -540,7 +540,7 @@ public sealed class BaseItemRepository
} }
var itemValueMaps = tuples 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(); .ToArray();
var allListedItemValues = itemValueMaps var allListedItemValues = itemValueMaps
.SelectMany(f => f.Values) .SelectMany(f => f.Values)
@ -567,7 +567,7 @@ public sealed class BaseItemRepository
var itemValuesStore = existingValues.Concat(missingItemValues).ToArray(); var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
var valueMap = itemValueMaps 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(); .ToArray();
var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList(); var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
@ -702,11 +702,11 @@ public sealed class BaseItemRepository
dto.ExternalId = entity.ExternalId; dto.ExternalId = entity.ExternalId;
dto.Size = entity.Size; dto.Size = entity.Size;
dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|'); dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
dto.DateCreated = entity.DateCreated.GetValueOrDefault(); dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
dto.DateModified = entity.DateModified.GetValueOrDefault(); dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
dto.ChannelId = entity.ChannelId ?? Guid.Empty; dto.ChannelId = entity.ChannelId ?? Guid.Empty;
dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault(); dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault(); 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.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
dto.Width = entity.Width.GetValueOrDefault(); dto.Width = entity.Width.GetValueOrDefault();
dto.Height = entity.Height.GetValueOrDefault(); dto.Height = entity.Height.GetValueOrDefault();
@ -807,7 +807,7 @@ public sealed class BaseItemRepository
if (dto is Folder folder) if (dto is Folder folder)
{ {
folder.DateLastMediaAdded = entity.DateLastMediaAdded; folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
} }
return dto; return dto;
@ -867,11 +867,11 @@ public sealed class BaseItemRepository
entity.ExternalId = dto.ExternalId; entity.ExternalId = dto.ExternalId;
entity.Size = dto.Size; entity.Size = dto.Size;
entity.Genres = string.Join('|', dto.Genres); entity.Genres = string.Join('|', dto.Genres);
entity.DateCreated = dto.DateCreated; entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
entity.DateModified = dto.DateModified; entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
entity.ChannelId = dto.ChannelId; entity.ChannelId = dto.ChannelId;
entity.DateLastRefreshed = dto.DateLastRefreshed; entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
entity.DateLastSaved = dto.DateLastSaved; entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
entity.OwnerId = dto.OwnerId.ToString(); entity.OwnerId = dto.OwnerId.ToString();
entity.Width = dto.Width; entity.Width = dto.Width;
entity.Height = dto.Height; entity.Height = dto.Height;
@ -981,7 +981,7 @@ public sealed class BaseItemRepository
if (dto is Folder folder) if (dto is Folder folder)
{ {
entity.DateLastMediaAdded = folder.DateLastMediaAdded; entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdded;
entity.IsFolder = folder.IsFolder; entity.IsFolder = folder.IsFolder;
} }
@ -1302,7 +1302,7 @@ public sealed class BaseItemRepository
{ {
Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path, Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash), 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, Height = e.Height,
Width = e.Width, Width = e.Width,
Type = (ImageType)e.ImageType Type = (ImageType)e.ImageType

View file

@ -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;
/// <summary>
/// Migration to fix dates saved in the database to always be UTC.
/// </summary>
[JellyfinMigration("2025-06-20T18:00:00", nameof(FixDates))]
public class FixDates : IAsyncMigrationRoutine
{
private const int PageSize = 5000;
private readonly ILogger _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
/// <summary>
/// Initializes a new instance of the <see cref="FixDates"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="startupLogger">The startup logger for Startup UI integration.</param>
/// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
public FixDates(
ILogger<FixDates> logger,
IStartupLogger<FixDates> startupLogger,
IDbContextFactory<JellyfinDbContext> dbProvider)
{
_logger = startupLogger.With(logger);
_dbProvider = dbProvider;
}
/// <inheritdoc />
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();
}
}

View file

@ -1423,23 +1423,16 @@ namespace MediaBrowser.Controller.Entities
public virtual bool RequiresRefresh() public virtual bool RequiresRefresh()
{ {
if (string.IsNullOrEmpty(Path) || DateModified == default) if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
{ {
return false; return false;
} }
var info = FileSystem.GetFileSystemInfo(Path); var info = FileSystem.GetFileSystemInfo(Path);
if (info.Exists)
{
if (info.IsDirectory)
{
return info.LastWriteTimeUtc != DateModified;
}
return info.LastWriteTimeUtc != DateModified; return info.Exists
} ? info.LastWriteTimeUtc != DateModified
: false;
return false;
} }
public virtual List<string> GetUserDataKeys() public virtual List<string> GetUserDataKeys()

View file

@ -235,11 +235,11 @@ namespace MediaBrowser.LocalMetadata.Savers
{ {
if (item is Person) 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) 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);
} }
} }

View file

@ -73,11 +73,11 @@ namespace MediaBrowser.Providers.Manager
public virtual int Order => 0; public virtual int Order => 0;
private FileSystemMetadata TryGetFile(string path, IDirectoryService directoryService) private FileSystemMetadata TryGetFileSystemMetadata(string path, IDirectoryService directoryService)
{ {
try try
{ {
return directoryService.GetFile(path); return directoryService.GetFileSystemEntry(path);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.Manager
var updateType = ItemUpdateType.None; var updateType = ItemUpdateType.None;
var libraryOptions = LibraryManager.GetLibraryOptions(item); var libraryOptions = LibraryManager.GetLibraryOptions(item);
var isFirstRefresh = item.DateLastRefreshed.Date == DateTime.MinValue.Date; var isFirstRefresh = item.DateLastRefreshed == DateTime.MinValue;
var hasRefreshedMetadata = true; var hasRefreshedMetadata = true;
var hasRefreshedImages = true; var hasRefreshedImages = true;
@ -225,7 +225,7 @@ namespace MediaBrowser.Providers.Manager
{ {
if (item.IsFileProtocol) if (item.IsFileProtocol)
{ {
var file = TryGetFile(item.Path, refreshOptions.DirectoryService); var file = TryGetFileSystemMetadata(item.Path, refreshOptions.DirectoryService);
if (file is not null) if (file is not null)
{ {
item.DateModified = file.LastWriteTimeUtc; item.DateModified = file.LastWriteTimeUtc;
@ -1180,12 +1180,12 @@ namespace MediaBrowser.Providers.Manager
target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray(); target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
} }
if (source.DateCreated != default) if (source.DateCreated != DateTime.MinValue)
{ {
target.DateCreated = source.DateCreated; target.DateCreated = source.DateCreated;
} }
if (replaceData || source.DateModified != default) if (replaceData || source.DateModified != DateTime.MinValue)
{ {
target.DateModified = source.DateModified; target.DateModified = source.DateModified;
} }

View file

@ -669,8 +669,13 @@ namespace MediaBrowser.Providers.Manager
private async Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers) private async Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers)
{ {
var libraryOptions = _libraryManager.GetLibraryOptions(item); 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); _logger.LogDebug("Saving {Item} to {Saver}", item.Path ?? item.Name, saver.Name);
@ -714,6 +719,8 @@ namespace MediaBrowser.Providers.Manager
} }
} }
} }
_libraryManager.CreateItem(item, null);
} }
/// <summary> /// <summary>

View file

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;

View file

@ -22,7 +22,7 @@ public class BaseItemImageInfo
/// <summary> /// <summary>
/// Gets or Sets the time the image was last modified. /// Gets or Sets the time the image was last modified.
/// </summary> /// </summary>
public DateTime DateModified { get; set; } public DateTime? DateModified { get; set; }
/// <summary> /// <summary>
/// Gets or Sets the imagetype. /// Gets or Sets the imagetype.

View file

@ -82,7 +82,7 @@ public static class QueryPartitionHelpers
/// <typeparam name="TEntity">The entity to load.</typeparam> /// <typeparam name="TEntity">The entity to load.</typeparam>
/// <param name="partitionInfo">The source query.</param> /// <param name="partitionInfo">The source query.</param>
/// <param name="partitionSize">The number of elements to load per partition.</param> /// <param name="partitionSize">The number of elements to load per partition.</param>
/// <param name="cancellationToken">The cancelation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A enumerable representing the whole of the query.</returns> /// <returns>A enumerable representing the whole of the query.</returns>
public static async IAsyncEnumerable<TEntity> PartitionAsync<TEntity>(this ProgressablePartitionReporting<TEntity> partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default) public static async IAsyncEnumerable<TEntity> PartitionAsync<TEntity>(this ProgressablePartitionReporting<TEntity> partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{ {
@ -98,7 +98,7 @@ public static class QueryPartitionHelpers
/// <typeparam name="TEntity">The entity to load.</typeparam> /// <typeparam name="TEntity">The entity to load.</typeparam>
/// <param name="partitionInfo">The source query.</param> /// <param name="partitionInfo">The source query.</param>
/// <param name="partitionSize">The number of elements to load per partition.</param> /// <param name="partitionSize">The number of elements to load per partition.</param>
/// <param name="cancellationToken">The cancelation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A enumerable representing the whole of the query.</returns> /// <returns>A enumerable representing the whole of the query.</returns>
public static async IAsyncEnumerable<TEntity> PartitionEagerAsync<TEntity>(this ProgressablePartitionReporting<TEntity> partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default) public static async IAsyncEnumerable<TEntity> PartitionEagerAsync<TEntity>(this ProgressablePartitionReporting<TEntity> partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{ {
@ -115,7 +115,7 @@ public static class QueryPartitionHelpers
/// <param name="query">The source query.</param> /// <param name="query">The source query.</param>
/// <param name="partitionSize">The number of elements to load per partition.</param> /// <param name="partitionSize">The number of elements to load per partition.</param>
/// <param name="progressablePartition">Reporting helper.</param> /// <param name="progressablePartition">Reporting helper.</param>
/// <param name="cancellationToken">The cancelation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A enumerable representing the whole of the query.</returns> /// <returns>A enumerable representing the whole of the query.</returns>
public static async IAsyncEnumerable<TEntity> PartitionAsync<TEntity>( public static async IAsyncEnumerable<TEntity> PartitionAsync<TEntity>(
this IOrderedQueryable<TEntity> query, this IOrderedQueryable<TEntity> query,
@ -154,7 +154,7 @@ public static class QueryPartitionHelpers
/// <param name="query">The source query.</param> /// <param name="query">The source query.</param>
/// <param name="partitionSize">The number of elements to load per partition.</param> /// <param name="partitionSize">The number of elements to load per partition.</param>
/// <param name="progressablePartition">Reporting helper.</param> /// <param name="progressablePartition">Reporting helper.</param>
/// <param name="cancellationToken">The cancelation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A enumerable representing the whole of the query.</returns> /// <returns>A enumerable representing the whole of the query.</returns>
public static async IAsyncEnumerable<TEntity> PartitionEagerAsync<TEntity>( public static async IAsyncEnumerable<TEntity> PartitionEagerAsync<TEntity>(
this IOrderedQueryable<TEntity> query, this IOrderedQueryable<TEntity> query,

View file

@ -0,0 +1,37 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class BaseItemImageInfoDateModifiedNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "DateModified",
table: "BaseItemImageInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "TEXT");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
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);
}
}
}

View file

@ -418,7 +418,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<byte[]>("Blurhash") b.Property<byte[]>("Blurhash")
.HasColumnType("BLOB"); .HasColumnType("BLOB");
b.Property<DateTime>("DateModified") b.Property<DateTime?>("DateModified")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("Height") b.Property<int>("Height")

View file

@ -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); _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
} }

View file

@ -22,7 +22,7 @@ namespace Jellyfin.Providers.Tests.Manager
{ {
var newLocked = new[] { MetadataField.Genres, MetadataField.Cast }; var newLocked = new[] { MetadataField.Genres, MetadataField.Cast };
var newString = "new"; var newString = "new";
var newDate = DateTime.Now; var newDate = DateTime.UtcNow;
var oldLocked = new[] { MetadataField.Genres }; var oldLocked = new[] { MetadataField.Genres };
var oldString = "old"; var oldString = "old";
@ -39,6 +39,7 @@ namespace Jellyfin.Providers.Tests.Manager
DateCreated = newDate DateCreated = newDate
} }
}; };
if (defaultDate) if (defaultDate)
{ {
source.Item.DateCreated = default; source.Item.DateCreated = default;
@ -141,8 +142,8 @@ namespace Jellyfin.Providers.Tests.Manager
{ "ProductionYear", 1, 2 }, { "ProductionYear", 1, 2 },
{ "CommunityRating", 1.0f, 2.0f }, { "CommunityRating", 1.0f, 2.0f },
{ "CriticRating", 1.0f, 2.0f }, { "CriticRating", 1.0f, 2.0f },
{ "EndDate", DateTime.UnixEpoch, DateTime.Now }, { "EndDate", DateTime.UnixEpoch, DateTime.UtcNow },
{ "PremiereDate", DateTime.UnixEpoch, DateTime.Now }, { "PremiereDate", DateTime.UnixEpoch, DateTime.UtcNow },
{ "Video3DFormat", Video3DFormat.HalfSideBySide, Video3DFormat.FullSideBySide } { "Video3DFormat", Video3DFormat.HalfSideBySide, Video3DFormat.FullSideBySide }
}; };

View file

@ -185,7 +185,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
Description = packageInfo.Description, Description = packageInfo.Description,
Overview = packageInfo.Overview, Overview = packageInfo.Overview,
TargetAbi = packageInfo.Versions[0].TargetAbi!, 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!, Changelog = packageInfo.Versions[0].Changelog!,
Version = new Version(1, 0).ToString(), Version = new Version(1, 0).ToString(),
ImagePath = string.Empty ImagePath = string.Empty
@ -221,7 +221,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
Description = packageInfo.Description, Description = packageInfo.Description,
Overview = packageInfo.Overview, Overview = packageInfo.Overview,
TargetAbi = packageInfo.Versions[0].TargetAbi!, 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!, Changelog = packageInfo.Versions[0].Changelog!,
Version = packageInfo.Versions[0].Version, Version = packageInfo.Versions[0].Version,
ImagePath = string.Empty ImagePath = string.Empty