Browse Source

schedule editing and playouts work again

pull/26/head
Jason Dove 6 years ago
parent
commit
3f143d87a7
  1. 17
      ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs
  2. 11
      ErsatzTV.Application/MediaCollections/Mapper.cs
  3. 5
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  4. 5
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  5. 50
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  6. 5
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  7. 9
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs
  8. 42
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  9. 9
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs
  10. 11
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs
  11. 9
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs
  12. 11
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs
  13. 16
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs
  14. 3
      ErsatzTV.Application/Television/Mapper.cs
  15. 7
      ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs
  16. 25
      ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs
  17. 7
      ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs
  18. 24
      ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs
  19. 2
      ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs
  20. 2
      ErsatzTV.Application/Television/TelevisionShowViewModel.cs
  21. 78
      ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs
  22. 15
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  23. 5
      ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs
  24. 7
      ErsatzTV.Core/Domain/ProgramScheduleItem.cs
  25. 9
      ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs
  26. 4
      ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs
  27. 90
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  28. 9
      ErsatzTV.Core/Scheduling/TelevisionSeasonMediaCollection.cs
  29. 9
      ErsatzTV.Core/Scheduling/TelevisionShowMediaCollection.cs
  30. 9
      ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs
  31. 19
      ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemConfiguration.cs
  32. 51
      ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs
  33. 4
      ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs
  34. 8
      ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs
  35. 55
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  36. 1305
      ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.Designer.cs
  37. 209
      ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.cs
  38. 74
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  39. 1
      ErsatzTV.sln.DotSettings
  40. 2
      ErsatzTV/ErsatzTV.csproj
  41. 74
      ErsatzTV/Pages/ScheduleItemsEditor.razor
  42. 2
      ErsatzTV/Pages/Schedules.razor
  43. 1460
      ErsatzTV/Properties/Annotations.cs
  44. 49
      ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

17
ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs

@ -29,14 +29,15 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -29,14 +29,15 @@ namespace ErsatzTV.Application.MediaCollections.Commands
_mediaCollectionRepository.Add(c).Map(ProjectToViewModel);
private Task<Validation<BaseError, SimpleMediaCollection>> Validate(CreateSimpleMediaCollection request) =>
ValidateName(request).MapT(name => new SimpleMediaCollection
{
Name = name,
Movies = new List<MovieMediaItem>(),
TelevisionShows = new List<TelevisionShow>(),
TelevisionEpisodes = new List<TelevisionEpisodeMediaItem>(),
TelevisionSeasons = new List<TelevisionSeason>()
});
ValidateName(request).MapT(
name => new SimpleMediaCollection
{
Name = name,
Movies = new List<MovieMediaItem>(),
TelevisionShows = new List<TelevisionShow>(),
TelevisionEpisodes = new List<TelevisionEpisodeMediaItem>(),
TelevisionSeasons = new List<TelevisionSeason>()
});
private async Task<Validation<BaseError, string>> ValidateName(CreateSimpleMediaCollection createCollection)
{

11
ErsatzTV.Application/MediaCollections/Mapper.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaCollections
{
@ -7,13 +6,5 @@ namespace ErsatzTV.Application.MediaCollections @@ -7,13 +6,5 @@ namespace ErsatzTV.Application.MediaCollections
{
internal static MediaCollectionViewModel ProjectToViewModel(MediaCollection mediaCollection) =>
new(mediaCollection.Id, mediaCollection.Name);
internal static MediaCollectionSummaryViewModel ProjectToViewModel(
MediaCollectionSummary mediaCollectionSummary) =>
new(
mediaCollectionSummary.Id,
mediaCollectionSummary.Name,
mediaCollectionSummary.ItemCount,
mediaCollectionSummary.IsSimple);
}
}

5
ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs

@ -11,7 +11,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -11,7 +11,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
StartType StartType,
TimeSpan? StartTime,
PlayoutMode PlayoutMode,
int MediaCollectionId,
ProgramScheduleItemCollectionType CollectionType,
int? MediaCollectionId,
int? TelevisionShowId,
int? TelevisionSeasonId,
int? MultipleCount,
TimeSpan? PlayoutDuration,
bool? OfflineTail) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;

5
ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs

@ -6,7 +6,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -6,7 +6,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
public interface IProgramScheduleItemRequest
{
TimeSpan? StartTime { get; }
int MediaCollectionId { get; }
ProgramScheduleItemCollectionType CollectionType { get; }
int? MediaCollectionId { get; }
int? TelevisionShowId { get; }
int? TelevisionSeasonId { get; }
PlayoutMode PlayoutMode { get; }
int? MultipleCount { get; }
TimeSpan? PlayoutDuration { get; }

50
ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs

@ -53,6 +53,40 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -53,6 +53,40 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
return programSchedule;
}
protected Validation<BaseError, ProgramSchedule> CollectionTypeMustBeValid(
IProgramScheduleItemRequest item,
ProgramSchedule programSchedule)
{
switch (item.CollectionType)
{
case ProgramScheduleItemCollectionType.Collection:
if (item.MediaCollectionId is null)
{
return BaseError.New("[MediaCollection] is required for collection type 'Collection'");
}
break;
case ProgramScheduleItemCollectionType.TelevisionShow:
if (item.TelevisionShowId is null)
{
return BaseError.New("[TelevisionShow] is required for collection type 'TelevisionShow'");
}
break;
case ProgramScheduleItemCollectionType.TelevisionSeason:
if (item.TelevisionSeasonId is null)
{
return BaseError.New("[TelevisionSeason] is required for collection type 'TelevisionSeason'");
}
break;
default:
return BaseError.New("[CollectionType] is invalid");
}
return programSchedule;
}
protected ProgramScheduleItem BuildItem(
ProgramSchedule programSchedule,
int index,
@ -64,21 +98,30 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -64,21 +98,30 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
MediaCollectionId = item.MediaCollectionId
CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId
},
PlayoutMode.One => new ProgramScheduleItemOne
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
MediaCollectionId = item.MediaCollectionId
CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId
},
PlayoutMode.Multiple => new ProgramScheduleItemMultiple
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId,
Count = item.MultipleCount.GetValueOrDefault()
},
PlayoutMode.Duration => new ProgramScheduleItemDuration
@ -86,7 +129,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -86,7 +129,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId,
PlayoutDuration = item.PlayoutDuration.GetValueOrDefault(),
OfflineTail = item.OfflineTail.GetValueOrDefault()
},

5
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs

@ -12,7 +12,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -12,7 +12,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
StartType StartType,
TimeSpan? StartTime,
PlayoutMode PlayoutMode,
int MediaCollectionId,
ProgramScheduleItemCollectionType CollectionType,
int? MediaCollectionId,
int? TelevisionShowId,
int? TelevisionSeasonId,
int? MultipleCount,
TimeSpan? PlayoutDuration,
bool? OfflineTail) : IProgramScheduleItemRequest;

9
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs

@ -55,12 +55,19 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -55,12 +55,19 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
private Task<Validation<BaseError, ProgramSchedule>> Validate(ReplaceProgramScheduleItems request) =>
ProgramScheduleMustExist(request.ProgramScheduleId)
.BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule));
.BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule))
.BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule));
private Validation<BaseError, ProgramSchedule> PlayoutModesMustBeValid(
ReplaceProgramScheduleItems request,
ProgramSchedule programSchedule) =>
request.Items.Map(item => PlayoutModeMustBeValid(item, programSchedule)).Sequence()
.Map(_ => programSchedule);
private Validation<BaseError, ProgramSchedule> CollectionTypesMustBeValid(
ReplaceProgramScheduleItems request,
ProgramSchedule programSchedule) =>
request.Items.Map(item => CollectionTypeMustBeValid(item, programSchedule)).Sequence()
.Map(_ => programSchedule);
}
}

