Browse Source

multiple fixes to duration mode (#442)

pull/443/head
Jason Dove 4 years ago committed by GitHub
parent
commit
33aaadae68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 206
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  3. 2
      ErsatzTV.Core/Domain/PlayoutItem.cs
  4. 17
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  5. 6
      ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs
  6. 2
      ErsatzTV/appsettings.json

6
CHANGELOG.md

@ -8,9 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -8,9 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix double scheduling; this could happen if the app was shutdown during a playout build
- Fix updating Jellyfin and Emby TV seasons
- Fix updating Jellyfin and Emby artwork
- Fix Plex, Jellyfin, Emby worker crash attempting to sync library that no longer exists
- Fix bug with `Duration` mode scheduling when media items are too long to fit in the requested duration
- Fix bug with `Duration` mode scheduling with `Filler` tail mode where other duration items in the schedule would be skipped
### Changed
- Automatically find working Plex address on startup
- Change default log level from `Debug` to `Information`
- The `Debug` log level can be enabled in the `appsettings.json` file for non-docker installs
- The `Debug` log level can be enabled by setting the environment variable `Serilog:MinimumLevel=Debug` for docker installs
## [0.1.4-alpha] - 2021-10-14
### Fixed

206
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

@ -1151,6 +1151,212 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -1151,6 +1151,212 @@ namespace ErsatzTV.Core.Tests.Scheduling
result.Anchor.NextScheduleItem.Should().Be(items[1]);
result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime);
}
[Test]
public async Task Alternating_Duration_With_Filler_Should_Alternate_Schedule_Items()
{
var collectionOne = new Collection
{
Id = 1,
Name = "Duration Items 1",
MediaItems = new List<MediaItem>
{
TestMovie(1, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1))
}
};
var collectionTwo = new Collection
{
Id = 2,
Name = "Duration Items 2",
MediaItems = new List<MediaItem>
{
TestMovie(2, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1))
}
};
var collectionThree = new Collection
{
Id = 3,
Name = "Filler Items",
MediaItems = new List<MediaItem>
{
TestMovie(3, TimeSpan.FromMinutes(5), new DateTime(2020, 1, 1))
}
};
var fakeRepository = new FakeMediaCollectionRepository(
Map(
(collectionOne.Id, collectionOne.MediaItems.ToList()),
(collectionTwo.Id, collectionTwo.MediaItems.ToList()),
(collectionThree.Id, collectionThree.MediaItems.ToList())));
var items = new List<ProgramScheduleItem>
{
new ProgramScheduleItemDuration
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(3),
PlaybackOrder = PlaybackOrder.Chronological,
TailMode = TailMode.Filler,
TailCollectionType = ProgramScheduleItemCollectionType.Collection,
TailCollection = collectionThree,
TailCollectionId = collectionThree.Id
},
new ProgramScheduleItemDuration
{
Id = 2,
Index = 2,
Collection = collectionTwo,
CollectionId = collectionTwo.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(3),
PlaybackOrder = PlaybackOrder.Chronological,
TailMode = TailMode.Filler,
TailCollectionType = ProgramScheduleItemCollectionType.Collection,
TailCollection = collectionThree,
TailCollectionId = collectionThree.Id
}
};
var playout = new Playout
{
ProgramSchedule = new ProgramSchedule
{
Items = items
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(
configRepo.Object,
fakeRepository,
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
result.Items.Count.Should().Be(12);
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(0));
result.Items[0].MediaItemId.Should().Be(1);
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(55));
result.Items[1].MediaItemId.Should().Be(1);
result.Items[2].StartOffset.TimeOfDay.Should().Be(new TimeSpan(1, 50, 0));
result.Items[2].MediaItemId.Should().Be(1);
result.Items[3].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 45, 0));
result.Items[3].MediaItemId.Should().Be(3);
result.Items[4].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 50, 0));
result.Items[4].MediaItemId.Should().Be(3);
result.Items[5].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 55, 0));
result.Items[5].MediaItemId.Should().Be(3);
result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
result.Items[6].MediaItemId.Should().Be(2);
result.Items[7].StartOffset.TimeOfDay.Should().Be(new TimeSpan(3, 55, 0));
result.Items[7].MediaItemId.Should().Be(2);
result.Items[8].StartOffset.TimeOfDay.Should().Be(new TimeSpan(4, 50, 0));
result.Items[8].MediaItemId.Should().Be(2);
result.Items[9].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 45, 0));
result.Items[9].MediaItemId.Should().Be(3);
result.Items[10].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 50, 0));
result.Items[10].MediaItemId.Should().Be(3);
result.Items[11].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 55, 0));
result.Items[11].MediaItemId.Should().Be(3);
result.Anchor.NextScheduleItem.Should().Be(items[0]);
result.Anchor.DurationFinish.Should().BeNull();
}
[Test]
public async Task Duration_Should_Skip_Items_That_Are_Too_Long()
{
var collectionOne = new Collection
{
Id = 1,
Name = "Duration Items 1",
MediaItems = new List<MediaItem>
{
TestMovie(1, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)),
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)),
TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)),
TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
}
};
var fakeRepository =
new FakeMediaCollectionRepository(Map((collectionOne.Id, collectionOne.MediaItems.ToList())));
var items = new List<ProgramScheduleItem>
{
new ProgramScheduleItemDuration
{
Id = 1,
Index = 1,
Collection = collectionOne,
CollectionId = collectionOne.Id,
StartTime = null,
PlayoutDuration = TimeSpan.FromHours(1),
PlaybackOrder = PlaybackOrder.Chronological,
TailMode = TailMode.None,
}
};
var playout = new Playout
{
ProgramSchedule = new ProgramSchedule
{
Items = items
},
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
};
var configRepo = new Mock<IConfigElementRepository>();
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var builder = new PlayoutBuilder(
configRepo.Object,
fakeRepository,
televisionRepo,
artistRepo.Object,
_logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
result.Items.Count.Should().Be(6);
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0));
result.Items[0].MediaItemId.Should().Be(2);
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1));
result.Items[1].MediaItemId.Should().Be(4);
result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2));
result.Items[2].MediaItemId.Should().Be(2);
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
result.Items[3].MediaItemId.Should().Be(4);
result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
result.Items[4].MediaItemId.Should().Be(2);
result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5));
result.Items[5].MediaItemId.Should().Be(4);
result.Anchor.NextScheduleItem.Should().Be(items[0]);
result.Anchor.DurationFinish.Should().BeNull();
}
private static DateTimeOffset HoursAfterMidnight(int hours)
{

2
ErsatzTV.Core/Domain/PlayoutItem.cs

@ -1,7 +1,9 @@ @@ -1,7 +1,9 @@
using System;
using System.Diagnostics;
namespace ErsatzTV.Core.Domain
{
[DebuggerDisplay("{MediaItemId} - {Start} - {Finish}")]
public class PlayoutItem
{
public int Id { get; set; }

17
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -302,9 +302,20 @@ namespace ErsatzTV.Core.Scheduling @@ -302,9 +302,20 @@ namespace ErsatzTV.Core.Scheduling
playoutItem.CustomTitle = scheduleItem.CustomTitle;
}
currentTime = itemStartTime + version.Duration;
enumerator.MoveNext();
if (scheduleItem is ProgramScheduleItemDuration d &&
version.Duration > d.PlayoutDuration)
{
_logger.LogWarning(
"Skipping playout item {Title} with duration {Duration} that is longer than schedule item duration {PlayoutDuration}",
DisplayTitle(mediaItem),
version.Duration,
d.PlayoutDuration);
return;
}
currentTime = itemStartTime + version.Duration;
playout.Items.Add(playoutItem);
switch (scheduleItem)
@ -442,6 +453,10 @@ namespace ErsatzTV.Core.Scheduling @@ -442,6 +453,10 @@ namespace ErsatzTV.Core.Scheduling
durationFinish,
collectionEnumerators))
{
// if we're starting filler, we don't actually need to move
// to the next schedule item yet
index--;
inDurationFiller = true;
durationFinish.Do(
f => playoutItem.GuideFinish = f.UtcDateTime);

6
ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs

@ -81,7 +81,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -81,7 +81,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public async Task<Option<PlexMediaSource>> GetPlexByLibraryId(int plexLibraryId)
{
int? id = await _dbConnection.QuerySingleAsync<int?>(
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
@"SELECT L.MediaSourceId FROM Library L
INNER JOIN PlexLibrary PL on L.Id = PL.Id
WHERE L.Id = @PlexLibraryId",
@ -562,7 +562,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -562,7 +562,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public async Task<Option<JellyfinMediaSource>> GetJellyfinByLibraryId(int jellyfinLibraryId)
{
int? id = await _dbConnection.QuerySingleAsync<int?>(
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
@"SELECT L.MediaSourceId FROM Library L
INNER JOIN JellyfinLibrary PL on L.Id = PL.Id
WHERE L.Id = @JellyfinLibraryId",
@ -745,7 +745,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -745,7 +745,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public async Task<Option<EmbyMediaSource>> GetEmbyByLibraryId(int embyLibraryId)
{
int? id = await _dbConnection.QuerySingleAsync<int?>(
int? id = await _dbConnection.QuerySingleOrDefaultAsync<int?>(
@"SELECT L.MediaSourceId FROM Library L
INNER JOIN EmbyLibrary PL on L.Id = PL.Id
WHERE L.Id = @EmbyLibraryId",

2
ErsatzTV/appsettings.json

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
"Serilog.Sinks.Console"
],
"MinimumLevel": {
"Default": "Debug",
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System.Net.Http.HttpClient": "Warning"

Loading…
Cancel
Save