Added sports livestreams

This commit is contained in:
2025-11-14 21:20:41 +01:00
parent d830f8ae5f
commit ce6c435a92
8 changed files with 476 additions and 9 deletions
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
@@ -145,6 +146,15 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
ImageUrl = null
});
items.Add(new ChannelItemInfo
{
Id = "live_sports",
Name = "Live Sports & Events",
Type = ChannelItemType.Folder,
FolderType = ChannelFolderType.Container,
ImageUrl = null
});
// Add category folders if enabled and CategoryService is available
if (config?.EnableCategoryFolders == true && _categoryService != null)
{
@@ -201,6 +211,54 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
items = await ConvertUrnsToChannelItems(urns, cancellationToken).ConfigureAwait(false);
}
// Live Sports & Events
else if (query.FolderId == "live_sports")
{
try
{
var businessUnit = config?.BusinessUnit.ToString().ToLowerInvariant() ?? "srf";
using var apiClient = new Api.SRFApiClient(_loggerFactory);
var scheduledLivestreams = await apiClient.GetScheduledLivestreamsAsync(businessUnit, "SPORT", cancellationToken).ConfigureAwait(false);
if (scheduledLivestreams != null)
{
// Filter for upcoming/current events (within next 7 days) that have URNs
var now = DateTime.UtcNow;
var weekFromNow = now.AddDays(7);
var upcomingEvents = scheduledLivestreams
.Where(p => p.Urn != null &&
!string.IsNullOrEmpty(p.Title) &&
p.ValidFrom != null &&
p.ValidFrom.Value.ToUniversalTime() <= weekFromNow)
.OrderBy(p => p.ValidFrom)
.ToList();
_logger.LogInformation("Found {Count} scheduled live sports events", upcomingEvents.Count);
var urns = upcomingEvents.Select(e => e.Urn!).ToList();
items = await ConvertUrnsToChannelItems(urns, cancellationToken).ConfigureAwait(false);
// Enhance items with scheduled time information
foreach (var item in items)
{
var matchingEvent = upcomingEvents.FirstOrDefault(e => item.ProviderIds.ContainsKey("SRF") && item.ProviderIds["SRF"] == e.Urn);
if (matchingEvent?.ValidFrom != null)
{
var eventTime = matchingEvent.ValidFrom.Value;
item.Name = $"[{eventTime:dd.MM HH:mm}] {matchingEvent.Title}";
item.PremiereDate = eventTime;
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load live sports events");
}
}
// Category folder - show videos for this category
else if (query.FolderId?.StartsWith("category_", StringComparison.Ordinal) == true)
{
@@ -316,7 +374,8 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
var enabledTopics = config?.EnabledTopics != null && config.EnabledTopics.Count > 0
? string.Join(",", config.EnabledTopics)
: "all";
return $"{config?.BusinessUnit}_{config?.EnableLatestContent}_{config?.EnableTrendingContent}_{config?.EnableCategoryFolders}_{enabledTopics}";
var date = DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
return $"{config?.BusinessUnit}_{config?.EnableLatestContent}_{config?.EnableTrendingContent}_{config?.EnableCategoryFolders}_{enabledTopics}_{date}";
}
private async Task<List<ChannelItemInfo>> ConvertUrnsToChannelItems(List<string> urns, CancellationToken cancellationToken)
@@ -373,17 +432,63 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
// Generate deterministic GUID from URN
var itemId = UrnToGuid(urn);
// Get stream URL and authenticate it
var streamUrl = _streamResolver.GetStreamUrl(chapter, config.QualityPreference);
// For scheduled livestreams that haven't started, streamUrl might be null
var isUpcomingLivestream = chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl);
if (!string.IsNullOrEmpty(streamUrl))
{
// Authenticate the stream URL (required for all SRF streams, especially livestreams)
streamUrl = await _streamResolver.GetAuthenticatedStreamUrlAsync(streamUrl, cancellationToken).ConfigureAwait(false);
}
else if (isUpcomingLivestream)
{
// Use a placeholder for upcoming events
streamUrl = "http://placeholder.local/upcoming.m3u8";
_logger.LogInformation(
"URN {Urn}: Upcoming livestream '{Title}' - stream will be available at {ValidFrom}",
urn,
chapter.Title,
chapter.ValidFrom);
}
// Build overview with event time for upcoming livestreams
var overview = chapter.Description ?? chapter.Lead;
if (isUpcomingLivestream && chapter.ValidFrom != null)
{
var eventStart = chapter.ValidFrom.Value.ToString("dd.MM.yyyy HH:mm", CultureInfo.InvariantCulture);
overview = $"[Upcoming Event - Starts at {eventStart}]\n\n{overview}";
}
// Get image URL - prefer chapter image, fall back to show image if available
var imageUrl = chapter.ImageUrl;
if (string.IsNullOrEmpty(imageUrl) && mediaComposition.Show != null)
{
imageUrl = mediaComposition.Show.ImageUrl;
_logger.LogDebug("URN {Urn}: Using show image as fallback", urn);
}
if (string.IsNullOrEmpty(imageUrl))
{
_logger.LogWarning("URN {Urn}: No image URL available for '{Title}'", urn, chapter.Title);
}
// Use ValidFrom for premiere date if this is an upcoming livestream, otherwise use Date
var premiereDate = isUpcomingLivestream ? chapter.ValidFrom?.ToUniversalTime() : chapter.Date?.ToUniversalTime();
var item = new ChannelItemInfo
{
Id = itemId,
Name = chapter.Title,
Overview = chapter.Description ?? chapter.Lead,
ImageUrl = chapter.ImageUrl,
Overview = overview,
ImageUrl = imageUrl,
Type = ChannelItemType.Media,
ContentType = ChannelMediaContentType.Episode,
MediaType = ChannelMediaType.Video,
DateCreated = chapter.Date?.ToUniversalTime(),
PremiereDate = chapter.Date?.ToUniversalTime(),
PremiereDate = premiereDate,
ProductionYear = chapter.Date?.Year,
RunTimeTicks = chapter.Duration > 0 ? TimeSpan.FromMilliseconds(chapter.Duration).Ticks : null,
ProviderIds = new Dictionary<string, string>
@@ -396,7 +501,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
{
Id = itemId,
Name = chapter.Title,
Path = _streamResolver.GetStreamUrl(chapter, config.QualityPreference),
Path = streamUrl,
Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http,
Container = "m3u8",
SupportsDirectStream = true,