|  |  | 1 |  | using System; | 
|  |  | 2 |  | using System.Collections.Generic; | 
|  |  | 3 |  | using System.IO; | 
|  |  | 4 |  | using System.Linq; | 
|  |  | 5 |  | using System.Text.Json; | 
|  |  | 6 |  | using System.Text.Json.Serialization; | 
|  |  | 7 |  | using Emby.Server.Implementations.Data; | 
|  |  | 8 |  | using Jellyfin.Database.Implementations; | 
|  |  | 9 |  | using Jellyfin.Database.Implementations.Entities; | 
|  |  | 10 |  | using Jellyfin.Database.Implementations.Enums; | 
|  |  | 11 |  | using MediaBrowser.Controller; | 
|  |  | 12 |  | using MediaBrowser.Controller.Library; | 
|  |  | 13 |  | using MediaBrowser.Model.Dto; | 
|  |  | 14 |  | using Microsoft.Data.Sqlite; | 
|  |  | 15 |  | using Microsoft.EntityFrameworkCore; | 
|  |  | 16 |  | using Microsoft.Extensions.Logging; | 
|  |  | 17 |  |  | 
|  |  | 18 |  | namespace Jellyfin.Server.Migrations.Routines | 
|  |  | 19 |  | { | 
|  |  | 20 |  |     /// <summary> | 
|  |  | 21 |  |     /// The migration routine for migrating the display preferences database to EF Core. | 
|  |  | 22 |  |     /// </summary> | 
|  |  | 23 |  | #pragma warning disable CS0618 // Type or member is obsolete | 
|  |  | 24 |  |     [JellyfinMigration("2025-04-20T12:00:00", nameof(MigrateDisplayPreferencesDb), "06387815-C3CC-421F-A888-FB5F9992BEA8 | 
|  |  | 25 |  |     public class MigrateDisplayPreferencesDb : IMigrationRoutine | 
|  |  | 26 |  | #pragma warning restore CS0618 // Type or member is obsolete | 
|  |  | 27 |  |     { | 
|  |  | 28 |  |         private const string DbFilename = "displaypreferences.db"; | 
|  |  | 29 |  |  | 
|  |  | 30 |  |         private readonly ILogger<MigrateDisplayPreferencesDb> _logger; | 
|  |  | 31 |  |         private readonly IServerApplicationPaths _paths; | 
|  |  | 32 |  |         private readonly IDbContextFactory<JellyfinDbContext> _provider; | 
|  |  | 33 |  |         private readonly JsonSerializerOptions _jsonOptions; | 
|  |  | 34 |  |         private readonly IUserManager _userManager; | 
|  |  | 35 |  |  | 
|  |  | 36 |  |         /// <summary> | 
|  |  | 37 |  |         /// Initializes a new instance of the <see cref="MigrateDisplayPreferencesDb"/> class. | 
|  |  | 38 |  |         /// </summary> | 
|  |  | 39 |  |         /// <param name="logger">The logger.</param> | 
|  |  | 40 |  |         /// <param name="paths">The server application paths.</param> | 
|  |  | 41 |  |         /// <param name="provider">The database provider.</param> | 
|  |  | 42 |  |         /// <param name="userManager">The user manager.</param> | 
|  |  | 43 |  |         public MigrateDisplayPreferencesDb( | 
|  |  | 44 |  |             ILogger<MigrateDisplayPreferencesDb> logger, | 
|  |  | 45 |  |             IServerApplicationPaths paths, | 
|  |  | 46 |  |             IDbContextFactory<JellyfinDbContext> provider, | 
|  |  | 47 |  |             IUserManager userManager) | 
|  |  | 48 |  |         { | 
|  | 0 | 49 |  |             _logger = logger; | 
|  | 0 | 50 |  |             _paths = paths; | 
|  | 0 | 51 |  |             _provider = provider; | 
|  | 0 | 52 |  |             _userManager = userManager; | 
|  | 0 | 53 |  |             _jsonOptions = new JsonSerializerOptions(); | 
|  | 0 | 54 |  |             _jsonOptions.Converters.Add(new JsonStringEnumConverter()); | 
|  | 0 | 55 |  |         } | 
|  |  | 56 |  |  | 
|  |  | 57 |  |         /// <inheritdoc /> | 
|  |  | 58 |  |         public void Perform() | 
|  |  | 59 |  |         { | 
|  | 0 | 60 |  |             HomeSectionType[] defaults = | 
|  | 0 | 61 |  |             { | 
|  | 0 | 62 |  |                 HomeSectionType.SmallLibraryTiles, | 
|  | 0 | 63 |  |                 HomeSectionType.Resume, | 
|  | 0 | 64 |  |                 HomeSectionType.ResumeAudio, | 
|  | 0 | 65 |  |                 HomeSectionType.LiveTv, | 
|  | 0 | 66 |  |                 HomeSectionType.NextUp, | 
|  | 0 | 67 |  |                 HomeSectionType.LatestMedia, | 
|  | 0 | 68 |  |                 HomeSectionType.None, | 
|  | 0 | 69 |  |             }; | 
|  |  | 70 |  |  | 
|  | 0 | 71 |  |             var chromecastDict = new Dictionary<string, ChromecastVersion>(StringComparer.OrdinalIgnoreCase) | 
|  | 0 | 72 |  |             { | 
|  | 0 | 73 |  |                 { "stable", ChromecastVersion.Stable }, | 
|  | 0 | 74 |  |                 { "nightly", ChromecastVersion.Unstable }, | 
|  | 0 | 75 |  |                 { "unstable", ChromecastVersion.Unstable } | 
|  | 0 | 76 |  |             }; | 
|  |  | 77 |  |  | 
|  | 0 | 78 |  |             var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | 
|  | 0 | 79 |  |             var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | 
|  | 0 | 80 |  |             var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); | 
|  | 0 | 81 |  |             using (var connection = new SqliteConnection($"Filename={dbFilePath}")) | 
|  |  | 82 |  |             { | 
|  | 0 | 83 |  |                 connection.Open(); | 
|  | 0 | 84 |  |                 using var dbContext = _provider.CreateDbContext(); | 
|  |  | 85 |  |  | 
|  | 0 | 86 |  |                 var results = connection.Query("SELECT * FROM userdisplaypreferences"); | 
|  | 0 | 87 |  |                 foreach (var result in results) | 
|  |  | 88 |  |                 { | 
|  | 0 | 89 |  |                     var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result.GetStream(3), _jsonOptions); | 
|  | 0 | 90 |  |                     if (dto is null) | 
|  |  | 91 |  |                     { | 
|  |  | 92 |  |                         continue; | 
|  |  | 93 |  |                     } | 
|  |  | 94 |  |  | 
|  | 0 | 95 |  |                     var itemId = result.GetGuid(1); | 
|  | 0 | 96 |  |                     var dtoUserId = itemId; | 
|  | 0 | 97 |  |                     var client = result.GetString(2); | 
|  | 0 | 98 |  |                     var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}"; | 
|  | 0 | 99 |  |                     if (displayPrefs.Contains(displayPreferencesKey)) | 
|  |  | 100 |  |                     { | 
|  |  | 101 |  |                         // Duplicate display preference. | 
|  |  | 102 |  |                         continue; | 
|  |  | 103 |  |                     } | 
|  |  | 104 |  |  | 
|  | 0 | 105 |  |                     displayPrefs.Add(displayPreferencesKey); | 
|  | 0 | 106 |  |                     var existingUser = _userManager.GetUserById(dtoUserId); | 
|  | 0 | 107 |  |                     if (existingUser is null) | 
|  |  | 108 |  |                     { | 
|  | 0 | 109 |  |                         _logger.LogWarning("User with ID {UserId} does not exist in the database, skipping migration.",  | 
|  | 0 | 110 |  |                         continue; | 
|  |  | 111 |  |                     } | 
|  |  | 112 |  |  | 
|  | 0 | 113 |  |                     var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version) | 
|  | 0 | 114 |  |                                             && !string.IsNullOrEmpty(version) | 
|  | 0 | 115 |  |                         ? chromecastDict[version] | 
|  | 0 | 116 |  |                         : ChromecastVersion.Stable; | 
|  | 0 | 117 |  |                     dto.CustomPrefs.Remove("chromecastVersion"); | 
|  |  | 118 |  |  | 
|  | 0 | 119 |  |                     var displayPreferences = new DisplayPreferences(dtoUserId, itemId, client) | 
|  | 0 | 120 |  |                     { | 
|  | 0 | 121 |  |                         IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : null, | 
|  | 0 | 122 |  |                         ShowBackdrop = dto.ShowBackdrop, | 
|  | 0 | 123 |  |                         ShowSidebar = dto.ShowSidebar, | 
|  | 0 | 124 |  |                         ScrollDirection = dto.ScrollDirection, | 
|  | 0 | 125 |  |                         ChromecastVersion = chromecastVersion, | 
|  | 0 | 126 |  |                         SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length) && int.TryP | 
|  | 0 | 127 |  |                             ? skipForwardLength | 
|  | 0 | 128 |  |                             : 30000, | 
|  | 0 | 129 |  |                         SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && int.TryParse(l | 
|  | 0 | 130 |  |                             ? skipBackwardLength | 
|  | 0 | 131 |  |                             : 10000, | 
|  | 0 | 132 |  |                         EnableNextVideoInfoOverlay = !dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var  | 
|  | 0 | 133 |  |                         DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.E | 
|  | 0 | 134 |  |                         TvHome = dto.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty | 
|  | 0 | 135 |  |                     }; | 
|  |  | 136 |  |  | 
|  | 0 | 137 |  |                     dto.CustomPrefs.Remove("skipForwardLength"); | 
|  | 0 | 138 |  |                     dto.CustomPrefs.Remove("skipBackLength"); | 
|  | 0 | 139 |  |                     dto.CustomPrefs.Remove("enableNextVideoInfoOverlay"); | 
|  | 0 | 140 |  |                     dto.CustomPrefs.Remove("dashboardtheme"); | 
|  | 0 | 141 |  |                     dto.CustomPrefs.Remove("tvhome"); | 
|  |  | 142 |  |  | 
|  | 0 | 143 |  |                     for (int i = 0; i < 7; i++) | 
|  |  | 144 |  |                     { | 
|  | 0 | 145 |  |                         var key = "homesection" + i; | 
|  | 0 | 146 |  |                         dto.CustomPrefs.TryGetValue(key, out var homeSection); | 
|  |  | 147 |  |  | 
|  | 0 | 148 |  |                         displayPreferences.HomeSections.Add(new HomeSection | 
|  | 0 | 149 |  |                         { | 
|  | 0 | 150 |  |                             Order = i, | 
|  | 0 | 151 |  |                             Type = Enum.TryParse<HomeSectionType>(homeSection, true, out var type) ? type : defaults[i] | 
|  | 0 | 152 |  |                         }); | 
|  |  | 153 |  |  | 
|  | 0 | 154 |  |                         dto.CustomPrefs.Remove(key); | 
|  |  | 155 |  |                     } | 
|  |  | 156 |  |  | 
|  | 0 | 157 |  |                     var defaultLibraryPrefs = new ItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayP | 
|  | 0 | 158 |  |                     { | 
|  | 0 | 159 |  |                         SortBy = dto.SortBy ?? "SortName", | 
|  | 0 | 160 |  |                         SortOrder = dto.SortOrder, | 
|  | 0 | 161 |  |                         RememberIndexing = dto.RememberIndexing, | 
|  | 0 | 162 |  |                         RememberSorting = dto.RememberSorting, | 
|  | 0 | 163 |  |                     }; | 
|  |  | 164 |  |  | 
|  | 0 | 165 |  |                     dbContext.Add(defaultLibraryPrefs); | 
|  |  | 166 |  |  | 
|  | 0 | 167 |  |                     foreach (var key in dto.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.Or | 
|  |  | 168 |  |                     { | 
|  | 0 | 169 |  |                         if (!Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var landingItemId)) | 
|  |  | 170 |  |                         { | 
|  |  | 171 |  |                             continue; | 
|  |  | 172 |  |                         } | 
|  |  | 173 |  |  | 
|  | 0 | 174 |  |                         var libraryDisplayPreferences = new ItemDisplayPreferences(displayPreferences.UserId, landingIte | 
|  | 0 | 175 |  |                         { | 
|  | 0 | 176 |  |                             SortBy = dto.SortBy ?? "SortName", | 
|  | 0 | 177 |  |                             SortOrder = dto.SortOrder, | 
|  | 0 | 178 |  |                             RememberIndexing = dto.RememberIndexing, | 
|  | 0 | 179 |  |                             RememberSorting = dto.RememberSorting, | 
|  | 0 | 180 |  |                         }; | 
|  |  | 181 |  |  | 
|  | 0 | 182 |  |                         if (Enum.TryParse<ViewType>(dto.ViewType, true, out var viewType)) | 
|  |  | 183 |  |                         { | 
|  | 0 | 184 |  |                             libraryDisplayPreferences.ViewType = viewType; | 
|  |  | 185 |  |                         } | 
|  |  | 186 |  |  | 
|  | 0 | 187 |  |                         dto.CustomPrefs.Remove(key); | 
|  | 0 | 188 |  |                         dbContext.ItemDisplayPreferences.Add(libraryDisplayPreferences); | 
|  |  | 189 |  |                     } | 
|  |  | 190 |  |  | 
|  | 0 | 191 |  |                     foreach (var (key, value) in dto.CustomPrefs) | 
|  |  | 192 |  |                     { | 
|  |  | 193 |  |                         // Custom display preferences can have a key collision. | 
|  | 0 | 194 |  |                         var indexKey = $"{displayPreferences.UserId}|{itemId}|{displayPreferences.Client}|{key}"; | 
|  | 0 | 195 |  |                         if (!customDisplayPrefs.Contains(indexKey)) | 
|  |  | 196 |  |                         { | 
|  | 0 | 197 |  |                             dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPre | 
|  | 0 | 198 |  |                             customDisplayPrefs.Add(indexKey); | 
|  |  | 199 |  |                         } | 
|  |  | 200 |  |                     } | 
|  |  | 201 |  |  | 
|  | 0 | 202 |  |                     dbContext.Add(displayPreferences); | 
|  |  | 203 |  |                 } | 
|  |  | 204 |  |  | 
|  | 0 | 205 |  |                 dbContext.SaveChanges(); | 
|  |  | 206 |  |             } | 
|  |  | 207 |  |  | 
|  |  | 208 |  |             try | 
|  |  | 209 |  |             { | 
|  | 0 | 210 |  |                 File.Move(dbFilePath, dbFilePath + ".old"); | 
|  |  | 211 |  |  | 
|  | 0 | 212 |  |                 var journalPath = dbFilePath + "-journal"; | 
|  | 0 | 213 |  |                 if (File.Exists(journalPath)) | 
|  |  | 214 |  |                 { | 
|  | 0 | 215 |  |                     File.Move(journalPath, dbFilePath + ".old-journal"); | 
|  |  | 216 |  |                 } | 
|  | 0 | 217 |  |             } | 
|  | 0 | 218 |  |             catch (IOException e) | 
|  |  | 219 |  |             { | 
|  | 0 | 220 |  |                 _logger.LogError(e, "Error renaming legacy display preferences database to 'displaypreferences.db.old'") | 
|  | 0 | 221 |  |             } | 
|  | 0 | 222 |  |         } | 
|  |  | 223 |  |     } | 
|  |  | 224 |  | } |