Compare commits
5
Commits
main
..
jellyfin12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd1cf12b6b | ||
|
|
3c82485a0a | ||
|
|
8c79cfd3e6 | ||
|
|
a6acccfa66 | ||
|
|
e38ccf7a0c |
@@ -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
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user