Clean up dead code, consolidate duplication, fix redundancies
🏗️ Build Plugin / build (push) Failing after 9s
🧪 Test Plugin / test (push) Successful in 1m28s
🚀 Release Plugin / build-and-release (push) Failing after 5s

Remove 9 dead methods, 6 unused constants, and redundant
ReaderWriterLockSlim from MetadataCache. Consolidate repeated
patterns into HasChapters, IsPlayable, and ToLowerString helpers.
Extract shared API methods in SRFApiClient. Move variant manifest
rewriting from controller to StreamProxyService. Make Auto quality
distinct from HD. Update README architecture section.
This commit is contained in:
2026-02-28 11:34:45 +01:00
parent 873e531599
commit 7c76402a4a
23 changed files with 245 additions and 625 deletions
@@ -74,7 +74,9 @@ public class Chapter
/// Gets or sets the list of available resources (streams).
/// </summary>
[JsonPropertyName("resourceList")]
public IReadOnlyList<Resource> ResourceList { get; set; } = new List<Resource>();
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Required for JSON deserialization")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA2227:Collection properties should be read only", Justification = "Required for JSON deserialization")]
public List<Resource> ResourceList { get; set; } = new List<Resource>();
/// <summary>
/// Gets or sets the episode number.
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
@@ -15,6 +14,12 @@ public class MediaComposition
[JsonPropertyName("chapterList")]
public IReadOnlyList<Chapter> ChapterList { get; set; } = new List<Chapter>();
/// <summary>
/// Gets a value indicating whether this composition has any chapters.
/// </summary>
[JsonIgnore]
public bool HasChapters => ChapterList != null && ChapterList.Count > 0;
/// <summary>
/// Gets or sets the episode information.
/// </summary>
@@ -2,6 +2,9 @@ using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.SRFPlay.Api.Models;
// NOTE: DrmList is typed as object? because the SRF API returns either null or a JSON array.
// IsPlayable checks for both null and empty array ("[]") to determine if content is DRM-free.
/// <summary>
/// Represents a streaming resource (URL) in the SRF API response.
/// </summary>
@@ -48,4 +51,10 @@ public class Resource
/// </summary>
[JsonPropertyName("drmList")]
public object? DrmList { get; set; }
/// <summary>
/// Gets a value indicating whether this resource is playable (not DRM-protected).
/// </summary>
[JsonIgnore]
public bool IsPlayable => DrmList == null || DrmList.ToString() == "[]";
}
+24 -100
View File
@@ -228,7 +228,7 @@ public class SRFApiClient : IDisposable
var result = JsonSerializer.Deserialize<MediaComposition>(output, _jsonOptions);
if (result?.ChapterList != null && result.ChapterList.Count > 0)
if (result?.HasChapters == true)
{
_logger.LogInformation("Successfully fetched media composition via curl - Chapters: {ChapterCount}", result.ChapterList.Count);
}
@@ -248,38 +248,8 @@ public class SRFApiClient : IDisposable
/// <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 ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
return null;
}
var content = await ReadAsUtf8StringAsync(response.Content, 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;
}
}
public Task<MediaComposition?> GetLatestVideosAsync(string businessUnit, CancellationToken cancellationToken = default)
=> GetMediaCompositionListAsync(businessUnit, "latest", cancellationToken);
/// <summary>
/// Gets the trending videos for a business unit.
@@ -287,16 +257,19 @@ public class SRFApiClient : IDisposable
/// <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)
public Task<MediaComposition?> GetTrendingVideosAsync(string businessUnit, CancellationToken cancellationToken = default)
=> GetMediaCompositionListAsync(businessUnit, "trending", cancellationToken);
private async Task<MediaComposition?> GetMediaCompositionListAsync(string businessUnit, string endpoint, CancellationToken cancellationToken)
{
try
{
var url = $"/video/{businessUnit}/trending.json";
_logger.LogInformation("Fetching trending videos for business unit: {BusinessUnit} from URL: {Url}", businessUnit, url);
var url = $"/video/{businessUnit}/{endpoint}.json";
_logger.LogInformation("Fetching {Endpoint} videos for business unit: {BusinessUnit} from URL: {Url}", endpoint, businessUnit, url);
var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Trending videos API response: {StatusCode}", response.StatusCode);
_logger.LogInformation("{Endpoint} videos API response: {StatusCode}", endpoint, response.StatusCode);
if (!response.IsSuccessStatusCode)
{
@@ -306,41 +279,16 @@ public class SRFApiClient : IDisposable
}
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Trending videos response length: {Length}", content.Length);
_logger.LogDebug("{Endpoint} videos response length: {Length}", endpoint, content.Length);
var result = JsonSerializer.Deserialize<MediaComposition>(content, _jsonOptions);
_logger.LogInformation("Successfully fetched trending videos for business unit: {BusinessUnit}", businessUnit);
_logger.LogInformation("Successfully fetched {Endpoint} videos for business unit: {BusinessUnit}", endpoint, 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 ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
return content;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching JSON from URL: {Url}", url);
_logger.LogError(ex, "Error fetching {Endpoint} videos for business unit: {BusinessUnit}", endpoint, businessUnit);
return null;
}
}
@@ -351,35 +299,8 @@ public class SRFApiClient : IDisposable
/// <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 ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
_logger.LogError("API returned error {StatusCode}: {Error}", response.StatusCode, errorContent);
return null;
}
var content = await ReadAsUtf8StringAsync(response.Content, 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;
}
}
public Task<System.Collections.Generic.List<PlayV3Show>?> GetAllShowsAsync(string businessUnit, CancellationToken cancellationToken = default)
=> GetPlayV3DirectListAsync<PlayV3Show>(businessUnit, "shows", cancellationToken);
/// <summary>
/// Gets all topics from the Play v3 API.
@@ -387,13 +308,16 @@ public class SRFApiClient : IDisposable
/// <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)
public Task<System.Collections.Generic.List<PlayV3Topic>?> GetAllTopicsAsync(string businessUnit, CancellationToken cancellationToken = default)
=> GetPlayV3DirectListAsync<PlayV3Topic>(businessUnit, "topics", cancellationToken);
private async Task<System.Collections.Generic.List<T>?> GetPlayV3DirectListAsync<T>(string businessUnit, string endpoint, CancellationToken cancellationToken)
{
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 url = $"{baseUrl}{endpoint}";
_logger.LogInformation("Fetching all {Endpoint} for business unit: {BusinessUnit} from URL: {Url}", endpoint, businessUnit, url);
var response = await _playV3HttpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
@@ -405,14 +329,14 @@ public class SRFApiClient : IDisposable
}
var content = await ReadAsUtf8StringAsync(response.Content, cancellationToken).ConfigureAwait(false);
var result = JsonSerializer.Deserialize<PlayV3DirectResponse<PlayV3Topic>>(content, _jsonOptions);
var result = JsonSerializer.Deserialize<PlayV3DirectResponse<T>>(content, _jsonOptions);
_logger.LogInformation("Successfully fetched {Count} topics for business unit: {BusinessUnit}", result?.Data?.Count ?? 0, businessUnit);
_logger.LogInformation("Successfully fetched {Count} {Endpoint} for business unit: {BusinessUnit}", result?.Data?.Count ?? 0, endpoint, businessUnit);
return result?.Data;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching topics for business unit: {BusinessUnit}", businessUnit);
_logger.LogError(ex, "Error fetching {Endpoint} for business unit: {BusinessUnit}", endpoint, businessUnit);
return null;
}
}