42
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -17,7 +17,16 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -17,7 +17,16 @@ namespace ErsatzTV.Application.ProgramSchedules
duration.Index,
duration.StartType,
duration.StartTime,
MediaCollections.Mapper.ProjectToViewModel(duration.MediaCollection),
duration.CollectionType,
duration.MediaCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.MediaCollection)
: null,
duration.TelevisionShow != null
? Television.Mapper.ProjectToViewModel(duration.TelevisionShow)
: null,
duration.TelevisionSeason != null
? Television.Mapper.ProjectToViewModel(duration.TelevisionSeason)
: null,
duration.PlayoutDuration,
duration.OfflineTail),
ProgramScheduleItemFlood flood =>
@ -26,14 +35,32 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -26,14 +35,32 @@ namespace ErsatzTV.Application.ProgramSchedules
flood.Index,
flood.StartType,
flood.StartTime,
MediaCollections.Mapper.ProjectToViewModel(flood.MediaCollection)),
flood.CollectionType,
flood.MediaCollection != null
? MediaCollections.Mapper.ProjectToViewModel(flood.MediaCollection)
: null,
flood.TelevisionShow != null
? Television.Mapper.ProjectToViewModel(flood.TelevisionShow)
: null,
flood.TelevisionSeason != null
? Television.Mapper.ProjectToViewModel(flood.TelevisionSeason)
: null),
ProgramScheduleItemMultiple multiple =>
new ProgramScheduleItemMultipleViewModel(
multiple.Id,
multiple.Index,
multiple.StartType,
multiple.StartTime,
MediaCollections.Mapper.ProjectToViewModel(multiple.MediaCollection),
multiple.CollectionType,
multiple.MediaCollection != null
? MediaCollections.Mapper.ProjectToViewModel(multiple.MediaCollection)
: null,
multiple.TelevisionShow != null
? Television.Mapper.ProjectToViewModel(multiple.TelevisionShow)
: null,
multiple.TelevisionSeason != null
? Television.Mapper.ProjectToViewModel(multiple.TelevisionSeason)
: null,
multiple.Count),
ProgramScheduleItemOne one =>
new ProgramScheduleItemOneViewModel(
@ -41,7 +68,14 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -41,7 +68,14 @@ namespace ErsatzTV.Application.ProgramSchedules
one.Index,
one.StartType,
one.StartTime,
MediaCollections.Mapper.ProjectToViewModel(one.MediaCollection)),
one.CollectionType,
one.MediaCollection != null
? MediaCollections.Mapper.ProjectToViewModel(one.MediaCollection)
: null,
one.TelevisionShow != null ? Television.Mapper.ProjectToViewModel(one.TelevisionShow) : null,
one.TelevisionSeason != null
? Television.Mapper.ProjectToViewModel(one.TelevisionSeason)
: null),
_ => throw new NotSupportedException(
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
};

9
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules
@ -11,7 +12,10 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -11,7 +12,10 @@ namespace ErsatzTV.Application.ProgramSchedules
int index,
StartType startType,
TimeSpan? startTime,
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason,
TimeSpan playoutDuration,
bool offlineTail) : base(
id,
@ -19,7 +23,10 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -19,7 +23,10 @@ namespace ErsatzTV.Application.ProgramSchedules
startType,
startTime,
PlayoutMode.Duration,
mediaCollection)
collectionType,
mediaCollection,
televisionShow,
televisionSeason)
{
PlayoutDuration = playoutDuration;
OfflineTail = offlineTail;

11
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules
@ -11,13 +12,19 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -11,13 +12,19 @@ namespace ErsatzTV.Application.ProgramSchedules
int index,
StartType startType,
TimeSpan? startTime,
MediaCollectionViewModel mediaCollection) : base(
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason) : base(
id,
index,
startType,
startTime,
PlayoutMode.Flood,
mediaCollection)
collectionType,
mediaCollection,
televisionShow,
televisionSeason)
{
}
}

9
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules
@ -11,14 +12,20 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -11,14 +12,20 @@ namespace ErsatzTV.Application.ProgramSchedules
int index,
StartType startType,
TimeSpan? startTime,
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason,
int count) : base(
id,
index,
startType,
startTime,
PlayoutMode.Multiple,
mediaCollection) =>
collectionType,
mediaCollection,
televisionShow,
televisionSeason) =>
Count = count;
public int Count { get; }

11
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules
@ -11,13 +12,19 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -11,13 +12,19 @@ namespace ErsatzTV.Application.ProgramSchedules
int index,
StartType startType,
TimeSpan? startTime,
MediaCollectionViewModel mediaCollection) : base(
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason) : base(
id,
index,
startType,
startTime,
PlayoutMode.One,
mediaCollection)
collectionType,
mediaCollection,
televisionShow,
televisionSeason)
{
}
}

16
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules
@ -10,5 +11,18 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -10,5 +11,18 @@ namespace ErsatzTV.Application.ProgramSchedules
StartType StartType,
TimeSpan? StartTime,
PlayoutMode PlayoutMode,
MediaCollectionViewModel MediaCollection);
ProgramScheduleItemCollectionType CollectionType,
MediaCollectionViewModel MediaCollection,
TelevisionShowViewModel TelevisionShow,
TelevisionSeasonViewModel TelevisionSeason)
{
public string Name => CollectionType switch
{
ProgramScheduleItemCollectionType.Collection => MediaCollection?.Name,
ProgramScheduleItemCollectionType.TelevisionShow => $"{TelevisionShow?.Title} ({TelevisionShow?.Year})",
ProgramScheduleItemCollectionType.TelevisionSeason =>
$"{TelevisionSeason?.Title} ({TelevisionSeason?.Plot})",
_ => string.Empty
};
}
}

3
ErsatzTV.Application/Television/Mapper.cs

@ -5,10 +5,11 @@ namespace ErsatzTV.Application.Television @@ -5,10 +5,11 @@ namespace ErsatzTV.Application.Television
internal static class Mapper
{
internal static TelevisionShowViewModel ProjectToViewModel(TelevisionShow show) =>
new(show.Metadata.Title, show.Metadata.Year?.ToString(), show.Metadata.Plot, show.Poster);
new(show.Id, show.Metadata.Title, show.Metadata.Year?.ToString(), show.Metadata.Plot, show.Poster);
internal static TelevisionSeasonViewModel ProjectToViewModel(TelevisionSeason season) =>
new(
season.Id,
season.TelevisionShowId,
season.TelevisionShow.Metadata.Title,
season.TelevisionShow.Metadata.Year?.ToString(),

7
ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetAllTelevisionSeasons : IRequest<List<TelevisionSeasonViewModel>>;
}

25
ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Television.Mapper;
namespace ErsatzTV.Application.Television.Queries
{
public class
GetAllTelevisionSeasonsHandler : IRequestHandler<GetAllTelevisionSeasons, List<TelevisionSeasonViewModel>>
{
private readonly ITelevisionRepository _televisionRepository;
public GetAllTelevisionSeasonsHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public Task<List<TelevisionSeasonViewModel>> Handle(
GetAllTelevisionSeasons request,
CancellationToken cancellationToken) =>
_televisionRepository.GetAllSeasons().Map(list => list.Map(ProjectToViewModel).ToList());
}
}

7
ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetAllTelevisionShows : IRequest<List<TelevisionShowViewModel>>;
}

