mixed refactor
This commit is contained in:
@@ -6,6 +6,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.SRFPlay.Constants;
|
||||
using Jellyfin.Plugin.SRFPlay.Services.Interfaces;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
@@ -26,9 +27,8 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IContentRefreshService _contentRefreshService;
|
||||
private readonly IStreamUrlResolver _streamResolver;
|
||||
private readonly IStreamProxyService _proxyService;
|
||||
private readonly IMediaSourceFactory _mediaSourceFactory;
|
||||
private readonly ICategoryService? _categoryService;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SRFPlayChannel"/> class.
|
||||
@@ -36,23 +36,20 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="contentRefreshService">The content refresh service.</param>
|
||||
/// <param name="streamResolver">The stream resolver.</param>
|
||||
/// <param name="proxyService">The stream proxy service.</param>
|
||||
/// <param name="appHost">The server application host.</param>
|
||||
/// <param name="mediaSourceFactory">The media source factory.</param>
|
||||
/// <param name="categoryService">The category service (optional).</param>
|
||||
public SRFPlayChannel(
|
||||
ILoggerFactory loggerFactory,
|
||||
IContentRefreshService contentRefreshService,
|
||||
IStreamUrlResolver streamResolver,
|
||||
IStreamProxyService proxyService,
|
||||
IServerApplicationHost appHost,
|
||||
IMediaSourceFactory mediaSourceFactory,
|
||||
ICategoryService? categoryService = null)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = loggerFactory.CreateLogger<SRFPlayChannel>();
|
||||
_contentRefreshService = contentRefreshService;
|
||||
_streamResolver = streamResolver;
|
||||
_proxyService = proxyService;
|
||||
_appHost = appHost;
|
||||
_mediaSourceFactory = mediaSourceFactory;
|
||||
_categoryService = categoryService;
|
||||
|
||||
if (_categoryService == null)
|
||||
@@ -73,7 +70,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
|
||||
public string DataVersion => "2.0"; // Back to authenticating at channel refresh with auto-refresh for fresh tokens
|
||||
|
||||
/// <inheritdoc />
|
||||
public string HomePageUrl => "https://www.srf.ch/play";
|
||||
public string HomePageUrl => ApiEndpoints.SrfPlayHomepage;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ChannelParentalRating ParentalRating => ChannelParentalRating.GeneralAudience;
|
||||
@@ -129,252 +126,261 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
|
||||
{
|
||||
_logger.LogInformation("=== GetChannelItems called! FolderId: {FolderId} ===", query.FolderId);
|
||||
|
||||
try
|
||||
{
|
||||
var items = await GetFolderItemsAsync(query.FolderId, cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("Returning {Count} channel items for folder {FolderId}", items.Count, query.FolderId);
|
||||
|
||||
return new ChannelItemResult
|
||||
{
|
||||
Items = items,
|
||||
TotalRecordCount = items.Count
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting channel items for folder {FolderId}", query.FolderId);
|
||||
return new ChannelItemResult { Items = new List<ChannelItemInfo>(), TotalRecordCount = 0 };
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<ChannelItemInfo>> GetFolderItemsAsync(string? folderId, CancellationToken cancellationToken)
|
||||
{
|
||||
// Root level - show folder list
|
||||
if (string.IsNullOrEmpty(folderId))
|
||||
{
|
||||
return await GetRootFoldersAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Handle known folder types
|
||||
return folderId switch
|
||||
{
|
||||
"latest" => await GetLatestVideosAsync(cancellationToken).ConfigureAwait(false),
|
||||
"trending" => await GetTrendingVideosAsync(cancellationToken).ConfigureAwait(false),
|
||||
"live_sports" => await GetLiveSportsAsync(cancellationToken).ConfigureAwait(false),
|
||||
_ when folderId.StartsWith("category_", StringComparison.Ordinal) => await GetCategoryVideosAsync(folderId, cancellationToken).ConfigureAwait(false),
|
||||
_ => new List<ChannelItemInfo>()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<ChannelItemInfo>> GetRootFoldersAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var items = new List<ChannelItemInfo>
|
||||
{
|
||||
CreateFolder("latest", "Latest Videos"),
|
||||
CreateFolder("trending", "Trending Videos"),
|
||||
CreateFolder("live_sports", "Live Sports & Events")
|
||||
};
|
||||
|
||||
// Add category folders if enabled
|
||||
var config = Plugin.Instance?.Configuration;
|
||||
if (config?.EnableCategoryFolders == true && _categoryService != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var businessUnit = config.BusinessUnit.ToString().ToLowerInvariant();
|
||||
var topics = await _categoryService.GetTopicsAsync(businessUnit, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var topic in topics.Where(t => !string.IsNullOrEmpty(t.Id)))
|
||||
{
|
||||
if (config.EnabledTopics != null && config.EnabledTopics.Count > 0 && !config.EnabledTopics.Contains(topic.Id!))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
items.Add(CreateFolder($"category_{topic.Id}", topic.Title ?? topic.Id!, topic.Lead));
|
||||
}
|
||||
|
||||
_logger.LogInformation("Added {Count} category folders", topics.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load category folders - continuing without categories");
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static ChannelItemInfo CreateFolder(string id, string name, string? overview = null)
|
||||
{
|
||||
return new ChannelItemInfo
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Type = ChannelItemType.Folder,
|
||||
FolderType = ChannelFolderType.Container,
|
||||
ImageUrl = null,
|
||||
Overview = overview
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<ChannelItemInfo>> GetLatestVideosAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var urns = await _contentRefreshService.RefreshLatestContentAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await ConvertUrnsToChannelItems(urns, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<List<ChannelItemInfo>> GetTrendingVideosAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var urns = await _contentRefreshService.RefreshTrendingContentAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await ConvertUrnsToChannelItems(urns, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<List<ChannelItemInfo>> GetLiveSportsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var items = new List<ChannelItemInfo>();
|
||||
var config = Plugin.Instance?.Configuration;
|
||||
|
||||
try
|
||||
{
|
||||
// Root level - show categories
|
||||
if (string.IsNullOrEmpty(query.FolderId))
|
||||
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)
|
||||
{
|
||||
items.Add(new ChannelItemInfo
|
||||
return items;
|
||||
}
|
||||
|
||||
// Filter for upcoming/current events (within next 7 days)
|
||||
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 &&
|
||||
(p.ValidTo == null || p.ValidTo.Value.ToUniversalTime() > now))
|
||||
.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)
|
||||
{
|
||||
Id = "latest",
|
||||
Name = "Latest Videos",
|
||||
Type = ChannelItemType.Folder,
|
||||
FolderType = ChannelFolderType.Container,
|
||||
ImageUrl = 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");
|
||||
}
|
||||
|
||||
items.Add(new ChannelItemInfo
|
||||
return items;
|
||||
}
|
||||
|
||||
private async Task<List<ChannelItemInfo>> GetCategoryVideosAsync(string folderId, CancellationToken cancellationToken)
|
||||
{
|
||||
var items = new List<ChannelItemInfo>();
|
||||
|
||||
if (_categoryService == null)
|
||||
{
|
||||
_logger.LogWarning("CategoryService not available - cannot display category folder");
|
||||
return items;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = Plugin.Instance?.Configuration;
|
||||
var topicId = folderId.Substring("category_".Length);
|
||||
var businessUnit = config?.BusinessUnit.ToString().ToLowerInvariant() ?? "srf";
|
||||
|
||||
var shows = await _categoryService.GetShowsByTopicAsync(topicId, businessUnit, 20, cancellationToken).ConfigureAwait(false);
|
||||
var urns = new List<string>();
|
||||
|
||||
using var apiClient = new Api.SRFApiClient(_loggerFactory);
|
||||
|
||||
foreach (var show in shows)
|
||||
{
|
||||
if (show.Id == null || cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Id = "trending",
|
||||
Name = "Trending Videos",
|
||||
Type = ChannelItemType.Folder,
|
||||
FolderType = ChannelFolderType.Container,
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
var businessUnit = config.BusinessUnit.ToString().ToLowerInvariant();
|
||||
var topics = await _categoryService.GetTopicsAsync(businessUnit, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var topic in topics.Where(t => !string.IsNullOrEmpty(t.Id)))
|
||||
{
|
||||
// Filter by enabled topics if configured
|
||||
if (config.EnabledTopics != null && config.EnabledTopics.Count > 0 && !config.EnabledTopics.Contains(topic.Id!))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
items.Add(new ChannelItemInfo
|
||||
{
|
||||
Id = $"category_{topic.Id}",
|
||||
Name = topic.Title ?? topic.Id!,
|
||||
Type = ChannelItemType.Folder,
|
||||
FolderType = ChannelFolderType.Container,
|
||||
ImageUrl = null,
|
||||
Overview = topic.Lead
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogInformation("Added {Count} category folders", topics.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load category folders - continuing without categories");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return new ChannelItemResult
|
||||
{
|
||||
Items = items,
|
||||
TotalRecordCount = items.Count
|
||||
};
|
||||
}
|
||||
|
||||
// Latest videos
|
||||
if (query.FolderId == "latest")
|
||||
{
|
||||
var urns = await _contentRefreshService.RefreshLatestContentAsync(cancellationToken).ConfigureAwait(false);
|
||||
items = await ConvertUrnsToChannelItems(urns, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Trending videos
|
||||
else if (query.FolderId == "trending")
|
||||
{
|
||||
var urns = await _contentRefreshService.RefreshTrendingContentAsync(cancellationToken).ConfigureAwait(false);
|
||||
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)
|
||||
var latestUrn = await GetLatestVideoUrnForShowAsync(apiClient, businessUnit, show, topicId, cancellationToken).ConfigureAwait(false);
|
||||
if (latestUrn != 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 &&
|
||||
(p.ValidTo == null || p.ValidTo.Value.ToUniversalTime() > now))
|
||||
.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;
|
||||
}
|
||||
}
|
||||
urns.Add(latestUrn);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load live sports events");
|
||||
_logger.LogWarning(ex, "Error fetching videos for show {ShowId} in category {TopicId}", show.Id, topicId);
|
||||
}
|
||||
}
|
||||
|
||||
// Category folder - show videos for this category
|
||||
else if (query.FolderId?.StartsWith("category_", StringComparison.Ordinal) == true)
|
||||
{
|
||||
if (_categoryService == null)
|
||||
{
|
||||
_logger.LogWarning("CategoryService not available - cannot display category folder");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var topicId = query.FolderId.Substring("category_".Length);
|
||||
var businessUnit = config?.BusinessUnit.ToString().ToLowerInvariant() ?? "srf";
|
||||
|
||||
var shows = await _categoryService.GetShowsByTopicAsync(topicId, businessUnit, 20, cancellationToken).ConfigureAwait(false);
|
||||
var urns = new List<string>();
|
||||
|
||||
using var apiClient = new Api.SRFApiClient(_loggerFactory);
|
||||
|
||||
foreach (var show in shows)
|
||||
{
|
||||
if (show.Id == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var videos = await apiClient.GetVideosForShowAsync(businessUnit, show.Id, cancellationToken).ConfigureAwait(false);
|
||||
if (videos != null && videos.Count > 0)
|
||||
{
|
||||
_logger.LogDebug("Category {TopicId}, Show {Show} ({ShowId}): Found {Count} videos", topicId, show.Title, show.Id, videos.Count);
|
||||
|
||||
// Filter to videos that are actually published and not expired
|
||||
var now = DateTime.UtcNow;
|
||||
var availableVideos = videos.Where(v =>
|
||||
(v.ValidFrom == null || v.ValidFrom.Value.ToUniversalTime() <= now) &&
|
||||
(v.ValidTo == null || v.ValidTo.Value.ToUniversalTime() > now)).ToList();
|
||||
|
||||
_logger.LogDebug("Category {TopicId}, Show {Show}: {AvailableCount} available out of {TotalCount} videos", topicId, show.Title, availableVideos.Count, videos.Count);
|
||||
|
||||
if (availableVideos.Count > 0)
|
||||
{
|
||||
// Get most recent available video from this show
|
||||
var latestVideo = availableVideos.OrderByDescending(v => v.Date).FirstOrDefault();
|
||||
if (latestVideo?.Urn != null)
|
||||
{
|
||||
urns.Add(latestVideo.Urn);
|
||||
_logger.LogInformation(
|
||||
"Category {TopicId}: Added video from show {Show}: {Title} (URN: {Urn}, Date: {Date}, ValidFrom: {ValidFrom}, ValidTo: {ValidTo})",
|
||||
topicId,
|
||||
show.Title,
|
||||
latestVideo.Title,
|
||||
latestVideo.Urn,
|
||||
latestVideo.Date,
|
||||
latestVideo.ValidFrom,
|
||||
latestVideo.ValidTo);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Category {TopicId}, Show {Show}: Latest available video has null URN", topicId, show.Title);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Category {TopicId}, Show {Show}: No available videos (all expired or not yet published)", topicId, show.Title);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Category {TopicId}, Show {Show} ({ShowId}): No videos returned from API", topicId, show.Title, show.Id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error fetching videos for show {ShowId} in category {TopicId}", show.Id, topicId);
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
items = await ConvertUrnsToChannelItems(urns, cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("Found {Count} videos for category {TopicId}", items.Count, topicId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load category videos");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Returning {Count} channel items for folder {FolderId}", items.Count, query.FolderId);
|
||||
items = await ConvertUrnsToChannelItems(urns, cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("Found {Count} videos for category {TopicId}", items.Count, topicId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting channel items for folder {FolderId}", query.FolderId);
|
||||
_logger.LogError(ex, "Failed to load category videos");
|
||||
}
|
||||
|
||||
return new ChannelItemResult
|
||||
return items;
|
||||
}
|
||||
|
||||
private async Task<string?> GetLatestVideoUrnForShowAsync(
|
||||
Api.SRFApiClient apiClient,
|
||||
string businessUnit,
|
||||
Api.Models.PlayV3Show show,
|
||||
string topicId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var videos = await apiClient.GetVideosForShowAsync(businessUnit, show.Id!, cancellationToken).ConfigureAwait(false);
|
||||
if (videos == null || videos.Count == 0)
|
||||
{
|
||||
Items = items,
|
||||
TotalRecordCount = items.Count
|
||||
};
|
||||
_logger.LogDebug("Category {TopicId}, Show {Show} ({ShowId}): No videos returned from API", topicId, show.Title, show.Id);
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Category {TopicId}, Show {Show} ({ShowId}): Found {Count} videos", topicId, show.Title, show.Id, videos.Count);
|
||||
|
||||
// Filter to available videos
|
||||
var now = DateTime.UtcNow;
|
||||
var availableVideos = videos.Where(v =>
|
||||
(v.ValidFrom == null || v.ValidFrom.Value.ToUniversalTime() <= now) &&
|
||||
(v.ValidTo == null || v.ValidTo.Value.ToUniversalTime() > now)).ToList();
|
||||
|
||||
_logger.LogDebug("Category {TopicId}, Show {Show}: {AvailableCount} available out of {TotalCount} videos", topicId, show.Title, availableVideos.Count, videos.Count);
|
||||
|
||||
if (availableVideos.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("Category {TopicId}, Show {Show}: No available videos (all expired or not yet published)", topicId, show.Title);
|
||||
return null;
|
||||
}
|
||||
|
||||
var latestVideo = availableVideos.OrderByDescending(v => v.Date).FirstOrDefault();
|
||||
if (latestVideo?.Urn == null)
|
||||
{
|
||||
_logger.LogWarning("Category {TopicId}, Show {Show}: Latest available video has null URN", topicId, show.Title);
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Category {TopicId}: Added video from show {Show}: {Title} (URN: {Urn}, Date: {Date})",
|
||||
topicId,
|
||||
show.Title,
|
||||
latestVideo.Title,
|
||||
latestVideo.Urn,
|
||||
latestVideo.Date);
|
||||
|
||||
return latestVideo.Urn;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -442,49 +448,37 @@ 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);
|
||||
// Use factory to create MediaSourceInfo (handles stream URL, auth, proxy registration)
|
||||
var mediaSource = await _mediaSourceFactory.CreateMediaSourceAsync(
|
||||
chapter,
|
||||
itemId,
|
||||
urn,
|
||||
config.QualityPreference,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Skip scheduled livestreams that haven't started yet (no stream URL available)
|
||||
if (chapter.Type == "SCHEDULED_LIVESTREAM" && string.IsNullOrEmpty(streamUrl))
|
||||
// Skip items without a valid media source (no stream URL available)
|
||||
if (mediaSource == null)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"URN {Urn}: Skipping upcoming livestream '{Title}' - stream not yet available (starts at {ValidFrom})",
|
||||
urn,
|
||||
chapter.Title,
|
||||
chapter.ValidFrom);
|
||||
if (chapter.Type == "SCHEDULED_LIVESTREAM")
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"URN {Urn}: Skipping upcoming livestream '{Title}' - stream not yet available (starts at {ValidFrom})",
|
||||
urn,
|
||||
chapter.Title,
|
||||
chapter.ValidFrom);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"URN {Urn}: Skipping '{Title}' - no valid stream URL available",
|
||||
urn,
|
||||
chapter.Title);
|
||||
noStreamCount++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Authenticate the stream URL with fresh token
|
||||
if (!string.IsNullOrEmpty(streamUrl))
|
||||
{
|
||||
streamUrl = await _streamResolver.GetAuthenticatedStreamUrlAsync(streamUrl, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Skip items without a valid stream URL
|
||||
if (string.IsNullOrEmpty(streamUrl))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"URN {Urn}: Skipping '{Title}' - no valid stream URL available",
|
||||
urn,
|
||||
chapter.Title);
|
||||
noStreamCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Register stream with proxy service
|
||||
_proxyService.RegisterStream(itemId, streamUrl);
|
||||
|
||||
// Get the server URL for proxy - prefer configured public URL for remote clients
|
||||
var serverUrl = !string.IsNullOrWhiteSpace(config.PublicServerUrl)
|
||||
? config.PublicServerUrl.TrimEnd('/') // Use configured public URL (important for Android/remote clients)
|
||||
: _appHost.GetSmartApiUrl(string.Empty); // Fall back to Jellyfin's smart URL resolution
|
||||
|
||||
// Create proxy URL as absolute HTTP URL (required for ffmpeg)
|
||||
// Use the actual server URL so remote clients can access it
|
||||
var proxyUrl = $"{serverUrl}/Plugins/SRFPlay/Proxy/{itemId}/master.m3u8";
|
||||
|
||||
// Build overview
|
||||
var overview = chapter.Description ?? chapter.Lead;
|
||||
|
||||
@@ -502,7 +496,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
|
||||
}
|
||||
|
||||
// Proxy image URL to fix Content-Type headers from SRF CDN
|
||||
var imageUrl = CreateProxiedImageUrl(originalImageUrl, serverUrl);
|
||||
var imageUrl = CreateProxiedImageUrl(originalImageUrl, _mediaSourceFactory.GetServerBaseUrl());
|
||||
|
||||
// Use ValidFrom for premiere date if this is a scheduled livestream, otherwise use Date
|
||||
var premiereDate = chapter.Type == "SCHEDULED_LIVESTREAM" ? chapter.ValidFrom?.ToUniversalTime() : chapter.Date?.ToUniversalTime();
|
||||
@@ -525,46 +519,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
|
||||
{
|
||||
{ "SRF", urn }
|
||||
},
|
||||
MediaSources = new List<MediaSourceInfo>
|
||||
{
|
||||
new MediaSourceInfo
|
||||
{
|
||||
Id = itemId,
|
||||
Name = chapter.Title,
|
||||
Path = proxyUrl, // Proxy URL instead of direct Akamai URL
|
||||
Protocol = MediaBrowser.Model.MediaInfo.MediaProtocol.Http,
|
||||
Container = "hls",
|
||||
SupportsDirectStream = true,
|
||||
SupportsDirectPlay = true, // ✅ Enabled! Proxy handles auth
|
||||
SupportsTranscoding = true,
|
||||
IsRemote = false, // False because it's a local proxy endpoint
|
||||
Type = MediaBrowser.Model.Dto.MediaSourceType.Default,
|
||||
VideoType = VideoType.VideoFile,
|
||||
RequiresOpening = false,
|
||||
RequiresClosing = false,
|
||||
SupportsProbing = false, // Disable probing for proxy URLs
|
||||
ReadAtNativeFramerate = false,
|
||||
MediaStreams = new List<MediaBrowser.Model.Entities.MediaStream>
|
||||
{
|
||||
new MediaBrowser.Model.Entities.MediaStream
|
||||
{
|
||||
Type = MediaBrowser.Model.Entities.MediaStreamType.Video,
|
||||
Codec = "h264",
|
||||
Profile = "high",
|
||||
IsInterlaced = false,
|
||||
IsDefault = true,
|
||||
Index = 0
|
||||
},
|
||||
new MediaBrowser.Model.Entities.MediaStream
|
||||
{
|
||||
Type = MediaBrowser.Model.Entities.MediaStreamType.Audio,
|
||||
Codec = "aac",
|
||||
IsDefault = true,
|
||||
Index = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MediaSources = new List<MediaSourceInfo> { mediaSource }
|
||||
};
|
||||
|
||||
// Add series info if available
|
||||
@@ -576,13 +531,11 @@ public class SRFPlayChannel : IChannel, IHasCacheKey
|
||||
items.Add(item);
|
||||
successCount++;
|
||||
_logger.LogInformation("URN {Urn}: Successfully converted to channel item - {Title}", urn, chapter.Title);
|
||||
_logger.LogInformation(
|
||||
"URN {Urn}: MediaSource configured - DirectStream={DirectStream}, DirectPlay={DirectPlay}, Transcoding={Transcoding}, Container={Container}",
|
||||
_logger.LogDebug(
|
||||
"URN {Urn}: MediaSource created via factory - DirectPlay={DirectPlay}, Transcoding={Transcoding}",
|
||||
urn,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"hls");
|
||||
mediaSource.SupportsDirectPlay,
|
||||
mediaSource.SupportsTranscoding);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user