// // Copyright (c) PlaceholderCompany. All rights reserved. // #nullable disable namespace Emby.Server.Implementations.Library; using System; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions.Json; using MediaBrowser.Controller; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; /// /// Repository for persisting collection-folder library options in the database. /// public class LibraryOptionsRepository : ILibraryOptionsRepository { private readonly IDbContextFactory _dbProvider; private readonly IServerApplicationHost _appHost; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The db context factory. /// The application host. /// The logger. public LibraryOptionsRepository( IDbContextFactory dbProvider, IServerApplicationHost appHost, ILogger logger) { _dbProvider = dbProvider; _appHost = appHost; _logger = logger; } /// public LibraryOptions GetLibraryOptions(string libraryPath) => GetLibraryOptionsAsync(libraryPath, CancellationToken.None).GetAwaiter().GetResult(); /// public void SaveLibraryOptions(string libraryPath, LibraryOptions options) => SaveLibraryOptionsAsync(libraryPath, options, CancellationToken.None).GetAwaiter().GetResult(); /// /// Gets library options from the database if the backing table is available. /// /// The collection folder path. /// The cancellation token. /// The expanded library options, or null if no row exists. public async Task GetLibraryOptionsAsync(string libraryPath, CancellationToken cancellationToken = default) { try { var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { var entity = await context.LibraryOptions .AsNoTracking() .SingleOrDefaultAsync(e => e.LibraryPath == libraryPath, cancellationToken) .ConfigureAwait(false); if (entity is null) { return null; } var options = JsonSerializer.Deserialize(entity.OptionsJson, JsonDefaults.Options) ?? new LibraryOptions(); foreach (var mediaPath in options.PathInfos) { if (!string.IsNullOrEmpty(mediaPath.Path)) { mediaPath.Path = _appHost.ExpandVirtualPath(mediaPath.Path); } } return options; } } catch (Exception ex) when (IsMissingTableException(ex)) { _logger.LogDebug(ex, "LibraryOptions table is not available yet. Falling back to XML for {LibraryPath}", libraryPath); return null; } catch (Exception ex) { _logger.LogError(ex, "Error loading library options from database for {LibraryPath}", libraryPath); return null; } } /// /// Saves library options to the database if the backing table is available. /// /// The collection folder path. /// The options to save. /// The cancellation token. /// A task representing the asynchronous operation. public async Task SaveLibraryOptionsAsync(string libraryPath, LibraryOptions options, CancellationToken cancellationToken = default) { try { var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { var executionStrategy = context.Database.CreateExecutionStrategy(); await executionStrategy.ExecuteAsync(async () => { var clone = JsonSerializer.Deserialize(JsonSerializer.SerializeToUtf8Bytes(options, JsonDefaults.Options), JsonDefaults.Options) ?? new LibraryOptions(); foreach (var mediaPath in clone.PathInfos) { if (!string.IsNullOrEmpty(mediaPath.Path)) { mediaPath.Path = _appHost.ReverseVirtualPath(mediaPath.Path); } } var entity = await context.LibraryOptions .SingleOrDefaultAsync(e => e.LibraryPath == libraryPath, cancellationToken) .ConfigureAwait(false); var utcNow = DateTime.UtcNow; var json = JsonSerializer.Serialize(clone, JsonDefaults.Options); if (entity is null) { entity = new LibraryOptionsEntity { LibraryPath = libraryPath, OptionsJson = json, Version = 1, DateCreated = utcNow, DateModified = utcNow }; await context.LibraryOptions.AddAsync(entity, cancellationToken).ConfigureAwait(false); } else { entity.OptionsJson = json; entity.DateModified = utcNow; } await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); }).ConfigureAwait(false); } } catch (Exception ex) when (IsMissingTableException(ex)) { _logger.LogDebug(ex, "LibraryOptions table is not available yet. Skipping database save for {LibraryPath}", libraryPath); } catch (Exception ex) { _logger.LogError(ex, "Error saving library options to database for {LibraryPath}", libraryPath); } } private static bool IsMissingTableException(Exception exception) { return exception.Message.Contains("LibraryOptions", StringComparison.OrdinalIgnoreCase) && (exception.Message.Contains("does not exist", StringComparison.OrdinalIgnoreCase) || exception.Message.Contains("no such table", StringComparison.OrdinalIgnoreCase)); } }