using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; namespace Jellyfin.Server.Migrations.Routines { /// /// The migration routine for migrating the display preferences database to EF Core. /// public class MigrateDisplayPreferencesDb : IMigrationRoutine { private const string DbFilename = "displaypreferences.db"; private readonly ILogger _logger; private readonly IServerApplicationPaths _paths; private readonly IDbContextFactory _provider; private readonly JsonSerializerOptions _jsonOptions; private readonly IUserManager _userManager; /// /// Initializes a new instance of the class. /// /// The logger. /// The server application paths. /// The database provider. /// The user manager. public MigrateDisplayPreferencesDb( ILogger logger, IServerApplicationPaths paths, IDbContextFactory provider, IUserManager userManager) { _logger = logger; _paths = paths; _provider = provider; _userManager = userManager; _jsonOptions = new JsonSerializerOptions(); _jsonOptions.Converters.Add(new JsonStringEnumConverter()); } /// public Guid Id => Guid.Parse("06387815-C3CC-421F-A888-FB5F9992BEA8"); /// public string Name => "MigrateDisplayPreferencesDatabase"; /// public bool PerformOnNewInstall => false; /// public void Perform() { HomeSectionType[] defaults = { HomeSectionType.SmallLibraryTiles, HomeSectionType.Resume, HomeSectionType.ResumeAudio, HomeSectionType.LiveTv, HomeSectionType.NextUp, HomeSectionType.LatestMedia, HomeSectionType.None, }; var chromecastDict = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "stable", ChromecastVersion.Stable }, { "nightly", ChromecastVersion.Unstable }, { "unstable", ChromecastVersion.Unstable } }; var displayPrefs = new HashSet(StringComparer.OrdinalIgnoreCase); var customDisplayPrefs = new HashSet(StringComparer.OrdinalIgnoreCase); var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null)) { using var dbContext = _provider.CreateDbContext(); var results = connection.Query("SELECT * FROM userdisplaypreferences"); foreach (var result in results) { var dto = JsonSerializer.Deserialize(result[3].ToBlob(), _jsonOptions); if (dto is null) { continue; } var itemId = new Guid(result[1].ToBlob()); var dtoUserId = new Guid(result[1].ToBlob()); var client = result[2].ToString(); var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}"; if (displayPrefs.Contains(displayPreferencesKey)) { // Duplicate display preference. continue; } displayPrefs.Add(displayPreferencesKey); var existingUser = _userManager.GetUserById(dtoUserId); if (existingUser is null) { _logger.LogWarning("User with ID {UserId} does not exist in the database, skipping migration.", dtoUserId); continue; } var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version) && !string.IsNullOrEmpty(version) ? chromecastDict[version] : ChromecastVersion.Stable; dto.CustomPrefs.Remove("chromecastVersion"); var displayPreferences = new DisplayPreferences(dtoUserId, itemId, client) { IndexBy = Enum.TryParse(dto.IndexBy, true, out var indexBy) ? indexBy : null, ShowBackdrop = dto.ShowBackdrop, ShowSidebar = dto.ShowSidebar, ScrollDirection = dto.ScrollDirection, ChromecastVersion = chromecastVersion, SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length) && int.TryParse(length, out var skipForwardLength) ? skipForwardLength : 30000, SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && int.TryParse(length, out var skipBackwardLength) ? skipBackwardLength : 10000, EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) && !string.IsNullOrEmpty(enabled) ? bool.Parse(enabled) : true, DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty, TvHome = dto.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty }; dto.CustomPrefs.Remove("skipForwardLength"); dto.CustomPrefs.Remove("skipBackLength"); dto.CustomPrefs.Remove("enableNextVideoInfoOverlay"); dto.CustomPrefs.Remove("dashboardtheme"); dto.CustomPrefs.Remove("tvhome"); for (int i = 0; i < 7; i++) { var key = "homesection" + i; dto.CustomPrefs.TryGetValue(key, out var homeSection); displayPreferences.HomeSections.Add(new HomeSection { Order = i, Type = Enum.TryParse(homeSection, true, out var type) ? type : defaults[i] }); dto.CustomPrefs.Remove(key); } var defaultLibraryPrefs = new ItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client) { SortBy = dto.SortBy ?? "SortName", SortOrder = dto.SortOrder, RememberIndexing = dto.RememberIndexing, RememberSorting = dto.RememberSorting, }; dbContext.Add(defaultLibraryPrefs); foreach (var key in dto.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.Ordinal))) { if (!Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var landingItemId)) { continue; } var libraryDisplayPreferences = new ItemDisplayPreferences(displayPreferences.UserId, landingItemId, displayPreferences.Client) { SortBy = dto.SortBy ?? "SortName", SortOrder = dto.SortOrder, RememberIndexing = dto.RememberIndexing, RememberSorting = dto.RememberSorting, }; if (Enum.TryParse(dto.ViewType, true, out var viewType)) { libraryDisplayPreferences.ViewType = viewType; } dto.CustomPrefs.Remove(key); dbContext.ItemDisplayPreferences.Add(libraryDisplayPreferences); } foreach (var (key, value) in dto.CustomPrefs) { // Custom display preferences can have a key collision. var indexKey = $"{displayPreferences.UserId}|{itemId}|{displayPreferences.Client}|{key}"; if (!customDisplayPrefs.Contains(indexKey)) { dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value)); customDisplayPrefs.Add(indexKey); } } dbContext.Add(displayPreferences); } dbContext.SaveChanges(); } try { File.Move(dbFilePath, dbFilePath + ".old"); var journalPath = dbFilePath + "-journal"; if (File.Exists(journalPath)) { File.Move(journalPath, dbFilePath + ".old-journal"); } } catch (IOException e) { _logger.LogError(e, "Error renaming legacy display preferences database to 'displaypreferences.db.old'"); } } } }