Merge pull request #22 from tubearchivist/playback-progress
Playback progress synchronization
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -14,6 +16,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
|
|||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
private string _tubeArchivistUrl;
|
private string _tubeArchivistUrl;
|
||||||
private string _tubeArchivistApiKey;
|
private string _tubeArchivistApiKey;
|
||||||
|
private HashSet<string> _jfUsernamesTo;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
/// Initializes a new instance of the <see cref="PluginConfiguration"/> class.
|
||||||
@@ -33,6 +36,11 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
|
|||||||
_tubeArchivistUrl = string.Empty;
|
_tubeArchivistUrl = string.Empty;
|
||||||
_tubeArchivistApiKey = string.Empty;
|
_tubeArchivistApiKey = string.Empty;
|
||||||
MaxDescriptionLength = 500;
|
MaxDescriptionLength = 500;
|
||||||
|
JFTASync = false;
|
||||||
|
JFUsernameFrom = string.Empty;
|
||||||
|
TAJFSync = false;
|
||||||
|
_jfUsernamesTo = new HashSet<string>();
|
||||||
|
TAJFTaskInterval = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -87,5 +95,53 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
|
|||||||
/// Gets or sets maximum series and episodes overviews length.
|
/// Gets or sets maximum series and episodes overviews length.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaxDescriptionLength { get; set; }
|
public int MaxDescriptionLength { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to enable TA->JF playback progress synchronization.
|
||||||
|
/// </summary>
|
||||||
|
public bool TAJFSync { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to enable JF->TA playback progress synchronization.
|
||||||
|
/// </summary>
|
||||||
|
public bool JFTASync { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the playback progress owner Jellyfin username to synchronize data to TubeArchivist.
|
||||||
|
/// </summary>
|
||||||
|
public string JFUsernameFrom { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the playback progress owners Jellyfin usernames to synchronize data from TubeArchivist.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public int TAJFTaskInterval { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the playback progress owners Jellyfin usernames to synchronize data from TubeArchivist.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An array of usernames.</returns>
|
||||||
|
public HashSet<string> GetJFUsernamesToArray()
|
||||||
|
{
|
||||||
|
return _jfUsernamesTo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Template</title>
|
<title>TubeArchivistMetadata</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -12,25 +12,72 @@
|
|||||||
<div data-role="content">
|
<div data-role="content">
|
||||||
<div class="content-primary">
|
<div class="content-primary">
|
||||||
<form id="TemplateConfigForm">
|
<form id="TemplateConfigForm">
|
||||||
|
<div>
|
||||||
|
<h1>TubeArchivistMetadata</h1>
|
||||||
|
</div>
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="CollectionTitle">Collection Title</label>
|
<label class="inputLabel inputLabelUnfocused" for="CollectionTitle">Collection Title</label>
|
||||||
<input id="CollectionTitle" name="CollectionTitle" type="text" is="emby-input" placeholder="eg. YouTube" />
|
<input id="CollectionTitle" name="CollectionTitle" type="text" is="emby-input"
|
||||||
|
placeholder="eg. YouTube" />
|
||||||
<div class="fieldDescription">TubeArchivist collection display name</div>
|
<div class="fieldDescription">TubeArchivist collection display name</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="TubeArchivistUrl">TubeArchivist URL</label>
|
<label class="inputLabel inputLabelUnfocused" for="TubeArchivistUrl">TubeArchivist URL</label>
|
||||||
<input id="TubeArchivistUrl" name="TubeArchivistUrl" type="text" is="emby-input" placeholder="http://..." />
|
<input id="TubeArchivistUrl" name="TubeArchivistUrl" type="text" is="emby-input"
|
||||||
|
placeholder="http://..." />
|
||||||
<div class="fieldDescription">TubeArchivist URL</div>
|
<div class="fieldDescription">TubeArchivist URL</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="TubeArchivistApiKey">TubeArchivist API key</label>
|
<label class="inputLabel inputLabelUnfocused" for="TubeArchivistApiKey">TubeArchivist API
|
||||||
|
key</label>
|
||||||
<input id="TubeArchivistApiKey" name="TubeArchivistApiKey" type="text" is="emby-input" />
|
<input id="TubeArchivistApiKey" name="TubeArchivistApiKey" type="text" is="emby-input" />
|
||||||
<div class="fieldDescription">TubeArchivist API key</div>
|
<div class="fieldDescription">TubeArchivist API key</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inputContainer">
|
<div class="inputContainer">
|
||||||
<label class="inputLabel inputLabelUnfocused" for="MaxDescriptionLength">Maximum overviews length</label>
|
<label class="inputLabel inputLabelUnfocused" for="MaxDescriptionLength">Maximum overviews
|
||||||
<input id="MaxDescriptionLength" name="MaxDescriptionLength" type="number" is="emby-input" min="0" />
|
length</label>
|
||||||
<div class="fieldDescription">This is the maximum length of the descriptions showed in series and episodes</div>
|
<input id="MaxDescriptionLength" name="MaxDescriptionLength" type="number" is="emby-input"
|
||||||
|
min="0" />
|
||||||
|
<div class="fieldDescription">This is the maximum length of the descriptions showed in series
|
||||||
|
and episodes</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Playback synchronization</h2>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label class="emby-checkbox-label" for="JFTASync">
|
||||||
|
<input id="JFTASync" name="JFTASync" type="checkbox" is="emby-checkbox" />
|
||||||
|
<span>Synchronize Jellyfin playback progress to TubeArchivist</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="JFUsernameFrom">Jellyfin username to sync
|
||||||
|
playback status from</label>
|
||||||
|
<input id="JFUsernameFrom" name="JFUsernameFrom" type="text" is="emby-input"
|
||||||
|
placeholder="Username1, username2, ..." />
|
||||||
|
<div class="fieldDescription">This is the Jellyfin username to synchronize data from on
|
||||||
|
TubeArchivist</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||||
|
<label class="emby-checkbox-label" for="TAJFSync">
|
||||||
|
<input id="TAJFSync" name="TAJFSync" type="checkbox" is="emby-checkbox" />
|
||||||
|
<span>Synchronize TubeArchivist playback progress to Jellyfin</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="JFUsernamesTo">Jellyfin usernames to sync
|
||||||
|
playback status to</label>
|
||||||
|
<input id="JFUsernamesTo" name="JFUsernamesTo" type="text" is="emby-input"
|
||||||
|
placeholder="Username1, username2, ..." />
|
||||||
|
<div class="fieldDescription">These are the (comma separated) Jellyfin usernames to synchronize
|
||||||
|
data from TubeArchivist</div>
|
||||||
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="TAJFTaskInterval">TubeArchivist to Jellyfin
|
||||||
|
playback progress synchronization interval</label>
|
||||||
|
<input id="TAJFTaskInterval" name="TAJFTaskInterval" type="number" is="emby-input" min="1" />
|
||||||
|
<div class="fieldDescription">This is the TubeArchivist to Jellyfin playback progress
|
||||||
|
synchronization interval in seconds (Jellyfin restart required).</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||||
@@ -53,6 +100,11 @@
|
|||||||
document.querySelector('#TubeArchivistUrl').value = config.TubeArchivistUrl;
|
document.querySelector('#TubeArchivistUrl').value = config.TubeArchivistUrl;
|
||||||
document.querySelector('#TubeArchivistApiKey').value = config.TubeArchivistApiKey;
|
document.querySelector('#TubeArchivistApiKey').value = config.TubeArchivistApiKey;
|
||||||
document.querySelector('#MaxDescriptionLength').value = config.MaxDescriptionLength;
|
document.querySelector('#MaxDescriptionLength').value = config.MaxDescriptionLength;
|
||||||
|
document.querySelector('#JFTASync').checked = config.JFTASync;
|
||||||
|
document.querySelector('#JFUsernamesTo').value = config.JFUsernamesTo;
|
||||||
|
document.querySelector('#TAJFSync').checked = config.TAJFSync;
|
||||||
|
document.querySelector('#JFUsernameFrom').value = config.JFUsernameFrom;
|
||||||
|
document.querySelector('#TAJFTaskInterval').value = config.TAJFTaskInterval;
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -65,6 +117,11 @@
|
|||||||
config.TubeArchivistUrl = document.querySelector('#TubeArchivistUrl').value;
|
config.TubeArchivistUrl = document.querySelector('#TubeArchivistUrl').value;
|
||||||
config.TubeArchivistApiKey = document.querySelector('#TubeArchivistApiKey').value;
|
config.TubeArchivistApiKey = document.querySelector('#TubeArchivistApiKey').value;
|
||||||
config.MaxDescriptionLength = document.querySelector('#MaxDescriptionLength').value;
|
config.MaxDescriptionLength = document.querySelector('#MaxDescriptionLength').value;
|
||||||
|
config.JFTASync = document.querySelector('#JFTASync').checked;
|
||||||
|
config.JFUsernamesTo = document.querySelector('#JFUsernamesTo').value;
|
||||||
|
config.TAJFSync = document.querySelector('#TAJFSync').checked;
|
||||||
|
config.JFUsernameFrom = document.querySelector('#JFUsernameFrom').value;
|
||||||
|
config.TAJFTaskInterval = document.querySelector('#TAJFTaskInterval').value;
|
||||||
ApiClient.updatePluginConfiguration(TAMetadataConfig.pluginUniqueId, config).then(function (result) {
|
ApiClient.updatePluginConfiguration(TAMetadataConfig.pluginUniqueId, config).then(function (result) {
|
||||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Plugin.TubeArchivistMetadata.Configuration;
|
using Jellyfin.Plugin.TubeArchivistMetadata.Configuration;
|
||||||
|
using Jellyfin.Plugin.TubeArchivistMetadata.Tasks;
|
||||||
using Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist;
|
using Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist;
|
||||||
|
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Plugin.TubeArchivistMetadata
|
namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||||
@@ -18,13 +27,28 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||||
{
|
{
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||||
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILogger<Plugin> logger)
|
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
||||||
|
public Plugin(
|
||||||
|
IApplicationPaths applicationPaths,
|
||||||
|
IXmlSerializer xmlSerializer,
|
||||||
|
ILogger<Plugin> logger,
|
||||||
|
ISessionManager sessionManager,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
ITaskManager taskManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
IUserDataManager userDataManager)
|
||||||
: base(applicationPaths, xmlSerializer)
|
: base(applicationPaths, xmlSerializer)
|
||||||
{
|
{
|
||||||
Instance = this;
|
Instance = this;
|
||||||
@@ -34,6 +58,30 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
|||||||
handler.CheckCertificateRevocationList = true;
|
handler.CheckCertificateRevocationList = true;
|
||||||
HttpClient = new HttpClient(handler);
|
HttpClient = new HttpClient(handler);
|
||||||
HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Token", Instance?.Configuration.TubeArchivistApiKey);
|
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<TAToJellyfinProgressSyncTask>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<JFToTubearchivistProgressSyncTask>();
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogInformation("{Message}", "Collection display name: " + Instance?.Configuration.CollectionTitle);
|
logger.LogInformation("{Message}", "Collection display name: " + Instance?.Configuration.CollectionTitle);
|
||||||
logger.LogInformation("{Message}", "TubeArchivist API URL: " + Instance?.Configuration.TubeArchivistUrl);
|
logger.LogInformation("{Message}", "TubeArchivist API URL: " + Instance?.Configuration.TubeArchivistUrl);
|
||||||
logger.LogInformation("{Message}", "Pinging TubeArchivist API...");
|
logger.LogInformation("{Message}", "Pinging TubeArchivist API...");
|
||||||
@@ -60,6 +108,16 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public HttpClient HttpClient { get; }
|
public HttpClient HttpClient { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the SessionManager used globally by the plugin.
|
||||||
|
/// </summary>
|
||||||
|
public ISessionManager SessionManager { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the LibraryManager used globally by the plugin.
|
||||||
|
/// </summary>
|
||||||
|
public ILibraryManager LibraryManager { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<PluginPageInfo> GetPages() => new[]
|
public IEnumerable<PluginPageInfo> GetPages() => new[]
|
||||||
{
|
{
|
||||||
@@ -92,5 +150,53 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
|||||||
},
|
},
|
||||||
TaskScheduler.Default);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Task to sync Jellyfin playback progresses to TubeArchivist.
|
||||||
|
/// </summary>
|
||||||
|
public class JFToTubearchivistProgressSyncTask : IScheduledTask
|
||||||
|
{
|
||||||
|
private readonly ILogger<Plugin> _logger;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IUserDataManager _userDataManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="JFToTubearchivistProgressSyncTask"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger.</param>
|
||||||
|
/// <param name="libraryManager">Library manager.</param>
|
||||||
|
/// <param name="userManager">User manager.</param>
|
||||||
|
/// <param name="userDataManager">User data manager.</param>
|
||||||
|
public JFToTubearchivistProgressSyncTask(ILogger<Plugin> logger, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_userDataManager = userDataManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Name => "JFToTubeArchivistProgressSyncTask";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Description => "This tasks syncs TubeArchivist playback progresses to Jellyfin";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Category => "TubeArchivistMetadata";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Key => "JFToTubeArchivistProgressSyncTask";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task ExecuteAsync(IProgress<double> 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new TaskTriggerInfo
|
||||||
|
{
|
||||||
|
Type = TaskTriggerInfo.TriggerStartup,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Task to sync TubeArchivist playback progresses to Jellyfin.
|
||||||
|
/// </summary>
|
||||||
|
public class TAToJellyfinProgressSyncTask : IScheduledTask
|
||||||
|
{
|
||||||
|
private readonly ILogger<Plugin> _logger;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IUserDataManager _userDataManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TAToJellyfinProgressSyncTask"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger.</param>
|
||||||
|
/// <param name="libraryManager">Library manager.</param>
|
||||||
|
/// <param name="userManager">User manager.</param>
|
||||||
|
/// <param name="userDataManager">User data manager.</param>
|
||||||
|
public TAToJellyfinProgressSyncTask(ILogger<Plugin> logger, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_userDataManager = userDataManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Name => "TAToJellyfinProgressSyncTask";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Description => "This tasks syncs TubeArchivist playback progresses to Jellyfin";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Category => "TubeArchivistMetadata";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Key => "TAToJellyfinProgressSyncTask";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task ExecuteAsync(IProgress<double> 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new TaskTriggerInfo
|
||||||
|
{
|
||||||
|
Type = TaskTriggerInfo.TriggerInterval,
|
||||||
|
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFTaskInterval).Ticks
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A class representing TubeArchivist API video watched status.
|
||||||
|
/// </summary>
|
||||||
|
public class Watched
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="Watched"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Id of the video/channel/playlist.</param>
|
||||||
|
/// <param name="isWatched">Watched status.</param>
|
||||||
|
public Watched(string id, bool isWatched)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
IsWatched = isWatched;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the id of the video/channel/playlist.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "id")]
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the item has been watched or not.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "is_watched")]
|
||||||
|
public bool IsWatched { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -146,5 +147,62 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
|||||||
|
|
||||||
return pong;
|
return pong;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send video playback progress to TubeArchivist.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="videoId">Video id.</param>
|
||||||
|
/// <param name="progress">Progress in seconds.</param>
|
||||||
|
/// <returns>The response <see cref="HttpStatusCode"/>.</returns>
|
||||||
|
public async Task<HttpStatusCode> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send video playback progress to TubeArchivist.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="videoId">Video id.</param>
|
||||||
|
/// <returns>Video playback progress data.</returns>
|
||||||
|
public async Task<Progress?> 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<Progress>(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set video/channel/playlist as watched on TubeArchivist.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">Video/channel/playlist id.</param>
|
||||||
|
/// <param name="isWatched">Whether the item has been watched or not.</param>
|
||||||
|
/// <returns>The response <see cref="HttpStatusCode"/>.</returns>
|
||||||
|
public async Task<HttpStatusCode> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A class representing TubeArchivist API video player data.
|
||||||
|
/// </summary>
|
||||||
|
public class Player
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="Player"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="duration">Duration of the video.</param>
|
||||||
|
/// <param name="isWatched">Whether the video is marked as watched.</param>
|
||||||
|
/// <param name="watchedUnixTime">Unix time of when the video has been marked watched.</param>
|
||||||
|
public Player(long duration, bool isWatched, long watchedUnixTime)
|
||||||
|
{
|
||||||
|
Duration = duration;
|
||||||
|
IsWatched = isWatched;
|
||||||
|
WatchedUnixTime = watchedUnixTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the duration of the video.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "duration")]
|
||||||
|
public long Duration { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the video is marked as watched.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "watched")]
|
||||||
|
public bool IsWatched { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Unix date and time when the video has been marked as watched.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "watched_date")]
|
||||||
|
public long WatchedUnixTime { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the date and time when the video has been marked as watched.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public DateTime WatchedTime
|
||||||
|
{
|
||||||
|
get => DateTimeOffset.FromUnixTimeSeconds(WatchedUnixTime).DateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A class representing TubeArchivist API video progress data.
|
||||||
|
/// </summary>
|
||||||
|
public class Progress
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="Progress"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">Playback progress in seconds.</param>
|
||||||
|
/// <param name="youtubeId">Video YouTube id.</param>
|
||||||
|
/// <param name="userId">Playback progress user id.</param>
|
||||||
|
[JsonConstructor]
|
||||||
|
public Progress(
|
||||||
|
long position,
|
||||||
|
string? youtubeId,
|
||||||
|
long? userId)
|
||||||
|
{
|
||||||
|
YoutubeId = youtubeId;
|
||||||
|
UserId = userId;
|
||||||
|
Position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="Progress"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">Playback progress in seconds.</param>
|
||||||
|
public Progress(long position)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the video YouTube id.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "youtube_id")]
|
||||||
|
public string? YoutubeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets playback progress user id.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "user_id")]
|
||||||
|
public long? UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets video playback progress (in seconds).
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "position")]
|
||||||
|
public long Position { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-1
@@ -27,6 +27,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
|||||||
/// <param name="published">Video published date.</param>
|
/// <param name="published">Video published date.</param>
|
||||||
/// <param name="vidThumbUrl">Video thuumb image URL.</param>
|
/// <param name="vidThumbUrl">Video thuumb image URL.</param>
|
||||||
/// <param name="youtubeId">Video YouTube id.</param>
|
/// <param name="youtubeId">Video YouTube id.</param>
|
||||||
|
/// <param name="player">Player info.</param>
|
||||||
public Video(
|
public Video(
|
||||||
Channel channel,
|
Channel channel,
|
||||||
Collection<string> tags,
|
Collection<string> tags,
|
||||||
@@ -34,7 +35,8 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
|||||||
string description,
|
string description,
|
||||||
DateTime published,
|
DateTime published,
|
||||||
string vidThumbUrl,
|
string vidThumbUrl,
|
||||||
string youtubeId)
|
string youtubeId,
|
||||||
|
Player player)
|
||||||
{
|
{
|
||||||
this.Channel = channel;
|
this.Channel = channel;
|
||||||
this.Tags = tags;
|
this.Tags = tags;
|
||||||
@@ -43,6 +45,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
|||||||
this.Published = published;
|
this.Published = published;
|
||||||
this.VidThumbUrl = vidThumbUrl;
|
this.VidThumbUrl = vidThumbUrl;
|
||||||
this.YoutubeId = youtubeId;
|
this.YoutubeId = youtubeId;
|
||||||
|
Player = player;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -88,6 +91,12 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
|||||||
[JsonProperty(PropertyName = "youtube_id")]
|
[JsonProperty(PropertyName = "youtube_id")]
|
||||||
public string YoutubeId { get; set; }
|
public string YoutubeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Getsthe player related info.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(PropertyName = "player")]
|
||||||
|
public Player Player { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts the TubeArchivist API video to a Jellyfin <see cref="RemoteSearchResult"/> object.
|
/// Converts the TubeArchivist API video to a Jellyfin <see cref="RemoteSearchResult"/> object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Reference in New Issue
Block a user