From 9179ea8602db30b43b36002de03a93af2843f87a Mon Sep 17 00:00:00 2001 From: DarkFighterLuke Date: Mon, 26 Aug 2024 15:04:47 +0200 Subject: [PATCH 01/19] Send playback progress to TubeArchivist --- .../Plugin.cs | 37 ++++++++++++++++++- .../TubeArchivist/TubeArchivistApi.cs | 24 ++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs index c1f3b40..9affaad 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs @@ -5,10 +5,15 @@ using System.Net.Http; using System.Threading.Tasks; using Jellyfin.Plugin.TubeArchivistMetadata.Configuration; using Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist; +using Jellyfin.Plugin.TubeArchivistMetadata.Utilities; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.TubeArchivistMetadata @@ -24,7 +29,9 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILogger logger) + /// Instance of the interface. + /// Instance of the interface. + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager) : base(applicationPaths, xmlSerializer) { Instance = this; @@ -34,6 +41,9 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata handler.CheckCertificateRevocationList = true; HttpClient = new HttpClient(handler); HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Token", Instance?.Configuration.TubeArchivistApiKey); + SessionManager = sessionManager; + sessionManager.PlaybackProgress += OnPlaybackProgress; + LibraryManager = libraryManager; logger.LogInformation("{Message}", "Collection display name: " + Instance?.Configuration.CollectionTitle); logger.LogInformation("{Message}", "TubeArchivist API URL: " + Instance?.Configuration.TubeArchivistUrl); logger.LogInformation("{Message}", "Pinging TubeArchivist API..."); @@ -60,6 +70,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata /// public HttpClient HttpClient { get; } + /// + /// Gets the SessionManager used globally by the plugin. + /// + public ISessionManager SessionManager { get; } + + /// + /// Gets the LibraryManager used globally by the plugin. + /// + public ILibraryManager LibraryManager { get; } + /// public IEnumerable GetPages() => new[] { @@ -92,5 +112,20 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata }, TaskScheduler.Default); } + + private async void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs eventArgs) + { + BaseItem? channel = LibraryManager.GetItemById(eventArgs.Item.ParentId); + BaseItem? collection = LibraryManager.GetItemById(channel!.ParentId); + if (collection?.Name.ToLower(CultureInfo.CurrentCulture) == Instance?.Configuration.CollectionTitle.ToLower(CultureInfo.CurrentCulture) && eventArgs.PlaybackPositionTicks != null) + { + long progress = (long)eventArgs.PlaybackPositionTicks / TimeSpan.TicksPerSecond; + var statusCode = await TubeArchivistApi.GetInstance().SetProgress(Utils.GetVideoNameFromPath(eventArgs.Item.Path), progress).ConfigureAwait(true); + if (statusCode != System.Net.HttpStatusCode.OK) + { + Logger.LogInformation("{Message}", $"POST /progress returned {statusCode} for video {eventArgs.Item.Name} with progress {progress} seconds"); + } + } + } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs index 53adfbb..ce7565e 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs @@ -2,6 +2,7 @@ using System; using System.Data; using System.Net; using System.Net.Http; +using System.Text; using System.Threading.Tasks; using Jellyfin.Plugin.TubeArchivistMetadata.Utilities; using Microsoft.Extensions.Logging; @@ -146,5 +147,28 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist return pong; } + + /// + /// Send video playback progress to TubeArchivist. + /// + /// Video id. + /// Progress in seconds. + /// Nothing if successful. + /// Throws this exception if the request was not successful. + public async Task SetProgress(string videoId, long progress) + { + var progressEndpoint = $"/api/video/{videoId}/progress/"; + var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + progressEndpoint)); + var body = JsonConvert.SerializeObject(new + { + position = progress + }); + + var sc = new StringContent(body, Encoding.UTF8, "application/json"); + + var response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json")).ConfigureAwait(true); + + return response.StatusCode; + } } } From cc280169844db076cb31d708f976f27d53163cbf Mon Sep 17 00:00:00 2001 From: DarkFighterLuke Date: Mon, 26 Aug 2024 18:11:06 +0200 Subject: [PATCH 02/19] Create progress TA API DTO --- .../TubeArchivist/Progress.cs | 53 +++++++++++++++++++ .../TubeArchivist/TubeArchivistApi.cs | 7 +-- 2 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Progress.cs diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Progress.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Progress.cs new file mode 100644 index 0000000..d4e6ce7 --- /dev/null +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Progress.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json; + +namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist +{ + /// + /// A class representing TubeArchivist API video progress data. + /// + public class Progress + { + /// + /// Initializes a new instance of the class. + /// + /// Playback progress in seconds. + /// Video YouTube id. + /// Playback progress user id. + public Progress( + long position, + string? youtubeId, + long? userId) + { + YoutubeId = youtubeId; + UserId = userId; + Position = position; + } + + /// + /// Initializes a new instance of the class. + /// + /// Playback progress in seconds. + public Progress(long position) + { + Position = position; + } + + /// + /// Gets or sets the video YouTube id. + /// + [JsonProperty(PropertyName = "youtube_id")] + public string? YoutubeId { get; set; } + + /// + /// Gets or sets playback progress user id. + /// + [JsonProperty(PropertyName = "user_id")] + public long? UserId { get; set; } + + /// + /// Gets or sets video playback progress (in seconds). + /// + [JsonProperty(PropertyName = "position")] + public long Position { get; set; } + } +} diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs index ce7565e..294bacf 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs @@ -159,12 +159,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist { var progressEndpoint = $"/api/video/{videoId}/progress/"; var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + progressEndpoint)); - var body = JsonConvert.SerializeObject(new - { - position = progress - }); - - var sc = new StringContent(body, Encoding.UTF8, "application/json"); + var body = JsonConvert.SerializeObject(new Progress(progress)); var response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json")).ConfigureAwait(true); From fd4a9a013045993c0dc1e8a1cc72067aa30dd5fe Mon Sep 17 00:00:00 2001 From: DarkFighterLuke Date: Tue, 27 Aug 2024 00:12:14 +0200 Subject: [PATCH 03/19] Implement TA->JF synchronization --- .../Plugin.cs | 21 ++- .../Tasks/TAToJellyfinProgressSyncTask.cs | 153 ++++++++++++++++++ .../TubeArchivist/TubeArchivistApi.cs | 24 ++- 3 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinProgressSyncTask.cs diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs index 9affaad..4a52a6d 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Jellyfin.Plugin.TubeArchivistMetadata.Configuration; +using Jellyfin.Plugin.TubeArchivistMetadata.Tasks; using Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist; using Jellyfin.Plugin.TubeArchivistMetadata.Utilities; using MediaBrowser.Common.Configuration; @@ -13,7 +15,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Session; +using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.TubeArchivistMetadata @@ -31,7 +33,18 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager) + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public Plugin( + IApplicationPaths applicationPaths, + IXmlSerializer xmlSerializer, + ILogger logger, + ISessionManager sessionManager, + ILibraryManager libraryManager, + ITaskManager taskManager, + IUserManager userManager, + IUserDataManager userDataManager) : base(applicationPaths, xmlSerializer) { Instance = this; @@ -44,6 +57,10 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata SessionManager = sessionManager; sessionManager.PlaybackProgress += OnPlaybackProgress; LibraryManager = libraryManager; + taskManager.AddTasks([new TAToJellyfinProgressSyncTask(logger, libraryManager, userManager, userDataManager)]); + taskManager.Execute(); + logger.LogInformation("Enabled tasks: {Tasks}", string.Join(", ", taskManager.ScheduledTasks.ToList().Select(t => t.Name))); + logger.LogInformation("{Message}", "Collection display name: " + Instance?.Configuration.CollectionTitle); logger.LogInformation("{Message}", "TubeArchivist API URL: " + Instance?.Configuration.TubeArchivistUrl); logger.LogInformation("{Message}", "Pinging TubeArchivist API..."); diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinProgressSyncTask.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinProgressSyncTask.cs new file mode 100644 index 0000000..3c36ef5 --- /dev/null +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinProgressSyncTask.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist; +using Jellyfin.Plugin.TubeArchivistMetadata.Utilities; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks +{ + /// + /// Task to sync TubeArchivist playback progresses to Jellyfin. + /// + public class TAToJellyfinProgressSyncTask : IScheduledTask + { + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataManager; + + /// + /// Initializes a new instance of the class. + /// + /// Logger. + /// Library manager. + /// User manager. + /// User data manager. + public TAToJellyfinProgressSyncTask(ILogger logger, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager) + { + _logger = logger; + _libraryManager = libraryManager; + _userManager = userManager; + _userDataManager = userDataManager; + } + + /// + public string Name => "TAToJellyfinProgressSyncTask"; + + /// + public string Description => "This tasks syncs TubeArchivist playback progresses to Jellyfin"; + + /// + public string Category => "TubeArchivistMetadata"; + + /// + public string Key => "TAToJellyfinProgressSyncTask"; + + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + // TODO: Check if the TA->JF synchronization is enabled from the configuration before proceeding + _logger.LogInformation("Starting TubeArchivist playback progresses synchronization."); + // TODO: Replace with the lists from configuration + var jfUsernames = new[] { "c", "c" }; + var taUsernames = new[] { "c", "c" }; + var taApi = TubeArchivistApi.GetInstance(); + foreach (var jfUsername in jfUsernames) + { + var user = _userManager.GetUserByName(jfUsername); + if (user == null) + { + _logger.LogInformation("{Message}", $"Jellyfin user with username {jfUsername} not found"); + continue; + } + + var collectionItem = _libraryManager.GetItemList(new InternalItemsQuery + { + Name = Plugin.Instance?.Configuration.CollectionTitle, + IncludeItemTypes = new[] { BaseItemKind.CollectionFolder } + }).FirstOrDefault(); + + if (collectionItem == null) + { + var message = $"Collection '{Plugin.Instance?.Configuration.CollectionTitle}' not found."; + _logger.LogCritical("{Message}", message); + } + else + { + var channels = _libraryManager.GetItemList(new InternalItemsQuery + { + ParentId = collectionItem.Id, + IncludeItemTypes = new[] { BaseItemKind.Series } + }); + + foreach (var channel in channels) + { + var years = _libraryManager.GetItemList(new InternalItemsQuery + { + ParentId = channel.Id, + IncludeItemTypes = new[] { BaseItemKind.Season } + }); + + foreach (var year in years) + { + var videos = _libraryManager.GetItemList(new InternalItemsQuery + { + ParentId = year.Id, + IncludeItemTypes = new[] { BaseItemKind.Episode } + }); + + foreach (var video in videos) + { + var playbackProgress = await taApi.GetProgress(Utils.GetVideoNameFromPath(video.Path)).ConfigureAwait(true); + if (playbackProgress != null) + { + var userItemData = _userDataManager.GetUserData(user, video); + var userUpdateData = new UpdateUserItemDataDto + { + PlaybackPositionTicks = playbackProgress.Position * TimeSpan.TicksPerSecond + }; + + // TODO: Also last played datetime should be updated once TA will return it + if (userItemData.PlaybackPositionTicks >= video.RunTimeTicks) + { + userUpdateData.Played = true; + } + else + { + userUpdateData.Played = false; + } + + _userDataManager.SaveUserData(user, video, userUpdateData, UserDataSaveReason.UpdateUserData); + _logger.LogInformation("{Message}", $"Playback progress for video {video.Name} set to {userItemData.PlaybackPositionTicks / TimeSpan.TicksPerSecond} seconds for user {jfUsername}."); + } + } + } + } + } + } + } + + /// + public IEnumerable GetDefaultTriggers() + { + return + [ + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, + // TODO: Use configuration defined interval + IntervalTicks = TimeSpan.FromSeconds(5).Ticks + }, + ]; + } + } +} diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs index 294bacf..13e1826 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs @@ -154,7 +154,6 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist /// Video id. /// Progress in seconds. /// Nothing if successful. - /// Throws this exception if the request was not successful. public async Task SetProgress(string videoId, long progress) { var progressEndpoint = $"/api/video/{videoId}/progress/"; @@ -165,5 +164,28 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist return response.StatusCode; } + + /// + /// Send video playback progress to TubeArchivist. + /// + /// Video id. + /// Video playback progress data. + public async Task GetProgress(string videoId) + { + Progress? progress = null; + + var progressEndpoint = $"/api/video/{videoId}/progress/"; + var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + progressEndpoint)); + + var response = await client.GetAsync(url).ConfigureAwait(true); + + if (response.IsSuccessStatusCode) + { + string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true); + progress = JsonConvert.DeserializeObject(rawData); + } + + return progress; + } } } From 9a40ba93e85810cc6b345879f6ea08afba9b3379 Mon Sep 17 00:00:00 2001 From: DarkFighterLuke Date: Fri, 30 Aug 2024 10:13:32 +0200 Subject: [PATCH 04/19] Add configuration options --- .../Configuration/PluginConfiguration.cs | 72 +++++++++++ .../Configuration/configPage.html | 62 ++++++++-- .../Plugin.cs | 18 +-- .../Tasks/TAToJellyfinProgressSyncTask.cs | 115 +++++++++--------- 4 files changed, 197 insertions(+), 70 deletions(-) diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs index 4d3686a..c9c592e 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Data; +using System.Linq; using Jellyfin.Plugin.TubeArchivistMetadata.Utilities; using MediaBrowser.Model.Plugins; using Microsoft.Extensions.Logging; @@ -14,6 +16,8 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration private ILogger _logger; private string _tubeArchivistUrl; private string _tubeArchivistApiKey; + private HashSet _jfUsernamesTo; + private HashSet _jfUsernamesFrom; /// /// Initializes a new instance of the class. @@ -33,6 +37,10 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration _tubeArchivistUrl = string.Empty; _tubeArchivistApiKey = string.Empty; MaxDescriptionLength = 500; + TAJFSync = false; + _jfUsernamesTo = new HashSet(); + JFTASync = false; + _jfUsernamesFrom = new HashSet(); } /// @@ -87,5 +95,69 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration /// Gets or sets maximum series and episodes overviews length. /// public int MaxDescriptionLength { get; set; } + + /// + /// Gets or sets a value indicating whether to enable TA->JF playback progress synchronization. + /// + public bool TAJFSync { get; set; } + + /// + /// Gets or sets a value indicating whether to enable JF->TA playback progress synchronization. + /// + public bool JFTASync { get; set; } + + /// + /// Gets or sets the playback progress owner Jellyfin usernames to synchronize data to TubeArchivist. + /// + public string JFUsernamesFrom + { + get + { + _logger.LogInformation("JFUsernamesFrom configured: {Message}", string.Join(", ", _jfUsernamesFrom)); + return string.Join(", ", _jfUsernamesFrom); + } + + set + { + value.Replace(" ", string.Empty, StringComparison.CurrentCulture).Split(',').ToList().ForEach(u => _jfUsernamesFrom.Add(u)); + _logger.LogInformation("Set JFUsernamesFrom to: {Message}", value); + } + } + + /// + /// Gets or sets the playback progress owners Jellyfin usernames to synchronize data from TubeArchivist. + /// + public string JFUsernamesTo + { + get + { + _logger.LogInformation("JFUsernamesTo configured: {Message}", string.Join(", ", _jfUsernamesTo)); + return string.Join(", ", _jfUsernamesTo); + } + + set + { + value.Replace(" ", string.Empty, StringComparison.CurrentCulture).Split(',').ToList().ForEach(u => _jfUsernamesTo.Add(u)); + _logger.LogInformation("Set JFUsernamesTo to: {Message}", value); + } + } + + /// + /// Gets the playback progress owner Jellyfin usernames to synchronize data to TubeArchivist. + /// + /// An array of usernames. + public HashSet GetJFUsernamesFromArray() + { + return _jfUsernamesFrom; + } + + /// + /// Gets the playback progress owners Jellyfin usernames to synchronize data from TubeArchivist. + /// + /// An array of usernames. + public HashSet GetJFUsernamesToArray() + { + return _jfUsernamesTo; + } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/configPage.html b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/configPage.html index 4c7bc20..c6a41f9 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/configPage.html +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/configPage.html @@ -3,7 +3,7 @@ - Template + TubeArchivistMetadata @@ -12,25 +12,65 @@
+
+

