Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d49fd4e1a1 | |||
| 13553ef1c6 | |||
| ac2e4dac9c | |||
| 0bbb3c36cd | |||
| db0d9b115c | |||
| bc343dd0c8 | |||
| 9ed4869ece | |||
| c2eebbe1a9 | |||
| e198b0fb92 | |||
| f6f6bbf978 | |||
| 75c2fce017 | |||
| 3c08e3c70c | |||
| d5c706b833 | |||
| ad4fb92577 | |||
| 8aec7f804d | |||
| 4c3de5146b | |||
| 1ce6587078 | |||
| 7942e89341 | |||
| 764dc8f326 | |||
| af8a02d289 |
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.4.2.0</Version>
|
||||
<AssemblyVersion>1.4.2.0</AssemblyVersion>
|
||||
<FileVersion>1.4.2.0</FileVersion>
|
||||
<Version>1.4.4.0</Version>
|
||||
<AssemblyVersion>1.4.4.0</AssemblyVersion>
|
||||
<FileVersion>1.4.4.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -172,10 +172,17 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
{
|
||||
long progress = (long)eventArgs.PlaybackPositionTicks / TimeSpan.TicksPerSecond;
|
||||
var videoId = Utils.GetVideoNameFromPath(eventArgs.Item.Path);
|
||||
var statusCode = await TubeArchivistApi.GetInstance().SetProgress(videoId, progress).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,17 +198,21 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
var topParent = eventArgs.Item.GetTopParent();
|
||||
var user = _userManager.GetUserById(eventArgs.UserId);
|
||||
if (
|
||||
Configuration.JFTAProgressSync &&
|
||||
user != null &&
|
||||
Configuration.GetJFUsernamesToArray().Contains(user!.Username) &&
|
||||
string.Equals(topParent?.Name, Instance?.Configuration.CollectionTitle, StringComparison.OrdinalIgnoreCase)
|
||||
Configuration.JFTAProgressSync
|
||||
&& user == null
|
||||
&& string.Equals(topParent?.Name, Instance?.Configuration.CollectionTitle, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
Logger.LogError("OnWatchedStatusChange callback called without user id for item {ItemName}", eventArgs.Item.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Configuration.JFTAProgressSync && user != null && Configuration.GetJFUsernamesToArray().Contains(user!.Username))
|
||||
if (
|
||||
Configuration.JFTAProgressSync
|
||||
&& user != null
|
||||
&& string.Equals(Configuration.JFUsernameFrom, user!.Username, StringComparison.Ordinal)
|
||||
&& string.Equals(topParent?.Name, Instance?.Configuration.CollectionTitle, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
var userItemData = _userDataManager.GetUserData(user, eventArgs.Item);
|
||||
var isPlayed = eventArgs.Item.IsPlayed(user, userItemData);
|
||||
|
||||
@@ -155,11 +155,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
var videoYTId = Utils.GetVideoNameFromPath(video.Path);
|
||||
_logger.LogDebug("Current video extracted YouTube id: {VideoYtId}", videoYTId);
|
||||
HttpStatusCode statusCode;
|
||||
var userItemData = _userDataManager.GetUserData(user, channel);
|
||||
var channelItemData = _userDataManager.GetUserData(user, channel);
|
||||
|
||||
if (!isChannelCheckedForWatched && channel.IsPlayed(user, userItemData))
|
||||
if (!isChannelCheckedForWatched && channel.IsPlayed(user, channelItemData))
|
||||
{
|
||||
var isChannelPlayed = channel.IsPlayed(user, userItemData);
|
||||
var isChannelPlayed = channel.IsPlayed(user, channelItemData);
|
||||
statusCode = await taApi.SetWatchedStatus(channelYTId, isChannelPlayed).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
@@ -173,13 +173,26 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
isChannelCheckedForWatched = true;
|
||||
}
|
||||
|
||||
var videoItemData = _userDataManager.GetUserData(user, video);
|
||||
if (!isChannelWatched)
|
||||
{
|
||||
var isVideoPlayed = video.IsPlayed(user, userItemData);
|
||||
statusCode = await taApi.SetWatchedStatus(videoYTId, isVideoPlayed).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
var isVideoPlayed = video.IsPlayed(user, videoItemData);
|
||||
var taVideo = await taApi.GetVideo(videoYTId).ConfigureAwait(true);
|
||||
if (taVideo != null)
|
||||
{
|
||||
_logger.LogCritical("{Message}", $"POST /watched returned {statusCode} for video {video.Name} ({videoYTId}) with wacthed status {isVideoPlayed}");
|
||||
var isTAVideoPlayed = taVideo?.Player.IsWatched ?? false;
|
||||
if (isTAVideoPlayed != isVideoPlayed)
|
||||
{
|
||||
statusCode = await taApi.SetWatchedStatus(videoYTId, isVideoPlayed).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogCritical("{Message}", $"POST /watched returned {statusCode} for video {video.Name} ({videoYTId}) with wacthed status {isVideoPlayed}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Video {VideoId} watch status marked as {Status} in TubeArchivist", videoYTId, isVideoPlayed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("{Message}", isVideoPlayed);
|
||||
@@ -188,10 +201,17 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
var playbackProgress = _userDataManager.GetUserData(user, video)?.PlaybackPositionTicks / TimeSpan.TicksPerSecond;
|
||||
if (playbackProgress != null)
|
||||
{
|
||||
statusCode = await taApi.SetProgress(videoYTId, playbackProgress.Value).ConfigureAwait(true);
|
||||
if (statusCode != System.Net.HttpStatusCode.OK)
|
||||
try
|
||||
{
|
||||
_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;
|
||||
progress.Report(processedVideosCount * 100 / totalVideosCount);
|
||||
|
||||
if (totalVideosCount == 0)
|
||||
{
|
||||
progress.Report(100);
|
||||
}
|
||||
else
|
||||
{
|
||||
progress.Report(processedVideosCount * 100 / totalVideosCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
[JsonProperty(PropertyName = "playlist_type")]
|
||||
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.
|
||||
/// </summary>
|
||||
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;
|
||||
_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<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;
|
||||
|
||||
@@ -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
|
||||
/// <returns>The URL string without spaces, doubled slashes and with a trailing slash.</returns>
|
||||
public static string SanitizeUrl(string inputUrl)
|
||||
{
|
||||
// Extract the schema part
|
||||
Match schemaMatch = Regex.Match(inputUrl, @"^(?<schema>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, @"^(?<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
|
||||
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;
|
||||
}
|
||||
|
||||
+3
-2
@@ -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,5 @@ owner: "DarkFighterLuke"
|
||||
artifacts:
|
||||
- "Jellyfin.Plugin.TubeArchivistMetadata.dll"
|
||||
changelog: >
|
||||
Introduce episode numbering alternative schema
|
||||
Handle TubeArchivist playlists pagination
|
||||
Handle prematurely ended response errors
|
||||
|
||||
@@ -8,6 +8,22 @@
|
||||
"overview": "Metadata for your TubeArchivist library on Jellyfin",
|
||||
"imageUrl": "https://raw.githubusercontent.com/tubearchivist/tubearchivist-jf-plugin/master/images/logo.png",
|
||||
"versions": [
|
||||
{
|
||||
"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",
|
||||
"changelog": "Introduce episode numbering alternative schema\n",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"sourceUrl": "https://github.com/tubearchivist/tubearchivist-jf-plugin/releases/download/v1.4.2/tubearchivistmetadata_1.4.2.0.zip",
|
||||
"checksum": "8df142d2f1f0c0dffa302a567b40ef84",
|
||||
"timestamp": "2025-10-23T20:27:32Z"
|
||||
},
|
||||
{
|
||||
"version": "1.4.1.0",
|
||||
"changelog": "Introduce bidirectional playlists synchronization\n",
|
||||
|
||||
Reference in New Issue
Block a user