Extract ItemsProxy request builder
This commit is contained in:
@@ -267,7 +267,7 @@ public sealed class MultilangController : ControllerBase
|
|||||||
return Unauthorized(new { Error = "NotAuthenticated" });
|
return Unauthorized(new { Error = "NotAuthenticated" });
|
||||||
|
|
||||||
var rules = GetRulesOrDefault(userId);
|
var rules = GetRulesOrDefault(userId);
|
||||||
var proxyRequest = BuildProxyRequest(url, token, userId, rules.Enabled);
|
var proxyRequest = ItemsProxyRequestBuilder.Build(Request, url, token, userId, rules.Enabled);
|
||||||
if (proxyRequest is null)
|
if (proxyRequest is null)
|
||||||
return BadRequest(new { Error = "InvalidUrl" });
|
return BadRequest(new { Error = "InvalidUrl" });
|
||||||
|
|
||||||
@@ -413,7 +413,7 @@ public sealed class MultilangController : ControllerBase
|
|||||||
var sortCulture = GetSortCulture(sortLocale);
|
var sortCulture = GetSortCulture(sortLocale);
|
||||||
var sortArticles = GetSortArticles(sortLocale);
|
var sortArticles = GetSortArticles(sortLocale);
|
||||||
|
|
||||||
var localGenreIds = rules.Enabled ? ParseLocalGenreIds(controls.GenreIds) : [];
|
var localGenreIds = rules.Enabled ? ItemsProxyRequestBuilder.ParseLocalGenreIds(controls.GenreIds) : [];
|
||||||
if (rules.Enabled || localGenreIds.Length > 0)
|
if (rules.Enabled || localGenreIds.Length > 0)
|
||||||
_refreshService.EnqueueOnTheFlyMissing(ids);
|
_refreshService.EnqueueOnTheFlyMissing(ids);
|
||||||
|
|
||||||
@@ -871,15 +871,6 @@ public sealed class MultilangController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int[] ParseLocalGenreIds(string raw)
|
|
||||||
=> (raw ?? string.Empty)
|
|
||||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
||||||
.Select(v => v.StartsWith("tmdb-", StringComparison.OrdinalIgnoreCase) ? v[5..] : string.Empty)
|
|
||||||
.Where(v => int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
|
||||||
.Select(v => int.Parse(v, CultureInfo.InvariantCulture))
|
|
||||||
.Distinct()
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
private static int[] ParseJsonIntArray(string json)
|
private static int[] ParseJsonIntArray(string json)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
@@ -1779,165 +1770,6 @@ public sealed class MultilangController : ControllerBase
|
|||||||
return [JellyfinAction];
|
return [JellyfinAction];
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record ProxyRequest(
|
|
||||||
Uri Upstream,
|
|
||||||
ItemsProxyControls Controls,
|
|
||||||
string CacheKey,
|
|
||||||
string NormalizedUrlForCache,
|
|
||||||
bool IsGenresRequest,
|
|
||||||
string GenreMedia);
|
|
||||||
|
|
||||||
private ProxyRequest? BuildProxyRequest(string rawUrl, string token, string userId, bool multilangEnabled)
|
|
||||||
{
|
|
||||||
var baseUri = $"{Request.Scheme}://{Request.Host}{Request.PathBase}";
|
|
||||||
var baseAddress = new Uri(baseUri);
|
|
||||||
var uri = new Uri(baseAddress, rawUrl.StartsWith('/') ? rawUrl : "/" + rawUrl);
|
|
||||||
if ((rawUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
rawUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) &&
|
|
||||||
Uri.TryCreate(rawUrl, UriKind.Absolute, out var absolute))
|
|
||||||
{
|
|
||||||
if (!absolute.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
!absolute.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!absolute.Host.Equals(baseAddress.Host, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
absolute.Port != baseAddress.Port)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
uri = absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = NormalizeUpstreamPath(uri.AbsolutePath, userId);
|
|
||||||
var builder = new UriBuilder(uri);
|
|
||||||
builder.Path = path;
|
|
||||||
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(builder.Query);
|
|
||||||
var rawControls = new ItemsProxyControls(
|
|
||||||
GetQueryString(query, "SortBy"),
|
|
||||||
GetQueryString(query, "SortOrder"),
|
|
||||||
GetQueryString(query, "NameStartsWith"),
|
|
||||||
GetQueryInt(query, "StartIndex"),
|
|
||||||
GetQueryInt(query, "Limit"),
|
|
||||||
Request.Query.TryGetValue("mlLocale", out var clientLocale) ? clientLocale.ToString() : string.Empty,
|
|
||||||
GetQueryString(query, "GenreIds"));
|
|
||||||
var hasLocalGenreFilter = multilangEnabled && ParseLocalGenreIds(rawControls.GenreIds).Length > 0;
|
|
||||||
var useLocalControls = multilangEnabled && ShouldUseLocalControls(path, query, hasLocalGenreFilter);
|
|
||||||
var controls = useLocalControls
|
|
||||||
? rawControls
|
|
||||||
: new ItemsProxyControls(string.Empty, string.Empty, string.Empty, 0, 0, rawControls.ClientLocale, rawControls.GenreIds);
|
|
||||||
var localCacheKey = string.Join('|',
|
|
||||||
controls.SortBy,
|
|
||||||
controls.SortOrder,
|
|
||||||
controls.NameStartsWith,
|
|
||||||
controls.StartIndex.ToString(CultureInfo.InvariantCulture),
|
|
||||||
controls.Limit.ToString(CultureInfo.InvariantCulture),
|
|
||||||
controls.ClientLocale,
|
|
||||||
controls.GenreIds);
|
|
||||||
var pairs = query
|
|
||||||
.Where(kv => !IsLocalProxyQuery(kv.Key, hasLocalGenreFilter, useLocalControls))
|
|
||||||
.SelectMany(kv => kv.Value.Select(v => KeyValuePair.Create<string, string?>(kv.Key, v ?? string.Empty)))
|
|
||||||
.ToList();
|
|
||||||
pairs.Add(KeyValuePair.Create<string, string?>("userId", userId));
|
|
||||||
if (hasLocalGenreFilter)
|
|
||||||
{
|
|
||||||
pairs.Add(KeyValuePair.Create<string, string?>("EnableImageTypes", "Primary,Backdrop,Banner,Thumb"));
|
|
||||||
pairs.Add(KeyValuePair.Create<string, string?>("Fields", "Genres,GenreItems"));
|
|
||||||
}
|
|
||||||
|
|
||||||
var cacheQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, pairs).TrimStart('?');
|
|
||||||
pairs.Add(KeyValuePair.Create<string, string?>("api_key", token));
|
|
||||||
builder.Query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, pairs).TrimStart('?');
|
|
||||||
var upstream = builder.Uri;
|
|
||||||
var normalizedForCache = builder.Path + (cacheQuery.Length > 0 ? "?" + cacheQuery : string.Empty);
|
|
||||||
return new ProxyRequest(
|
|
||||||
upstream,
|
|
||||||
controls,
|
|
||||||
$"{userId}|{normalizedForCache}|{localCacheKey}",
|
|
||||||
normalizedForCache,
|
|
||||||
IsGenresPath(path),
|
|
||||||
GenreMediaFromQuery(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ShouldUseLocalControls(
|
|
||||||
string normalizedPath,
|
|
||||||
IReadOnlyDictionary<string, Microsoft.Extensions.Primitives.StringValues> query,
|
|
||||||
bool hasGenreFilter)
|
|
||||||
{
|
|
||||||
if (hasGenreFilter || !string.IsNullOrWhiteSpace(GetQueryString(query, "NameStartsWith")))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var segments = normalizedPath
|
|
||||||
.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
||||||
.Select(s => s.ToLowerInvariant())
|
|
||||||
.ToArray();
|
|
||||||
var itemsIndex = Array.LastIndexOf(segments, "items");
|
|
||||||
if (itemsIndex < 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return itemsIndex == segments.Length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsGenresPath(string path)
|
|
||||||
{
|
|
||||||
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
||||||
return segments.Length > 0 && segments[^1].Equals("Genres", StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GenreMediaFromQuery(IReadOnlyDictionary<string, Microsoft.Extensions.Primitives.StringValues> query)
|
|
||||||
{
|
|
||||||
var types = GetQueryString(query, "IncludeItemTypes")
|
|
||||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
||||||
if (types.Any(t => t.Equals("Series", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
t.Equals("Season", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
t.Equals("Episode", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
return "tv";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "movie";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static bool IsLocalProxyQuery(string key, bool hasGenreFilter, bool useLocalControls)
|
|
||||||
=> key.Equals("token", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
key.Equals("api_key", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
key.Equals("mlLocale", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
key.Equals("userId", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
(useLocalControls && (key.Equals("SortBy", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
key.Equals("SortOrder", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
key.Equals("StartIndex", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
key.Equals("Limit", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
key.Equals("NameStartsWith", StringComparison.OrdinalIgnoreCase))) ||
|
|
||||||
(hasGenreFilter && (key.Equals("GenreIds", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
key.Equals("EnableTotalRecordCount", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
key.Equals("EnableImageTypes", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
key.Equals("Fields", StringComparison.OrdinalIgnoreCase)));
|
|
||||||
|
|
||||||
private static string NormalizeUpstreamPath(string path, string userId)
|
|
||||||
{
|
|
||||||
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries).ToList();
|
|
||||||
if (segments.Count >= 2 &&
|
|
||||||
segments[0].Equals("Users", StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
segments[1].Length == 32 &&
|
|
||||||
segments[1].All(Uri.IsHexDigit))
|
|
||||||
{
|
|
||||||
segments[1] = userId;
|
|
||||||
return "/" + string.Join('/', segments);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segments.Count > 0 && segments[0].Equals("Items", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return "/Users/" + userId + "/" + string.Join('/', segments);
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetQueryString(IReadOnlyDictionary<string, Microsoft.Extensions.Primitives.StringValues> query, string key)
|
|
||||||
=> query.TryGetValue(key, out var value) ? value.ToString() : string.Empty;
|
|
||||||
|
|
||||||
private static int GetQueryInt(IReadOnlyDictionary<string, Microsoft.Extensions.Primitives.StringValues> query, string key)
|
|
||||||
=> int.TryParse(GetQueryString(query, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
|
|
||||||
? parsed
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
private static string ResolveSortLocale(string? configuredLocale, string? clientLocale)
|
private static string ResolveSortLocale(string? configuredLocale, string? clientLocale)
|
||||||
{
|
{
|
||||||
var configured = (configuredLocale ?? string.Empty).Trim();
|
var configured = (configuredLocale ?? string.Empty).Trim();
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.Multilang.Services;
|
||||||
|
|
||||||
|
public sealed record ItemsProxyRequest(
|
||||||
|
Uri Upstream,
|
||||||
|
ItemsProxyControls Controls,
|
||||||
|
string CacheKey,
|
||||||
|
string NormalizedUrlForCache,
|
||||||
|
bool IsGenresRequest,
|
||||||
|
string GenreMedia);
|
||||||
|
|
||||||
|
public static class ItemsProxyRequestBuilder
|
||||||
|
{
|
||||||
|
public static ItemsProxyRequest? Build(HttpRequest request, string rawUrl, string token, string userId, bool multilangEnabled)
|
||||||
|
{
|
||||||
|
var baseUri = $"{request.Scheme}://{request.Host}{request.PathBase}";
|
||||||
|
var baseAddress = new Uri(baseUri);
|
||||||
|
var uri = new Uri(baseAddress, rawUrl.StartsWith('/') ? rawUrl : "/" + rawUrl);
|
||||||
|
if ((rawUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
rawUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) &&
|
||||||
|
Uri.TryCreate(rawUrl, UriKind.Absolute, out var absolute))
|
||||||
|
{
|
||||||
|
if (!absolute.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!absolute.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!absolute.Host.Equals(baseAddress.Host, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
absolute.Port != baseAddress.Port)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
uri = absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = NormalizeUpstreamPath(uri.AbsolutePath, userId);
|
||||||
|
var builder = new UriBuilder(uri) { Path = path };
|
||||||
|
var query = QueryHelpers.ParseQuery(builder.Query);
|
||||||
|
var rawControls = new ItemsProxyControls(
|
||||||
|
GetQueryString(query, "SortBy"),
|
||||||
|
GetQueryString(query, "SortOrder"),
|
||||||
|
GetQueryString(query, "NameStartsWith"),
|
||||||
|
GetQueryInt(query, "StartIndex"),
|
||||||
|
GetQueryInt(query, "Limit"),
|
||||||
|
request.Query.TryGetValue("mlLocale", out var clientLocale) ? clientLocale.ToString() : string.Empty,
|
||||||
|
GetQueryString(query, "GenreIds"));
|
||||||
|
var hasLocalGenreFilter = multilangEnabled && ParseLocalGenreIds(rawControls.GenreIds).Length > 0;
|
||||||
|
var useLocalControls = multilangEnabled && ShouldUseLocalControls(path, query, hasLocalGenreFilter);
|
||||||
|
var controls = useLocalControls
|
||||||
|
? rawControls
|
||||||
|
: new ItemsProxyControls(string.Empty, string.Empty, string.Empty, 0, 0, rawControls.ClientLocale, rawControls.GenreIds);
|
||||||
|
var localCacheKey = string.Join('|',
|
||||||
|
controls.SortBy,
|
||||||
|
controls.SortOrder,
|
||||||
|
controls.NameStartsWith,
|
||||||
|
controls.StartIndex.ToString(CultureInfo.InvariantCulture),
|
||||||
|
controls.Limit.ToString(CultureInfo.InvariantCulture),
|
||||||
|
controls.ClientLocale,
|
||||||
|
controls.GenreIds);
|
||||||
|
var pairs = query
|
||||||
|
.Where(kv => !IsLocalProxyQuery(kv.Key, hasLocalGenreFilter, useLocalControls))
|
||||||
|
.SelectMany(kv => kv.Value.Select(v => KeyValuePair.Create<string, string?>(kv.Key, v ?? string.Empty)))
|
||||||
|
.ToList();
|
||||||
|
pairs.Add(KeyValuePair.Create<string, string?>("userId", userId));
|
||||||
|
if (hasLocalGenreFilter)
|
||||||
|
{
|
||||||
|
pairs.Add(KeyValuePair.Create<string, string?>("EnableImageTypes", "Primary,Backdrop,Banner,Thumb"));
|
||||||
|
pairs.Add(KeyValuePair.Create<string, string?>("Fields", "Genres,GenreItems"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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('?');
|
||||||
|
var normalizedForCache = builder.Path + (cacheQuery.Length > 0 ? "?" + cacheQuery : string.Empty);
|
||||||
|
return new ItemsProxyRequest(
|
||||||
|
builder.Uri,
|
||||||
|
controls,
|
||||||
|
$"{userId}|{normalizedForCache}|{localCacheKey}",
|
||||||
|
normalizedForCache,
|
||||||
|
IsGenresPath(path),
|
||||||
|
GenreMediaFromQuery(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] ParseLocalGenreIds(string raw)
|
||||||
|
=> (raw ?? string.Empty)
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(v => v.StartsWith("tmdb-", StringComparison.OrdinalIgnoreCase) ? v[5..] : string.Empty)
|
||||||
|
.Where(v => int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
||||||
|
.Select(v => int.Parse(v, CultureInfo.InvariantCulture))
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
private static bool ShouldUseLocalControls(
|
||||||
|
string normalizedPath,
|
||||||
|
IReadOnlyDictionary<string, StringValues> query,
|
||||||
|
bool hasGenreFilter)
|
||||||
|
{
|
||||||
|
if (hasGenreFilter || !string.IsNullOrWhiteSpace(GetQueryString(query, "NameStartsWith")))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var segments = normalizedPath
|
||||||
|
.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(s => s.ToLowerInvariant())
|
||||||
|
.ToArray();
|
||||||
|
var itemsIndex = Array.LastIndexOf(segments, "items");
|
||||||
|
return itemsIndex == segments.Length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsGenresPath(string path)
|
||||||
|
{
|
||||||
|
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
return segments.Length > 0 && segments[^1].Equals("Genres", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GenreMediaFromQuery(IReadOnlyDictionary<string, StringValues> query)
|
||||||
|
{
|
||||||
|
var types = GetQueryString(query, "IncludeItemTypes")
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
return types.Any(t => t.Equals("Series", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
t.Equals("Season", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
t.Equals("Episode", StringComparison.OrdinalIgnoreCase))
|
||||||
|
? "tv"
|
||||||
|
: "movie";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsLocalProxyQuery(string key, bool hasGenreFilter, bool useLocalControls)
|
||||||
|
=> key.Equals("token", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
key.Equals("api_key", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
key.Equals("mlLocale", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
key.Equals("userId", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(useLocalControls && (key.Equals("SortBy", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
key.Equals("SortOrder", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
key.Equals("StartIndex", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
key.Equals("Limit", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
key.Equals("NameStartsWith", StringComparison.OrdinalIgnoreCase))) ||
|
||||||
|
(hasGenreFilter && (key.Equals("GenreIds", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
key.Equals("EnableTotalRecordCount", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
key.Equals("EnableImageTypes", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
key.Equals("Fields", StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
|
private static string NormalizeUpstreamPath(string path, string userId)
|
||||||
|
{
|
||||||
|
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||||
|
if (segments.Count >= 2 &&
|
||||||
|
segments[0].Equals("Users", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
segments[1].Length == 32 &&
|
||||||
|
segments[1].All(Uri.IsHexDigit))
|
||||||
|
{
|
||||||
|
segments[1] = userId;
|
||||||
|
return "/" + string.Join('/', segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.Count > 0 && segments[0].Equals("Items", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "/Users/" + userId + "/" + string.Join('/', segments)
|
||||||
|
: path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetQueryString(IReadOnlyDictionary<string, StringValues> query, string key)
|
||||||
|
=> query.TryGetValue(key, out var value) ? value.ToString() : string.Empty;
|
||||||
|
|
||||||
|
private static int GetQueryInt(IReadOnlyDictionary<string, StringValues> query, string key)
|
||||||
|
=> int.TryParse(GetQueryString(query, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
|
||||||
|
? parsed
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user