24
ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Television.Mapper;
namespace ErsatzTV.Application.Television.Queries
{
public class GetAllTelevisionShowsHandler : IRequestHandler<GetAllTelevisionShows, List<TelevisionShowViewModel>>
{
private readonly ITelevisionRepository _televisionRepository;
public GetAllTelevisionShowsHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public Task<List<TelevisionShowViewModel>> Handle(
GetAllTelevisionShows request,
CancellationToken cancellationToken) =>
_televisionRepository.GetAllShows().Map(list => list.Map(ProjectToViewModel).ToList());
}
}

2
ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionSeasonViewModel(int ShowId, string Title, string Year, string Plot, string Poster);
public record TelevisionSeasonViewModel(int Id, int ShowId, string Title, string Year, string Plot, string Poster);
}

2
ErsatzTV.Application/Television/TelevisionShowViewModel.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionShowViewModel(string Title, string Year, string Plot, string Poster);
public record TelevisionShowViewModel(int Id, string Title, string Year, string Plot, string Poster);
}

78
ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Core.Tests.Fakes
{
public class FakeTelevisionRepository : ITelevisionRepository
{
public Task<bool> Update(TelevisionShow show) => throw new NotSupportedException();
public Task<bool> Update(TelevisionSeason season) => throw new NotSupportedException();
public Task<bool> Update(TelevisionEpisodeMediaItem episode) => throw new NotSupportedException();
public Task<List<TelevisionShow>> GetAllShows() => throw new NotSupportedException();
public Task<Option<TelevisionShow>> GetShow(int televisionShowId) => throw new NotSupportedException();
public Task<int> GetShowCount() => throw new NotSupportedException();
public Task<List<TelevisionShow>> GetPagedShows(int pageNumber, int pageSize) =>
throw new NotSupportedException();
public Task<List<TelevisionEpisodeMediaItem>> GetShowItems(int televisionShowId) =>
throw new NotSupportedException();
public Task<List<TelevisionSeason>> GetAllSeasons() => throw new NotSupportedException();
public Task<Option<TelevisionSeason>> GetSeason(int televisionSeasonId) => throw new NotSupportedException();
public Task<int> GetSeasonCount(int televisionShowId) => throw new NotSupportedException();
public Task<List<TelevisionSeason>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) =>
throw new NotSupportedException();
public Task<List<TelevisionEpisodeMediaItem>> GetSeasonItems(int televisionSeasonId) =>
throw new NotSupportedException();
public Task<Option<TelevisionEpisodeMediaItem>> GetEpisode(int televisionEpisodeId) =>
throw new NotSupportedException();
public Task<int> GetEpisodeCount(int televisionSeasonId) => throw new NotSupportedException();
public Task<List<TelevisionEpisodeMediaItem>> GetPagedEpisodes(
int televisionSeasonId,
int pageNumber,
int pageSize) => throw new NotSupportedException();
public Task<Option<TelevisionShow>> GetShowByPath(int mediaSourceId, string path) =>
throw new NotSupportedException();
public Task<Option<TelevisionShow>> GetShowByMetadata(TelevisionShowMetadata metadata) =>
throw new NotSupportedException();
public Task<Either<BaseError, TelevisionShow>> AddShow(
int localMediaSourceId,
string showFolder,
TelevisionShowMetadata metadata) => throw new NotSupportedException();
public Task<Either<BaseError, TelevisionSeason>> GetOrAddSeason(
TelevisionShow show,
string path,
int seasonNumber) => throw new NotSupportedException();
public Task<Either<BaseError, TelevisionEpisodeMediaItem>> GetOrAddEpisode(
TelevisionSeason season,
int mediaSourceId,
string path) => throw new NotSupportedException();
public Task<Unit> DeleteMissingSources(int localMediaSourceId, List<string> allFolders) =>
throw new NotSupportedException();
public Task<Unit> DeleteEmptyShows() => throw new NotSupportedException();
}
}

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

@ -309,7 +309,8 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -309,7 +309,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }
};
var builder = new PlayoutBuilder(fakeRepository, _logger);
var televisionRepo = new FakeTelevisionRepository();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
@ -388,7 +389,8 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -388,7 +389,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }
};
var builder = new PlayoutBuilder(fakeRepository, _logger);
var televisionRepo = new FakeTelevisionRepository();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(7);
@ -473,7 +475,8 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -473,7 +475,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }
};
var builder = new PlayoutBuilder(fakeRepository, _logger);
var televisionRepo = new FakeTelevisionRepository();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
@ -562,7 +565,8 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -562,7 +565,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }
};
var builder = new PlayoutBuilder(fakeRepository, _logger);
var televisionRepo = new FakeTelevisionRepository();
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
@ -620,7 +624,8 @@ namespace ErsatzTV.Core.Tests.Scheduling @@ -620,7 +624,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
};
var collectionRepo = new FakeMediaCollectionRepository(Map((mediaCollection.Id, mediaItems)));
var builder = new PlayoutBuilder(collectionRepo, _logger);
var televisionRepo = new FakeTelevisionRepository();
var builder = new PlayoutBuilder(collectionRepo, televisionRepo, _logger);
var items = new List<ProgramScheduleItem> { Flood(mediaCollection) };

5
ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs

@ -2,12 +2,13 @@ @@ -2,12 +2,13 @@
{
public class PlayoutProgramScheduleAnchor
{
public int Id { get; set; }
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public int ProgramScheduleId { get; set; }
public ProgramSchedule ProgramSchedule { get; set; }
public int MediaCollectionId { get; set; }
public MediaCollection MediaCollection { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public int CollectionId { get; set; }
public MediaCollectionEnumeratorState EnumeratorState { get; set; }
}
}

7
ErsatzTV.Core/Domain/ProgramScheduleItem.cs

@ -8,8 +8,13 @@ namespace ErsatzTV.Core.Domain @@ -8,8 +8,13 @@ namespace ErsatzTV.Core.Domain
public int Index { get; set; }
public StartType StartType => StartTime.HasValue ? StartType.Fixed : StartType.Dynamic;
public TimeSpan? StartTime { get; set; }
public int MediaCollectionId { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public int? MediaCollectionId { get; set; }
public MediaCollection MediaCollection { get; set; }
public int? TelevisionShowId { get; set; }
public TelevisionShow TelevisionShow { get; set; }
public int? TelevisionSeasonId { get; set; }
public TelevisionSeason TelevisionSeason { get; set; }
public int ProgramScheduleId { get; set; }
public ProgramSchedule ProgramSchedule { get; set; }
}

9
ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
namespace ErsatzTV.Core.Domain
{
public enum ProgramScheduleItemCollectionType
{
Collection = 0,
TelevisionShow = 1,
TelevisionSeason = 2
}
}

4
ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs

@ -10,12 +10,16 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -10,12 +10,16 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<bool> Update(TelevisionShow show);
Task<bool> Update(TelevisionSeason season);
Task<bool> Update(TelevisionEpisodeMediaItem episode);
Task<List<TelevisionShow>> GetAllShows();
Task<Option<TelevisionShow>> GetShow(int televisionShowId);
Task<int> GetShowCount();
Task<List<TelevisionShow>> GetPagedShows(int pageNumber, int pageSize);
Task<List<TelevisionEpisodeMediaItem>> GetShowItems(int televisionShowId);
Task<List<TelevisionSeason>> GetAllSeasons();
Task<Option<TelevisionSeason>> GetSeason(int televisionSeasonId);
Task<int> GetSeasonCount(int televisionShowId);
Task<List<TelevisionSeason>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize);
Task<List<TelevisionEpisodeMediaItem>> GetSeasonItems(int televisionSeasonId);
Task<Option<TelevisionEpisodeMediaItem>> GetEpisode(int televisionEpisodeId);
Task<int> GetEpisodeCount(int televisionSeasonId);
Task<List<TelevisionEpisodeMediaItem>> GetPagedEpisodes(int televisionSeasonId, int pageNumber, int pageSize);

