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; /// /// Service for resolving stream URLs from media composition resources. /// public class StreamUrlResolver { private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The logger instance. public StreamUrlResolver(ILogger logger) { _logger = logger; } /// /// Gets the best stream URL from a chapter based on quality preference. /// /// The chapter containing resources. /// The quality preference. /// The stream URL, or null if no suitable stream found. 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; } /// /// Checks if a chapter has non-DRM playable content. /// /// The chapter to check. /// True if playable content is available. 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() == "[]"); } /// /// Checks if content is expired based on ValidTo date. /// /// The chapter to check. /// True if the content is expired. 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 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 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 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(); } }