diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/PluginConfiguration.cs index 4d3686a..1e96e3e 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,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration private ILogger _logger; private string _tubeArchivistUrl; private string _tubeArchivistApiKey; + private HashSet _jfUsernamesTo; /// /// Initializes a new instance of the class. @@ -33,6 +36,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration _tubeArchivistUrl = string.Empty; _tubeArchivistApiKey = string.Empty; MaxDescriptionLength = 500; + JFTASync = false; + JFUsernameFrom = string.Empty; + TAJFSync = false; + _jfUsernamesTo = new HashSet(); + TAJFTaskInterval = 1; } /// @@ -87,5 +95,53 @@ 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 username to synchronize data to TubeArchivist. + /// + public string JFUsernameFrom { get; set; } + + /// + /// 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 or sets the interval in seconds at which the TubeArchivist to Jellyfin playback progress synchronization task should run. + /// It requires Jellyfin server restart to take effect. + /// + public int TAJFTaskInterval { get; set; } + + /// + /// 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..d4e07d5 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/configPage.html +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Configuration/configPage.html @@ -3,7 +3,7 @@ - Template + TubeArchivistMetadata @@ -12,25 +12,72 @@
+
+

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

+
+
+ +
+
+ + +
This is the Jellyfin username to synchronize data from on + TubeArchivist
+
+
+ +
+
+ + +
These are the (comma separated) Jellyfin usernames to synchronize + data from TubeArchivist
+
+
+ + +
This is the TubeArchivist to Jellyfin playback progress + synchronization interval in seconds (Jellyfin restart required).
public class Plugin : BasePlugin, IHasWebPages { + private readonly IUserManager _userManager; + /// /// Initializes a new instance of the class. /// /// 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. + /// 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; @@ -34,6 +58,30 @@ 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; + _userManager = userManager; + userDataManager.UserDataSaved += OnWatchedStatusChange; + + var taToJellyfinProgressSyncTask = new TAToJellyfinProgressSyncTask(logger, libraryManager, userManager, userDataManager); + 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) + { + logger.LogInformation("Queueing task {TaskName}.", taToJellyfinProgressSyncTask.Name); + taskManager.AddTasks([taToJellyfinProgressSyncTask]); + taskManager.Execute(); + } + + var isJFTATaskPresent = taskManager.ScheduledTasks.Any(t => t.Name.Equals(jfToTubearchivistProgressSyncTask.Name, StringComparison.Ordinal)); + if (Instance!.Configuration.JFTASync && !isJFTATaskPresent) + { + logger.LogInformation("Queueing task {TaskName}.", jfToTubearchivistProgressSyncTask.Name); + taskManager.AddTasks([jfToTubearchivistProgressSyncTask]); + taskManager.Execute(); + } + 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 +108,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 +150,53 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata }, TaskScheduler.Default); } + + private async void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs eventArgs) + { + if (Instance!.Configuration.JFTASync && eventArgs.Users.Any(u => Instance!.Configuration.JFUsernameFrom.Equals(u.Username, StringComparison.Ordinal))) + { + 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 videoId = Utils.GetVideoNameFromPath(eventArgs.Item.Path); + var statusCode = await TubeArchivistApi.GetInstance().SetProgress(videoId, progress).ConfigureAwait(true); + if (statusCode != System.Net.HttpStatusCode.OK) + { + Logger.LogInformation("{Message}", $"POST /video/{videoId}/progress returned {statusCode} for video {eventArgs.Item.Name} with progress {progress} seconds"); + } + } + } + } + + private async void OnWatchedStatusChange(object? sender, UserDataSaveEventArgs eventArgs) + { + var user = _userManager.GetUserById(eventArgs.UserId); + if (user != null && Configuration.GetJFUsernamesToArray().Contains(user!.Username)) + { + var isPlayed = eventArgs.Item.IsPlayed(user); + Logger.LogInformation("User {UserId} changed watched status to {Status} for the item {ItemName}", eventArgs.UserId, isPlayed, eventArgs.Item.Name); + string itemYTId; + if (eventArgs.Item is Series) + { + itemYTId = Utils.GetChannelNameFromPath(eventArgs.Item.Path); + } + else if (eventArgs.Item is Episode) + { + itemYTId = Utils.GetVideoNameFromPath(eventArgs.Item.Path); + } + else + { + return; + } + + var statusCode = await TubeArchivistApi.GetInstance().SetWatchedStatus(itemYTId, isPlayed).ConfigureAwait(true); + if (statusCode != System.Net.HttpStatusCode.OK) + { + Logger.LogInformation("POST /watched returned {StatusCode} for item {ItemName} ({VideoYTId}) with watched status {IsPlayed}", statusCode, eventArgs.Item.Name, itemYTId, isPlayed); + } + } + } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistProgressSyncTask.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistProgressSyncTask.cs new file mode 100644 index 0000000..121edcd --- /dev/null +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/JFToTubeArchivistProgressSyncTask.cs @@ -0,0 +1,174 @@ +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 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 TubeArchivist playback progresses to Jellyfin"; + + /// + public string Category => "TubeArchivistMetadata"; + + /// + public string Key => "JFToTubeArchivistProgressSyncTask"; + + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + if (Plugin.Instance!.Configuration.JFTASync) + { + var start = DateTime.Now; + _logger.LogInformation("Starting Jellyfin->TubeArchivist playback progresses 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); + var playbackProgress = _userDataManager.GetUserData(user.Id, video).PlaybackPositionTicks / TimeSpan.TicksPerSecond; + var statusCode = await taApi.SetProgress(videoYTId, playbackProgress).ConfigureAwait(true); + if (statusCode != System.Net.HttpStatusCode.OK) + { + _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}"); + } + } + } + } + } + } + } + + _logger.LogInformation("Time elapsed: {Time}", DateTime.Now - start); + } + else + { + _logger.LogInformation("Jellyfin->TubeArchivist playback synchronization is currently disabled."); + } + } + + /// + public IEnumerable GetDefaultTriggers() + { + return + [ + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerStartup, + }, + ]; + } + } +} diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinProgressSyncTask.cs b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinProgressSyncTask.cs new file mode 100644 index 0000000..af98bb5 --- /dev/null +++ b/Jellyfin.Plugin.TubeArchivistMetadata/Tasks/TAToJellyfinProgressSyncTask.cs @@ -0,0 +1,165 @@ +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 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) + { + if (Plugin.Instance!.Configuration.TAJFSync) + { + var start = DateTime.Now; + _logger.LogInformation("Starting TubeArchivist->Jellyfin playback progresses 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 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 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 + }; + + var taVideoInfo = await taApi.GetVideo(Utils.GetVideoNameFromPath(video.Path)).ConfigureAwait(true); + if (taVideoInfo != null) + { + if (taVideoInfo.Player.IsWatched) + { + 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}."); + _logger.LogInformation("{Message}", $"Watched status for video {video.Name} set to {userItemData.Played} seconds for user {jfUsername}."); + } + } + } + } + } + } + + _logger.LogInformation("Time elapsed: {Time}", DateTime.Now - start); + } + else + { + _logger.LogInformation("TubeArchivist->Jellyfin playback synchronization is currently disabled."); + } + } + + /// + public IEnumerable GetDefaultTriggers() + { + return + [ + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, + IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFTaskInterval).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/Channel.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Channel/Channel.cs similarity index 100% rename from Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Channel.cs rename to Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Channel/Channel.cs diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/TubeArchivistApi.cs index 53adfbb..df6d363 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,62 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist return pong; } + + /// + /// Send video playback progress to TubeArchivist. + /// + /// Video id. + /// Progress in seconds. + /// The response . + 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 Progress(progress)); + + var response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json")).ConfigureAwait(true); + + 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; + } + + /// + /// 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; + } } } diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Video/Player.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Video/Player.cs new file mode 100644 index 0000000..3689c7c --- /dev/null +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Video/Player.cs @@ -0,0 +1,51 @@ +using System; +using Newtonsoft.Json; + +namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist +{ + /// + /// A class representing TubeArchivist API video player data. + /// + public class Player + { + /// + /// Initializes a new instance of the class. + /// + /// Duration of the video. + /// Whether the video is marked as watched. + /// Unix time of when the video has been marked watched. + public Player(long duration, bool isWatched, long watchedUnixTime) + { + Duration = duration; + IsWatched = isWatched; + WatchedUnixTime = watchedUnixTime; + } + + /// + /// Gets the duration of the video. + /// + [JsonProperty(PropertyName = "duration")] + public long Duration { get; } + + /// + /// Gets a value indicating whether the video is marked as watched. + /// + [JsonProperty(PropertyName = "watched")] + public bool IsWatched { get; } + + /// + /// Gets the Unix date and time when the video has been marked as watched. + /// + [JsonProperty(PropertyName = "watched_date")] + public long WatchedUnixTime { get; } + + /// + /// Gets the date and time when the video has been marked as watched. + /// + [JsonIgnore] + public DateTime WatchedTime + { + get => DateTimeOffset.FromUnixTimeSeconds(WatchedUnixTime).DateTime; + } + } +} diff --git a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Video/Progress.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Video/Progress.cs new file mode 100644 index 0000000..a11a504 --- /dev/null +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Video/Progress.cs @@ -0,0 +1,54 @@ +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. + [JsonConstructor] + 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/Video.cs b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Video/Video.cs similarity index 94% rename from Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Video.cs rename to Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Video/Video.cs index 97ea596..dec2ec0 100644 --- a/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Video.cs +++ b/Jellyfin.Plugin.TubeArchivistMetadata/TubeArchivist/Video/Video.cs @@ -27,6 +27,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist /// Video published date. /// Video thuumb image URL. /// Video YouTube id. + /// Player info. public Video( Channel channel, Collection tags, @@ -34,7 +35,8 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist string description, DateTime published, string vidThumbUrl, - string youtubeId) + string youtubeId, + Player player) { this.Channel = channel; this.Tags = tags; @@ -43,6 +45,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist this.Published = published; this.VidThumbUrl = vidThumbUrl; this.YoutubeId = youtubeId; + Player = player; } /// @@ -88,6 +91,12 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist [JsonProperty(PropertyName = "youtube_id")] public string YoutubeId { get; set; } + /// + /// Getsthe player related info. + /// + [JsonProperty(PropertyName = "player")] + public Player Player { get; } + /// /// Converts the TubeArchivist API video to a Jellyfin object. ///