diff --git a/CHANGELOG.md b/CHANGELOG.md index ce1f5e473..ef0f16199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Fix removing Jellyfin and Emby libraries that have been deleted from the source media server - Fix `Work-Ahead HLS Segmenter Limit` setting to properly limit number of channels that can work-ahead at once +- Include base path value in generated channel playlist (M3U) and channel guide (XMLTV) links ### Added - Add audio stream selector scripts for episodes and movies diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelGuide.cs b/ErsatzTV.Application/Channels/Queries/GetChannelGuide.cs index fd5695d3c..eeadc2f11 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelGuide.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelGuide.cs @@ -2,4 +2,4 @@ namespace ErsatzTV.Application.Channels; -public record GetChannelGuide(string Scheme, string Host) : IRequest; +public record GetChannelGuide(string Scheme, string Host, string BaseUrl) : IRequest; diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs index bd1397135..356cedb5e 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs @@ -19,5 +19,11 @@ public class GetChannelGuideHandler : IRequestHandler Handle(GetChannelGuide request, CancellationToken cancellationToken) => _channelRepository.GetAllForGuide() - .Map(channels => new ChannelGuide(_recyclableMemoryStreamManager, request.Scheme, request.Host, channels)); + .Map( + channels => new ChannelGuide( + _recyclableMemoryStreamManager, + request.Scheme, + request.Host, + request.BaseUrl, + channels)); } diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs b/ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs index 9ef4c26e4..7fe529a52 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs @@ -2,4 +2,4 @@ namespace ErsatzTV.Application.Channels; -public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest; +public record GetChannelPlaylist(string Scheme, string Host, string BaseUrl, string Mode) : IRequest; diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs index 9df0c42d9..2a647df7c 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs @@ -14,7 +14,7 @@ public class GetChannelPlaylistHandler : IRequestHandler Handle(GetChannelPlaylist request, CancellationToken cancellationToken) => _channelRepository.GetAll() .Map(channels => EnsureMode(channels, request.Mode)) - .Map(channels => new ChannelPlaylist(request.Scheme, request.Host, channels)); + .Map(channels => new ChannelPlaylist(request.Scheme, request.Host, request.BaseUrl, channels)); private static List EnsureMode(IEnumerable channels, string mode) { diff --git a/ErsatzTV.Core/Iptv/ChannelGuide.cs b/ErsatzTV.Core/Iptv/ChannelGuide.cs index 516e1b703..636945fdb 100644 --- a/ErsatzTV.Core/Iptv/ChannelGuide.cs +++ b/ErsatzTV.Core/Iptv/ChannelGuide.cs @@ -13,6 +13,7 @@ public class ChannelGuide { private readonly List _channels; private readonly string _host; + private readonly string _baseUrl; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; private readonly string _scheme; @@ -20,11 +21,13 @@ public class ChannelGuide RecyclableMemoryStreamManager recyclableMemoryStreamManager, string scheme, string host, + string baseUrl, List channels) { _recyclableMemoryStreamManager = recyclableMemoryStreamManager; _scheme = scheme; _host = host; + _baseUrl = baseUrl; _channels = channels; } @@ -67,8 +70,8 @@ public class ChannelGuide .Filter(a => a.ArtworkKind == ArtworkKind.Logo) .HeadOrNone() .Match( - artwork => $"{_scheme}://{_host}/iptv/logos/{artwork.Path}.jpg", - () => $"{_scheme}://{_host}/iptv/images/ersatztv-500.png"); + artwork => $"{_scheme}://{_host}{_baseUrl}/iptv/logos/{artwork.Path}.jpg", + () => $"{_scheme}://{_host}{_baseUrl}/iptv/images/ersatztv-500.png"); xml.WriteAttributeString("src", logo); xml.WriteEndElement(); // icon @@ -394,7 +397,7 @@ public class ChannelGuide _ => "posters" }; - artworkPath = $"{_scheme}://{_host}/iptv/artwork/{artworkFolder}/{artwork.Path}.jpg"; + artworkPath = $"{_scheme}://{_host}{_baseUrl}/iptv/artwork/{artworkFolder}/{artwork.Path}.jpg"; } return artworkPath; diff --git a/ErsatzTV.Core/Iptv/ChannelPlaylist.cs b/ErsatzTV.Core/Iptv/ChannelPlaylist.cs index a68bb6272..6014e5c70 100644 --- a/ErsatzTV.Core/Iptv/ChannelPlaylist.cs +++ b/ErsatzTV.Core/Iptv/ChannelPlaylist.cs @@ -7,12 +7,14 @@ public class ChannelPlaylist { private readonly List _channels; private readonly string _host; + private readonly string _baseUrl; private readonly string _scheme; - public ChannelPlaylist(string scheme, string host, List channels) + public ChannelPlaylist(string scheme, string host, string baseUrl, List channels) { _scheme = scheme; _host = host; + _baseUrl = baseUrl; _channels = channels; } @@ -20,7 +22,7 @@ public class ChannelPlaylist { var sb = new StringBuilder(); - var xmltv = $"{_scheme}://{_host}/iptv/xmltv.xml"; + string xmltv = $"{_scheme}://{_host}{_baseUrl}/iptv/xmltv.xml"; sb.AppendLine($"#EXTM3U url-tvg=\"{xmltv}\" x-tvg-url=\"{xmltv}\""); foreach (Channel channel in _channels.OrderBy(c => decimal.Parse(c.Number))) { @@ -28,8 +30,8 @@ public class ChannelPlaylist .Filter(a => a.ArtworkKind == ArtworkKind.Logo) .HeadOrNone() .Match( - artwork => $"{_scheme}://{_host}/iptv/logos/{artwork.Path}.jpg", - () => $"{_scheme}://{_host}/iptv/images/ersatztv-500.png"); + artwork => $"{_scheme}://{_host}{_baseUrl}/iptv/logos/{artwork.Path}.jpg", + () => $"{_scheme}://{_host}{_baseUrl}/iptv/images/ersatztv-500.png"); string shortUniqueId = Convert.ToBase64String(channel.UniqueId.ToByteArray()) .TrimEnd('=') @@ -49,7 +51,7 @@ public class ChannelPlaylist sb.AppendLine( $"#EXTINF:0 tvg-id=\"{channel.Number}.etv\" channel-id=\"{shortUniqueId}\" channel-number=\"{channel.Number}\" CUID=\"{shortUniqueId}\" tvg-chno=\"{channel.Number}\" tvg-name=\"{channel.Name}\" tvg-logo=\"{logo}\" group-title=\"{channel.Group}\" tvc-stream-vcodec=\"{vcodec}\" tvc-stream-acodec=\"{acodec}\", {channel.Name}"); - sb.AppendLine($"{_scheme}://{_host}/iptv/channel/{channel.Number}.{format}"); + sb.AppendLine($"{_scheme}://{_host}{_baseUrl}/iptv/channel/{channel.Number}.{format}"); } return sb.ToString(); diff --git a/ErsatzTV/Controllers/IptvController.cs b/ErsatzTV/Controllers/IptvController.cs index c61356ec9..5b667b1f1 100644 --- a/ErsatzTV/Controllers/IptvController.cs +++ b/ErsatzTV/Controllers/IptvController.cs @@ -36,12 +36,12 @@ public class IptvController : ControllerBase public Task GetChannelPlaylist( [FromQuery] string mode = "mixed") => - _mediator.Send(new GetChannelPlaylist(Request.Scheme, Request.Host.ToString(), mode)) + _mediator.Send(new GetChannelPlaylist(Request.Scheme, Request.Host.ToString(), Request.PathBase, mode)) .Map(Ok); [HttpGet("iptv/xmltv.xml")] public Task GetGuide() => - _mediator.Send(new GetChannelGuide(Request.Scheme, Request.Host.ToString())) + _mediator.Send(new GetChannelGuide(Request.Scheme, Request.Host.ToString(), Request.PathBase)) .Map(Ok); [HttpGet("iptv/hdhr/channel/{channelNumber}.ts")] diff --git a/ErsatzTV/Services/RunOnce/EndpointValidatorService.cs b/ErsatzTV/Services/RunOnce/EndpointValidatorService.cs index 91e2cfdcc..d0fdc01bc 100644 --- a/ErsatzTV/Services/RunOnce/EndpointValidatorService.cs +++ b/ErsatzTV/Services/RunOnce/EndpointValidatorService.cs @@ -43,11 +43,13 @@ public class EndpointValidatorService : IHostedService { throw new NotSupportedException($"Invalid endpoint format: {urls}"); } + + string baseUrl = Environment.GetEnvironmentVariable("ETV_BASE_URL"); _logger.LogInformation( "Server will listen on port {Port} - try UI at {UI}", Settings.ListenPort, - $"http://localhost:{Settings.ListenPort}"); + $"http://localhost:{Settings.ListenPort}{baseUrl}"); return Task.CompletedTask; } diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 49ea3e410..f9e1fe39d 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -288,7 +288,17 @@ public class Startup string baseUrl = Environment.GetEnvironmentVariable("ETV_BASE_URL"); if (!string.IsNullOrWhiteSpace(baseUrl)) { - app.UsePathBase(baseUrl); + try + { + app.UsePathBase(baseUrl); + } + catch (Exception ex) + { + Log.Error( + ex, + "Failed to configure ETV_BASE_URL; please check syntax and include leading slash e.g. `/etv`: {BaseUrl}", + baseUrl); + } } app.UseCors("AllowAll");