Add backup export and local artwork storage
This commit is contained in:
@@ -7,6 +7,8 @@ using Jellyfin.Plugin.Multilang.Configuration;
|
||||
using Jellyfin.Plugin.Multilang.Data;
|
||||
using Jellyfin.Plugin.Multilang.Rules;
|
||||
using Jellyfin.Plugin.Multilang.Services;
|
||||
using Jellyfin.Plugin.Multilang.Services.Assets;
|
||||
using Jellyfin.Plugin.Multilang.Services.Backup;
|
||||
using Jellyfin.Plugin.Multilang.Services.Providers;
|
||||
using Jellyfin.Plugin.Multilang.Services.Refresh;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
@@ -14,6 +16,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -44,6 +47,8 @@ public sealed class MultilangController : ControllerBase
|
||||
private readonly RefreshService _refreshService;
|
||||
private readonly FanartClient _fanartClient;
|
||||
private readonly ItemsProxyCache _itemsProxyCache;
|
||||
private readonly AssetStorageService _assetStorage;
|
||||
private readonly MultilangBackupService _backupService;
|
||||
private readonly ILogger<MultilangController> _logger;
|
||||
|
||||
public MultilangController(
|
||||
@@ -59,6 +64,8 @@ public sealed class MultilangController : ControllerBase
|
||||
RefreshService refreshService,
|
||||
FanartClient fanartClient,
|
||||
ItemsProxyCache itemsProxyCache,
|
||||
AssetStorageService assetStorage,
|
||||
MultilangBackupService backupService,
|
||||
ILogger<MultilangController> logger)
|
||||
{
|
||||
_store = store;
|
||||
@@ -73,6 +80,8 @@ public sealed class MultilangController : ControllerBase
|
||||
_refreshService = refreshService;
|
||||
_fanartClient = fanartClient;
|
||||
_itemsProxyCache = itemsProxyCache;
|
||||
_assetStorage = assetStorage;
|
||||
_backupService = backupService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -88,6 +97,12 @@ public sealed class MultilangController : ControllerBase
|
||||
public IActionResult SharedCss()
|
||||
=> ServeEmbedded("Jellyfin.Plugin.Multilang.Configuration.shared.css", "text/css");
|
||||
|
||||
[HttpGet("Assets/{**path}")]
|
||||
public IActionResult Asset(string path)
|
||||
=> _assetStorage.TryResolveLocalAsset(path, out var fullPath, out var contentType)
|
||||
? PhysicalFile(fullPath, contentType)
|
||||
: NotFound();
|
||||
|
||||
[HttpGet("Providers")]
|
||||
public IActionResult Providers()
|
||||
=> Ok(_providerCatalog.Providers);
|
||||
@@ -127,6 +142,7 @@ public sealed class MultilangController : ControllerBase
|
||||
|
||||
private static bool AdminConfigAffectsCachedItems(PluginConfiguration previous, PluginConfiguration next)
|
||||
=> !StringArrayEquals(previous.Languages, next.Languages, StringComparer.OrdinalIgnoreCase) ||
|
||||
!string.Equals(previous.AssetStorageMode, next.AssetStorageMode, StringComparison.OrdinalIgnoreCase) ||
|
||||
!ArticleEntriesEqual(previous.ArticleEntries, next.ArticleEntries);
|
||||
|
||||
private static bool StringArrayEquals(string[] previous, string[] next, StringComparer comparer)
|
||||
@@ -963,6 +979,154 @@ public sealed class MultilangController : ControllerBase
|
||||
return Ok(_refreshService.GetDiagnostics());
|
||||
}
|
||||
|
||||
[HttpGet("ExportInfo")]
|
||||
public async Task<IActionResult> ExportInfo()
|
||||
{
|
||||
if (!await IsRequesterAdminAsync().ConfigureAwait(false))
|
||||
return Unauthorized(new { Error = "AdminRequired" });
|
||||
|
||||
var storage = _backupService.GetStorageInfo();
|
||||
return Ok(new
|
||||
{
|
||||
storage.DatabaseBytes,
|
||||
storage.AssetsBytes,
|
||||
CleanupDataOnUninstall = Plugin.Instance?.Configuration?.CleanupDataOnUninstall == true,
|
||||
AssetStorageMode = Plugin.Instance?.Configuration?.AssetStorageMode ?? "url"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("Export")]
|
||||
public async Task<IActionResult> Export(
|
||||
[FromQuery] bool pluginSettings = true,
|
||||
[FromQuery] bool userSettings = true,
|
||||
[FromQuery] bool translationsDatabase = false,
|
||||
[FromQuery] bool downloadedAssets = false)
|
||||
{
|
||||
if (!await IsRequesterAdminAsync().ConfigureAwait(false))
|
||||
return Unauthorized(new { Error = "AdminRequired" });
|
||||
|
||||
if (downloadedAssets && !translationsDatabase)
|
||||
return BadRequest(new { Error = "AssetsRequireDatabase" });
|
||||
|
||||
var bytes = _backupService.Export(
|
||||
new BackupExportOptions(pluginSettings, userSettings, translationsDatabase, downloadedAssets),
|
||||
Plugin.Instance?.Configuration ?? new PluginConfiguration());
|
||||
var name = "multilang-export-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + ".zip";
|
||||
return File(bytes, "application/zip", name);
|
||||
}
|
||||
|
||||
[HttpPost("Import")]
|
||||
public async Task<IActionResult> Import()
|
||||
{
|
||||
if (!await IsRequesterAdminAsync().ConfigureAwait(false))
|
||||
return Unauthorized(new { Error = "AdminRequired" });
|
||||
|
||||
var form = await Request.ReadFormAsync(HttpContext.RequestAborted).ConfigureAwait(false);
|
||||
var file = form.Files.GetFile("file");
|
||||
if (file is null || file.Length == 0)
|
||||
return BadRequest(new { Error = "MissingFile" });
|
||||
|
||||
var options = new BackupImportOptions(
|
||||
FormBool(form, "pluginSettings", true),
|
||||
FormBool(form, "userSettings", true),
|
||||
FormBool(form, "translationsDatabase", true),
|
||||
FormBool(form, "downloadedAssets", true));
|
||||
if (options.DownloadedAssets && !options.TranslationsDatabase)
|
||||
return BadRequest(new { Error = "AssetsRequireDatabase" });
|
||||
|
||||
try
|
||||
{
|
||||
await using var stream = file.OpenReadStream();
|
||||
var result = _backupService.ImportAdmin(
|
||||
stream,
|
||||
options,
|
||||
Plugin.Instance?.Configuration ?? new PluginConfiguration(),
|
||||
cfg => Plugin.Instance?.UpdateConfiguration(cfg));
|
||||
_itemsProxyCache.ClearAll();
|
||||
return Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("CleanupAll")]
|
||||
public async Task<IActionResult> CleanupAll([FromBody] CleanupAllRequest request)
|
||||
{
|
||||
if (!await IsRequesterAdminAsync().ConfigureAwait(false))
|
||||
return Unauthorized(new { Error = "AdminRequired" });
|
||||
|
||||
if (request.Confirm != true)
|
||||
return BadRequest(new { Error = "ConfirmationRequired" });
|
||||
|
||||
Plugin.Instance?.UpdateConfiguration(new PluginConfiguration());
|
||||
_store.ResetAll();
|
||||
_itemsProxyCache.ClearAll();
|
||||
return Ok(new { Cleared = true });
|
||||
}
|
||||
|
||||
[HttpGet("UserRules/self/export")]
|
||||
public async Task<IActionResult> ExportSelfRules()
|
||||
{
|
||||
var userId = await GetUserIdAsync().ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
return Unauthorized(new { Error = "NotAuthenticated" });
|
||||
|
||||
var bytes = _backupService.ExportUserRules(userId, GetRulesOrDefault(userId));
|
||||
var name = "multilang-user-rules-" + userId + ".zip";
|
||||
return File(bytes, "application/zip", name);
|
||||
}
|
||||
|
||||
[HttpPost("UserRules/self/import/inspect")]
|
||||
public async Task<IActionResult> InspectSelfRulesImport()
|
||||
{
|
||||
var userId = await GetUserIdAsync().ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
return Unauthorized(new { Error = "NotAuthenticated" });
|
||||
|
||||
var form = await Request.ReadFormAsync(HttpContext.RequestAborted).ConfigureAwait(false);
|
||||
var file = form.Files.GetFile("file");
|
||||
if (file is null || file.Length == 0)
|
||||
return BadRequest(new { Error = "MissingFile" });
|
||||
|
||||
try
|
||||
{
|
||||
await using var stream = file.OpenReadStream();
|
||||
return Ok(_backupService.InspectUserImport(stream, userId));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("UserRules/self/import")]
|
||||
public async Task<IActionResult> ImportSelfRules()
|
||||
{
|
||||
var userId = await GetUserIdAsync().ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
return Unauthorized(new { Error = "NotAuthenticated" });
|
||||
|
||||
var form = await Request.ReadFormAsync(HttpContext.RequestAborted).ConfigureAwait(false);
|
||||
var file = form.Files.GetFile("file");
|
||||
if (file is null || file.Length == 0)
|
||||
return BadRequest(new { Error = "MissingFile" });
|
||||
|
||||
try
|
||||
{
|
||||
await using var stream = file.OpenReadStream();
|
||||
var result = _backupService.ImportUser(stream, userId, form.TryGetValue("sourceUserId", out var sourceUserId) ? sourceUserId.ToString() : null);
|
||||
if (result.UserSettingsImported > 0)
|
||||
_itemsProxyCache.ClearAll();
|
||||
return Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("Debug/{itemId}")]
|
||||
public async Task<IActionResult> DebugItem(string itemId)
|
||||
{
|
||||
@@ -1989,6 +2153,16 @@ public sealed class MultilangController : ControllerBase
|
||||
return dto.Policy?.IsAdministrator == true;
|
||||
}
|
||||
|
||||
private static bool FormBool(IFormCollection form, string key, bool fallback)
|
||||
{
|
||||
if (!form.TryGetValue(key, out var values))
|
||||
return fallback;
|
||||
var value = values.ToString();
|
||||
return value.Equals("1", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("true", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("on", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool TryGetIsAdmin(object user, out bool isAdmin)
|
||||
{
|
||||
isAdmin = false;
|
||||
@@ -2063,6 +2237,8 @@ public sealed class MultilangController : ControllerBase
|
||||
|
||||
public sealed record TestProviderRequest(string Provider, string ApiKey);
|
||||
|
||||
public sealed record CleanupAllRequest(bool Confirm);
|
||||
|
||||
public sealed class AdminConfigDto
|
||||
{
|
||||
public string TmdbApiKey { get; set; } = string.Empty;
|
||||
@@ -2083,12 +2259,16 @@ public sealed class AdminConfigDto
|
||||
|
||||
public int ItemsProxyCacheMaxMiB { get; set; }
|
||||
|
||||
public string AssetStorageMode { get; set; } = "url";
|
||||
|
||||
public SortArticleEntry[] ArticleEntries { get; set; } = [];
|
||||
|
||||
public bool EnableLogging { get; set; }
|
||||
|
||||
public bool VerboseLogging { get; set; }
|
||||
|
||||
public bool CleanupDataOnUninstall { get; set; }
|
||||
|
||||
public static AdminConfigDto FromConfiguration(PluginConfiguration cfg)
|
||||
=> new()
|
||||
{
|
||||
@@ -2101,9 +2281,11 @@ public sealed class AdminConfigDto
|
||||
ItemsProxyCacheThresholdMs = cfg.ItemsProxyCacheThresholdMs,
|
||||
ItemsProxyCacheTtlMinutes = cfg.ItemsProxyCacheTtlMinutes,
|
||||
ItemsProxyCacheMaxMiB = cfg.ItemsProxyCacheMaxMiB,
|
||||
AssetStorageMode = NormalizeAssetStorageMode(cfg.AssetStorageMode),
|
||||
ArticleEntries = cfg.ArticleEntries,
|
||||
EnableLogging = cfg.EnableLogging,
|
||||
VerboseLogging = cfg.VerboseLogging
|
||||
VerboseLogging = cfg.VerboseLogging,
|
||||
CleanupDataOnUninstall = cfg.CleanupDataOnUninstall
|
||||
};
|
||||
|
||||
public PluginConfiguration ToConfiguration(ProviderCatalog providerCatalog)
|
||||
@@ -2121,12 +2303,14 @@ public sealed class AdminConfigDto
|
||||
ItemsProxyCacheThresholdMs = Math.Max(0, ItemsProxyCacheThresholdMs),
|
||||
ItemsProxyCacheTtlMinutes = Math.Max(1, ItemsProxyCacheTtlMinutes),
|
||||
ItemsProxyCacheMaxMiB = Math.Max(1, ItemsProxyCacheMaxMiB),
|
||||
AssetStorageMode = NormalizeAssetStorageMode(AssetStorageMode),
|
||||
ArticleEntries = ArticleEntries
|
||||
.Where(e => !string.IsNullOrWhiteSpace(e.Language))
|
||||
.Select(e => new SortArticleEntry { Language = e.Language.Trim(), Articles = e.Articles ?? string.Empty, AlwaysApply = e.AlwaysApply })
|
||||
.ToArray(),
|
||||
EnableLogging = EnableLogging || VerboseLogging,
|
||||
VerboseLogging = VerboseLogging
|
||||
VerboseLogging = VerboseLogging,
|
||||
CleanupDataOnUninstall = CleanupDataOnUninstall
|
||||
};
|
||||
|
||||
private static string[] NormalizeLanguages(IEnumerable<string> languages)
|
||||
@@ -2134,4 +2318,7 @@ public sealed class AdminConfigDto
|
||||
.Where(l => l.Length > 0)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
private static string NormalizeAssetStorageMode(string? value)
|
||||
=> string.Equals(value?.Trim(), "local", StringComparison.OrdinalIgnoreCase) ? "local" : "url";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user