90
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -19,12 +19,15 @@ namespace ErsatzTV.Core.Scheduling @@ -19,12 +19,15 @@ namespace ErsatzTV.Core.Scheduling
private static readonly Random Random = new();
private readonly ILogger<PlayoutBuilder> _logger;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public PlayoutBuilder(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
ILogger<PlayoutBuilder> logger)
{
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
_logger = logger;
}
@ -40,13 +43,30 @@ namespace ErsatzTV.Core.Scheduling @@ -40,13 +43,30 @@ namespace ErsatzTV.Core.Scheduling
DateTimeOffset playoutFinish,
bool rebuild = false)
{
var collections = playout.ProgramSchedule.Items.Map(i => i.MediaCollection).Distinct().ToList();
var collectionKeys = playout.ProgramSchedule.Items
.Map(CollectionKeyForItem)
.ToList();
IEnumerable<Tuple<MediaCollection, List<MediaItem>>> tuples = await collections.Map(
async collection =>
IEnumerable<Tuple<CollectionKey, List<MediaItem>>> tuples = await collectionKeys.Map(
async collectionKey =>
{
Option<List<MediaItem>> maybeItems = await _mediaCollectionRepository.GetItems(collection.Id);
return Tuple(collection, maybeItems.IfNone(new List<MediaItem>()));
switch (collectionKey.CollectionType)
{
case ProgramScheduleItemCollectionType.Collection:
Option<List<MediaItem>> maybeItems =
await _mediaCollectionRepository.GetItems(collectionKey.Id);
return Tuple(collectionKey, maybeItems.IfNone(new List<MediaItem>()));
case ProgramScheduleItemCollectionType.TelevisionShow:
List<TelevisionEpisodeMediaItem> showItems =
await _televisionRepository.GetShowItems(collectionKey.Id);
return Tuple(collectionKey, showItems.Cast<MediaItem>().ToList());
case ProgramScheduleItemCollectionType.TelevisionSeason:
List<TelevisionEpisodeMediaItem> seasonItems =
await _televisionRepository.GetSeasonItems(collectionKey.Id);
return Tuple(collectionKey, seasonItems.Cast<MediaItem>().ToList());
default:
return Tuple(collectionKey, new List<MediaItem>());
}
}).Sequence();
var collectionMediaItems = Map.createRange(tuples);
@ -58,11 +78,11 @@ namespace ErsatzTV.Core.Scheduling @@ -58,11 +78,11 @@ namespace ErsatzTV.Core.Scheduling
playout.Channel.Number,
playout.Channel.Name);
Option<string> emptyCollection = collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key.Name);
Option<CollectionKey> emptyCollection = collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key);
if (emptyCollection.IsSome)
{
_logger.LogError(
"Unable to rebuild playout; collection {Name} has no items!",
"Unable to rebuild playout; collection {@CollectionKey} has no items!",
emptyCollection.ValueUnsafe());
return playout;
@ -79,7 +99,7 @@ namespace ErsatzTV.Core.Scheduling @@ -79,7 +99,7 @@ namespace ErsatzTV.Core.Scheduling
}
var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
Map<MediaCollection, IMediaCollectionEnumerator> collectionEnumerators =
Map<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators =
MapExtensions.Map(collectionMediaItems, (c, i) => GetMediaCollectionEnumerator(playout, c, i));
// find start anchor
@ -112,15 +132,14 @@ namespace ErsatzTV.Core.Scheduling @@ -112,15 +132,14 @@ namespace ErsatzTV.Core.Scheduling
multipleRemaining.IsSome,
durationFinish.IsSome);
IMediaCollectionEnumerator enumerator = collectionEnumerators[scheduleItem.MediaCollection];
IMediaCollectionEnumerator enumerator = collectionEnumerators[CollectionKeyForItem(scheduleItem)];
enumerator.Current.IfSome(
mediaItem =>
{
_logger.LogDebug(
"Scheduling media item: {ScheduleItemNumber} / {MediaCollectionId} - {MediaCollectionName} / {MediaItemId} - {MediaItemTitle} / {StartTime}",
"Scheduling media item: {ScheduleItemNumber} / {CollectionType} / {MediaItemId} - {MediaItemTitle} / {StartTime}",
scheduleItem.Index,
scheduleItem.MediaCollection.Id,
scheduleItem.MediaCollection.Name,
scheduleItem.CollectionType,
mediaItem.Id,
DisplayTitle(mediaItem),
itemStartTime);
@ -297,14 +316,15 @@ namespace ErsatzTV.Core.Scheduling @@ -297,14 +316,15 @@ namespace ErsatzTV.Core.Scheduling
private static List<PlayoutProgramScheduleAnchor> BuildProgramScheduleAnchors(
Playout playout,
Map<MediaCollection, IMediaCollectionEnumerator> collectionEnumerators)
Map<CollectionKey, IMediaCollectionEnumerator> collectionEnumerators)
{
var result = new List<PlayoutProgramScheduleAnchor>();
foreach (MediaCollection collection in collectionEnumerators.Keys)
foreach (CollectionKey collectionKey in collectionEnumerators.Keys)
{
Option<PlayoutProgramScheduleAnchor> maybeExisting = playout.ProgramScheduleAnchors
.FirstOrDefault(a => a.MediaCollection == collection);
.FirstOrDefault(
a => a.CollectionType == collectionKey.CollectionType && a.CollectionId == collectionKey.Id);
var maybeEnumeratorState = collectionEnumerators.GroupBy(e => e.Key, e => e.Value.State)
.ToDictionary(mcs => mcs.Key, mcs => mcs.Head());
@ -312,7 +332,7 @@ namespace ErsatzTV.Core.Scheduling @@ -312,7 +332,7 @@ namespace ErsatzTV.Core.Scheduling
PlayoutProgramScheduleAnchor scheduleAnchor = maybeExisting.Match(
existing =>
{
existing.EnumeratorState = maybeEnumeratorState[collection];
existing.EnumeratorState = maybeEnumeratorState[collectionKey];
return existing;
},
() => new PlayoutProgramScheduleAnchor
@ -321,9 +341,9 @@ namespace ErsatzTV.Core.Scheduling @@ -321,9 +341,9 @@ namespace ErsatzTV.Core.Scheduling
PlayoutId = playout.Id,
ProgramSchedule = playout.ProgramSchedule,
ProgramScheduleId = playout.ProgramScheduleId,
MediaCollection = collection,
MediaCollectionId = collection.Id,
EnumeratorState = maybeEnumeratorState[collection]
CollectionType = collectionKey.CollectionType,
CollectionId = collectionKey.Id,
EnumeratorState = maybeEnumeratorState[collectionKey]
});
result.Add(scheduleAnchor);
@ -334,12 +354,14 @@ namespace ErsatzTV.Core.Scheduling @@ -334,12 +354,14 @@ namespace ErsatzTV.Core.Scheduling
private static IMediaCollectionEnumerator GetMediaCollectionEnumerator(
Playout playout,
MediaCollection mediaCollection,
CollectionKey collectionKey,
List<MediaItem> mediaItems)
{
Option<PlayoutProgramScheduleAnchor> maybeAnchor = playout.ProgramScheduleAnchors
.FirstOrDefault(
a => a.ProgramScheduleId == playout.ProgramScheduleId && a.MediaCollectionId == mediaCollection.Id);
a => a.ProgramScheduleId == playout.ProgramScheduleId && a.CollectionType ==
collectionKey.CollectionType
&& a.CollectionId == collectionKey.Id);
MediaCollectionEnumeratorState state = maybeAnchor.Match(
anchor => anchor.EnumeratorState,
@ -368,5 +390,31 @@ namespace ErsatzTV.Core.Scheduling @@ -368,5 +390,31 @@ namespace ErsatzTV.Core.Scheduling
MovieMediaItem m => m.Metadata?.Title ?? Path.GetFileName(m.Path),
_ => string.Empty
};
private static CollectionKey CollectionKeyForItem(ProgramScheduleItem item) =>
item.CollectionType switch
{
ProgramScheduleItemCollectionType.Collection => new CollectionKey
{
CollectionType = item.CollectionType,
Id = item.MediaCollectionId.Value
},
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
{
CollectionType = item.CollectionType,
Id = item.TelevisionShowId.Value
},
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
{
CollectionType = item.CollectionType,
Id = item.TelevisionSeasonId.Value
}
};
private class CollectionKey : Record<CollectionKey>
{
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public int Id { get; set; }
}
}
}

