Duncan Tourolle ac6a3842dd
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
📝 Create/Update Release Draft & Release Bump PR / call (push) Failing after 0s
🧪 Test Plugin / call (push) Failing after 0s
🔬 Run CodeQL / call (push) Failing after 0s
first commit
2025-11-12 22:05:36 +01:00

199 lines
7.1 KiB
C#

using System;
using System.Linq;
using Jellyfin.Plugin.SRFPlay.Api.Models;
using Jellyfin.Plugin.SRFPlay.Configuration;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.SRFPlay.Services;
/// <summary>
/// Service for resolving stream URLs from media composition resources.
/// </summary>
public class StreamUrlResolver
{
private readonly ILogger<StreamUrlResolver> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="StreamUrlResolver"/> class.
/// </summary>
/// <param name="logger">The logger instance.</param>
public StreamUrlResolver(ILogger<StreamUrlResolver> logger)
{
_logger = logger;
}
/// <summary>
/// Gets the best stream URL from a chapter based on quality preference.
/// </summary>
/// <param name="chapter">The chapter containing resources.</param>
/// <param name="qualityPreference">The quality preference.</param>
/// <returns>The stream URL, or null if no suitable stream found.</returns>
public string? GetStreamUrl(Chapter chapter, QualityPreference qualityPreference)
{
if (chapter?.ResourceList == null || chapter.ResourceList.Count == 0)
{
_logger.LogWarning("No resources found for chapter: {ChapterId}", chapter?.Id);
return null;
}
_logger.LogInformation(
"Processing chapter {ChapterId} with {ResourceCount} resources",
chapter.Id,
chapter.ResourceList.Count);
// Filter out DRM-protected content
var nonDrmResources = chapter.ResourceList
.Where(r => r.DrmList == null || r.DrmList.ToString() == "[]")
.ToList();
_logger.LogInformation(
"Chapter {ChapterId}: Total resources={Total}, Non-DRM resources={NonDrm}",
chapter.Id,
chapter.ResourceList.Count,
nonDrmResources.Count);
if (nonDrmResources.Count == 0)
{
_logger.LogWarning("All resources for chapter {ChapterId} require DRM", chapter.Id);
// Log what DRM types are present
foreach (var resource in chapter.ResourceList)
{
_logger.LogDebug(
"DRM resource: Protocol={Protocol}, Streaming={Streaming}, DRM={Drm}",
resource.Protocol,
resource.Streaming,
resource.DrmList);
}
return null;
}
// Prefer HLS protocol
var hlsResources = nonDrmResources
.Where(r => string.Equals(r.Protocol, "HLS", StringComparison.OrdinalIgnoreCase) ||
string.Equals(r.Streaming, "HLS", StringComparison.OrdinalIgnoreCase) ||
r.Url.Contains(".m3u8", StringComparison.OrdinalIgnoreCase))
.ToList();
_logger.LogInformation(
"Chapter {ChapterId}: HLS resources found={HlsCount}",
chapter.Id,
hlsResources.Count);
if (hlsResources.Count == 0)
{
_logger.LogWarning("No HLS resources found for chapter: {ChapterId}", chapter.Id);
// Log available protocols
foreach (var resource in nonDrmResources)
{
_logger.LogDebug(
"Non-HLS resource: Protocol={Protocol}, Streaming={Streaming}, URL={Url}",
resource.Protocol,
resource.Streaming,
resource.Url);
}
// Fallback to any available non-DRM resource
var fallbackResource = nonDrmResources.FirstOrDefault();
if (fallbackResource != null)
{
_logger.LogInformation(
"Using fallback resource for chapter {ChapterId}: {Url}",
chapter.Id,
fallbackResource.Url);
}
return fallbackResource?.Url;
}
// Select based on quality preference
Resource? selectedResource = qualityPreference switch
{
QualityPreference.HD => SelectHDResource(hlsResources) ?? SelectBestAvailableResource(hlsResources),
QualityPreference.SD => SelectSDResource(hlsResources) ?? SelectBestAvailableResource(hlsResources),
QualityPreference.Auto => SelectBestAvailableResource(hlsResources),
_ => SelectBestAvailableResource(hlsResources)
};
if (selectedResource != null)
{
_logger.LogDebug(
"Selected stream for chapter {ChapterId}: Quality={Quality}, Protocol={Protocol}, URL={Url}",
chapter.Id,
selectedResource.Quality,
selectedResource.Protocol,
selectedResource.Url);
return selectedResource.Url;
}
_logger.LogWarning("Could not select appropriate stream for chapter: {ChapterId}", chapter.Id);
return null;
}
/// <summary>
/// Checks if a chapter has non-DRM playable content.
/// </summary>
/// <param name="chapter">The chapter to check.</param>
/// <returns>True if playable content is available.</returns>
public bool HasPlayableContent(Chapter chapter)
{
if (chapter?.ResourceList == null || chapter.ResourceList.Count == 0)
{
return false;
}
return chapter.ResourceList.Any(r => r.DrmList == null || r.DrmList.ToString() == "[]");
}
/// <summary>
/// Checks if content is expired based on ValidTo date.
/// </summary>
/// <param name="chapter">The chapter to check.</param>
/// <returns>True if the content is expired.</returns>
public bool IsContentExpired(Chapter chapter)
{
if (chapter?.ValidTo == null)
{
return false;
}
return DateTime.UtcNow > chapter.ValidTo.Value.ToUniversalTime();
}
private Resource? SelectHDResource(System.Collections.Generic.List<Resource> resources)
{
return resources.FirstOrDefault(r =>
string.Equals(r.Quality, "HD", StringComparison.OrdinalIgnoreCase) ||
string.Equals(r.Quality, "1080", StringComparison.OrdinalIgnoreCase) ||
string.Equals(r.Quality, "720", StringComparison.OrdinalIgnoreCase));
}
private Resource? SelectSDResource(System.Collections.Generic.List<Resource> resources)
{
return resources.FirstOrDefault(r =>
string.Equals(r.Quality, "SD", StringComparison.OrdinalIgnoreCase) ||
string.Equals(r.Quality, "480", StringComparison.OrdinalIgnoreCase) ||
string.Equals(r.Quality, "360", StringComparison.OrdinalIgnoreCase));
}
private Resource? SelectBestAvailableResource(System.Collections.Generic.List<Resource> resources)
{
// Try HD first
var hdResource = SelectHDResource(resources);
if (hdResource != null)
{
return hdResource;
}
// Fall back to SD
var sdResource = SelectSDResource(resources);
if (sdResource != null)
{
return sdResource;
}
// Return first available
return resources.FirstOrDefault();
}
}