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/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/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/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;
+ }
}
}
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..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);
@@ -228,6 +228,50 @@ 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);
+ 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)
+ {
+ 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);
+ }
+ }
+ else
+ {
+ _logger.LogCritical("Failed to retrieve page {PageNumber} of playlists during pagination.", nextPage);
+ break;
+ }
+ }
+ }
}
return playlists?.Data;
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;
}
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