* Add select all controls to media lists
* Refine select-all helper and add coverage
* Adjust select-all button alignment
* Tighten select-all helper semantics
* Allow tests to access internal members
* Rename select-all helper and avoid shift tracking
* Simplify select-all reset helper
* Keep pager centered and move select-all right
* Add missing div
* create test project for main app; move and rename new tests
* remove core => main app reference
* cleanup unused imports
* Fix button behavior when the screen is small
* update changelog
---------
Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>
* fix nvenc playback when color metadata changes mid-stream
* update dependencies (needed to fix unit test runner)
* limit noautoscale to when it's not already present
* improve build time by only running analyzers explicitly
* don't exclude scanner from analyzers
* Revert "don't exclude scanner from analyzers"
This reverts commit d927f9850a.
* fix sed syntax for linux
@ -5,6 +5,88 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
@@ -5,6 +5,88 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Graphics Engine:
- Add `script` graphics element type
- Supported in playback troubleshooting and all scheduling types
- Supports arbitrary scripts or executables that output graphics to ETV via stdout
- Supports EPG and Media Item replacement in entire template
- EPG data is sourced from XMLTV for the current time
- EPG data can also load a configurable number of subsequent (up next) entries
- Media Item data is sourced from the currently playing media item
- All template data will also be passed as JSON to the stdin stream of the command
- Template supports:
- Script and arguments (`command` and `args`)
- Draw order (`z_index`)
- Timing (`start_seconds` and `duration_seconds`)
- Data format (`format`)
- `raw` format means full frames of BGRA data to stdout
- `packet` format means ETV graphics packets to stdout
- Add framerate template data
- `RFrameRate` - the real content framerate (or channel normalized framerate) as reported by ffmpeg, e.g. `30000/1001`
- `FrameRate` - the decimal representation of `RFrameRate`, e.g. `29.97002997`
- Add `Channel_StartTime` template data
- This indicates the time that the transcode session started for the current channel
- Add remote stream metadata
- Remote stream definitions (yaml files) can now contain `title`, `plot`, `year` and `content_rating` fields
- Remote streams can now have thumbnails (same name as yaml file but with image extension)
- This metadata will be used in generated XMLTV entries, using a template that can be customized like other media kinds
- Add `Download Media Sample` button to playback troubleshooting
- This button will extract up to 30 seconds of the media item and zip it
- Add `Target Loudness` (LUFS/LKFS) to ffmpeg profile when loudness normalization is enabled
- Default value is `-16`; some sources normalize to a quieter value, e.g. `-24`
- Add environment variables to help troubleshoot performance
- e.g. if this is set to `1000`, queries taking longer than 1 second will be logged
- `ETV_SLOW_API_MS` - milliseconds threshold for logging slow API calls (at DEBUG level)
- This is currently limited to *Jellyfin*
- `ETV_JF_PAGE_SIZE` - page size for library scan API calls to Jellyfin; default value is 10
- Add `Select All` button to media pages by @Erotemic
### Fixed
- Fix startup on systems unsupported by NvEncSharp
- Fix detection of Plex Other Video libraries using `Plex Personal Media` agent
- If the library is already detected as a Movies library in ETV, synchronization must be disabled for the library to change it to an Other Videos library
- A warning will be logged when this scenario is detected
- Graphics Engine:
- Optimize graphics engine to generate element frames in parallel and to eliminate redundant frame copies
- Match graphics engine framerate with source content (or channel normalized) framerate
- Fix loading requested number of epg entries for motion graphics elements
- Fix bug with mirror channels where seemingly random content would be played every ~40 seconds
- Fix chronological sorting for Other Videos that have release date metadata
- Fix playout sorting after using channel number editor
- VAAPI: Only include `-sei a53_cc` flags when misc packed headers are supported by the encoder
- This should fix playback in some cases, e.g. AMD VAAPI h264 encoder
- AMD VAAPI:
- work around buggy ffmpeg behavior where hevc_vaapi encoder with RadeonSI driver incorrectly outputs height of 1088 instead of 1080
- fix green padding when encoding h264 using main profile
- Automatically kill playback troubleshooting ffmpeg process if it hasn't completed after two minutes
- Fix playback of certain BT.2020 content
- Use playlist item count when using a playlist as filler (instead of a fixed count of 1 for each playlist item)
- NVIDIA:
- Fix stream failure with certain content that should decode in hardware but falls back to software
- Fix stream failure with content that changes color metadata mid-stream
- Fix stream failure when configured fallback filler collection is empty
- Fix high CPU when errors are displayed; errors will now work ahead before throttling to realtime, similar to primary content
- Fix startup error caused by duplicate smart collection names (and no longer allow duplicate smart collection names)
- Fix erroneous downgrade health check failure with some installations that use MariaDB
- Sequential schedules: fix `count` instruction validation to accept integer (constant) or string (expression)
- Fix multi-part episode grouping logic so that it does NOT require release date metadata for episodes within a single show
- When **Treat Collections As Shows** is enabled (i.e. for crossover episodes) release date metadata is required for proper grouping
### Changed
- No longer round framerate to nearest integer when normalizing framerate
- Allow playlists to have no items included in EPG
- Change how fallback filler works
- Items will no longer loop; instead, a sequence of random items will be selected from the collection
- Items may still be cut as needed
- Hardware acceleration will now be used
- Items can "work ahead" (transcode faster than realtime) when less than 3 minutes in duration
- Optimize Jellyfin database fields and indexes
- Optimize Jellyfin show library scans by only requesting `People` (actors, directors, writers) when etags don't match
- This should significantly speed up periodic library scans, particularly against Jellyfin 10.11.x
## [25.9.0] - 2025-11-29
### Added
- Show playout warnings count badge in left menu
- Graphics Engine:
- Add `MediaItem_Resolution` template data (the current `Resolution` variable is the FFmpeg Profile resolution)
@ -83,6 +165,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
@@ -83,6 +165,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add toggle to hide/show disabled channels in channel list
- Add disabled text color and `(D)` and `(H)` labels for disabled and hidden channels in channel list
- Graphics engine: fix subtitle path escaping and font loading
- Fix corrupt output (green artifacts) when decoding certain 10-bit content using AMD Polaris GPUs
- Work around sequential schedule validation limit (1000/hr by Newtonsoft.Json.Schema library)
- Playout builds now use JsonSchema.Net library which has no validation limit
- Validation tool in the UI still uses Newtonsoft.Json.Schema (with 1000/hr limit) as the error output is easier to understand
- Fix editing scripted and sequential playouts when using MySql
- Fix HLS Direct streams remaining open after client disconnect
- Always log scanner exit code when it is non-zero
### Changed
- Classic schedules: `Refresh` classic playouts from playout list; do not `Reset` them
@ -97,6 +186,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
@@ -97,6 +186,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Template groups in template list
- Block groups and blocks in template editor
- Replace template tree view with searchable table (like blocks)
- Upgrade to dotnet 10
## [25.8.0] - 2025-10-26
### Added
@ -2981,7 +3071,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
@@ -2981,7 +3071,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker.
@ -44,7 +44,7 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
@@ -44,7 +44,7 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
@ -26,6 +27,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -26,6 +27,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@ -33,12 +35,14 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -33,12 +35,14 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@ -69,9 +73,11 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -69,9 +73,11 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@ -101,6 +107,9 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -101,6 +107,9 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@ -189,6 +198,10 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -189,6 +198,10 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@ -199,6 +212,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -199,6 +212,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@ -239,6 +253,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -239,6 +253,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
musicVideoTemplate,
songTemplate,
otherVideoTemplate,
remoteStreamTemplate,
minifier,
xml,
cancellationToken);
@ -264,6 +279,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -264,6 +279,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
musicVideoTemplate,
songTemplate,
otherVideoTemplate,
remoteStreamTemplate,
minifier,
xml,
cancellationToken);
@ -287,6 +303,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -287,6 +303,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
musicVideoTemplate,
songTemplate,
otherVideoTemplate,
remoteStreamTemplate,
minifier,
xml,
cancellationToken);
@ -316,6 +333,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -316,6 +333,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
TemplatemusicVideoTemplate,
TemplatesongTemplate,
TemplateotherVideoTemplate,
TemplateremoteStreamTemplate,
XmlMinifierminifier,
XmlWriterxml,
CancellationTokencancellationToken)
@ -390,6 +408,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -390,6 +408,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
musicVideoTemplate,
songTemplate,
otherVideoTemplate,
remoteStreamTemplate,
minifier,
xml);
@ -406,6 +425,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -406,6 +425,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
TemplatemusicVideoTemplate,
TemplatesongTemplate,
TemplateotherVideoTemplate,
TemplateremoteStreamTemplate,
XmlMinifierminifier,
XmlWriterxml,
CancellationTokencancellationToken)
@ -461,6 +481,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -461,6 +481,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
musicVideoTemplate,
songTemplate,
otherVideoTemplate,
remoteStreamTemplate,
minifier,
xml);
}
@ -500,6 +521,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -500,6 +521,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
musicVideoTemplate,
songTemplate,
otherVideoTemplate,
remoteStreamTemplate,
minifier,
xml);
@ -523,6 +545,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -523,6 +545,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
TemplatemusicVideoTemplate,
TemplatesongTemplate,
TemplateotherVideoTemplate,
TemplateremoteStreamTemplate,
XmlMinifierminifier,
XmlWriterxml)
{
@ -584,6 +607,16 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -584,6 +607,16 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@ -879,6 +912,55 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -879,6 +912,55 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@ -886,7 +968,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -886,7 +968,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
"Unable to generate movie XMLTV fragment without template file {File}; please restart ErsatzTV",
@ -905,7 +987,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -905,7 +987,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
"Unable to generate episode XMLTV fragment without template file {File}; please restart ErsatzTV",
@ -924,7 +1006,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -924,7 +1006,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
"Unable to generate music video XMLTV fragment without template file {File}; please restart ErsatzTV",
@ -943,7 +1025,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -943,7 +1025,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
"Unable to generate song XMLTV fragment without template file {File}; please restart ErsatzTV",
@ -962,7 +1044,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -962,7 +1044,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
"Unable to generate other video XMLTV fragment without template file {File}; please restart ErsatzTV",
@ -974,6 +1056,25 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -974,6 +1056,25 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@ -1029,6 +1130,8 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -1029,6 +1130,8 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@ -1077,7 +1180,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@@ -1077,7 +1180,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
@ -26,11 +28,13 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
@@ -26,11 +28,13 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
@ -44,13 +48,13 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
@@ -44,13 +48,13 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
@ -34,11 +27,11 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
@@ -34,11 +27,11 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
if(!ffmpegProfile.NormalizeFramerate)
{
returnOption<int>.None;
returnOption<FrameRate>.None;
}
// TODO: expand to check everything in collection rather than what's scheduled?
_logger.LogDebug("Checking frame rates for channel {ChannelNumber}",request.ChannelNumber);
logger.LogDebug("Checking frame rates for channel {ChannelNumber}",request.ChannelNumber);
List<Playout>playouts=awaitdbContext.Playouts
.AsNoTracking()
@ -68,51 +61,53 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
@@ -68,51 +61,53 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate} instead of min value {MinFrameRate}",
request.ChannelNumber,
distinct,
result);
returnresult;
distinct.Map(fr=>fr.RFrameRate),
FrameRate.DefaultFrameRate.RFrameRate,
minFrameRate.RFrameRate);
returnFrameRate.DefaultFrameRate;
}
if(distinct.Count!=0)
{
_logger.LogInformation(
logger.LogInformation(
"All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize",
request.ChannelNumber,
distinct[0]);
distinct[0].RFrameRate);
}
else
{
_logger.LogInformation(
logger.LogInformation(
"No content on channel {ChannelNumber} has frame rate information; will not normalize",
request.ChannelNumber);
}
}
catch(Exceptionex)
{
_logger.LogWarning(
logger.LogWarning(
ex,
"Unexpected error checking frame rates on channel {ChannelNumber}",
request.ChannelNumber);
@ -120,22 +115,4 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
@@ -120,22 +115,4 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
@ -15,14 +16,17 @@ public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, E
@@ -15,14 +16,17 @@ public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, E
@ -39,7 +43,7 @@ public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, E
@@ -39,7 +43,7 @@ public partial class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, E
@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
@@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
usingErsatzTV.Infrastructure.Data;
usingErsatzTV.Infrastructure.Extensions;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.Logging;
namespaceErsatzTV.Application.Emby;
@ -21,7 +22,12 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
@@ -21,7 +22,12 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
@@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
usingErsatzTV.Infrastructure.Data;
usingErsatzTV.Infrastructure.Extensions;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.Logging;
namespaceErsatzTV.Application.Emby;
@ -22,8 +23,9 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
@@ -22,8 +23,9 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
@@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
usingErsatzTV.FFmpeg.Runtime;
usingErsatzTV.Infrastructure.Data;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.Logging;
namespaceErsatzTV.Application.Emby;
@ -19,8 +20,9 @@ public class CallEmbyShowScannerHandler : CallLibraryScannerHandler<SynchronizeE
@@ -19,8 +20,9 @@ public class CallEmbyShowScannerHandler : CallLibraryScannerHandler<SynchronizeE
@ -59,7 +59,12 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa
@@ -59,7 +59,12 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa
@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
@@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
usingErsatzTV.Infrastructure.Data;
usingErsatzTV.Infrastructure.Extensions;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.Logging;
namespaceErsatzTV.Application.Jellyfin;
@ -21,7 +22,12 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy
@@ -21,7 +22,12 @@ public class CallJellyfinCollectionScannerHandler : CallLibraryScannerHandler<Sy
@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
@@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
usingErsatzTV.Infrastructure.Data;
usingErsatzTV.Infrastructure.Extensions;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.Logging;
namespaceErsatzTV.Application.Jellyfin;
@ -22,8 +23,9 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
@@ -22,8 +23,9 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
@@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
usingErsatzTV.FFmpeg.Runtime;
usingErsatzTV.Infrastructure.Data;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.Logging;
namespaceErsatzTV.Application.Jellyfin;
@ -19,8 +20,9 @@ public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler<Synchron
@@ -19,8 +20,9 @@ public class CallJellyfinShowScannerHandler : CallLibraryScannerHandler<Synchron
@ -42,6 +45,7 @@ public abstract class CallLibraryScannerHandler<TRequest>(
@@ -42,6 +45,7 @@ public abstract class CallLibraryScannerHandler<TRequest>(
if(process.ExitCode!=0)
{
logger.LogWarning("ErsatzTV.Scanner exited with code {ExitCode}",process.ExitCode);
returnBaseError.New($"ErsatzTV.Scanner exited with code {process.ExitCode}");
}
}
@ -64,7 +68,7 @@ public abstract class CallLibraryScannerHandler<TRequest>(
@@ -64,7 +68,7 @@ public abstract class CallLibraryScannerHandler<TRequest>(
@ -71,7 +73,8 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
@@ -71,7 +73,8 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
@ -80,4 +83,23 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
@@ -80,4 +83,23 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
@ -57,14 +57,6 @@ public class GetPlaylistItemsHandler(IDbContextFactory<TvContext> dbContextFacto
@@ -57,14 +57,6 @@ public class GetPlaylistItemsHandler(IDbContextFactory<TvContext> dbContextFacto
@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
@@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
usingErsatzTV.Infrastructure.Data;
usingErsatzTV.Infrastructure.Extensions;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.Logging;
namespaceErsatzTV.Application.MediaSources;
@ -22,8 +23,9 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc
@@ -22,8 +23,9 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc
@ -44,15 +32,15 @@ public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseEr
@@ -44,15 +32,15 @@ public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseEr
@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
@@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
usingErsatzTV.Infrastructure.Data;
usingErsatzTV.Infrastructure.Extensions;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.Logging;
namespaceErsatzTV.Application.Plex;
@ -21,7 +22,12 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr
@@ -21,7 +22,12 @@ public class CallPlexCollectionScannerHandler : CallLibraryScannerHandler<Synchr
@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
@@ -9,6 +9,7 @@ using ErsatzTV.FFmpeg.Runtime;
usingErsatzTV.Infrastructure.Data;
usingErsatzTV.Infrastructure.Extensions;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.Logging;
namespaceErsatzTV.Application.Plex;
@ -22,8 +23,9 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
@@ -22,8 +23,9 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
@ -10,6 +10,7 @@ using ErsatzTV.FFmpeg.Runtime;
@@ -10,6 +10,7 @@ using ErsatzTV.FFmpeg.Runtime;
usingErsatzTV.Infrastructure.Data;
usingErsatzTV.Infrastructure.Extensions;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.Logging;
namespaceErsatzTV.Application.Plex;
@ -22,7 +23,9 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<Synchroni
@@ -22,7 +23,9 @@ public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<Synchroni
@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
@@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
usingErsatzTV.FFmpeg.Runtime;
usingErsatzTV.Infrastructure.Data;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.Logging;
namespaceErsatzTV.Application.Plex;
@ -19,8 +20,9 @@ public class CallPlexShowScannerHandler : CallLibraryScannerHandler<SynchronizeP
@@ -19,8 +20,9 @@ public class CallPlexShowScannerHandler : CallLibraryScannerHandler<SynchronizeP
@ -40,6 +43,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
@@ -40,6 +43,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
IServiceScopeFactoryserviceScopeFactory,
IMediatormediator,
IClientclient,
IFileSystemfileSystem,
ILocalFileSystemlocalFileSystem,
ILogger<StartFFmpegSessionHandler>logger,
ILogger<HlsSessionWorker>sessionWorkerLogger,
@ -54,6 +58,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
@@ -54,6 +58,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_serviceScopeFactory=serviceScopeFactory;
_mediator=mediator;
_client=client;
_fileSystem=fileSystem;
_localFileSystem=localFileSystem;
_logger=logger;
_sessionWorkerLogger=sessionWorkerLogger;
@ -78,7 +83,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
@@ -78,7 +83,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
@ -118,7 +123,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
@@ -118,7 +123,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
@ -129,6 +134,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
@@ -129,6 +134,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
@ -45,6 +45,8 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetCo
@@ -45,6 +45,8 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetCo
@ -47,7 +49,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -47,7 +49,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -64,7 +66,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -64,7 +66,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -284,9 +286,20 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -284,9 +286,20 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
boolisComplete=true;
booleffectiveRealtime=request.HlsRealtime;
// only work ahead on fallback filler up to 3 minutes in duration
// since we always transcode a full fallback filler item
// if we are working ahead, limit to 44s (multiple of segment size)
limit=TimeSpan.FromSeconds(44);
@ -319,7 +332,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -319,7 +332,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
now,
duration,
$"DEBUG_NO_SYNC:\n{Mapper.GetDisplayTitle(playoutItemWithPath.PlayoutItem.MediaItem, Option<string>.None)}\nFrom: {start} To: {finish}",
request.HlsRealtime,
effectiveRealtime,
request.PtsOffset,
channel.FFmpegProfile.VaapiDisplay,
channel.FFmpegProfile.VaapiDriver,
@ -332,8 +345,10 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -332,8 +345,10 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -442,7 +457,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -442,7 +457,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -463,7 +478,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -463,7 +478,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -480,6 +497,13 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -480,6 +497,13 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -519,7 +543,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -519,7 +543,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -541,7 +567,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -541,7 +567,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -563,7 +591,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -563,7 +591,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
finish,
true,
now.ToUnixTimeSeconds(),
Option<int>.None);
Option<int>.None,
Optional(channel.PlayoutOffset),
!request.HlsRealtime);
}
}
@ -706,8 +736,14 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -706,8 +736,14 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
collectionKey,
cancellationToken);
// TODO: shuffle? does it really matter since we loop anyway
@ -729,15 +765,14 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -729,15 +765,14 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
.Filter(ms=>ms.MediaVersionId==version.Id)
.ToListAsync(cancellationToken);
DateTimeOffsetfinish=maybeDuration.Match(
// next playout item exists
// loop until it starts
now.Add,
// no next playout item exists
// loop for 5 minutes if less than 30s, otherwise play full item
()=>version.Duration<TimeSpan.FromSeconds(30)
?now.AddMinutes(5)
:now.Add(version.Duration));
// always play min(duration to next item, version.Duration)
@ -747,7 +782,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -747,7 +782,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -766,7 +801,11 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -766,7 +801,11 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -775,7 +814,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -775,7 +814,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -848,42 +887,6 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@@ -848,42 +887,6 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
@ -47,6 +47,8 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetW
@@ -47,6 +47,8 @@ public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetW
@ -23,12 +23,12 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa
@@ -23,12 +23,12 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa
@ -133,7 +133,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
@@ -133,7 +133,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
@ -193,7 +193,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
@@ -193,7 +193,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
@ -212,7 +212,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
@@ -212,7 +212,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
// ffmpeg seems to return exit code 1 in all cases when dumping an attachment
// so ignore it and check success a different way
if(localFileSystem.FileExists(fullOutputPath))
if(fileSystem.File.Exists(fullOutputPath))
{
logger.LogDebug("Successfully extracted font {Font}",fontStream.FileName);
}
@ -300,7 +300,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
@@ -300,7 +300,7 @@ public abstract class ExtractEmbeddedSubtitlesHandlerBase(ILocalFileSystem local
@ -120,7 +120,8 @@ public partial class StartTroubleshootingPlaybackHandler(
@@ -120,7 +120,8 @@ public partial class StartTroubleshootingPlaybackHandler(
[Description("The 'key' for the content that should be added")]
publicstringContent{get;set;}
publicrequiredstringContent{get;set;}
[Description("The amount of time to add using the referenced content")]
publicstringDuration{get;set;}
publicrequiredstringDuration{get;set;}
[Description(
"The 'key' for the content that should be used to fill any remaining unscheduled time. One item will be selected to be looped and trimmed to exactly fit.")]
publicstringFallback{get;set;}
publicstring?Fallback{get;set;}
[Description("Controls whether content will be trimmed to exactly fit the specified duration")]
publicboolTrim{get;set;}
@ -29,10 +29,10 @@ public record PlayoutDuration
@@ -29,10 +29,10 @@ public record PlayoutDuration
publicboolOfflineTail{get;set;}
[Description("Flags this content as filler, which influences EPG grouping")]
publicstringFillerKind{get;set;}
publicstring?FillerKind{get;set;}
[Description("Overrides the title used in the EPG")]
[Description("The 'key' for the content that should be added")]
publicstringContent{get;set;}
publicrequiredstringContent{get;set;}
[Description("The minutes interval")]
publicintMinutes{get;set;}
publicrequiredintMinutes{get;set;}
[Description(
"The 'key' for the content that should be used to fill any remaining unscheduled time. One item will be selected to be looped and trimmed to exactly fit.")]
publicstringFallback{get;set;}
publicstring?Fallback{get;set;}
[Description("Controls whether content will be trimmed to exactly fit the specified interval")]
publicboolTrim{get;set;}
@ -29,10 +29,10 @@ public record PlayoutPadToNext
@@ -29,10 +29,10 @@ public record PlayoutPadToNext
publicboolOfflineTail{get;set;}=true;
[Description("Flags this content as filler, which influences EPG grouping")]
publicstringFillerKind{get;set;}
publicstring?FillerKind{get;set;}
[Description("Overrides the title used in the EPG")]
[Description("The 'key' for the content that should be added")]
publicstringContent{get;set;}
publicrequiredstringContent{get;set;}
[Description("The time of day that content should be added until")]
publicstringWhen{get;set;}
publicrequiredstringWhen{get;set;}
[Description(
"Only used when the current playout time is already after the specified pad until time. When true, content will be scheduled until the specified time of day (the next day). When false, no content will be scheduled by this request.")]
@ -16,7 +16,7 @@ public record PlayoutPadUntil
@@ -16,7 +16,7 @@ public record PlayoutPadUntil
[Description(
"The 'key' for the content that should be used to fill any remaining unscheduled time. One item will be selected to be looped and trimmed to exactly fit.")]
publicstringFallback{get;set;}
publicstring?Fallback{get;set;}
[Description("Controls whether content will be trimmed to exactly fit until the specified time")]
publicboolTrim{get;set;}
@ -34,10 +34,10 @@ public record PlayoutPadUntil
@@ -34,10 +34,10 @@ public record PlayoutPadUntil
publicboolOfflineTail{get;set;}
[Description("Flags this content as filler, which influences EPG grouping")]
publicstringFillerKind{get;set;}
publicstring?FillerKind{get;set;}
[Description("Overrides the title used in the EPG")]
[Description("The 'key' for the content that should be added")]
publicstringContent{get;set;}
publicrequiredstringContent{get;set;}
[Description("The time content should be added until")]
publicDateTimeOffsetWhen{get;set;}
publicrequiredDateTimeOffsetWhen{get;set;}
[Description(
"The 'key' for the content that should be used to fill any remaining unscheduled time. One item will be selected to be looped and trimmed to exactly fit.")]
publicstringFallback{get;set;}
publicstring?Fallback{get;set;}
[Description("Controls whether content will be trimmed to exactly fit until the specified time")]
publicboolTrim{get;set;}
@ -30,10 +30,10 @@ public record PlayoutPadUntilExact
@@ -30,10 +30,10 @@ public record PlayoutPadUntilExact
publicboolOfflineTail{get;set;}
[Description("Flags this content as filler, which influences EPG grouping")]
publicstringFillerKind{get;set;}
publicstring?FillerKind{get;set;}
[Description("Overrides the title used in the EPG")]
@ -588,7 +588,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
@@ -588,7 +588,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
@ -596,7 +595,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
@@ -596,7 +595,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
newMockFileSystem(),
rerunHelper,
Logger);
@ -714,7 +713,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
@@ -714,7 +713,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
@ -722,7 +720,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
@@ -722,7 +720,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
televisionRepo,
artistRepo,
factory,
localFileSystem,
newMockFileSystem(),
rerunHelper,
Logger);
@ -842,7 +840,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
@@ -842,7 +840,6 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
@ -850,7 +847,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase
@@ -850,7 +847,7 @@ public class ContinuePlayoutTests : PlayoutBuilderTestBase