From fd4a9a013045993c0dc1e8a1cc72067aa30dd5fe Mon Sep 17 00:00:00 2001 From: DarkFighterLuke Date: Tue, 27 Aug 2024 00:12:14 +0200 Subject: [PATCH] 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; + } } }