5 Commits
Author SHA1 Message Date
ajp_anton fd1cf12b6b Release version 0.3.1 2026-07-19 01:39:11 +00:00
ajp_anton 3c82485a0a Add language-aware article sorting 2026-07-19 01:35:06 +00:00
ajp_anton 8c79cfd3e6 Use standard auth headers for Jellyfin 12 2026-07-16 00:50:12 +00:00
ajp_anton a6acccfa66 Retarget plugin to Jellyfin 12 RC 2026-07-16 00:42:52 +00:00
ajp_anton e38ccf7a0c Release version 0.2.1 2026-07-16 00:40:31 +00:00
9 changed files with 20 additions and 47 deletions
+2 -2
View File
@@ -3,8 +3,8 @@
Multilang is a Jellyfin plugin that lets different users see different metadata Multilang is a Jellyfin plugin that lets different users see different metadata
languages without changing Jellyfin's stored metadata. languages without changing Jellyfin's stored metadata.
The plugin is built for the Jellyfin 10.11.x stable series on Linux x64 and is The `main` branch is built for the Jellyfin 10.11.x stable series. The
currently early beta software. `jellyfin12` branch targets Jellyfin 12 and is currently early beta software.
## What It Does ## What It Does
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "9.0.116", "version": "10.0.110",
"rollForward": "latestFeature" "rollForward": "latestFeature"
} }
} }
@@ -322,6 +322,7 @@ public sealed class MultilangController : ControllerBase
var upstreamSw = Stopwatch.StartNew(); var upstreamSw = Stopwatch.StartNew();
var http = _httpClientFactory.CreateClient(); var http = _httpClientFactory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Get, proxyRequest.Upstream); using var request = new HttpRequestMessage(HttpMethod.Get, proxyRequest.Upstream);
request.Headers.TryAddWithoutValidation("Authorization", $"MediaBrowser Token=\"{token}\"");
request.Headers.TryAddWithoutValidation("X-Emby-Token", token); request.Headers.TryAddWithoutValidation("X-Emby-Token", token);
request.Headers.TryAddWithoutValidation("X-MediaBrowser-Token", token); request.Headers.TryAddWithoutValidation("X-MediaBrowser-Token", token);
using var response = await http.SendAsync(request, cancellationToken).ConfigureAwait(false); using var response = await http.SendAsync(request, cancellationToken).ConfigureAwait(false);
@@ -1263,10 +1264,6 @@ public sealed class MultilangController : ControllerBase
if (string.IsNullOrWhiteSpace(token)) if (string.IsNullOrWhiteSpace(token))
return null; return null;
var tokenUserId = await GetUserIdFromJellyfinApiAsync(token).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(tokenUserId))
return tokenUserId;
var remoteEndpoint = HttpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty; var remoteEndpoint = HttpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty;
var deviceId = info?.DeviceId ?? string.Empty; var deviceId = info?.DeviceId ?? string.Empty;
var session = await _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint).ConfigureAwait(false); var session = await _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint).ConfigureAwait(false);
@@ -1287,34 +1284,6 @@ public sealed class MultilangController : ControllerBase
: null; : null;
} }
private async Task<string?> GetUserIdFromJellyfinApiAsync(string token)
{
var baseUri = $"{Request.Scheme}://{Request.Host}{Request.PathBase}";
var usersMe = new UriBuilder(new Uri(new Uri(baseUri), "/Users/Me"));
usersMe.Query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(
string.Empty,
"api_key",
token).TrimStart('?');
var http = _httpClientFactory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Get, usersMe.Uri);
request.Headers.TryAddWithoutValidation("X-Emby-Token", token);
request.Headers.TryAddWithoutValidation("X-MediaBrowser-Token", token);
using var response = await http.SendAsync(request, HttpContext.RequestAborted).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
return null;
await using var stream = await response.Content.ReadAsStreamAsync(HttpContext.RequestAborted).ConfigureAwait(false);
using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: HttpContext.RequestAborted).ConfigureAwait(false);
if (!doc.RootElement.TryGetProperty("Id", out var idProperty))
return null;
var raw = idProperty.GetString();
return Guid.TryParse(raw, out var guid)
? guid.ToString("N")
: raw?.Length == 32 && raw.All(Uri.IsHexDigit) ? raw.ToLowerInvariant() : null;
}
private async Task<bool> IsRequesterAdminAsync() private async Task<bool> IsRequesterAdminAsync()
{ {
var info = await _authorizationContext.GetAuthorizationInfo(HttpContext).ConfigureAwait(false); var info = await _authorizationContext.GetAuthorizationInfo(HttpContext).ConfigureAwait(false);
@@ -19,6 +19,7 @@
const t = token() || qs.get("token") || qs.get("api_key") || ""; const t = token() || qs.get("token") || qs.get("api_key") || "";
const h = { "Content-Type": "application/json" }; const h = { "Content-Type": "application/json" };
if (t) { if (t) {
h.Authorization = `MediaBrowser Token="${t}"`;
h["X-Emby-Token"] = t; h["X-Emby-Token"] = t;
h["X-MediaBrowser-Token"] = t; h["X-MediaBrowser-Token"] = t;
} }
@@ -31,6 +32,7 @@
const h = {}; const h = {};
if (contentType) h["Content-Type"] = contentType; if (contentType) h["Content-Type"] = contentType;
if (t) { if (t) {
h.Authorization = `MediaBrowser Token="${t}"`;
h["X-Emby-Token"] = t; h["X-Emby-Token"] = t;
h["X-MediaBrowser-Token"] = t; h["X-MediaBrowser-Token"] = t;
} }
@@ -2,14 +2,14 @@
<PropertyGroup> <PropertyGroup>
<RootNamespace>Jellyfin.Plugin.Multilang</RootNamespace> <RootNamespace>Jellyfin.Plugin.Multilang</RootNamespace>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier> <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
<Version>0.2.2</Version> <Version>0.3.1</Version>
<AssemblyVersion>0.2.2.0</AssemblyVersion> <AssemblyVersion>0.3.1.0</AssemblyVersion>
<FileVersion>0.2.2.0</FileVersion> <FileVersion>0.3.1.0</FileVersion>
<Authors>ajp_anton</Authors> <Authors>ajp_anton</Authors>
</PropertyGroup> </PropertyGroup>
@@ -18,13 +18,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.11.9"> <PackageReference Include="Jellyfin.Controller" Version="12.0.0-rc2">
<ExcludeAssets>runtime</ExcludeAssets> <ExcludeAssets>runtime</ExcludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Jellyfin.Model" Version="10.11.9"> <PackageReference Include="Jellyfin.Model" Version="12.0.0-rc2">
<ExcludeAssets>runtime</ExcludeAssets> <ExcludeAssets>runtime</ExcludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -71,7 +71,6 @@ public static class ItemsProxyRequestBuilder
} }
var cacheQuery = QueryHelpers.AddQueryString(string.Empty, pairs).TrimStart('?'); var cacheQuery = QueryHelpers.AddQueryString(string.Empty, pairs).TrimStart('?');
pairs.Add(KeyValuePair.Create<string, string?>("api_key", token));
builder.Query = QueryHelpers.AddQueryString(string.Empty, pairs).TrimStart('?'); builder.Query = QueryHelpers.AddQueryString(string.Empty, pairs).TrimStart('?');
var normalizedForCache = builder.Path + (cacheQuery.Length > 0 ? "?" + cacheQuery : string.Empty); var normalizedForCache = builder.Path + (cacheQuery.Length > 0 ? "?" + cacheQuery : string.Empty);
return new ItemsProxyRequest( return new ItemsProxyRequest(
@@ -311,7 +311,9 @@
function refreshItems(itemIds) { function refreshItems(itemIds) {
const token = getClientToken(); const token = getClientToken();
const headers = token ? { "X-Emby-Token": token, "X-MediaBrowser-Token": token } : {}; const headers = token
? { Authorization: `MediaBrowser Token="${token}"`, "X-Emby-Token": token, "X-MediaBrowser-Token": token }
: {};
itemIds.forEach((itemId) => { itemIds.forEach((itemId) => {
fetch(`${BASE_PATH}/Multilang/RefreshItem/${itemId}?includeChildren=true${token ? `&token=${encodeURIComponent(token)}` : ""}`, { method: "POST", headers }); fetch(`${BASE_PATH}/Multilang/RefreshItem/${itemId}?includeChildren=true${token ? `&token=${encodeURIComponent(token)}` : ""}`, { method: "POST", headers });
}); });
@@ -22,8 +22,8 @@ public sealed class ItemsProxyRequestBuilderTests
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal($"/Users/{UserId}/Items", result.Upstream.AbsolutePath); Assert.Equal($"/Users/{UserId}/Items", result.Upstream.AbsolutePath);
Assert.Contains("api_key=token", result.Upstream.Query);
Assert.DoesNotContain("api_key=old", result.Upstream.Query); Assert.DoesNotContain("api_key=old", result.Upstream.Query);
Assert.DoesNotContain("api_key", result.Upstream.Query, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("api_key", result.NormalizedUrlForCache, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("api_key", result.NormalizedUrlForCache, StringComparison.OrdinalIgnoreCase);
Assert.Contains("userId=" + UserId, result.NormalizedUrlForCache); Assert.Contains("userId=" + UserId, result.NormalizedUrlForCache);
} }
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
@@ -13,7 +13,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" /> <PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Jellyfin.Common" Version="10.11.9" /> <PackageReference Include="Jellyfin.Common" Version="12.0.0-rc2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />