Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7ebb32a1d | ||
|
|
9ea4459988 | ||
|
|
745b03af73 | ||
|
|
a62c4ecfcf | ||
|
|
c48f0a7d51 | ||
|
|
f2c105174b |
@@ -73,13 +73,12 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
private Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) =>
|
||||
Optional(createChannel.PreferredLanguageCode)
|
||||
Optional(createChannel.PreferredLanguageCode ?? string.Empty)
|
||||
.Filter(
|
||||
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToValidation<BaseError>("Preferred language code is invalid");
|
||||
|
||||
|
||||
private async Task<Validation<BaseError, string>> ValidateNumber(CreateChannel createChannel)
|
||||
{
|
||||
Option<Channel> maybeExistingChannel = await _channelRepository.GetByNumber(createChannel.Number);
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
}
|
||||
|
||||
private Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
|
||||
Optional(updateChannel.PreferredLanguageCode)
|
||||
Optional(updateChannel.PreferredLanguageCode ?? string.Empty)
|
||||
.Filter(
|
||||
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
||||
|
||||
@@ -16,5 +16,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
int? MediaItemId,
|
||||
int? MultipleCount,
|
||||
TimeSpan? PlayoutDuration,
|
||||
bool? OfflineTail) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
|
||||
bool? OfflineTail,
|
||||
string CustomTitle) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
int? MultipleCount { get; }
|
||||
TimeSpan? PlayoutDuration { get; }
|
||||
bool? OfflineTail { get; }
|
||||
string CustomTitle { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
StartTime = item.StartTime,
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId
|
||||
MediaItemId = item.MediaItemId,
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
PlayoutMode.One => new ProgramScheduleItemOne
|
||||
{
|
||||
@@ -109,7 +110,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
StartTime = item.StartTime,
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId
|
||||
MediaItemId = item.MediaItemId,
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
PlayoutMode.Multiple => new ProgramScheduleItemMultiple
|
||||
{
|
||||
@@ -119,7 +121,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId,
|
||||
Count = item.MultipleCount.GetValueOrDefault()
|
||||
Count = item.MultipleCount.GetValueOrDefault(),
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
PlayoutMode.Duration => new ProgramScheduleItemDuration
|
||||
{
|
||||
@@ -130,7 +133,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId,
|
||||
PlayoutDuration = item.PlayoutDuration.GetValueOrDefault(),
|
||||
OfflineTail = item.OfflineTail.GetValueOrDefault()
|
||||
OfflineTail = item.OfflineTail.GetValueOrDefault(),
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
|
||||
};
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
int? MediaItemId,
|
||||
int? MultipleCount,
|
||||
TimeSpan? PlayoutDuration,
|
||||
bool? OfflineTail) : IProgramScheduleItemRequest;
|
||||
bool? OfflineTail,
|
||||
string CustomTitle) : IProgramScheduleItemRequest;
|
||||
|
||||
public record ReplaceProgramScheduleItems
|
||||
(int ProgramScheduleId, List<ReplaceProgramScheduleItem> Items) : IRequest<
|
||||
|
||||
@@ -28,7 +28,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
_ => null
|
||||
},
|
||||
duration.PlayoutDuration,
|
||||
duration.OfflineTail),
|
||||
duration.OfflineTail,
|
||||
duration.CustomTitle),
|
||||
ProgramScheduleItemFlood flood =>
|
||||
new ProgramScheduleItemFloodViewModel(
|
||||
flood.Id,
|
||||
@@ -44,7 +45,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
Show show => MediaItems.Mapper.ProjectToViewModel(show),
|
||||
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
||||
_ => null
|
||||
}),
|
||||
},
|
||||
flood.CustomTitle),
|
||||
ProgramScheduleItemMultiple multiple =>
|
||||
new ProgramScheduleItemMultipleViewModel(
|
||||
multiple.Id,
|
||||
@@ -61,7 +63,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
||||
_ => null
|
||||
},
|
||||
multiple.Count),
|
||||
multiple.Count,
|
||||
multiple.CustomTitle),
|
||||
ProgramScheduleItemOne one =>
|
||||
new ProgramScheduleItemOneViewModel(
|
||||
one.Id,
|
||||
@@ -77,7 +80,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
Show show => MediaItems.Mapper.ProjectToViewModel(show),
|
||||
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
||||
_ => null
|
||||
}),
|
||||
},
|
||||
one.CustomTitle),
|
||||
_ => throw new NotSupportedException(
|
||||
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
|
||||
};
|
||||
|
||||
@@ -16,7 +16,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
TimeSpan playoutDuration,
|
||||
bool offlineTail) : base(
|
||||
bool offlineTail,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -24,7 +25,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.Duration,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem)
|
||||
mediaItem,
|
||||
customTitle)
|
||||
{
|
||||
PlayoutDuration = playoutDuration;
|
||||
OfflineTail = offlineTail;
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
TimeSpan? startTime,
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem) : base(
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -22,7 +23,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.Flood,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem)
|
||||
mediaItem,
|
||||
customTitle)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
int count) : base(
|
||||
int count,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -23,7 +24,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.Multiple,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem) =>
|
||||
mediaItem,
|
||||
customTitle) =>
|
||||
Count = count;
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
TimeSpan? startTime,
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem) : base(
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -22,7 +23,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.One,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem)
|
||||
mediaItem,
|
||||
customTitle)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode PlayoutMode,
|
||||
ProgramScheduleItemCollectionType CollectionType,
|
||||
MediaCollectionViewModel Collection,
|
||||
NamedMediaItemViewModel MediaItem)
|
||||
NamedMediaItemViewModel MediaItem,
|
||||
string CustomTitle)
|
||||
{
|
||||
public string Name => CollectionType switch
|
||||
{
|
||||
|
||||
@@ -56,8 +56,8 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
|
||||
public Task<byte[]> ReadAllBytes(string path) => TestBytes.AsTask();
|
||||
|
||||
public Unit CopyFile(string source, string destination) =>
|
||||
Unit.Default;
|
||||
public Task<Either<BaseError, Unit>> CopyFile(string source, string destination) =>
|
||||
Task.FromResult(Right<BaseError, Unit>(Unit.Default));
|
||||
|
||||
private static List<DirectoryInfo> Split(DirectoryInfo path)
|
||||
{
|
||||
|
||||
@@ -610,6 +610,190 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
result.Items[5].MediaItemId.Should().Be(4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Alternating_MultipleContent_Should_Maintain_Counts()
|
||||
{
|
||||
var collectionOne = new Collection
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Multiple Items 1",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var collectionTwo = new Collection
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Multiple Items 2",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var fakeRepository = new FakeMediaCollectionRepository(
|
||||
Map(
|
||||
(collectionOne.Id, collectionOne.MediaItems.ToList()),
|
||||
(collectionTwo.Id, collectionTwo.MediaItems.ToList())));
|
||||
|
||||
var items = new List<ProgramScheduleItem>
|
||||
{
|
||||
new ProgramScheduleItemMultiple
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
Count = 3
|
||||
},
|
||||
new ProgramScheduleItemMultiple
|
||||
{
|
||||
Id = 2,
|
||||
Index = 2,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id,
|
||||
StartTime = null,
|
||||
Count = 3
|
||||
}
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
ProgramSchedule = new ProgramSchedule
|
||||
{
|
||||
Items = items,
|
||||
MediaCollectionPlaybackOrder = PlaybackOrder.Chronological
|
||||
},
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
Anchor = new PlayoutAnchor
|
||||
{
|
||||
NextStart = HoursAfterMidnight(1).UtcDateTime,
|
||||
NextScheduleItem = items[0],
|
||||
NextScheduleItemId = 1,
|
||||
MultipleRemaining = 2
|
||||
}
|
||||
};
|
||||
|
||||
var televisionRepo = new FakeTelevisionRepository();
|
||||
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
|
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(0);
|
||||
DateTimeOffset finish = start + TimeSpan.FromHours(5);
|
||||
|
||||
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
|
||||
|
||||
result.Items.Count.Should().Be(4);
|
||||
|
||||
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1));
|
||||
result.Items[0].MediaItemId.Should().Be(1);
|
||||
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2));
|
||||
result.Items[1].MediaItemId.Should().Be(1);
|
||||
|
||||
result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
|
||||
result.Items[2].MediaItemId.Should().Be(2);
|
||||
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
|
||||
result.Items[3].MediaItemId.Should().Be(2);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[1]);
|
||||
result.Anchor.MultipleRemaining.Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Alternating_Duration_Should_Maintain_Duration()
|
||||
{
|
||||
var collectionOne = new Collection
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Duration Items 1",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var collectionTwo = new Collection
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Duration Items 2",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var fakeRepository = new FakeMediaCollectionRepository(
|
||||
Map(
|
||||
(collectionOne.Id, collectionOne.MediaItems.ToList()),
|
||||
(collectionTwo.Id, collectionTwo.MediaItems.ToList())));
|
||||
|
||||
var items = new List<ProgramScheduleItem>
|
||||
{
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
OfflineTail = false
|
||||
},
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 2,
|
||||
Index = 2,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
OfflineTail = false
|
||||
}
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
ProgramSchedule = new ProgramSchedule
|
||||
{
|
||||
Items = items,
|
||||
MediaCollectionPlaybackOrder = PlaybackOrder.Chronological
|
||||
},
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
Anchor = new PlayoutAnchor
|
||||
{
|
||||
NextStart = HoursAfterMidnight(1).UtcDateTime,
|
||||
NextScheduleItem = items[0],
|
||||
NextScheduleItemId = 1,
|
||||
DurationFinish = HoursAfterMidnight(3).UtcDateTime
|
||||
}
|
||||
};
|
||||
|
||||
var televisionRepo = new FakeTelevisionRepository();
|
||||
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
|
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(0);
|
||||
DateTimeOffset finish = start + TimeSpan.FromHours(5);
|
||||
|
||||
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
|
||||
|
||||
result.Items.Count.Should().Be(4);
|
||||
|
||||
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1));
|
||||
result.Items[0].MediaItemId.Should().Be(1);
|
||||
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2));
|
||||
result.Items[1].MediaItemId.Should().Be(1);
|
||||
|
||||
result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
|
||||
result.Items[2].MediaItemId.Should().Be(2);
|
||||
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
|
||||
result.Items[3].MediaItemId.Should().Be(2);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[1]);
|
||||
result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime);
|
||||
}
|
||||
|
||||
private static DateTimeOffset HoursAfterMidnight(int hours)
|
||||
{
|
||||
DateTimeOffset now = DateTimeOffset.Now;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
@@ -9,7 +11,13 @@ namespace ErsatzTV.Core.Domain
|
||||
public ProgramScheduleItem NextScheduleItem { get; set; }
|
||||
|
||||
public DateTime NextStart { get; set; }
|
||||
public int? MultipleRemaining { get; set; }
|
||||
public DateTime? DurationFinish { get; set; }
|
||||
|
||||
public DateTimeOffset NextStartOffset => new DateTimeOffset(NextStart, TimeSpan.Zero).ToLocalTime();
|
||||
|
||||
public Option<DateTimeOffset> DurationFinishOffset =>
|
||||
Optional(DurationFinish)
|
||||
.Map(durationFinish => new DateTimeOffset(durationFinish, TimeSpan.Zero).ToLocalTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace ErsatzTV.Core.Domain
|
||||
public MediaItem MediaItem { get; set; }
|
||||
public DateTime Start { get; set; }
|
||||
public DateTime Finish { get; set; }
|
||||
public string CustomTitle { get; set; }
|
||||
public bool CustomGroup { get; set; }
|
||||
public int PlayoutId { get; set; }
|
||||
public Playout Playout { get; set; }
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Domain
|
||||
public StartType StartType => StartTime.HasValue ? StartType.Fixed : StartType.Dynamic;
|
||||
public TimeSpan? StartTime { get; set; }
|
||||
public ProgramScheduleItemCollectionType CollectionType { get; set; }
|
||||
public string CustomTitle { get; set; }
|
||||
public int ProgramScheduleId { get; set; }
|
||||
public ProgramSchedule ProgramSchedule { get; set; }
|
||||
public int? CollectionId { get; set; }
|
||||
|
||||
@@ -331,8 +331,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
|
||||
public FFmpegProcessBuilder WithFilterComplex(int videoStreamIndex, int audioStreamIndex)
|
||||
{
|
||||
var videoLabel = $"0:v:{videoStreamIndex}";
|
||||
var audioLabel = $"0:a:{audioStreamIndex}";
|
||||
var videoLabel = $"0:{videoStreamIndex}";
|
||||
var audioLabel = $"0:{audioStreamIndex}";
|
||||
|
||||
Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build(videoStreamIndex, audioStreamIndex);
|
||||
maybeFilter.IfSome(
|
||||
|
||||
@@ -8,6 +8,6 @@ namespace ErsatzTV.Core.Interfaces.Images
|
||||
{
|
||||
Task<Either<BaseError, byte[]>> ResizeImage(byte[] imageBuffer, int height);
|
||||
Task<Either<BaseError, string>> SaveArtworkToCache(byte[] imageBuffer, ArtworkKind artworkKind);
|
||||
string CopyArtworkToCache(string path, ArtworkKind artworkKind);
|
||||
Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
IEnumerable<string> ListFiles(string folder);
|
||||
bool FileExists(string path);
|
||||
Task<byte[]> ReadAllBytes(string path);
|
||||
Unit CopyFile(string source, string destination);
|
||||
Task<Either<BaseError, Unit>> CopyFile(string source, string destination);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,49 +57,36 @@ namespace ErsatzTV.Core.Iptv
|
||||
|
||||
foreach (Channel channel in _channels.OrderBy(c => c.Number))
|
||||
{
|
||||
foreach (PlayoutItem playoutItem in channel.Playouts.Collect(p => p.Items).OrderBy(i => i.Start))
|
||||
var sorted = channel.Playouts.Collect(p => p.Items).OrderBy(x => x.Start).ToList();
|
||||
var i = 0;
|
||||
while (i < sorted.Count)
|
||||
{
|
||||
string start = playoutItem.StartOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
|
||||
string stop = playoutItem.FinishOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
|
||||
PlayoutItem startItem = sorted[i];
|
||||
bool hasCustomTitle = !string.IsNullOrWhiteSpace(startItem.CustomTitle);
|
||||
|
||||
string title = playoutItem.MediaItem switch
|
||||
int finishIndex = i;
|
||||
while (hasCustomTitle && finishIndex + 1 < sorted.Count && sorted[finishIndex + 1].CustomGroup)
|
||||
{
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title ?? string.Empty)
|
||||
.IfNone("[unknown movie]"),
|
||||
Episode e => e.Season.Show.ShowMetadata.HeadOrNone().Map(em => em.Title ?? string.Empty)
|
||||
.IfNone("[unknown show]"),
|
||||
_ => "[unknown]"
|
||||
};
|
||||
finishIndex++;
|
||||
}
|
||||
|
||||
string subtitle = playoutItem.MediaItem switch
|
||||
{
|
||||
Episode e => e.EpisodeMetadata.HeadOrNone().Match(
|
||||
em => em.Title ?? string.Empty,
|
||||
() => string.Empty),
|
||||
_ => string.Empty
|
||||
};
|
||||
PlayoutItem finishItem = sorted[finishIndex];
|
||||
i = finishIndex;
|
||||
|
||||
string description = playoutItem.MediaItem switch
|
||||
{
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Plot ?? string.Empty).IfNone(string.Empty),
|
||||
Episode e => e.EpisodeMetadata.HeadOrNone().Map(em => em.Plot ?? string.Empty)
|
||||
.IfNone(string.Empty),
|
||||
_ => string.Empty
|
||||
};
|
||||
string start = startItem.StartOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
|
||||
string stop = finishItem.FinishOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
|
||||
|
||||
string contentRating = playoutItem.MediaItem switch
|
||||
{
|
||||
// TODO: re-implement content rating
|
||||
// Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.ContentRating).IfNone(string.Empty),
|
||||
_ => string.Empty
|
||||
};
|
||||
string title = GetTitle(startItem);
|
||||
string subtitle = GetSubtitle(startItem);
|
||||
string description = GetDescription(startItem);
|
||||
string contentRating = string.Empty;
|
||||
|
||||
xml.WriteStartElement("programme");
|
||||
xml.WriteAttributeString("start", start);
|
||||
xml.WriteAttributeString("stop", stop);
|
||||
xml.WriteAttributeString("channel", channel.Number);
|
||||
|
||||
if (playoutItem.MediaItem is Movie movie)
|
||||
if (!hasCustomTitle && startItem.MediaItem is Movie movie)
|
||||
{
|
||||
xml.WriteStartElement("category");
|
||||
xml.WriteAttributeString("lang", "en");
|
||||
@@ -150,7 +137,7 @@ namespace ErsatzTV.Core.Iptv
|
||||
xml.WriteStartElement("previously-shown");
|
||||
xml.WriteEndElement(); // previously-shown
|
||||
|
||||
if (playoutItem.MediaItem is Episode episode)
|
||||
if (!hasCustomTitle && startItem.MediaItem is Episode episode)
|
||||
{
|
||||
Option<ShowMetadata> maybeMetadata =
|
||||
Optional(episode.Season?.Show?.ShowMetadata.HeadOrNone()).Flatten();
|
||||
@@ -209,6 +196,8 @@ namespace ErsatzTV.Core.Iptv
|
||||
}
|
||||
|
||||
xml.WriteEndElement(); // programme
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,5 +207,54 @@ namespace ErsatzTV.Core.Iptv
|
||||
xml.Flush();
|
||||
return Encoding.UTF8.GetString(ms.ToArray());
|
||||
}
|
||||
|
||||
private static string GetTitle(PlayoutItem playoutItem)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))
|
||||
{
|
||||
return playoutItem.CustomTitle;
|
||||
}
|
||||
|
||||
return playoutItem.MediaItem switch
|
||||
{
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title ?? string.Empty)
|
||||
.IfNone("[unknown movie]"),
|
||||
Episode e => e.Season.Show.ShowMetadata.HeadOrNone().Map(em => em.Title ?? string.Empty)
|
||||
.IfNone("[unknown show]"),
|
||||
_ => "[unknown]"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetSubtitle(PlayoutItem playoutItem)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return playoutItem.MediaItem switch
|
||||
{
|
||||
Episode e => e.EpisodeMetadata.HeadOrNone().Match(
|
||||
em => em.Title ?? string.Empty,
|
||||
() => string.Empty),
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetDescription(PlayoutItem playoutItem)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return playoutItem.MediaItem switch
|
||||
{
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Plot ?? string.Empty).IfNone(string.Empty),
|
||||
Episode e => e.EpisodeMetadata.HeadOrNone().Map(em => em.Plot ?? string.Empty)
|
||||
.IfNone(string.Empty),
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,17 +36,26 @@ namespace ErsatzTV.Core.Metadata
|
||||
public bool FileExists(string path) => File.Exists(path);
|
||||
public Task<byte[]> ReadAllBytes(string path) => File.ReadAllBytesAsync(path);
|
||||
|
||||
public Unit CopyFile(string source, string destination)
|
||||
public async Task<Either<BaseError, Unit>> CopyFile(string source, string destination)
|
||||
{
|
||||
string directory = Path.GetDirectoryName(destination) ?? string.Empty;
|
||||
if (!Directory.Exists(directory))
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
string directory = Path.GetDirectoryName(destination) ?? string.Empty;
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
await using FileStream sourceStream = File.OpenRead(source);
|
||||
await using FileStream destinationStream = File.Create(destination);
|
||||
await sourceStream.CopyToAsync(destinationStream);
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
|
||||
File.Copy(source, destination, true);
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,30 +120,47 @@ namespace ErsatzTV.Core.Metadata
|
||||
|
||||
if (shouldRefresh)
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile);
|
||||
string cacheName = _imageCache.CopyArtworkToCache(artworkFile, artworkKind);
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile);
|
||||
Either<BaseError, string> maybeCacheName =
|
||||
await _imageCache.CopyArtworkToCache(artworkFile, artworkKind);
|
||||
|
||||
await maybeArtwork.Match(
|
||||
async artwork =>
|
||||
{
|
||||
artwork.Path = cacheName;
|
||||
artwork.DateUpdated = lastWriteTime;
|
||||
await _metadataRepository.UpdateArtworkPath(artwork);
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
var artwork = new Artwork
|
||||
return await maybeCacheName.Match(
|
||||
async cacheName =>
|
||||
{
|
||||
Path = cacheName,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = lastWriteTime,
|
||||
ArtworkKind = artworkKind
|
||||
};
|
||||
metadata.Artwork.Add(artwork);
|
||||
await _metadataRepository.AddArtwork(metadata, artwork);
|
||||
});
|
||||
await maybeArtwork.Match(
|
||||
async artwork =>
|
||||
{
|
||||
artwork.Path = cacheName;
|
||||
artwork.DateUpdated = lastWriteTime;
|
||||
await _metadataRepository.UpdateArtworkPath(artwork);
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
var artwork = new Artwork
|
||||
{
|
||||
Path = cacheName,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = lastWriteTime,
|
||||
ArtworkKind = artworkKind
|
||||
};
|
||||
metadata.Artwork.Add(artwork);
|
||||
await _metadataRepository.AddArtwork(metadata, artwork);
|
||||
});
|
||||
|
||||
return true;
|
||||
return true;
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogDebug("Failed to cache artwork from {Path}: {Error}", artworkFile, error.Value);
|
||||
return Task.FromResult(false);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error refreshing artwork");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
existing.Plot = metadata.Plot;
|
||||
existing.Tagline = metadata.Tagline;
|
||||
existing.Title = metadata.Title;
|
||||
|
||||
|
||||
if (existing.DateAdded == DateTime.MinValue)
|
||||
{
|
||||
existing.DateAdded = metadata.DateAdded;
|
||||
@@ -149,17 +149,17 @@ namespace ErsatzTV.Core.Metadata
|
||||
async existing =>
|
||||
{
|
||||
var updated = false;
|
||||
|
||||
|
||||
existing.Outline = metadata.Outline;
|
||||
existing.Plot = metadata.Plot;
|
||||
existing.Tagline = metadata.Tagline;
|
||||
existing.Title = metadata.Title;
|
||||
|
||||
|
||||
if (existing.DateAdded == DateTime.MinValue)
|
||||
{
|
||||
existing.DateAdded = metadata.DateAdded;
|
||||
}
|
||||
|
||||
|
||||
existing.DateUpdated = metadata.DateUpdated;
|
||||
existing.MetadataKind = metadata.MetadataKind;
|
||||
existing.OriginalTitle = metadata.OriginalTitle;
|
||||
@@ -254,7 +254,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
existing.Plot = metadata.Plot;
|
||||
existing.Tagline = metadata.Tagline;
|
||||
existing.Title = metadata.Title;
|
||||
|
||||
|
||||
if (existing.DateAdded == DateTime.MinValue)
|
||||
{
|
||||
existing.DateAdded = metadata.DateAdded;
|
||||
|
||||
@@ -80,9 +80,22 @@ namespace ErsatzTV.Core.Metadata
|
||||
await _searchIndex.UpdateItems(new List<MediaItem> { result.Item });
|
||||
}
|
||||
|
||||
await ScanSeasons(libraryPath, ffprobePath, result.Item, showFolder, lastScan);
|
||||
await ScanSeasons(
|
||||
libraryPath,
|
||||
ffprobePath,
|
||||
result.Item,
|
||||
showFolder,
|
||||
// force scanning all folders if we're adding a new show
|
||||
result.IsAdded ? DateTimeOffset.MinValue : lastScan);
|
||||
},
|
||||
_ => Task.FromResult(Unit.Default));
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Error processing show in folder {Folder}: {Error}",
|
||||
showFolder,
|
||||
error.Value);
|
||||
return Task.FromResult(Unit.Default);
|
||||
});
|
||||
}
|
||||
|
||||
foreach (string path in await _televisionRepository.FindEpisodePaths(libraryPath))
|
||||
@@ -132,7 +145,14 @@ namespace ErsatzTV.Core.Metadata
|
||||
|
||||
await maybeSeason.Match(
|
||||
season => ScanEpisodes(libraryPath, ffprobePath, season, seasonFolder, lastScan),
|
||||
_ => Task.FromResult(Unit.Default));
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Error processing season in folder {Folder}: {Error}",
|
||||
seasonFolder,
|
||||
error.Value);
|
||||
return Task.FromResult(Unit.Default);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -145,8 +145,12 @@ namespace ErsatzTV.Core.Scheduling
|
||||
// start with the previously-decided schedule item
|
||||
int index = sortedScheduleItems.IndexOf(startAnchor.NextScheduleItem);
|
||||
|
||||
Option<int> multipleRemaining = None;
|
||||
Option<DateTimeOffset> durationFinish = None;
|
||||
// start with the previous multiple/duration states
|
||||
Option<int> multipleRemaining = Optional(startAnchor.MultipleRemaining);
|
||||
Option<DateTimeOffset> durationFinish = startAnchor.DurationFinishOffset;
|
||||
|
||||
bool customGroup = multipleRemaining.IsSome || durationFinish.IsSome;
|
||||
|
||||
// loop until we're done filling the desired amount of time
|
||||
while (currentTime < playoutFinish)
|
||||
{
|
||||
@@ -183,9 +187,15 @@ namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
MediaItemId = mediaItem.Id,
|
||||
Start = itemStartTime.UtcDateTime,
|
||||
Finish = itemStartTime.UtcDateTime + version.Duration
|
||||
Finish = itemStartTime.UtcDateTime + version.Duration,
|
||||
CustomGroup = customGroup
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(scheduleItem.CustomTitle))
|
||||
{
|
||||
playoutItem.CustomTitle = scheduleItem.CustomTitle;
|
||||
}
|
||||
|
||||
currentTime = itemStartTime + version.Duration;
|
||||
enumerator.MoveNext();
|
||||
|
||||
@@ -199,11 +209,13 @@ namespace ErsatzTV.Core.Scheduling
|
||||
"Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
"One");
|
||||
index++;
|
||||
customGroup = false;
|
||||
break;
|
||||
case ProgramScheduleItemMultiple multiple:
|
||||
if (multipleRemaining.IsNone)
|
||||
{
|
||||
multipleRemaining = multiple.Count;
|
||||
customGroup = true;
|
||||
}
|
||||
|
||||
multipleRemaining = multipleRemaining.Map(i => i - 1);
|
||||
@@ -214,6 +226,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
"Multiple");
|
||||
index++;
|
||||
multipleRemaining = None;
|
||||
customGroup = false;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -221,6 +234,8 @@ namespace ErsatzTV.Core.Scheduling
|
||||
enumerator.Current.Do(
|
||||
peekMediaItem =>
|
||||
{
|
||||
customGroup = true;
|
||||
|
||||
MediaVersion peekVersion = peekMediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
@@ -247,6 +262,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
"Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
"Flood");
|
||||
index++;
|
||||
customGroup = false;
|
||||
}
|
||||
});
|
||||
break;
|
||||
@@ -265,6 +281,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
if (durationFinish.IsNone)
|
||||
{
|
||||
durationFinish = itemStartTime + duration.PlayoutDuration;
|
||||
customGroup = true;
|
||||
}
|
||||
|
||||
bool willNotFinishInTime =
|
||||
@@ -277,6 +294,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
"Advancing to next schedule item after playout mode {PlayoutMode}",
|
||||
"Duration");
|
||||
index++;
|
||||
customGroup = false;
|
||||
|
||||
if (duration.OfflineTail)
|
||||
{
|
||||
@@ -298,7 +316,9 @@ namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
NextScheduleItem = nextScheduleItem,
|
||||
NextScheduleItemId = nextScheduleItem.Id,
|
||||
NextStart = GetStartTimeAfter(nextScheduleItem, currentTime).UtcDateTime
|
||||
NextStart = GetStartTimeAfter(nextScheduleItem, currentTime).UtcDateTime,
|
||||
MultipleRemaining = multipleRemaining.IsSome ? multipleRemaining.ValueUnsafe() : null,
|
||||
DurationFinish = durationFinish.IsSome ? durationFinish.ValueUnsafe().UtcDateTime : null
|
||||
};
|
||||
|
||||
// build program schedule anchors
|
||||
|
||||
@@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
@@ -18,10 +19,15 @@ namespace ErsatzTV.Infrastructure.Images
|
||||
{
|
||||
private static readonly SHA1CryptoServiceProvider Crypto;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILogger<ImageCache> _logger;
|
||||
|
||||
static ImageCache() => Crypto = new SHA1CryptoServiceProvider();
|
||||
|
||||
public ImageCache(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem;
|
||||
public ImageCache(ILocalFileSystem localFileSystem, ILogger<ImageCache> logger)
|
||||
{
|
||||
_localFileSystem = localFileSystem;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, byte[]>> ResizeImage(byte[] imageBuffer, int height)
|
||||
{
|
||||
@@ -75,24 +81,32 @@ namespace ErsatzTV.Infrastructure.Images
|
||||
}
|
||||
}
|
||||
|
||||
public string CopyArtworkToCache(string path, ArtworkKind artworkKind)
|
||||
public async Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind)
|
||||
{
|
||||
var filenameKey = $"{path}:{_localFileSystem.GetLastWriteTime(path).ToFileTimeUtc()}";
|
||||
byte[] hash = Crypto.ComputeHash(Encoding.UTF8.GetBytes(filenameKey));
|
||||
string hex = BitConverter.ToString(hash).Replace("-", string.Empty);
|
||||
string subfolder = hex.Substring(0, 2);
|
||||
string baseFolder = artworkKind switch
|
||||
try
|
||||
{
|
||||
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
|
||||
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
|
||||
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
|
||||
ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder),
|
||||
_ => FileSystemLayout.LegacyImageCacheFolder
|
||||
};
|
||||
string target = Path.Combine(baseFolder, hex);
|
||||
_localFileSystem.CopyFile(path, target);
|
||||
|
||||
return hex;
|
||||
var filenameKey = $"{path}:{_localFileSystem.GetLastWriteTime(path).ToFileTimeUtc()}";
|
||||
byte[] hash = Crypto.ComputeHash(Encoding.UTF8.GetBytes(filenameKey));
|
||||
string hex = BitConverter.ToString(hash).Replace("-", string.Empty);
|
||||
string subfolder = hex.Substring(0, 2);
|
||||
string baseFolder = artworkKind switch
|
||||
{
|
||||
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
|
||||
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
|
||||
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
|
||||
ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder),
|
||||
_ => FileSystemLayout.LegacyImageCacheFolder
|
||||
};
|
||||
string target = Path.Combine(baseFolder, hex);
|
||||
Either<BaseError, Unit> maybeResult = await _localFileSystem.CopyFile(path, target);
|
||||
return maybeResult.Match<Either<BaseError, string>>(
|
||||
_ => hex,
|
||||
error => error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1820
File diff suppressed because it is too large
Load Diff
+34
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_PlayoutAnchor_DurationMultiple : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
"Anchor_DurationFinish",
|
||||
"Playout",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"Anchor_MultipleRemaining",
|
||||
"Playout",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
"Anchor_DurationFinish",
|
||||
"Playout");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"Anchor_MultipleRemaining",
|
||||
"Playout");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1829
File diff suppressed because it is too large
Load Diff
+44
@@ -0,0 +1,44 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_ProgramScheduleItem_CustomTitle : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
"CustomTitle",
|
||||
"ProgramScheduleItem",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
"CustomGroup",
|
||||
"PlayoutItem",
|
||||
"INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
"CustomTitle",
|
||||
"PlayoutItem",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
"CustomTitle",
|
||||
"ProgramScheduleItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"CustomGroup",
|
||||
"PlayoutItem");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"CustomTitle",
|
||||
"PlayoutItem");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -611,6 +611,12 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("CustomGroup")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CustomTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Finish")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -752,6 +758,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int>("CollectionType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CustomTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Index")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1412,6 +1421,12 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b1.Property<int>("PlayoutId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<DateTime?>("DurationFinish")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int?>("MultipleRemaining")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("NextScheduleItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -76,17 +76,21 @@
|
||||
_messageStore.Clear();
|
||||
if (_editContext.Validate())
|
||||
{
|
||||
Seq<BaseError> errorMessage = IsEdit ?
|
||||
(await Mediator.Send(_model.ToUpdate())).LeftToSeq() :
|
||||
(await Mediator.Send(_model.ToCreate())).LeftToSeq();
|
||||
Either<BaseError, ProgramScheduleViewModel> result = IsEdit ?
|
||||
await Mediator.Send(_model.ToUpdate()) :
|
||||
await Mediator.Send(_model.ToCreate());
|
||||
|
||||
errorMessage.HeadOrNone().Match(
|
||||
result.Match(
|
||||
programSchedule =>
|
||||
{
|
||||
string destination = IsEdit ? "/schedules" : $"/schedules/{programSchedule.Id}/items";
|
||||
NavigationManager.NavigateTo(destination);
|
||||
},
|
||||
error =>
|
||||
{
|
||||
Snackbar.Add(error.Value, Severity.Error);
|
||||
Logger.LogError("Unexpected error saving schedule: {Error}", error.Value);
|
||||
},
|
||||
() => NavigationManager.NavigateTo("/schedules"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudSwitch Label="Offline Tail" @bind-Checked="@_selectedItem.OfflineTail" For="@(() => _selectedItem.OfflineTail)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
|
||||
</MudElement>
|
||||
<MudTextField Class="mt-3" Label="Custom Title" @bind-Value="@_selectedItem.CustomTitle" For="@(() => _selectedItem.CustomTitle)"/>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</EditForm>
|
||||
@@ -210,7 +211,8 @@
|
||||
PlayoutMode = item.PlayoutMode,
|
||||
CollectionType = item.CollectionType,
|
||||
Collection = item.Collection,
|
||||
MediaItem = item.MediaItem
|
||||
MediaItem = item.MediaItem,
|
||||
CustomTitle = item.CustomTitle
|
||||
};
|
||||
|
||||
switch (item)
|
||||
@@ -286,7 +288,8 @@
|
||||
item.MediaItem?.MediaItemId,
|
||||
item.MultipleCount,
|
||||
item.PlayoutDuration,
|
||||
item.PlayoutMode == PlayoutMode.Duration ? item.OfflineTail.IfNone(false) : null)).ToList();
|
||||
item.PlayoutMode == PlayoutMode.Duration ? item.OfflineTail.IfNone(false) : null,
|
||||
item.CustomTitle)).ToList();
|
||||
|
||||
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceProgramScheduleItems(Id, items)).Map(e => e.LeftToSeq());
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
|
||||
{
|
||||
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, SeasonId, null, null, null));
|
||||
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, SeasonId, null, null, null, null));
|
||||
NavigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule)
|
||||
{
|
||||
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, ShowId, null, null, null));
|
||||
await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, ShowId, null, null, null, null));
|
||||
NavigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@ namespace ErsatzTV.ViewModels
|
||||
set => _offlineTail = value;
|
||||
}
|
||||
|
||||
public string CustomTitle { get; set; }
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
[NotifyPropertyChangedInvocator]
|
||||
|
||||
Reference in New Issue
Block a user