< 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: 171
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            var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 45            await using (context.ConfigureAwait(false))
 46            {
 47                var sw = Stopwatch.StartNew();
 48
 49                await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false);
 50                sw.Reset();
 51                await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false);
 52                sw.Reset();
 53                await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false);
 54            }
 55        }
 56    }
 57
 58    private async Task FixBaseItemsAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
 59    {
 60        int itemCount = 0;
 61
 62        var baseQuery = context.BaseItems.OrderBy(e => e.Id);
 63        var records = baseQuery.Count();
 64        _logger.LogInformation("Fixing dates for {Count} BaseItems.", records);
 65
 66        sw.Start();
 67        await foreach (var result in context.BaseItems.OrderBy(e => e.Id)
 68                        .WithPartitionProgress(
 69                            (partition) =>
 70                                _logger.LogInformation(
 71                                    "Processing BaseItems batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: 
 72                                    partition + 1,
 73                                    Math.Min((partition + 1) * PageSize, records),
 74                                    records,
 75                                    sw.Elapsed))
 76                        .PartitionEagerAsync(PageSize, cancellationToken)
 77                        .WithCancellation(cancellationToken)
 78                        .ConfigureAwait(false))
 79        {
 80            result.DateCreated = ToUniversalTime(result.DateCreated);
 81            result.DateLastMediaAdded = ToUniversalTime(result.DateLastMediaAdded);
 82            result.DateLastRefreshed = ToUniversalTime(result.DateLastRefreshed);
 83            result.DateLastSaved = ToUniversalTime(result.DateLastSaved);
 84            result.DateModified = ToUniversalTime(result.DateModified);
 85            itemCount++;
 86        }
 87
 88        var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 89        _logger.LogInformation("BaseItems: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", ite
 90    }
 91
 92    private async Task FixChaptersAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
 93    {
 94        int itemCount = 0;
 95
 96        var baseQuery = context.Chapters;
 97        var records = baseQuery.Count();
 98        _logger.LogInformation("Fixing dates for {Count} Chapters.", records);
 99
 100        sw.Start();
 101        await foreach (var result in context.Chapters.OrderBy(e => e.ItemId)
 102                        .WithPartitionProgress(
 103                            (partition) =>
 104                                _logger.LogInformation(
 105                                    "Processing Chapter batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {E
 106                                    partition + 1,
 107                                    Math.Min((partition + 1) * PageSize, records),
 108                                    records,
 109                                    sw.Elapsed))
 110                        .PartitionEagerAsync(PageSize, cancellationToken)
 111                        .WithCancellation(cancellationToken)
 112                        .ConfigureAwait(false))
 113        {
 114            result.ImageDateModified = ToUniversalTime(result.ImageDateModified, true);
 115            itemCount++;
 116        }
 117
 118        var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 119        _logger.LogInformation("Chapters: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", item
 120    }
 121
 122    private async Task FixBaseItemImageInfos(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToke
 123    {
 124        int itemCount = 0;
 125
 126        var baseQuery = context.BaseItemImageInfos;
 127        var records = baseQuery.Count();
 128        _logger.LogInformation("Fixing dates for {Count} BaseItemImageInfos.", records);
 129
 130        sw.Start();
 131        await foreach (var result in context.BaseItemImageInfos.OrderBy(e => e.Id)
 132                        .WithPartitionProgress(
 133                            (partition) =>
 134                                _logger.LogInformation(
 135                                    "Processing BaseItemImageInfos batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords})
 136                                    partition + 1,
 137                                    Math.Min((partition + 1) * PageSize, records),
 138                                    records,
 139                                    sw.Elapsed))
 140                        .PartitionEagerAsync(PageSize, cancellationToken)
 141                        .WithCancellation(cancellationToken)
 142                        .ConfigureAwait(false))
 143        {
 144            result.DateModified = ToUniversalTime(result.DateModified);
 145            itemCount++;
 146        }
 147
 148        var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 149        _logger.LogInformation("BaseItemImageInfos: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTi
 150    }
 151
 152    private DateTime? ToUniversalTime(DateTime? dateTime, bool isUTC = false)
 153    {
 0154        if (dateTime is null)
 155        {
 0156            return null;
 157        }
 158
 0159        if (dateTime.Value.Year == 1 && dateTime.Value.Month == 1 && dateTime.Value.Day == 1)
 160        {
 0161            return null;
 162        }
 163
 0164        if (dateTime.Value.Kind == DateTimeKind.Utc || isUTC)
 165        {
 0166            return dateTime.Value;
 167        }
 168
 0169        return dateTime.Value.ToUniversalTime();
 170    }
 171}