TubeArchivistMetadata

+
- +
TubeArchivist collection display name
- +
TubeArchivist URL
- +
TubeArchivist API key
- - -
This is the maximum length of the descriptions showed in series and episodes
+ + +
This is the maximum length of the descriptions showed in series + and episodes
+
+
+

Playback synchronization

+
+
+ +
+
+ + +
These are the (comma separated) Jellyfin usernames to synchronize + data from TubeArchivist
+
+
+ +
+
+ + +
These are the (comma separated) Jellyfin usernames to synchronize data from on + TubeArchivist
-
public class Plugin : BasePlugin, IHasWebPages { + private readonly IUserManager _userManager; + /// /// Initializes a new instance of the class. /// @@ -57,9 +61,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata SessionManager = sessionManager; sessionManager.PlaybackProgress += OnPlaybackProgress; LibraryManager = libraryManager; + _userManager = userManager; var taToJellyfinProgressSyncTask = new TAToJellyfinProgressSyncTask(logger, libraryManager, userManager, userDataManager); var jfToTubearchivistProgressSyncTask = new JFToTubearchivistProgressSyncTask(logger, libraryManager, userManager, userDataManager); + var jfToTubearchivistWatchedSyncTask = new JFToTubearchivistWatchedSyncTask(logger, libraryManager, userManager, userDataManager); var isTAJFTaskPresent = taskManager.ScheduledTasks.Any(t => t.Name.Equals(taToJellyfinProgressSyncTask.Name, StringComparison.Ordinal)); if (Instance!.Configuration.TAJFSync && !isTAJFTaskPresent) { @@ -72,8 +78,10 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata if (Instance!.Configuration.JFTASync && !isJFTATaskPresent) { logger.LogInformation("Queueing task {TaskName}.", jfToTubearchivistProgressSyncTask.Name); - taskManager.AddTasks([jfToTubearchivistProgressSyncTask]); + logger.LogInformation("Queueing task {TaskName}.", jfToTubearchivistWatchedSyncTask.Name); + taskManager.AddTasks([jfToTubearchivistProgressSyncTask, jfToTubearchivistWatchedSyncTask]); taskManager.Execute(); + taskManager.Execute(); } logger.LogInformation("{Message}", "Collection display name: " + Instance?.Configuration.CollectionTitle); diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistProgressSyncTask.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistProgressSyncTask.cs index 103c3d5..ab5b9a0 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistProgressSyncTask.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistProgressSyncTask.cs @@ -56,7 +56,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { - if (Plugin.Instance!.Configuration.TAJFSync) + if (Plugin.Instance!.Configuration.JFTASync) { var start = DateTime.Now; _logger.LogInformation("Starting Jellyfin->TubeArchivist playback progresses synchronization."); @@ -93,6 +93,9 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks foreach (Series channel in channels) { + var channelYTId = Utils.GetChannelNameFromPath(channel.Path); + var isChannelWatched = false; + var isChannelCheckedForWatched = false; var years = channel.GetChildren(user, false, new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.Season } @@ -116,6 +119,32 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks { _logger.LogInformation("{Message}", $"POST /video/{videoYTId}/progress returned {statusCode} for video {video.Name} with progress {progress} seconds"); } + + if (!isChannelCheckedForWatched && channel.IsPlayed(user)) + { + var isChannelPlayed = channel.IsPlayed(user); + statusCode = await taApi.SetWatchedStatus(channelYTId, isChannelPlayed).ConfigureAwait(true); + if (statusCode != System.Net.HttpStatusCode.OK) + { + _logger.LogInformation("{Message}", $"POST /watched returned {statusCode} for channel {channel.Name} ({channelYTId}) with wacthed status {isChannelPlayed}"); + } + else + { + isChannelWatched = true; + } + + isChannelCheckedForWatched = true; + } + + if (!isChannelWatched) + { + var isVideoPlayed = video.IsPlayed(user); + statusCode = await taApi.SetWatchedStatus(videoYTId, isVideoPlayed).ConfigureAwait(true); + if (statusCode != System.Net.HttpStatusCode.OK) + { + _logger.LogInformation("{Message}", $"POST /watched returned {statusCode} for video {video.Name} ({videoYTId}) with wacthed status {isVideoPlayed}"); + } + } } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs new file mode 100644 index 0000000..7bae4da --- /dev/null +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist; +using Jellyfin.Plugin.TubeArchivistMetadata.Utilities; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks +{ + /// + /// Task to sync Jellyfin playback progresses to TubeArchivist. + /// + public class JFToTubearchivistWatchedSyncTask : IScheduledTask + { + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataManager; + + /// + /// Initializes a new instance of the class. + /// + /// Logger. + /// Library manager. + /// User manager. + /// User data manager. + public JFToTubearchivistWatchedSyncTask(ILogger logger, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager) + { + _logger = logger; + _libraryManager = libraryManager; + _userManager = userManager; + _userDataManager = userDataManager; + } + + /// + public string Name => "JFToTubearchivistWatchedSyncTask"; + + /// + public string Description => "This tasks syncs TubeArchivist watched statuses to Jellyfin"; + + /// + public string Category => "TubeArchivistMetadata"; + + /// + public string Key => "JFToTubearchivistWatchedSyncTask"; + + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + if (Plugin.Instance!.Configuration.JFTASync) + { + var start = DateTime.Now; + _logger.LogInformation("Starting Jellyfin->TubeArchivist watched statuses synchronization."); + var taApi = TubeArchivistApi.GetInstance(); + foreach (var jfUsername in Plugin.Instance!.Configuration.GetJFUsernamesToArray()) + { + var user = _userManager.GetUserByName(jfUsername); + if (user == null) + { + _logger.LogInformation("{Message}", $"Jellyfin user with username {jfUsername} not found"); + continue; + } + + var collectionItem = _libraryManager.GetItemList(new InternalItemsQuery + { + Name = Plugin.Instance?.Configuration.CollectionTitle, + IncludeItemTypes = new[] { BaseItemKind.CollectionFolder } + }).FirstOrDefault(); + + if (collectionItem == null) + { + var message = $"Collection '{Plugin.Instance?.Configuration.CollectionTitle}' not found."; + _logger.LogCritical("{Message}", message); + } + else + { + var collection = (CollectionFolder)collectionItem; + var channels = collection.GetChildren(user, false, new InternalItemsQuery + { + IncludeItemTypes = new[] { BaseItemKind.Series } + }); + _logger.LogInformation("Analyzing collection {Id} with name {Name}", collectionItem.Id, collectionItem.Name); + _logger.LogInformation("Found {Message} channels", channels.Count); + + foreach (Series channel in channels) + { + var channelYTId = Utils.GetChannelNameFromPath(channel.Path); + var isChannelWatched = false; + var isChannelCheckedForWatched = false; + var years = channel.GetChildren(user, false, new InternalItemsQuery + { + IncludeItemTypes = new[] { BaseItemKind.Season } + }); + _logger.LogInformation("Found {Years} years in channel {ChannelName}", years.Count, channel.Name); + + foreach (Season year in years) + { + var videos = year.GetChildren(user, false, new InternalItemsQuery + { + IncludeItemTypes = new[] { BaseItemKind.Episode } + }); + _logger.LogInformation("Found {Videos} videos in year {YearName} of the channel {ChannelName}", videos.Count, year.Name, channel.Name); + + foreach (Episode video in videos) + { + var videoYTId = Utils.GetVideoNameFromPath(video.Path); + + if (!isChannelCheckedForWatched && channel.IsPlayed(user)) + { + var isChannelPlayed = channel.IsPlayed(user); + var statusCode = await taApi.SetWatchedStatus(channelYTId, isChannelPlayed).ConfigureAwait(true); + if (statusCode != System.Net.HttpStatusCode.OK) + { + _logger.LogInformation("{Message}", $"POST /watched returned {statusCode} for channel {channel.Name} ({channelYTId}) with wacthed status {isChannelPlayed}"); + } + else + { + isChannelWatched = true; + } + + isChannelCheckedForWatched = true; + } + + if (!isChannelWatched) + { + var isVideoPlayed = video.IsPlayed(user); + var statusCode = await taApi.SetWatchedStatus(videoYTId, isVideoPlayed).ConfigureAwait(true); + if (statusCode != System.Net.HttpStatusCode.OK) + { + _logger.LogInformation("{Message}", $"POST /watched returned {statusCode} for video {video.Name} ({videoYTId}) with wacthed status {isVideoPlayed}"); + } + } + } + } + } + } + } + + _logger.LogInformation("Time elapsed: {Time}", DateTime.Now - start); + } + else + { + _logger.LogInformation("Jellyfin->TubeArchivist watched status is currently disabled."); + } + } + + /// + public IEnumerable GetDefaultTriggers() + { + return + [ + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, + IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.JFTAWatchedTaskInterval).Ticks + }, + ]; + } + } +} diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Additional/Watched.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Additional/Watched.cs new file mode 100644 index 0000000..fc5f76c --- /dev/null +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Additional/Watched.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; + +namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist +{ + /// + /// A class representing TubeArchivist API video watched status. + /// + public class Watched + { + /// + /// Initializes a new instance of the class. + /// + /// Id of the video/channel/playlist. + /// Watched status. + public Watched(string id, bool isWatched) + { + Id = id; + IsWatched = isWatched; + } + + /// + /// Gets the id of the video/channel/playlist. + /// + [JsonProperty(PropertyName = "id")] + public string Id { get; } + + /// + /// Gets a value indicating whether the item has been watched or not. + /// + [JsonProperty(PropertyName = "is_watched")] + public bool IsWatched { get; } + } +} diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs index 13e1826..df6d363 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs @@ -153,7 +153,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist /// /// Video id. /// Progress in seconds. - /// Nothing if successful. + /// The response . public async Task SetProgress(string videoId, long progress) { var progressEndpoint = $"/api/video/{videoId}/progress/"; @@ -187,5 +187,22 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist return progress; } + + /// + /// Set video/channel/playlist as watched on TubeArchivist. + /// + /// Video/channel/playlist id. + /// Whether the item has been watched or not. + /// The response . + public async Task SetWatchedStatus(string itemId, bool isWatched) + { + var watchedEndpoint = $"/api/watched/"; + var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + watchedEndpoint)); + var body = JsonConvert.SerializeObject(new Watched(itemId, isWatched)); + + var response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json")).ConfigureAwait(true); + + return response.StatusCode; + } } } From 9fff31c451e67901434d273a1fb2c90703561a6a Mon Sep 17 00:00:00 2001 From: DarkFighterLuke Date: Fri, 6 Sep 2024 13:49:24 +0200 Subject: [PATCH 17/19] Fix typo in tasks properties --- .../Tasks/JFToTubeArchivistProgressSyncTask.cs | 4 ++-- .../Tasks/JFToTubeArchivistWatchedSyncTask.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistProgressSyncTask.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistProgressSyncTask.cs index ab5b9a0..121edcd 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistProgressSyncTask.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistProgressSyncTask.cs @@ -42,7 +42,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks } /// - public string Name => "JFToTubearchivistProgressSyncTask"; + public string Name => "JFToTubeArchivistProgressSyncTask"; /// public string Description => "This tasks syncs TubeArchivist playback progresses to Jellyfin"; @@ -51,7 +51,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks public string Category => "TubeArchivistMetadata"; /// - public string Key => "JFToTubearchivistProgressSyncTask"; + public string Key => "JFToTubeArchivistProgressSyncTask"; /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs index 7bae4da..8e35e2a 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs @@ -42,7 +42,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks } /// - public string Name => "JFToTubearchivistWatchedSyncTask"; + public string Name => "JFToTubeArchivistWatchedSyncTask"; /// public string Description => "This tasks syncs TubeArchivist watched statuses to Jellyfin"; @@ -51,7 +51,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks public string Category => "TubeArchivistMetadata"; /// - public string Key => "JFToTubearchivistWatchedSyncTask"; + public string Key => "JFToTubeArchivistWatchedSyncTask"; /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) From 52342b3dc3c39429c498901c5589184991aaf9e8 Mon Sep 17 00:00:00 2001 From: DarkFighterLuke Date: Wed, 11 Sep 2024 15:08:29 +0200 Subject: [PATCH 18/19] Fix typo in log message --- .../Tasks/JFToTubeArchivistWatchedSyncTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs index 8e35e2a..d4ff290 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs @@ -136,7 +136,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks var statusCode = await taApi.SetWatchedStatus(videoYTId, isVideoPlayed).ConfigureAwait(true); if (statusCode != System.Net.HttpStatusCode.OK) { - _logger.LogInformation("{Message}", $"POST /watched returned {statusCode} for video {video.Name} ({videoYTId}) with wacthed status {isVideoPlayed}"); + _logger.LogInformation("{Message}", $"POST /watched returned {statusCode} for video {video.Name} ({videoYTId}) with watched status {isVideoPlayed}"); } } } From c9cc4f150e89a990a570e050bac465160613190a Mon Sep 17 00:00:00 2001 From: DarkFighterLuke Date: Wed, 11 Sep 2024 16:19:18 +0200 Subject: [PATCH 19/19] Switch from watched status synchronization task to an event based solution --- .../Configuration/PluginConfiguration.cs | 7 - .../Configuration/configPage.html | 9 - .../Plugin.cs | 35 +++- .../Tasks/JFToTubeArchivistWatchedSyncTask.cs | 169 ------------------ 4 files changed, 31 insertions(+), 189 deletions(-) delete mode 100644 Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistWatchedSyncTask.cs diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs index 5b21c00..1e96e3e 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs @@ -41,7 +41,6 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration TAJFSync = false; _jfUsernamesTo = new HashSet(); TAJFTaskInterval = 1; - JFTAWatchedTaskInterval = 1; } /// @@ -136,12 +135,6 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration /// public int TAJFTaskInterval { get; set; } - /// - /// Gets or sets the interval in seconds at which the Jellyfin to TubeArchivist watched statuses synchronization task should run. - /// It requires Jellyfin server restart to take effect. - /// - public int JFTAWatchedTaskInterval { get; set; } - /// /// Gets the playback progress owners Jellyfin usernames to synchronize data from TubeArchivist. /// diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/configPage.html b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/configPage.html index 84b2798..d4e07d5 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/configPage.html +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/configPage.html @@ -58,13 +58,6 @@
This is the Jellyfin username to synchronize data from on TubeArchivist
-
- - -
This is the Jellyfin to TubeArchivist watched statuses - synchronization interval in seconds (Jellyfin restart required).
-