Compare commits

...

48 Commits

Author SHA1 Message Date
Jason Dove
55903430ae update changelog for release v0.8.1-beta [no ci] 2023-08-07 13:32:07 -05:00
Jason Dove
f929dc92d1 update dependencies; code cleanup (#1357)
* update dependencies

* code cleanup
2023-08-07 09:34:25 -05:00
Jason Dove
2ad27c2be0 update dependencies (#1348)
* update dependencies

* silence mudblazor debug logs
2023-07-24 20:40:32 -05:00
Jason Dove
df2db5caf7 add plex file name logging (#1342) 2023-07-12 19:27:16 -05:00
Jason Dove
5978e8ecb1 fix vaapi rate control mode (#1340) 2023-07-08 12:36:07 -05:00
Jason Dove
a540efc2e1 add community to readme [no ci] 2023-07-03 13:07:42 -05:00
Jason Dove
1938cef6ae add community link to docs (#1339) 2023-07-03 13:01:54 -05:00
Jason Dove
b23d798aff update dependencies (#1329) 2023-06-26 11:11:40 -05:00
Jason Dove
ebad7664b0 force hw accel to use one thread (#1327) 2023-06-25 09:56:58 -05:00
Jason Dove
a9c93ff498 add custom resolution management (#1326)
* update some dependencies

* add custom resolution management
2023-06-25 09:14:19 -05:00
Jason Dove
8277894f7b show database and search index initialization in ui (#1325)
* unblock startup, show database initialization message

* wait on search index to be ready (rebuild)

* clean logging and fake delay
2023-06-24 09:12:46 -05:00
Jason Dove
0d66f752b6 add global mutex to ensure single instance (#1324) 2023-06-24 06:30:55 -05:00
Jason Dove
c128f72a54 update changelog for release v0.8.0 [no ci] 2023-06-23 22:16:26 -05:00
Jason Dove
4af2d7aa61 don't trust emby's anamorphic flag (#1321) 2023-06-22 20:07:58 -05:00
Jason Dove
20a6727158 fix vaapi hw decoding (#1320) 2023-06-22 15:05:02 -05:00
Jason Dove
52e1874426 vaapi cqp rate control mode (#1319) 2023-06-22 11:42:11 -05:00
Jason Dove
015f5e9798 fix playout build loop that was recently introduced (#1318) 2023-06-22 09:40:20 -05:00
Jason Dove
1fc461e476 update dapper (#1316) 2023-06-21 15:51:35 -05:00
Jason Dove
85792f0811 fix nvidia color normalization (#1314) 2023-06-20 09:23:41 -05:00
Jason Dove
0f91a43e3f fix scaling subtitles with nvidia accel (#1313) 2023-06-20 06:25:15 -05:00
Jason Dove
7a25996ab4 scale subtitles with all accels (#1311)
* properly scale subtitles with qsv and vaapi

* fixes
2023-06-19 15:55:23 -05:00
Jason Dove
6985826072 add mpeg-ts output format for hls direct (#1310) 2023-06-19 11:19:19 -05:00
Jason Dove
52482ef2fb only discard items with random or shuffle playback order (#1309) 2023-06-19 09:17:10 -05:00
Jason Dove
c148f2eb11 fix discard to fill calculation (#1308) 2023-06-17 05:11:08 -05:00
Jason Dove
d490cc6f4b dont give up on scheduling filler while some should fit (#1306) 2023-06-14 16:58:37 -05:00
Jason Dove
99bd827bd9 fix multi episode shuffle (#1305) 2023-06-14 16:40:18 -05:00
Jason Dove
e8cbcc935f rework pad and duration filler (#1304) 2023-06-14 15:54:41 -05:00
Jason Dove
a2acfe4d80 add finish column to playout detail table (#1302) 2023-06-13 19:09:19 -05:00
Jason Dove
5da2bdbab4 add duration discard to fill attempts (#1301) 2023-06-13 17:02:31 -05:00
Jason Dove
66607b95bb update dependencies (#1300) 2023-06-13 13:58:44 -05:00
Jason Dove
81a6251f65 properly lock playout before build (#1299) 2023-06-13 13:45:00 -05:00
Jason Dove
c554d83d60 playout management ui improvements (#1298) 2023-06-13 13:26:34 -05:00
Jason Dove
875010bbf4 update changelog for release v0.7.9-beta [no ci] 2023-06-10 10:40:05 -05:00
Jason Dove
c5692ef5f1 update dependencies (#1296) 2023-06-08 09:20:06 -05:00
Jason Dove
147ab6143d hls direct mkv container (#1292)
* use mkv container for hls direct

* add setting for mp4/mkv container with hls direct

* cleanup

* update changelog
2023-06-06 10:21:09 -05:00
Jason Dove
aca441074e subtitle improvements with hls direct (#1290)
* wip: hls direct subtitles

* convert picture subtitles with hls direct

* use mp4 for hls direct to support more codecs

* disable subtitle conversion in hls direct

* fix tests

* update changelog
2023-06-04 12:29:47 -05:00
Jason Dove
ef6adf9cbb update dependencies (#1289) 2023-06-02 06:35:31 -05:00
Jason Dove
ddb7e1887f fix nvidia h264 decoder (#1281) 2023-05-22 21:26:48 -05:00
Jason Dove
4997699b4d sync jf and emby episode actors (#1280) 2023-05-22 15:58:08 -05:00
Jason Dove
c27b906cd5 update docs (#1277)
* tweak mkdocs config; update install

* path replacement doc updates
2023-05-21 10:43:48 -05:00
Jason Dove
bec3cb864d update dependencies (#1276)
* update dependencies

* fix ide warnings

* tweak ef config
2023-05-21 10:13:44 -05:00
Jason Dove
03df2a6c8a overdue code cleanup (#1271) 2023-05-10 13:18:18 -05:00
dependabot[bot]
6142dcf153 Bump jetbrains.resharper.globaltools from 2022.1.0 to 2023.1.1 (#1264)
Bumps jetbrains.resharper.globaltools from 2022.1.0 to 2023.1.1.

---
updated-dependencies:
- dependency-name: jetbrains.resharper.globaltools
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-10 08:56:08 -05:00
Jason Dove
b287f791e6 fix pgs subtitle burn in from media servers (#1270) 2023-05-09 22:43:25 -05:00
Jason Dove
2ccba9e476 timeout playout builds after 2 minutes (#1269)
* add cancellation token support to playout builds and collection enumerators

* fix playout bug with shuffle in order

* update changelog
2023-05-08 11:53:02 -05:00
Jason Dove
e215807e56 add worker service debug logs (#1267)
* add worker service debug logs

* update mudblazor
2023-05-05 08:42:33 -05:00
Jason Dove
b0333e89cd fix fallback filler (#1265) 2023-05-03 12:08:14 -05:00
Jason Dove
bc240a40e0 fix extracting text subtitles (#1262) 2023-04-29 21:46:20 -05:00
375 changed files with 13847 additions and 3581 deletions

View File

@@ -3,16 +3,10 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2022.1.0",
"version": "2023.2.0",
"commands": [
"jb"
]
},
"swashbuckle.aspnetcore.cli": {
"version": "5.6.2",
"commands": [
"swagger"
]
}
}
}

View File

@@ -5,6 +5,71 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [0.8.1-beta] - 2023-08-07
### Added
- Add custom resolution management to `Settings` page
### Fixed
- Only allow a single instance of ErsatzTV to run
- This fixes some cases where the search index would become unusable
- Fix VAAPI rate control mode capability check
### Changed
- Rework startup process to show UI as early as possible
- A minimal UI will indicate when the database and search index are initializing
- The UI will automatically refresh when the initialization processes have completed
- Force ffmpeg to use one thread when hardware acceleration is used since hardware acceleration does not support multiple threads
## [0.8.0-beta] - 2023-06-23
### Added
- Disable playout buttons and show spinning indicator when a playout is being modified (built/extended, or subtitles are being extracted)
- Automatically reload playout details table when playout build is complete
- Add `Discard To Fill Attempts` setting to duration playout mode
- This setting only has an effect when it's configured to be greater than zero and when using `Shuffle` or `Random` playback order
- When the current item is longer than the remaining duration, it will be discarded and ETV will try to fit the next item in the collection, up to the configured number of times
- When the remaining duration is shorter than all items in the collection, the normal filler logic will be used
- Add `Finish` column to playout detail table
### Fixed
- Skip checking for subtitles to extract when subtitles are not enabled on a channel/schedule item
- Properly scale subtitles when using hardware acceleration
- Fix color normalization of content with missing color metadata when using NVIDIA acceleration
- `VAAPI`: explicitly use `CQP` rate control mode when it's the only compatible mode
- Fix scaling anamorphic Emby content that Emby claims is not anamorphic
### Changed
- `HLS Direct` streaming mode
- Use `MPEG-TS` container/output format by default to maintain v0.7.8 compatibility
- `MP4` and `MKV` container/output format can still be configured in `Settings`
- Improve `MP4` compatibility with certain content
- For `Pad` and `Duration` filler - prioritize filling the configured pad/duration
- This will skip filler that is too long in an attempt to avoid unscheduled time
- You may see the same filler more often, which means you may want to add more filler to your library so ETV has more options
- Update ffmpeg, libraries and drivers in all docker images
## [0.7.9-beta] - 2023-06-10
### Added
- Synchronize actor metadata from Jellyfin and Emby television libraries
- New libraries and new episodes will get actor data automatically
- Existing libraries can deep scan (one time) to retrieve actor data for existing episodes
- `HLS Direct` streaming mode
- Use `MP4` container/output format by default, with new global option to use `MKV` container/output format
- `MP4` output format: stream copy dvd subtitles
- `MKV` output format: stream copy any embedded subtitles
### Fixed
- Fix extracting embedded text subtitles that had been incompletely extracted in the past
- Fix fallback filler looping by forcing software mode for this content
- Other content will still use hardware acceleration as configured
- Hardware-accelerated fallback filler may be re-enabled in the future
- Fix playout building when shuffle in order is used with a single media item
- Fix pgs subtitle burn in from media server libraries
- Fix subtitle and watermark overlays with RadeonSI VAAPI driver
- Fix NVIDIA pipeline to use hardware-accelerated decoder with 8-bit h264 content
### Changed
- Timeout playout builds after 2 minutes; this should prevent playout bugs from blocking other functionality
## [0.7.8-beta] - 2023-04-29
### Added
- Add `Season, Episode` playback order
@@ -1642,7 +1707,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.8-beta...HEAD
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.8.1-beta...HEAD
[0.8.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.8.0-beta...v0.8.1-beta
[0.8.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.9-beta...v0.8.0-beta
[0.7.9-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.8-beta...v0.7.9-beta
[0.7.8-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.7-beta...v0.7.8-beta
[0.7.7-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.6-beta...v0.7.7-beta
[0.7.6-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.5-beta...v0.7.6-beta

View File

@@ -10,10 +10,7 @@ public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMed
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllArtistsHandler(IDbContextFactory<TvContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public GetAllArtistsHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<List<NamedMediaItemViewModel>> Handle(
GetAllArtists request,
@@ -24,8 +21,8 @@ public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMed
List<Artist> allArtists = await dbContext.Artists
.AsNoTracking()
.Include(a => a.ArtistMetadata)
.ToListAsync(cancellationToken: cancellationToken);
.ToListAsync(cancellationToken);
return allArtists.Bind(a => ProjectArtist(a)).ToList();
}

View File

@@ -1,19 +1,18 @@
using System.Threading;
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Channels;
public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public DeleteChannelHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel,
@@ -28,12 +27,12 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
public async Task<Either<BaseError, Unit>> Handle(DeleteChannel request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Core.Domain.Channel> validation = await ChannelMustExist(dbContext, request);
Validation<BaseError, Channel> validation = await ChannelMustExist(dbContext, request);
return await validation.Apply(c => DoDeletion(dbContext, c, cancellationToken));
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c, cancellationToken));
}
private async Task<Unit> DoDeletion(TvContext dbContext, Core.Domain.Channel channel, CancellationToken cancellationToken)
private async Task<Unit> DoDeletion(TvContext dbContext, Channel channel, CancellationToken cancellationToken)
{
dbContext.Channels.Remove(channel);
await dbContext.SaveChangesAsync();
@@ -51,9 +50,11 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
return Unit.Default;
}
private static async Task<Validation<BaseError, Core.Domain.Channel>> ChannelMustExist(TvContext dbContext, DeleteChannel deleteChannel)
private static async Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
DeleteChannel deleteChannel)
{
Option<Core.Domain.Channel> maybeChannel = await dbContext.Channels
Option<Channel> maybeChannel = await dbContext.Channels
.SelectOneAsync(c => c.Id, c => c.Id == deleteChannel.ChannelId);
return maybeChannel.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.");
}

View File

@@ -14,10 +14,10 @@ namespace ErsatzTV.Application.Channels;
public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
{
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<RefreshChannelDataHandler> _logger;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public RefreshChannelDataHandler(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
@@ -359,7 +359,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{request.ChannelNumber}.xml");
File.Move(tempFile, targetFile, true);
}
private static string GetArtworkUrl(Artwork artwork, ArtworkKind artworkKind)
{
string artworkPath = artwork.Path;
@@ -456,7 +456,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
_ => string.Empty
};
}
private Option<ContentRating> GetContentRating(PlayoutItem playoutItem)
{
try
@@ -478,7 +478,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
return None;
}
}
private static Option<ContentRating> ParseContentRating(string contentRating, string system)
{
Option<string> maybeFirst = (contentRating ?? string.Empty).Split('/').HeadOrNone();
@@ -499,8 +499,6 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
}).Flatten();
}
private record ContentRating(Option<string> System, string Value);
private string GetPrioritizedArtworkPath(Metadata metadata)
{
Option<string> maybeArtwork = Optional(metadata.Artwork).Flatten()
@@ -518,4 +516,6 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
return maybeArtwork.IfNone(string.Empty);
}
private record ContentRating(Option<string> System, string Value);
}

View File

@@ -1,4 +1,3 @@
using System.Data;
using System.Data.Common;
using System.Xml;
using Dapper;
@@ -12,9 +11,9 @@ namespace ErsatzTV.Application.Channels;
public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
{
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public RefreshChannelListHandler(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
@@ -29,7 +28,7 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
public async Task Handle(RefreshChannelList request, CancellationToken cancellationToken)
{
_localFileSystem.EnsureFolderExists(FileSystemLayout.ChannelGuideCacheFolder);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
@@ -73,7 +72,7 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
string tempFile = Path.GetTempFileName();
await File.WriteAllBytesAsync(tempFile, ms.ToArray(), cancellationToken);
string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml");
File.Move(tempFile, targetFile, true);
}
@@ -87,15 +86,18 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
order by CAST(C.Number as real)";
await using var reader = (DbDataReader)await dbContext.Connection.ExecuteReaderAsync(QUERY);
Func<IDataReader, ChannelResult> rowParser = reader.GetRowParser<ChannelResult>();
Func<DbDataReader, ChannelResult> rowParser = reader.GetRowParser<ChannelResult>();
while (await reader.ReadAsync()) {
while (await reader.ReadAsync())
{
yield return rowParser(reader);
}
while (await reader.NextResultAsync()) {}
while (await reader.NextResultAsync())
{
}
}
private static List<string> GetCategories(string categories) =>
(categories ?? string.Empty).Split(',')
.Map(s => s.Trim())

View File

@@ -31,7 +31,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
}
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)

View File

@@ -11,8 +11,8 @@ namespace ErsatzTV.Application.Channels;
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<BaseError, ChannelGuide>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private readonly ILocalFileSystem _localFileSystem;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public GetChannelGuideHandler(
IDbContextFactory<TvContext> dbContextFactory,
@@ -35,7 +35,7 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
{
return BaseError.New($"Required file {channelsFile} is missing");
}
string accessTokenUri = string.Empty;
if (!string.IsNullOrWhiteSpace(request.AccessToken))
{
@@ -43,7 +43,7 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
}
string channelsFragment = await File.ReadAllTextAsync(channelsFile, Encoding.UTF8, cancellationToken);
// TODO: is regex faster?
channelsFragment = channelsFragment
.Replace("{RequestBase}", $"{request.Scheme}://{request.Host}{request.BaseUrl}")
@@ -59,7 +59,7 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
}
string channelDataFragment = await File.ReadAllTextAsync(fileName, Encoding.UTF8, cancellationToken);
channelDataFragment = channelDataFragment
.Replace("{RequestBase}", $"{request.Scheme}://{request.Host}{request.BaseUrl}")
.Replace("{AccessTokenUri}", accessTokenUri);

View File

@@ -8,10 +8,8 @@ public class GetChannelNameByPlayoutIdHandler : IRequestHandler<GetChannelNameBy
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetChannelNameByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory)
{
public GetChannelNameByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
}
public async Task<Option<string>> Handle(GetChannelNameByPlayoutId request, CancellationToken cancellationToken)
{

View File

@@ -2,4 +2,5 @@ using ErsatzTV.Core.Iptv;
namespace ErsatzTV.Application.Channels;
public record GetChannelPlaylist(string Scheme, string Host, string BaseUrl, string Mode, string AccessToken) : IRequest<ChannelPlaylist>;
public record GetChannelPlaylist
(string Scheme, string Host, string BaseUrl, string Mode, string AccessToken) : IRequest<ChannelPlaylist>;

View File

@@ -14,7 +14,13 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => EnsureMode(channels, request.Mode))
.Map(channels => new ChannelPlaylist(request.Scheme, request.Host, request.BaseUrl, channels, request.AccessToken));
.Map(
channels => new ChannelPlaylist(
request.Scheme,
request.Host,
request.BaseUrl,
channels,
request.AccessToken));
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
{

View File

@@ -9,8 +9,6 @@ public class SaveConfigElementByKeyHandler : IRequestHandler<SaveConfigElementBy
public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
{
public async Task Handle(SaveConfigElementByKey request, CancellationToken cancellationToken) =>
await _configElementRepository.Upsert(request.Key, request.Value);
}
}

View File

@@ -22,6 +22,23 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
{
}
public async Task<Either<BaseError, Unit>>
Handle(SynchronizeEmbyCollections request, CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request);
return await validation.Match(
scanner => PerformScan(scanner, request, cancellationToken),
error =>
{
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
{
return Task.FromResult<Either<BaseError, Unit>>(scanIsNotRequired);
}
return Task.FromResult<Either<BaseError, Unit>>(error.Join());
});
}
protected override async Task<DateTimeOffset> GetLastScan(TvContext dbContext, SynchronizeEmbyCollections request)
{
DateTime minDateTime = await dbContext.EmbyMediaSources
@@ -42,26 +59,9 @@ public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<Synchr
}
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now);
return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
}
public async Task<Either<BaseError, Unit>>
Handle(SynchronizeEmbyCollections request, CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request);
return await validation.Match(
scanner => PerformScan(scanner, request, cancellationToken),
error =>
{
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
{
return Task.FromResult<Either<BaseError, Unit>>(scanIsNotRequired);
}
return Task.FromResult<Either<BaseError, Unit>>(error.Join());
});
}
private async Task<Either<BaseError, Unit>> PerformScan(
string scanner,
SynchronizeEmbyCollections request,

View File

@@ -28,9 +28,10 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
ForceSynchronizeEmbyLibraryById request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
Task<Either<BaseError, string>> IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>.Handle(
SynchronizeEmbyLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
Task<Either<BaseError, string>> IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>.
Handle(
SynchronizeEmbyLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
private async Task<Either<BaseError, string>> Handle(
ISynchronizeEmbyLibraryById request,
@@ -80,7 +81,7 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
DateTime minDateTime = await dbContext.EmbyLibraries
.SelectOneAsync(l => l.Id, l => l.Id == request.EmbyLibraryId)
.Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
return new DateTimeOffset(minDateTime, TimeSpan.Zero);
}
@@ -95,6 +96,6 @@ public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
}
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now);
return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
}
}

View File

@@ -8,8 +8,8 @@ namespace ErsatzTV.Application.Emby;
public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmbyMediaSources,
Either<BaseError, List<EmbyMediaSource>>>
{
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
public SynchronizeEmbyMediaSourcesHandler(
IMediaSourceRepository mediaSourceRepository,

View File

@@ -10,8 +10,8 @@ namespace ErsatzTV.Application.Emby;
public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnectionParameters,
Either<BaseError, EmbyConnectionParametersViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IEmbySecretStore _embySecretStore;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMemoryCache _memoryCache;
public GetEmbyConnectionParametersHandler(
@@ -65,7 +65,7 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
{

View File

@@ -8,11 +8,11 @@
<ItemGroup>
<PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="CliWrap" Version="3.6.1" />
<PackageReference Include="CliWrap" Version="3.6.4" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MediatR" Version="12.0.1" />
<PackageReference Include="MediatR" Version="12.1.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.5.22">
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.6.40">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -33,6 +33,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plex_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Cqueries/@EntryIndexedValue">True</s:Boolean>

View File

@@ -19,7 +19,7 @@ public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, E
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FFmpegProfile> validation = await FFmpegProfileMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, p => DoDeletion(dbContext, p));
return await validation.Apply(p => DoDeletion(dbContext, p));
}
private static async Task<Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)

View File

@@ -20,7 +20,7 @@ public class
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
return await LanguageExtensions.Apply(validation, p => ApplyUpdateRequest(dbContext, p, request));
}
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
@@ -36,12 +36,12 @@ public class
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
p.ResolutionId = update.ResolutionId;
p.VideoFormat = update.VideoFormat;
// mpeg2video only supports 8-bit content
p.BitDepth = update.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video
? FFmpegProfileBitDepth.EightBit
: update.BitDepth;
p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize;
p.AudioFormat = update.AudioFormat;

View File

@@ -75,6 +75,9 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegSaveReports,
request.Settings.SaveReports.ToString());
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegHlsDirectOutputFormat,
request.Settings.HlsDirectOutputFormat);
if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder))
{

View File

@@ -1,4 +1,6 @@
namespace ErsatzTV.Application.FFmpegProfiles;
using ErsatzTV.FFmpeg.OutputFormat;
namespace ErsatzTV.Application.FFmpegProfiles;
public class FFmpegSettingsViewModel
{
@@ -12,4 +14,5 @@ public class FFmpegSettingsViewModel
public int HlsSegmenterIdleTimeout { get; set; }
public int WorkAheadSegmenterLimit { get; set; }
public int InitialSegmentCount { get; set; }
public OutputFormatKind HlsDirectOutputFormat { get; set; }
}

View File

@@ -1,5 +1,4 @@
using ErsatzTV.Application.Resolutions;
using ErsatzTV.Core.Api.FFmpegProfiles;
using ErsatzTV.Core.Api.FFmpegProfiles;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.FFmpegProfiles;
@@ -15,7 +14,7 @@ internal static class Mapper
profile.VaapiDriver,
profile.VaapiDevice,
profile.QsvExtraHardwareFrames,
Project(profile.Resolution),
Resolutions.Mapper.ProjectToViewModel(profile.Resolution),
profile.VideoFormat,
profile.BitDepth,
profile.VideoBitrate,
@@ -57,7 +56,4 @@ internal static class Mapper
ffmpegProfile.AudioSampleRate,
ffmpegProfile.NormalizeFramerate,
ffmpegProfile.DeinterlaceVideo);
private static ResolutionViewModel Project(Resolution resolution) =>
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
}

View File

@@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.OutputFormat;
namespace ErsatzTV.Application.FFmpegProfiles;
@@ -32,6 +33,8 @@ public class GetFFmpegSettingsHandler : IRequestHandler<GetFFmpegSettings, FFmpe
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters);
Option<int> initialSegmentCount =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegInitialSegmentCount);
Option<OutputFormatKind> outputFormatKind =
await _configElementRepository.GetValue<OutputFormatKind>(ConfigElementKey.FFmpegHlsDirectOutputFormat);
var result = new FFmpegSettingsViewModel
{
@@ -42,7 +45,8 @@ public class GetFFmpegSettingsHandler : IRequestHandler<GetFFmpegSettings, FFmpe
PreferredAudioLanguageCode = await preferredAudioLanguageCode.IfNoneAsync("eng"),
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1)
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1),
HlsDirectOutputFormat = await outputFormatKind.IfNoneAsync(OutputFormatKind.MpegTs)
};
foreach (int watermarkId in watermark)

View File

@@ -19,7 +19,7 @@ public class DeleteFillerPresetHandler : IRequestHandler<DeleteFillerPreset, Eit
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, ps => DoDeletion(dbContext, ps));
return await validation.Apply(ps => DoDeletion(dbContext, ps));
}
private static Task<Unit> DoDeletion(TvContext dbContext, FillerPreset fillerPreset)

View File

@@ -24,13 +24,15 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
{
}
Task<Either<BaseError, string>> IRequestHandler<ForceSynchronizeJellyfinLibraryById, Either<BaseError, string>>.Handle(
ForceSynchronizeJellyfinLibraryById request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
Task<Either<BaseError, string>> IRequestHandler<ForceSynchronizeJellyfinLibraryById, Either<BaseError, string>>.
Handle(
ForceSynchronizeJellyfinLibraryById request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
Task<Either<BaseError, string>> IRequestHandler<SynchronizeJellyfinLibraryByIdIfNeeded, Either<BaseError, string>>.Handle(
SynchronizeJellyfinLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
Task<Either<BaseError, string>> IRequestHandler<SynchronizeJellyfinLibraryByIdIfNeeded, Either<BaseError, string>>.
Handle(
SynchronizeJellyfinLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
private async Task<Either<BaseError, string>> Handle(
ISynchronizeJellyfinLibraryById request,
@@ -64,7 +66,7 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
{
arguments.Add("--force");
}
if (request.DeepScan)
{
arguments.Add("--deep");
@@ -95,6 +97,6 @@ public class CallJellyfinLibraryScannerHandler : CallLibraryScannerHandler<ISync
}
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now);
return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
}
}

View File

@@ -8,8 +8,8 @@ namespace ErsatzTV.Application.Jellyfin;
public class SynchronizeJellyfinMediaSourcesHandler : IRequestHandler<SynchronizeJellyfinMediaSources,
Either<BaseError, List<JellyfinMediaSource>>>
{
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
public SynchronizeJellyfinMediaSourcesHandler(
IMediaSourceRepository mediaSourceRepository,
@@ -26,7 +26,9 @@ public class SynchronizeJellyfinMediaSourcesHandler : IRequestHandler<Synchroniz
List<JellyfinMediaSource> mediaSources = await _mediaSourceRepository.GetAllJellyfin();
foreach (JellyfinMediaSource mediaSource in mediaSources)
{
await _scannerWorkerChannel.WriteAsync(new SynchronizeJellyfinAdminUserId(mediaSource.Id), cancellationToken);
await _scannerWorkerChannel.WriteAsync(
new SynchronizeJellyfinAdminUserId(mediaSource.Id),
cancellationToken);
await _scannerWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(mediaSource.Id), cancellationToken);
}

View File

@@ -20,9 +20,9 @@ namespace ErsatzTV.Application.Libraries;
public abstract class CallLibraryScannerHandler<TRequest>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IConfigElementRepository _configElementRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _channel;
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediator _mediator;
private readonly IRuntimeInfo _runtimeInfo;
private string _libraryName;
@@ -152,9 +152,9 @@ public abstract class CallLibraryScannerHandler<TRequest>
.IfNoneAsync(0);
libraryRefreshInterval = Math.Clamp(libraryRefreshInterval, 0, 999_999);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
DateTimeOffset lastScan = await GetLastScan(dbContext, request);
if (!ScanIsRequired(lastScan, libraryRefreshInterval, request))
{
@@ -164,7 +164,7 @@ public abstract class CallLibraryScannerHandler<TRequest>
string executable = _runtimeInfo.IsOSPlatform(OSPlatform.Windows)
? "ErsatzTV.Scanner.exe"
: "ErsatzTV.Scanner";
string processFileName = Environment.ProcessPath ?? string.Empty;
if (!string.IsNullOrWhiteSpace(processFileName))
{

View File

@@ -32,7 +32,7 @@ public class CreateLocalLibraryHandler : LocalLibraryHandlerBase,
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, LocalLibrary> validation = await Validate(dbContext, request);
return await validation.Apply(localLibrary => PersistLocalLibrary(dbContext, localLibrary));
return await LanguageExtensions.Apply(validation, localLibrary => PersistLocalLibrary(dbContext, localLibrary));
}
private async Task<LocalLibraryViewModel> PersistLocalLibrary(

View File

@@ -39,7 +39,7 @@ public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath,
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await validation.Apply(parameters => MovePath(dbContext, parameters));
return await LanguageExtensions.Apply(validation, parameters => MovePath(dbContext, parameters));
}
private async Task<Unit> MovePath(TvContext dbContext, Parameters parameters)

View File

@@ -16,8 +16,8 @@ public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase,
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEntityLocker _entityLocker;
private readonly ISearchIndex _searchIndex;
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
private readonly ISearchIndex _searchIndex;
public UpdateLocalLibraryHandler(
ChannelWriter<IScannerBackgroundServiceRequest> scannerWorkerChannel,

View File

@@ -8,10 +8,8 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetExternalCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory)
{
public GetExternalCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
}
public async Task<List<LibraryViewModel>> Handle(
GetExternalCollections request,
@@ -21,7 +19,7 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
List<int> mediaSourceIds = await dbContext.EmbyMediaSources
.Filter(ems => ems.Libraries.Any(l => ((EmbyLibrary)l).ShouldSyncItems))
.Map(ems => ems.Id)
.ToListAsync(cancellationToken: cancellationToken);
.ToListAsync(cancellationToken);
return mediaSourceIds.Map(
id => new LibraryViewModel(

View File

@@ -7,7 +7,7 @@ internal partial class Mapper
{
[GeneratedRegex(@"(.*)\[(DBG|INF|WRN|ERR|FTL)\](.*)")]
private static partial Regex LogEntryRegex();
internal static Option<LogEntryViewModel> ProjectToViewModel(string line)
{
Match match = LogEntryRegex().Match(line);

View File

@@ -8,10 +8,7 @@ public class GetRecentLogEntriesHandler : IRequestHandler<GetRecentLogEntries, P
{
private readonly ILocalFileSystem _localFileSystem;
public GetRecentLogEntriesHandler(ILocalFileSystem localFileSystem)
{
_localFileSystem = localFileSystem;
}
public GetRecentLogEntriesHandler(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem;
public Task<PagedLogEntriesViewModel> Handle(
GetRecentLogEntries request,

View File

@@ -9,10 +9,8 @@ public class DeleteOrphanedSubtitlesHandler : IRequestHandler<DeleteOrphanedSubt
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteOrphanedSubtitlesHandler(IDbContextFactory<TvContext> dbContextFactory)
{
public DeleteOrphanedSubtitlesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, Unit>> Handle(
DeleteOrphanedSubtitles request,

View File

@@ -7,7 +7,7 @@ namespace ErsatzTV.Application.Maintenance;
public class ReleaseMemoryHandler : IRequestHandler<ReleaseMemory>
{
private static long _lastRelease;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly ILogger<ReleaseMemoryHandler> _logger;
@@ -31,12 +31,12 @@ public class ReleaseMemoryHandler : IRequestHandler<ReleaseMemory>
if (request.ForceAggressive || !hasActiveWorkers)
{
_logger.LogDebug("Starting aggressive garbage collection");
GC.Collect(2, GCCollectionMode.Aggressive, blocking: true, compacting: true);
GC.Collect(2, GCCollectionMode.Aggressive, true, true);
}
else
{
_logger.LogDebug("Starting garbage collection");
GC.Collect(2, GCCollectionMode.Forced, blocking: false);
GC.Collect(2, GCCollectionMode.Forced, false);
}
GC.WaitForPendingFinalizers();

View File

@@ -20,7 +20,7 @@ public class CreateCollectionHandler :
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => PersistCollection(dbContext, c));
return await validation.Apply(c => PersistCollection(dbContext, c));
}
private static async Task<MediaCollectionViewModel> PersistCollection(

View File

@@ -20,7 +20,7 @@ public class CreateMultiCollectionHandler :
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, MultiCollection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => PersistCollection(dbContext, c));
return await validation.Apply(c => PersistCollection(dbContext, c));
}
private static async Task<MultiCollectionViewModel> PersistCollection(

View File

@@ -20,7 +20,7 @@ public class CreateSmartCollectionHandler :
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, SmartCollection> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => PersistCollection(dbContext, c));
return await validation.Apply(c => PersistCollection(dbContext, c));
}
private static async Task<SmartCollectionViewModel> PersistCollection(

View File

@@ -20,7 +20,7 @@ public class DeleteCollectionHandler : IRequestHandler<DeleteCollection, Either<
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Collection> validation = await CollectionMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c));
return await validation.Apply(c => DoDeletion(dbContext, c));
}
private static Task<Unit> DoDeletion(TvContext dbContext, Collection collection)

View File

@@ -20,7 +20,7 @@ public class DeleteMultiCollectionHandler : IRequestHandler<DeleteMultiCollectio
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, MultiCollection> validation = await MultiCollectionMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c));
return await validation.Apply(c => DoDeletion(dbContext, c));
}
private static Task<Unit> DoDeletion(TvContext dbContext, MultiCollection multiCollection)

View File

@@ -20,7 +20,7 @@ public class DeleteSmartCollectionHandler : IRequestHandler<DeleteSmartCollectio
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, SmartCollection> validation = await SmartCollectionMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c));
return await validation.Apply(c => DoDeletion(dbContext, c));
}
private static Task<Unit> DoDeletion(TvContext dbContext, SmartCollection smartCollection)

View File

@@ -11,10 +11,8 @@ public class GetMediaItemInfoHandler : IRequestHandler<GetMediaItemInfo, Either<
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetMediaItemInfoHandler(IDbContextFactory<TvContext> dbContextFactory)
{
public GetMediaItemInfoHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, MediaItemInfo>> Handle(
GetMediaItemInfo request,
@@ -56,7 +54,7 @@ public class GetMediaItemInfoHandler : IRequestHandler<GetMediaItemInfo, Either<
JellyfinMediaSource jellyfinMediaSource => jellyfinMediaSource.ServerName,
_ => null
};
return new MediaItemInfo(
mediaItem.Id,
mediaItem.GetType().Name,

View File

@@ -71,7 +71,7 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc
List<LibraryPath> libraryPaths = await dbContext.LibraryPaths
.Filter(lp => lp.LibraryId == request.LibraryId)
.ToListAsync();
DateTime minDateTime = libraryPaths.Any()
? libraryPaths.Min(lp => lp.LastScan ?? SystemTime.MinValueUtc)
: SystemTime.MaxValueUtc;
@@ -90,6 +90,6 @@ public class CallLocalLibraryScannerHandler : CallLibraryScannerHandler<IScanLoc
}
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now);
return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
}
}

View File

@@ -6,6 +6,7 @@ using ErsatzTV.Application.Subtitles;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
@@ -18,21 +19,24 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
{
private readonly IClient _client;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEntityLocker _entityLocker;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IPlayoutBuilder _playoutBuilder;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public BuildPlayoutHandler(
IClient client,
IDbContextFactory<TvContext> dbContextFactory,
IPlayoutBuilder playoutBuilder,
IFFmpegSegmenterService ffmpegSegmenterService,
IEntityLocker entityLocker,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_client = client;
_dbContextFactory = dbContextFactory;
_playoutBuilder = playoutBuilder;
_ffmpegSegmenterService = ffmpegSegmenterService;
_entityLocker = entityLocker;
_workerChannel = workerChannel;
}
@@ -40,24 +44,34 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Playout> validation = await Validate(dbContext, request);
return await validation.Apply(playout => ApplyUpdateRequest(dbContext, request, playout));
return await validation.Match(
playout => ApplyUpdateRequest(dbContext, request, playout, cancellationToken),
error => Task.FromResult<Either<BaseError, Unit>>(error.Join()));
}
private async Task<Unit> ApplyUpdateRequest(TvContext dbContext, BuildPlayout request, Playout playout)
private async Task<Either<BaseError, Unit>> ApplyUpdateRequest(
TvContext dbContext,
BuildPlayout request,
Playout playout,
CancellationToken cancellationToken)
{
try
{
await _playoutBuilder.Build(playout, request.Mode);
_entityLocker.LockPlayout(playout.Id);
await _playoutBuilder.Build(playout, request.Mode, cancellationToken);
// let any active segmenter processes know that the playout has been modified
// and therefore the segmenter may need to seek into the next item instead of
// starting at the beginning (if already working ahead)
bool hasChanges = await dbContext.SaveChangesAsync() > 0;
bool hasChanges = await dbContext.SaveChangesAsync(cancellationToken) > 0;
if (request.Mode != PlayoutBuildMode.Continue && hasChanges)
{
_ffmpegSegmenterService.PlayoutUpdated(playout.Channel.Number);
}
_entityLocker.UnlockPlayout(playout.Id);
Option<string> maybeChannelNumber = await dbContext.Connection
.QuerySingleOrDefaultAsync<string>(
@"select C.Number from Channel C
@@ -71,22 +85,45 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
string fileName = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channelNumber}.xml");
if (hasChanges || !File.Exists(fileName))
{
await _workerChannel.WriteAsync(new RefreshChannelData(channelNumber));
await _workerChannel.WriteAsync(new RefreshChannelData(channelNumber), cancellationToken);
}
}
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id), cancellationToken);
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
{
_client.Notify(ex);
return BaseError.New(
$"Timeout building playout for channel {playout.Channel.Name}; this may be a bug!");
}
catch (Exception ex)
{
_client.Notify(ex);
return BaseError.New(
$"Unexpected error building playout for channel {playout.Channel.Name}: {ex.Message}");
}
return Unit.Default;
}
private static Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, BuildPlayout request) =>
PlayoutMustExist(dbContext, request);
PlayoutMustExist(dbContext, request).BindT(DiscardAttemptsMustBeValid);
private static Validation<BaseError, Playout> DiscardAttemptsMustBeValid(Playout playout)
{
foreach (ProgramScheduleItemDuration item in
playout.ProgramSchedule.Items.OfType<ProgramScheduleItemDuration>())
{
item.DiscardToFillAttempts = item.PlaybackOrder switch
{
PlaybackOrder.Random or PlaybackOrder.Shuffle => item.DiscardToFillAttempts,
_ => 0
};
}
return playout;
}
private static Task<Validation<BaseError, Playout>> PlayoutMustExist(
TvContext dbContext,
@@ -94,7 +131,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
@@ -123,12 +159,10 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(psa => psa.EnumeratorState)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.MediaItem)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Collection)
@@ -150,7 +184,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId)
.Map(o => o.ToValidation<BaseError>("Playout does not exist."));
}

View File

@@ -29,7 +29,7 @@ public class CreatePlayoutHandler : IRequestHandler<CreatePlayout, Either<BaseEr
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Playout> validation = await Validate(dbContext, request);
return await validation.Apply(playout => PersistPlayout(dbContext, playout));
return await LanguageExtensions.Apply(validation, playout => PersistPlayout(dbContext, playout));
}
private async Task<CreatePlayoutResponse> PersistPlayout(TvContext dbContext, Playout playout)

View File

@@ -11,9 +11,9 @@ namespace ErsatzTV.Application.Playouts;
public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public DeletePlayoutHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel,

View File

@@ -12,8 +12,8 @@ namespace ErsatzTV.Application.Playouts;
public class ReplacePlayoutAlternateScheduleItemsHandler :
IRequestHandler<ReplacePlayoutAlternateScheduleItems, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<ReplacePlayoutAlternateScheduleItemsHandler> _logger;
public ReplacePlayoutAlternateScheduleItemsHandler(
@@ -46,7 +46,7 @@ public class ReplacePlayoutAlternateScheduleItemsHandler :
{
var existingScheduleMap = new Dictionary<DateTimeOffset, ProgramSchedule>();
var daysToCheck = new List<DateTimeOffset>();
Option<PlayoutItem> maybeLastPlayoutItem = await dbContext.PlayoutItems
.Filter(pi => pi.PlayoutId == request.PlayoutId)
.OrderByDescending(pi => pi.Start)
@@ -146,7 +146,7 @@ public class ReplacePlayoutAlternateScheduleItemsHandler :
}
}
}
return Unit.Default;
}
catch (Exception ex)

View File

@@ -19,7 +19,7 @@ public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseEr
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Playout> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, playout => ApplyUpdateRequest(dbContext, request, playout));
return await validation.Apply(playout => ApplyUpdateRequest(dbContext, request, playout));
}
private static async Task<PlayoutNameViewModel> ApplyUpdateRequest(

View File

@@ -8,6 +8,7 @@ internal static class Mapper
new(
GetDisplayTitle(playoutItem),
playoutItem.StartOffset,
playoutItem.FinishOffset,
GetDisplayDuration(playoutItem.FinishOffset - playoutItem.StartOffset));
internal static PlayoutAlternateScheduleViewModel ProjectToViewModel(

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Playouts;
public record PlayoutItemViewModel(string Title, DateTimeOffset Start, string Duration);
public record PlayoutItemViewModel(string Title, DateTimeOffset Start, DateTimeOffset Finish, string Duration);

View File

@@ -28,9 +28,10 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
ForceSynchronizePlexLibraryById request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
Task<Either<BaseError, string>> IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>.Handle(
SynchronizePlexLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
Task<Either<BaseError, string>> IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>.
Handle(
SynchronizePlexLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
private async Task<Either<BaseError, string>> Handle(
ISynchronizePlexLibraryById request,
@@ -77,10 +78,10 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
TvContext dbContext,
ISynchronizePlexLibraryById request)
{
DateTime minDateTime = await dbContext.PlexLibraries
DateTime minDateTime = await dbContext.PlexLibraries
.SelectOneAsync(l => l.Id, l => l.Id == request.PlexLibraryId)
.Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
return new DateTimeOffset(minDateTime, TimeSpan.Zero);
}
@@ -95,6 +96,6 @@ public class CallPlexLibraryScannerHandler : CallLibraryScannerHandler<ISynchron
}
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now);
return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
}
}

View File

@@ -17,6 +17,7 @@ public record AddProgramScheduleItem(
int? MultipleCount,
TimeSpan? PlayoutDuration,
TailMode TailMode,
int? DiscardToFillAttempts,
string CustomTitle,
GuideMode GuideMode,
int? PreRollFillerId,

View File

@@ -23,7 +23,9 @@ public class
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
return await validation.Apply(p => PerformCopy(dbContext, p, request, cancellationToken));
return await LanguageExtensions.Apply(
validation,
p => PerformCopy(dbContext, p, request, cancellationToken));
}
catch (Exception ex)
{

View File

@@ -19,7 +19,7 @@ public class DeleteProgramScheduleHandler : IRequestHandler<DeleteProgramSchedul
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, ProgramSchedule> validation = await ProgramScheduleMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, ps => DoDeletion(dbContext, ps));
return await validation.Apply(ps => DoDeletion(dbContext, ps));
}
private static Task<Unit> DoDeletion(TvContext dbContext, ProgramSchedule programSchedule)

View File

@@ -15,6 +15,7 @@ public interface IProgramScheduleItemRequest
int? MultipleCount { get; }
TimeSpan? PlayoutDuration { get; }
TailMode TailMode { get; }
int? DiscardToFillAttempts { get; }
string CustomTitle { get; }
GuideMode GuideMode { get; }
int? PreRollFillerId { get; }

View File

@@ -83,6 +83,11 @@ public abstract class ProgramScheduleItemCommandBase
return BaseError.New("[PlayoutDuration] is required for playout mode 'duration'");
}
if (item.DiscardToFillAttempts is null)
{
return BaseError.New("[DiscardToFillAttempts] is required for playout mode 'duration'");
}
if (item.TailMode == TailMode.Filler && item.TailFillerId == null)
{
return BaseError.New("Tail Filler is required with tail mode Filler");
@@ -248,6 +253,9 @@ public abstract class ProgramScheduleItemCommandBase
PlaybackOrder = item.PlaybackOrder,
PlayoutDuration = FixDuration(item.PlayoutDuration.GetValueOrDefault()),
TailMode = item.TailMode,
DiscardToFillAttempts = FixDiscardToFillAttempts(
item.PlaybackOrder,
item.DiscardToFillAttempts.GetValueOrDefault()),
CustomTitle = item.CustomTitle,
GuideMode = item.GuideMode,
PreRollFillerId = item.PreRollFillerId,
@@ -271,4 +279,10 @@ public abstract class ProgramScheduleItemCommandBase
startTime.HasValue && startTime.Value >= TimeSpan.FromDays(1)
? startTime.Value.Subtract(TimeSpan.FromDays(1))
: startTime;
private static int FixDiscardToFillAttempts(PlaybackOrder playbackOrder, int value) => playbackOrder switch
{
PlaybackOrder.Random or PlaybackOrder.Shuffle => value,
_ => 0
};
}

View File

@@ -17,6 +17,7 @@ public record ReplaceProgramScheduleItem(
int? MultipleCount,
TimeSpan? PlayoutDuration,
TailMode TailMode,
int? DiscardToFillAttempts,
string CustomTitle,
GuideMode GuideMode,
int? PreRollFillerId,

View File

@@ -42,6 +42,7 @@ internal static class Mapper
duration.PlaybackOrder,
duration.PlayoutDuration,
duration.TailMode,
duration.DiscardToFillAttempts,
duration.CustomTitle,
duration.GuideMode,
duration.PreRollFiller != null

View File

@@ -21,6 +21,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
PlaybackOrder playbackOrder,
TimeSpan playoutDuration,
TailMode tailMode,
int discardToFillAttempts,
string customTitle,
GuideMode guideMode,
FillerPresetViewModel preRollFiller,
@@ -59,8 +60,10 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
{
PlayoutDuration = playoutDuration;
TailMode = tailMode;
DiscardToFillAttempts = discardToFillAttempts;
}
public TimeSpan PlayoutDuration { get; }
public TailMode TailMode { get; }
public int DiscardToFillAttempts { get; }
}

View File

@@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Resolutions;
public record CreateCustomResolution(int Width, int Height) : IRequest<Option<BaseError>>;

View File

@@ -0,0 +1,74 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Resolutions;
public class CreateCustomResolutionHandler : IRequestHandler<CreateCustomResolution, Option<BaseError>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateCustomResolutionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Option<BaseError>> Handle(CreateCustomResolution request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Resolution> validation = await Validate(dbContext, request);
return await validation.Match(
r => PersistResolution(dbContext, r, cancellationToken),
error => Task.FromResult<Option<BaseError>>(error.Join()));
}
private static async Task<Option<BaseError>> PersistResolution(
TvContext dbContext,
Resolution resolution,
CancellationToken cancellationToken)
{
try
{
await dbContext.Resolutions.AddAsync(resolution, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return Option<BaseError>.None;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
private static Task<Validation<BaseError, Resolution>> Validate(
TvContext dbContext,
CreateCustomResolution request) =>
ResolutionMustBeUnique(dbContext, request)
.MapT(
_ => new Resolution
{
Name = $"{request.Width}x{request.Height}",
Width = request.Width,
Height = request.Height,
IsCustom = true
});
private static async Task<Validation<BaseError, Unit>> ResolutionMustBeUnique(
TvContext dbContext,
CreateCustomResolution request)
{
Option<Resolution> maybeExisting = await dbContext.Resolutions
.FirstOrDefaultAsync(r => r.Height == request.Height && r.Width == request.Width)
.Map(Optional);
if (maybeExisting.IsSome)
{
return BaseError.New("Resolution width and height must be unique");
}
if (request.Height <= 0 || request.Width <= 0)
{
return BaseError.New("Resolution width or height is invalid");
}
return Unit.Default;
}
}

View File

@@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Resolutions;
public record DeleteCustomResolution(int ResolutionId) : IRequest<Option<BaseError>>;

View File

@@ -0,0 +1,40 @@
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Resolutions;
public class DeleteCustomResolutionHandler : IRequestHandler<DeleteCustomResolution, Option<BaseError>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteCustomResolutionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Option<BaseError>> Handle(DeleteCustomResolution request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Resolution> maybeResolution = await dbContext.Resolutions
.AsNoTracking()
.SelectOneAsync(p => p.Id, p => p.Id == request.ResolutionId && p.IsCustom == true);
foreach (Resolution resolution in maybeResolution)
{
// reset any ffmpeg profiles using this resolution to 1920x1080
await dbContext.Connection.ExecuteAsync(
@"UPDATE FFmpegProfile SET ResolutionId = 3 WHERE ResolutionId = @ResolutionId",
new { request.ResolutionId });
dbContext.Resolutions.Remove(resolution);
await dbContext.SaveChangesAsync(cancellationToken);
}
return maybeResolution.IsNone
? BaseError.New($"Resolution {request.ResolutionId} does not exist.")
: Option<BaseError>.None;
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Resolutions;
public record ResolutionViewModel(int Id, string Name, int Width, int Height);
public record ResolutionViewModel(int Id, string Name, int Width, int Height, bool IsCustom);

View File

@@ -5,5 +5,5 @@ namespace ErsatzTV.Application.Resolutions;
internal static class Mapper
{
internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) =>
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height, resolution.IsCustom);
}

View File

@@ -15,9 +15,9 @@ public class GetAllResolutionsHandler : IRequestHandler<GetAllResolutions, List<
GetAllResolutions request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Resolutions
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
.Map(list => list.OrderBy(r => r.Width).ThenBy(r => r.Height).Map(ProjectToViewModel).ToList());
}
}

View File

@@ -18,6 +18,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
private readonly ILogger<RebuildSearchIndexHandler> _logger;
private readonly ISearchIndex _searchIndex;
private readonly ICachingSearchRepository _searchRepository;
private readonly SystemStartup _systemStartup;
public RebuildSearchIndexHandler(
ISearchIndex searchIndex,
@@ -25,6 +26,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem,
IFallbackMetadataProvider fallbackMetadataProvider,
SystemStartup systemStartup,
ILogger<RebuildSearchIndexHandler> logger)
{
_searchIndex = searchIndex;
@@ -33,6 +35,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
_configElementRepository = configElementRepository;
_localFileSystem = localFileSystem;
_fallbackMetadataProvider = fallbackMetadataProvider;
_systemStartup = systemStartup;
}
public async Task Handle(RebuildSearchIndex request, CancellationToken cancellationToken)
@@ -63,5 +66,7 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex>
{
_logger.LogInformation("Search index is already version {Version}", _searchIndex.Version);
}
_systemStartup.SearchIndexIsReady();
}
}

View File

@@ -19,13 +19,13 @@ namespace ErsatzTV.Application.Search;
public class
QuerySearchIndexEpisodesHandler : IRequestHandler<QuerySearchIndexEpisodes, TelevisionEpisodeCardResultsViewModel>
{
private readonly IClient _client;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly IClient _client;
private readonly ISearchIndex _searchIndex;
private readonly ITelevisionRepository _televisionRepository;

View File

@@ -10,9 +10,9 @@ namespace ErsatzTV.Application.Search;
public class QuerySearchIndexMoviesHandler : IRequestHandler<QuerySearchIndexMovies, MovieCardResultsViewModel>
{
private readonly IClient _client;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMovieRepository _movieRepository;
private readonly IClient _client;
private readonly ISearchIndex _searchIndex;
public QuerySearchIndexMoviesHandler(

View File

@@ -15,11 +15,11 @@ namespace ErsatzTV.Application.Search;
public class
QuerySearchIndexMusicVideosHandler : IRequestHandler<QuerySearchIndexMusicVideos, MusicVideoCardResultsViewModel>
{
private readonly IClient _client;
private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly IMusicVideoRepository _musicVideoRepository;
private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly IClient _client;
private readonly ISearchIndex _searchIndex;
public QuerySearchIndexMusicVideosHandler(

View File

@@ -11,8 +11,8 @@ public class
QuerySearchIndexOtherVideosHandler : IRequestHandler<QuerySearchIndexOtherVideos,
OtherVideoCardResultsViewModel>
{
private readonly IOtherVideoRepository _otherVideoRepository;
private readonly IClient _client;
private readonly IOtherVideoRepository _otherVideoRepository;
private readonly ISearchIndex _searchIndex;
public QuerySearchIndexOtherVideosHandler(

View File

@@ -11,8 +11,8 @@ namespace ErsatzTV.Application.Search;
public class
QuerySearchIndexSeasonsHandler : IRequestHandler<QuerySearchIndexSeasons, TelevisionSeasonCardResultsViewModel>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IClient _client;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
private readonly ITelevisionRepository _televisionRepository;

View File

@@ -11,8 +11,8 @@ namespace ErsatzTV.Application.Search;
public class
QuerySearchIndexShowsHandler : IRequestHandler<QuerySearchIndexShows, TelevisionShowCardResultsViewModel>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IClient _client;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
private readonly ITelevisionRepository _televisionRepository;

View File

@@ -12,7 +12,9 @@ public class SearchCollectionsHandler : IRequestHandler<SearchCollections, List<
public SearchCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<MediaCollectionViewModel>> Handle(SearchCollections request, CancellationToken cancellationToken)
public async Task<List<MediaCollectionViewModel>> Handle(
SearchCollections request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Collections.FromSqlRaw(

View File

@@ -12,7 +12,9 @@ public class SearchMultiCollectionsHandler : IRequestHandler<SearchMultiCollecti
public SearchMultiCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<MultiCollectionViewModel>> Handle(SearchMultiCollections request, CancellationToken cancellationToken)
public async Task<List<MultiCollectionViewModel>> Handle(
SearchMultiCollections request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.MultiCollections.FromSqlRaw(

View File

@@ -12,7 +12,9 @@ public class SearchSmartCollectionsHandler : IRequestHandler<SearchSmartCollecti
public SearchSmartCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<SmartCollectionViewModel>> Handle(SearchSmartCollections request, CancellationToken cancellationToken)
public async Task<List<SmartCollectionViewModel>> Handle(
SearchSmartCollections request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.SmartCollections.FromSqlRaw(

View File

@@ -12,7 +12,9 @@ public class SearchTelevisionSeasonsHandler : IRequestHandler<SearchTelevisionSe
public SearchTelevisionSeasonsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<NamedMediaItemViewModel>> Handle(SearchTelevisionSeasons request, CancellationToken cancellationToken)
public async Task<List<NamedMediaItemViewModel>> Handle(
SearchTelevisionSeasons request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<TelevisionSeason>(

View File

@@ -12,7 +12,9 @@ public class SearchTelevisionShowsHandler : IRequestHandler<SearchTelevisionShow
public SearchTelevisionShowsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<NamedMediaItemViewModel>> Handle(SearchTelevisionShows request, CancellationToken cancellationToken)
public async Task<List<NamedMediaItemViewModel>> Handle(
SearchTelevisionShows request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Connection.QueryAsync<TelevisionShow>(

View File

@@ -16,11 +16,11 @@ namespace ErsatzTV.Application.Streaming;
public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<StartFFmpegSessionHandler> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public StartFFmpegSessionHandler(
ILocalFileSystem localFileSystem,
@@ -104,7 +104,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_logger.LogDebug("Playlist exists");
var segmentCount = 0;
var lastSegmentCount = -1;
int lastSegmentCount = -1;
while (DateTimeOffset.Now < finish && segmentCount < initialSegmentCount)
{
if (segmentCount != lastSegmentCount)

View File

@@ -54,8 +54,8 @@ public class HlsSessionWorker : IHlsSessionWorker
{
lock (_sync)
{
_logger.LogDebug("Keep alive - session worker for channel {ChannelNumber}", _channelNumber);
// _logger.LogDebug("Keep alive - session worker for channel {ChannelNumber}", _channelNumber);
_lastAccess = DateTimeOffset.Now;
_timer?.Stop();

View File

@@ -458,6 +458,11 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
foreach (int plexMediaSourceId in maybeId)
{
_logger.LogDebug(
"Attempting to stream Plex file {PlexFileName} using key {PlexKey}",
pmf.Path,
pmf.Key);
return new PlayoutItemWithPath(
playoutItem,
$"http://localhost:{Settings.ListenPort}/media/plex/{plexMediaSourceId}/{pmf.Key}");
@@ -465,7 +470,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
break;
}
// attempt to remotely stream jellyfin
Option<string> jellyfinItemId = playoutItem.MediaItem switch
{
@@ -480,7 +485,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
playoutItem,
$"http://localhost:{Settings.ListenPort}/media/jellyfin/{itemId}");
}
// attempt to remotely stream emby
Option<string> embyItemId = playoutItem.MediaItem switch
{

View File

@@ -9,6 +9,7 @@ using ErsatzTV.Application.Maintenance;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@@ -20,18 +21,21 @@ namespace ErsatzTV.Application.Subtitles;
public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSubtitles, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IEntityLocker _entityLocker;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<ExtractEmbeddedSubtitlesHandler> _logger;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public ExtractEmbeddedSubtitlesHandler(
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem,
IEntityLocker entityLocker,
ChannelWriter<IBackgroundServiceRequest> workerChannel,
ILogger<ExtractEmbeddedSubtitlesHandler> logger)
{
_dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem;
_entityLocker = entityLocker;
_workerChannel = workerChannel;
_logger = logger;
}
@@ -70,7 +74,8 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.AsNoTracking()
.Filter(
p => p.Channel.SubtitleMode != ChannelSubtitleMode.None ||
p.ProgramSchedule.Items.Any(psi => psi.SubtitleMode != ChannelSubtitleMode.None))
p.ProgramSchedule.Items.Any(
psi => psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None))
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId.IfNone(-1));
playoutIdsToCheck.AddRange(requestedPlayout.Map(p => p.Id));
@@ -82,7 +87,8 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.AsNoTracking()
.Filter(
p => p.Channel.SubtitleMode != ChannelSubtitleMode.None ||
p.ProgramSchedule.Items.Any(psi => psi.SubtitleMode != ChannelSubtitleMode.None))
p.ProgramSchedule.Items.Any(
psi => psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None))
.Map(p => p.Id)
.ToList();
}
@@ -101,6 +107,11 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
return Unit.Default;
}
foreach (int playoutId in playoutIdsToCheck)
{
_entityLocker.LockPlayout(playoutId);
}
_logger.LogDebug("Checking playouts {PlayoutIds} for text subtitles to extract", playoutIdsToCheck);
// find all playout items in the next hour
@@ -154,6 +165,11 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
_logger.LogDebug("Done checking playouts {PlayoutIds} for text subtitles to extract", playoutIdsToCheck);
foreach (int playoutId in playoutIdsToCheck)
{
_entityLocker.UnlockPlayout(playoutId);
}
return Unit.Default;
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
@@ -177,7 +193,8 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(
em => em.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "vobsub" && s.Codec != "pgssub"))
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(em => em.EpisodeId)
.ToListAsync(cancellationToken);
result.AddRange(episodeIds);
@@ -188,7 +205,8 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(
mm => mm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "vobsub" && s.Codec != "pgssub"))
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(mm => mm.MovieId)
.ToListAsync(cancellationToken);
result.AddRange(movieIds);
@@ -199,7 +217,8 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(
mm => mm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "vobsub" && s.Codec != "pgssub"))
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(mm => mm.MusicVideoId)
.ToListAsync(cancellationToken);
result.AddRange(musicVideoIds);
@@ -210,7 +229,8 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(
ovm => ovm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "vobsub" && s.Codec != "pgssub"))
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(ovm => ovm.OtherVideoId)
.ToListAsync(cancellationToken);
result.AddRange(otherVideoIds);
@@ -237,9 +257,11 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
// find each subtitle that needs extraction
IEnumerable<Subtitle> subtitles = allSubtitles
.Filter(s => s.SubtitleKind == SubtitleKind.Embedded)
.Filter(
s => s.SubtitleKind == SubtitleKind.Embedded && s.IsExtracted == false &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "vobsub" && s.Codec != "pgssub");
s => s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs")
.Filter(s => s.IsExtracted == false || string.IsNullOrWhiteSpace(s.Path));
// find cache paths for each subtitle
foreach (Subtitle subtitle in subtitles)

View File

@@ -12,10 +12,8 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetSubtitlePathByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
{
public GetSubtitlePathByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, string>> Handle(
GetSubtitlePathById request,
@@ -34,7 +32,7 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
{
return jellyfinUrl;
}
foreach (string embyUrl in await GetEmbyUrl(request, dbContext, maybeSubtitle))
{
return embyUrl;
@@ -125,10 +123,10 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
return $"http://localhost:{Settings.ListenPort}/media/jellyfin/{subtitlePath}";
}
}
return Option<string>.None;
}
private static async Task<Option<string>> GetEmbyUrl(
GetSubtitlePathById request,
TvContext dbContext,
@@ -165,7 +163,7 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
return $"http://localhost:{Settings.ListenPort}/media/emby/{subtitlePath}";
}
}
return Option<string>.None;
}
}

View File

@@ -7,10 +7,7 @@ public class GetMusicVideoCreditTemplatesHandler : IRequestHandler<GetMusicVideo
{
private readonly ILocalFileSystem _localFileSystem;
public GetMusicVideoCreditTemplatesHandler(ILocalFileSystem localFileSystem)
{
_localFileSystem = localFileSystem;
}
public GetMusicVideoCreditTemplatesHandler(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem;
public Task<List<string>> Handle(GetMusicVideoCreditTemplates request, CancellationToken cancellationToken) =>
_localFileSystem.ListFiles(FileSystemLayout.MusicVideoCreditsTemplatesFolder)

View File

@@ -9,7 +9,6 @@ using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Runtime;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
@@ -17,12 +16,12 @@ namespace ErsatzTV.Application.Troubleshooting.Queries;
public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingInfo, TroubleshootingInfo>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IHealthCheckService _healthCheckService;
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
private readonly IConfigElementRepository _configElementRepository;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
private readonly IHealthCheckService _healthCheckService;
private readonly IMemoryCache _memoryCache;
private readonly IRuntimeInfo _runtimeInfo;
public GetTroubleshootingInfoHandler(
IDbContextFactory<TvContext> dbContextFactory,
@@ -105,7 +104,8 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
Optional(GetDriverName(activeDriver)),
vaapiDevice))
{
vaapiCapabilities += $"Checking driver {activeDriver} device {vaapiDevice}{Environment.NewLine}{Environment.NewLine}";
vaapiCapabilities +=
$"Checking driver {activeDriver} device {vaapiDevice}{Environment.NewLine}{Environment.NewLine}";
vaapiCapabilities += output;
vaapiCapabilities += Environment.NewLine + Environment.NewLine;
}
@@ -124,7 +124,7 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
nvidiaCapabilities,
vaapiCapabilities);
}
// lifted from GetFFmpegSettingsHandler
private async Task<FFmpegSettingsViewModel> GetFFmpegSettings()
{

View File

@@ -18,7 +18,7 @@ public class CreateWatermarkHandler : IRequestHandler<CreateWatermark, Either<Ba
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, ChannelWatermark> validation = Validate(request);
return await LanguageExtensions.Apply(validation, profile => PersistChannelWatermark(dbContext, profile));
return await validation.Apply(profile => PersistChannelWatermark(dbContext, profile));
}
private static async Task<CreateWatermarkResult> PersistChannelWatermark(

View File

@@ -19,7 +19,7 @@ public class DeleteWatermarkHandler : IRequestHandler<DeleteWatermark, Either<Ba
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, ChannelWatermark> validation = await WatermarkMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, p => DoDeletion(dbContext, p));
return await validation.Apply(p => DoDeletion(dbContext, p));
}
private static async Task<Unit> DoDeletion(TvContext dbContext, ChannelWatermark watermark)

View File

@@ -8,24 +8,24 @@
<ItemGroup>
<PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="CliWrap" Version="3.6.1" />
<PackageReference Include="CliWrap" Version="3.6.4" />
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="LanguageExt.Core" Version="4.4.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.5.22">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.6.40">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="Serilog" Version="3.0.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
</ItemGroup>

View File

@@ -1,8 +1,8 @@
using System.Runtime.InteropServices;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.FFmpeg.Runtime;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;

View File

@@ -70,7 +70,7 @@ public class FallbackMetadataProviderTests
// metadata.Season.Should().Be(season);
metadata.Head().EpisodeNumber.Should().Be(episode);
}
[TestCase("Awesome Show - S01_BLAH.mkv", 0)]
[TestCase("Awesome Show - NO_EPISODE_NUMBER_HERE.mkv", 0)]
public void GetFallbackMetadata_ShouldHandleNonEpisodes(string path, int episode)

View File

@@ -1,8 +1,8 @@
using System.Runtime.InteropServices;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Core.Plex;
using ErsatzTV.FFmpeg.Runtime;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;

View File

@@ -8,6 +8,11 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture]
public class ChronologicalContentTests
{
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
private CancellationToken _cancellationToken;
[Test]
public void Episodes_Should_Sort_By_Aired()
{

View File

@@ -7,6 +7,11 @@ namespace ErsatzTV.Core.Tests.Scheduling;
public class CustomOrderContentTests
{
private CancellationToken _cancellationToken;
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
[Test]
public void MediaItems_Should_Sort_By_CustomOrder()
{

View File

@@ -17,6 +17,9 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture]
public class PlayoutBuilderTests
{
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
private readonly ILogger<PlayoutBuilder> _logger;
public PlayoutBuilderTests()
@@ -32,6 +35,8 @@ public class PlayoutBuilderTests
_logger = loggerFactory?.CreateLogger<PlayoutBuilder>();
}
private CancellationToken _cancellationToken;
[TestFixture]
public class NewPlayout : PlayoutBuilderTests
{
@@ -46,7 +51,7 @@ public class PlayoutBuilderTests
(PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, _cancellationToken);
result.Items.Should().BeEmpty();
}
@@ -64,7 +69,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(1);
result.Items.Head().MediaItemId.Should().Be(2);
@@ -94,7 +99,7 @@ public class PlayoutBuilderTests
(PlayoutBuilder builder, Playout playout) =
TestDataFloodForItems(mediaItems, PlaybackOrder.Random, configRepo);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, _cancellationToken);
configRepo.Verify();
@@ -123,7 +128,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
configRepo.Verify();
@@ -155,7 +160,7 @@ public class PlayoutBuilderTests
(PlayoutBuilder builder, Playout playout) =
TestDataFloodForItems(mediaItems, PlaybackOrder.Random, configRepo);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, _cancellationToken);
configRepo.Verify();
@@ -184,7 +189,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
configRepo.Verify();
@@ -217,7 +222,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(1);
result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
@@ -247,7 +252,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(1);
result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
@@ -268,7 +273,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(1);
result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
@@ -289,7 +294,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(1);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(2);
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
@@ -313,7 +318,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(4);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(4);
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
@@ -342,7 +347,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(1);
result.Items.Head().MediaItemId.Should().Be(1);
@@ -355,7 +360,12 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = HoursAfterMidnight(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2);
Playout result2 = await builder.Build(
playout,
PlayoutBuildMode.Continue,
start2,
finish2,
_cancellationToken);
result2.Items.Count.Should().Be(2);
result2.Items.Last().StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
@@ -380,7 +390,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(1);
result.Items.Head().MediaItemId.Should().Be(1);
@@ -392,7 +402,12 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = HoursAfterMidnight(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12);
Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2);
Playout result2 = await builder.Build(
playout,
PlayoutBuildMode.Continue,
start2,
finish2,
_cancellationToken);
result2.Items.Count.Should().Be(3);
result2.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
@@ -422,7 +437,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(6);
result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6));
@@ -435,7 +450,7 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
Playout result2 = await builder.Build(playout, PlayoutBuildMode.Reset, start2, finish2);
Playout result2 = await builder.Build(playout, PlayoutBuildMode.Reset, start2, finish2, _cancellationToken);
result2.Items.Count.Should().Be(6);
result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6));
@@ -462,7 +477,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(6);
result.ProgramScheduleAnchors.Count.Should().Be(1);
@@ -475,7 +490,12 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2);
Playout result2 = await builder.Build(
playout,
PlayoutBuildMode.Continue,
start2,
finish2,
_cancellationToken);
int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed;
@@ -564,7 +584,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(5);
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
@@ -661,7 +681,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(30);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(28);
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero);
@@ -806,7 +826,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(7);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(6);
@@ -909,7 +929,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(24);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(6);
@@ -1021,7 +1041,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(32);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken);
result.Items.Count.Should().Be(5);
@@ -1126,7 +1146,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(7);
@@ -1235,7 +1255,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(6);
@@ -1349,7 +1369,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(5);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken);
result.Items.Count.Should().Be(4);
@@ -1452,7 +1472,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(5);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(5);
@@ -1566,7 +1586,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(5);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken);
result.Items.Count.Should().Be(4);
@@ -1691,7 +1711,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(12);
@@ -1808,7 +1828,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(1);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(2);
@@ -1885,7 +1905,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(6);
@@ -1921,7 +1941,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromDays(2);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(8);
result.Items[0].MediaItemId.Should().Be(1);
@@ -1980,7 +2000,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(6);
result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6));
@@ -1993,7 +2013,7 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
Playout result2 = await builder.Build(playout, PlayoutBuildMode.Reset, start2, finish2);
Playout result2 = await builder.Build(playout, PlayoutBuildMode.Reset, start2, finish2, _cancellationToken);
result2.Items.Count.Should().Be(6);
result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6));
@@ -2098,7 +2118,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(24);
DateTimeOffset finish = start + TimeSpan.FromDays(1);
Playout result = await builder.Build(playout, PlayoutBuildMode.Refresh, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Refresh, start, finish, _cancellationToken);
result.Items.Count.Should().Be(4);
result.Items[0].MediaItemId.Should().Be(2);
@@ -2135,7 +2155,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(1);
result.Items.Head().MediaItemId.Should().Be(1);
@@ -2148,7 +2168,12 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = HoursAfterMidnight(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2);
Playout result2 = await builder.Build(
playout,
PlayoutBuildMode.Continue,
start2,
finish2,
_cancellationToken);
result2.Items.Count.Should().Be(2);
result2.Items.Last().StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
@@ -2173,7 +2198,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(1);
result.Items.Head().MediaItemId.Should().Be(1);
@@ -2185,7 +2210,12 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = HoursAfterMidnight(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12);
Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2);
Playout result2 = await builder.Build(
playout,
PlayoutBuildMode.Continue,
start2,
finish2,
_cancellationToken);
result2.Items.Count.Should().Be(3);
result2.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6));
@@ -2212,7 +2242,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromDays(1);
await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
playout.Items.Count.Should().Be(4);
playout.Items.Map(i => i.MediaItemId).ToList().Should().Equal(1, 2, 1, 2);
@@ -2249,7 +2279,7 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = HoursAfterMidnight(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(1);
await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2);
await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2, _cancellationToken);
playout.Items.Count.Should().Be(5);
playout.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0));
@@ -2272,7 +2302,7 @@ public class PlayoutBuilderTests
DateTimeOffset start3 = HoursAfterMidnight(2);
DateTimeOffset finish3 = start3 + TimeSpan.FromDays(1);
await builder.Build(playout, PlayoutBuildMode.Continue, start3, finish3);
await builder.Build(playout, PlayoutBuildMode.Continue, start3, finish3, _cancellationToken);
playout.Items.Count.Should().Be(5);
playout.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0));
@@ -2306,7 +2336,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(6);
result.ProgramScheduleAnchors.Count.Should().Be(1);
@@ -2318,7 +2348,12 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
Playout result2 = await builder.Build(
result,
PlayoutBuildMode.Continue,
start2,
finish2,
_cancellationToken);
int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed;
@@ -2331,7 +2366,7 @@ public class PlayoutBuilderTests
public async Task ShuffleFlood_Should_MaintainRandomSeed_MultipleDays()
{
var mediaItems = new List<MediaItem>();
for (int i = 1; i <= 25; i++)
for (var i = 1; i <= 25; i++)
{
mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i)));
}
@@ -2340,7 +2375,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5);
DateTimeOffset finish = start + TimeSpan.FromDays(2);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(53);
result.ProgramScheduleAnchors.Count.Should().Be(2);
@@ -2351,11 +2386,13 @@ public class PlayoutBuilderTests
.First();
lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0);
lastCheckpoint.EnumeratorState.Index.Should().Be(3);
// we need to mess up the ordering to trigger the problematic behavior
// this simulates the way the rows are loaded with EF
PlayoutProgramScheduleAnchor oldest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate).Last();
PlayoutProgramScheduleAnchor newest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate).First();
PlayoutProgramScheduleAnchor oldest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate)
.Last();
PlayoutProgramScheduleAnchor newest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate)
.First();
result.ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>
{
@@ -2368,16 +2405,21 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = start.AddHours(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2);
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
Playout result2 = await builder.Build(
result,
PlayoutBuildMode.Continue,
start2,
finish2,
_cancellationToken);
PlayoutProgramScheduleAnchor continueAnchor =
result2.ProgramScheduleAnchors.First(x => x.AnchorDate is null);
int secondSeedValue = continueAnchor.EnumeratorState.Seed;
// the continue anchor should have the same seed as the most recent (last) checkpoint from the first run
firstSeedValue.Should().Be(secondSeedValue);
}
[Test]
public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed()
{
@@ -2393,11 +2435,12 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(6);
result.ProgramScheduleAnchors.Count.Should().Be(2);
PlayoutProgramScheduleAnchor primaryAnchor = result.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1);
PlayoutProgramScheduleAnchor primaryAnchor =
result.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1);
primaryAnchor.EnumeratorState.Seed.Should().BeGreaterThan(0);
primaryAnchor.EnumeratorState.Index.Should().Be(0);
@@ -2406,7 +2449,12 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
Playout result2 = await builder.Build(
result,
PlayoutBuildMode.Continue,
start2,
finish2,
_cancellationToken);
primaryAnchor = result2.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1);
int secondSeedValue = primaryAnchor.EnumeratorState.Seed;
@@ -2415,12 +2463,12 @@ public class PlayoutBuilderTests
primaryAnchor.EnumeratorState.Index.Should().Be(0);
}
[Test]
public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed_MultipleDays()
{
var mediaItems = new List<MediaItem>();
for (int i = 1; i <= 100; i++)
for (var i = 1; i <= 100; i++)
{
mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i)));
}
@@ -2430,7 +2478,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5);
DateTimeOffset finish = start + TimeSpan.FromDays(2);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken);
result.Items.Count.Should().Be(53);
result.ProgramScheduleAnchors.Count.Should().Be(4);
@@ -2442,7 +2490,7 @@ public class PlayoutBuilderTests
.First();
lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0);
lastCheckpoint.EnumeratorState.Index.Should().Be(53);
int firstSeedValue = lastCheckpoint.EnumeratorState.Seed;
for (var i = 1; i < 20; i++)
@@ -2450,7 +2498,12 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = start.AddHours(i);
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2);
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
Playout result2 = await builder.Build(
result,
PlayoutBuildMode.Continue,
start2,
finish2,
_cancellationToken);
PlayoutProgramScheduleAnchor continueAnchor =
result2.ProgramScheduleAnchors
@@ -2554,7 +2607,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(32);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken);
result.Items.Count.Should().Be(5);
@@ -2666,7 +2719,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(5);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken);
result.Items.Count.Should().Be(4);
@@ -2778,7 +2831,7 @@ public class PlayoutBuilderTests
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(5);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish);
Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken);
result.Items.Count.Should().Be(4);
@@ -2813,7 +2866,7 @@ public class PlayoutBuilderTests
Collection = mediaCollection,
CollectionId = mediaCollection.Id,
StartTime = null,
PlaybackOrder = playbackOrder,
PlaybackOrder = playbackOrder
};
private static ProgramScheduleItem Flood(
@@ -2897,7 +2950,7 @@ public class PlayoutBuilderTests
return new TestData(builder, playout);
}
private TestData TestDataFloodForSmartCollectionItems(
List<MediaItem> mediaItems,
PlaybackOrder playbackOrder,

View File

@@ -13,191 +13,18 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture]
public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
{
[TestFixture]
public class CalculateEndTimeWithFiller
[SetUp]
public void SetUp()
{
[Test]
public void Should_Not_Touch_Enumerator()
{
var collection = new Collection
{
Id = 1,
Name = "Filler Items",
MediaItems = new List<MediaItem>()
};
for (var i = 0; i < 5; i++)
{
collection.MediaItems.Add(TestMovie(i + 1, TimeSpan.FromHours(i + 1), new DateTime(2020, 2, i + 1)));
}
var fillerPreset = new FillerPreset
{
FillerKind = FillerKind.PreRoll,
FillerMode = FillerMode.Count,
Count = 3,
Collection = collection,
CollectionId = collection.Id
};
var enumerator = new ChronologicalMediaCollectionEnumerator(
collection.MediaItems,
new CollectionEnumeratorState { Index = 0, Seed = 1 });
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>
{
{ CollectionKey.ForFillerPreset(fillerPreset), enumerator }
},
new ProgramScheduleItemOne
{
PreRollFiller = fillerPreset
},
new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 18, 12, 30, TimeSpan.FromHours(-5)));
enumerator.State.Index.Should().Be(0);
enumerator.State.Seed.Should().Be(1);
}
[Test]
public void Should_Pad_To_15_Minutes_15()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 15
}
},
new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 15, 0, TimeSpan.FromHours(-5)));
}
[Test]
public void Should_Pad_To_15_Minutes_30()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 15
}
},
new DateTimeOffset(2020, 2, 1, 12, 16, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)));
}
[Test]
public void Should_Pad_To_15_Minutes_45()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 15
}
},
new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 45, 0, TimeSpan.FromHours(-5)));
}
[Test]
public void Should_Pad_To_15_Minutes_00()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 15
}
},
new DateTimeOffset(2020, 2, 1, 12, 46, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5)));
}
[Test]
public void Should_Pad_To_30_Minutes_30()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 30
}
},
new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)));
}
[Test]
public void Should_Pad_To_30_Minutes_00()
{
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
.CalculateEndTimeWithFiller(
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
new ProgramScheduleItemOne
{
MidRollFiller = new FillerPreset
{
FillerKind = FillerKind.MidRoll,
FillerMode = FillerMode.Pad,
PadToNearestMinute = 30
}
},
new DateTimeOffset(2020, 2, 1, 12, 20, 0, TimeSpan.FromHours(-5)),
new TimeSpan(0, 12, 30),
new List<MediaChapter>());
result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5)));
}
_cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
_scheduler = new TestScheduler();
}
private CancellationToken _cancellationToken;
private PlayoutModeSchedulerBase<ProgramScheduleItem> _scheduler;
[TestFixture]
public class AddFiller
public class AddFiller : PlayoutModeSchedulerBaseTests
{
[Test]
public void Should_Not_Crash_Mid_Roll_Zero_Chapters()
@@ -224,13 +51,15 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
List<PlayoutItem> playoutItems = Scheduler()
List<PlayoutItem> playoutItems = _scheduler
.AddFiller(
startState,
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
new PlayoutItem(),
new List<MediaChapter>());
new List<MediaChapter>(),
true,
_cancellationToken);
playoutItems.Count.Should().Be(1);
}
@@ -275,13 +104,15 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
// too lazy to make another enumerator for the filler that we don't want
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), enumerator);
List<PlayoutItem> playoutItems = Scheduler()
List<PlayoutItem> playoutItems = _scheduler
.AddFiller(
startState,
enumerators,
scheduleItem,
new PlayoutItem(),
new List<MediaChapter> { new() });
new List<MediaChapter> { new() },
true,
_cancellationToken);
playoutItems.Count.Should().Be(1);
}
@@ -331,7 +162,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), fillerEnumerator);
List<PlayoutItem> playoutItems = Scheduler()
List<PlayoutItem> playoutItems = _scheduler
.AddFiller(
startState,
enumerators,
@@ -346,7 +177,9 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
{
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(60) }
});
},
true,
_cancellationToken);
playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1);
@@ -356,14 +189,14 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11));
}
[Test]
public void Should_Schedule_Post_Roll_After_Padded_Mid_Roll()
{
// content 45 min, mid roll pad to 60, post roll 5 min
// content + post = 50 min, mid roll will add two 5 min items
// content + mid + post = 60 min
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(45));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(5));
@@ -421,7 +254,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), midRollFillerEnumerator);
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.PostRollFiller), postRollFillerEnumerator);
List<PlayoutItem> playoutItems = Scheduler()
List<PlayoutItem> playoutItems = _scheduler
.AddFiller(
startState,
enumerators,
@@ -436,38 +269,40 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
{
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(45) }
});
},
true,
_cancellationToken);
playoutItems.Count.Should().Be(5);
// content chapter 1
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
// mid-roll 1
playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(6));
// mid-roll 2
playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11));
// content chapter 2
playoutItems[3].MediaItemId.Should().Be(1);
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(16));
// post-roll
playoutItems[4].MediaItemId.Should().Be(5);
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(55));
}
[Test]
public void Should_Schedule_Padded_Post_Roll_After_Mid_Roll_Count()
{
// content 45 min, mid roll 5 min, post roll pad to 60
// content + mid = 50 min, post roll will add two 5 min items
// content + mid + post = 60 min
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(45));
Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5));
Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(5));
@@ -525,7 +360,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), midRollFillerEnumerator);
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.PostRollFiller), postRollFillerEnumerator);
List<PlayoutItem> playoutItems = Scheduler()
List<PlayoutItem> playoutItems = _scheduler
.AddFiller(
startState,
enumerators,
@@ -540,18 +375,20 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
{
new() { StartTime = TimeSpan.Zero, EndTime = TimeSpan.FromMinutes(6) },
new() { StartTime = TimeSpan.FromMinutes(6), EndTime = TimeSpan.FromMinutes(45) }
});
},
true,
_cancellationToken);
playoutItems.Count.Should().Be(5);
// content chapter 1
playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
// mid-roll 1
playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(6));
// content chapter 2
playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(11));
@@ -559,7 +396,7 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
// post-roll 1
playoutItems[3].MediaItemId.Should().Be(5);
playoutItems[3].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(50));
// post-roll 2
playoutItems[4].MediaItemId.Should().Be(6);
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime + TimeSpan.FromMinutes(55));
@@ -606,8 +443,6 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
}
};
private static PlayoutModeSchedulerBase<ProgramScheduleItem> Scheduler() =>
new TestScheduler();
private class TestScheduler : PlayoutModeSchedulerBase<ProgramScheduleItem>
{
@@ -632,7 +467,8 @@ public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
Dictionary<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators,
ProgramScheduleItem scheduleItem,
ProgramScheduleItem nextScheduleItem,
DateTimeOffset hardStop) =>
DateTimeOffset hardStop,
CancellationToken cancellationToken) =>
throw new NotSupportedException();
}
}

