Compare commits

..
11 Commits
Author SHA1 Message Date
felixfoertsch 754d4abd45 release tubearchivistmetadata 1.4.5.0
TubeArchivistMetadata Release / Build & Release (push) Failing after 1m34s
2026-07-01 09:58:49 +02:00
felixfoertsch 5f4afcc28e scope tubearchivist providers, fix sync intervals 2026-07-01 08:12:39 +02:00
DarkFighterLuke 42c02b33eb Update manifest.json
Signed-off-by: DarkFighterLuke <DarkFighterLuke@users.noreply.github.com>
2025-12-02 13:40:50 +00:00
DarkFighterLuke 13553ef1c6 Merge pull request #77 from DerSeegler/master
Add error handling to SetProgress calls
2025-12-02 14:37:59 +01:00
DarkFighterLuke ac2e4dac9c Merge branch 'master' into master 2025-12-02 14:37:51 +01:00
REPLYNET\l.consoli 0bbb3c36cd Add Playlist ToString method 2025-12-02 14:27:19 +01:00
REPLYNET\l.consoli db0d9b115c Update version 2025-12-02 12:37:15 +01:00
REPLYNET\l.consoli bc343dd0c8 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
2025-12-02 12:31:44 +01:00
Yannik S. 9ed4869ece Add error handling to SetProgress calls 2025-11-28 23:59:02 +01:00
REPLYNET\l.consoli c2eebbe1a9 Handle TA playlists pagination 2025-11-18 15:52:13 +01:00
DarkFighterLuke e198b0fb92 Update manifest.json
Signed-off-by: DarkFighterLuke <DarkFighterLuke@users.noreply.github.com>
2025-11-13 19:31:52 +00:00
18 changed files with 355 additions and 47 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>1.4.3.0</Version> <Version>1.4.5.0</Version>
<AssemblyVersion>1.4.3.0</AssemblyVersion> <AssemblyVersion>1.4.5.0</AssemblyVersion>
<FileVersion>1.4.3.0</FileVersion> <FileVersion>1.4.5.0</FileVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
@@ -172,10 +172,17 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
{ {
long progress = (long)eventArgs.PlaybackPositionTicks / TimeSpan.TicksPerSecond; long progress = (long)eventArgs.PlaybackPositionTicks / TimeSpan.TicksPerSecond;
var videoId = Utils.GetVideoNameFromPath(eventArgs.Item.Path); var videoId = Utils.GetVideoNameFromPath(eventArgs.Item.Path);
var statusCode = await TubeArchivistApi.GetInstance().SetProgress(videoId, progress).ConfigureAwait(true); try
if (statusCode != System.Net.HttpStatusCode.OK)
{ {
Logger.LogCritical("{Message}", $"POST /video/{videoId}/progress returned {statusCode} for video {eventArgs.Item.Name} with progress {progress} seconds"); var statusCode = await TubeArchivistApi.GetInstance().SetProgress(videoId, progress).ConfigureAwait(true);
if (statusCode != System.Net.HttpStatusCode.OK)
{
Logger.LogCritical("{Message}", $"POST /video/{videoId}/progress returned {statusCode} for video {eventArgs.Item.Name} with progress {progress} seconds");
}
}
catch (Exception ex)
{
Logger.LogCritical("An exception occurred while calling POST /video/{VideoId}/progress for for video {VideoName} with progress {Progress} seconds: {ExceptionMessage}", videoId, eventArgs.Item.Name, progress, ex.Message);
} }
} }
} }
@@ -47,7 +47,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
public string Name => "TubeArchivist"; public string Name => "TubeArchivist";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(BaseItem item) => item is Episode; public bool Supports(BaseItem item) => item is Episode && Utils.IsTubeArchivistItem(item);
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
@@ -65,7 +65,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
_logger.LogDebug("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting images for video: {0} ({1})", video?.Title, videoTAId)); _logger.LogDebug("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting images for video: {0} ({1})", video?.Title, videoTAId));
_logger.LogDebug("{Message}", "Thumb URI: " + video?.VidThumbUrl); _logger.LogDebug("{Message}", "Thumb URI: " + video?.VidThumbUrl);
if (video != null) if (video != null && Utils.HasImageUrl(video.VidThumbUrl))
{ {
list.Add(new RemoteImageInfo list.Add(new RemoteImageInfo
{ {
@@ -87,6 +87,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
} }
else else
{ {
if (!Utils.HasImageUrl(url))
{
throw new HttpRequestException("TubeArchivist returned an empty image URL.");
}
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(new Uri(Utils.SanitizeUrl(Plugin.Instance.Configuration.TubeArchivistUrl + url).TrimEnd('/')), cancellationToken).ConfigureAwait(false);
} }
} }
@@ -50,6 +50,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken) public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
{ {
var result = new MetadataResult<Episode>(); var result = new MetadataResult<Episode>();
if (string.IsNullOrWhiteSpace(info.Path))
{
return result;
}
var taApi = TubeArchivistApi.GetInstance(); var taApi = TubeArchivistApi.GetInstance();
var videoTAId = Utils.GetVideoNameFromPath(info.Path); var videoTAId = Utils.GetVideoNameFromPath(info.Path);
var video = await taApi.GetVideo(videoTAId).ConfigureAwait(true); var video = await taApi.GetVideo(videoTAId).ConfigureAwait(true);
@@ -79,6 +84,10 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{ {
var results = new List<RemoteSearchResult>(); var results = new List<RemoteSearchResult>();
if (string.IsNullOrWhiteSpace(searchInfo.Path))
{
return results;
}
var taApi = TubeArchivistApi.GetInstance(); var taApi = TubeArchivistApi.GetInstance();
var videoTAId = Utils.GetVideoNameFromPath(searchInfo.Path); var videoTAId = Utils.GetVideoNameFromPath(searchInfo.Path);
@@ -45,12 +45,12 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
public string Name => "TubeArchivist"; public string Name => "TubeArchivist";
/// <inheritdoc /> /// <inheritdoc />
public bool Supports(BaseItem item) => item is Series; public bool Supports(BaseItem item) => item is Series && Utils.IsTubeArchivistItem(item);
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{ {
return new[] { ImageType.Primary }; return new[] { ImageType.Primary, ImageType.Art, ImageType.Banner, ImageType.Backdrop };
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -62,10 +62,8 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
var channel = await taApi.GetChannel(channelTAId).ConfigureAwait(true); var channel = await taApi.GetChannel(channelTAId).ConfigureAwait(true);
_logger.LogDebug("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting images for channel: {0} ({1})", channel?.Name, channelTAId)); _logger.LogDebug("{Message}", string.Format(CultureInfo.CurrentCulture, "Getting images for channel: {0} ({1})", channel?.Name, channelTAId));
_logger.LogDebug("{Message}", "Thumb URI: " + channel?.ThumbUrl); _logger.LogDebug("{Message}", "Thumb URI: " + channel?.ThumbUrl);
_logger.LogDebug("{Message}", "TVArt URI: " + channel?.TvartUrl);
_logger.LogDebug("{Message}", "Banner URI: " + channel?.BannerUrl);
if (channel != null) if (channel != null && Utils.HasImageUrl(channel.ThumbUrl))
{ {
list.Add(new RemoteImageInfo list.Add(new RemoteImageInfo
{ {
@@ -73,18 +71,17 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
Type = ImageType.Primary, Type = ImageType.Primary,
Url = channel.ThumbUrl Url = channel.ThumbUrl
}); });
}
if (channel != null && Utils.HasImageUrl(channel.TvartUrl))
{
list.Add(new RemoteImageInfo list.Add(new RemoteImageInfo
{ {
ProviderName = Name, ProviderName = Name,
Type = ImageType.Art, Type = ImageType.Art,
Url = channel.TvartUrl Url = channel.TvartUrl
}); });
list.Add(new RemoteImageInfo
{
ProviderName = Name,
Type = ImageType.Banner,
Url = channel.BannerUrl
});
list.Add(new RemoteImageInfo list.Add(new RemoteImageInfo
{ {
ProviderName = Name, ProviderName = Name,
@@ -93,6 +90,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
}); });
} }
if (channel != null && Utils.HasImageUrl(channel.BannerUrl))
{
list.Add(new RemoteImageInfo
{
ProviderName = Name,
Type = ImageType.Banner,
Url = channel.BannerUrl
});
}
return list; return list;
} }
@@ -105,6 +112,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
} }
else else
{ {
if (!Utils.HasImageUrl(url))
{
throw new HttpRequestException("TubeArchivist returned an empty image URL.");
}
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(new Uri(Utils.SanitizeUrl(Plugin.Instance.Configuration.TubeArchivistUrl + url).TrimEnd('/')), cancellationToken).ConfigureAwait(false);
} }
} }
@@ -49,6 +49,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
{ {
var result = new MetadataResult<Series>(); var result = new MetadataResult<Series>();
if (string.IsNullOrWhiteSpace(info.Path))
{
return result;
}
var taApi = TubeArchivistApi.GetInstance(); var taApi = TubeArchivistApi.GetInstance();
var channelTAId = Utils.GetChannelNameFromPath(info.Path); var channelTAId = Utils.GetChannelNameFromPath(info.Path);
var channel = await taApi.GetChannel(channelTAId).ConfigureAwait(true); var channel = await taApi.GetChannel(channelTAId).ConfigureAwait(true);
@@ -77,6 +82,10 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Providers
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{ {
var results = new List<RemoteSearchResult>(); var results = new List<RemoteSearchResult>();
if (string.IsNullOrWhiteSpace(searchInfo.Path))
{
return results;
}
var taApi = TubeArchivistApi.GetInstance(); var taApi = TubeArchivistApi.GetInstance();
var channelTAId = Utils.GetChannelNameFromPath(searchInfo.Path); var channelTAId = Utils.GetChannelNameFromPath(searchInfo.Path);
@@ -379,7 +379,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
new TaskTriggerInfo new TaskTriggerInfo
{ {
Type = TaskTriggerInfoType.IntervalTrigger, Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFProgressTaskInterval).Ticks IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.JFTAPlaylistsSyncTaskInterval).Ticks
}, },
]; ];
} }
@@ -201,10 +201,17 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
var playbackProgress = _userDataManager.GetUserData(user, video)?.PlaybackPositionTicks / TimeSpan.TicksPerSecond; var playbackProgress = _userDataManager.GetUserData(user, video)?.PlaybackPositionTicks / TimeSpan.TicksPerSecond;
if (playbackProgress != null) if (playbackProgress != null)
{ {
statusCode = await taApi.SetProgress(videoYTId, playbackProgress.Value).ConfigureAwait(true); try
if (statusCode != System.Net.HttpStatusCode.OK)
{ {
_logger.LogCritical("{Message}", $"POST /video/{videoYTId}/progress returned {statusCode} for video {video.Name} with progress {progress} seconds"); statusCode = await taApi.SetProgress(videoYTId, playbackProgress.Value).ConfigureAwait(true);
if (statusCode != System.Net.HttpStatusCode.OK)
{
_logger.LogCritical("{Message}", $"POST /video/{videoYTId}/progress returned {statusCode} for video {video.Name} with progress {progress} seconds");
}
}
catch (Exception ex)
{
_logger.LogCritical("An exception occurred while calling POST /video/{VideoId}/progress for for video {VideoName} with progress {Progress} seconds: {ExceptionMessage}", videoYTId, videoYTId, playbackProgress.Value, ex.Message);
} }
} }
} }
@@ -170,7 +170,15 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
} }
processedVideosCount += currentPlaylistVideos; processedVideosCount += currentPlaylistVideos;
progress.Report(processedVideosCount * 100 / totalVideosCount);
if (totalVideosCount == 0)
{
progress.Report(100);
}
else
{
progress.Report(processedVideosCount * 100 / totalVideosCount);
}
} }
} }
} }
@@ -193,7 +201,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
new TaskTriggerInfo new TaskTriggerInfo
{ {
Type = TaskTriggerInfoType.IntervalTrigger, Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFProgressTaskInterval).Ticks IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFPlaylistsSyncTaskInterval).Ticks
}, },
]; ];
} }
@@ -96,7 +96,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
{ {
Name = Name, Name = Name,
SearchProviderName = Constants.ProviderName, SearchProviderName = Constants.ProviderName,
ImageUrl = ThumbUrl, ImageUrl = Utils.HasImageUrl(ThumbUrl) ? ThumbUrl : null,
ProviderIds = new Dictionary<string, string>() { { Constants.ProviderName, Id } } ProviderIds = new Dictionary<string, string>() { { Constants.ProviderName, Id } }
}; };
} }
@@ -118,14 +118,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
Constants.ProviderName, Id Constants.ProviderName, Id
} }
}, },
ImageInfos = new[] ImageInfos = Utils.HasImageUrl(ThumbUrl) ?
{ [
new ItemImageInfo new ItemImageInfo
{ {
Path = ThumbUrl, Path = ThumbUrl,
Type = ImageType.Primary Type = ImageType.Primary
} }
},
] :
[],
Tags = this.Tags != null ? Tags = this.Tags != null ?
this.Tags.ToArray<string>() : this.Tags.ToArray<string>() :
[] []
@@ -0,0 +1,99 @@
using System.Collections.ObjectModel;
using Newtonsoft.Json;
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
{
/// <summary>
/// A class representing pagination information from the TubeArchivist API.
/// </summary>
public class PaginationInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="PaginationInfo"/> class.
/// </summary>
/// <param name="pageSize">The size of the page.</param>
/// <param name="pageFrom">The starting page number.</param>
/// <param name="prevPages">The collection of previous page numbers.</param>
/// <param name="currentPage">The current page number.</param>
/// <param name="maxHits">A value indicating whether the max hits have been reached.</param>
/// <param name="parameters">The parameters used for pagination.</param>
/// <param name="lastPage">The last page number.</param>
/// <param name="nextPages">The collection of next page numbers.</param>
/// <param name="totalHits">The total number of hits.</param>
public PaginationInfo(
int pageSize,
int pageFrom,
Collection<int> prevPages,
int currentPage,
bool maxHits,
string parameters,
int lastPage,
Collection<int> 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;
}
/// <summary>
/// Gets the page size.
/// </summary>
[JsonProperty(PropertyName = "page_size")]
public int PageSize { get; }
/// <summary>
/// Gets the page from.
/// </summary>
[JsonProperty(PropertyName = "page_from")]
public int PageFrom { get; }
/// <summary>
/// Gets the previous pages.
/// </summary>
[JsonProperty(PropertyName = "prev_pages")]
public Collection<int> PrevPages { get; }
/// <summary>
/// Gets the current page.
/// </summary>
[JsonProperty(PropertyName = "current_page")]
public int CurrentPage { get; }
/// <summary>
/// Gets a value indicating whether the max hits have been reached.
/// </summary>
[JsonProperty(PropertyName = "max_hits")]
public bool MaxHits { get; }
/// <summary>
/// Gets the parameters.
/// </summary>
[JsonProperty(PropertyName = "params")]
public string Parameters { get; }
/// <summary>
/// Gets the last page.
/// </summary>
[JsonProperty(PropertyName = "last_page")]
public int LastPage { get; }
/// <summary>
/// Gets the next pages.
/// </summary>
[JsonProperty(PropertyName = "next_pages")]
public Collection<int> NextPages { get; }
/// <summary>
/// Gets the total hits.
/// </summary>
[JsonProperty(PropertyName = "total_hits")]
public int TotalHits { get; }
}
}
@@ -112,5 +112,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
/// <value>One of the <see cref="PlaylistType"/> values indicating the playlist's type.</value> /// <value>One of the <see cref="PlaylistType"/> values indicating the playlist's type.</value>
[JsonProperty(PropertyName = "playlist_type")] [JsonProperty(PropertyName = "playlist_type")]
public PlaylistType Type { get; set; } public PlaylistType Type { get; set; }
/// <summary>
/// Returns a string of the playlist's key fields for logging.
/// </summary>
/// <returns>A string containing the playlist id, name, and entries count.</returns>
public override string ToString()
{
return "PlaylistId: " + this.Id +
", Name: " + this.Name +
", EntriesCount: " + this.Entries.Count;
}
} }
} }
@@ -10,5 +10,10 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
/// Gets or sets the contained data object. /// Gets or sets the contained data object.
/// </summary> /// </summary>
public T? Data { get; set; } public T? Data { get; set; }
/// <summary>
/// Gets or sets the pagination info object.
/// </summary>
public PaginationInfo? Paginate { get; set; }
} }
} }
@@ -219,7 +219,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
{ {
url = response.Headers.Location; url = response.Headers.Location;
_logger.LogInformation("{Message}", "Received redirect to: " + url); _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); _logger.LogInformation("{Message}", url + ": " + response.StatusCode);
@@ -228,6 +228,50 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
{ {
string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true); string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true);
playlists = JsonConvert.DeserializeObject<ResponseContainer<ISet<Playlist>?>>(rawData); playlists = JsonConvert.DeserializeObject<ResponseContainer<ISet<Playlist>?>>(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<ResponseContainer<ISet<Playlist>?>>(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; return playlists?.Data;
@@ -109,7 +109,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
Name = Title, Name = Title,
SearchProviderName = Constants.ProviderName, SearchProviderName = Constants.ProviderName,
ProductionYear = Published.Year, ProductionYear = Published.Year,
ImageUrl = VidThumbUrl, ImageUrl = Utils.HasImageUrl(VidThumbUrl) ? VidThumbUrl : null,
PremiereDate = Published, PremiereDate = Published,
ProviderIds = new Dictionary<string, string>() { { Constants.ProviderName, YoutubeId } } ProviderIds = new Dictionary<string, string>() { { Constants.ProviderName, YoutubeId } }
}; };
@@ -142,14 +142,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
Constants.ProviderName, YoutubeId Constants.ProviderName, YoutubeId
} }
}, },
ImageInfos = new[] ImageInfos = Utils.HasImageUrl(VidThumbUrl) ?
{ [
new ItemImageInfo new ItemImageInfo
{ {
Path = VidThumbUrl, Path = VidThumbUrl,
Type = ImageType.Primary Type = ImageType.Primary
} }
},
] :
[],
Tags = this.Tags.ToArray<string>() Tags = this.Tags.ToArray<string>()
}; };
} }
@@ -1,8 +1,7 @@
using System; using System;
using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging; using MediaBrowser.Controller.Entities;
namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
{ {
@@ -22,20 +21,59 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
/// <returns>The URL string without spaces, doubled slashes and with a trailing slash.</returns> /// <returns>The URL string without spaces, doubled slashes and with a trailing slash.</returns>
public static string SanitizeUrl(string inputUrl) public static string SanitizeUrl(string inputUrl)
{ {
// Extract the schema part if (string.IsNullOrWhiteSpace(inputUrl))
Match schemaMatch = Regex.Match(inputUrl, @"^(?<schema>https?://)"); {
return string.Empty;
}
// Remove double slashes and spaces from the remaining part of the URL // Extract the schema part (http:// or https://)
string cleanedPath = Regex.Replace(inputUrl.Substring(schemaMatch.Length), @"[/\s]+", "/"); Match schemaMatch = Regex.Match(inputUrl, @"^(?<schema>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 // Remove slashes at the start
cleanedPath = cleanedPath.TrimStart('/'); cleanedPath = cleanedPath.TrimStart('/');
// Add a trailing slash if not already present // Add a trailing slash only when there are no query or fragment parts
cleanedPath = cleanedPath.TrimEnd('/') + "/"; if (string.IsNullOrEmpty(queryAndFragment))
{
cleanedPath = cleanedPath.TrimEnd('/') + "/";
}
// Combine the schema and cleaned path // Combine the schema and cleaned path and re-append query/fragment
string cleanedUrl = schemaMatch.Groups["schema"].Value + cleanedPath; string cleanedUrl = (schemaMatch.Success ? schemaMatch.Groups["schema"].Value : string.Empty) + cleanedPath + queryAndFragment;
return cleanedUrl; return cleanedUrl;
} }
@@ -88,6 +126,32 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
return path.Split(DetectDirectorySeparator(path)).Last(); return path.Split(DetectDirectorySeparator(path)).Last();
} }
/// <summary>
/// Checks whether the Jellyfin item belongs to the configured TubeArchivist collection.
/// </summary>
/// <param name="item">Jellyfin item.</param>
/// <returns>True when the item belongs to the configured TubeArchivist collection.</returns>
public static bool IsTubeArchivistItem(BaseItem item)
{
if (Plugin.Instance == null)
{
return false;
}
var topParent = item.GetTopParent();
return string.Equals(topParent?.Name, Plugin.Instance.Configuration.CollectionTitle, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Checks whether a TubeArchivist image URL can be fetched safely.
/// </summary>
/// <param name="url">Image URL returned by TubeArchivist.</param>
/// <returns>True when the image URL is non-empty.</returns>
public static bool HasImageUrl(string? url)
{
return !string.IsNullOrWhiteSpace(url);
}
private static char DetectDirectorySeparator(string path) private static char DetectDirectorySeparator(string path)
{ {
int backslashCount = path.Count(c => c == '\\'); int backslashCount = path.Count(c => c == '\\');
+2 -2
View File
@@ -2,7 +2,7 @@
name: "TubeArchivistMetadata" name: "TubeArchivistMetadata"
guid: "dc97d0c6-28b0-4242-afb4-5833ae1b3715" guid: "dc97d0c6-28b0-4242-afb4-5833ae1b3715"
imageUrl: https://raw.githubusercontent.com/tubearchivist/tubearchivist-jf-plugin/master/images/logo.png imageUrl: https://raw.githubusercontent.com/tubearchivist/tubearchivist-jf-plugin/master/images/logo.png
version: "1.4.3.0" version: "1.4.5.0"
targetAbi: "10.11.0.0" targetAbi: "10.11.0.0"
framework: "net9.0" framework: "net9.0"
overview: "Metadata for your TubeArchivist library on Jellyfin" overview: "Metadata for your TubeArchivist library on Jellyfin"
@@ -14,4 +14,4 @@ owner: "DarkFighterLuke"
artifacts: artifacts:
- "Jellyfin.Plugin.TubeArchivistMetadata.dll" - "Jellyfin.Plugin.TubeArchivistMetadata.dll"
changelog: > changelog: >
Fix JF->TA watched status sync Limit providers to TubeArchivist paths, skip empty image URLs, and fix playlist sync intervals
+25 -1
View File
@@ -8,6 +8,30 @@
"overview": "Metadata for your TubeArchivist library on Jellyfin", "overview": "Metadata for your TubeArchivist library on Jellyfin",
"imageUrl": "https://raw.githubusercontent.com/tubearchivist/tubearchivist-jf-plugin/master/images/logo.png", "imageUrl": "https://raw.githubusercontent.com/tubearchivist/tubearchivist-jf-plugin/master/images/logo.png",
"versions": [ "versions": [
{
"version": "1.4.5.0",
"changelog": "Limit providers to TubeArchivist paths, skip empty image URLs, and fix playlist sync intervals\n",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.felixfoertsch.de/felixfoertsch/tubearchivist-jf-plugin/releases/download/v1.4.5/tubearchivistmetadata_1.4.5.0.zip",
"checksum": "dad305a75a6a07f5d8185a6e8340cbbd",
"timestamp": "2026-07-01T06:20:00Z"
},
{
"version": "1.4.4.0",
"changelog": "Handle TubeArchivist playlists pagination\n",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.4.4/tubearchivistmetadata_1.4.4.0.zip",
"checksum": "90f1d388f7363537df8287e996a96bbd",
"timestamp": "2025-12-02T13:40:47Z"
},
{
"version": "1.4.3.0",
"changelog": "Introduce episode numbering alternative schema\n",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.4.3/tubearchivistmetadata_1.4.3.0.zip",
"checksum": "297d4f15af1132939f43c905b1ad393e",
"timestamp": "2025-11-13T19:31:49Z"
},
{ {
"version": "1.4.2.0", "version": "1.4.2.0",
"changelog": "Introduce episode numbering alternative schema\n", "changelog": "Introduce episode numbering alternative schema\n",
@@ -170,4 +194,4 @@
} }
] ]
} }
] ]