Add backup export and local artwork storage
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Diagnostics;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.Multilang.Configuration;
|
||||
using Jellyfin.Plugin.Multilang.Data;
|
||||
using Jellyfin.Plugin.Multilang.Services.Assets;
|
||||
using Jellyfin.Plugin.Multilang.Services.Providers;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -18,6 +19,7 @@ public sealed class RefreshService
|
||||
private readonly TranslationStore _store;
|
||||
private readonly TmdbClient _tmdbClient;
|
||||
private readonly FanartClient _fanartClient;
|
||||
private readonly AssetStorageService _assetStorage;
|
||||
private readonly RefreshCoordinator _coordinator;
|
||||
private readonly ILogger<RefreshService> _logger;
|
||||
private readonly object _diagnosticsLock = new();
|
||||
@@ -28,6 +30,7 @@ public sealed class RefreshService
|
||||
TranslationStore store,
|
||||
TmdbClient tmdbClient,
|
||||
FanartClient fanartClient,
|
||||
AssetStorageService assetStorage,
|
||||
RefreshCoordinator coordinator,
|
||||
ILogger<RefreshService> logger)
|
||||
{
|
||||
@@ -35,6 +38,7 @@ public sealed class RefreshService
|
||||
_store = store;
|
||||
_tmdbClient = tmdbClient;
|
||||
_fanartClient = fanartClient;
|
||||
_assetStorage = assetStorage;
|
||||
_coordinator = coordinator;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -156,7 +160,19 @@ public sealed class RefreshService
|
||||
var trackedBefore = _store.GetTrackedItemIds();
|
||||
deletedCount = trackedBefore.Except(liveIds, StringComparer.OrdinalIgnoreCase).Count();
|
||||
newCount = liveIds.Except(trackedBefore, StringComparer.OrdinalIgnoreCase).Count();
|
||||
_store.CleanupNotInLibrary(liveIds);
|
||||
// Run local housekeeping only from full-library scans. ItemsProxy browsing may enqueue
|
||||
// missing items, but it must not delete local assets just because the admin briefly
|
||||
// changed artwork storage mode.
|
||||
var localCleanup = _store.CleanupForConfiguration(liveIds, NormalizeLanguages(cfg.Languages), IsLocalAssetStorage(cfg));
|
||||
if (localCleanup.TranslationsDeleted > 0 || localCleanup.AssetsDeleted > 0 || localCleanup.GenresDeleted > 0 || localCleanup.AssetFilesDeleted > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Multilang local cleanup translations={Translations} assets={Assets} genres={Genres} assetFiles={AssetFiles}",
|
||||
localCleanup.TranslationsDeleted,
|
||||
localCleanup.AssetsDeleted,
|
||||
localCleanup.GenresDeleted,
|
||||
localCleanup.AssetFilesDeleted);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Multilang refresh started mode={Mode} source={Source} items={Items} new={New} deleted={Deleted}", jobType, runSource, ordered.Count, newCount, deletedCount);
|
||||
var index = 0;
|
||||
@@ -168,9 +184,10 @@ public sealed class RefreshService
|
||||
SetCurrent(item, index, "checking");
|
||||
var status = _store.GetFactsStatus(item.ItemId);
|
||||
var isNew = !status.Exists;
|
||||
var missingConfiguredData = jobType == RefreshJobType.Missing && status.Exists && ItemNeedsMissingRefresh(item, cfg, langs: null);
|
||||
var due = jobType == RefreshJobType.Full
|
||||
? !status.Exists || status.FullCheckedAt < fullCutoff
|
||||
: !status.Exists || status.MissingCheckedAt < missingCutoff;
|
||||
: !status.Exists || status.MissingCheckedAt < missingCutoff || missingConfiguredData;
|
||||
|
||||
if (!due)
|
||||
{
|
||||
@@ -304,6 +321,8 @@ public sealed class RefreshService
|
||||
|
||||
public bool EnqueueOnTheFlyMissing(IEnumerable<string> itemIds)
|
||||
{
|
||||
// Browsing can only backfill completely untracked items. It deliberately does not run
|
||||
// local cleanup or convert existing local assets to URL rows.
|
||||
var cfg = Plugin.Instance?.Configuration;
|
||||
if (cfg is null)
|
||||
return false;
|
||||
@@ -530,9 +549,9 @@ public sealed class RefreshService
|
||||
if (tmdbImages is null)
|
||||
continue;
|
||||
|
||||
StoreImageMap(item.ItemId, languages, "poster", tmdbImages.Posters, updatedAt, written);
|
||||
StoreImageMap(item.ItemId, languages, "logo", tmdbImages.Logos, updatedAt, written);
|
||||
StoreImageMap(item.ItemId, languages, "backdrop", tmdbImages.Backdrops, updatedAt, written);
|
||||
await StoreImageMapAsync(item.ItemId, languages, "poster", tmdbImages.Posters, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false);
|
||||
await StoreImageMapAsync(item.ItemId, languages, "logo", tmdbImages.Logos, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false);
|
||||
await StoreImageMapAsync(item.ItemId, languages, "backdrop", tmdbImages.Backdrops, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false);
|
||||
Log(cfg.VerboseLogging && cfg.EnableLogging, LogLevel.Information, "TMDb artwork stored item={ItemId} langs={Languages}", item.ItemId, string.Join(",", languages));
|
||||
continue;
|
||||
}
|
||||
@@ -543,22 +562,24 @@ public sealed class RefreshService
|
||||
if (fanartImages is null)
|
||||
continue;
|
||||
|
||||
StoreImageMap(item.ItemId, languages, "poster", fanartImages.Posters, updatedAt, written);
|
||||
StoreImageMap(item.ItemId, languages, "logo", fanartImages.Logos, updatedAt, written);
|
||||
StoreImageMap(item.ItemId, languages, "banner", fanartImages.Banners, updatedAt, written);
|
||||
StoreImageMap(item.ItemId, languages, "thumb", fanartImages.Thumbs, updatedAt, written);
|
||||
await StoreImageMapAsync(item.ItemId, languages, "poster", fanartImages.Posters, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false);
|
||||
await StoreImageMapAsync(item.ItemId, languages, "logo", fanartImages.Logos, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false);
|
||||
await StoreImageMapAsync(item.ItemId, languages, "banner", fanartImages.Banners, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false);
|
||||
await StoreImageMapAsync(item.ItemId, languages, "thumb", fanartImages.Thumbs, cfg, updatedAt, written, cancellationToken).ConfigureAwait(false);
|
||||
Log(cfg.VerboseLogging && cfg.EnableLogging, LogLevel.Information, "Fanart artwork stored item={ItemId} langs={Languages}", item.ItemId, string.Join(",", languages));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StoreImageMap(
|
||||
private async Task StoreImageMapAsync(
|
||||
string itemId,
|
||||
IEnumerable<string> languages,
|
||||
string kind,
|
||||
IReadOnlyDictionary<string, string> images,
|
||||
PluginConfiguration cfg,
|
||||
long updatedAt,
|
||||
HashSet<string> written)
|
||||
HashSet<string> written,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var lang in languages)
|
||||
{
|
||||
@@ -573,11 +594,25 @@ public sealed class RefreshService
|
||||
if (!images.TryGetValue(iso, out var url) || string.IsNullOrWhiteSpace(url))
|
||||
continue;
|
||||
|
||||
_store.UpsertAsset(itemId, storeLang, kind, url, updatedAt);
|
||||
var storedPath = await _assetStorage.StoreAsync(itemId, storeLang, kind, url, IsLocalAssetStorage(cfg), cancellationToken).ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(storedPath))
|
||||
continue;
|
||||
|
||||
_store.UpsertAsset(itemId, storeLang, kind, storedPath, updatedAt);
|
||||
written.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ItemNeedsMissingRefresh(RefreshItemInfo item, PluginConfiguration cfg, string[]? langs)
|
||||
{
|
||||
langs ??= NormalizeLanguages(cfg.Languages);
|
||||
if (_store.HasMissingConfiguredTranslations(item.ItemId, langs))
|
||||
return true;
|
||||
|
||||
var assetPresence = _store.GetAssetPresence(item.ItemId, langs.Append(OriginalLanguageTag));
|
||||
return IsLocalAssetStorage(cfg) && assetPresence.AnyRemote;
|
||||
}
|
||||
|
||||
private static string[] AppendOriginalLanguage(string[] languages, string originalLanguage)
|
||||
=> languages
|
||||
.Concat([OriginalLanguageTag + ":" + originalLanguage])
|
||||
@@ -743,6 +778,9 @@ public sealed class RefreshService
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
private static bool IsLocalAssetStorage(PluginConfiguration cfg)
|
||||
=> string.Equals(cfg.AssetStorageMode, "local", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string FirstNonEmpty(params string?[] values)
|
||||
=> values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)) ?? string.Empty;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user