View File

@@ -11,6 +11,11 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture]
public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
{
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
private CancellationToken _cancellationToken;
[Test]
public void Should_Fill_Exact_Duration()
{
@@ -45,7 +50,8 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -117,7 +123,8 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -188,7 +195,8 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -256,7 +264,8 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
// duration block should end after exact duration, with gap
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
@@ -289,7 +298,7 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
// offline should not set guide finish
playoutItems[2].GuideFinish.HasValue.Should().BeFalse();
}
@@ -338,7 +347,8 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -424,7 +434,8 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -522,7 +533,8 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
@@ -637,7 +649,8 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -738,7 +751,8 @@ public class PlayoutModeSchedulerDurationTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutItems.Should().BeEmpty();

View File

@@ -11,6 +11,11 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture]
public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
{
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
private CancellationToken _cancellationToken;
[Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item()
{
@@ -51,7 +56,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -85,7 +91,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].CustomTitle.Should().Be("CustomTitle");
}
[Test]
public void Should_Schedule_Single_Item_Fixed_Start_Flood()
{
@@ -125,7 +131,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
scheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(6));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -164,13 +171,13 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].CustomTitle.Should().Be("CustomTitle");
playoutItems[4].MediaItemId.Should().Be(1);
playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.AddHours(4));
playoutItems[4].GuideGroup.Should().Be(1);
playoutItems[4].FillerKind.Should().Be(FillerKind.None);
playoutItems[4].CustomTitle.Should().Be("CustomTitle");
playoutItems[5].MediaItemId.Should().Be(2);
playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.AddHours(5));
playoutItems[5].GuideGroup.Should().Be(1);
@@ -221,7 +228,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -305,7 +313,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -392,7 +401,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -473,7 +483,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -570,7 +581,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -657,7 +669,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -770,7 +783,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -823,7 +837,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
playoutItems[6].GuideGroup.Should().Be(3);
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
}
[Test]
public void Should_Not_Schedule_Fallback_Filler_Incomplete_Flood()
{
@@ -868,7 +882,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
// hard stop at 2, an hour before the "next schedule item" at 3
DateTimeOffset hardStop = StartState(scheduleItemsEnumerator).CurrentTime.AddHours(2);
@@ -881,7 +895,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
enumerator2),
scheduleItem,
NextScheduleItem,
hardStop);
hardStop,
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(2));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -928,7 +943,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
playoutItems[5].GuideGroup.Should().Be(6);
playoutItems[5].FillerKind.Should().Be(FillerKind.None);
}
[Test]
public void Should_Not_Schedule_Tail_Filler_Incomplete_Flood()
{
@@ -973,7 +988,7 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
// hard stop at 2, an hour before the "next schedule item" at 3
DateTimeOffset hardStop = StartState(scheduleItemsEnumerator).CurrentTime.AddHours(2);
@@ -986,7 +1001,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
enumerator2),
scheduleItem,
NextScheduleItem,
hardStop);
hardStop,
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(2));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -1099,7 +1115,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -1172,7 +1189,8 @@ public class PlayoutModeSchedulerFloodTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutItems.Should().BeEmpty();