9
ErsatzTV.Core/Scheduling/TelevisionSeasonMediaCollection.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Scheduling
{
public class TelevisionSeasonMediaCollection : MediaCollection
{
public int TelevisionSeasonId { get; set; }
}
}

9
ErsatzTV.Core/Scheduling/TelevisionShowMediaCollection.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Scheduling
{
public class TelevisionShowMediaCollection : MediaCollection
{
public int TelevisionShowId { get; set; }
}
}

9
ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs

@ -6,11 +6,8 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -6,11 +6,8 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class PlayoutProgramScheduleAnchorConfiguration : IEntityTypeConfiguration<PlayoutProgramScheduleAnchor>
{
public void Configure(EntityTypeBuilder<PlayoutProgramScheduleAnchor> builder)
{
builder.HasKey(a => new { a.PlayoutId, a.ProgramScheduleId, ContentGroupId = a.MediaCollectionId });
builder.OwnsOne(a => a.EnumeratorState);
}
public void Configure(EntityTypeBuilder<PlayoutProgramScheduleAnchor> builder) =>
builder.OwnsOne(a => a.EnumeratorState)
.WithOwner();
}
}

19
ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemConfiguration.cs

@ -6,7 +6,24 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -6,7 +6,24 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
{
public class ProgramScheduleItemConfiguration : IEntityTypeConfiguration<ProgramScheduleItem>
{
public void Configure(EntityTypeBuilder<ProgramScheduleItem> builder) =>
public void Configure(EntityTypeBuilder<ProgramScheduleItem> builder)
{
builder.ToTable("ProgramScheduleItems");
builder.HasOne(i => i.MediaCollection)
.WithMany()
.HasForeignKey(i => i.MediaCollectionId)
.IsRequired(false);
builder.HasOne(i => i.TelevisionShow)
.WithMany()
.HasForeignKey(i => i.TelevisionShowId)
.IsRequired(false);
builder.HasOne(i => i.TelevisionSeason)
.WithMany()
.HasForeignKey(i => i.TelevisionSeasonId)
.IsRequired(false);
}
}
}

51
ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
@ -70,55 +69,5 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -70,55 +69,5 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
_dbContext.MediaItems.Remove(mediaItem);
return _dbContext.SaveChangesAsync();
}).ToUnit();
public Task<List<MediaItemSummary>> GetPageByType(MediaType mediaType, int pageNumber, int pageSize) =>
mediaType switch
{
MediaType.Movie => _dbContext.MediaItemSummaries.FromSqlRaw(
@"SELECT
m.Id AS MediaItemId,
mm.Title AS Title,
mm.SortTitle AS SortTitle,
mm.Year AS Subtitle,
mi.Poster AS Poster
FROM Movies m
INNER JOIN MediaItems mi on m.Id = mi.Id
INNER JOIN MovieMetadata mm on mm.MovieId = m.Id
ORDER BY SortTitle
LIMIT {0} OFFSET {1}",
pageSize,
(pageNumber - 1) * pageSize)
.AsNoTracking()
.ToListAsync(),
MediaType.TvShow => _dbContext.MediaItemSummaries.FromSqlRaw(
@"SELECT
min(ts.Id) AS MediaItemId,
tsm.Title AS Title,
tsm.SortTitle AS SortTitle,
tsm.Year AS Subtitle,
max(ts.Poster) AS Poster
FROM TelevisionShows ts
INNER JOIN TelevisionShowMetadata tsm on tsm.TelevisionShowId = ts.Id
GROUP BY tsm.Title, tsm.SortTitle, tsm.Year
ORDER BY tsm.SortTitle
LIMIT {0} OFFSET {1}",
pageSize,
(pageNumber - 1) * pageSize)
.AsNoTracking()
.ToListAsync(),
_ => Task.FromResult(new List<MediaItemSummary>())
};
public Task<int> GetCountByType(MediaType mediaType) =>
mediaType switch
{
MediaType.Movie => _dbContext.MovieMediaItems
.CountAsync(),
MediaType.TvShow => _dbContext.TelevisionShows
.GroupBy(i => new { i.Metadata.Title, i.Metadata.SortTitle })
.CountAsync(),
_ => Task.FromResult(0)
};
}
}

4
ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs

@ -35,6 +35,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -35,6 +35,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MediaCollection)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.TelevisionShow)
.OrderBy(p => p.Id) // https://github.com/dotnet/efcore/issues/22579#issuecomment-694772289
.SingleOrDefaultAsync(p => p.Id == id);
@ -52,7 +55,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -52,7 +55,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.SingleOrDefaultAsync();
}
// TODO: does this actually work?
public Task<List<PlayoutItem>> GetPlayoutItems(int playoutId) =>
_dbContext.PlayoutItems
.Include(i => i.MediaItem)

8
ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs

@ -57,7 +57,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -57,7 +57,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
await _dbContext.Entry(programSchedule).Collection(s => s.Items).LoadAsync();
await _dbContext.Entry(programSchedule).Collection(s => s.Items).Query()
.Include(i => i.MediaCollection).LoadAsync();
.Include(i => i.MediaCollection)
.Include(i => i.TelevisionShow)
.ThenInclude(s => s.Metadata)
.Include(i => i.TelevisionSeason)
.ThenInclude(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.LoadAsync();
return programSchedule.Items;
}).Sequence();
}

