fix mid-roll filler scheduling bug (#675)
This commit is contained in:
@@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Fix playback of interlaced mpeg2video content with NVIDIA acceleration
|
||||
- Fix playback of all interlaced content with QSV acceleration
|
||||
- Fix adding songs to collections from search results page
|
||||
- Fix bug scheduling mid-roll filler with content that contains one chapter
|
||||
- No mid-roll filler will be inserted for content with zero or one chapters
|
||||
|
||||
### Added
|
||||
- Add automated error reporting via Bugsnag
|
||||
|
||||
@@ -3,190 +3,287 @@ using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling;
|
||||
|
||||
[TestFixture]
|
||||
public class PlayoutModeSchedulerBaseTests
|
||||
public class PlayoutModeSchedulerBaseTests : SchedulerTestBase
|
||||
{
|
||||
[Test]
|
||||
public void CalculateEndTimeWithFiller_Should_Not_Touch_Enumerator()
|
||||
[TestFixture]
|
||||
public class CalculateEndTimeWithFiller
|
||||
{
|
||||
var collection = new Collection
|
||||
[Test]
|
||||
public void Should_Not_Touch_Enumerator()
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Filler Items",
|
||||
MediaItems = new List<MediaItem>()
|
||||
};
|
||||
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)));
|
||||
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);
|
||||
}
|
||||
|
||||
var fillerPreset = new FillerPreset
|
||||
[Test]
|
||||
public void Should_Pad_To_15_Minutes_15()
|
||||
{
|
||||
FillerKind = FillerKind.PreRoll,
|
||||
FillerMode = FillerMode.Count,
|
||||
Count = 3,
|
||||
Collection = collection,
|
||||
CollectionId = collection.Id
|
||||
};
|
||||
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>());
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collection.MediaItems,
|
||||
new CollectionEnumeratorState { Index = 0, Seed = 1 });
|
||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 15, 0, TimeSpan.FromHours(-5)));
|
||||
}
|
||||
|
||||
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>());
|
||||
[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, 18, 12, 30, TimeSpan.FromHours(-5)));
|
||||
enumerator.State.Index.Should().Be(0);
|
||||
enumerator.State.Seed.Should().Be(1);
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_15()
|
||||
[TestFixture]
|
||||
public class AddFiller
|
||||
{
|
||||
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
||||
.CalculateEndTimeWithFiller(
|
||||
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
|
||||
new ProgramScheduleItemOne
|
||||
[Test]
|
||||
public void Should_Not_Crash_Mid_Roll_Zero_Chapters()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
MidRollFiller = new FillerPreset
|
||||
{
|
||||
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>());
|
||||
FillerKind = FillerKind.MidRoll,
|
||||
FillerMode = FillerMode.Pad,
|
||||
PadToNearestMinute = 15
|
||||
}
|
||||
};
|
||||
|
||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 15, 0, TimeSpan.FromHours(-5)));
|
||||
}
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
[Test]
|
||||
public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_30()
|
||||
{
|
||||
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
||||
.CalculateEndTimeWithFiller(
|
||||
new Dictionary<CollectionKey, IMediaCollectionEnumerator>(),
|
||||
new ProgramScheduleItemOne
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
List<PlayoutItem> playoutItems = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
||||
.AddFiller(
|
||||
startState,
|
||||
CollectionEnumerators(scheduleItem, enumerator),
|
||||
scheduleItem,
|
||||
new PlayoutItem(),
|
||||
new List<MediaChapter>());
|
||||
|
||||
playoutItems.Count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_Crash_Mid_Roll_One_Chapter()
|
||||
{
|
||||
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
|
||||
|
||||
var scheduleItem = new ProgramScheduleItemOne
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlaybackOrder = PlaybackOrder.Chronological,
|
||||
TailFiller = null,
|
||||
FallbackFiller = null,
|
||||
MidRollFiller = new FillerPreset
|
||||
{
|
||||
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>());
|
||||
FillerKind = FillerKind.MidRoll,
|
||||
FillerMode = FillerMode.Pad,
|
||||
PadToNearestMinute = 15
|
||||
}
|
||||
};
|
||||
|
||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)));
|
||||
}
|
||||
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||
new List<ProgramScheduleItem> { scheduleItem },
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
[Test]
|
||||
public void CalculateEndTimeWithFiller_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>());
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(
|
||||
collectionOne.MediaItems,
|
||||
new CollectionEnumeratorState());
|
||||
|
||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 45, 0, TimeSpan.FromHours(-5)));
|
||||
}
|
||||
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
|
||||
|
||||
[Test]
|
||||
public void CalculateEndTimeWithFiller_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>());
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators = CollectionEnumerators(
|
||||
scheduleItem,
|
||||
enumerator);
|
||||
|
||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5)));
|
||||
}
|
||||
// too lazy to make another enumerator for the filler that we don't want
|
||||
enumerators.Add(CollectionKey.ForFillerPreset(scheduleItem.MidRollFiller), enumerator);
|
||||
|
||||
[Test]
|
||||
public void CalculateEndTimeWithFiller_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>());
|
||||
List<PlayoutItem> playoutItems = PlayoutModeSchedulerBase<ProgramScheduleItem>
|
||||
.AddFiller(
|
||||
startState,
|
||||
enumerators,
|
||||
scheduleItem,
|
||||
new PlayoutItem(),
|
||||
new List<MediaChapter> { new() });
|
||||
|
||||
result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CalculateEndTimeWithFiller_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)));
|
||||
playoutItems.Count.Should().Be(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) =>
|
||||
|
||||
@@ -310,7 +310,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
return itemStartTime + totalDuration;
|
||||
}
|
||||
|
||||
protected List<PlayoutItem> AddFiller(
|
||||
internal static List<PlayoutItem> AddFiller(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
|
||||
ProgramScheduleItem scheduleItem,
|
||||
@@ -333,7 +333,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
}
|
||||
|
||||
List<MediaChapter> effectiveChapters = chapters;
|
||||
if (allFiller.All(fp => fp.FillerKind != FillerKind.MidRoll))
|
||||
if (allFiller.All(fp => fp.FillerKind != FillerKind.MidRoll) || effectiveChapters.Count <= 1)
|
||||
{
|
||||
effectiveChapters = new List<MediaChapter>();
|
||||
}
|
||||
@@ -356,7 +356,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
}
|
||||
}
|
||||
|
||||
if (!effectiveChapters.Any())
|
||||
if (effectiveChapters.Count <= 1)
|
||||
{
|
||||
result.Add(playoutItem);
|
||||
}
|
||||
@@ -493,7 +493,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
? remainingToFill
|
||||
: remainingToFill / (effectiveChapters.Count - 1);
|
||||
TimeSpan filled = TimeSpan.Zero;
|
||||
for (var i = 0; i < chapters.Count; i++)
|
||||
for (var i = 0; i < effectiveChapters.Count; i++)
|
||||
{
|
||||
result.Add(playoutItem.ForChapter(effectiveChapters[i]));
|
||||
if (i < effectiveChapters.Count - 1)
|
||||
@@ -650,7 +650,7 @@ public abstract class PlayoutModeSchedulerBase<T> : IPlayoutModeScheduler<T> whe
|
||||
return result;
|
||||
}
|
||||
|
||||
private Option<PlayoutItem> FallbackFillerForPad(
|
||||
private static Option<PlayoutItem> FallbackFillerForPad(
|
||||
PlayoutBuilderState playoutBuilderState,
|
||||
Dictionary<CollectionKey, IMediaCollectionEnumerator> enumerators,
|
||||
ProgramScheduleItem scheduleItem,
|
||||
|
||||
Reference in New Issue
Block a user