From a9c6611dcfe326760bcfbb5956f61ee14edb905f Mon Sep 17 00:00:00 2001 From: DarkFighterLuke Date: Fri, 30 May 2025 15:36:25 +0200 Subject: [PATCH] Implement TA-> JF playlist sync scheduled task --- .../Plugin.cs | 10 +- .../Tasks/TAToJellyfinPlaylistsSyncTask.cs | 189 ++++++++++++++++++ 2 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinPlaylistsSyncTask.cs diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs index 3e3e95d..51d006b 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Plugin.cs @@ -14,6 +14,7 @@ using MediaBrowser.Common.Plugins; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; @@ -41,6 +42,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. public Plugin( IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, @@ -49,7 +51,8 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata ILibraryManager libraryManager, ITaskManager taskManager, IUserManager userManager, - IUserDataManager userDataManager) + IUserDataManager userDataManager, + IPlaylistManager playlistManager) : base(applicationPaths, xmlSerializer) { Instance = this; @@ -68,6 +71,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata userDataManager.UserDataSaved += OnWatchedStatusChange; var taToJellyfinProgressSyncTask = new TAToJellyfinProgressSyncTask(logger, libraryManager, userManager, userDataManager); + var taToJellyfinPlaylistsSyncTask = new TAToJellyfinPlaylistsSyncTask(logger, libraryManager, userManager, playlistManager); var jfToTubearchivistProgressSyncTask = new JFToTubearchivistProgressSyncTask(logger, libraryManager, userManager, userDataManager); var isTAJFTaskPresent = taskManager.ScheduledTasks.Any(t => t.Name.Equals(taToJellyfinProgressSyncTask.Name, StringComparison.Ordinal)); if (Instance!.Configuration.TAJFSync && !isTAJFTaskPresent) @@ -75,6 +79,10 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata logger.LogInformation("Queueing task {TaskName}.", taToJellyfinProgressSyncTask.Name); taskManager.AddTasks([taToJellyfinProgressSyncTask]); taskManager.Execute(); + + logger.LogInformation("Queueing task {TaskName}.", taToJellyfinPlaylistsSyncTask.Name); + taskManager.AddTasks([taToJellyfinProgressSyncTask]); + taskManager.Execute(); } var isJFTATaskPresent = taskManager.ScheduledTasks.Any(t => t.Name.Equals(jfToTubearchivistProgressSyncTask.Name, StringComparison.Ordinal)); diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinPlaylistsSyncTask.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinPlaylistsSyncTask.cs new file mode 100644 index 0000000..17a86ed --- /dev/null +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinPlaylistsSyncTask.cs @@ -0,0 +1,189 @@ +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.Controller.Playlists; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Playlists; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks +{ + /// + /// Task to sync TubeArchivist playback progresses to Jellyfin. + /// + public class TAToJellyfinPlaylistsSyncTask : IScheduledTask + { + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IPlaylistManager _playlistManager; + + /// + /// Initializes a new instance of the class. + /// + /// Logger. + /// Library manager. + /// User manager. + /// Playlists manager. + public TAToJellyfinPlaylistsSyncTask(ILogger logger, ILibraryManager libraryManager, IUserManager userManager, IPlaylistManager playlistManager) + { + _logger = logger; + _libraryManager = libraryManager; + _userManager = userManager; + _playlistManager = playlistManager; + } + + /// + public string Name => "TAToJellyfinPlaylistsSyncTask"; + + /// + public string Description => "This tasks syncs TubeArchivist playlists to Jellyfin"; + + /// + public string Category => "TubeArchivistMetadata"; + + /// + public string Key => "TAToJellyfinPlaylistsSyncTask"; + + private int CountTotalVideos(ISet taPlaylists) + { + var totalEntries = 0; + foreach (var taPlaylist in taPlaylists) + { + totalEntries += taPlaylist.Entries.Count; + } + + return totalEntries; + } + + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + progress.Report(0); + if (Plugin.Instance!.Configuration.TAJFSync) + { + var start = DateTime.Now; + _logger.LogInformation("Starting TubeArchivist->Jellyfin playlists synchronization."); + var taApi = TubeArchivistApi.GetInstance(); + _logger.LogInformation("Getting TubeArchivist playlists"); + var taPlaylists = await taApi.GetPlaylists().ConfigureAwait(true); + _logger.LogInformation("Received playlists:\n{Playlists}", taPlaylists); + if (taPlaylists != null) + { + var totalVideosCount = CountTotalVideos(taPlaylists); + var processedVideosCount = 0; + 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 userPlaylists = _playlistManager.GetPlaylists(user.Id); + + foreach (var taPlaylist in taPlaylists) + { + var playlistName = taPlaylist.Type == PlaylistType.Regular ? $"{taPlaylist.Name} - {taPlaylist.Channel} ({taPlaylist.Id})" : $"{taPlaylist.Name} ({taPlaylist.Id})"; + var userPlaylist = userPlaylists.Where(up => up.Name == playlistName).FirstOrDefault(); + + var jfEntryIds = new List(); + var currentPlaylistVideos = 0; + foreach (var taEntry in taPlaylist.Entries) + { + currentPlaylistVideos++; + var taEntryStr = $"{taEntry.Uploader} - {taEntry.Title} ({taEntry.YoutubeId}) in playlist {playlistName}"; + if (!taEntry.IsDownloaded) + { + _logger.LogInformation("The entry {TAEntry} was skipped because has not been downloaded by TubeArchivist", taEntryStr); + continue; + } + + var jfEntry = _libraryManager.GetItemList(new InternalItemsQuery + { + HasAnyProviderId = new Dictionary() + { + { + Constants.ProviderName, taEntry.YoutubeId + } + } + }).FirstOrDefault(); + + if (jfEntry != null) + { + jfEntryIds.Add(jfEntry.Id); + } + else + { + _logger.LogWarning("The relative video for {TAEntry} was not found on Jellyfin", taEntryStr); + } + } + + if (userPlaylist != null) + { + var updateRequest = new PlaylistUpdateRequest + { + Id = userPlaylist.Id, + UserId = user.Id, + Name = playlistName, + Ids = jfEntryIds + }; + + var result = _playlistManager.UpdatePlaylist(updateRequest); + _logger.LogInformation("Updated playlist {PlaylistName} with id {Id}", playlistName, result.Id); + } + else + { + var creationRequest = new PlaylistCreationRequest + { + Name = playlistName, + ItemIdList = jfEntryIds, + MediaType = MediaType.Video, + UserId = user.Id + }; + + var result = await _playlistManager.CreatePlaylist(creationRequest).ConfigureAwait(true); + _logger.LogInformation("Created playlist {PlaylistName} with id {Id}", playlistName, result.Id); + } + + processedVideosCount += currentPlaylistVideos; + progress.Report(processedVideosCount * 100 / totalVideosCount); + } + } + } + + _logger.LogInformation("Time elapsed: {Time}", DateTime.Now - start); + } + else + { + _logger.LogInformation("TubeArchivist->Jellyfin playback synchronization is currently disabled."); + } + + progress.Report(100); + } + + /// + public IEnumerable GetDefaultTriggers() + { + return + [ + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, + IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFTaskInterval).Ticks + }, + ]; + } + } +}