diff --git a/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs b/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs index 4276497..5a1c3a1 100644 --- a/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs +++ b/Jellyfin.Plugin.SRFPlay/Channels/SRFPlayChannel.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Plugin.SRFPlay.Api; +using Jellyfin.Plugin.SRFPlay.Api.Models; using Jellyfin.Plugin.SRFPlay.Constants; using Jellyfin.Plugin.SRFPlay.Services.Interfaces; using Jellyfin.Plugin.SRFPlay.Utilities; @@ -15,6 +16,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Channels; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.SRFPlay.Channels; @@ -30,6 +32,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey private readonly IMediaSourceFactory _mediaSourceFactory; private readonly ICategoryService? _categoryService; private readonly ISRFApiClientFactory _apiClientFactory; + private readonly IRecordingService _recordingService; /// /// Initializes a new instance of the class. @@ -40,13 +43,15 @@ public class SRFPlayChannel : IChannel, IHasCacheKey /// The media source factory. /// The category service (optional). /// The API client factory. + /// The recording service. public SRFPlayChannel( ILoggerFactory loggerFactory, IContentRefreshService contentRefreshService, IStreamUrlResolver streamResolver, IMediaSourceFactory mediaSourceFactory, ICategoryService? categoryService, - ISRFApiClientFactory apiClientFactory) + ISRFApiClientFactory apiClientFactory, + IRecordingService recordingService) { _logger = loggerFactory.CreateLogger(); _contentRefreshService = contentRefreshService; @@ -54,6 +59,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey _mediaSourceFactory = mediaSourceFactory; _categoryService = categoryService; _apiClientFactory = apiClientFactory; + _recordingService = recordingService; if (_categoryService == null) { @@ -170,6 +176,7 @@ public class SRFPlayChannel : IChannel, IHasCacheKey "latest" => await GetLatestVideosAsync(cancellationToken).ConfigureAwait(false), "trending" => await GetTrendingVideosAsync(cancellationToken).ConfigureAwait(false), "live_sports" => await GetLiveSportsAsync(cancellationToken).ConfigureAwait(false), + "recordings" => GetRecordingItems(), _ when folderId.StartsWith("category_", StringComparison.Ordinal) => await GetCategoryVideosAsync(folderId, cancellationToken).ConfigureAwait(false), _ => new List() }; @@ -181,7 +188,8 @@ public class SRFPlayChannel : IChannel, IHasCacheKey { CreateFolder("latest", "Latest Videos"), CreateFolder("trending", "Trending Videos"), - CreateFolder("live_sports", "Live Sports & Events") + CreateFolder("live_sports", "Live Sports & Events"), + CreateFolder("recordings", "Recordings") }; // Add category folders if enabled @@ -296,6 +304,58 @@ public class SRFPlayChannel : IChannel, IHasCacheKey return items; } + private List GetRecordingItems() + { + var items = new List(); + var recordings = _recordingService.GetRecordings(RecordingState.Completed); + + foreach (var recording in recordings) + { + if (string.IsNullOrEmpty(recording.OutputPath) || !System.IO.File.Exists(recording.OutputPath)) + { + continue; + } + + var fileInfo = new System.IO.FileInfo(recording.OutputPath); + var itemId = $"recording_{recording.Id}"; + + var mediaSource = new MediaSourceInfo + { + Id = itemId, + Name = recording.Title, + Path = recording.OutputPath, + Protocol = MediaProtocol.File, + Container = "mkv", + SupportsDirectPlay = true, + SupportsDirectStream = true, + SupportsTranscoding = true, + IsRemote = false, + Size = fileInfo.Length, + Type = MediaSourceType.Default + }; + + var item = new ChannelItemInfo + { + Id = itemId, + Name = recording.Title, + Overview = recording.Description, + Type = ChannelItemType.Media, + ContentType = ChannelMediaContentType.Movie, + MediaType = ChannelMediaType.Video, + DateCreated = recording.RecordingStartedAt, + ImageUrl = !string.IsNullOrEmpty(recording.ImageUrl) + ? CreateProxiedImageUrl(recording.ImageUrl, _mediaSourceFactory.GetServerBaseUrl()) + : CreatePlaceholderImageUrl(recording.Title, _mediaSourceFactory.GetServerBaseUrl()), + MediaSources = new List { mediaSource } + }; + + items.Add(item); + } + + _logger.LogInformation("Returning {Count} completed recordings as channel items", items.Count); + return items; + } + private async Task> GetCategoryVideosAsync(string folderId, CancellationToken cancellationToken) { var items = new List(); @@ -411,7 +471,8 @@ public class SRFPlayChannel : IChannel, IHasCacheKey var timeBucket = new DateTime(now.Year, now.Month, now.Day, now.Hour, (now.Minute / 15) * 15, 0); var timeKey = timeBucket.ToString("yyyy-MM-dd-HH-mm", CultureInfo.InvariantCulture); - return $"{config?.BusinessUnit}_{config?.EnableLatestContent}_{config?.EnableTrendingContent}_{config?.EnableCategoryFolders}_{enabledTopics}_{timeKey}"; + var recordingCount = _recordingService.GetRecordings(RecordingState.Completed).Count; + return $"{config?.BusinessUnit}_{config?.EnableLatestContent}_{config?.EnableTrendingContent}_{config?.EnableCategoryFolders}_{enabledTopics}_{timeKey}_rec{recordingCount}"; } private async Task> ConvertUrnsToChannelItems(List urns, CancellationToken cancellationToken) diff --git a/Jellyfin.Plugin.SRFPlay/Configuration/recordingPage.html b/Jellyfin.Plugin.SRFPlay/Configuration/recordingPage.html new file mode 100644 index 0000000..77e68de --- /dev/null +++ b/Jellyfin.Plugin.SRFPlay/Configuration/recordingPage.html @@ -0,0 +1,344 @@ + + + + + + SRF Sport Recordings + + + + + + + + + + diff --git a/Jellyfin.Plugin.SRFPlay/Controllers/RecordingController.cs b/Jellyfin.Plugin.SRFPlay/Controllers/RecordingController.cs index d5f82f4..6c75811 100644 --- a/Jellyfin.Plugin.SRFPlay/Controllers/RecordingController.cs +++ b/Jellyfin.Plugin.SRFPlay/Controllers/RecordingController.cs @@ -1,3 +1,5 @@ +using System.IO; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Jellyfin.Plugin.SRFPlay.Api.Models; @@ -33,6 +35,26 @@ public class RecordingController : ControllerBase _recordingService = recordingService; } + /// + /// Serves the recording manager page accessible to any authenticated user. + /// + /// The recording manager HTML page. + [HttpGet("Page")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public IActionResult GetRecordingPage() + { + var assembly = Assembly.GetExecutingAssembly(); + var resourceStream = assembly.GetManifestResourceStream("Jellyfin.Plugin.SRFPlay.Configuration.recordingPage.html"); + + if (resourceStream == null) + { + return NotFound("Recording page not found"); + } + + return File(resourceStream, "text/html"); + } + /// /// Gets upcoming sport livestreams available for recording. /// diff --git a/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj b/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj index f9005cf..5c74e22 100644 --- a/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj +++ b/Jellyfin.Plugin.SRFPlay/Jellyfin.Plugin.SRFPlay.csproj @@ -26,6 +26,8 @@ + + diff --git a/Jellyfin.Plugin.SRFPlay/Plugin.cs b/Jellyfin.Plugin.SRFPlay/Plugin.cs index bc308f3..33d3e87 100644 --- a/Jellyfin.Plugin.SRFPlay/Plugin.cs +++ b/Jellyfin.Plugin.SRFPlay/Plugin.cs @@ -45,6 +45,15 @@ public class Plugin : BasePlugin, IHasWebPages { Name = Name, EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace) + }, + new PluginPageInfo + { + Name = "SRF Play Recordings", + DisplayName = "SRF Sport Recordings", + EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.recordingPage.html", GetType().Namespace), + EnableInMainMenu = true, + MenuSection = "Live TV", + MenuIcon = "fiber_smart_record" } ]; }