From c2eebbe1a9977909bc39dc65ad0f8491ab6fd54a Mon Sep 17 00:00:00 2001 From: "REPLYNET\\l.consoli" Date: Tue, 18 Nov 2025 15:52:13 +0100 Subject: [PATCH 1/4] Handle TA playlists pagination --- .../TubeArchivist/PaginationInfo.cs | 99 +++++++++++++++++++ .../TubeArchivist/ResponseContainer.cs | 5 + .../TubeArchivist/TubeArchivistApi.cs | 32 ++++++ 3 files changed, 136 insertions(+) create mode 100644 Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/PaginationInfo.cs diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/PaginationInfo.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/PaginationInfo.cs new file mode 100644 index 0000000..ca6e2d8 --- /dev/null +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/PaginationInfo.cs @@ -0,0 +1,99 @@ +using System.Collections.ObjectModel; +using Newtonsoft.Json; + +namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist +{ + /// + /// A class representing pagination information from the TubeArchivist API. + /// + public class PaginationInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The size of the page. + /// The starting page number. + /// The collection of previous page numbers. + /// The current page number. + /// A value indicating whether the max hits have been reached. + /// The parameters used for pagination. + /// The last page number. + /// The collection of next page numbers. + /// The total number of hits. + public PaginationInfo( + int pageSize, + int pageFrom, + Collection prevPages, + int currentPage, + bool maxHits, + string parameters, + int lastPage, + Collection nextPages, + int totalHits) + { + this.PageSize = pageSize; + this.PageFrom = pageFrom; + this.PrevPages = prevPages; + this.CurrentPage = currentPage; + this.MaxHits = maxHits; + this.Parameters = parameters; + this.LastPage = lastPage; + this.NextPages = nextPages; + this.TotalHits = totalHits; + } + + /// + /// Gets the page size. + /// + [JsonProperty(PropertyName = "page_size")] + public int PageSize { get; } + + /// + /// Gets the page from. + /// + [JsonProperty(PropertyName = "page_from")] + public int PageFrom { get; } + + /// + /// Gets the previous pages. + /// + [JsonProperty(PropertyName = "prev_pages")] + public Collection PrevPages { get; } + + /// + /// Gets the current page. + /// + [JsonProperty(PropertyName = "current_page")] + public int CurrentPage { get; } + + /// + /// Gets a value indicating whether the max hits have been reached. + /// + [JsonProperty(PropertyName = "max_hits")] + public bool MaxHits { get; } + + /// + /// Gets the parameters. + /// + [JsonProperty(PropertyName = "params")] + public string Parameters { get; } + + /// + /// Gets the last page. + /// + [JsonProperty(PropertyName = "last_page")] + public int LastPage { get; } + + /// + /// Gets the next pages. + /// + [JsonProperty(PropertyName = "next_pages")] + public Collection NextPages { get; } + + /// + /// Gets the total hits. + /// + [JsonProperty(PropertyName = "total_hits")] + public int TotalHits { get; } + } +} diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/ResponseContainer.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/ResponseContainer.cs index 753c10a..9c4a38c 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/ResponseContainer.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/ResponseContainer.cs @@ -10,5 +10,10 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist /// Gets or sets the contained data object. /// public T? Data { get; set; } + + /// + /// Gets or sets the pagination info object. + /// + public PaginationInfo? Paginate { get; set; } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs index 3b81f5d..fe13273 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs @@ -228,6 +228,38 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist { string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true); playlists = JsonConvert.DeserializeObject?>>(rawData); + if (playlists?.Paginate != null) + { + var lastPage = playlists.Paginate.LastPage; + _logger.LogInformation("Pagination info: Current page {CurrentPage} / Last page {LastPage}, Total hits: {TotalHits}", playlists.Paginate.CurrentPage, playlists.Paginate.LastPage, playlists.Paginate.TotalHits); + + 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); + _logger.LogInformation("{Message}", pagedUrl + ": " + response.StatusCode); + + if (response.IsSuccessStatusCode) + { + rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true); + var nextPagePlaylists = JsonConvert.DeserializeObject?>>(rawData); + if (nextPagePlaylists?.Data != null) + { + foreach (var playlist in nextPagePlaylists.Data) + { + playlists.Data?.Add(playlist); + } + } + + if (nextPagePlaylists?.Paginate != null) + { + playlists.Paginate = nextPagePlaylists.Paginate; + _logger.LogInformation("Pagination info: Current page {CurrentPage} / Last page {LastPage}, Total hits: {TotalHits}", playlists.Paginate.CurrentPage, playlists.Paginate.LastPage, playlists.Paginate.TotalHits); + } + } + } + } } return playlists?.Data; From bc343dd0c85bb9a354f4c91ce7e24ebcfcff6515 Mon Sep 17 00:00:00 2001 From: "REPLYNET\\l.consoli" Date: Tue, 2 Dec 2025 12:31:07 +0100 Subject: [PATCH 2/4] Edit SanitizeUrl to handle also query parameters modified: Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinPlaylistsSyncTask.cs modified: Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs modified: Jellyfin.Plugin.TubeArchivistMetadata/Utils/Utils.cs --- .../Tasks/TAToJellyfinPlaylistsSyncTask.cs | 10 +++- .../TubeArchivist/TubeArchivistApi.cs | 14 ++++- .../Utils/Utils.cs | 57 +++++++++++++++---- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinPlaylistsSyncTask.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinPlaylistsSyncTask.cs index a2eccba..19c66ae 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinPlaylistsSyncTask.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinPlaylistsSyncTask.cs @@ -170,7 +170,15 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks } processedVideosCount += currentPlaylistVideos; - progress.Report(processedVideosCount * 100 / totalVideosCount); + + if (totalVideosCount == 0) + { + progress.Report(100); + } + else + { + progress.Report(processedVideosCount * 100 / totalVideosCount); + } } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs index fe13273..123ca60 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs @@ -219,7 +219,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist { url = response.Headers.Location; _logger.LogInformation("{Message}", "Received redirect to: " + url); - response = await client.GetAsync(url).ConfigureAwait(true); + response = await client.GetAsync(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + url)).ConfigureAwait(true); } _logger.LogInformation("{Message}", url + ": " + response.StatusCode); @@ -238,6 +238,13 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist 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); + } + _logger.LogInformation("{Message}", pagedUrl + ": " + response.StatusCode); if (response.IsSuccessStatusCode) @@ -258,6 +265,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist _logger.LogInformation("Pagination info: Current page {CurrentPage} / Last page {LastPage}, Total hits: {TotalHits}", playlists.Paginate.CurrentPage, playlists.Paginate.LastPage, playlists.Paginate.TotalHits); } } + else + { + _logger.LogCritical("Failed to retrieve page {PageNumber} of playlists during pagination.", nextPage); + break; + } } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Utils/Utils.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Utils/Utils.cs index be9f97b..f526db0 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Utils/Utils.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Utils/Utils.cs @@ -1,8 +1,6 @@ using System; -using System.IO; using System.Linq; using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities { @@ -22,20 +20,59 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities /// The URL string without spaces, doubled slashes and with a trailing slash. public static string SanitizeUrl(string inputUrl) { - // Extract the schema part - Match schemaMatch = Regex.Match(inputUrl, @"^(?https?://)"); + if (string.IsNullOrWhiteSpace(inputUrl)) + { + return string.Empty; + } - // Remove double slashes and spaces from the remaining part of the URL - string cleanedPath = Regex.Replace(inputUrl.Substring(schemaMatch.Length), @"[/\s]+", "/"); + // Extract the schema part (http:// or https://) + Match schemaMatch = Regex.Match(inputUrl, @"^(?https?://)", RegexOptions.IgnoreCase); + + // If no schema found, treat whole string as the rest + int schemaLength = schemaMatch.Success ? schemaMatch.Length : 0; + + // Separate the main part from query (?) and fragment (#) + string rest = inputUrl.Substring(schemaLength); + string pathPart = rest; + string queryAndFragment = string.Empty; + + int qIndex = rest.IndexOf('?', StringComparison.Ordinal); + int fIndex = rest.IndexOf('#', StringComparison.Ordinal); + + int splitIndex = -1; + if (qIndex >= 0 && fIndex >= 0) + { + splitIndex = Math.Min(qIndex, fIndex); + } + else if (qIndex >= 0) + { + splitIndex = qIndex; + } + else if (fIndex >= 0) + { + splitIndex = fIndex; + } + + if (splitIndex >= 0) + { + pathPart = rest.Substring(0, splitIndex); + queryAndFragment = rest.Substring(splitIndex); + } + + // Remove double slashes and spaces from the path part + string cleanedPath = Regex.Replace(pathPart, @"[/\s]+", "/"); // Remove slashes at the start cleanedPath = cleanedPath.TrimStart('/'); - // Add a trailing slash if not already present - cleanedPath = cleanedPath.TrimEnd('/') + "/"; + // Add a trailing slash only when there are no query or fragment parts + if (string.IsNullOrEmpty(queryAndFragment)) + { + cleanedPath = cleanedPath.TrimEnd('/') + "/"; + } - // Combine the schema and cleaned path - string cleanedUrl = schemaMatch.Groups["schema"].Value + cleanedPath; + // Combine the schema and cleaned path and re-append query/fragment + string cleanedUrl = (schemaMatch.Success ? schemaMatch.Groups["schema"].Value : string.Empty) + cleanedPath + queryAndFragment; return cleanedUrl; } From db0d9b115c0a250995ad49ee717effa1ede94002 Mon Sep 17 00:00:00 2001 From: "REPLYNET\\l.consoli" Date: Tue, 2 Dec 2025 12:37:15 +0100 Subject: [PATCH 3/4] Update version --- Directory.Build.props | 6 +++--- build.yaml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index ead052b..641cdf1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 1.4.2.0 - 1.4.2.0 - 1.4.2.0 + 1.4.4.0 + 1.4.4.0 + 1.4.4.0 diff --git a/build.yaml b/build.yaml index f58f7e0..42c71fc 100644 --- a/build.yaml +++ b/build.yaml @@ -2,7 +2,7 @@ name: "TubeArchivistMetadata" guid: "dc97d0c6-28b0-4242-afb4-5833ae1b3715" imageUrl: https://raw.githubusercontent.com/tubearchivist/tubearchivist-jf-plugin/master/images/logo.png -version: "1.4.2.0" +version: "1.4.4.0" targetAbi: "10.11.0.0" framework: "net9.0" overview: "Metadata for your TubeArchivist library on Jellyfin" @@ -14,4 +14,4 @@ owner: "DarkFighterLuke" artifacts: - "Jellyfin.Plugin.TubeArchivistMetadata.dll" changelog: > - Introduce episode numbering alternative schema + Handle TubeArchivist playlists pagination From 0bbb3c36cd6cd459fee56400e5fd36f83db0b038 Mon Sep 17 00:00:00 2001 From: "REPLYNET\\l.consoli" Date: Tue, 2 Dec 2025 14:27:19 +0100 Subject: [PATCH 4/4] Add Playlist ToString method --- .../TubeArchivist/Playlist/Playlist.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Playlist/Playlist.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Playlist/Playlist.cs index 91e0a16..c33e3f7 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Playlist/Playlist.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Playlist/Playlist.cs @@ -112,5 +112,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist /// One of the values indicating the playlist's type. [JsonProperty(PropertyName = "playlist_type")] public PlaylistType Type { get; set; } + + /// + /// Returns a string of the playlist's key fields for logging. + /// + /// A string containing the playlist id, name, and entries count. + public override string ToString() + { + return "PlaylistId: " + this.Id + + ", Name: " + this.Name + + ", EntriesCount: " + this.Entries.Count; + } } }