diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs index 152e37861..362fab63c 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs @@ -29,14 +29,15 @@ namespace ErsatzTV.Application.MediaCollections.Commands _mediaCollectionRepository.Add(c).Map(ProjectToViewModel); private Task> Validate(CreateSimpleMediaCollection request) => - ValidateName(request).MapT(name => new SimpleMediaCollection - { - Name = name, - Movies = new List(), - TelevisionShows = new List(), - TelevisionEpisodes = new List(), - TelevisionSeasons = new List() - }); + ValidateName(request).MapT( + name => new SimpleMediaCollection + { + Name = name, + Movies = new List(), + TelevisionShows = new List(), + TelevisionEpisodes = new List(), + TelevisionSeasons = new List() + }); private async Task> ValidateName(CreateSimpleMediaCollection createCollection) { diff --git a/ErsatzTV.Application/MediaCollections/Mapper.cs b/ErsatzTV.Application/MediaCollections/Mapper.cs index 021ae7fa0..592174056 100644 --- a/ErsatzTV.Application/MediaCollections/Mapper.cs +++ b/ErsatzTV.Application/MediaCollections/Mapper.cs @@ -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 { 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); } } diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs b/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs index ebd5a337e..bb7265c47 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs @@ -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>, IProgramScheduleItemRequest; diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs b/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs index 717e2897c..be9def8d7 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs @@ -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; } diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs index 696d4d902..f205279d8 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs @@ -53,6 +53,40 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands return programSchedule; } + protected Validation 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 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 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() }, diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs index 0a1b051bf..50c7a18e2 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs @@ -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; diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs index 54e1587af..fcd8a7d99 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs @@ -55,12 +55,19 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands private Task> Validate(ReplaceProgramScheduleItems request) => ProgramScheduleMustExist(request.ProgramScheduleId) - .BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule)); + .BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule)) + .BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule)); private Validation PlayoutModesMustBeValid( ReplaceProgramScheduleItems request, ProgramSchedule programSchedule) => request.Items.Map(item => PlayoutModeMustBeValid(item, programSchedule)).Sequence() .Map(_ => programSchedule); + + private Validation CollectionTypesMustBeValid( + ReplaceProgramScheduleItems request, + ProgramSchedule programSchedule) => + request.Items.Map(item => CollectionTypeMustBeValid(item, programSchedule)).Sequence() + .Map(_ => programSchedule); } } diff --git a/ErsatzTV.Application/ProgramSchedules/Mapper.cs b/ErsatzTV.Application/ProgramSchedules/Mapper.cs index 1b7056882..9d8ea1e68 100644 --- a/ErsatzTV.Application/ProgramSchedules/Mapper.cs +++ b/ErsatzTV.Application/ProgramSchedules/Mapper.cs @@ -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 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 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}") }; diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs index 76fc0ab3b..6071ffc9b 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs @@ -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 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 startType, startTime, PlayoutMode.Duration, - mediaCollection) + collectionType, + mediaCollection, + televisionShow, + televisionSeason) { PlayoutDuration = playoutDuration; OfflineTail = offlineTail; diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs index 13105513f..e1f13b322 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs @@ -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 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) { } } diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs index b298d63cb..4106836f6 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs @@ -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 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; } diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs index 62f55f10d..e77e55667 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs @@ -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 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) { } } diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs index 7a0ed5e82..dc7392ab7 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs @@ -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 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 + }; + } } diff --git a/ErsatzTV.Application/Television/Mapper.cs b/ErsatzTV.Application/Television/Mapper.cs index 37366fa1b..983e973d2 100644 --- a/ErsatzTV.Application/Television/Mapper.cs +++ b/ErsatzTV.Application/Television/Mapper.cs @@ -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(), diff --git a/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs b/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs new file mode 100644 index 000000000..303c8aacd --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; +using MediatR; + +namespace ErsatzTV.Application.Television.Queries +{ + public record GetAllTelevisionSeasons : IRequest>; +} diff --git a/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs b/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs new file mode 100644 index 000000000..5c0668b32 --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs @@ -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> + { + private readonly ITelevisionRepository _televisionRepository; + + public GetAllTelevisionSeasonsHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; + + public Task> Handle( + GetAllTelevisionSeasons request, + CancellationToken cancellationToken) => + _televisionRepository.GetAllSeasons().Map(list => list.Map(ProjectToViewModel).ToList()); + } +} diff --git a/ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs b/ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs new file mode 100644 index 000000000..87f91eb15 --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; +using MediatR; + +namespace ErsatzTV.Application.Television.Queries +{ + public record GetAllTelevisionShows : IRequest>; +} diff --git a/ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs b/ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs new file mode 100644 index 000000000..09c73e8a3 --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs @@ -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> + { + private readonly ITelevisionRepository _televisionRepository; + + public GetAllTelevisionShowsHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; + + public Task> Handle( + GetAllTelevisionShows request, + CancellationToken cancellationToken) => + _televisionRepository.GetAllShows().Map(list => list.Map(ProjectToViewModel).ToList()); + } +} diff --git a/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs b/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs index b7abbc2a6..d5781734c 100644 --- a/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs +++ b/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs @@ -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); } diff --git a/ErsatzTV.Application/Television/TelevisionShowViewModel.cs b/ErsatzTV.Application/Television/TelevisionShowViewModel.cs index 13351ff97..885187f6e 100644 --- a/ErsatzTV.Application/Television/TelevisionShowViewModel.cs +++ b/ErsatzTV.Application/Television/TelevisionShowViewModel.cs @@ -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); } diff --git a/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs new file mode 100644 index 000000000..002c86c73 --- /dev/null +++ b/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs @@ -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 Update(TelevisionShow show) => throw new NotSupportedException(); + + public Task Update(TelevisionSeason season) => throw new NotSupportedException(); + + public Task Update(TelevisionEpisodeMediaItem episode) => throw new NotSupportedException(); + + public Task> GetAllShows() => throw new NotSupportedException(); + + public Task> GetShow(int televisionShowId) => throw new NotSupportedException(); + + public Task GetShowCount() => throw new NotSupportedException(); + + public Task> GetPagedShows(int pageNumber, int pageSize) => + throw new NotSupportedException(); + + public Task> GetShowItems(int televisionShowId) => + throw new NotSupportedException(); + + public Task> GetAllSeasons() => throw new NotSupportedException(); + + public Task> GetSeason(int televisionSeasonId) => throw new NotSupportedException(); + + public Task GetSeasonCount(int televisionShowId) => throw new NotSupportedException(); + + public Task> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) => + throw new NotSupportedException(); + + public Task> GetSeasonItems(int televisionSeasonId) => + throw new NotSupportedException(); + + public Task> GetEpisode(int televisionEpisodeId) => + throw new NotSupportedException(); + + public Task GetEpisodeCount(int televisionSeasonId) => throw new NotSupportedException(); + + public Task> GetPagedEpisodes( + int televisionSeasonId, + int pageNumber, + int pageSize) => throw new NotSupportedException(); + + public Task> GetShowByPath(int mediaSourceId, string path) => + throw new NotSupportedException(); + + public Task> GetShowByMetadata(TelevisionShowMetadata metadata) => + throw new NotSupportedException(); + + public Task> AddShow( + int localMediaSourceId, + string showFolder, + TelevisionShowMetadata metadata) => throw new NotSupportedException(); + + public Task> GetOrAddSeason( + TelevisionShow show, + string path, + int seasonNumber) => throw new NotSupportedException(); + + public Task> GetOrAddEpisode( + TelevisionSeason season, + int mediaSourceId, + string path) => throw new NotSupportedException(); + + public Task DeleteMissingSources(int localMediaSourceId, List allFolders) => + throw new NotSupportedException(); + + public Task DeleteEmptyShows() => throw new NotSupportedException(); + } +} diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs index 5638f92c2..acee730a0 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs @@ -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 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 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 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 }; 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 { Flood(mediaCollection) }; diff --git a/ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs b/ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs index 174055613..d7cea7059 100644 --- a/ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs +++ b/ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs @@ -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; } } } diff --git a/ErsatzTV.Core/Domain/ProgramScheduleItem.cs b/ErsatzTV.Core/Domain/ProgramScheduleItem.cs index 25363406b..ecc12d028 100644 --- a/ErsatzTV.Core/Domain/ProgramScheduleItem.cs +++ b/ErsatzTV.Core/Domain/ProgramScheduleItem.cs @@ -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; } } diff --git a/ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs b/ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs new file mode 100644 index 000000000..e0dcd6cf5 --- /dev/null +++ b/ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Core.Domain +{ + public enum ProgramScheduleItemCollectionType + { + Collection = 0, + TelevisionShow = 1, + TelevisionSeason = 2 + } +} diff --git a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs index d6687fc66..88d7ebcae 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs @@ -10,12 +10,16 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task Update(TelevisionShow show); Task Update(TelevisionSeason season); Task Update(TelevisionEpisodeMediaItem episode); + Task> GetAllShows(); Task> GetShow(int televisionShowId); Task GetShowCount(); Task> GetPagedShows(int pageNumber, int pageSize); + Task> GetShowItems(int televisionShowId); + Task> GetAllSeasons(); Task> GetSeason(int televisionSeasonId); Task GetSeasonCount(int televisionShowId); Task> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize); + Task> GetSeasonItems(int televisionSeasonId); Task> GetEpisode(int televisionEpisodeId); Task GetEpisodeCount(int televisionSeasonId); Task> GetPagedEpisodes(int televisionSeasonId, int pageNumber, int pageSize); diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index e6b3f3945..b0076aceb 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -19,12 +19,15 @@ namespace ErsatzTV.Core.Scheduling private static readonly Random Random = new(); private readonly ILogger _logger; private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly ITelevisionRepository _televisionRepository; public PlayoutBuilder( IMediaCollectionRepository mediaCollectionRepository, + ITelevisionRepository televisionRepository, ILogger logger) { _mediaCollectionRepository = mediaCollectionRepository; + _televisionRepository = televisionRepository; _logger = logger; } @@ -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>> tuples = await collections.Map( - async collection => + IEnumerable>> tuples = await collectionKeys.Map( + async collectionKey => { - Option> maybeItems = await _mediaCollectionRepository.GetItems(collection.Id); - return Tuple(collection, maybeItems.IfNone(new List())); + switch (collectionKey.CollectionType) + { + case ProgramScheduleItemCollectionType.Collection: + Option> maybeItems = + await _mediaCollectionRepository.GetItems(collectionKey.Id); + return Tuple(collectionKey, maybeItems.IfNone(new List())); + case ProgramScheduleItemCollectionType.TelevisionShow: + List showItems = + await _televisionRepository.GetShowItems(collectionKey.Id); + return Tuple(collectionKey, showItems.Cast().ToList()); + case ProgramScheduleItemCollectionType.TelevisionSeason: + List seasonItems = + await _televisionRepository.GetSeasonItems(collectionKey.Id); + return Tuple(collectionKey, seasonItems.Cast().ToList()); + default: + return Tuple(collectionKey, new List()); + } }).Sequence(); var collectionMediaItems = Map.createRange(tuples); @@ -58,11 +78,11 @@ namespace ErsatzTV.Core.Scheduling playout.Channel.Number, playout.Channel.Name); - Option emptyCollection = collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key.Name); + Option 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 } var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList(); - Map collectionEnumerators = + Map collectionEnumerators = MapExtensions.Map(collectionMediaItems, (c, i) => GetMediaCollectionEnumerator(playout, c, i)); // find start anchor @@ -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 private static List BuildProgramScheduleAnchors( Playout playout, - Map collectionEnumerators) + Map collectionEnumerators) { var result = new List(); - foreach (MediaCollection collection in collectionEnumerators.Keys) + foreach (CollectionKey collectionKey in collectionEnumerators.Keys) { Option 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 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 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 private static IMediaCollectionEnumerator GetMediaCollectionEnumerator( Playout playout, - MediaCollection mediaCollection, + CollectionKey collectionKey, List mediaItems) { Option 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 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 + { + public ProgramScheduleItemCollectionType CollectionType { get; set; } + public int Id { get; set; } + } } } diff --git a/ErsatzTV.Core/Scheduling/TelevisionSeasonMediaCollection.cs b/ErsatzTV.Core/Scheduling/TelevisionSeasonMediaCollection.cs new file mode 100644 index 000000000..037e2e24a --- /dev/null +++ b/ErsatzTV.Core/Scheduling/TelevisionSeasonMediaCollection.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Core.Scheduling +{ + public class TelevisionSeasonMediaCollection : MediaCollection + { + public int TelevisionSeasonId { get; set; } + } +} diff --git a/ErsatzTV.Core/Scheduling/TelevisionShowMediaCollection.cs b/ErsatzTV.Core/Scheduling/TelevisionShowMediaCollection.cs new file mode 100644 index 000000000..8693ebb2a --- /dev/null +++ b/ErsatzTV.Core/Scheduling/TelevisionShowMediaCollection.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Core.Scheduling +{ + public class TelevisionShowMediaCollection : MediaCollection + { + public int TelevisionShowId { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs index b25a39e45..8585e9207 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs @@ -6,11 +6,8 @@ namespace ErsatzTV.Infrastructure.Data.Configurations { public class PlayoutProgramScheduleAnchorConfiguration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey(a => new { a.PlayoutId, a.ProgramScheduleId, ContentGroupId = a.MediaCollectionId }); - - builder.OwnsOne(a => a.EnumeratorState); - } + public void Configure(EntityTypeBuilder builder) => + builder.OwnsOne(a => a.EnumeratorState) + .WithOwner(); } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemConfiguration.cs index 08ef84828..f3c72b358 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleItemConfiguration.cs @@ -6,7 +6,24 @@ namespace ErsatzTV.Infrastructure.Data.Configurations { public class ProgramScheduleItemConfiguration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) => + public void Configure(EntityTypeBuilder 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); + } } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs index c5572c34b..2cb83e096 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs @@ -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 _dbContext.MediaItems.Remove(mediaItem); return _dbContext.SaveChangesAsync(); }).ToUnit(); - - - public Task> 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()) - }; - - public Task 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) - }; } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs index 028124bf2..42e3657d9 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs @@ -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 .SingleOrDefaultAsync(); } - // TODO: does this actually work? public Task> GetPlayoutItems(int playoutId) => _dbContext.PlayoutItems .Include(i => i.MediaItem) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs index 7cb8150e5..315abeaac 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs @@ -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(); } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs index 0ca8c2935..8b7f3d42f 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs @@ -35,6 +35,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories return await _dbContext.SaveChangesAsync() > 0; } + public Task> GetAllShows() => + _dbContext.TelevisionShows + .AsNoTracking() + .Include(s => s.Metadata) + .ToListAsync(); + public Task> GetShow(int televisionShowId) => _dbContext.TelevisionShows .AsNoTracking() @@ -57,6 +63,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Take(pageSize) .ToListAsync(); + public Task> GetAllSeasons() => + _dbContext.TelevisionSeasons + .AsNoTracking() + .Include(s => s.TelevisionShow) + .ThenInclude(s => s.Metadata) + .ToListAsync(); + public Task> GetSeason(int televisionSeasonId) => _dbContext.TelevisionSeasons .AsNoTracking() @@ -230,10 +243,44 @@ namespace ErsatzTV.Infrastructure.Data.Repositories }) .ToUnit(); - public Task Delete(TelevisionShow show) => - _dbContext.TelevisionShows.Remove(show).AsTask() - .Bind(_ => _dbContext.SaveChangesAsync()) - .ToUnit(); + public async Task> GetShowItems(int televisionShowId) + { + // TODO: would be nice to get the media items in one go, but ef... + List 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> GetSeasonItems(int televisionSeasonId) + { + // TODO: would be nice to get the media items in one go, but ef... + List 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> AddSeason( TelevisionShow show, diff --git a/ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.Designer.cs new file mode 100644 index 000000000..cc3780976 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.Designer.cs @@ -0,0 +1,1305 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ErsatzTV.Infrastructure.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20210220220723_ScheduleCollectionTypes")] + partial class ScheduleCollectionTypes + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.3"); + + modelBuilder.Entity("ErsatzTV.Core.AggregateModels.GenericIntegerId", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.ToView("No table or view exists for GenericIntegerId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaCollectionSummary", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("IsSimple") + .HasColumnType("INTEGER"); + + b.Property("ItemCount") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.ToView("No table or view exists for MediaCollectionSummary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaItemSummary", b => + { + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Subtitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.ToView("No table or view exists for MediaItemSummary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("Logo") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("Number") + .IsUnique(); + + b.ToTable("Channels"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("ConfigElements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioBitrate") + .HasColumnType("INTEGER"); + + b.Property("AudioBufferSize") + .HasColumnType("INTEGER"); + + b.Property("AudioChannels") + .HasColumnType("INTEGER"); + + b.Property("AudioCodec") + .HasColumnType("TEXT"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("AudioVolume") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .HasColumnType("INTEGER"); + + b.Property("NormalizeAudioCodec") + .HasColumnType("INTEGER"); + + b.Property("NormalizeResolution") + .HasColumnType("INTEGER"); + + b.Property("NormalizeVideoCodec") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("Transcode") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfiles"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MediaCollections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("PosterLastWriteTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaSourceId"); + + b.ToTable("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasColumnType("TEXT"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("Premiered") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MovieId") + .IsUnique(); + + b.ToTable("MovieMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ProgramSchedulePlayoutType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.ToTable("PlayoutItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("PlayoutProgramScheduleItemAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexMediaSourceConnections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MediaType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexMediaSourceLibraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionPlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedules"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("TelevisionSeasonId") + .HasColumnType("INTEGER"); + + b.Property("TelevisionShowId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaCollectionId"); + + b.HasIndex("ProgramScheduleId"); + + b.HasIndex("TelevisionSeasonId"); + + b.HasIndex("TelevisionShowId"); + + b.ToTable("ProgramScheduleItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Resolutions"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionEpisodeMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Aired") + .HasColumnType("TEXT"); + + b.Property("Episode") + .HasColumnType("INTEGER"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("Season") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("TelevisionEpisodeId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TelevisionEpisodeId") + .IsUnique(); + + b.ToTable("TelevisionEpisodeMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionSeason", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("PosterLastWriteTime") + .HasColumnType("TEXT"); + + b.Property("TelevisionShowId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TelevisionShowId"); + + b.ToTable("TelevisionSeasons"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Poster") + .HasColumnType("TEXT"); + + b.Property("PosterLastWriteTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TelevisionShows"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("TelevisionShowId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TelevisionShowId") + .IsUnique(); + + b.ToTable("TelevisionShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TelevisionShowId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TelevisionShowId"); + + b.ToTable("TelevisionShowSource"); + + b.HasDiscriminator("Discriminator").HasValue("TelevisionShowSource"); + }); + + modelBuilder.Entity("MovieMediaItemSimpleMediaCollection", b => + { + b.Property("MoviesId") + .HasColumnType("INTEGER"); + + b.Property("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.HasKey("MoviesId", "SimpleMediaCollectionsId"); + + b.HasIndex("SimpleMediaCollectionsId"); + + b.ToTable("SimpleMediaCollectionMovies"); + }); + + modelBuilder.Entity("SimpleMediaCollectionTelevisionEpisodeMediaItem", b => + { + b.Property("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.Property("TelevisionEpisodesId") + .HasColumnType("INTEGER"); + + b.HasKey("SimpleMediaCollectionsId", "TelevisionEpisodesId"); + + b.HasIndex("TelevisionEpisodesId"); + + b.ToTable("SimpleMediaCollectionEpisodes"); + }); + + modelBuilder.Entity("SimpleMediaCollectionTelevisionSeason", b => + { + b.Property("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.Property("TelevisionSeasonsId") + .HasColumnType("INTEGER"); + + b.HasKey("SimpleMediaCollectionsId", "TelevisionSeasonsId"); + + b.HasIndex("TelevisionSeasonsId"); + + b.ToTable("SimpleMediaCollectionSeasons"); + }); + + modelBuilder.Entity("SimpleMediaCollectionTelevisionShow", b => + { + b.Property("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.Property("TelevisionShowsId") + .HasColumnType("INTEGER"); + + b.HasKey("SimpleMediaCollectionsId", "TelevisionShowsId"); + + b.HasIndex("TelevisionShowsId"); + + b.ToTable("SimpleMediaCollectionShows"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection"); + + b.ToTable("SimpleMediaCollections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMediaItem", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("MetadataId") + .HasColumnType("INTEGER"); + + b.ToTable("Movies"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("TelevisionEpisodes"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("Folder") + .HasColumnType("TEXT"); + + b.Property("MediaType") + .HasColumnType("INTEGER"); + + b.ToTable("LocalMediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("ClientIdentifier") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.ToTable("PlexMediaSources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("OfflineTail") + .HasColumnType("INTEGER"); + + b.Property("PlayoutDuration") + .HasColumnType("TEXT"); + + b.ToTable("ProgramScheduleDurationItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalTelevisionShowSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.TelevisionShowSource"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasIndex("MediaSourceId"); + + b.HasDiscriminator().HasValue("LocalTelevisionShowSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") + .WithMany() + .HasForeignKey("FFmpegProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FFmpegProfile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") + .WithMany() + .HasForeignKey("ResolutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", "Source") + .WithMany() + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.MediaItemStatistics", "Statistics", b1 => + { + b1.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b1.Property("AudioCodec") + .HasColumnType("TEXT"); + + b1.Property("DisplayAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("Duration") + .HasColumnType("TEXT"); + + b1.Property("Height") + .HasColumnType("INTEGER"); + + b1.Property("LastWriteTime") + .HasColumnType("TEXT"); + + b1.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b1.Property("VideoCodec") + .HasColumnType("TEXT"); + + b1.Property("VideoScanType") + .HasColumnType("INTEGER"); + + b1.Property("Width") + .HasColumnType("INTEGER"); + + b1.HasKey("MediaItemId"); + + b1.ToTable("MediaItems"); + + b1.WithOwner() + .HasForeignKey("MediaItemId"); + }); + + b.Navigation("Source"); + + b.Navigation("Statistics"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.MovieMediaItem", "Movie") + .WithOne("Metadata") + .HasForeignKey("ErsatzTV.Core.Domain.MovieMetadata", "MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel") + .WithMany("Playouts") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Playouts") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + { + b1.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("NextScheduleItemId") + .HasColumnType("INTEGER"); + + b1.Property("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.HasIndex("NextScheduleItemId"); + + b1.ToTable("Playouts"); + + b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem") + .WithMany() + .HasForeignKey("NextScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.Navigation("NextScheduleItem"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAnchors") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany() + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.MediaCollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutProgramScheduleAnchorId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorId"); + + b1.ToTable("PlayoutProgramScheduleItemAnchors"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorId"); + }); + + b.Navigation("EnumeratorState"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null) + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null) + .WithMany("Libraries") + .HasForeignKey("PlexMediaSourceId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection") + .WithMany() + .HasForeignKey("MediaCollectionId"); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .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("ErsatzTV.Core.Domain.TelevisionEpisodeMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", "TelevisionEpisode") + .WithOne("Metadata") + .HasForeignKey("ErsatzTV.Core.Domain.TelevisionEpisodeMetadata", "TelevisionEpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TelevisionEpisode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionSeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", "TelevisionShow") + .WithMany("Seasons") + .HasForeignKey("TelevisionShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TelevisionShow"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", "TelevisionShow") + .WithOne("Metadata") + .HasForeignKey("ErsatzTV.Core.Domain.TelevisionShowMetadata", "TelevisionShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TelevisionShow"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", "TelevisionShow") + .WithMany("Sources") + .HasForeignKey("TelevisionShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TelevisionShow"); + }); + + modelBuilder.Entity("MovieMediaItemSimpleMediaCollection", b => + { + b.HasOne("ErsatzTV.Core.Domain.MovieMediaItem", null) + .WithMany() + .HasForeignKey("MoviesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null) + .WithMany() + .HasForeignKey("SimpleMediaCollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SimpleMediaCollectionTelevisionEpisodeMediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null) + .WithMany() + .HasForeignKey("SimpleMediaCollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", null) + .WithMany() + .HasForeignKey("TelevisionEpisodesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SimpleMediaCollectionTelevisionSeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null) + .WithMany() + .HasForeignKey("SimpleMediaCollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.TelevisionSeason", null) + .WithMany() + .HasForeignKey("TelevisionSeasonsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SimpleMediaCollectionTelevisionShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null) + .WithMany() + .HasForeignKey("SimpleMediaCollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", null) + .WithMany() + .HasForeignKey("TelevisionShowsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.SimpleMediaCollection", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.MovieMediaItem", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.TelevisionSeason", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalTelevisionShowSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.LocalMediaSource", "MediaSource") + .WithMany() + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionSeason", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShow", b => + { + b.Navigation("Metadata"); + + b.Navigation("Seasons"); + + b.Navigation("Sources"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMediaItem", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("Libraries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.cs b/ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.cs new file mode 100644 index 000000000..22c5aabe8 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.cs @@ -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( + "MediaCollectionId", + "ProgramScheduleItems", + "INTEGER", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + + migrationBuilder.AddColumn( + "CollectionType", + "ProgramScheduleItems", + "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + "TelevisionSeasonId", + "ProgramScheduleItems", + "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + "TelevisionShowId", + "ProgramScheduleItems", + "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + "Id", + "PlayoutProgramScheduleItemAnchors", + "INTEGER", + nullable: false, + defaultValue: 0) + .Annotation("Sqlite:Autoincrement", true); + + migrationBuilder.AddColumn( + "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( + "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); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 1b09d5314..ebc91a0a9 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -370,18 +370,25 @@ namespace ErsatzTV.Infrastructure.Migrations "ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => { - b.Property("PlayoutId") + b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("ProgramScheduleId") + b.Property("CollectionId") .HasColumnType("INTEGER"); - b.Property("MediaCollectionId") + b.Property("CollectionType") .HasColumnType("INTEGER"); - b.HasKey("PlayoutId", "ProgramScheduleId", "MediaCollectionId"); + b.Property("PlayoutId") + .HasColumnType("INTEGER"); - b.HasIndex("MediaCollectionId"); + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayoutId"); b.HasIndex("ProgramScheduleId"); @@ -469,10 +476,13 @@ namespace ErsatzTV.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("CollectionType") + .HasColumnType("INTEGER"); + b.Property("Index") .HasColumnType("INTEGER"); - b.Property("MediaCollectionId") + b.Property("MediaCollectionId") .HasColumnType("INTEGER"); b.Property("ProgramScheduleId") @@ -481,12 +491,22 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("StartTime") .HasColumnType("TEXT"); + b.Property("TelevisionSeasonId") + .HasColumnType("INTEGER"); + + b.Property("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 "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 "EnumeratorState", b1 => { - b1.Property("PlayoutProgramScheduleAnchorPlayoutId") - .HasColumnType("INTEGER"); - - b1.Property("PlayoutProgramScheduleAnchorProgramScheduleId") - .HasColumnType("INTEGER"); - - b1.Property("PlayoutProgramScheduleAnchorMediaCollectionId") + b1.Property("PlayoutProgramScheduleAnchorId") .HasColumnType("INTEGER"); b1.Property("Index") @@ -1075,24 +1083,16 @@ namespace ErsatzTV.Infrastructure.Migrations b1.Property("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 { 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 .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( diff --git a/ErsatzTV.sln.DotSettings b/ErsatzTV.sln.DotSettings index 302e12cb6..b3fe1e0f3 100644 --- a/ErsatzTV.sln.DotSettings +++ b/ErsatzTV.sln.DotSettings @@ -1,4 +1,5 @@  + True DTO HDHR SAR diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index 4ebab80ad..96955e2be 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -20,7 +20,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/ErsatzTV/Pages/ScheduleItemsEditor.razor b/ErsatzTV/Pages/ScheduleItemsEditor.razor index b37b91871..41a231f37 100644 --- a/ErsatzTV/Pages/ScheduleItemsEditor.razor +++ b/ErsatzTV/Pages/ScheduleItemsEditor.razor @@ -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 Logger @inject ISnackbar Snackbar @@ -31,12 +33,12 @@ @(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic") - + - @context.MediaCollection.Name + @context.CollectionName - + @context.PlayoutMode @if (context.PlayoutMode == PlayoutMode.Multiple && context.MultipleCount.HasValue) @@ -53,7 +55,7 @@ - + Add Schedule Item @@ -74,7 +76,39 @@ } - + + @foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues()) + { + @collectionType + } + + @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection) + { + + } + @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow) + { + + } + @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason) + { + + } @foreach (PlayoutMode playoutMode in Enum.GetValues()) { @@ -99,7 +133,8 @@ private ProgramScheduleItemsEditViewModel _schedule; private List _mediaCollections; - private Option _defaultCollection; + private List _televisionShows; + private List _televisionSeasons; private ProgramScheduleItemEditViewModel _selectedItem; @@ -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 maybeSchedule = await Mediator.Send(new GetProgramScheduleById(Id)); @@ -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 @@ { 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 @@ private Task> SearchMediaCollections(string value) => _mediaCollections.Filter(c => c.Name.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask(); + private Task> SearchTelevisionShows(string value) => + _televisionShows.Filter(s => s.Title.Contains(value ?? string.Empty, StringComparison.OrdinalIgnoreCase)).AsTask(); + + private Task> 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 @@ 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 @@ 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")); diff --git a/ErsatzTV/Pages/Schedules.razor b/ErsatzTV/Pages/Schedules.razor index 484efd636..803985768 100644 --- a/ErsatzTV/Pages/Schedules.razor +++ b/ErsatzTV/Pages/Schedules.razor @@ -61,7 +61,7 @@ @(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic") - @context.MediaCollection.Name + @context.Name @context.PlayoutMode diff --git a/ErsatzTV/Properties/Annotations.cs b/ErsatzTV/Properties/Annotations.cs new file mode 100644 index 000000000..928c91c46 --- /dev/null +++ b/ErsatzTV/Properties/Annotations.cs @@ -0,0 +1,1460 @@ +/* MIT License + +Copyright (c) 2016 JetBrains http://www.jetbrains.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +using System; + +// ReSharper disable InheritdocConsiderUsage + +#pragma warning disable 1591 +// ReSharper disable UnusedMember.Global +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable IntroduceOptionalParameters.Global +// ReSharper disable MemberCanBeProtected.Global +// ReSharper disable InconsistentNaming + +namespace ErsatzTV.Annotations +{ + /// + /// Indicates that the value of the marked element could be null sometimes, + /// so checking for null is required before its usage. + /// + /// + /// + /// [CanBeNull] object Test() => null; + /// + /// void UseTest() { + /// var p = Test(); + /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' + /// } + /// + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class CanBeNullAttribute : Attribute + { + } + + /// + /// Indicates that the value of the marked element can never be null. + /// + /// + /// + /// [NotNull] object Foo() { + /// return null; // Warning: Possible 'null' assignment + /// } + /// + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class NotNullAttribute : Attribute + { + } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can never be null. + /// + /// + /// + /// public void Foo([ItemNotNull]List<string> books) + /// { + /// foreach (var book in books) { + /// if (book != null) // Warning: Expression is always true + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemNotNullAttribute : Attribute + { + } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can be null. + /// + /// + /// + /// public void Foo([ItemCanBeNull]List<string> books) + /// { + /// foreach (var book in books) + /// { + /// // Warning: Possible 'System.NullReferenceException' + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemCanBeNullAttribute : Attribute + { + } + + /// + /// Indicates that the marked method builds string by the format pattern and (optional) arguments. + /// The parameter, which contains the format string, should be given in constructor. The format string + /// should be in -like form. + /// + /// + /// + /// [StringFormatMethod("message")] + /// void ShowError(string message, params object[] args) { /* do something */ } + /// + /// void Foo() { + /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string + /// } + /// + /// + [AttributeUsage( + AttributeTargets.Constructor | AttributeTargets.Method | + AttributeTargets.Property | AttributeTargets.Delegate)] + public sealed class StringFormatMethodAttribute : Attribute + { + /// + /// Specifies which parameter of an annotated method should be treated as the format string + /// + public StringFormatMethodAttribute( + [NotNull] + string formatParameterName) => FormatParameterName = formatParameterName; + + [NotNull] + public string FormatParameterName { get; } + } + + /// + /// Use this annotation to specify a type that contains static or const fields + /// with values for the annotated property/field/parameter. + /// The specified type will be used to improve completion suggestions. + /// + /// + /// + /// namespace TestNamespace + /// { + /// public class Constants + /// { + /// public static int INT_CONST = 1; + /// public const string STRING_CONST = "1"; + /// } + /// + /// public class Class1 + /// { + /// [ValueProvider("TestNamespace.Constants")] public int myField; + /// public void Foo([ValueProvider("TestNamespace.Constants")] string str) { } + /// + /// public void Test() + /// { + /// Foo(/*try completion here*/);// + /// myField = /*try completion here*/ + /// } + /// } + /// } + /// + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field, + AllowMultiple = true)] + public sealed class ValueProviderAttribute : Attribute + { + public ValueProviderAttribute( + [NotNull] + string name) => Name = name; + + [NotNull] + public string Name { get; } + } + + /// + /// Indicates that the integral value falls into the specified interval. + /// It's allowed to specify multiple non-intersecting intervals. + /// Values of interval boundaries are inclusive. + /// + /// + /// + /// void Foo([ValueRange(0, 100)] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate, + AllowMultiple = true)] + public sealed class ValueRangeAttribute : Attribute + { + public ValueRangeAttribute(long from, long to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(ulong from, ulong to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(long value) => From = To = value; + + public ValueRangeAttribute(ulong value) => From = To = value; + + public object From { get; } + public object To { get; } + } + + /// + /// Indicates that the integral value never falls below zero. + /// + /// + /// + /// void Foo([NonNegativeValue] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate)] + public sealed class NonNegativeValueAttribute : Attribute + { + } + + /// + /// Indicates that the function argument should be a string literal and match one + /// of the parameters of the caller function. For example, ReSharper annotates + /// the parameter of . + /// + /// + /// + /// void Foo(string param) { + /// if (param == null) + /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol + /// } + /// + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InvokerParameterNameAttribute : Attribute + { + } + + /// + /// Indicates that the method is contained in a type that implements + /// System.ComponentModel.INotifyPropertyChanged interface and this method + /// is used to notify that some property value changed. + /// + /// + /// The method should be non-static and conform to one of the supported signatures: + /// + /// + /// NotifyChanged(string) + /// + /// + /// NotifyChanged(params string[]) + /// + /// + /// NotifyChanged{T}(Expression{Func{T}}) + /// + /// + /// NotifyChanged{T,U}(Expression{Func{T,U}}) + /// + /// + /// SetProperty{T}(ref T, T, string) + /// + /// + /// + /// + /// + /// public class Foo : INotifyPropertyChanged { + /// public event PropertyChangedEventHandler PropertyChanged; + /// + /// [NotifyPropertyChangedInvocator] + /// protected virtual void NotifyChanged(string propertyName) { ... } + /// + /// string _name; + /// + /// public string Name { + /// get { return _name; } + /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } + /// } + /// } + /// + /// Examples of generated notifications: + /// + /// + /// NotifyChanged("Property") + /// + /// + /// NotifyChanged(() => Property) + /// + /// + /// NotifyChanged((VM x) => x.Property) + /// + /// + /// SetProperty(ref myField, value, "Property") + /// + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute + { + public NotifyPropertyChangedInvocatorAttribute() + { + } + + public NotifyPropertyChangedInvocatorAttribute( + [NotNull] + string parameterName) => ParameterName = parameterName; + + [CanBeNull] + public string ParameterName { get; } + } + + /// + /// Describes dependency between method input and output. + /// + /// + ///

Function Definition Table syntax:

+ /// + /// FDT ::= FDTRow [;FDTRow]* + /// FDTRow ::= Input => Output | Output <= Input + /// Input ::= ParameterName: Value [, Input]* + /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} + /// Value ::= true | false | null | notnull | canbenull + /// + /// If the method has a single input parameter, its name could be omitted.
+ /// Using halt (or void/nothing, which is the same) for the method output + /// means that the method doesn't return normally (throws or terminates the process).
+ /// Value canbenull is only applicable for output parameters.
+ /// You can use multiple [ContractAnnotation] for each FDT row, or use single attribute + /// with rows separated by semicolon. There is no notion of order rows, all rows are checked + /// for applicability and applied per each program state tracked by the analysis engine.
+ ///
+ /// + /// + /// + /// + /// [ContractAnnotation("=> halt")] + /// public void TerminationMethod() + /// + /// + /// + /// + /// [ContractAnnotation("null <= param:null")] // reverse condition syntax + /// public string GetName(string surname) + /// + /// + /// + /// + /// [ContractAnnotation("s:null => true")] + /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() + /// + /// + /// + /// + /// // A method that returns null if the parameter is null, + /// // and not null if the parameter is not null + /// [ContractAnnotation("null => null; notnull => notnull")] + /// public object Transform(object data) + /// + /// + /// + /// + /// [ContractAnnotation("=> true, result: notnull; => false, result: null")] + /// public bool TryParse(string s, out Person result) + /// + /// + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public sealed class ContractAnnotationAttribute : Attribute + { + public ContractAnnotationAttribute( + [NotNull] + string contract) + : this(contract, false) + { + } + + public ContractAnnotationAttribute( + [NotNull] + string contract, + bool forceFullStates) + { + Contract = contract; + ForceFullStates = forceFullStates; + } + + [NotNull] + public string Contract { get; } + + public bool ForceFullStates { get; } + } + + /// + /// Indicates whether the marked element should be localized. + /// + /// + /// + /// [LocalizationRequiredAttribute(true)] + /// class Foo { + /// string str = "my string"; // Warning: Localizable string + /// } + /// + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class LocalizationRequiredAttribute : Attribute + { + public LocalizationRequiredAttribute() : this(true) + { + } + + public LocalizationRequiredAttribute(bool required) => Required = required; + + public bool Required { get; } + } + + /// + /// Indicates that the value of the marked type (or its derivatives) + /// cannot be compared using '==' or '!=' operators and Equals() + /// should be used instead. However, using '==' or '!=' for comparison + /// with null is always permitted. + /// + /// + /// + /// [CannotApplyEqualityOperator] + /// class NoEquality { } + /// + /// class UsesNoEquality { + /// void Test() { + /// var ca1 = new NoEquality(); + /// var ca2 = new NoEquality(); + /// if (ca1 != null) { // OK + /// bool condition = ca1 == ca2; // Warning + /// } + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] + public sealed class CannotApplyEqualityOperatorAttribute : Attribute + { + } + + /// + /// When applied to a target attribute, specifies a requirement for any type marked + /// with the target attribute to implement or inherit specific type or types. + /// + /// + /// + /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement + /// class ComponentAttribute : Attribute { } + /// + /// [Component] // ComponentAttribute requires implementing IComponent interface + /// class MyComponent : IComponent { } + /// + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [BaseTypeRequired(typeof(Attribute))] + public sealed class BaseTypeRequiredAttribute : Attribute + { + public BaseTypeRequiredAttribute( + [NotNull] + Type baseType) => BaseType = baseType; + + [NotNull] + public Type BaseType { get; } + } + + /// + /// Indicates that the marked symbol is used implicitly (e.g. via reflection, in external library), + /// so this symbol will not be reported as unused (as well as by other usage inspections). + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class UsedImplicitlyAttribute : Attribute + { + public UsedImplicitlyAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) + { + } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) + { + } + + public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) + { + } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + public ImplicitUseKindFlags UseKindFlags { get; } + + public ImplicitUseTargetFlags TargetFlags { get; } + } + + /// + /// Can be applied to attributes, type parameters, and parameters of a type assignable from + /// . + /// When applied to an attribute, the decorated attribute behaves the same as . + /// When applied to a type parameter or to a parameter of type , indicates that the + /// corresponding type + /// is used implicitly. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter | AttributeTargets.Parameter)] + public sealed class MeansImplicitUseAttribute : Attribute + { + public MeansImplicitUseAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) + { + } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) + { + } + + public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) + { + } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + [UsedImplicitly] + public ImplicitUseKindFlags UseKindFlags { get; } + + [UsedImplicitly] + public ImplicitUseTargetFlags TargetFlags { get; } + } + + /// + /// Specify the details of implicitly used symbol when it is marked + /// with or . + /// + [Flags] + public enum ImplicitUseKindFlags + { + Default = Access | Assign | InstantiatedWithFixedConstructorSignature, + + /// Only entity marked with attribute considered used. + Access = 1, + + /// Indicates implicit assignment to a member. + Assign = 2, + + /// + /// Indicates implicit instantiation of a type with fixed constructor signature. + /// That means any unused constructor parameters won't be reported as such. + /// + InstantiatedWithFixedConstructorSignature = 4, + + /// Indicates implicit instantiation of a type. + InstantiatedNoFixedConstructorSignature = 8 + } + + /// + /// Specify what is considered to be used implicitly when marked + /// with or . + /// + [Flags] + public enum ImplicitUseTargetFlags + { + Default = Itself, + Itself = 1, + + /// Members of entity marked with attribute are considered used. + Members = 2, + + /// Inherited entities are considered used. + WithInheritors = 4, + + /// Entity marked with attribute and all its members considered used. + WithMembers = Itself | Members + } + + /// + /// This attribute is intended to mark publicly available API + /// which should not be removed and so is treated as used. + /// + [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class PublicAPIAttribute : Attribute + { + public PublicAPIAttribute() + { + } + + public PublicAPIAttribute( + [NotNull] + string comment) => Comment = comment; + + [CanBeNull] + public string Comment { get; } + } + + /// + /// Tells code analysis engine if the parameter is completely handled when the invoked method is on stack. + /// If the parameter is a delegate, indicates that delegate is executed while the method is executed. + /// If the parameter is an enumerable, indicates that it is enumerated while the method is executed. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InstantHandleAttribute : Attribute + { + } + + /// + /// Indicates that a method does not make any observable state changes. + /// The same as System.Diagnostics.Contracts.PureAttribute. + /// + /// + /// + /// [Pure] int Multiply(int x, int y) => x * y; + /// + /// void M() { + /// Multiply(123, 42); // Warning: Return value of pure method is not used + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class PureAttribute : Attribute + { + } + + /// + /// Indicates that the return value of the method invocation must be used. + /// + /// + /// Methods decorated with this attribute (in contrast to pure methods) might change state, + /// but make no sense without using their return value.
+ /// Similarly to , this attribute + /// will help detecting usages of the method when the return value in not used. + /// Additionally, you can optionally specify a custom message, which will be used when showing warnings, e.g. + /// [MustUseReturnValue("Use the return value to...")]. + ///
+ [AttributeUsage(AttributeTargets.Method)] + public sealed class MustUseReturnValueAttribute : Attribute + { + public MustUseReturnValueAttribute() + { + } + + public MustUseReturnValueAttribute( + [NotNull] + string justification) => Justification = justification; + + [CanBeNull] + public string Justification { get; } + } + + /// + /// Indicates the type member or parameter of some type, that should be used instead of all other ways + /// to get the value of that type. This annotation is useful when you have some "context" value evaluated + /// and stored somewhere, meaning that all other ways to get this value must be consolidated with existing one. + /// + /// + /// + /// class Foo { + /// [ProvidesContext] IBarService _barService = ...; + /// + /// void ProcessNode(INode node) { + /// DoSomething(node, node.GetGlobalServices().Bar); + /// // ^ Warning: use value of '_barService' field + /// } + /// } + /// + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Method | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | + AttributeTargets.GenericParameter)] + public sealed class ProvidesContextAttribute : Attribute + { + } + + /// + /// Indicates that a parameter is a path to a file or a folder within a web project. + /// Path can be relative or absolute, starting from web root (~). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class PathReferenceAttribute : Attribute + { + public PathReferenceAttribute() + { + } + + public PathReferenceAttribute( + [NotNull] [PathReference] + string basePath) => BasePath = basePath; + + [CanBeNull] + public string BasePath { get; } + } + + /// + /// An extension method marked with this attribute is processed by code completion + /// as a 'Source Template'. When the extension method is completed over some expression, its source code + /// is automatically expanded like a template at call site. + /// + /// + /// Template method body can contain valid source code and/or special comments starting with '$'. + /// Text inside these comments is added as source code when the template is applied. Template parameters + /// can be used either as additional method parameters or as identifiers wrapped in two '$' signs. + /// Use the attribute to specify macros for parameters. + /// + /// + /// In this example, the 'forEach' method is a source template available over all values + /// of enumerable types, producing ordinary C# 'foreach' statement and placing caret inside block: + /// + /// [SourceTemplate] + /// public static void forEach<T>(this IEnumerable<T> xs) { + /// foreach (var x in xs) { + /// //$ $END$ + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class SourceTemplateAttribute : Attribute + { + } + + /// + /// Allows specifying a macro for a parameter of a source template. + /// + /// + /// You can apply the attribute on the whole method or on any of its additional parameters. The macro expression + /// is defined in the property. When applied on a method, the target + /// template parameter is defined in the property. To apply the macro silently + /// for the parameter, set the property value = -1. + /// + /// + /// Applying the attribute on a source template method: + /// + /// [SourceTemplate, Macro(Target = "item", Expression = "suggestVariableName()")] + /// public static void forEach<T>(this IEnumerable<T> collection) { + /// foreach (var item in collection) { + /// //$ $END$ + /// } + /// } + /// + /// Applying the attribute on a template method parameter: + /// + /// [SourceTemplate] + /// public static void something(this Entity x, [Macro(Expression = "guid()", Editable = -1)] string newguid) { + /// /*$ var $x$Id = "$newguid$" + x.ToString(); + /// x.DoSomething($x$Id); */ + /// } + /// + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = true)] + public sealed class MacroAttribute : Attribute + { + /// + /// Allows specifying a macro that will be executed for a source template + /// parameter when the template is expanded. + /// + [CanBeNull] + public string Expression { get; set; } + + /// + /// Allows specifying which occurrence of the target parameter becomes editable when the template is deployed. + /// + /// + /// If the target parameter is used several times in the template, only one occurrence becomes editable; + /// other occurrences are changed synchronously. To specify the zero-based index of the editable occurrence, + /// use values >= 0. To make the parameter non-editable when the template is expanded, use -1. + /// + public int Editable { get; set; } + + /// + /// Identifies the target parameter of a source template if the + /// is applied on a template method. + /// + [CanBeNull] + public string Target { get; set; } + } + + [AttributeUsage( + AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, + AllowMultiple = true)] + public sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute + { + public AspMvcAreaMasterLocationFormatAttribute( + [NotNull] + string format) => Format = format; + + [NotNull] + public string Format { get; } + } + + [AttributeUsage( + AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, + AllowMultiple = true)] + public sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute + { + public AspMvcAreaPartialViewLocationFormatAttribute( + [NotNull] + string format) => Format = format; + + [NotNull] + public string Format { get; } + } + + [AttributeUsage( + AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, + AllowMultiple = true)] + public sealed class AspMvcAreaViewLocationFormatAttribute : Attribute + { + public AspMvcAreaViewLocationFormatAttribute( + [NotNull] + string format) => Format = format; + + [NotNull] + public string Format { get; } + } + + [AttributeUsage( + AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, + AllowMultiple = true)] + public sealed class AspMvcMasterLocationFormatAttribute : Attribute + { + public AspMvcMasterLocationFormatAttribute( + [NotNull] + string format) => Format = format; + + [NotNull] + public string Format { get; } + } + + [AttributeUsage( + AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, + AllowMultiple = true)] + public sealed class AspMvcPartialViewLocationFormatAttribute : Attribute + { + public AspMvcPartialViewLocationFormatAttribute( + [NotNull] + string format) => Format = format; + + [NotNull] + public string Format { get; } + } + + [AttributeUsage( + AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, + AllowMultiple = true)] + public sealed class AspMvcViewLocationFormatAttribute : Attribute + { + public AspMvcViewLocationFormatAttribute( + [NotNull] + string format) => Format = format; + + [NotNull] + public string Format { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC action. If applied to a method, the MVC action name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcActionAttribute : Attribute + { + public AspMvcActionAttribute() + { + } + + public AspMvcActionAttribute( + [NotNull] + string anonymousProperty) => AnonymousProperty = anonymousProperty; + + [CanBeNull] + public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC area. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcAreaAttribute : Attribute + { + public AspMvcAreaAttribute() + { + } + + public AspMvcAreaAttribute( + [NotNull] + string anonymousProperty) => AnonymousProperty = anonymousProperty; + + [CanBeNull] + public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is + /// an MVC controller. If applied to a method, the MVC controller name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String). + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcControllerAttribute : Attribute + { + public AspMvcControllerAttribute() + { + } + + public AspMvcControllerAttribute( + [NotNull] + string anonymousProperty) => AnonymousProperty = anonymousProperty; + + [CanBeNull] + public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC Master. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcMasterAttribute : Attribute + { + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC model type. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, Object). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcModelTypeAttribute : Attribute + { + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is an MVC + /// partial view. If applied to a method, the MVC partial view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String). + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcPartialViewAttribute : Attribute + { + } + + /// + /// ASP.NET MVC attribute. Allows disabling inspections for MVC views within a class or a method. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class AspMvcSuppressViewErrorAttribute : Attribute + { + } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcDisplayTemplateAttribute : Attribute + { + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC editor template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcEditorTemplateAttribute : Attribute + { + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC template. + /// Use this attribute for custom wrappers similar to + /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcTemplateAttribute : Attribute + { + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component. If applied to a method, the MVC view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Controller.View(Object). + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewAttribute : Attribute + { + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component name. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewComponentAttribute : Attribute + { + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component view. If applied to a method, the MVC view component view name is default. + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class AspMvcViewComponentViewAttribute : Attribute + { + } + + /// + /// ASP.NET MVC attribute. When applied to a parameter of an attribute, + /// indicates that this parameter is an MVC action name. + /// + /// + /// + /// [ActionName("Foo")] + /// public ActionResult Login(string returnUrl) { + /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK + /// return RedirectToAction("Bar"); // Error: Cannot resolve action + /// } + /// + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] + public sealed class AspMvcActionSelectorAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] + public sealed class HtmlElementAttributesAttribute : Attribute + { + public HtmlElementAttributesAttribute() + { + } + + public HtmlElementAttributesAttribute( + [NotNull] + string name) => Name = name; + + [CanBeNull] + public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class HtmlAttributeValueAttribute : Attribute + { + public HtmlAttributeValueAttribute( + [NotNull] + string name) => Name = name; + + [NotNull] + public string Name { get; } + } + + /// + /// Razor attribute. Indicates that the marked parameter or method is a Razor section. + /// Use this attribute for custom wrappers similar to + /// System.Web.WebPages.WebPageBase.RenderSection(String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + public sealed class RazorSectionAttribute : Attribute + { + } + + /// + /// Indicates how method, constructor invocation, or property access + /// over collection type affects the contents of the collection. + /// Use to specify the access type. + /// + /// + /// Using this attribute only makes sense if all collection methods are marked with this attribute. + /// + /// + /// + /// public class MyStringCollection : List<string> + /// { + /// [CollectionAccess(CollectionAccessType.Read)] + /// public string GetFirstString() + /// { + /// return this.ElementAt(0); + /// } + /// } + /// class Test + /// { + /// public void Foo() + /// { + /// // Warning: Contents of the collection is never updated + /// var col = new MyStringCollection(); + /// string x = col.GetFirstString(); + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property)] + public sealed class CollectionAccessAttribute : Attribute + { + public CollectionAccessAttribute(CollectionAccessType collectionAccessType) => + CollectionAccessType = collectionAccessType; + + public CollectionAccessType CollectionAccessType { get; } + } + + /// + /// Provides a value for the to define + /// how the collection method invocation affects the contents of the collection. + /// + [Flags] + public enum CollectionAccessType + { + /// Method does not use or modify content of the collection. + None = 0, + + /// Method only reads content of the collection but does not modify it. + Read = 1, + + /// Method can change content of the collection but does not add new elements. + ModifyExistingContent = 2, + + /// Method can add new elements to the collection. + UpdatedContent = ModifyExistingContent | 4 + } + + /// + /// Indicates that the marked method is assertion method, i.e. it halts the control flow if + /// one of the conditions is satisfied. To set the condition, mark one of the parameters with + /// attribute. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class AssertionMethodAttribute : Attribute + { + } + + /// + /// Indicates the condition parameter of the assertion method. The method itself should be + /// marked by attribute. The mandatory argument of + /// the attribute is the assertion type. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AssertionConditionAttribute : Attribute + { + public AssertionConditionAttribute(AssertionConditionType conditionType) => ConditionType = conditionType; + + public AssertionConditionType ConditionType { get; } + } + + /// + /// Specifies assertion type. If the assertion method argument satisfies the condition, + /// then the execution continues. Otherwise, execution is assumed to be halted. + /// + public enum AssertionConditionType + { + /// Marked parameter should be evaluated to true. + IS_TRUE = 0, + + /// Marked parameter should be evaluated to false. + IS_FALSE = 1, + + /// Marked parameter should be evaluated to null value. + IS_NULL = 2, + + /// Marked parameter should be evaluated to not null value. + IS_NOT_NULL = 3 + } + + /// + /// Indicates that the marked method unconditionally terminates control flow execution. + /// For example, it could unconditionally throw exception. + /// + [Obsolete("Use [ContractAnnotation('=> halt')] instead")] + [AttributeUsage(AttributeTargets.Method)] + public sealed class TerminatesProgramAttribute : Attribute + { + } + + /// + /// Indicates that method is pure LINQ method, with postponed enumeration (like Enumerable.Select, + /// .Where). This annotation allows inference of [InstantHandle] annotation for parameters + /// of delegate type by analyzing LINQ method chains. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class LinqTunnelAttribute : Attribute + { + } + + /// + /// Indicates that IEnumerable passed as a parameter is not enumerated. + /// Use this annotation to suppress the 'Possible multiple enumeration of IEnumerable' inspection. + /// + /// + /// + /// static void ThrowIfNull<T>([NoEnumeration] T v, string n) where T : class + /// { + /// // custom check for null but no enumeration + /// } + /// + /// void Foo(IEnumerable<string> values) + /// { + /// ThrowIfNull(values, nameof(values)); + /// var x = values.ToList(); // No warnings about multiple enumeration + /// } + /// + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class NoEnumerationAttribute : Attribute + { + } + + /// + /// Indicates that the marked parameter is a regular expression pattern. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class RegexPatternAttribute : Attribute + { + } + + /// + /// Prevents the Member Reordering feature from tossing members of the marked class. + /// + /// + /// The attribute must be mentioned in your member reordering patterns. + /// + [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.Enum)] + public sealed class NoReorderAttribute : Attribute + { + } + + /// + /// XAML attribute. Indicates the type that has ItemsSource property and should be treated + /// as ItemsControl-derived type, to enable inner items DataContext type resolve. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class XamlItemsControlAttribute : Attribute + { + } + + /// + /// XAML attribute. Indicates the property of some BindingBase-derived type, that + /// is used to bind some item of ItemsControl-derived type. This annotation will + /// enable the DataContext type resolve for XAML bindings for such properties. + /// + /// + /// Property should have the tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class XamlItemBindingOfItemsControlAttribute : Attribute + { + } + + /// + /// XAML attribute. Indicates the property of some Style-derived type, that + /// is used to style items of ItemsControl-derived type. This annotation will + /// enable the DataContext type resolve for XAML bindings for such properties. + /// + /// + /// Property should have the tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class XamlItemStyleOfItemsControlAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspChildControlTypeAttribute : Attribute + { + public AspChildControlTypeAttribute( + [NotNull] + string tagName, + [NotNull] + Type controlType) + { + TagName = tagName; + ControlType = controlType; + } + + [NotNull] + public string TagName { get; } + + [NotNull] + public Type ControlType { get; } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldsAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspMethodPropertyAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspRequiredAttributeAttribute : Attribute + { + public AspRequiredAttributeAttribute( + [NotNull] + string attribute) => Attribute = attribute; + + [NotNull] + public string Attribute { get; } + } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspTypePropertyAttribute : Attribute + { + public AspTypePropertyAttribute(bool createConstructorReferences) => + CreateConstructorReferences = createConstructorReferences; + + public bool CreateConstructorReferences { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorImportNamespaceAttribute : Attribute + { + public RazorImportNamespaceAttribute( + [NotNull] + string name) => Name = name; + + [NotNull] + public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorInjectionAttribute : Attribute + { + public RazorInjectionAttribute( + [NotNull] + string type, + [NotNull] + string fieldName) + { + Type = type; + FieldName = fieldName; + } + + [NotNull] + public string Type { get; } + + [NotNull] + public string FieldName { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorDirectiveAttribute : Attribute + { + public RazorDirectiveAttribute( + [NotNull] + string directive) => Directive = directive; + + [NotNull] + public string Directive { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorPageBaseTypeAttribute : Attribute + { + public RazorPageBaseTypeAttribute( + [NotNull] + string baseType) => BaseType = baseType; + + public RazorPageBaseTypeAttribute( + [NotNull] + string baseType, + string pageName) + { + BaseType = baseType; + PageName = pageName; + } + + [NotNull] + public string BaseType { get; } + + [CanBeNull] + public string PageName { get; } + } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorHelperCommonAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class RazorLayoutAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteLiteralMethodAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteMethodAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class RazorWriteMethodParameterAttribute : Attribute + { + } +} diff --git a/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs b/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs index c9f04651c..e3260cc8d 100644 --- a/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs +++ b/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs @@ -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 } 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 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)); } }