Release 0.3.4: Jellyfin 12 stable support and audit fixes

Harden proxy permissions and cache behavior, streamline metadata fetching, remove obsolete rules, and validate the stable SDK with expanded regression coverage. Document backup format changes.
This commit is contained in:
ajp_anton
2026-09-09 01:18:43 +00:00
parent 8f7462fd3c
commit 096c6be8c7
50 changed files with 2126 additions and 1784 deletions
@@ -1,4 +1,3 @@
using System.Net.Http.Headers;
using System.Security.Cryptography;
using Jellyfin.Plugin.Multilang.Data;
@@ -18,8 +17,6 @@ public sealed class AssetStorageService
public async Task<string?> StoreAsync(
string itemId,
string lang,
string kind,
string sourceUrl,
bool localStorage,
CancellationToken cancellationToken)
@@ -30,22 +27,17 @@ public sealed class AssetStorageService
if (!localStorage)
return sourceUrl;
var relative = BuildRelativePath(itemId, lang, kind, sourceUrl);
var relative = BuildRelativePath(itemId, sourceUrl);
var expectedUrl = TranslationStore.LocalAssetUrlPrefix + relative.Replace('\\', '/');
if (_store.GetAssetPath(itemId, lang, kind) is { } existing &&
existing.Equals(expectedUrl, StringComparison.OrdinalIgnoreCase) &&
_store.TryNormalizeLocalAssetPath(existing, out var existingPath) &&
File.Exists(existingPath))
{
return existing;
}
var destination = Path.GetFullPath(Path.Combine(_store.AssetsDirectory, relative));
var root = Path.GetFullPath(_store.AssetsDirectory) + Path.DirectorySeparatorChar;
if (!destination.StartsWith(root, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Resolved asset path escaped the Multilang assets directory.");
Directory.CreateDirectory(Path.GetDirectoryName(destination) ?? _store.AssetsDirectory);
if (File.Exists(destination))
return expectedUrl;
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
await DownloadAsync(sourceUrl, destination, cancellationToken).ConfigureAwait(false);
return expectedUrl;
}
@@ -101,11 +93,11 @@ public sealed class AssetStorageService
File.Move(temp, destination, overwrite: true);
}
private static string BuildRelativePath(string itemId, string lang, string kind, string sourceUrl)
private static string BuildRelativePath(string itemId, string sourceUrl)
{
var extension = ExtensionFromUrl(sourceUrl);
var hash = Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(sourceUrl))).ToLowerInvariant()[..16];
return Path.Combine(SafeSegment(itemId), SafeSegment(lang), SafeSegment(kind) + "-" + hash + extension);
return Path.Combine(SafeSegment(itemId), hash + extension);
}
private static string SafeSegment(string value)
@@ -133,6 +125,6 @@ public sealed class AssetStorageService
".png" => "image/png",
".webp" => "image/webp",
".gif" => "image/gif",
_ => MediaTypeHeaderValue.Parse("application/octet-stream").MediaType ?? "application/octet-stream"
_ => "application/octet-stream"
};
}