first commit
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a chapter (video/episode) in the SRF API response.
|
||||
/// </summary>
|
||||
public class Chapter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URN (Uniform Resource Name).
|
||||
/// </summary>
|
||||
[JsonPropertyName("urn")]
|
||||
public string Urn { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lead/description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lead")]
|
||||
public string? Lead { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("imageUrl")]
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration in milliseconds.
|
||||
/// </summary>
|
||||
[JsonPropertyName("duration")]
|
||||
public long Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the publication date.
|
||||
/// </summary>
|
||||
[JsonPropertyName("date")]
|
||||
public DateTime? Date { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the valid from date.
|
||||
/// </summary>
|
||||
[JsonPropertyName("validFrom")]
|
||||
public DateTime? ValidFrom { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the valid to date (expiration).
|
||||
/// </summary>
|
||||
[JsonPropertyName("validTo")]
|
||||
public DateTime? ValidTo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of available resources (streams).
|
||||
/// </summary>
|
||||
[JsonPropertyName("resourceList")]
|
||||
public IReadOnlyList<Resource> ResourceList { get; set; } = new List<Resource>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the episode number.
|
||||
/// </summary>
|
||||
[JsonPropertyName("episodeNumber")]
|
||||
public int? EpisodeNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the season number.
|
||||
/// </summary>
|
||||
[JsonPropertyName("seasonNumber")]
|
||||
public int? SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the media type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("mediaType")]
|
||||
public string? MediaType { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an episode in the SRF API response.
|
||||
/// </summary>
|
||||
public class Episode
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URN.
|
||||
/// </summary>
|
||||
[JsonPropertyName("urn")]
|
||||
public string? Urn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lead/description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lead")]
|
||||
public string? Lead { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("imageUrl")]
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the episode number.
|
||||
/// </summary>
|
||||
[JsonPropertyName("episodeNumber")]
|
||||
public int? EpisodeNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the season number.
|
||||
/// </summary>
|
||||
[JsonPropertyName("seasonNumber")]
|
||||
public int? SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the publication date.
|
||||
/// </summary>
|
||||
[JsonPropertyName("publishedDate")]
|
||||
public DateTime? PublishedDate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the root media composition response from SRF API.
|
||||
/// </summary>
|
||||
public class MediaComposition
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of chapters (videos/episodes).
|
||||
/// </summary>
|
||||
[JsonPropertyName("chapterList")]
|
||||
public IReadOnlyList<Chapter> ChapterList { get; set; } = new List<Chapter>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the episode information.
|
||||
/// </summary>
|
||||
[JsonPropertyName("episode")]
|
||||
public Episode? Episode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the show information.
|
||||
/// </summary>
|
||||
[JsonPropertyName("show")]
|
||||
public Show? Show { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the data container in a Play v3 response.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of data items.</typeparam>
|
||||
public class PlayV3DataContainer<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of data items.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
[SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Required for JSON deserialization")]
|
||||
[SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "DTO for JSON deserialization")]
|
||||
public List<T>? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the next page cursor.
|
||||
/// </summary>
|
||||
[JsonPropertyName("next")]
|
||||
public string? Next { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a direct data response (for shows list).
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of data items.</typeparam>
|
||||
public class PlayV3DirectResponse<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of data items.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
[SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Required for JSON deserialization")]
|
||||
[SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "DTO for JSON deserialization")]
|
||||
public List<T>? Data { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a paginated response from the Play v3 API.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of data items.</typeparam>
|
||||
public class PlayV3Response<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the data container.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public PlayV3DataContainer<T>? Data { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a show from the Play v3 API.
|
||||
/// </summary>
|
||||
public class PlayV3Show
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the show ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the show URN.
|
||||
/// </summary>
|
||||
[JsonPropertyName("urn")]
|
||||
public string? Urn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the show title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("imageUrl")]
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transmission type (TV or Radio).
|
||||
/// </summary>
|
||||
[JsonPropertyName("transmission")]
|
||||
public string? Transmission { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vendor/business unit.
|
||||
/// </summary>
|
||||
[JsonPropertyName("vendor")]
|
||||
public string? Vendor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lead description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lead")]
|
||||
public string? Lead { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the full description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the poster image URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("posterImageUrl")]
|
||||
public string? PosterImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of episodes.
|
||||
/// </summary>
|
||||
[JsonPropertyName("numberOfEpisodes")]
|
||||
public int NumberOfEpisodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of topic IDs.
|
||||
/// </summary>
|
||||
[JsonPropertyName("topicList")]
|
||||
[SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Required for JSON deserialization")]
|
||||
[SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "DTO for JSON deserialization")]
|
||||
public List<string>? TopicList { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a topic/category from the Play v3 API.
|
||||
/// </summary>
|
||||
public class PlayV3Topic
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the topic ID (UUID).
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the topic title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the topic description/lead.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lead")]
|
||||
public string? Lead { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transmission type (TV or Radio).
|
||||
/// </summary>
|
||||
[JsonPropertyName("transmission")]
|
||||
public string? Transmission { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a video/episode from the Play v3 API.
|
||||
/// </summary>
|
||||
public class PlayV3Video
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the video ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video URN.
|
||||
/// </summary>
|
||||
[JsonPropertyName("urn")]
|
||||
public string? Urn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the episode ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("episodeId")]
|
||||
public string? EpisodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lead description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lead")]
|
||||
public string? Lead { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the full description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("imageUrl")]
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration in milliseconds.
|
||||
/// </summary>
|
||||
[JsonPropertyName("duration")]
|
||||
public long Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the publication date.
|
||||
/// </summary>
|
||||
[JsonPropertyName("date")]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the valid from date.
|
||||
/// </summary>
|
||||
[JsonPropertyName("validFrom")]
|
||||
public DateTime? ValidFrom { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the valid to date (expiration).
|
||||
/// </summary>
|
||||
[JsonPropertyName("validTo")]
|
||||
public DateTime? ValidTo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the media type (VIDEO or AUDIO).
|
||||
/// </summary>
|
||||
[JsonPropertyName("mediaType")]
|
||||
public string? MediaType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether subtitles are available.
|
||||
/// </summary>
|
||||
[JsonPropertyName("subtitlesAvailable")]
|
||||
public bool SubtitlesAvailable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the content is playable abroad.
|
||||
/// </summary>
|
||||
[JsonPropertyName("playableAbroad")]
|
||||
public bool PlayableAbroad { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of possible block reasons.
|
||||
/// </summary>
|
||||
[JsonPropertyName("possibleBlockReasons")]
|
||||
[SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Required for JSON deserialization")]
|
||||
[SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "DTO for JSON deserialization")]
|
||||
public List<string>? PossibleBlockReasons { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the content type (EPISODE, CLIP, etc.).
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the associated show information.
|
||||
/// </summary>
|
||||
[JsonPropertyName("show")]
|
||||
public PlayV3Show? Show { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a streaming resource (URL) in the SRF API response.
|
||||
/// </summary>
|
||||
public class Resource
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the stream URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the streaming protocol (e.g., HLS, DASH).
|
||||
/// </summary>
|
||||
[JsonPropertyName("protocol")]
|
||||
public string? Protocol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the quality level (e.g., HD, SD).
|
||||
/// </summary>
|
||||
[JsonPropertyName("quality")]
|
||||
public string? Quality { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the streaming method.
|
||||
/// </summary>
|
||||
[JsonPropertyName("streaming")]
|
||||
public string? Streaming { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MIME type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("mimeType")]
|
||||
public string? MimeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the encoding.
|
||||
/// </summary>
|
||||
[JsonPropertyName("encoding")]
|
||||
public string? Encoding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether DRM is required.
|
||||
/// </summary>
|
||||
[JsonPropertyName("drmList")]
|
||||
public object? DrmList { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a show/program in the SRF API response.
|
||||
/// </summary>
|
||||
public class Show
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URN.
|
||||
/// </summary>
|
||||
[JsonPropertyName("urn")]
|
||||
public string? Urn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lead/description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lead")]
|
||||
public string? Lead { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("imageUrl")]
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the banner image URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("bannerImageUrl")]
|
||||
public string? BannerImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vendor (business unit).
|
||||
/// </summary>
|
||||
[JsonPropertyName("vendor")]
|
||||
public string? Vendor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transmission type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("transmission")]
|
||||
public string? Transmission { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.SRFPlay.Api.Models;
|
||||
using Jellyfin.Plugin.SRFPlay.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.SRFPlay.Api;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for interacting with the SRF Integration Layer API.
|
||||
/// </summary>
|
||||
public class SRFApiClient : IDisposable
|
||||
{
|
||||
private const string BaseUrl = "https://il.srgssr.ch/integrationlayer/2.0";
|
||||
private const string PlayV3BaseUrlTemplate = "https://www.{0}.ch/play/v3/api/{0}/production/";
|
||||
private static readonly System.Text.CompositeFormat PlayV3UrlFormat = System.Text.CompositeFormat.Parse(PlayV3BaseUrlTemplate);
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly HttpClient _playV3HttpClient;
|
||||
private readonly ILogger _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SRFApiClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public SRFApiClient(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<SRFApiClient>();
|
||||
_configuration = Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||
|
||||
// Log proxy configuration status
|
||||
if (_configuration.UseProxy && !string.IsNullOrWhiteSpace(_configuration.ProxyAddress))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"SRFApiClient initializing with proxy enabled: {ProxyAddress}",
|
||||
_configuration.ProxyAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("SRFApiClient initializing without proxy");
|
||||
}
|
||||
|
||||
_httpClient = CreateHttpClient(BaseUrl);
|
||||
_playV3HttpClient = CreateHttpClient(null);
|
||||
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an HttpClient with optional proxy configuration.
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">The base address for the HTTP client.</param>
|
||||
/// <returns>Configured HttpClient.</returns>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5399:HttpClient is created without enabling CheckCertificateRevocationList", Justification = "CRL check disabled for proxy compatibility")]
|
||||
private HttpClient CreateHttpClient(string? baseAddress)
|
||||
{
|
||||
HttpClientHandler handler = new HttpClientHandler
|
||||
{
|
||||
CheckCertificateRevocationList = false, // Disable CRL check for proxy compatibility
|
||||
AutomaticDecompression = System.Net.DecompressionMethods.All
|
||||
};
|
||||
|
||||
// Configure proxy if enabled
|
||||
if (_configuration.UseProxy && !string.IsNullOrWhiteSpace(_configuration.ProxyAddress))
|
||||
{
|
||||
try
|
||||
{
|
||||
var proxy = new WebProxy(_configuration.ProxyAddress);
|
||||
|
||||
// Add credentials if provided
|
||||
if (!string.IsNullOrWhiteSpace(_configuration.ProxyUsername))
|
||||
{
|
||||
proxy.Credentials = new NetworkCredential(
|
||||
_configuration.ProxyUsername,
|
||||
_configuration.ProxyPassword);
|
||||
}
|
||||
|
||||
handler.Proxy = proxy;
|
||||
handler.UseProxy = true;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Proxy configured: {ProxyAddress} (Authentication: {HasAuth})",
|
||||
_configuration.ProxyAddress,
|
||||
!string.IsNullOrWhiteSpace(_configuration.ProxyUsername));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to configure proxy: {ProxyAddress}", _configuration.ProxyAddress);
|
||||
handler.UseProxy = false;
|
||||
}
|
||||
}
|
||||
|
||||
var client = new HttpClient(handler)
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(30),
|
||||
DefaultRequestVersion = HttpVersion.Version11, // Force HTTP/1.1
|
||||
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact // Ensure HTTP/1.1 is used, not upgraded
|
||||
};
|
||||
|
||||
// Add browser headers - required by SRF API
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd("*/*");
|
||||
client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-US,en;q=0.9");
|
||||
|
||||
_logger.LogInformation(
|
||||
"HttpClient created with HTTP/1.1 and headers - User-Agent: {UserAgent}, Accept: {Accept}, Accept-Language: {AcceptLanguage}",
|
||||
client.DefaultRequestHeaders.UserAgent.ToString(),
|
||||
client.DefaultRequestHeaders.Accept.ToString(),
|
||||
client.DefaultRequestHeaders.AcceptLanguage.ToString());
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(baseAddress))
|
||||
{
|
||||
client.BaseAddress = new Uri(baseAddress);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets media composition by URN using curl as a fallback.
|
||||
/// </summary>
|
||||
/// <param name="urn">The URN of the content.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The media composition.</returns>
|
||||
public async Task<MediaComposition?> GetMediaCompositionByUrnAsync(string urn, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"/mediaComposition/byUrn/{urn}.json";
|
||||
var fullUrl = $"{BaseUrl}{url}";
|
||||
_logger.LogInformation("Fetching media composition for URN: {Urn} from {Url}", urn, fullUrl);
|
||||
|
||||
var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Log response headers to diagnose geo-blocking
|
||||
var xLocation = response.Headers.Contains("x-location")
|
||||
? string.Join(", ", response.Headers.GetValues("x-location"))
|
||||
: "not present";
|
||||
|
||||
_logger.LogInformation(
|
||||
"Media composition response for URN {Urn}: StatusCode={StatusCode}, x-location={XLocation}",
|
||||
urn,
|
||||
response.StatusCode,
|
||||
xLocation);
|
||||
|
||||
// If HttpClient fails, try curl as fallback
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("HttpClient failed with {StatusCode}, trying curl fallback", response.StatusCode);
|
||||
return await FetchWithCurlAsync(fullUrl, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
var result = JsonSerializer.Deserialize<MediaComposition>(content, _jsonOptions);
|
||||
|
||||
if (result?.ChapterList != null && result.ChapterList.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Successfully fetched media composition for URN: {Urn} - Chapters: {ChapterCount}",
|
||||
urn,
|
||||
result.ChapterList.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Media composition for URN {Urn} has no chapters", urn);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"HTTP error fetching media composition for URN: {Urn} - StatusCode: {StatusCode}, trying curl fallback",
|
||||
urn,
|
||||
ex.StatusCode);
|
||||
|
||||
var fullUrl = $"{BaseUrl}/mediaComposition/byUrn/{urn}.json";
|
||||
return await FetchWithCurlAsync(fullUrl, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching media composition for URN: {Urn}", urn);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches content using curl command as a fallback.
|
||||
/// </summary>
|
||||
/// <param name="url">The full URL to fetch.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The deserialized media composition.</returns>
|
||||
private async Task<MediaComposition?> FetchWithCurlAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Using curl to fetch: {Url}", url);
|
||||
|
||||
var curlArgs = $"-s -H \"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\"";
|
||||
|
||||
// Add proxy if configured
|
||||
if (_configuration.UseProxy && !string.IsNullOrWhiteSpace(_configuration.ProxyAddress))
|
||||
{
|
||||
curlArgs += $" -x {_configuration.ProxyAddress}";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_configuration.ProxyUsername))
|
||||
{
|
||||
curlArgs += $" -U {_configuration.ProxyUsername}:{_configuration.ProxyPassword}";
|
||||
}
|
||||
|
||||
_logger.LogInformation("curl using proxy: {ProxyAddress}", _configuration.ProxyAddress);
|
||||
}
|
||||
|
||||
curlArgs += $" \"{url}\"";
|
||||
|
||||
var processStartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "curl",
|
||||
Arguments = curlArgs,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = new System.Diagnostics.Process { StartInfo = processStartInfo };
|
||||
process.Start();
|
||||
|
||||
var output = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
var error = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
_logger.LogError("curl failed with exit code {ExitCode}: {Error}", process.ExitCode, error);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
_logger.LogWarning("curl returned empty response");
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogInformation("curl succeeded, response length: {Length}", output.Length);
|
||||
|
||||
var result = JsonSerializer.Deserialize<MediaComposition>(output, _jsonOptions);
|
||||
|
||||
if (result?.ChapterList != null && result.ChapterList.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Successfully fetched media composition via curl - Chapters: {ChapterCount}", result.ChapterList.Count);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error using curl to fetch URL: {Url}", url);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest videos for a business unit.
|
||||
/// </summary>
|
||||
/// <param name="businessUnit">The business unit (e.g., srf, rts).</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The media composition containing latest videos.</returns>
|
||||
public async Task<MediaComposition?> GetLatestVideosAsync(string businessUnit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"/video/{businessUnit}/latest.json";
|
||||
_logger.LogInformation("Fetching latest videos for business unit: {BusinessUnit} from URL: {Url}", businessUnit, url);
|
||||
|
||||
var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Latest videos API response: {StatusCode}", response.StatusCode);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
||||
return null;
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogDebug("Latest videos response length: {Length}", content.Length);
|
||||
|
||||
var result = JsonSerializer.Deserialize<MediaComposition>(content, _jsonOptions);
|
||||
|
||||
_logger.LogInformation("Successfully fetched latest videos for business unit: {BusinessUnit}", businessUnit);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching latest videos for business unit: {BusinessUnit}", businessUnit);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the trending videos for a business unit.
|
||||
/// </summary>
|
||||
/// <param name="businessUnit">The business unit (e.g., srf, rts).</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The media composition containing trending videos.</returns>
|
||||
public async Task<MediaComposition?> GetTrendingVideosAsync(string businessUnit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"/video/{businessUnit}/trending.json";
|
||||
_logger.LogInformation("Fetching trending videos for business unit: {BusinessUnit} from URL: {Url}", businessUnit, url);
|
||||
|
||||
var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Trending videos API response: {StatusCode}", response.StatusCode);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
||||
return null;
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogDebug("Trending videos response length: {Length}", content.Length);
|
||||
|
||||
var result = JsonSerializer.Deserialize<MediaComposition>(content, _jsonOptions);
|
||||
|
||||
_logger.LogInformation("Successfully fetched trending videos for business unit: {BusinessUnit}", businessUnit);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching trending videos for business unit: {BusinessUnit}", businessUnit);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets raw JSON response from a URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The relative URL.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The JSON string.</returns>
|
||||
public async Task<string?> GetJsonAsync(string url, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Fetching JSON from URL: {Url}", url);
|
||||
|
||||
var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
return content;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching JSON from URL: {Url}", url);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all shows from the Play v3 API.
|
||||
/// </summary>
|
||||
/// <param name="businessUnit">The business unit (e.g., srf, rts).</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>List of shows.</returns>
|
||||
public async Task<System.Collections.Generic.List<PlayV3Show>?> GetAllShowsAsync(string businessUnit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseUrl = string.Format(CultureInfo.InvariantCulture, PlayV3UrlFormat, businessUnit);
|
||||
var url = $"{baseUrl}shows";
|
||||
_logger.LogInformation("Fetching all shows for business unit: {BusinessUnit} from URL: {Url}", businessUnit, url);
|
||||
|
||||
var response = await _playV3HttpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
||||
return null;
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
var result = JsonSerializer.Deserialize<PlayV3DirectResponse<PlayV3Show>>(content, _jsonOptions);
|
||||
|
||||
_logger.LogInformation("Successfully fetched {Count} shows for business unit: {BusinessUnit}", result?.Data?.Count ?? 0, businessUnit);
|
||||
return result?.Data;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching shows for business unit: {BusinessUnit}", businessUnit);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all topics from the Play v3 API.
|
||||
/// </summary>
|
||||
/// <param name="businessUnit">The business unit (e.g., srf, rts).</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>List of topics.</returns>
|
||||
public async Task<System.Collections.Generic.List<PlayV3Topic>?> GetAllTopicsAsync(string businessUnit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseUrl = string.Format(CultureInfo.InvariantCulture, PlayV3UrlFormat, businessUnit);
|
||||
var url = $"{baseUrl}topics";
|
||||
_logger.LogInformation("Fetching all topics for business unit: {BusinessUnit} from URL: {Url}", businessUnit, url);
|
||||
|
||||
var response = await _playV3HttpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
||||
return null;
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
var result = JsonSerializer.Deserialize<PlayV3DirectResponse<PlayV3Topic>>(content, _jsonOptions);
|
||||
|
||||
_logger.LogInformation("Successfully fetched {Count} topics for business unit: {BusinessUnit}", result?.Data?.Count ?? 0, businessUnit);
|
||||
return result?.Data;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching topics for business unit: {BusinessUnit}", businessUnit);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets videos for a specific show from the Play v3 API.
|
||||
/// </summary>
|
||||
/// <param name="businessUnit">The business unit (e.g., srf, rts).</param>
|
||||
/// <param name="showId">The show ID.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>List of videos.</returns>
|
||||
public async Task<System.Collections.Generic.List<PlayV3Video>?> GetVideosForShowAsync(string businessUnit, string showId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseUrl = string.Format(CultureInfo.InvariantCulture, PlayV3UrlFormat, businessUnit);
|
||||
var url = $"{baseUrl}videos-by-show-id?showId={showId}";
|
||||
_logger.LogDebug("Fetching videos for show {ShowId} from business unit: {BusinessUnit}", showId, businessUnit);
|
||||
|
||||
var response = await _playV3HttpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
|
||||
return null;
|
||||
}
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
var result = JsonSerializer.Deserialize<PlayV3Response<PlayV3Video>>(content, _jsonOptions);
|
||||
|
||||
_logger.LogDebug("Successfully fetched {Count} videos for show {ShowId}", result?.Data?.Data?.Count ?? 0, showId);
|
||||
return result?.Data?.Data;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching videos for show {ShowId} from business unit: {BusinessUnit}", showId, businessUnit);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the unmanaged resources and optionally releases the managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing">True to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
_playV3HttpClient?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user