Merge pull request #68 from tubearchivist/feature/playlists-sync
Playlists Sync
This commit is contained in:
Vendored
+6
-5
@@ -1,15 +1,16 @@
|
||||
{
|
||||
// jellyfinDir : The directory of the cloned jellyfin server project
|
||||
// This needs to be built once before it can be used
|
||||
"jellyfinDir" : "${workspaceFolder}/../jellyfin/Jellyfin.Server",
|
||||
"jellyfinDir": "${workspaceFolder}/../jellyfin/Jellyfin.Server",
|
||||
// jellyfinWebDir : The directory of the cloned jellyfin-web project
|
||||
// This needs to be built once before it can be used
|
||||
"jellyfinWebDir" : "${workspaceFolder}/../jellyfin-web",
|
||||
"jellyfinWebDir": "${workspaceFolder}/../jellyfin-web",
|
||||
// jellyfinDataDir : the root data directory for a running jellyfin instance
|
||||
// This is where jellyfin stores its configs, plugins, metadata etc
|
||||
// This is platform specific by default, but on Windows defaults to
|
||||
// ${env:LOCALAPPDATA}/jellyfin
|
||||
"jellyfinDataDir" : "${env:LOCALAPPDATA}/jellyfin",
|
||||
"jellyfinDataDir": "${env:LOCALAPPDATA}/jellyfin",
|
||||
// The name of the plugin
|
||||
"pluginName" : "Jellyfin.Plugin.Template",
|
||||
}
|
||||
"pluginName": "Jellyfin.Plugin.Template",
|
||||
"git.ignoreLimitWarning": true,
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.4.0.0</Version>
|
||||
<AssemblyVersion>1.4.0.0</AssemblyVersion>
|
||||
<FileVersion>1.4.0.0</FileVersion>
|
||||
<Version>1.4.1.0</Version>
|
||||
<AssemblyVersion>1.4.1.0</AssemblyVersion>
|
||||
<FileVersion>1.4.1.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -36,11 +36,17 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
|
||||
_tubeArchivistUrl = string.Empty;
|
||||
_tubeArchivistApiKey = string.Empty;
|
||||
MaxDescriptionLength = 500;
|
||||
JFTASync = false;
|
||||
JFTAProgressSync = false;
|
||||
JFUsernameFrom = string.Empty;
|
||||
TAJFSync = false;
|
||||
TAJFProgressSync = false;
|
||||
JFTAPlaylistsSync = false;
|
||||
JFTAPlaylistsDelete = false;
|
||||
TAJFPlaylistsSync = false;
|
||||
TAJFPlaylistsDelete = false;
|
||||
_jfUsernamesTo = new HashSet<string>();
|
||||
TAJFTaskInterval = 1;
|
||||
TAJFProgressTaskInterval = 60;
|
||||
JFTAPlaylistsSyncTaskInterval = 60;
|
||||
TAJFPlaylistsSyncTaskInterval = 60;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -100,12 +106,32 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable TA->JF playback progress synchronization.
|
||||
/// </summary>
|
||||
public bool TAJFSync { get; set; }
|
||||
public bool TAJFProgressSync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable JF->TA playback progress synchronization.
|
||||
/// </summary>
|
||||
public bool JFTASync { get; set; }
|
||||
public bool JFTAProgressSync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable JF->TA playlists synchronization.
|
||||
/// </summary>
|
||||
public bool JFTAPlaylistsSync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to delete playlists from TA when not found on JF.
|
||||
/// </summary>
|
||||
public bool JFTAPlaylistsDelete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable TA->JF playlists synchronization.
|
||||
/// </summary>
|
||||
public bool TAJFPlaylistsSync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to delete playlists from JF when not found on TA.
|
||||
/// </summary>
|
||||
public bool TAJFPlaylistsDelete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playback progress owner Jellyfin username to synchronize data to TubeArchivist.
|
||||
@@ -146,7 +172,19 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Configuration
|
||||
/// 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; }
|
||||
public int TAJFProgressTaskInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the interval in seconds at which the Jellyfin to TubeArchivist playlists synchronization task should run.
|
||||
/// It requires Jellyfin server restart to take effect.
|
||||
/// </summary>
|
||||
public int JFTAPlaylistsSyncTaskInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the interval in seconds at which the TubeArchivist to Jellyfin playlists synchronization task should run.
|
||||
/// It requires Jellyfin server restart to take effect.
|
||||
/// </summary>
|
||||
public int TAJFPlaylistsSyncTaskInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playback progress owners Jellyfin usernames to synchronize data from TubeArchivist.
|
||||
|
||||
@@ -42,28 +42,43 @@
|
||||
and episodes</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Playback synchronization</h2>
|
||||
<h2>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>
|
||||
<h3>Jellyfin to TubeArchivist</h3>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="JFUsernameFrom">Jellyfin username to sync
|
||||
playback status from</label>
|
||||
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
|
||||
placeholder="Username" />
|
||||
<div class="fieldDescription">This is the Jellyfin username to synchronize data to
|
||||
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 class="emby-checkbox-label" for="JFTAProgressSync">
|
||||
<input id="JFTAProgressSync" name="JFTAProgressSync" type="checkbox" is="emby-checkbox" />
|
||||
<span>Synchronize Jellyfin playback progress to TubeArchivist</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display: flex; gap: 2em;">
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="JFTAPlaylistsSync">
|
||||
<input id="JFTAPlaylistsSync" name="JFTAPlaylistsSync" type="checkbox" is="emby-checkbox" />
|
||||
<span>Synchronize Jellyfin playlists to TubeArchivist</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="JFTAPlaylistsDelete">
|
||||
<input id="JFTAPlaylistsDelete" name="JFTAPlaylistsDelete" type="checkbox" is="emby-checkbox" />
|
||||
<span>Delete TubeArchivist playlists when no more present on Jellyfin</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>TubeArchivist to Jellyfin</h3>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="JFUsernamesTo">Jellyfin usernames to sync
|
||||
playback status to</label>
|
||||
@@ -72,13 +87,52 @@
|
||||
<div class="fieldDescription">These are the (comma separated) Jellyfin usernames to synchronize
|
||||
data from TubeArchivist</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="TAJFProgressSync">
|
||||
<input id="TAJFProgressSync" name="TAJFProgressSync" type="checkbox" is="emby-checkbox" />
|
||||
<span>Synchronize TubeArchivist playback progress to Jellyfin</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display: flex; gap: 2em;">
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="TAJFPlaylistsSync">
|
||||
<input id="TAJFPlaylistsSync" name="TAJFPlaylistsSync" type="checkbox" is="emby-checkbox" />
|
||||
<span>Synchronize TubeArchivist playlists to Jellyfin</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="TAJFPlaylistsDelete">
|
||||
<input id="TAJFPlaylistsDelete" name="TAJFPlaylistsDelete" type="checkbox" is="emby-checkbox" />
|
||||
<span>Delete Jellyfin playlists when no more present on TubeArchivist</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Tasks intervals</h2>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="TAJFTaskInterval">TubeArchivist to Jellyfin
|
||||
<label class="inputLabel inputLabelUnfocused" for="TAJFProgressTaskInterval">TubeArchivist to Jellyfin
|
||||
playback progress synchronization interval</label>
|
||||
<input id="TAJFTaskInterval" name="TAJFTaskInterval" type="number" is="emby-input" min="1" />
|
||||
<input id="TAJFProgressTaskInterval" name="TAJFProgressTaskInterval" type="number" is="emby-input" min="60" />
|
||||
<div class="fieldDescription">This is the TubeArchivist to Jellyfin playback progress
|
||||
synchronization interval in seconds (Jellyfin restart required).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="JFTAPlaylistsSyncTaskInterval">Jellyfin to TubeArchivist
|
||||
playlists synchronization interval</label>
|
||||
<input id="JFTAPlaylistsSyncTaskInterval" name="JFTAPlaylistsSyncTaskInterval" type="number" is="emby-input" min="60" />
|
||||
<div class="fieldDescription">This is the Jellyfin to TubeArchivist playlists
|
||||
synchronization interval in seconds (Jellyfin restart required).</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="TAJFPlaylistsSyncTaskInterval">TubeArchivist to Jellyfin
|
||||
playlists synchronization interval</label>
|
||||
<input id="TAJFPlaylistsSyncTaskInterval" name="TAJFPlaylistsSyncTaskInterval" type="number" is="emby-input"
|
||||
min="60" />
|
||||
<div class="fieldDescription">This is the TubeArchivist to Jellyfin playlists
|
||||
synchronization interval in seconds (Jellyfin restart required).</div>
|
||||
</div>
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
||||
<span>Save</span>
|
||||
@@ -100,11 +154,15 @@
|
||||
document.querySelector('#TubeArchivistUrl').value = config.TubeArchivistUrl;
|
||||
document.querySelector('#TubeArchivistApiKey').value = config.TubeArchivistApiKey;
|
||||
document.querySelector('#MaxDescriptionLength').value = config.MaxDescriptionLength;
|
||||
document.querySelector('#JFTASync').checked = config.JFTASync;
|
||||
document.querySelector('#JFTAProgressSync').checked = config.JFTAProgressSync;
|
||||
document.querySelector('#JFUsernamesTo').value = config.JFUsernamesTo;
|
||||
document.querySelector('#TAJFSync').checked = config.TAJFSync;
|
||||
document.querySelector('#TAJFProgressSync').checked = config.TAJFProgressSync;
|
||||
document.querySelector('#JFTAPlaylistsSync').checked = config.JFTAPlaylistsSync;
|
||||
document.querySelector('#TAJFPlaylistsSync').checked = config.TAJFPlaylistsSync;
|
||||
document.querySelector('#JFUsernameFrom').value = config.JFUsernameFrom;
|
||||
document.querySelector('#TAJFTaskInterval').value = config.TAJFTaskInterval;
|
||||
document.querySelector('#TAJFProgressTaskInterval').value = config.TAJFProgressTaskInterval;
|
||||
document.querySelector('#JFTAPlaylistsSyncTaskInterval').value = config.JFTAPlaylistsSyncTaskInterval;
|
||||
document.querySelector('#TAJFPlaylistsSyncTaskInterval').value = config.TAJFPlaylistsSyncTaskInterval;
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
@@ -117,11 +175,15 @@
|
||||
config.TubeArchivistUrl = document.querySelector('#TubeArchivistUrl').value;
|
||||
config.TubeArchivistApiKey = document.querySelector('#TubeArchivistApiKey').value;
|
||||
config.MaxDescriptionLength = document.querySelector('#MaxDescriptionLength').value;
|
||||
config.JFTASync = document.querySelector('#JFTASync').checked;
|
||||
config.JFTAProgressSync = document.querySelector('#JFTAProgressSync').checked;
|
||||
config.JFUsernamesTo = document.querySelector('#JFUsernamesTo').value;
|
||||
config.TAJFSync = document.querySelector('#TAJFSync').checked;
|
||||
config.TAJFProgressSync = document.querySelector('#TAJFProgressSync').checked;
|
||||
config.JFTAPlaylistsSync = document.querySelector('#JFTAPlaylistsSync').checked;
|
||||
config.TAJFPlaylistsSync = document.querySelector('#TAJFPlaylistsSync').checked;
|
||||
config.JFUsernameFrom = document.querySelector('#JFUsernameFrom').value;
|
||||
config.TAJFTaskInterval = document.querySelector('#TAJFTaskInterval').value;
|
||||
config.TAJFProgressTaskInterval = document.querySelector('#TAJFProgressTaskInterval').value;
|
||||
config.JFTAPlaylistsSyncTaskInterval = document.querySelector('#JFTAPlaylistsSyncTaskInterval').value;
|
||||
config.TAJFPlaylistsSyncTaskInterval = document.querySelector('#TAJFPlaylistsSyncTaskInterval').value;
|
||||
ApiClient.updatePluginConfiguration(TAMetadataConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -40,6 +41,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
||||
/// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
|
||||
public Plugin(
|
||||
IApplicationPaths applicationPaths,
|
||||
IXmlSerializer xmlSerializer,
|
||||
@@ -47,7 +49,8 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
ISessionManager sessionManager,
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager,
|
||||
IUserDataManager userDataManager)
|
||||
IUserDataManager userDataManager,
|
||||
IPlaylistManager playlistManager)
|
||||
: base(applicationPaths, xmlSerializer)
|
||||
{
|
||||
Instance = this;
|
||||
@@ -159,7 +162,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
return;
|
||||
}
|
||||
|
||||
if (Instance!.Configuration.JFTASync && eventArgs.Users.Any(u => Instance!.Configuration.JFUsernameFrom.Equals(u.Username, StringComparison.Ordinal)))
|
||||
if (Instance!.Configuration.JFTAProgressSync && eventArgs.Users.Any(u => Instance!.Configuration.JFUsernameFrom.Equals(u.Username, StringComparison.Ordinal)))
|
||||
{
|
||||
BaseItem? season = LibraryManager.GetItemById(eventArgs.Item.ParentId);
|
||||
BaseItem? channel = LibraryManager.GetItemById(season!.ParentId);
|
||||
@@ -193,7 +196,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata
|
||||
}
|
||||
|
||||
var userItemData = _userDataManager.GetUserData(user, eventArgs.Item);
|
||||
if (Configuration.JFTASync && user != null && Configuration.GetJFUsernamesToArray().Contains(user!.Username))
|
||||
if (Configuration.JFTAProgressSync && user != null && Configuration.GetJFUsernamesToArray().Contains(user!.Username))
|
||||
{
|
||||
var isPlayed = eventArgs.Item.IsPlayed(user, userItemData);
|
||||
Logger.LogDebug("User {UserId} changed watched status to {Status} for the item {ItemName}", eventArgs.UserId, isPlayed, eventArgs.Item.Name);
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using J2N.Collections.Generic.Extensions;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Task to sync TubeArchivist playlists to Jellyfin.
|
||||
/// </summary>
|
||||
public class JFToTubeArchivistPlaylistsSyncTask : IScheduledTask
|
||||
{
|
||||
private readonly ILogger<Plugin> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JFToTubeArchivistPlaylistsSyncTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger.</param>
|
||||
/// <param name="libraryManager">Library manager.</param>
|
||||
/// <param name="userManager">User manager.</param>
|
||||
/// <param name="playlistManager">Playlists manager.</param>
|
||||
public JFToTubeArchivistPlaylistsSyncTask(ILogger<Plugin> logger, ILibraryManager libraryManager, IUserManager userManager, IPlaylistManager playlistManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_playlistManager = playlistManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "JFToTubeArchivistPlaylistsSyncTask";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description => "This tasks syncs Jellyfin playlists to TubeArchivist";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Category => "TubeArchivistMetadata";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Key => "JFToTubeArchivistPlaylistsSyncTask";
|
||||
|
||||
private Dictionary<Guid, List<BaseItem>> GetAllVideos(IEnumerable<MediaBrowser.Controller.Playlists.Playlist> playlists, User user)
|
||||
{
|
||||
var items = new Dictionary<Guid, List<BaseItem>>();
|
||||
foreach (var playlist in playlists)
|
||||
{
|
||||
items[playlist.Id] = new List<BaseItem>(playlist.GetChildren(user, true, new InternalItemsQuery()));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private int CountTotalVideos(Dictionary<Guid, List<BaseItem>> playlistsItems)
|
||||
{
|
||||
var totalVideosCount = 0;
|
||||
foreach (var playlistId in playlistsItems.Keys)
|
||||
{
|
||||
totalVideosCount += playlistsItems[playlistId].Count;
|
||||
}
|
||||
|
||||
return totalVideosCount;
|
||||
}
|
||||
|
||||
private string GetPlaylistUpdatedName(string playlistName, string newId)
|
||||
{
|
||||
var index = playlistName.LastIndexOf(" (", StringComparison.CurrentCulture);
|
||||
if (index < 0)
|
||||
{
|
||||
return $"{playlistName} ({newId})";
|
||||
}
|
||||
else
|
||||
{
|
||||
var authorAndName = playlistName.Substring(0, index);
|
||||
return $"{authorAndName} ({newId})";
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveElementToPosition(Collection<PlaylistEntry> playlistEntries, int oldPosition, int newPosition)
|
||||
{
|
||||
var oldItem = playlistEntries[oldPosition];
|
||||
playlistEntries.RemoveAt(oldPosition);
|
||||
playlistEntries.Insert(newPosition, oldItem);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
progress.Report(0);
|
||||
if (Plugin.Instance!.Configuration.JFTAPlaylistsSync)
|
||||
{
|
||||
var start = DateTime.Now;
|
||||
_logger.LogInformation("Starting Jellyfin->TubeArchivist playlists synchronization.");
|
||||
var taApi = TubeArchivistApi.GetInstance();
|
||||
var taPlaylists = await taApi.GetPlaylists().ConfigureAwait(true);
|
||||
|
||||
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 userPlaylists = _playlistManager.GetPlaylists(user.Id).ToList();
|
||||
var jfItemsToAnalyze = GetAllVideos(userPlaylists, user);
|
||||
var totalVideosCount = CountTotalVideos(jfItemsToAnalyze);
|
||||
var processedVideosCount = 0;
|
||||
|
||||
_logger.LogInformation("Found a total of {PlaylistsCount} playlists to analyze with a total of {VideosCount} videos", userPlaylists.Count, totalVideosCount);
|
||||
if (taPlaylists != null && Plugin.Instance!.Configuration.JFTAPlaylistsDelete)
|
||||
{
|
||||
var taPlaylistsToDelete = taPlaylists.Where(tp => !userPlaylists.Select(up => Utils.GetTAPlaylistIdFromName(up.Name)).Contains(tp.Id));
|
||||
foreach (var taPlaylistToDelete in taPlaylistsToDelete)
|
||||
{
|
||||
_logger.LogInformation("Deleting TubeArchivist playlist {PlaylistName} ({PlaylistId})", taPlaylistToDelete.Name, taPlaylistToDelete.Id);
|
||||
await taApi.DeletePlaylist(taPlaylistToDelete.Id).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var jfPlaylist in userPlaylists)
|
||||
{
|
||||
_logger.LogInformation("Analyzing playlist {PlaylistName}...", jfPlaylist.Name);
|
||||
var taPlaylistId = Utils.GetTAPlaylistIdFromName(jfPlaylist.Name);
|
||||
if (taPlaylistId == null)
|
||||
{
|
||||
_logger.LogDebug("The playlist {PlaylistName} was not a TA playlist: could not find TA playlist id at the end", jfPlaylist.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to find a TA playlist with matching id
|
||||
// N.B.: only videos with TubeArchivist provider can be synced to TA, if there is a non matching video log a warn
|
||||
var taPlaylist = taPlaylists?.Where(tp => tp.Id == taPlaylistId).FirstOrDefault();
|
||||
|
||||
if (taPlaylist != null)
|
||||
{
|
||||
// If it exists and is custom add new videos and update videos order
|
||||
if (taPlaylist.Type == PlaylistType.Custom)
|
||||
{
|
||||
var jfItems = jfItemsToAnalyze[jfPlaylist.Id];
|
||||
_logger.LogInformation("Found {PlaylistVideosCount} videos in playlist {PlaylistName}", jfItems.Count, jfPlaylist.Name);
|
||||
|
||||
var itemsToProcess = new List<PlaylistItemAction>();
|
||||
|
||||
// Add videos to move/add to the TA playlist
|
||||
for (var i = 0; i < jfItems.Count; i++)
|
||||
{
|
||||
if (!jfItems[i].ProviderIds.ContainsKey(Constants.ProviderName))
|
||||
{
|
||||
_logger.LogError("Could not sync {JFItem} video from playlist {JFPlaylist} because it doesn't belong to TubeArchivist", jfItems[i].Name, jfPlaylist.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
var position = taPlaylist.Entries.FindIndex(e => e.YoutubeId == jfItems[i].ProviderIds[Constants.ProviderName]);
|
||||
PlaylistItemAction action;
|
||||
if (position < 0)
|
||||
{
|
||||
_logger.LogDebug("Video {VideoName} from playlist {PlaylistName} not found in TA playlist", jfItems[i].Name, jfPlaylist.Name);
|
||||
var positionsToMove = i - position;
|
||||
action = new PlaylistItemAction(i, jfItems[i].ProviderIds[Constants.ProviderName], CustomPlaylistAction.Create, positionsToMove, jfItems[i].Name);
|
||||
taPlaylist.Entries.Insert(i, new PlaylistEntry(jfItems[i].ProviderIds[Constants.ProviderName], jfItems[i].Name, jfItems[i].Studios[0], i, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Found video {VideoName} at position {JFPosition} in playlist {PlaylistName} at position {TAPosition} in TA playlist", jfItems[i].Name, i, jfPlaylist.Name, position);
|
||||
if (i == 0)
|
||||
{
|
||||
action = new PlaylistItemAction(i, jfItems[i].ProviderIds[Constants.ProviderName], CustomPlaylistAction.Top, jfItems[i].Name);
|
||||
}
|
||||
else if (i == jfItems.Count)
|
||||
{
|
||||
action = new PlaylistItemAction(i, jfItems[i].ProviderIds[Constants.ProviderName], CustomPlaylistAction.Bottom, jfItems[i].Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
var positionsToMove = i - position;
|
||||
action = new PlaylistItemAction(i, jfItems[i].ProviderIds[Constants.ProviderName], positionsToMove > 0 ? CustomPlaylistAction.Up : CustomPlaylistAction.Down, Math.Abs(positionsToMove), jfItems[i].Name);
|
||||
}
|
||||
|
||||
var temp = taPlaylist.Entries[position];
|
||||
taPlaylist.Entries.RemoveAt(position);
|
||||
taPlaylist.Entries.Insert(i, temp);
|
||||
}
|
||||
|
||||
itemsToProcess.Add(action);
|
||||
}
|
||||
|
||||
// Add videos to delete from the TA playlist
|
||||
var itemsToDelete = taPlaylist.Entries
|
||||
.Where(e => !itemsToProcess
|
||||
.Select(i => i.YoutubeId)
|
||||
.Contains(e.YoutubeId))
|
||||
.ToList();
|
||||
foreach (var item in itemsToDelete)
|
||||
{
|
||||
_logger.LogDebug("Video {VideoName} not found in JF playlist, marking for deletion", item.Title);
|
||||
var ret = taPlaylist.Entries.Remove(item);
|
||||
}
|
||||
|
||||
itemsToProcess.AddRange(itemsToDelete.Select(e => new PlaylistItemAction(e.YoutubeId, e.Title)));
|
||||
|
||||
foreach (var item in itemsToProcess)
|
||||
{
|
||||
_logger.LogDebug("Analyzing video {VideoName} from playlist {PlaylistName} at position {Position}", item.Name, jfPlaylist.Name, item.Index);
|
||||
switch (item.Action)
|
||||
{
|
||||
case CustomPlaylistAction.Create:
|
||||
var success = await AddVideoToTAPlaylist(taApi, jfPlaylist, taPlaylistId, item).ConfigureAwait(true);
|
||||
if (!success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case CustomPlaylistAction.Top:
|
||||
case CustomPlaylistAction.Bottom:
|
||||
_logger.LogDebug("Moving the video {VideoName} to the {Side} of the playlist {PlaylistName}", item.Name, item.Action, jfPlaylist.Name);
|
||||
await MoveOrDeleteVideo(taApi, jfPlaylist, taPlaylistId, item).ConfigureAwait(true);
|
||||
break;
|
||||
|
||||
case CustomPlaylistAction.Remove:
|
||||
_logger.LogDebug("Removing the video {VideoName} in playlist {PlaylistName}", item.Name, jfPlaylist.Name);
|
||||
await MoveOrDeleteVideo(taApi, jfPlaylist, taPlaylistId, item).ConfigureAwait(true);
|
||||
break;
|
||||
|
||||
default:
|
||||
_logger.LogDebug("Moving the video {VideoName} {Direction} in playlist {PlaylistName}", item.Name, item.Action, jfPlaylist.Name);
|
||||
await MoveOrDeleteVideo(taApi, jfPlaylist, taPlaylistId, item).ConfigureAwait(true);
|
||||
break;
|
||||
}
|
||||
|
||||
processedVideosCount++;
|
||||
progress.Report(processedVideosCount * 100 / totalVideosCount);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Could not sync modifications on a YouTube downloaded TA playlist: {TAPlaylist}", $"{taPlaylist.Name} - {taPlaylist.Channel} ({taPlaylist.Id})");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If it doesn't, create a new custom playlist and add videos
|
||||
_logger.LogInformation("Playlist {PlaylistName} was not found on TubeArchivist. Creating a new playlist...", jfPlaylist.Name);
|
||||
|
||||
var playlistName = Utils.GetTAPlaylistNameFromName(jfPlaylist.Name);
|
||||
if (string.IsNullOrEmpty(playlistName))
|
||||
{
|
||||
playlistName = jfPlaylist.Name;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Creating a new TubeArchivist playlist with name: {PlaylistName}", playlistName);
|
||||
|
||||
var creationRequest = new CustomPlaylistCreation(playlistName);
|
||||
var createdTAPlaylist = await taApi.CreateCustomPlaylist(creationRequest).ConfigureAwait(true);
|
||||
if (createdTAPlaylist == null)
|
||||
{
|
||||
_logger.LogError("Failed to create the playlist {JFPlaylist}", playlistName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update the JF playlist name with the new TA playlist id
|
||||
var newPlaylistName = GetPlaylistUpdatedName(jfPlaylist.Name, createdTAPlaylist.Id);
|
||||
var updateRequest = new PlaylistUpdateRequest
|
||||
{
|
||||
Id = jfPlaylist.Id,
|
||||
Name = newPlaylistName,
|
||||
UserId = user.Id
|
||||
};
|
||||
await _playlistManager.UpdatePlaylist(updateRequest).ConfigureAwait(true);
|
||||
_logger.LogDebug("Updated the playlist name from {NewPlaylistName} to {NewPlaylistName}", playlistName, newPlaylistName);
|
||||
|
||||
foreach (var jfItem in jfPlaylist.GetItemList(new InternalItemsQuery()))
|
||||
{
|
||||
_logger.LogDebug("Adding the video {VideoName} to the playlist {PlaylistName}", jfItem.Name, jfPlaylist.Name);
|
||||
var action = new CustomPlaylistEntryAction(CustomPlaylistAction.Create, jfItem.ProviderIds[Constants.ProviderName]);
|
||||
var response = await taApi.CustomPlaylistEntryAction(taPlaylistId, action).ConfigureAwait(true);
|
||||
if (response != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Failed to add the video {JFItem} to playlist {JFPlaylist}", $"{jfItem.Name} ({jfItem.ProviderIds[Constants.ProviderName]})", jfPlaylist.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
processedVideosCount++;
|
||||
progress.Report(processedVideosCount * 100 / totalVideosCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Time elapsed: {Time}", DateTime.Now - start);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Jellyfin->TubeArchivist playlists synchronization is currently disabled.");
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
private async Task MoveOrDeleteVideo(TubeArchivistApi taApi, MediaBrowser.Controller.Playlists.Playlist jfPlaylist, string taPlaylistId, PlaylistItemAction item)
|
||||
{
|
||||
var response = HttpStatusCode.OK;
|
||||
for (var i = 0; i < item.PositionsToMove; i++)
|
||||
{
|
||||
var action = new CustomPlaylistEntryAction(item.Action, item.YoutubeId);
|
||||
response = await taApi.CustomPlaylistEntryAction(taPlaylistId, action).ConfigureAwait(true);
|
||||
if (response != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Failed to execute the operation {Operation} for the video {JFItem} in playlist {PlaylistName}", item.Action, $"{item.Name} ({item.YoutubeId})", jfPlaylist.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> AddVideoToTAPlaylist(TubeArchivistApi taApi, MediaBrowser.Controller.Playlists.Playlist jfPlaylist, string taPlaylistId, PlaylistItemAction item)
|
||||
{
|
||||
// TA playlist doesn't contain the JF video
|
||||
_logger.LogDebug("Adding the video {VideoName} to the playlist {PlaylistName}", item.Name, jfPlaylist.Name);
|
||||
var action = new CustomPlaylistEntryAction(CustomPlaylistAction.Create, item.YoutubeId);
|
||||
var response = await taApi.CustomPlaylistEntryAction(taPlaylistId, action).ConfigureAwait(true);
|
||||
if (response != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Failed to add the video {JFItem} to playlist {JFPlaylist}", $"{item.Name} ({item.YoutubeId})", jfPlaylist.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since now we have the video added at the bottom of the TA playlist (previouse entries count + 1),
|
||||
// we need to move the video to the correct position
|
||||
var positionsToMove = item.PositionsToMove;
|
||||
action = new CustomPlaylistEntryAction(CustomPlaylistAction.Up, item.YoutubeId);
|
||||
if (item.PositionsToMove < 0)
|
||||
{
|
||||
positionsToMove = (int)Math.Abs((decimal)positionsToMove!);
|
||||
action = new CustomPlaylistEntryAction(CustomPlaylistAction.Up, item.YoutubeId);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Moving the video {VideoName} of the playlist {PlaylistName} {Positions} up", item.Name, jfPlaylist.Name, positionsToMove);
|
||||
for (var i = 0; i < positionsToMove; i++)
|
||||
{
|
||||
response = await taApi.CustomPlaylistEntryAction(taPlaylistId, action).ConfigureAwait(true);
|
||||
if (response != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Failed to move {Direction} the video {JFItem} in playlist {JFPlaylist}", item.Action, $"{item.Name} ({item.YoutubeId})", jfPlaylist.Name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
return
|
||||
[
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFProgressTaskInterval).Ticks
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private sealed class PlaylistItemAction
|
||||
{
|
||||
public PlaylistItemAction(
|
||||
int index,
|
||||
string youtubeId,
|
||||
CustomPlaylistAction action,
|
||||
int positionsToMove,
|
||||
string name)
|
||||
{
|
||||
Index = index;
|
||||
YoutubeId = youtubeId;
|
||||
Action = action;
|
||||
PositionsToMove = positionsToMove;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public PlaylistItemAction(
|
||||
int index,
|
||||
string youtubeId,
|
||||
CustomPlaylistAction action,
|
||||
string name)
|
||||
{
|
||||
Index = index;
|
||||
YoutubeId = youtubeId;
|
||||
Action = action;
|
||||
Name = name;
|
||||
PositionsToMove = 1;
|
||||
}
|
||||
|
||||
public PlaylistItemAction(
|
||||
string youtubeId,
|
||||
string name)
|
||||
{
|
||||
YoutubeId = youtubeId;
|
||||
Action = CustomPlaylistAction.Remove;
|
||||
Name = name;
|
||||
PositionsToMove = 1;
|
||||
}
|
||||
|
||||
public int? Index { get; set; }
|
||||
|
||||
public string YoutubeId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public CustomPlaylistAction Action { get; set; }
|
||||
|
||||
public int? PositionsToMove { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
/// <summary>
|
||||
/// Task to sync Jellyfin playback progresses to TubeArchivist.
|
||||
/// </summary>
|
||||
public class JFToTubearchivistProgressSyncTask : IScheduledTask
|
||||
public class JFToTubeArchivistProgressSyncTask : IScheduledTask
|
||||
{
|
||||
private readonly ILogger<Plugin> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@@ -28,13 +28,13 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JFToTubearchivistProgressSyncTask"/> class.
|
||||
/// 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)
|
||||
public JFToTubeArchivistProgressSyncTask(ILogger<Plugin> logger, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
@@ -58,7 +58,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
progress.Report(0);
|
||||
if (Plugin.Instance!.Configuration.JFTASync)
|
||||
if (Plugin.Instance!.Configuration.JFTAProgressSync)
|
||||
{
|
||||
var start = DateTime.Now;
|
||||
_logger.LogInformation("Starting Jellyfin->TubeArchivist playback progresses synchronization.");
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Task to sync TubeArchivist playlists to Jellyfin.
|
||||
/// </summary>
|
||||
public class TAToJellyfinPlaylistsSyncTask : IScheduledTask
|
||||
{
|
||||
private readonly ILogger<Plugin> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TAToJellyfinPlaylistsSyncTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger.</param>
|
||||
/// <param name="libraryManager">Library manager.</param>
|
||||
/// <param name="userManager">User manager.</param>
|
||||
/// <param name="playlistManager">Playlists manager.</param>
|
||||
public TAToJellyfinPlaylistsSyncTask(ILogger<Plugin> logger, ILibraryManager libraryManager, IUserManager userManager, IPlaylistManager playlistManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_playlistManager = playlistManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "TAToJellyfinPlaylistsSyncTask";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description => "This tasks syncs TubeArchivist playlists to Jellyfin";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Category => "TubeArchivistMetadata";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Key => "TAToJellyfinPlaylistsSyncTask";
|
||||
|
||||
private int CountTotalVideos(ISet<TubeArchivist.Playlist> taPlaylists)
|
||||
{
|
||||
var totalEntries = 0;
|
||||
foreach (var taPlaylist in taPlaylists)
|
||||
{
|
||||
totalEntries += taPlaylist.Entries.Count;
|
||||
}
|
||||
|
||||
return totalEntries;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
progress.Report(0);
|
||||
if (Plugin.Instance!.Configuration.TAJFPlaylistsSync)
|
||||
{
|
||||
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).ToList();
|
||||
|
||||
if (Plugin.Instance!.Configuration.TAJFPlaylistsDelete)
|
||||
{
|
||||
var jfPlaylistsToDelete = userPlaylists.Where(up => !taPlaylists.Select(tp => tp.Id).Contains(Utils.GetTAPlaylistIdFromName(up.Name)));
|
||||
foreach (var jfPlaylistToDelete in jfPlaylistsToDelete)
|
||||
{
|
||||
_logger.LogInformation("Deleting Jellyfin playlist {PlaylistName}", jfPlaylistToDelete.Name);
|
||||
_libraryManager.DeleteItem(jfPlaylistToDelete, new DeleteOptions());
|
||||
}
|
||||
}
|
||||
|
||||
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<Guid>();
|
||||
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 entries = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
HasAnyProviderId = new Dictionary<string, string>()
|
||||
{
|
||||
{
|
||||
Constants.ProviderName, taEntry.YoutubeId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var jfEntry = entries.Count > 0 ? entries[0] : null;
|
||||
|
||||
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 playlists synchronization is currently disabled.");
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
return
|
||||
[
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFProgressTaskInterval).Ticks
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
progress.Report(0);
|
||||
if (Plugin.Instance!.Configuration.TAJFSync)
|
||||
if (Plugin.Instance!.Configuration.TAJFProgressSync)
|
||||
{
|
||||
var start = DateTime.Now;
|
||||
_logger.LogInformation("Starting TubeArchivist->Jellyfin playback progresses synchronization.");
|
||||
@@ -219,7 +219,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Tasks
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFTaskInterval).Ticks
|
||||
IntervalTicks = TimeSpan.FromSeconds(Plugin.Instance!.Configuration.TAJFProgressTaskInterval).Ticks
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API custom playlist creation request.
|
||||
/// </summary>
|
||||
public class CustomPlaylistCreation
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CustomPlaylistCreation"/> class.
|
||||
/// </summary>
|
||||
/// <param name="playlistName">The name of the new custom playlist to create.</param>
|
||||
public CustomPlaylistCreation(string playlistName)
|
||||
{
|
||||
this.PlaylistName = playlistName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the new custom playlist to create.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_name")]
|
||||
public string PlaylistName { get; set; }
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum representing the possible actions to execute on a TubeArchivist custom playlist video.
|
||||
/// </summary>
|
||||
public enum CustomPlaylistAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new custom playlist video.
|
||||
/// </summary>
|
||||
Create,
|
||||
|
||||
/// <summary>
|
||||
/// Removea custom playlist video.
|
||||
/// </summary>
|
||||
Remove,
|
||||
|
||||
/// <summary>
|
||||
/// Move a custom playlist video to the top of the list.
|
||||
/// </summary>
|
||||
Top,
|
||||
|
||||
/// <summary>
|
||||
/// Move a custom playlist video to the bottom of the list.
|
||||
/// </summary>
|
||||
Bottom,
|
||||
|
||||
/// <summary>
|
||||
/// Move a custom playlist video up in the list.
|
||||
/// </summary>
|
||||
Up,
|
||||
|
||||
/// <summary>
|
||||
/// Move a custom playlist video down in the list.
|
||||
/// </summary>
|
||||
Down
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API custom playlist entry operation request.
|
||||
/// </summary>
|
||||
public class CustomPlaylistEntryAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CustomPlaylistEntryAction"/> class.
|
||||
/// </summary>
|
||||
/// <param name="action">The standard action to create a new entry.</param>
|
||||
/// <param name="videoId">The entry YoutubeId.</param>
|
||||
public CustomPlaylistEntryAction(
|
||||
CustomPlaylistAction action,
|
||||
string videoId)
|
||||
{
|
||||
this.Action = action.ToString().ToLower(CultureInfo.CurrentCulture);
|
||||
this.VideoId = videoId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the standard action to create a new entry.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "action")]
|
||||
public string Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the entry YoutubeId.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "video_id")]
|
||||
public string VideoId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum representing whether the playlist on TubeArchivist belongs from YouTube or is created by the user.
|
||||
/// </summary>
|
||||
public enum PlaylistType
|
||||
{
|
||||
/// <summary>
|
||||
/// Playlist belongs from YouTube.
|
||||
/// </summary>
|
||||
Regular,
|
||||
|
||||
/// <summary>
|
||||
/// Playlist created by the user.
|
||||
/// </summary>
|
||||
Custom
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API playlist data.
|
||||
/// </summary>
|
||||
public class Playlist
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Playlist"/> class.
|
||||
/// </summary>
|
||||
/// <param name="isActive">URL of the channel banner image.</param>
|
||||
/// <param name="channel">Playlist channel name.</param>
|
||||
/// <param name="channelId">Playlist channel YouTube id.</param>
|
||||
/// <param name="description">Playlist description.</param>
|
||||
/// <param name="entries">Playlist videos.</param>
|
||||
/// <param name="id">Playlist YouTube id.</param>
|
||||
/// <param name="name">Playlist name.</param>
|
||||
/// <param name="thumbnailUrl">URL of the playlist thumbnail.</param>
|
||||
/// <param name="type">Type of the playlist. See <see cref="PlaylistType"/>.</param>
|
||||
public Playlist(
|
||||
bool isActive,
|
||||
string channel,
|
||||
string channelId,
|
||||
string description,
|
||||
Collection<PlaylistEntry> entries,
|
||||
string id,
|
||||
string name,
|
||||
string thumbnailUrl, // TODO: Check if settable on Jellyfin
|
||||
PlaylistType type)
|
||||
{
|
||||
this.IsActive = isActive;
|
||||
this.Channel = channel;
|
||||
this.ChannelId = channelId;
|
||||
this.Description = description;
|
||||
this.Entries = entries;
|
||||
this.Id = id;
|
||||
this.Name = name;
|
||||
this.ThumbnailUrl = thumbnailUrl;
|
||||
this.Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the playlist is active or not.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_active")]
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist channel name.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_channel")]
|
||||
public string Channel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist channel YouTube id.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_channel_id")]
|
||||
public string ChannelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist description.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playlist videos.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_entries")]
|
||||
public Collection<PlaylistEntry> Entries { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist YouTube id.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist name.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist thumbnail URL.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "playlist_thumbnail")]
|
||||
public string ThumbnailUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist type.
|
||||
/// </summary>
|
||||
/// <value>One of the <see cref="PlaylistType"/> values indicating the playlist's type.</value>
|
||||
[JsonProperty(PropertyName = "playlist_type")]
|
||||
public PlaylistType Type { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using ICU4N.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
{
|
||||
/// <summary>
|
||||
/// A class representing TubeArchivist API playlist entries data.
|
||||
/// </summary>
|
||||
public class PlaylistEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlaylistEntry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="youtubeId">Video YouTube id.</param>
|
||||
/// <param name="title">Video title.</param>
|
||||
/// <param name="uploader">Video uploader.</param>
|
||||
/// <param name="index">Video index in the playlist.</param>
|
||||
/// <param name="isDownloaded">A value indicating whether the video has been already downloaded or not.</param>
|
||||
public PlaylistEntry(
|
||||
string youtubeId,
|
||||
string title,
|
||||
string uploader,
|
||||
int index,
|
||||
bool isDownloaded)
|
||||
{
|
||||
this.YoutubeId = youtubeId;
|
||||
this.Title = title;
|
||||
this.Uploader = uploader;
|
||||
this.Index = index;
|
||||
this.IsDownloaded = isDownloaded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video YouTube id.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "youtube_id")]
|
||||
public string YoutubeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video title.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video uploader.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "uploader")]
|
||||
public string Uploader { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video index in the playlist.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "idx")]
|
||||
public int Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the video has been already downloaded or not.
|
||||
/// </summary>
|
||||
[JsonProperty(PropertyName = "downloaded")]
|
||||
public bool IsDownloaded { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.TubeArchivistMetadata.Utilities;
|
||||
using MediaBrowser.Model.Playlists;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@@ -150,7 +152,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send video playback progress to TubeArchivist.
|
||||
/// Sends video playback progress to TubeArchivist.
|
||||
/// </summary>
|
||||
/// <param name="videoId">Video id.</param>
|
||||
/// <param name="progress">Progress in seconds.</param>
|
||||
@@ -167,7 +169,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send video playback progress to TubeArchivist.
|
||||
/// Sends video playback progress to TubeArchivist.
|
||||
/// </summary>
|
||||
/// <param name="videoId">Video id.</param>
|
||||
/// <returns>Video playback progress data.</returns>
|
||||
@@ -186,7 +188,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set video/channel/playlist as watched on TubeArchivist.
|
||||
/// Sets 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>
|
||||
@@ -201,5 +203,93 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.TubeArchivist
|
||||
|
||||
return response.StatusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the playlists from TubeArchivist.
|
||||
/// </summary>
|
||||
/// <returns>A task.</returns>
|
||||
public async Task<ISet<Playlist>?> GetPlaylists()
|
||||
{
|
||||
ResponseContainer<ISet<Playlist>?>? playlists = null;
|
||||
|
||||
var playlistsEndpoint = "/api/playlist/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance?.Configuration.TubeArchivistUrl + playlistsEndpoint));
|
||||
var response = await client.GetAsync(url).ConfigureAwait(true);
|
||||
while (response.StatusCode == HttpStatusCode.Moved)
|
||||
{
|
||||
url = response.Headers.Location;
|
||||
_logger.LogInformation("{Message}", "Received redirect to: " + url);
|
||||
response = await client.GetAsync(url).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
_logger.LogInformation("{Message}", url + ": " + response.StatusCode);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true);
|
||||
playlists = JsonConvert.DeserializeObject<ResponseContainer<ISet<Playlist>?>>(rawData);
|
||||
}
|
||||
|
||||
return playlists?.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new custom playlist.
|
||||
/// </summary>
|
||||
/// <param name="creationRequest">Playlist creation request.</param>
|
||||
/// <returns>The created <see cref="Playlist"/>.</returns>
|
||||
public async Task<Playlist?> CreateCustomPlaylist(CustomPlaylistCreation creationRequest)
|
||||
{
|
||||
Playlist? playlist = null;
|
||||
var customPlaylistEndpoint = $"/api/playlist/custom/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + customPlaylistEndpoint));
|
||||
var body = JsonConvert.SerializeObject(creationRequest);
|
||||
_logger.LogInformation("{Message}", body);
|
||||
|
||||
var response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json")).ConfigureAwait(true);
|
||||
_logger.LogInformation("POST {Message}", url + ": " + response.StatusCode);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string rawData = await response.Content.ReadAsStringAsync().ConfigureAwait(true);
|
||||
playlist = JsonConvert.DeserializeObject<Playlist>(rawData);
|
||||
}
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new custom playlist.
|
||||
/// </summary>
|
||||
/// <param name="playlistId">Playlist id.</param>
|
||||
/// <param name="entryAction">Playlist creation request.</param>
|
||||
/// <returns>The response <see cref="HttpStatusCode"/>.</returns>
|
||||
public async Task<HttpStatusCode> CustomPlaylistEntryAction(string playlistId, CustomPlaylistEntryAction entryAction)
|
||||
{
|
||||
var customPlaylistEndpoint = $"/api/playlist/custom/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + customPlaylistEndpoint + playlistId));
|
||||
var body = JsonConvert.SerializeObject(entryAction);
|
||||
_logger.LogDebug("CustomPlaylistEntryAction body: {Message}", body);
|
||||
|
||||
var response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json")).ConfigureAwait(true);
|
||||
_logger.LogDebug("Response code: {Message}", response.StatusCode);
|
||||
|
||||
return response.StatusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a playlist.
|
||||
/// </summary>
|
||||
/// <param name="playlistId">Playlist id.</param>
|
||||
/// <returns>Whether the playlist has been deleted successfully or not.</returns>
|
||||
public async Task<bool> DeletePlaylist(string playlistId)
|
||||
{
|
||||
var deletePlaylistEndpoint = $"/api/playlist/";
|
||||
var url = new Uri(Utils.SanitizeUrl(Plugin.Instance!.Configuration.TubeArchivistUrl + deletePlaylistEndpoint + playlistId));
|
||||
var response = await client.DeleteAsync(url).ConfigureAwait(true);
|
||||
_logger.LogDebug("Response code: {Message}", response.StatusCode);
|
||||
|
||||
return response.StatusCode == HttpStatusCode.NoContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,12 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
/// </summary>
|
||||
public static class Utils
|
||||
{
|
||||
private const string TAPlaylistIdRegex = @"^(.*)\((.*)\)$";
|
||||
private const string YTTAPlaylistNameFormatRegex = @"^(.*)\s\-\s(.*)\s\((.*)\)$";
|
||||
private const string TAPlaylistNameFormatRegex = @"^(.*)\s\((.*)\)$";
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize the given URL.
|
||||
/// Sanitizes the given URL.
|
||||
/// </summary>
|
||||
/// <param name="inputUrl">An URL string.</param>
|
||||
/// <returns>The URL string without spaces, doubled slashes and with a trailing slash.</returns>
|
||||
@@ -35,7 +39,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format episodes and series descriptions replacing newlines with br tags.
|
||||
/// Formats episodes and series descriptions replacing newlines with br tags.
|
||||
/// </summary>
|
||||
/// <param name="description">String to format.</param>
|
||||
/// <returns>A string with \n replaced by br tags.</returns>
|
||||
@@ -57,7 +61,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get video name from file path on the disk.
|
||||
/// Gets video name from file path on the disk.
|
||||
/// </summary>
|
||||
/// <param name="path">File path on disk.</param>
|
||||
/// <returns>The video name.</returns>
|
||||
@@ -67,7 +71,7 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get channel name from directory path on the disk.
|
||||
/// Gets channel name from directory path on the disk.
|
||||
/// </summary>
|
||||
/// <param name="path">Directory path on disk.</param>
|
||||
/// <returns>The channel name.</returns>
|
||||
@@ -90,5 +94,35 @@ namespace Jellyfin.Plugin.TubeArchivistMetadata.Utilities
|
||||
return '/'; // Unix directory separator
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TubeArchivist playlist id from Jellyfin playlist name.
|
||||
/// </summary>
|
||||
/// <param name="playlistName">The Jellyfin playlist name.</param>
|
||||
/// <returns>The TubeArchvist playlist id.</returns>
|
||||
public static string? GetTAPlaylistIdFromName(string playlistName)
|
||||
{
|
||||
var regex = new Regex(TAPlaylistIdRegex);
|
||||
return regex.Match(playlistName).Groups[2].ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TubeArchivist playlist name from Jellyfin playlist name.
|
||||
/// </summary>
|
||||
/// <param name="playlistName">The Jellyfin playlist name.</param>
|
||||
/// <returns>The TubeArchivist playlist name.</returns>
|
||||
public static string? GetTAPlaylistNameFromName(string playlistName)
|
||||
{
|
||||
var ytRegex = new Regex(YTTAPlaylistNameFormatRegex);
|
||||
var regex = new Regex(TAPlaylistNameFormatRegex);
|
||||
|
||||
var name = ytRegex.Match(playlistName).Groups[1].ToString();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
name = regex.Match(playlistName).Groups[1].ToString();
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@ The plugin interacts with TubeArchivist APIs to fetch videos and channels metada
|
||||
- Add images for videos (episodes), ie. thumb images
|
||||
- Add images for channels (shows), ie. thumb, tvart and banner images
|
||||
- Organize videos (episodes) by year (seasons)
|
||||
- Bidirectional playback progress synchronization
|
||||
- Bidirectional playlists synchronization
|
||||
|
||||
> [!WARNING]
|
||||
> Enabling synchronization in both directions you can run in race conditions and unexpected results.
|
||||
|
||||
## Installation
|
||||
### From official repository (recommended)
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
name: "TubeArchivistMetadata"
|
||||
guid: "dc97d0c6-28b0-4242-afb4-5833ae1b3715"
|
||||
imageUrl: https://raw.githubusercontent.com/tubearchivist/tubearchivist-jf-plugin/master/images/logo.png
|
||||
version: "1.4.0.0"
|
||||
version: "1.4.1.0"
|
||||
targetAbi: "10.11.0.0"
|
||||
framework: "net9.0"
|
||||
overview: "Metadata for your TubeArchivist library on Jellyfin"
|
||||
@@ -14,4 +14,4 @@ owner: "DarkFighterLuke"
|
||||
artifacts:
|
||||
- "Jellyfin.Plugin.TubeArchivistMetadata.dll"
|
||||
changelog: >
|
||||
Align compatibility with Jellyfin 10.11.x
|
||||
Introduce bidirectional playlists synchronization
|
||||
|
||||
Reference in New Issue
Block a user