< Summary - Jellyfin

Information
Class: Jellyfin.Server.Migrations.Routines.FixDates
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/FixDates.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 10
Coverable lines: 10
Total lines: 168
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 10
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
ToUniversalTime(...)0%110100%

File(s)

/srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/FixDates.cs

#LineLine coverage
 1using System;
 2using System.Diagnostics;
 3using System.Linq;
 4using System.Threading;
 5using System.Threading.Tasks;
 6using Jellyfin.Database.Implementations;
 7using Jellyfin.Server.ServerSetupApp;
 8using Microsoft.EntityFrameworkCore;
 9using Microsoft.Extensions.Logging;
 10
 11namespace Jellyfin.Server.Migrations.Routines;
 12
 13/// <summary>
 14/// Migration to fix dates saved in the database to always be UTC.
 15/// </summary>
 16[JellyfinMigration("2025-06-20T18:00:00", nameof(FixDates))]
 17public class FixDates : IAsyncMigrationRoutine
 18{
 19    private const int PageSize = 5000;
 20
 21    private readonly ILogger _logger;
 22    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 23
 24    /// <summary>
 25    /// Initializes a new instance of the <see cref="FixDates"/> class.
 26    /// </summary>
 27    /// <param name="logger">The logger.</param>
 28    /// <param name="startupLogger">The startup logger for Startup UI integration.</param>
 29    /// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
 30    public FixDates(
 31        ILogger<FixDates> logger,
 32        IStartupLogger<FixDates> startupLogger,
 33        IDbContextFactory<JellyfinDbContext> dbProvider)
 34    {
 035        _logger = startupLogger.With(logger);
 036        _dbProvider = dbProvider;
 037    }
 38
 39    /// <inheritdoc />
 40    public async Task PerformAsync(CancellationToken cancellationToken)
 41    {
 42        if (!TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc))
 43        {
 44            using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 45            var sw = Stopwatch.StartNew();
 46
 47            await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false);
 48            sw.Reset();
 49            await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false);
 50            sw.Reset();
 51            await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false);
 52        }
 53    }
 54
 55    private async Task FixBaseItemsAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
 56    {
 57        int itemCount = 0;
 58
 59        var baseQuery = context.BaseItems.OrderBy(e => e.Id);
 60        var records = baseQuery.Count();
 61        _logger.LogInformation("Fixing dates for {Count} BaseItems.", records);
 62
 63        sw.Start();
 64        await foreach (var result in context.BaseItems.OrderBy(e => e.Id)
 65                        .WithPartitionProgress(
 66                            (partition) =>
 67                                _logger.LogInformation(
 68                                    "Processing BaseItems batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: 
 69                                    partition + 1,
 70                                    Math.Min((partition + 1) * PageSize, records),
 71                                    records,
 72                                    sw.Elapsed))
 73                        .PartitionEagerAsync(PageSize, cancellationToken)
 74                        .WithCancellation(cancellationToken)
 75                        .ConfigureAwait(false))
 76        {
 77            result.DateCreated = ToUniversalTime(result.DateCreated);
 78            result.DateLastMediaAdded = ToUniversalTime(result.DateLastMediaAdded);
 79            result.DateLastRefreshed = ToUniversalTime(result.DateLastRefreshed);
 80            result.DateLastSaved = ToUniversalTime(result.DateLastSaved);
 81            result.DateModified = ToUniversalTime(result.DateModified);
 82            itemCount++;
 83        }
 84
 85        var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 86        _logger.LogInformation("BaseItems: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", ite
 87    }
 88
 89    private async Task FixChaptersAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
 90    {
 91        int itemCount = 0;
 92
 93        var baseQuery = context.Chapters;
 94        var records = baseQuery.Count();
 95        _logger.LogInformation("Fixing dates for {Count} Chapters.", records);
 96
 97        sw.Start();
 98        await foreach (var result in context.Chapters.OrderBy(e => e.ItemId)
 99                        .WithPartitionProgress(
 100                            (partition) =>
 101                                _logger.LogInformation(
 102                                    "Processing Chapter batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {E
 103                                    partition + 1,
 104                                    Math.Min((partition + 1) * PageSize, records),
 105                                    records,
 106                                    sw.Elapsed))
 107                        .PartitionEagerAsync(PageSize, cancellationToken)
 108                        .WithCancellation(cancellationToken)
 109                        .ConfigureAwait(false))
 110        {
 111            result.ImageDateModified = ToUniversalTime(result.ImageDateModified, true);
 112            itemCount++;
 113        }
 114
 115        var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 116        _logger.LogInformation("Chapters: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", item
 117    }
 118
 119    private async Task FixBaseItemImageInfos(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToke
 120    {
 121        int itemCount = 0;
 122
 123        var baseQuery = context.BaseItemImageInfos;
 124        var records = baseQuery.Count();
 125        _logger.LogInformation("Fixing dates for {Count} BaseItemImageInfos.", records);
 126
 127        sw.Start();
 128        await foreach (var result in context.BaseItemImageInfos.OrderBy(e => e.Id)
 129                        .WithPartitionProgress(
 130                            (partition) =>
 131                                _logger.LogInformation(
 132                                    "Processing BaseItemImageInfos batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords})
 133                                    partition + 1,
 134                                    Math.Min((partition + 1) * PageSize, records),
 135                                    records,
 136                                    sw.Elapsed))
 137                        .PartitionEagerAsync(PageSize, cancellationToken)
 138                        .WithCancellation(cancellationToken)
 139                        .ConfigureAwait(false))
 140        {
 141            result.DateModified = ToUniversalTime(result.DateModified);
 142            itemCount++;
 143        }
 144
 145        var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 146        _logger.LogInformation("BaseItemImageInfos: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTi
 147    }
 148
 149    private DateTime? ToUniversalTime(DateTime? dateTime, bool isUTC = false)
 150    {
 0151        if (dateTime is null)
 152        {
 0153            return null;
 154        }
 155
 0156        if (dateTime.Value.Year == 1 && dateTime.Value.Month == 1 && dateTime.Value.Day == 1)
 157        {
 0158            return null;
 159        }
 160
 0161        if (dateTime.Value.Kind == DateTimeKind.Utc || isUTC)
 162        {
 0163            return dateTime.Value;
 164        }
 165
 0166        return dateTime.Value.ToUniversalTime();
 167    }
 168}