Use TV guide for livestreams
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -16,8 +15,8 @@ namespace Jellyfin.Plugin.SRFPlay.Services;
|
||||
/// </summary>
|
||||
public class CategoryService : ICategoryService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger<CategoryService> _logger;
|
||||
private readonly ISRFApiClientFactory _apiClientFactory;
|
||||
private readonly TimeSpan _topicsCacheDuration = TimeSpan.FromHours(24);
|
||||
private Dictionary<string, PlayV3Topic>? _topicsCache;
|
||||
private DateTime _topicsCacheExpiry = DateTime.MinValue;
|
||||
@@ -25,19 +24,15 @@ public class CategoryService : ICategoryService
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CategoryService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public CategoryService(ILoggerFactory loggerFactory)
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="apiClientFactory">The API client factory.</param>
|
||||
public CategoryService(ILogger<CategoryService> logger, ISRFApiClientFactory apiClientFactory)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = loggerFactory.CreateLogger<CategoryService>();
|
||||
_logger = logger;
|
||||
_apiClientFactory = apiClientFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all topics for a business unit.
|
||||
/// </summary>
|
||||
/// <param name="businessUnit">The business unit.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>List of topics.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<List<PlayV3Topic>> GetTopicsAsync(string businessUnit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Return cached topics if still valid
|
||||
@@ -48,7 +43,7 @@ public class CategoryService : ICategoryService
|
||||
}
|
||||
|
||||
_logger.LogInformation("Fetching topics for business unit: {BusinessUnit}", businessUnit);
|
||||
using var apiClient = new SRFApiClient(_loggerFactory);
|
||||
using var apiClient = _apiClientFactory.CreateClient();
|
||||
var topics = await apiClient.GetAllTopicsAsync(businessUnit, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (topics != null && topics.Count > 0)
|
||||
@@ -65,13 +60,7 @@ public class CategoryService : ICategoryService
|
||||
return topics ?? new List<PlayV3Topic>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a topic by ID.
|
||||
/// </summary>
|
||||
/// <param name="topicId">The topic ID.</param>
|
||||
/// <param name="businessUnit">The business unit.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The topic, or null if not found.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<PlayV3Topic?> GetTopicByIdAsync(string topicId, string businessUnit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Ensure topics are loaded
|
||||
@@ -83,70 +72,14 @@ public class CategoryService : ICategoryService
|
||||
return _topicsCache?.GetValueOrDefault(topicId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters shows by topic ID.
|
||||
/// </summary>
|
||||
/// <param name="shows">The shows to filter.</param>
|
||||
/// <param name="topicId">The topic ID to filter by.</param>
|
||||
/// <returns>Filtered list of shows.</returns>
|
||||
public IReadOnlyList<PlayV3Show> FilterShowsByTopic(IReadOnlyList<PlayV3Show> shows, string topicId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(topicId))
|
||||
{
|
||||
return shows;
|
||||
}
|
||||
|
||||
return shows
|
||||
.Where(s => s.TopicList != null && s.TopicList.Contains(topicId))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Groups shows by their topics.
|
||||
/// </summary>
|
||||
/// <param name="shows">The shows to group.</param>
|
||||
/// <returns>Dictionary mapping topic IDs to shows.</returns>
|
||||
public IReadOnlyDictionary<string, List<PlayV3Show>> GroupShowsByTopics(IReadOnlyList<PlayV3Show> shows)
|
||||
{
|
||||
var groupedShows = new Dictionary<string, List<PlayV3Show>>();
|
||||
|
||||
foreach (var show in shows)
|
||||
{
|
||||
if (show.TopicList == null || show.TopicList.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var topicId in show.TopicList)
|
||||
{
|
||||
if (!groupedShows.TryGetValue(topicId, out var showList))
|
||||
{
|
||||
showList = new List<PlayV3Show>();
|
||||
groupedShows[topicId] = showList;
|
||||
}
|
||||
|
||||
showList.Add(show);
|
||||
}
|
||||
}
|
||||
|
||||
return groupedShows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets shows for a specific topic, sorted by number of episodes.
|
||||
/// </summary>
|
||||
/// <param name="topicId">The topic ID.</param>
|
||||
/// <param name="businessUnit">The business unit.</param>
|
||||
/// <param name="maxResults">Maximum number of results to return.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>List of shows for the topic.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<List<PlayV3Show>> GetShowsByTopicAsync(
|
||||
string topicId,
|
||||
string businessUnit,
|
||||
int maxResults = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var apiClient = new SRFApiClient(_loggerFactory);
|
||||
using var apiClient = _apiClientFactory.CreateClient();
|
||||
var allShows = await apiClient.GetAllShowsAsync(businessUnit, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (allShows == null || allShows.Count == 0)
|
||||
@@ -165,43 +98,29 @@ public class CategoryService : ICategoryService
|
||||
return filteredShows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets video count for each topic.
|
||||
/// </summary>
|
||||
/// <param name="shows">The shows to analyze.</param>
|
||||
/// <returns>Dictionary mapping topic IDs to video counts.</returns>
|
||||
public IReadOnlyDictionary<string, int> GetVideoCountByTopic(IReadOnlyList<PlayV3Show> shows)
|
||||
{
|
||||
var topicCounts = new Dictionary<string, int>();
|
||||
|
||||
foreach (var show in shows)
|
||||
{
|
||||
if (show.TopicList == null || show.TopicList.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var topicId in show.TopicList)
|
||||
{
|
||||
if (!topicCounts.TryGetValue(topicId, out var count))
|
||||
{
|
||||
count = 0;
|
||||
}
|
||||
|
||||
topicCounts[topicId] = count + show.NumberOfEpisodes;
|
||||
}
|
||||
}
|
||||
|
||||
return topicCounts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the topics cache.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public void ClearCache()
|
||||
{
|
||||
_topicsCache = null;
|
||||
_topicsCacheExpiry = DateTime.MinValue;
|
||||
_logger.LogInformation("Topics cache cleared");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters shows by topic ID.
|
||||
/// </summary>
|
||||
/// <param name="shows">The shows to filter.</param>
|
||||
/// <param name="topicId">The topic ID to filter by.</param>
|
||||
/// <returns>Filtered list of shows.</returns>
|
||||
private static IReadOnlyList<PlayV3Show> FilterShowsByTopic(IReadOnlyList<PlayV3Show> shows, string topicId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(topicId))
|
||||
{
|
||||
return shows;
|
||||
}
|
||||
|
||||
return shows
|
||||
.Where(s => s.TopicList != null && s.TopicList.Contains(topicId))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,21 +27,6 @@ public interface ICategoryService
|
||||
/// <returns>The topic, or null if not found.</returns>
|
||||
Task<PlayV3Topic?> GetTopicByIdAsync(string topicId, string businessUnit, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Filters shows by topic ID.
|
||||
/// </summary>
|
||||
/// <param name="shows">The shows to filter.</param>
|
||||
/// <param name="topicId">The topic ID to filter by.</param>
|
||||
/// <returns>Filtered list of shows.</returns>
|
||||
IReadOnlyList<PlayV3Show> FilterShowsByTopic(IReadOnlyList<PlayV3Show> shows, string topicId);
|
||||
|
||||
/// <summary>
|
||||
/// Groups shows by their topics.
|
||||
/// </summary>
|
||||
/// <param name="shows">The shows to group.</param>
|
||||
/// <returns>Dictionary mapping topic IDs to shows.</returns>
|
||||
IReadOnlyDictionary<string, List<PlayV3Show>> GroupShowsByTopics(IReadOnlyList<PlayV3Show> shows);
|
||||
|
||||
/// <summary>
|
||||
/// Gets shows for a specific topic, sorted by number of episodes.
|
||||
/// </summary>
|
||||
@@ -56,13 +41,6 @@ public interface ICategoryService
|
||||
int maxResults = 50,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets video count for each topic.
|
||||
/// </summary>
|
||||
/// <param name="shows">The shows to analyze.</param>
|
||||
/// <returns>Dictionary mapping topic IDs to video counts.</returns>
|
||||
IReadOnlyDictionary<string, int> GetVideoCountByTopic(IReadOnlyList<PlayV3Show> shows);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the topics cache.
|
||||
/// </summary>
|
||||
|
||||
@@ -73,7 +73,7 @@ public class StreamProxyService : IStreamProxyService
|
||||
|
||||
if (tokenExpiry.HasValue)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
_logger.LogDebug(
|
||||
"Registered stream for item {ItemId} (token expires at {ExpiresAt} UTC): {Url}",
|
||||
itemId,
|
||||
tokenExpiry.Value,
|
||||
@@ -81,7 +81,7 @@ public class StreamProxyService : IStreamProxyService
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Registered stream for item {ItemId}: {Url}", itemId, authenticatedUrl);
|
||||
_logger.LogDebug("Registered stream for item {ItemId}: {Url}", itemId, authenticatedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,50 @@ public class StreamProxyService : IStreamProxyService
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Live TV channelId_guid format lookup
|
||||
// For Live TV streams, itemId format is "channelId_urnGuid"
|
||||
// Try matching by suffix (urnGuid part) or prefix (channelId part)
|
||||
if (itemId.Contains('_', StringComparison.Ordinal))
|
||||
{
|
||||
var parts = itemId.Split('_', 2);
|
||||
var prefix = parts[0]; // channelId part
|
||||
var suffix = parts.Length > 1 ? parts[1] : null; // urnGuid part
|
||||
|
||||
foreach (var kvp in _streamMappings)
|
||||
{
|
||||
// Check if registered key contains the same suffix (urnGuid)
|
||||
if (suffix != null && kvp.Key.Contains(suffix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Found stream by Live TV suffix match - Requested: {RequestedId}, Registered: {RegisteredId}",
|
||||
itemId,
|
||||
kvp.Key);
|
||||
var url = await ValidateAndReturnStreamAsync(kvp.Key, kvp.Value, cancellationToken).ConfigureAwait(false);
|
||||
if (url != null)
|
||||
{
|
||||
// Also register with the requested itemId for future lookups
|
||||
_streamMappings.AddOrUpdate(itemId, kvp.Value, (key, old) => kvp.Value);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if registered key starts with the same prefix (channelId)
|
||||
if (kvp.Key.StartsWith(prefix + "_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Found stream by Live TV prefix match - Requested: {RequestedId}, Registered: {RegisteredId}",
|
||||
itemId,
|
||||
kvp.Key);
|
||||
var url = await ValidateAndReturnStreamAsync(kvp.Key, kvp.Value, cancellationToken).ConfigureAwait(false);
|
||||
if (url != null)
|
||||
{
|
||||
_streamMappings.AddOrUpdate(itemId, kvp.Value, (key, old) => kvp.Value);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: Use active stream fallbacks (helps when Jellyfin creates random transcoding session IDs)
|
||||
var activeStreams = _streamMappings.Where(kvp =>
|
||||
{
|
||||
|
||||
@@ -142,8 +142,8 @@ public class StreamUrlResolver : IStreamUrlResolver
|
||||
|
||||
if (selectedResource != null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"✅ Selected stream for chapter {ChapterId}: Quality={Quality}, Protocol={Protocol}, URL={Url}",
|
||||
_logger.LogDebug(
|
||||
"Selected stream for chapter {ChapterId}: Quality={Quality}, Protocol={Protocol}, URL={Url}",
|
||||
chapter.Id,
|
||||
selectedResource.Quality ?? "NULL",
|
||||
selectedResource.Protocol,
|
||||
|
||||
Reference in New Issue
Block a user