View File

@@ -11,6 +11,11 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture]
public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
{
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
private CancellationToken _cancellationToken;
[Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item()
{
@@ -52,7 +57,8 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -126,7 +132,8 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -208,7 +215,8 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -306,7 +314,8 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -394,7 +403,8 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -509,7 +519,8 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -630,7 +641,8 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -709,7 +721,8 @@ public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutItems.Should().BeEmpty();

View File

@@ -11,6 +11,11 @@ namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture]
public class PlayoutModeSchedulerOneTests : SchedulerTestBase
{
[SetUp]
public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
private CancellationToken _cancellationToken;
[Test]
public void Should_Have_Gap_With_No_Tail_No_Fallback()
{
@@ -45,7 +50,8 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -127,7 +133,8 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -194,7 +201,8 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -275,7 +283,8 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -346,7 +355,8 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -443,7 +453,8 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -546,7 +557,8 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -631,7 +643,8 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -730,7 +743,8 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
enumerator3),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
@@ -808,7 +822,8 @@ public class PlayoutModeSchedulerOneTests : SchedulerTestBase
CollectionEnumerators(scheduleItem, enumerator),
scheduleItem,
NextScheduleItem,
HardStop(scheduleItemsEnumerator));
HardStop(scheduleItemsEnumerator),
_cancellationToken);
playoutItems.Should().BeEmpty();

Some files were not shown because too many files have changed in this diff Show More