55
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -35,6 +35,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -35,6 +35,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return await _dbContext.SaveChangesAsync() > 0;
}
public Task<List<TelevisionShow>> GetAllShows() =>
_dbContext.TelevisionShows
.AsNoTracking()
.Include(s => s.Metadata)
.ToListAsync();
public Task<Option<TelevisionShow>> GetShow(int televisionShowId) =>
_dbContext.TelevisionShows
.AsNoTracking()
@ -57,6 +63,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -57,6 +63,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Take(pageSize)
.ToListAsync();
public Task<List<TelevisionSeason>> GetAllSeasons() =>
_dbContext.TelevisionSeasons
.AsNoTracking()
.Include(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.ToListAsync();
public Task<Option<TelevisionSeason>> GetSeason(int televisionSeasonId) =>
_dbContext.TelevisionSeasons
.AsNoTracking()
@ -230,10 +243,44 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -230,10 +243,44 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
})
.ToUnit();
public Task<Unit> Delete(TelevisionShow show) =>
_dbContext.TelevisionShows.Remove(show).AsTask()
.Bind(_ => _dbContext.SaveChangesAsync())
.ToUnit();
public async Task<List<TelevisionEpisodeMediaItem>> GetShowItems(int televisionShowId)
{
// TODO: would be nice to get the media items in one go, but ef...
List<int> showItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"select tmi.Id
from TelevisionEpisodes tmi
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
inner join TelevisionShows ts on ts.Id = tsn.TelevisionShowId
where ts.Id = {0}",
televisionShowId)
.Select(i => i.Id)
.ToListAsync();
return await _dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(e => e.Metadata)
.Where(mi => showItemIds.Contains(mi.Id))
.ToListAsync();
}
public async Task<List<TelevisionEpisodeMediaItem>> GetSeasonItems(int televisionSeasonId)
{
// TODO: would be nice to get the media items in one go, but ef...
List<int> seasonItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"select tmi.Id
from TelevisionEpisodes tmi
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
where tsn.Id = {0}",
televisionSeasonId)
.Select(i => i.Id)
.ToListAsync();
return await _dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(e => e.Metadata)
.Where(mi => seasonItemIds.Contains(mi.Id))
.ToListAsync();
}
private async Task<Either<BaseError, TelevisionSeason>> AddSeason(
TelevisionShow show,

1305
ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.Designer.cs generated

File diff suppressed because it is too large Load Diff

209
ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.cs

@ -0,0 +1,209 @@ @@ -0,0 +1,209 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class ScheduleCollectionTypes : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
"FK_PlayoutProgramScheduleItemAnchors_MediaCollections_MediaCollectionId",
"PlayoutProgramScheduleItemAnchors");
migrationBuilder.DropForeignKey(
"FK_ProgramScheduleItems_MediaCollections_MediaCollectionId",
"ProgramScheduleItems");
migrationBuilder.DropPrimaryKey(
"PK_PlayoutProgramScheduleItemAnchors",
"PlayoutProgramScheduleItemAnchors");
migrationBuilder.DropIndex(
"IX_PlayoutProgramScheduleItemAnchors_MediaCollectionId",
"PlayoutProgramScheduleItemAnchors");
migrationBuilder.RenameColumn(
"MediaCollectionId",
"PlayoutProgramScheduleItemAnchors",
"CollectionType");
migrationBuilder.AlterColumn<int>(
"MediaCollectionId",
"ProgramScheduleItems",
"INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AddColumn<int>(
"CollectionType",
"ProgramScheduleItems",
"INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
"TelevisionSeasonId",
"ProgramScheduleItems",
"INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
"TelevisionShowId",
"ProgramScheduleItems",
"INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
"Id",
"PlayoutProgramScheduleItemAnchors",
"INTEGER",
nullable: false,
defaultValue: 0)
.Annotation("Sqlite:Autoincrement", true);
migrationBuilder.AddColumn<int>(
"CollectionId",
"PlayoutProgramScheduleItemAnchors",
"INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddPrimaryKey(
"PK_PlayoutProgramScheduleItemAnchors",
"PlayoutProgramScheduleItemAnchors",
"Id");
migrationBuilder.CreateIndex(
"IX_ProgramScheduleItems_TelevisionSeasonId",
"ProgramScheduleItems",
"TelevisionSeasonId");
migrationBuilder.CreateIndex(
"IX_ProgramScheduleItems_TelevisionShowId",
"ProgramScheduleItems",
"TelevisionShowId");
migrationBuilder.CreateIndex(
"IX_PlayoutProgramScheduleItemAnchors_PlayoutId",
"PlayoutProgramScheduleItemAnchors",
"PlayoutId");
migrationBuilder.AddForeignKey(
"FK_ProgramScheduleItems_MediaCollections_MediaCollectionId",
"ProgramScheduleItems",
"MediaCollectionId",
"MediaCollections",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
"FK_ProgramScheduleItems_TelevisionSeasons_TelevisionSeasonId",
"ProgramScheduleItems",
"TelevisionSeasonId",
"TelevisionSeasons",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
"FK_ProgramScheduleItems_TelevisionShows_TelevisionShowId",
"ProgramScheduleItems",
"TelevisionShowId",
"TelevisionShows",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
"FK_ProgramScheduleItems_MediaCollections_MediaCollectionId",
"ProgramScheduleItems");
migrationBuilder.DropForeignKey(
"FK_ProgramScheduleItems_TelevisionSeasons_TelevisionSeasonId",
"ProgramScheduleItems");
migrationBuilder.DropForeignKey(
"FK_ProgramScheduleItems_TelevisionShows_TelevisionShowId",
"ProgramScheduleItems");
migrationBuilder.DropIndex(
"IX_ProgramScheduleItems_TelevisionSeasonId",
"ProgramScheduleItems");
migrationBuilder.DropIndex(
"IX_ProgramScheduleItems_TelevisionShowId",
"ProgramScheduleItems");
migrationBuilder.DropPrimaryKey(
"PK_PlayoutProgramScheduleItemAnchors",
"PlayoutProgramScheduleItemAnchors");
migrationBuilder.DropIndex(
"IX_PlayoutProgramScheduleItemAnchors_PlayoutId",
"PlayoutProgramScheduleItemAnchors");
migrationBuilder.DropColumn(
"CollectionType",
"ProgramScheduleItems");
migrationBuilder.DropColumn(
"TelevisionSeasonId",
"ProgramScheduleItems");
migrationBuilder.DropColumn(
"TelevisionShowId",
"ProgramScheduleItems");
migrationBuilder.DropColumn(
"Id",
"PlayoutProgramScheduleItemAnchors");
migrationBuilder.DropColumn(
"CollectionId",
"PlayoutProgramScheduleItemAnchors");
migrationBuilder.RenameColumn(
"CollectionType",
"PlayoutProgramScheduleItemAnchors",
"MediaCollectionId");
migrationBuilder.AlterColumn<int>(
"MediaCollectionId",
"ProgramScheduleItems",
"INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AddPrimaryKey(
"PK_PlayoutProgramScheduleItemAnchors",
"PlayoutProgramScheduleItemAnchors",
new[] { "PlayoutId", "ProgramScheduleId", "MediaCollectionId" });
migrationBuilder.CreateIndex(
"IX_PlayoutProgramScheduleItemAnchors_MediaCollectionId",
"PlayoutProgramScheduleItemAnchors",
"MediaCollectionId");
migrationBuilder.AddForeignKey(
"FK_PlayoutProgramScheduleItemAnchors_MediaCollections_MediaCollectionId",
"PlayoutProgramScheduleItemAnchors",
"MediaCollectionId",
"MediaCollections",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
"FK_ProgramScheduleItems_MediaCollections_MediaCollectionId",
"ProgramScheduleItems",
"MediaCollectionId",
"MediaCollections",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

74
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -370,18 +370,25 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -370,18 +370,25 @@ namespace ErsatzTV.Infrastructure.Migrations
"ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor",
b =>
{
b.Property<int>("PlayoutId")
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
b.Property<int>("CollectionId")
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionId")
b.Property<int>("CollectionType")
.HasColumnType("INTEGER");
b.HasKey("PlayoutId", "ProgramScheduleId", "MediaCollectionId");
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.HasIndex("MediaCollectionId");
b.Property<int>("ProgramScheduleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PlayoutId");
b.HasIndex("ProgramScheduleId");
@ -469,10 +476,13 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -469,10 +476,13 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CollectionType")
.HasColumnType("INTEGER");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<int>("MediaCollectionId")
b.Property<int?>("MediaCollectionId")
.HasColumnType("INTEGER");
b.Property<int>("ProgramScheduleId")
@ -481,12 +491,22 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -481,12 +491,22 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<TimeSpan?>("StartTime")
.HasColumnType("TEXT");
b.Property<int?>("TelevisionSeasonId")
.HasColumnType("INTEGER");
b.Property<int?>("TelevisionShowId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("MediaCollectionId");
b.HasIndex("ProgramScheduleId");
b.HasIndex("TelevisionSeasonId");
b.HasIndex("TelevisionShowId");
b.ToTable("ProgramScheduleItems");
});
@ -1037,12 +1057,6 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1037,12 +1057,6 @@ namespace ErsatzTV.Infrastructure.Migrations
"ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection")
.WithMany()
.HasForeignKey("MediaCollectionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("ProgramScheduleAnchors")
.HasForeignKey("PlayoutId")
@ -1060,13 +1074,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1060,13 +1074,7 @@ namespace ErsatzTV.Infrastructure.Migrations
"EnumeratorState",
b1 =>
{
b1.Property<int>("PlayoutProgramScheduleAnchorPlayoutId")
.HasColumnType("INTEGER");
b1.Property<int>("PlayoutProgramScheduleAnchorProgramScheduleId")
.HasColumnType("INTEGER");
b1.Property<int>("PlayoutProgramScheduleAnchorMediaCollectionId")
b1.Property<int>("PlayoutProgramScheduleAnchorId")
.HasColumnType("INTEGER");
b1.Property<int>("Index")
@ -1075,24 +1083,16 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1075,24 +1083,16 @@ namespace ErsatzTV.Infrastructure.Migrations
b1.Property<int>("Seed")
.HasColumnType("INTEGER");
b1.HasKey(
"PlayoutProgramScheduleAnchorPlayoutId",
"PlayoutProgramScheduleAnchorProgramScheduleId",
"PlayoutProgramScheduleAnchorMediaCollectionId");
b1.HasKey("PlayoutProgramScheduleAnchorId");
b1.ToTable("PlayoutProgramScheduleItemAnchors");
b1.WithOwner()
.HasForeignKey(
"PlayoutProgramScheduleAnchorPlayoutId",
"PlayoutProgramScheduleAnchorProgramScheduleId",
"PlayoutProgramScheduleAnchorMediaCollectionId");
.HasForeignKey("PlayoutProgramScheduleAnchorId");
});
b.Navigation("EnumeratorState");
b.Navigation("MediaCollection");
b.Navigation("Playout");
b.Navigation("ProgramSchedule");
@ -1122,9 +1122,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1122,9 +1122,7 @@ namespace ErsatzTV.Infrastructure.Migrations
{
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection")
.WithMany()
.HasForeignKey("MediaCollectionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.HasForeignKey("MediaCollectionId");
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
.WithMany("Items")
@ -1132,9 +1130,21 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1132,9 +1130,21 @@ namespace ErsatzTV.Infrastructure.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.TelevisionSeason", "TelevisionSeason")
.WithMany()
.HasForeignKey("TelevisionSeasonId");
b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", "TelevisionShow")
.WithMany()
.HasForeignKey("TelevisionShowId");
b.Navigation("MediaCollection");
b.Navigation("ProgramSchedule");
b.Navigation("TelevisionSeason");
b.Navigation("TelevisionShow");
});
modelBuilder.Entity(

1
ErsatzTV.sln.DotSettings

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/CodeAnnotations/NamespacesWithAnnotations/=ErsatzTV_002EAnnotations/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DTO/@EntryIndexedValue">DTO</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HDHR/@EntryIndexedValue">HDHR</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SAR/@EntryIndexedValue">SAR</s:String>

2
ErsatzTV/ErsatzTV.csproj

@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MudBlazor" Version="5.0.1" />
<PackageReference Include="MudBlazor" Version="5.0.1-dev.2" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.1" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />

74
ErsatzTV/Pages/ScheduleItemsEditor.razor

@ -4,6 +4,8 @@ @@ -4,6 +4,8 @@
@using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.ProgramSchedules.Commands
@using ErsatzTV.Application.ProgramSchedules.Queries
@using ErsatzTV.Application.Television
@using ErsatzTV.Application.Television.Queries
@inject NavigationManager NavigationManager
@inject ILogger<ScheduleItemsEditor> Logger
@inject ISnackbar Snackbar
@ -31,12 +33,12 @@ @@ -31,12 +33,12 @@
@(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic")
</MudText>
</MudTd>
<MudTd DataLabel="Media Collection">
<MudTd DataLabel="Collection">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@context.MediaCollection.Name
@context.CollectionName
</MudText>
</MudTd>
<MudTd DataLabel="Media Collection">
<MudTd DataLabel="Playout Mode">
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
@context.PlayoutMode
@if (context.PlayoutMode == PlayoutMode.Multiple && context.MultipleCount.HasValue)
@ -53,7 +55,7 @@ @@ -53,7 +55,7 @@
<MudTablePager/>
</PagerContent>
</MudTable>
<MudButton Variant="Variant.Filled" Color="Color.Default" Disabled="@_defaultCollection.IsNone" OnClick="@(_ => AddScheduleItem())" Class="mt-4">
<MudButton Variant="Variant.Filled" Color="Color.Default" OnClick="@(_ => AddScheduleItem())" Class="mt-4">
Add Schedule Item
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4 ml-4">
@ -74,7 +76,39 @@ @@ -74,7 +76,39 @@
}
</MudSelect>
<MudTimePicker Class="mt-3" Label="Start Time" @bind-Time="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)"/>
<MudAutocomplete Class="mt-3" T="MediaCollectionViewModel" Label="Media Collection" @bind-value="_selectedItem.MediaCollection" SearchFunc="@SearchMediaCollections" ToStringFunc="@(c => c?.Name)"/>
<MudSelect Label="Collection Type" @bind-Value="_selectedItem.CollectionType" For="@(() => _selectedItem.CollectionType)">
@foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues<ProgramScheduleItemCollectionType>())
{
<MudSelectItem Value="@collectionType">@collectionType</MudSelectItem>
}
</MudSelect>
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection)
{
<MudAutocomplete Class="mt-3"
T="MediaCollectionViewModel"
Label="Media Collection"
@bind-value="_selectedItem.MediaCollection"
SearchFunc="@SearchMediaCollections"
ToStringFunc="@(c => c?.Name)"/>
}
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow)
{
<MudAutocomplete Class="mt-3"
T="TelevisionShowViewModel"
Label="Television Show"
@bind-value="_selectedItem.TelevisionShow"
SearchFunc="@SearchTelevisionShows"
ToStringFunc="@(s => s == null ? null : $"{s.Title} ({s.Year})")"/>
}
@if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason)
{
<MudAutocomplete Class="mt-3"
T="TelevisionSeasonViewModel"
Label="Television Season"
@bind-value="_selectedItem.TelevisionSeason"
SearchFunc="@SearchTelevisionSeasons"
ToStringFunc="@(s => s == null ? null : $"{s.Title} ({s.Year}) - {s.Plot}")"/>
}
<MudSelect Class="mt-3" Label="Playout Mode" @bind-Value="@_selectedItem.PlayoutMode" For="@(() => _selectedItem.PlayoutMode)">
@foreach (PlayoutMode playoutMode in Enum.GetValues<PlayoutMode>())
{
@ -99,7 +133,8 @@ @@ -99,7 +133,8 @@
private ProgramScheduleItemsEditViewModel _schedule;
private List<MediaCollectionViewModel> _mediaCollections;
private Option<MediaCollectionViewModel> _defaultCollection;
private List<TelevisionShowViewModel> _televisionShows;
private List<TelevisionSeasonViewModel> _televisionSeasons;
private ProgramScheduleItemEditViewModel _selectedItem;
@ -108,7 +143,8 @@ @@ -108,7 +143,8 @@
private async Task LoadScheduleItems()
{
_mediaCollections = await Mediator.Send(new GetAllMediaCollections());
_defaultCollection = _mediaCollections.HeadOrNone();
_televisionShows = await Mediator.Send(new GetAllTelevisionShows());
_televisionSeasons = await Mediator.Send(new GetAllTelevisionSeasons());
string name = string.Empty;
Option<ProgramScheduleViewModel> maybeSchedule = await Mediator.Send(new GetProgramScheduleById(Id));
@ -131,7 +167,11 @@ @@ -131,7 +167,11 @@
StartType = item.StartType,
StartTime = item.StartTime,
PlayoutMode = item.PlayoutMode,
MediaCollection = item.MediaCollection
CollectionType = item.CollectionType,
MediaCollection = item.MediaCollection,
TelevisionShow = item.TelevisionShow,
TelevisionSeason = item.TelevisionSeason,
CollectionName = item.Name
};
switch (item)
@ -154,11 +194,10 @@ @@ -154,11 +194,10 @@
{
Index = _schedule.Items.Map(i => i.Index).DefaultIfEmpty().Max() + 1,
StartType = StartType.Dynamic,
PlayoutMode = PlayoutMode.One
PlayoutMode = PlayoutMode.One,
CollectionType = ProgramScheduleItemCollectionType.Collection
};
_defaultCollection.IfSome(c => item.MediaCollection = c);
_schedule.Items.Add(item);
_selectedItem = item;
}
@ -172,6 +211,12 @@ @@ -172,6 +211,12 @@
private Task<IEnumerable<MediaCollectionViewModel>> SearchMediaCollections(string value) =>
_mediaCollections.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private Task<IEnumerable<TelevisionShowViewModel>> SearchTelevisionShows(string value) =>
_televisionShows.Filter(s => s.Title.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private Task<IEnumerable<TelevisionSeasonViewModel>> SearchTelevisionSeasons(string value) =>
_televisionSeasons.Filter(s => s.Title.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask();
private async Task SaveChanges()
{
var items = _schedule.Items.Map(item => new ReplaceProgramScheduleItem(
@ -179,7 +224,10 @@ @@ -179,7 +224,10 @@
item.StartType,
item.StartTime,
item.PlayoutMode,
item.MediaCollection.Id,
item.CollectionType,
item.MediaCollection?.Id,
item.TelevisionShow?.Id,
item.TelevisionSeason?.Id,
item.MultipleCount,
item.PlayoutDuration,
item.PlayoutMode == PlayoutMode.Duration ? item.OfflineTail.IfNone(false) : null)).ToList();
@ -189,7 +237,7 @@ @@ -189,7 +237,7 @@
errorMessages.HeadOrNone().Match(
error =>
{
Snackbar.Add($"Unexpected error saving schedule: {error.Value}");
Snackbar.Add($"Unexpected error saving schedule: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving schedule: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("/schedules"));

2
ErsatzTV/Pages/Schedules.razor

@ -61,7 +61,7 @@ @@ -61,7 +61,7 @@
<MudTd DataLabel="Start Time">
@(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic")
</MudTd>
<MudTd DataLabel="Media Collection">@context.MediaCollection.Name</MudTd>
<MudTd DataLabel="Media Collection">@context.Name</MudTd>
<MudTd DataLabel="Media Collection">@context.PlayoutMode</MudTd>
</RowTemplate>
<PagerContent>

1460
ErsatzTV/Properties/Annotations.cs

File diff suppressed because it is too large Load Diff

49
ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs

@ -1,11 +1,16 @@ @@ -1,11 +1,16 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using ErsatzTV.Annotations;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.ViewModels
{
public class ProgramScheduleItemEditViewModel
public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged
{
private ProgramScheduleItemCollectionType _collectionType;
private int? _multipleCount;
private bool? _offlineTail;
private TimeSpan? _playoutDuration;
@ -22,7 +27,41 @@ namespace ErsatzTV.ViewModels @@ -22,7 +27,41 @@ namespace ErsatzTV.ViewModels
}
public PlayoutMode PlayoutMode { get; set; }
public ProgramScheduleItemCollectionType CollectionType
{
get => _collectionType;
set
{
_collectionType = value;
switch (CollectionType)
{
case ProgramScheduleItemCollectionType.Collection:
TelevisionShow = null;
TelevisionSeason = null;
break;
case ProgramScheduleItemCollectionType.TelevisionShow:
MediaCollection = null;
TelevisionSeason = null;
break;
case ProgramScheduleItemCollectionType.TelevisionSeason:
MediaCollection = null;
TelevisionShow = null;
break;
}
OnPropertyChanged(nameof(MediaCollection));
OnPropertyChanged(nameof(TelevisionShow));
OnPropertyChanged(nameof(TelevisionSeason));
}
}
public MediaCollectionViewModel MediaCollection { get; set; }
public TelevisionShowViewModel TelevisionShow { get; set; }
public TelevisionSeasonViewModel TelevisionSeason { get; set; }
public string CollectionName { get; set; }
public int? MultipleCount
{
@ -41,5 +80,13 @@ namespace ErsatzTV.ViewModels @@ -41,5 +80,13 @@ namespace ErsatzTV.ViewModels
get => PlayoutMode == PlayoutMode.Duration ? _offlineTail : null;
set => _offlineTail = value;
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(
[CallerMemberName]
string propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

Loading…
Cancel
Save