using System; using System.Collections.Generic; using System.Linq; using System.Net; 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 JFToTubeArchivistProgressSyncTask : 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 JFToTubeArchivistProgressSyncTask(ILogger logger, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager) { _logger = logger; _libraryManager = libraryManager; _userManager = userManager; _userDataManager = userDataManager; } /// public string Name => "JFToTubeArchivistProgressSyncTask"; /// public string Description => "This tasks syncs Jellyfin playback progresses to TubeArchivist"; /// public string Category => "TubeArchivistMetadata"; /// public string Key => "JFToTubeArchivistProgressSyncTask"; /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { progress.Report(0); if (Plugin.Instance!.Configuration.JFTAProgressSync) { var start = DateTime.Now; _logger.LogInformation("Starting Jellyfin->TubeArchivist playback progresses synchronization."); var taApi = TubeArchivistApi.GetInstance(); var videosCount = 0; var jfUsername = Plugin.Instance!.Configuration.JFUsernameFrom; var user = _userManager.GetUserByName(jfUsername); if (user == null) { _logger.LogInformation("{Message}", $"Jellyfin user with username {jfUsername} not found"); return; } var items = _libraryManager.GetItemList(new InternalItemsQuery { Name = Plugin.Instance?.Configuration.CollectionTitle, IncludeItemTypes = new[] { BaseItemKind.CollectionFolder } }); var collectionItem = items.Count > 0 ? items[0] : null; 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.LogDebug("Analyzing collection {Id} with name {Name}", collectionItem.Id, collectionItem.Name); _logger.LogDebug("Found {Message} channels", channels.Count); foreach (Series channel in channels) { var channelYTId = Utils.GetChannelNameFromPath(channel.Path); var years = channel.GetChildren(user, false, new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.Season } }); _logger.LogDebug("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.LogDebug("Found {Videos} videos in year {YearName} of the channel {ChannelName}", videos.Count, year.Name, channel.Name); videosCount += videos.Count; } } } _logger.LogDebug("Found a total of {VideosCount} videos", videosCount); var processedVideosCount = 0; 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 } }); 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 } }); foreach (Season year in years) { var videos = year.GetChildren(user, false, new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.Episode } }); videosCount += videos.Count; foreach (Episode video in videos) { var videoYTId = Utils.GetVideoNameFromPath(video.Path); _logger.LogDebug("Current video extracted YouTube id: {VideoYtId}", videoYTId); HttpStatusCode statusCode; var channelItemData = _userDataManager.GetUserData(user, channel); if (!isChannelCheckedForWatched && channel.IsPlayed(user, channelItemData)) { var isChannelPlayed = channel.IsPlayed(user, channelItemData); statusCode = await taApi.SetWatchedStatus(channelYTId, isChannelPlayed).ConfigureAwait(true); if (statusCode != System.Net.HttpStatusCode.OK) { _logger.LogCritical("{Message}", $"POST /watched returned {statusCode} for channel {channel.Name} ({channelYTId}) with wacthed status {isChannelPlayed}"); } else { isChannelWatched = true; } isChannelCheckedForWatched = true; } var videoItemData = _userDataManager.GetUserData(user, video); if (!isChannelWatched) { var isVideoPlayed = video.IsPlayed(user, videoItemData); var taVideo = await taApi.GetVideo(videoYTId).ConfigureAwait(true); if (taVideo != null) { 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); if (!isVideoPlayed) { var playbackProgress = _userDataManager.GetUserData(user, video)?.PlaybackPositionTicks / TimeSpan.TicksPerSecond; if (playbackProgress != null) { try { 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); } } } } processedVideosCount++; progress.Report(processedVideosCount * 100 / videosCount); } } } } _logger.LogInformation("Time elapsed: {Time}", DateTime.Now - start); } else { _logger.LogInformation("Jellyfin->TubeArchivist playback synchronization is currently disabled."); } progress.Report(100); } /// public IEnumerable GetDefaultTriggers() { return [ new TaskTriggerInfo { Type = TaskTriggerInfoType.StartupTrigger, }, ]; } } }