chore: merge upstream master and fix conflicts

This commit is contained in:
wolffshots
2025-10-22 11:35:12 +02:00
20 changed files with 1276 additions and 86 deletions
@@ -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; }
}
}
@@ -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;
}
}
}