diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Providers/EpisodeImageProvider.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Providers/EpisodeImageProvider.cs index fa00c01..09edc66 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Providers/EpisodeImageProvider.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Providers/EpisodeImageProvider.cs @@ -87,7 +87,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers } else { - return await Plugin.Instance.HttpClient.GetAsync(new Uri(Utils.SanitizeUrl(Plugin.Instance.Configuration.TubeArchivistUrl + url).TrimEnd('/')), cancellationToken).ConfigureAwait(false); + return await Plugin.Instance.HttpClient.GetAsync(Utils.ResolveUrl(Plugin.Instance.Configuration.TubeArchivistUrl, url), cancellationToken).ConfigureAwait(false); } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Providers/EpisodeMetadataProvider.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Providers/EpisodeMetadataProvider.cs index d62e530..ac7f1ee 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Providers/EpisodeMetadataProvider.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Providers/EpisodeMetadataProvider.cs @@ -100,7 +100,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers } else { - return await Plugin.Instance.HttpClient.GetAsync(new Uri(Utils.SanitizeUrl(Plugin.Instance.Configuration.TubeArchivistUrl + url).TrimEnd('/')), cancellationToken).ConfigureAwait(false); + return await Plugin.Instance.HttpClient.GetAsync(Utils.ResolveUrl(Plugin.Instance.Configuration.TubeArchivistUrl, url), cancellationToken).ConfigureAwait(false); } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Providers/SeriesImageProvider.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Providers/SeriesImageProvider.cs index 6fef96a..d195974 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Providers/SeriesImageProvider.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Providers/SeriesImageProvider.cs @@ -105,7 +105,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers } else { - return await Plugin.Instance.HttpClient.GetAsync(new Uri(Utils.SanitizeUrl(Plugin.Instance.Configuration.TubeArchivistUrl + url).TrimEnd('/')), cancellationToken).ConfigureAwait(false); + return await Plugin.Instance.HttpClient.GetAsync(Utils.ResolveUrl(Plugin.Instance.Configuration.TubeArchivistUrl, url), cancellationToken).ConfigureAwait(false); } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Providers/SeriesMetadataProvider.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Providers/SeriesMetadataProvider.cs index d44a847..dd87cd0 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Providers/SeriesMetadataProvider.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Providers/SeriesMetadataProvider.cs @@ -98,7 +98,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers } else { - return await Plugin.Instance.HttpClient.GetAsync(new Uri(Utils.SanitizeUrl(Plugin.Instance.Configuration.TubeArchivistUrl + url).TrimEnd('/')), cancellationToken).ConfigureAwait(false); + return await Plugin.Instance.HttpClient.GetAsync(Utils.ResolveUrl(Plugin.Instance.Configuration.TubeArchivistUrl, url), cancellationToken).ConfigureAwait(false); } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs index 123ca60..a8ef3c0 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs @@ -17,6 +17,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist /// public class TubeArchivistApi { + private const int MaxRedirectCount = 10; private ILogger _logger; private HttpClient client; private static TubeArchivistApi _taApiInstance = null!; @@ -60,6 +61,42 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist return _taApiInstance; } + private static bool IsRedirectStatusCode(HttpStatusCode statusCode) + { + return statusCode == HttpStatusCode.Moved + || statusCode == HttpStatusCode.Redirect + || statusCode == HttpStatusCode.RedirectMethod + || statusCode == HttpStatusCode.TemporaryRedirect + || statusCode == HttpStatusCode.PermanentRedirect; + } + + private static Uri ResolveTubeArchivistUri(string? url) + { + return Utils.ResolveUrl(Plugin.Instance!.Configuration.TubeArchivistUrl, url); + } + + private async Task GetAsync(string url) + { + var resolvedUrl = ResolveTubeArchivistUri(url); + var response = await client.GetAsync(resolvedUrl).ConfigureAwait(true); + var redirectCount = 0; + + while (IsRedirectStatusCode(response.StatusCode) && response.Headers.Location != null && redirectCount < MaxRedirectCount) + { + resolvedUrl = ResolveTubeArchivistUri(response.Headers.Location.OriginalString); + _logger.LogDebug("Received redirect to: {ResolvedUrl}", resolvedUrl); + response = await client.GetAsync(resolvedUrl).ConfigureAwait(true); + redirectCount++; + } + + if (redirectCount >= MaxRedirectCount && IsRedirectStatusCode(response.StatusCode) && response.Headers.Location != null) + { + _logger.LogWarning("Stopped following TubeArchivist redirect chain after {RedirectCount} redirects. Last location: {RedirectLocation}", redirectCount, response.Headers.Location); + } + + return response; + } + /// /// Retrieves the given channel information from TubeArchivist. /// @@ -70,14 +107,9 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist Channel? channel = null; var channelsEndpoint = "/api/channel/"; - var url = new Uri(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + channelsEndpoint + channelId)); - var response = await client.GetAsync(url).ConfigureAwait(true); - while (response.StatusCode == HttpStatusCode.Moved) - { - url = response.Headers.Location; - _logger.LogDebug("{Message}", "Received redirect to: " + url); - response = await client.GetAsync(url).ConfigureAwait(true); - } + var requestPath = channelsEndpoint + channelId; + var url = ResolveTubeArchivistUri(requestPath); + var response = await GetAsync(requestPath).ConfigureAwait(true); _logger.LogDebug("{Message}", url + ": " + response.StatusCode); @@ -100,14 +132,9 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist Video? video = null; var videosEndpoint = "/api/video/"; - var url = new Uri(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + videosEndpoint + videoId)); - var response = await client.GetAsync(url).ConfigureAwait(true); - while (response.StatusCode == HttpStatusCode.Moved) - { - url = response.Headers.Location; - _logger.LogDebug("{Message}", "Received redirect to: " + url); - response = await client.GetAsync(url).ConfigureAwait(true); - } + var requestPath = videosEndpoint + videoId; + var url = ResolveTubeArchivistUri(requestPath); + var response = await GetAsync(requestPath).ConfigureAwait(true); _logger.LogDebug("{Message}", url + ": " + response.StatusCode); @@ -130,15 +157,8 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist PingResponse? pong = null; var pingEndpoint = "/api/ping/"; - var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + pingEndpoint)); - var response = await client.GetAsync(url).ConfigureAwait(true); - - while (response.StatusCode == HttpStatusCode.Moved) - { - url = response.Headers.Location; - _logger.LogDebug("{Message}", "Received redirect to: " + url); - response = await client.GetAsync(url).ConfigureAwait(true); - } + var url = ResolveTubeArchivistUri(pingEndpoint); + var response = await GetAsync(pingEndpoint).ConfigureAwait(true); _logger.LogDebug("{Message}", url + ": " + response.StatusCode); @@ -213,14 +233,8 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist ResponseContainer?>? playlists = null; var playlistsEndpoint = "/api/playlist/"; - var url = new Uri(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + playlistsEndpoint)); - var response = await client.GetAsync(url).ConfigureAwait(true); - while (response.StatusCode == HttpStatusCode.Moved) - { - url = response.Headers.Location; - _logger.LogInformation("{Message}", "Received redirect to: " + url); - response = await client.GetAsync(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + url)).ConfigureAwait(true); - } + var url = ResolveTubeArchivistUri(playlistsEndpoint); + var response = await GetAsync(playlistsEndpoint).ConfigureAwait(true); _logger.LogInformation("{Message}", url + ": " + response.StatusCode); @@ -236,14 +250,9 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist while (playlists.Paginate.CurrentPage < lastPage) { var nextPage = playlists.Paginate.CurrentPage + 1; - var pagedUrl = new Uri(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + playlistsEndpoint + "?page=" + nextPage)); - response = await client.GetAsync(pagedUrl).ConfigureAwait(true); - while (response.StatusCode == HttpStatusCode.Moved) - { - url = response.Headers.Location; - _logger.LogInformation("{Message}", "Received redirect to: " + url); - response = await client.GetAsync(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + url)).ConfigureAwait(true); - } + var nextPagePath = playlistsEndpoint + "?page=" + nextPage; + var pagedUrl = ResolveTubeArchivistUri(nextPagePath); + response = await GetAsync(nextPagePath).ConfigureAwait(true); _logger.LogInformation("{Message}", pagedUrl + ": " + response.StatusCode); diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Utils/Utils.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Utils/Utils.cs index f526db0..1e43fa7 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Utils/Utils.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Utils/Utils.cs @@ -77,6 +77,38 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities return cleanedUrl; } + /// + /// Resolves a URL against the configured TubeArchivist base URL. + /// + /// Configured TubeArchivist base URL. + /// The relative or absolute URL to resolve. If null or whitespace, the base URL is returned. + /// + /// A resolved absolute . + /// Absolute URLs pointing to a different origin keep only their path, query and fragment, + /// so requests stay pinned to the configured TubeArchivist host. + /// + public static Uri ResolveUrl(string baseUrl, string? url) + { + var sanitizedBaseUrl = SanitizeUrl(baseUrl); + var baseUri = new Uri(sanitizedBaseUrl, UriKind.Absolute); + if (string.IsNullOrWhiteSpace(url)) + { + return baseUri; + } + + if (Uri.TryCreate(url, UriKind.Absolute, out Uri? absoluteUri)) + { + if (Uri.Compare(baseUri, absoluteUri, UriComponents.SchemeAndServer, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase) == 0) + { + return absoluteUri; + } + + return new Uri(baseUri, absoluteUri.PathAndQuery + absoluteUri.Fragment); + } + + return new Uri(baseUri, url); + } + /// /// Formats episodes and series descriptions replacing newlines with br tags. ///