Added sports livestreams
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
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;
|
||||
@@ -9,9 +13,11 @@ namespace Jellyfin.Plugin.SRFPlay.Services;
|
||||
/// <summary>
|
||||
/// Service for resolving stream URLs from media composition resources.
|
||||
/// </summary>
|
||||
public class StreamUrlResolver
|
||||
public class StreamUrlResolver : IDisposable
|
||||
{
|
||||
private readonly ILogger<StreamUrlResolver> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StreamUrlResolver"/> class.
|
||||
@@ -20,6 +26,7 @@ public class StreamUrlResolver
|
||||
public StreamUrlResolver(ILogger<StreamUrlResolver> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClient = new HttpClient();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -137,6 +144,23 @@ public class StreamUrlResolver
|
||||
/// <returns>True if playable content is available.</returns>
|
||||
public bool HasPlayableContent(Chapter chapter)
|
||||
{
|
||||
// For scheduled livestreams that haven't started yet, resources won't exist
|
||||
// but we still want to show them. The stream will become available when the event starts.
|
||||
if (chapter?.Type == "SCHEDULED_LIVESTREAM")
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var hasStarted = chapter.ValidFrom == null || chapter.ValidFrom.Value.ToUniversalTime() <= now;
|
||||
|
||||
if (!hasStarted)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Scheduled livestream '{Title}' hasn't started yet (starts at {ValidFrom}), will be playable when live",
|
||||
chapter.Title,
|
||||
chapter.ValidFrom);
|
||||
return true; // Show it, stream will be available when event starts
|
||||
}
|
||||
}
|
||||
|
||||
if (chapter?.ResourceList == null || chapter.ResourceList.Count == 0)
|
||||
{
|
||||
return false;
|
||||
@@ -195,4 +219,87 @@ public class StreamUrlResolver
|
||||
// Return first available
|
||||
return resources.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates a stream URL by fetching an Akamai token.
|
||||
/// Based on the Kodi addon implementation.
|
||||
/// </summary>
|
||||
/// <param name="streamUrl">The unauthenticated stream URL.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The authenticated stream URL with token.</returns>
|
||||
public async Task<string> GetAuthenticatedStreamUrlAsync(string streamUrl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse the stream URL to extract path components
|
||||
var uri = new Uri(streamUrl);
|
||||
var pathSegments = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (pathSegments.Length < 2)
|
||||
{
|
||||
_logger.LogWarning("Stream URL has insufficient path segments for authentication: {Url}", streamUrl);
|
||||
return streamUrl;
|
||||
}
|
||||
|
||||
// Build ACL path: /{segment1}/{segment2}/*
|
||||
var aclPath = $"/{pathSegments[0]}/{pathSegments[1]}/*";
|
||||
var tokenUrl = $"https://tp.srgssr.ch/akahd/token?acl={Uri.EscapeDataString(aclPath)}";
|
||||
|
||||
_logger.LogDebug("Fetching auth token from: {TokenUrl}", tokenUrl);
|
||||
|
||||
var response = await _httpClient.GetAsync(tokenUrl, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var jsonContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
var tokenResponse = JsonSerializer.Deserialize<JsonElement>(jsonContent);
|
||||
|
||||
// Extract authparams from response
|
||||
if (tokenResponse.TryGetProperty("token", out var token) &&
|
||||
token.TryGetProperty("authparams", out var authParams))
|
||||
{
|
||||
var authParamsValue = authParams.GetString();
|
||||
if (!string.IsNullOrEmpty(authParamsValue))
|
||||
{
|
||||
// Append auth params to original URL
|
||||
var separator = streamUrl.Contains('?', StringComparison.Ordinal) ? "&" : "?";
|
||||
var authenticatedUrl = $"{streamUrl}{separator}{authParamsValue}";
|
||||
|
||||
_logger.LogInformation("Successfully authenticated stream URL");
|
||||
return authenticatedUrl;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogWarning("No auth params found in token response, returning original URL");
|
||||
return streamUrl;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to authenticate stream URL: {Url}", streamUrl);
|
||||
return streamUrl; // Return original URL as fallback
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user