< Summary - Jellyfin

Information
Class: Jellyfin.Server.Migrations.Routines.MigrateLibraryUserData
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 4
Coverable lines: 4
Total lines: 107
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
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%

File(s)

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

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2
 3using System;
 4using System.Collections.Generic;
 5using System.IO;
 6using System.Linq;
 7using System.Threading;
 8using System.Threading.Tasks;
 9using Emby.Server.Implementations.Data;
 10using Jellyfin.Database.Implementations;
 11using Jellyfin.Server.Implementations.Item;
 12using Jellyfin.Server.ServerSetupApp;
 13using MediaBrowser.Controller;
 14using Microsoft.Data.Sqlite;
 15using Microsoft.EntityFrameworkCore;
 16using Microsoft.Extensions.Logging;
 17
 18namespace Jellyfin.Server.Migrations.Routines;
 19
 20[JellyfinMigration("2025-06-18T01:00:00", nameof(MigrateLibraryUserData))]
 21[JellyfinMigrationBackup(JellyfinDb = true)]
 22internal class MigrateLibraryUserData : IAsyncMigrationRoutine
 23{
 24    private const string DbFilename = "library.db.old";
 25
 26    private readonly IStartupLogger _logger;
 27    private readonly IServerApplicationPaths _paths;
 28    private readonly IDbContextFactory<JellyfinDbContext> _provider;
 29
 30    public MigrateLibraryUserData(
 31            IStartupLogger<MigrateLibraryDb> startupLogger,
 32            IDbContextFactory<JellyfinDbContext> provider,
 33            IServerApplicationPaths paths)
 34    {
 035        _logger = startupLogger;
 036        _provider = provider;
 037        _paths = paths;
 038    }
 39
 40    public async Task PerformAsync(CancellationToken cancellationToken)
 41    {
 42        _logger.LogInformation("Migrating the userdata from library.db.old may take a while, do not stop Jellyfin.");
 43
 44        var dataPath = _paths.DataPath;
 45        var libraryDbPath = Path.Combine(dataPath, DbFilename);
 46        if (!File.Exists(libraryDbPath))
 47        {
 48            _logger.LogError("Cannot migrate userdata from {LibraryDb} as it does not exist. This migration expects the 
 49            return;
 50        }
 51
 52        var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 53        await using (dbContext.ConfigureAwait(false))
 54        {
 55            if (!await dbContext.BaseItems.AnyAsync(e => e.Id == BaseItemRepository.PlaceholderId, cancellationToken).Co
 56            {
 57                // the placeholder baseitem has been deleted by the librarydb migration so we need to readd it.
 58                await dbContext.BaseItems.AddAsync(
 59                    new Database.Implementations.Entities.BaseItemEntity()
 60                    {
 61                        Id = BaseItemRepository.PlaceholderId,
 62                        Type = "PLACEHOLDER",
 63                        Name = "This is a placeholder item for UserData that has been detacted from its original item"
 64                    },
 65                    cancellationToken)
 66                    .ConfigureAwait(false);
 67                await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 68            }
 69
 70            var users = dbContext.Users.AsNoTracking().ToArray();
 71            var userIdBlacklist = new HashSet<int>();
 72            using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
 73            var retentionDate = DateTime.UtcNow;
 74
 75            var queryResult = connection.Query(
 76"""
 77    SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, 
 78
 79    WHERE NOT EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
 80""");
 81            foreach (var entity in queryResult)
 82            {
 83                var userData = MigrateLibraryDb.GetUserData(users, entity, userIdBlacklist, _logger);
 84                if (userData is null)
 85                {
 86                    var userDataId = entity.GetString(0);
 87                    var internalUserId = entity.GetInt32(1);
 88
 89                    if (!userIdBlacklist.Contains(internalUserId))
 90                    {
 91                        _logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId} doe
 92                        userIdBlacklist.Add(internalUserId);
 93                    }
 94
 95                    continue;
 96                }
 97
 98                userData.ItemId = BaseItemRepository.PlaceholderId;
 99                userData.RetentionDate = retentionDate;
 100                dbContext.UserData.Add(userData);
 101            }
 102
 103            _logger.LogInformation("Try saving {NewSaved} UserData entries.", dbContext.UserData.Local.Count);
 104            await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 105        }
 106    }
 107}