From 871a031467ab7e2991a1051e29770bca54cbb27a Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sun, 21 Feb 2021 18:54:41 -0600 Subject: [PATCH] rework television media (#26) * rework television media * refactor poster saving * television and movie views are working again * remove dead code * use paper styling for all cards * add show poster, plot to seasons page * remove missing shows; cleanup interfaces * fix split show display (same show in different folders/sources) * add placeholder "add to schedule" button * no more duplicate television shows, even with the same show split across sources * stop releasing CLI for now * use season number as season placeholder * add television shows to collections * add television seasons to collections * add television episodes to collections * add movies to collections * remove movies, shows, seasons, episodes from collections * fix page width and menus * fix buffer size defaults * fix chronological episode ordering * allow deleting media collections * don't get stuck building a playout with an empty collection * schedule editing and playouts work again * minor cleanup * remove dead code * fix bugs with viewing movies as they are loading * add scanner tests; support nested movie folders * update collections docs * rearrange order of schedule items * add show and season to schedule * delete schedules that use legacy collections, reset all posters * move cleanup to new migration * load fallback metadata when nfo fails; don't require metadata in ui * update readme and screenshots --- .github/workflows/release.yml | 10 +- ErsatzTV.Application/IMediaCard.cs | 9 - ErsatzTV.Application/MediaCards/Mapper.cs | 60 + .../MediaCards/MediaCardViewModel.cs | 4 + .../MediaCards/MovieCardResultsViewModel.cs | 6 + .../MediaCards/MovieCardViewModel.cs | 11 + .../MediaCards/Queries/GetMovieCards.cs | 6 + .../Queries/GetMovieCardsHandler.cs | 32 + .../Queries/GetSimpleMediaCollectionCards.cs | 9 + .../GetSimpleMediaCollectionCardsHandler.cs | 26 + .../Queries/GetTelevisionEpisodeCards.cs | 7 + .../GetTelevisionEpisodeCardsHandler.cs | 34 + .../Queries/GetTelevisionSeasonCards.cs | 7 + .../GetTelevisionSeasonCardsHandler.cs | 34 + .../Queries/GetTelevisionShowCards.cs | 6 + .../Queries/GetTelevisionShowCardsHandler.cs | 33 + ...mpleMediaCollectionCardResultsViewModel.cs | 11 + .../TelevisionEpisodeCardResultsViewModel.cs | 6 + .../TelevisionEpisodeCardViewModel.cs | 21 + .../TelevisionSeasonCardResultsViewModel.cs | 6 + .../TelevisionSeasonCardViewModel.cs | 19 + .../TelevisionShowCardResultsViewModel.cs | 6 + .../MediaCards/TelevisionShowCardViewModel.cs | 11 + .../AddItemsToSimpleMediaCollection.cs | 9 - .../AddItemsToSimpleMediaCollectionHandler.cs | 79 - .../AddMovieToSimpleMediaCollection.cs | 8 + .../AddMovieToSimpleMediaCollectionHandler.cs | 62 + ...elevisionEpisodeToSimpleMediaCollection.cs | 8 + ...onEpisodeToSimpleMediaCollectionHandler.cs | 68 + ...TelevisionSeasonToSimpleMediaCollection.cs | 8 + ...ionSeasonToSimpleMediaCollectionHandler.cs | 68 + ...ddTelevisionShowToSimpleMediaCollection.cs | 8 + ...isionShowToSimpleMediaCollectionHandler.cs | 67 + .../CreateSimpleMediaCollectionHandler.cs | 10 +- .../RemoveItemsFromSimpleMediaCollection.cs | 15 + ...veItemsFromSimpleMediaCollectionHandler.cs | 74 + .../ReplaceSimpleMediaCollectionItems.cs | 11 - ...eplaceSimpleMediaCollectionItemsHandler.cs | 65 - .../MediaCollections/Mapper.cs | 11 +- .../MediaCollectionViewModel.cs | 10 +- .../Queries/GetMediaCollectionSummaries.cs | 7 - .../GetMediaCollectionSummariesHandler.cs | 27 - .../GetSimpleMediaCollectionWithItemsById.cs | 11 - ...mpleMediaCollectionWithItemsByIdHandler.cs | 37 - .../MediaItems/AggregateMediaItemResults.cs | 6 - .../MediaItems/AggregateMediaItemViewModel.cs | 9 - .../MediaItems/Commands/CreateMediaItem.cs | 8 - .../Commands/CreateMediaItemHandler.cs | 101 -- .../MediaItems/Commands/DeleteMediaItem.cs | 9 - .../Commands/DeleteMediaItemHandler.cs | 31 - .../MediaItems/Commands/RefreshMediaItem.cs | 8 - .../Commands/RefreshMediaItemCollections.cs | 9 - .../RefreshMediaItemCollectionsHandler.cs | 46 - .../Commands/RefreshMediaItemMetadata.cs | 9 - .../RefreshMediaItemMetadataHandler.cs | 53 - .../Commands/RefreshMediaItemPoster.cs | 9 - .../Commands/RefreshMediaItemPosterHandler.cs | 45 - .../Commands/RefreshMediaItemStatistics.cs | 9 - .../RefreshMediaItemStatisticsHandler.cs | 65 - ErsatzTV.Application/MediaItems/Mapper.cs | 42 +- .../Queries/GetAggregateMediaItems.cs | 8 - .../Queries/GetAggregateMediaItemsHandler.cs | 43 - .../Commands/DeleteLocalMediaSourceHandler.cs | 16 +- .../Commands/ScanLocalMediaSource.cs | 4 +- .../Commands/ScanLocalMediaSourceHandler.cs | 26 +- ErsatzTV.Application/Movies/Mapper.cs | 10 + ErsatzTV.Application/Movies/MovieViewModel.cs | 4 + .../Movies/Queries/GetMovieById.cs | 7 + .../Movies/Queries/GetMovieByIdHandler.cs | 22 + ErsatzTV.Application/Playouts/Mapper.cs | 21 +- .../Commands/AddProgramScheduleItem.cs | 5 +- .../Commands/IProgramScheduleItemRequest.cs | 5 +- .../ProgramScheduleItemCommandBase.cs | 50 +- .../Commands/ReplaceProgramScheduleItems.cs | 5 +- .../ReplaceProgramScheduleItemsHandler.cs | 9 +- .../ProgramSchedules/Mapper.cs | 42 +- .../ProgramScheduleItemDurationViewModel.cs | 9 +- .../ProgramScheduleItemFloodViewModel.cs | 11 +- .../ProgramScheduleItemMultipleViewModel.cs | 9 +- .../ProgramScheduleItemOneViewModel.cs | 11 +- .../ProgramScheduleItemViewModel.cs | 16 +- ErsatzTV.Application/Television/Mapper.cs | 28 + .../Queries/GetAllTelevisionSeasons.cs | 7 + .../Queries/GetAllTelevisionSeasonsHandler.cs | 25 + .../Queries/GetAllTelevisionShows.cs | 7 + .../Queries/GetAllTelevisionShowsHandler.cs | 24 + .../Queries/GetTelevisionEpisodeById.cs | 7 + .../GetTelevisionEpisodeByIdHandler.cs | 24 + .../Queries/GetTelevisionSeasonById.cs | 7 + .../Queries/GetTelevisionSeasonByIdHandler.cs | 24 + .../Queries/GetTelevisionShowById.cs | 7 + .../Queries/GetTelevisionShowByIdHandler.cs | 23 + .../Television/TelevisionEpisodeViewModel.cs | 10 + .../Television/TelevisionSeasonViewModel.cs | 4 + .../Television/TelevisionShowViewModel.cs | 4 + .../MediaCollectionMediaItemsCommand.cs | 143 -- .../Commands/MediaItemCommandBase.cs | 42 - .../Commands/MediaItemsCommand.cs | 99 -- .../FFmpegPlaybackSettingsServiceTests.cs | 118 +- ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs | 9 + ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs | 4 + .../Fakes/FakeLocalFileSystem.cs | 75 + .../Fakes/FakeMediaCollectionRepository.cs | 22 +- .../Fakes/FakeTelevisionRepository.cs | 78 + .../Metadata/FallbackMetadataProviderTests.cs | 10 +- .../Metadata/LocalMediaSourcePlannerTests.cs | 1002 ----------- .../Metadata/MovieFolderScannerTests.cs | 370 +++++ .../Scheduling/ChronologicalContentTests.cs | 6 +- .../Scheduling/PlayoutBuilderTests.cs | 70 +- .../Scheduling/RandomizedContentTests.cs | 6 +- .../Scheduling/ShuffledContentTests.cs | 6 +- ErsatzTV.Core/Domain/FFmpegProfile.cs | 4 +- .../Domain/LocalTelevisionShowSource.cs | 9 + ErsatzTV.Core/Domain/MediaItem.cs | 9 +- ErsatzTV.Core/Domain/MediaItemMetadata.cs | 12 + ...ediaMetadata.cs => MediaItemStatistics.cs} | 12 +- ErsatzTV.Core/Domain/MovieMediaItem.cs | 11 + ErsatzTV.Core/Domain/MovieMetadata.cs | 17 + .../Domain/PlayoutProgramScheduleAnchor.cs | 5 +- ErsatzTV.Core/Domain/ProgramScheduleItem.cs | 7 +- .../ProgramScheduleItemCollectionType.cs | 9 + ErsatzTV.Core/Domain/ResolutionKey.cs | 11 - ErsatzTV.Core/Domain/SimpleMediaCollection.cs | 5 +- .../Domain/TelevisionEpisodeMediaItem.cs | 12 + .../Domain/TelevisionEpisodeMetadata.cs | 15 + .../Domain/TelevisionMediaCollection.cs | 8 - ErsatzTV.Core/Domain/TelevisionSeason.cs | 19 + ErsatzTV.Core/Domain/TelevisionShow.cs | 16 + .../Domain/TelevisionShowMetadata.cs | 17 + ErsatzTV.Core/Domain/TelevisionShowSource.cs | 9 + .../Errors/MediaSourceInaccessible.cs | 10 + .../FFmpegPlaybackSettingsCalculator.cs | 64 +- ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs | 2 +- .../Interfaces/Domain/IHasAPoster.cs | 11 + .../Interfaces/FFmpeg/IDisplaySize.cs | 4 +- .../Interfaces/FFmpeg/IFFmpegLocator.cs | 2 +- .../Interfaces/Locking/IEntityLocker.cs | 8 +- .../Metadata/IFallbackMetadataProvider.cs | 9 + .../Interfaces/Metadata/ILocalFileSystem.cs | 14 +- .../Interfaces/Metadata/ILocalMediaScanner.cs | 15 - .../Metadata/ILocalMediaSourcePlanner.cs | 14 - .../Metadata/ILocalMetadataProvider.cs | 8 +- .../Metadata/ILocalPosterProvider.cs | 10 - .../Metadata/IMovieFolderScanner.cs | 11 + .../Metadata/ISmartCollectionBuilder.cs | 10 - .../Metadata/ITelevisionFolderScanner.cs | 11 + .../Interfaces/Plex/IPlexSecretStore.cs | 12 +- .../Repositories/IChannelRepository.cs | 14 +- .../Repositories/IConfigElementRepository.cs | 10 +- .../Repositories/IFFmpegProfileRepository.cs | 10 +- .../Interfaces/Repositories/ILogRepository.cs | 2 +- .../IMediaCollectionRepository.cs | 28 +- .../Repositories/IMediaItemRepository.cs | 14 +- .../Repositories/IMediaSourceRepository.cs | 18 +- .../Repositories/IMovieRepository.cs | 16 + .../Repositories/IPlayoutRepository.cs | 18 +- .../IProgramScheduleRepository.cs | 14 +- .../Repositories/IResolutionRepository.cs | 4 +- .../Repositories/ITelevisionRepository.cs | 47 + .../Scheduling/IMediaCollectionEnumerator.cs | 4 +- .../Interfaces/Scheduling/IPlayoutBuilder.cs | 4 +- ErsatzTV.Core/Iptv/ChannelGuide.cs | 60 +- ErsatzTV.Core/LanguageExtensions.cs | 20 + ErsatzTV.Core/Metadata/ActionPlan.cs | 4 - .../Metadata/FallbackMetadataProvider.cs | 75 +- ErsatzTV.Core/Metadata/LocalFileSystem.cs | 55 +- ErsatzTV.Core/Metadata/LocalFolderScanner.cs | 108 ++ ErsatzTV.Core/Metadata/LocalMediaScanner.cs | 289 ---- .../Metadata/LocalMediaSourcePlan.cs | 11 - .../Metadata/LocalMediaSourcePlanner.cs | 179 -- .../Metadata/LocalMetadataProvider.cs | 256 ++- ErsatzTV.Core/Metadata/LocalPosterProvider.cs | 93 -- .../Metadata/LocalStatisticsProvider.cs | 44 +- ErsatzTV.Core/Metadata/MovieFolderScanner.cs | 167 ++ ErsatzTV.Core/Metadata/ScanningAction.cs | 14 - ErsatzTV.Core/Metadata/ScanningMode.cs | 8 - .../Metadata/SmartCollectionBuilder.cs | 65 - .../Metadata/TelevisionFolderScanner.cs | 361 ++++ .../ChronologicalMediaCollectionEnumerator.cs | 71 +- ErsatzTV.Core/Scheduling/PlayoutBuilder.cs | 119 +- .../GenericIntegerIdConfiguration.cs | 2 +- .../MediaCollectionSummaryConfiguration.cs | 2 +- .../Configurations/MediaItemConfiguration.cs | 7 +- .../MediaItemSummaryConfiguration.cs | 2 +- .../MovieMediaItemConfiguration.cs | 19 + .../MovieMetadataConfiguration.cs | 12 + ...ayoutProgramScheduleAnchorConfiguration.cs | 9 +- .../ProgramScheduleItemConfiguration.cs | 19 +- .../SimpleMediaCollectionConfiguration.cs | 17 +- ...TelevisionEpisodeMediaItemConfiguration.cs | 19 + .../TelevisionEpisodeMetadataConfiguration.cs | 12 + .../TelevisionMediaCollectionConfiguration.cs | 17 - .../TelevisionSeasonConfiguration.cs | 19 + .../TelevisionShowConfiguration.cs | 27 + .../TelevisionShowMetadataConfiguration.cs | 12 + ErsatzTV.Infrastructure/Data/DbInitializer.cs | 8 - .../Data/Repositories/ChannelRepository.cs | 5 + .../Repositories/MediaCollectionRepository.cs | 168 +- .../Data/Repositories/MediaItemRepository.cs | 80 +- .../Data/Repositories/MovieRepository.cs | 72 + .../Data/Repositories/PlayoutRepository.cs | 7 +- .../Repositories/ProgramScheduleRepository.cs | 8 +- .../Data/Repositories/TelevisionRepository.cs | 330 ++++ ErsatzTV.Infrastructure/Data/TvContext.cs | 7 +- ...0219165123_TelevisionExpansion.Designer.cs | 1224 ++++++++++++++ .../20210219165123_TelevisionExpansion.cs | 453 +++++ ...210220003018_CollectionsRework.Designer.cs | 1289 +++++++++++++++ .../20210220003018_CollectionsRework.cs | 212 +++ ...220723_ScheduleCollectionTypes.Designer.cs | 1305 +++++++++++++++ .../20210220220723_ScheduleCollectionTypes.cs | 209 +++ ..._RemoveScheduleItemsAndPosters.Designer.cs | 1305 +++++++++++++++ ...221215810_RemoveScheduleItemsAndPosters.cs | 25 + .../Migrations/TvContextModelSnapshot.cs | 597 ++++++- ErsatzTV.sln.DotSettings | 4 + .../Api/MediaCollectionsController.cs | 9 - .../Controllers/Api/MediaItemsController.cs | 18 - ErsatzTV/ErsatzTV.csproj | 2 +- ErsatzTV/Pages/Channels.razor | 112 +- ErsatzTV/Pages/FFmpeg.razor | 168 +- ErsatzTV/Pages/Index.razor | 5 +- ErsatzTV/Pages/LocalMediaSourceEditor.razor | 5 +- ErsatzTV/Pages/Logs.razor | 36 +- ErsatzTV/Pages/MediaCollectionEditor.razor | 4 +- ErsatzTV/Pages/MediaCollectionItems.razor | 168 ++ .../Pages/MediaCollectionItemsEditor.razor | 121 -- ErsatzTV/Pages/MediaCollections.razor | 111 +- ErsatzTV/Pages/MediaMovieItems.razor | 8 - ErsatzTV/Pages/MediaOtherItems.razor | 8 - ErsatzTV/Pages/MediaSources.razor | 18 +- ErsatzTV/Pages/MediaTvItems.razor | 8 - ErsatzTV/Pages/Movie.razor | 76 + .../MovieList.razor} | 19 +- ErsatzTV/Pages/Playouts.razor | 92 +- ErsatzTV/Pages/ScheduleItemsEditor.razor | 111 +- ErsatzTV/Pages/Schedules.razor | 80 +- ErsatzTV/Pages/TelevisionEpisode.razor | 82 + ErsatzTV/Pages/TelevisionEpisodeList.razor | 122 ++ ErsatzTV/Pages/TelevisionSeasonList.razor | 117 ++ ErsatzTV/Pages/TelevisionShowList.razor | 52 + ErsatzTV/Properties/Annotations.cs | 1460 +++++++++++++++++ ErsatzTV/Services/SchedulerService.cs | 3 +- ErsatzTV/Services/WorkerService.cs | 23 - ErsatzTV/Shared/AddToCollectionDialog.razor | 58 + ErsatzTV/Shared/AddToScheduleDialog.razor | 58 + ErsatzTV/Shared/LocalMediaSources.razor | 3 +- ErsatzTV/Shared/MainLayout.razor | 8 +- ErsatzTV/Shared/MediaCard.razor | 94 +- .../Shared/RemoveFromCollectionDialog.razor | 43 + ErsatzTV/Startup.cs | 9 +- .../ProgramScheduleItemEditViewModel.cs | 56 +- ErsatzTV/wwwroot/css/site.css | 24 +- README.md | 14 +- docs/media-collection.png | Bin 0 -> 222469 bytes docs/television-show.png | Bin 0 -> 285544 bytes .../ErsatzTV.Api.Sdk/.openapi-generator/FILES | 4 - .../src/ErsatzTV.Api.Sdk/Api/MediaItemsApi.cs | 326 ---- .../ErsatzTV.Api.Sdk/Model/CreateMediaItem.cs | 139 -- .../ErsatzTV.Api.Sdk/Model/DeleteMediaItem.cs | 123 -- .../Model/PlayoutChannelViewModel.cs | 16 +- 259 files changed, 13476 insertions(+), 4754 deletions(-) delete mode 100644 ErsatzTV.Application/IMediaCard.cs create mode 100644 ErsatzTV.Application/MediaCards/Mapper.cs create mode 100644 ErsatzTV.Application/MediaCards/MediaCardViewModel.cs create mode 100644 ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs create mode 100644 ErsatzTV.Application/MediaCards/MovieCardViewModel.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetMovieCards.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetMovieCardsHandler.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCards.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCards.cs create mode 100644 ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCardsHandler.cs create mode 100644 ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs create mode 100644 ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs create mode 100644 ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs create mode 100644 ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs create mode 100644 ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs create mode 100644 ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs create mode 100644 ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs delete mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollection.cs delete mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollectionHandler.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollection.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollectionHandler.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollection.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollection.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollectionHandler.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollection.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollection.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollectionHandler.cs delete mode 100644 ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItems.cs delete mode 100644 ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItemsHandler.cs delete mode 100644 ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummaries.cs delete mode 100644 ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummariesHandler.cs delete mode 100644 ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsById.cs delete mode 100644 ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsByIdHandler.cs delete mode 100644 ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs delete mode 100644 ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/CreateMediaItem.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/DeleteMediaItem.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/DeleteMediaItemHandler.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/RefreshMediaItem.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollections.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollectionsHandler.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadata.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadataHandler.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPosterHandler.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatistics.cs delete mode 100644 ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatisticsHandler.cs delete mode 100644 ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs delete mode 100644 ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs create mode 100644 ErsatzTV.Application/Movies/Mapper.cs create mode 100644 ErsatzTV.Application/Movies/MovieViewModel.cs create mode 100644 ErsatzTV.Application/Movies/Queries/GetMovieById.cs create mode 100644 ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs create mode 100644 ErsatzTV.Application/Television/Mapper.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetTelevisionShowById.cs create mode 100644 ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs create mode 100644 ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs create mode 100644 ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs create mode 100644 ErsatzTV.Application/Television/TelevisionShowViewModel.cs delete mode 100644 ErsatzTV.CommandLine/Commands/MediaCollections/MediaCollectionMediaItemsCommand.cs delete mode 100644 ErsatzTV.CommandLine/Commands/MediaItemCommandBase.cs delete mode 100644 ErsatzTV.CommandLine/Commands/MediaItemsCommand.cs create mode 100644 ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs create mode 100644 ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs create mode 100644 ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs create mode 100644 ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs delete mode 100644 ErsatzTV.Core.Tests/Metadata/LocalMediaSourcePlannerTests.cs create mode 100644 ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs create mode 100644 ErsatzTV.Core/Domain/LocalTelevisionShowSource.cs create mode 100644 ErsatzTV.Core/Domain/MediaItemMetadata.cs rename ErsatzTV.Core/Domain/{MediaMetadata.cs => MediaItemStatistics.cs} (51%) create mode 100644 ErsatzTV.Core/Domain/MovieMediaItem.cs create mode 100644 ErsatzTV.Core/Domain/MovieMetadata.cs create mode 100644 ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs delete mode 100644 ErsatzTV.Core/Domain/ResolutionKey.cs create mode 100644 ErsatzTV.Core/Domain/TelevisionEpisodeMediaItem.cs create mode 100644 ErsatzTV.Core/Domain/TelevisionEpisodeMetadata.cs delete mode 100644 ErsatzTV.Core/Domain/TelevisionMediaCollection.cs create mode 100644 ErsatzTV.Core/Domain/TelevisionSeason.cs create mode 100644 ErsatzTV.Core/Domain/TelevisionShow.cs create mode 100644 ErsatzTV.Core/Domain/TelevisionShowMetadata.cs create mode 100644 ErsatzTV.Core/Domain/TelevisionShowSource.cs create mode 100644 ErsatzTV.Core/Errors/MediaSourceInaccessible.cs create mode 100644 ErsatzTV.Core/Interfaces/Domain/IHasAPoster.cs create mode 100644 ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs delete mode 100644 ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs delete mode 100644 ErsatzTV.Core/Interfaces/Metadata/ILocalMediaSourcePlanner.cs delete mode 100644 ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs create mode 100644 ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs delete mode 100644 ErsatzTV.Core/Interfaces/Metadata/ISmartCollectionBuilder.cs create mode 100644 ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs create mode 100644 ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs create mode 100644 ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs delete mode 100644 ErsatzTV.Core/Metadata/ActionPlan.cs create mode 100644 ErsatzTV.Core/Metadata/LocalFolderScanner.cs delete mode 100644 ErsatzTV.Core/Metadata/LocalMediaScanner.cs delete mode 100644 ErsatzTV.Core/Metadata/LocalMediaSourcePlan.cs delete mode 100644 ErsatzTV.Core/Metadata/LocalMediaSourcePlanner.cs delete mode 100644 ErsatzTV.Core/Metadata/LocalPosterProvider.cs create mode 100644 ErsatzTV.Core/Metadata/MovieFolderScanner.cs delete mode 100644 ErsatzTV.Core/Metadata/ScanningAction.cs delete mode 100644 ErsatzTV.Core/Metadata/ScanningMode.cs delete mode 100644 ErsatzTV.Core/Metadata/SmartCollectionBuilder.cs create mode 100644 ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/MovieMediaItemConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/MovieMetadataConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/TelevisionEpisodeMediaItemConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/TelevisionEpisodeMetadataConfiguration.cs delete mode 100644 ErsatzTV.Infrastructure/Data/Configurations/TelevisionMediaCollectionConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/TelevisionSeasonConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowMetadataConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs create mode 100644 ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210221215810_RemoveScheduleItemsAndPosters.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210221215810_RemoveScheduleItemsAndPosters.cs create mode 100644 ErsatzTV/Pages/MediaCollectionItems.razor delete mode 100644 ErsatzTV/Pages/MediaCollectionItemsEditor.razor delete mode 100644 ErsatzTV/Pages/MediaMovieItems.razor delete mode 100644 ErsatzTV/Pages/MediaOtherItems.razor delete mode 100644 ErsatzTV/Pages/MediaTvItems.razor create mode 100644 ErsatzTV/Pages/Movie.razor rename ErsatzTV/{Shared/MediaItemsGrid.razor => Pages/MovieList.razor} (73%) create mode 100644 ErsatzTV/Pages/TelevisionEpisode.razor create mode 100644 ErsatzTV/Pages/TelevisionEpisodeList.razor create mode 100644 ErsatzTV/Pages/TelevisionSeasonList.razor create mode 100644 ErsatzTV/Pages/TelevisionShowList.razor create mode 100644 ErsatzTV/Properties/Annotations.cs create mode 100644 ErsatzTV/Shared/AddToCollectionDialog.razor create mode 100644 ErsatzTV/Shared/AddToScheduleDialog.razor create mode 100644 ErsatzTV/Shared/RemoveFromCollectionDialog.razor create mode 100644 docs/media-collection.png create mode 100644 docs/television-show.png delete mode 100644 generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/CreateMediaItem.cs delete mode 100644 generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/DeleteMediaItem.cs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 785d5211..358ab1a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,24 +39,24 @@ jobs: # Define some variables for things we need tag=$(git describe --tags --abbrev=0) release_name="ErsatzTV-$tag-${{ matrix.target }}" - release_name_cli="ErsatzTV.CommandLine-$tag-${{ matrix.target }}" + #release_name_cli="ErsatzTV.CommandLine-$tag-${{ matrix.target }}" # Build everything dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" - dotnet publish ErsatzTV.CommandLine/ErsatzTV.CommandLine.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name_cli" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" + #dotnet publish ErsatzTV.CommandLine/ErsatzTV.CommandLine.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name_cli" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" # Pack files if [ "${{ matrix.target }}" == "win-x64" ]; then 7z a -tzip "${release_name}.zip" "./${release_name}/*" - 7z a -tzip "${release_name_cli}.zip" "./${release_name_cli}/*" + #7z a -tzip "${release_name_cli}.zip" "./${release_name_cli}/*" else tar czvf "${release_name}.tar.gz" "$release_name" - tar czvf "${release_name_cli}.tar.gz" "$release_name_cli" + #tar czvf "${release_name_cli}.tar.gz" "$release_name_cli" fi # Delete output directory rm -r "$release_name" - rm -r "$release_name_cli" + #rm -r "$release_name_cli" - name: Publish uses: softprops/action-gh-release@v1 diff --git a/ErsatzTV.Application/IMediaCard.cs b/ErsatzTV.Application/IMediaCard.cs deleted file mode 100644 index a50da975..00000000 --- a/ErsatzTV.Application/IMediaCard.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ErsatzTV.Application -{ - public interface IMediaCard - { - string Title { get; } - string SortTitle { get; } - string Subtitle { get; } - } -} diff --git a/ErsatzTV.Application/MediaCards/Mapper.cs b/ErsatzTV.Application/MediaCards/Mapper.cs new file mode 100644 index 00000000..d087d3a4 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Mapper.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Application.MediaCards +{ + internal static class Mapper + { + internal static TelevisionShowCardViewModel ProjectToViewModel(TelevisionShow televisionShow) => + new( + televisionShow.Id, + televisionShow.Metadata?.Title, + televisionShow.Metadata?.Year.ToString(), + televisionShow.Metadata?.SortTitle, + televisionShow.Poster); + + internal static TelevisionSeasonCardViewModel ProjectToViewModel(TelevisionSeason televisionSeason) => + new( + televisionSeason.TelevisionShow.Metadata?.Title, + televisionSeason.Id, + televisionSeason.Number, + GetSeasonName(televisionSeason.Number), + string.Empty, + GetSeasonName(televisionSeason.Number), + televisionSeason.Poster, + televisionSeason.Number == 0 ? "S" : televisionSeason.Number.ToString()); + + internal static TelevisionEpisodeCardViewModel ProjectToViewModel( + TelevisionEpisodeMediaItem televisionEpisode) => + new( + televisionEpisode.Id, + televisionEpisode.Metadata?.Aired ?? DateTime.MinValue, + televisionEpisode.Season.TelevisionShow.Metadata.Title, + televisionEpisode.Metadata?.Title, + $"Episode {televisionEpisode.Metadata?.Episode}", + televisionEpisode.Metadata?.Episode.ToString(), + televisionEpisode.Poster, + televisionEpisode.Metadata?.Episode.ToString()); + + internal static MovieCardViewModel ProjectToViewModel(MovieMediaItem movie) => + new( + movie.Id, + movie.Metadata?.Title, + movie.Metadata?.Year?.ToString(), + movie.Metadata?.SortTitle, + movie.Poster); + + internal static SimpleMediaCollectionCardResultsViewModel + ProjectToViewModel(SimpleMediaCollection collection) => + new( + collection.Name, + collection.Movies.Map(ProjectToViewModel).ToList(), + collection.TelevisionShows.Map(ProjectToViewModel).ToList(), + collection.TelevisionSeasons.Map(ProjectToViewModel).ToList(), + collection.TelevisionEpisodes.Map(ProjectToViewModel).ToList()); + + private static string GetSeasonName(int number) => + number == 0 ? "Specials" : $"Season {number}"; + } +} diff --git a/ErsatzTV.Application/MediaCards/MediaCardViewModel.cs b/ErsatzTV.Application/MediaCards/MediaCardViewModel.cs new file mode 100644 index 00000000..eeb87c6a --- /dev/null +++ b/ErsatzTV.Application/MediaCards/MediaCardViewModel.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Application.MediaCards +{ + public record MediaCardViewModel(string Title, string Subtitle, string SortTitle, string Poster); +} diff --git a/ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs new file mode 100644 index 00000000..f4062bba --- /dev/null +++ b/ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Application.MediaCards +{ + public record MovieCardResultsViewModel(int Count, List Cards); +} diff --git a/ErsatzTV.Application/MediaCards/MovieCardViewModel.cs b/ErsatzTV.Application/MediaCards/MovieCardViewModel.cs new file mode 100644 index 00000000..a1e97a8d --- /dev/null +++ b/ErsatzTV.Application/MediaCards/MovieCardViewModel.cs @@ -0,0 +1,11 @@ +namespace ErsatzTV.Application.MediaCards +{ + public record MovieCardViewModel + (int MovieId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel( + Title, + Subtitle, + SortTitle, + Poster) + { + } +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetMovieCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetMovieCards.cs new file mode 100644 index 00000000..67efe3c9 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetMovieCards.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public record GetMovieCards(int PageNumber, int PageSize) : IRequest; +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetMovieCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetMovieCardsHandler.cs new file mode 100644 index 00000000..1a55618c --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetMovieCardsHandler.cs @@ -0,0 +1,32 @@ +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.MediaCards.Mapper; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public class + GetMovieCardsHandler : IRequestHandler + { + private readonly IMovieRepository _movieRepository; + + public GetMovieCardsHandler(IMovieRepository movieRepository) => _movieRepository = movieRepository; + + public async Task Handle( + GetMovieCards request, + CancellationToken cancellationToken) + { + int count = await _movieRepository.GetMovieCount(); + + List results = await _movieRepository + .GetPagedMovies(request.PageNumber, request.PageSize) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new MovieCardResultsViewModel(count, results); + } + } +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs new file mode 100644 index 00000000..d99fc0ea --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core; +using LanguageExt; +using MediatR; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public record GetSimpleMediaCollectionCards + (int Id) : IRequest>; +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs new file mode 100644 index 00000000..07393be8 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using MediatR; +using static ErsatzTV.Application.MediaCards.Mapper; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public class GetSimpleMediaCollectionCardsHandler : IRequestHandler> + { + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public GetSimpleMediaCollectionCardsHandler(IMediaCollectionRepository mediaCollectionRepository) => + _mediaCollectionRepository = mediaCollectionRepository; + + public async Task> Handle( + GetSimpleMediaCollectionCards request, + CancellationToken cancellationToken) => + (await _mediaCollectionRepository.GetSimpleMediaCollectionWithItemsUntracked(request.Id)) + .ToEither(BaseError.New("Unable to load collection")) + .Map(ProjectToViewModel); + } +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs new file mode 100644 index 00000000..b6aa3cc5 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public record GetTelevisionEpisodeCards + (int TelevisionSeasonId, int PageNumber, int PageSize) : IRequest; +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs new file mode 100644 index 00000000..942a1541 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs @@ -0,0 +1,34 @@ +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.MediaCards.Mapper; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public class + GetTelevisionEpisodeCardsHandler : IRequestHandler + { + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionEpisodeCardsHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; + + public async Task Handle( + GetTelevisionEpisodeCards request, + CancellationToken cancellationToken) + { + int count = await _televisionRepository.GetEpisodeCount(request.TelevisionSeasonId); + + List results = await _televisionRepository + .GetPagedEpisodes(request.TelevisionSeasonId, request.PageNumber, request.PageSize) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new TelevisionEpisodeCardResultsViewModel(count, results); + } + } +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCards.cs new file mode 100644 index 00000000..85f7546f --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCards.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public record GetTelevisionSeasonCards + (int TelevisionShowId, int PageNumber, int PageSize) : IRequest; +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs new file mode 100644 index 00000000..1a339a96 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs @@ -0,0 +1,34 @@ +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.MediaCards.Mapper; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public class + GetTelevisionSeasonCardsHandler : IRequestHandler + { + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionSeasonCardsHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; + + public async Task Handle( + GetTelevisionSeasonCards request, + CancellationToken cancellationToken) + { + int count = await _televisionRepository.GetSeasonCount(request.TelevisionShowId); + + List results = await _televisionRepository + .GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new TelevisionSeasonCardResultsViewModel(count, results); + } + } +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCards.cs new file mode 100644 index 00000000..1e790324 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCards.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public record GetTelevisionShowCards(int PageNumber, int PageSize) : IRequest; +} diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCardsHandler.cs new file mode 100644 index 00000000..ca8465d3 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCardsHandler.cs @@ -0,0 +1,33 @@ +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.MediaCards.Mapper; + +namespace ErsatzTV.Application.MediaCards.Queries +{ + public class + GetTelevisionShowCardsHandler : IRequestHandler + { + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionShowCardsHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; + + public async Task Handle( + GetTelevisionShowCards request, + CancellationToken cancellationToken) + { + int count = await _televisionRepository.GetShowCount(); + + List results = await _televisionRepository + .GetPagedShows(request.PageNumber, request.PageSize) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new TelevisionShowCardResultsViewModel(count, results); + } + } +} diff --git a/ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs new file mode 100644 index 00000000..0ac4bebb --- /dev/null +++ b/ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Application.MediaCards +{ + public record SimpleMediaCollectionCardResultsViewModel( + string Name, + List MovieCards, + List ShowCards, + List SeasonCards, + List EpisodeCards); +} diff --git a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs new file mode 100644 index 00000000..348b7f9e --- /dev/null +++ b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Application.MediaCards +{ + public record TelevisionEpisodeCardResultsViewModel(int Count, List Cards); +} diff --git a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs new file mode 100644 index 00000000..c14521e4 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs @@ -0,0 +1,21 @@ +using System; + +namespace ErsatzTV.Application.MediaCards +{ + public record TelevisionEpisodeCardViewModel + ( + int EpisodeId, + DateTime Aired, + string ShowTitle, + string Title, + string Subtitle, + string SortTitle, + string Poster, + string Placeholder) : MediaCardViewModel( + Title, + Subtitle, + SortTitle, + Poster) + { + } +} diff --git a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs new file mode 100644 index 00000000..9df90f1a --- /dev/null +++ b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Application.MediaCards +{ + public record TelevisionSeasonCardResultsViewModel(int Count, List Cards); +} diff --git a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs new file mode 100644 index 00000000..0cc0b4d4 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs @@ -0,0 +1,19 @@ +namespace ErsatzTV.Application.MediaCards +{ + public record TelevisionSeasonCardViewModel + ( + string ShowTitle, + int TelevisionSeasonId, + int TelevisionSeasonNumber, + string Title, + string Subtitle, + string SortTitle, + string Poster, + string Placeholder) : MediaCardViewModel( + Title, + Subtitle, + SortTitle, + Poster) + { + } +} diff --git a/ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs new file mode 100644 index 00000000..228085f1 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Application.MediaCards +{ + public record TelevisionShowCardResultsViewModel(int Count, List Cards); +} diff --git a/ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs new file mode 100644 index 00000000..975781c2 --- /dev/null +++ b/ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs @@ -0,0 +1,11 @@ +namespace ErsatzTV.Application.MediaCards +{ + public record TelevisionShowCardViewModel + (int TelevisionShowId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel( + Title, + Subtitle, + SortTitle, + Poster) + { + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollection.cs deleted file mode 100644 index a3d1a2d7..00000000 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollection.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; - -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddItemsToSimpleMediaCollection - (int MediaCollectionId, List ItemIds) : MediatR.IRequest>; -} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollectionHandler.cs deleted file mode 100644 index a94211d8..00000000 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollectionHandler.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using static LanguageExt.Prelude; - -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public class - AddItemsToSimpleMediaCollectionHandler : MediatR.IRequestHandler> - { - private readonly IMediaCollectionRepository _mediaCollectionRepository; - private readonly IMediaItemRepository _mediaItemRepository; - - public AddItemsToSimpleMediaCollectionHandler( - IMediaCollectionRepository mediaCollectionRepository, - IMediaItemRepository mediaItemRepository) - { - _mediaCollectionRepository = mediaCollectionRepository; - _mediaItemRepository = mediaItemRepository; - } - - public Task> Handle( - AddItemsToSimpleMediaCollection request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(ApplyAddItemsRequest) - .Bind(v => v.ToEitherAsync()); - - private async Task ApplyAddItemsRequest(RequestParameters parameters) - { - foreach (MediaItem item in parameters.ItemsToAdd.Where( - item => parameters.Collection.Items.All(i => i.Id != item.Id))) - { - parameters.Collection.Items.Add(item); - } - - await _mediaCollectionRepository.Update(parameters.Collection); - - return Unit.Default; - } - - private async Task> - Validate(AddItemsToSimpleMediaCollection request) => - (await SimpleMediaCollectionMustExist(request), await ValidateItems(request)) - .Apply( - (simpleMediaCollectionToUpdate, itemsToAdd) => - new RequestParameters(simpleMediaCollectionToUpdate, itemsToAdd)); - - private Task> SimpleMediaCollectionMustExist( - AddItemsToSimpleMediaCollection updateSimpleMediaCollection) => - _mediaCollectionRepository.GetSimpleMediaCollection(updateSimpleMediaCollection.MediaCollectionId) - .Map(v => v.ToValidation("SimpleMediaCollection does not exist.")); - - private Task>> ValidateItems( - AddItemsToSimpleMediaCollection request) => - LoadAllMediaItems(request) - .Map(v => v.ToValidation("MediaItem does not exist")); - - private async Task>> LoadAllMediaItems(AddItemsToSimpleMediaCollection request) - { - var items = (await request.ItemIds.Map(async id => await _mediaItemRepository.Get(id)).Sequence()) - .ToList(); - if (items.Any(i => i.IsNone)) - { - return None; - } - - return items.Somes().ToList(); - } - - private record RequestParameters(SimpleMediaCollection Collection, List ItemsToAdd); - } -} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollection.cs new file mode 100644 index 00000000..cacb5f4b --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollection.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record AddMovieToSimpleMediaCollection + (int MediaCollectionId, int MovieId) : MediatR.IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollectionHandler.cs new file mode 100644 index 00000000..a42304b0 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollectionHandler.cs @@ -0,0 +1,62 @@ +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public class + AddMovieToSimpleMediaCollectionHandler : MediatR.IRequestHandler> + { + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly IMovieRepository _movieRepository; + + public AddMovieToSimpleMediaCollectionHandler( + IMediaCollectionRepository mediaCollectionRepository, + IMovieRepository movieRepository) + { + _mediaCollectionRepository = mediaCollectionRepository; + _movieRepository = movieRepository; + } + + public Task> Handle( + AddMovieToSimpleMediaCollection request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(ApplyAddMoviesRequest) + .Bind(v => v.ToEitherAsync()); + + private async Task ApplyAddMoviesRequest(RequestParameters parameters) + { + parameters.Collection.Movies.Add(parameters.MovieToAdd); + await _mediaCollectionRepository.Update(parameters.Collection); + + return Unit.Default; + } + + private async Task> + Validate(AddMovieToSimpleMediaCollection request) => + (await SimpleMediaCollectionMustExist(request), await ValidateMovies(request)) + .Apply( + (simpleMediaCollectionToUpdate, movieToAdd) => + new RequestParameters(simpleMediaCollectionToUpdate, movieToAdd)); + + private Task> SimpleMediaCollectionMustExist( + AddMovieToSimpleMediaCollection updateSimpleMediaCollection) => + _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) + .Map(v => v.ToValidation("SimpleMediaCollection does not exist.")); + + private Task> ValidateMovies( + AddMovieToSimpleMediaCollection request) => + LoadMovie(request) + .Map(v => v.ToValidation("MovieMediaItem does not exist")); + + private Task> LoadMovie(AddMovieToSimpleMediaCollection request) => + _movieRepository.GetMovie(request.MovieId); + + private record RequestParameters(SimpleMediaCollection Collection, MovieMediaItem MovieToAdd); + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollection.cs new file mode 100644 index 00000000..290d8d17 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollection.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record AddTelevisionEpisodeToSimpleMediaCollection + (int MediaCollectionId, int TelevisionEpisodeId) : MediatR.IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs new file mode 100644 index 00000000..9604b9ba --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs @@ -0,0 +1,68 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public class + AddTelevisionEpisodeToSimpleMediaCollectionHandler : MediatR.IRequestHandler< + AddTelevisionEpisodeToSimpleMediaCollection, + Either> + { + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly ITelevisionRepository _televisionRepository; + + public AddTelevisionEpisodeToSimpleMediaCollectionHandler( + IMediaCollectionRepository mediaCollectionRepository, + ITelevisionRepository televisionRepository) + { + _mediaCollectionRepository = mediaCollectionRepository; + _televisionRepository = televisionRepository; + } + + public Task> Handle( + AddTelevisionEpisodeToSimpleMediaCollection request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(ApplyAddTelevisionEpisodeRequest) + .Bind(v => v.ToEitherAsync()); + + private async Task ApplyAddTelevisionEpisodeRequest(RequestParameters parameters) + { + if (parameters.Collection.TelevisionEpisodes.All(s => s.Id != parameters.EpisodeToAdd.Id)) + { + parameters.Collection.TelevisionEpisodes.Add(parameters.EpisodeToAdd); + await _mediaCollectionRepository.Update(parameters.Collection); + } + + return Unit.Default; + } + + private async Task> + Validate(AddTelevisionEpisodeToSimpleMediaCollection request) => + (await SimpleMediaCollectionMustExist(request), await ValidateEpisode(request)) + .Apply( + (simpleMediaCollectionToUpdate, episode) => + new RequestParameters(simpleMediaCollectionToUpdate, episode)); + + private Task> SimpleMediaCollectionMustExist( + AddTelevisionEpisodeToSimpleMediaCollection updateSimpleMediaCollection) => + _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) + .Map(v => v.ToValidation("SimpleMediaCollection does not exist.")); + + private Task> ValidateEpisode( + AddTelevisionEpisodeToSimpleMediaCollection request) => + LoadTelevisionEpisode(request) + .Map(v => v.ToValidation("TelevisionEpisode does not exist")); + + private Task> LoadTelevisionEpisode( + AddTelevisionEpisodeToSimpleMediaCollection request) => + _televisionRepository.GetEpisode(request.TelevisionEpisodeId); + + private record RequestParameters(SimpleMediaCollection Collection, TelevisionEpisodeMediaItem EpisodeToAdd); + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollection.cs new file mode 100644 index 00000000..54cde04a --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollection.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record AddTelevisionSeasonToSimpleMediaCollection + (int MediaCollectionId, int TelevisionSeasonId) : MediatR.IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollectionHandler.cs new file mode 100644 index 00000000..fb4ada45 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollectionHandler.cs @@ -0,0 +1,68 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public class + AddTelevisionSeasonToSimpleMediaCollectionHandler : MediatR.IRequestHandler< + AddTelevisionSeasonToSimpleMediaCollection, + Either> + { + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly ITelevisionRepository _televisionRepository; + + public AddTelevisionSeasonToSimpleMediaCollectionHandler( + IMediaCollectionRepository mediaCollectionRepository, + ITelevisionRepository televisionRepository) + { + _mediaCollectionRepository = mediaCollectionRepository; + _televisionRepository = televisionRepository; + } + + public Task> Handle( + AddTelevisionSeasonToSimpleMediaCollection request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(ApplyAddTelevisionSeasonRequest) + .Bind(v => v.ToEitherAsync()); + + private async Task ApplyAddTelevisionSeasonRequest(RequestParameters parameters) + { + if (parameters.Collection.TelevisionSeasons.All(s => s.Id != parameters.SeasonToAdd.Id)) + { + parameters.Collection.TelevisionSeasons.Add(parameters.SeasonToAdd); + await _mediaCollectionRepository.Update(parameters.Collection); + } + + return Unit.Default; + } + + private async Task> + Validate(AddTelevisionSeasonToSimpleMediaCollection request) => + (await SimpleMediaCollectionMustExist(request), await ValidateSeason(request)) + .Apply( + (simpleMediaCollectionToUpdate, season) => + new RequestParameters(simpleMediaCollectionToUpdate, season)); + + private Task> SimpleMediaCollectionMustExist( + AddTelevisionSeasonToSimpleMediaCollection updateSimpleMediaCollection) => + _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) + .Map(v => v.ToValidation("SimpleMediaCollection does not exist.")); + + private Task> ValidateSeason( + AddTelevisionSeasonToSimpleMediaCollection request) => + LoadTelevisionSeason(request) + .Map(v => v.ToValidation("TelevisionSeason does not exist")); + + private Task> LoadTelevisionSeason( + AddTelevisionSeasonToSimpleMediaCollection request) => + _televisionRepository.GetSeason(request.TelevisionSeasonId); + + private record RequestParameters(SimpleMediaCollection Collection, TelevisionSeason SeasonToAdd); + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollection.cs new file mode 100644 index 00000000..faf5eb3f --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollection.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record AddTelevisionShowToSimpleMediaCollection + (int MediaCollectionId, int TelevisionShowId) : MediatR.IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs new file mode 100644 index 00000000..77197f62 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs @@ -0,0 +1,67 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public class + AddTelevisionShowToSimpleMediaCollectionHandler : MediatR.IRequestHandler< + AddTelevisionShowToSimpleMediaCollection, + Either> + { + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly ITelevisionRepository _televisionRepository; + + public AddTelevisionShowToSimpleMediaCollectionHandler( + IMediaCollectionRepository mediaCollectionRepository, + ITelevisionRepository televisionRepository) + { + _mediaCollectionRepository = mediaCollectionRepository; + _televisionRepository = televisionRepository; + } + + public Task> Handle( + AddTelevisionShowToSimpleMediaCollection request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(ApplyAddTelevisionShowRequest) + .Bind(v => v.ToEitherAsync()); + + private async Task ApplyAddTelevisionShowRequest(RequestParameters parameters) + { + if (parameters.Collection.TelevisionShows.All(s => s.Id != parameters.ShowToAdd.Id)) + { + parameters.Collection.TelevisionShows.Add(parameters.ShowToAdd); + await _mediaCollectionRepository.Update(parameters.Collection); + } + + return Unit.Default; + } + + private async Task> + Validate(AddTelevisionShowToSimpleMediaCollection request) => + (await SimpleMediaCollectionMustExist(request), await ValidateShow(request)) + .Apply( + (simpleMediaCollectionToUpdate, show) => + new RequestParameters(simpleMediaCollectionToUpdate, show)); + + private Task> SimpleMediaCollectionMustExist( + AddTelevisionShowToSimpleMediaCollection updateSimpleMediaCollection) => + _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) + .Map(v => v.ToValidation("SimpleMediaCollection does not exist.")); + + private Task> ValidateShow( + AddTelevisionShowToSimpleMediaCollection request) => + LoadTelevisionShow(request) + .Map(v => v.ToValidation("TelevisionShow does not exist")); + + private Task> LoadTelevisionShow(AddTelevisionShowToSimpleMediaCollection request) => + _televisionRepository.GetShow(request.TelevisionShowId); + + private record RequestParameters(SimpleMediaCollection Collection, TelevisionShow ShowToAdd); + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs index 8940f710..362fab63 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs @@ -29,7 +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 }); + 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/Commands/RemoveItemsFromSimpleMediaCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollection.cs new file mode 100644 index 00000000..a3ea0249 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollection.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record RemoveItemsFromSimpleMediaCollection + (int MediaCollectionId) : MediatR.IRequest> + { + public List MovieIds { get; set; } = new(); + public List TelevisionShowIds { get; set; } = new(); + public List TelevisionSeasonIds { get; set; } = new(); + public List TelevisionEpisodeIds { get; set; } = new(); + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollectionHandler.cs new file mode 100644 index 00000000..d7342eba --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollectionHandler.cs @@ -0,0 +1,74 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public class + RemoveItemsFromSimpleMediaCollectionHandler : MediatR.IRequestHandler< + RemoveItemsFromSimpleMediaCollection, + Either> + { + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public RemoveItemsFromSimpleMediaCollectionHandler( + IMediaCollectionRepository mediaCollectionRepository) => + _mediaCollectionRepository = mediaCollectionRepository; + + public Task> Handle( + RemoveItemsFromSimpleMediaCollection request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(collection => ApplyAddTelevisionEpisodeRequest(request, collection)) + .Bind(v => v.ToEitherAsync()); + + private Task ApplyAddTelevisionEpisodeRequest( + RemoveItemsFromSimpleMediaCollection request, + SimpleMediaCollection collection) + { + var moviesToRemove = collection.Movies + .Filter(m => request.MovieIds.Contains(m.Id)) + .ToList(); + + moviesToRemove.ForEach(m => collection.Movies.Remove(m)); + + var showsToRemove = collection.TelevisionShows + .Filter(s => request.TelevisionShowIds.Contains(s.Id)) + .ToList(); + + showsToRemove.ForEach(s => collection.TelevisionShows.Remove(s)); + + var seasonsToRemove = collection.TelevisionSeasons + .Filter(s => request.TelevisionSeasonIds.Contains(s.Id)) + .ToList(); + + seasonsToRemove.ForEach(s => collection.TelevisionSeasons.Remove(s)); + + var episodesToRemove = collection.TelevisionEpisodes + .Filter(e => request.TelevisionEpisodeIds.Contains(e.Id)) + .ToList(); + + episodesToRemove.ForEach(e => collection.TelevisionEpisodes.Remove(e)); + + if (moviesToRemove.Any() || showsToRemove.Any() || seasonsToRemove.Any() || episodesToRemove.Any()) + { + return _mediaCollectionRepository.Update(collection).ToUnit(); + } + + return Task.FromResult(Unit.Default); + } + + private Task> Validate( + RemoveItemsFromSimpleMediaCollection request) => + SimpleMediaCollectionMustExist(request); + + private Task> SimpleMediaCollectionMustExist( + RemoveItemsFromSimpleMediaCollection updateSimpleMediaCollection) => + _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId) + .Map(v => v.ToValidation("SimpleMediaCollection does not exist.")); + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItems.cs b/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItems.cs deleted file mode 100644 index 84c4c14c..00000000 --- a/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItems.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using ErsatzTV.Application.MediaItems; -using ErsatzTV.Core; -using LanguageExt; -using MediatR; - -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record ReplaceSimpleMediaCollectionItems - (int MediaCollectionId, List MediaItemIds) : IRequest>>; -} diff --git a/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItemsHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItemsHandler.cs deleted file mode 100644 index 5c7c7996..00000000 --- a/ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItemsHandler.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaItems; -using ErsatzTV.Core; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using LanguageExt.UnsafeValueAccess; -using MediatR; - -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public class ReplaceSimpleMediaCollectionItemsHandler : IRequestHandler>> - { - private readonly IMediaCollectionRepository _mediaCollectionRepository; - private readonly IMediaItemRepository _mediaItemRepository; - - public ReplaceSimpleMediaCollectionItemsHandler( - IMediaCollectionRepository mediaCollectionRepository, - IMediaItemRepository mediaItemRepository) - { - _mediaCollectionRepository = mediaCollectionRepository; - _mediaItemRepository = mediaItemRepository; - } - - public Task>> Handle( - ReplaceSimpleMediaCollectionItems request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(mediaItems => PersistItems(request, mediaItems)) - .Bind(v => v.ToEitherAsync()); - - private async Task> PersistItems( - ReplaceSimpleMediaCollectionItems request, - List mediaItems) - { - await _mediaCollectionRepository.ReplaceItems(request.MediaCollectionId, mediaItems); - return mediaItems.Map(MediaItems.Mapper.ProjectToViewModel).ToList(); - } - - private Task>> Validate(ReplaceSimpleMediaCollectionItems request) => - MediaCollectionMustExist(request).BindT(_ => MediaItemsMustExist(request)); - - private async Task> MediaCollectionMustExist( - ReplaceSimpleMediaCollectionItems request) => - (await _mediaCollectionRepository.GetSimpleMediaCollection(request.MediaCollectionId)) - .ToValidation("[MediaCollectionId] does not exist."); - - private async Task>> MediaItemsMustExist( - ReplaceSimpleMediaCollectionItems replaceItems) - { - var allMediaItems = (await replaceItems.MediaItemIds.Map(i => _mediaItemRepository.Get(i)).Sequence()) - .ToList(); - if (allMediaItems.Any(x => x.IsNone)) - { - return BaseError.New("[MediaItemId] does not exist"); - } - - return allMediaItems.Sequence().ValueUnsafe().ToList(); - } - } -} diff --git a/ErsatzTV.Application/MediaCollections/Mapper.cs b/ErsatzTV.Application/MediaCollections/Mapper.cs index 021ae7fa..59217405 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/MediaCollections/MediaCollectionViewModel.cs b/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs index add855bf..7bfe6e0c 100644 --- a/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs @@ -1,4 +1,10 @@ -namespace ErsatzTV.Application.MediaCollections +using ErsatzTV.Application.MediaCards; + +namespace ErsatzTV.Application.MediaCollections { - public record MediaCollectionViewModel(int Id, string Name); + public record MediaCollectionViewModel(int Id, string Name) : MediaCardViewModel( + Name, + string.Empty, + Name, + string.Empty); } diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummaries.cs b/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummaries.cs deleted file mode 100644 index d48c716d..00000000 --- a/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummaries.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Collections.Generic; -using MediatR; - -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetMediaCollectionSummaries(string SearchString) : IRequest>; -} diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummariesHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummariesHandler.cs deleted file mode 100644 index b08800ae..00000000 --- a/ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummariesHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -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.MediaCollections.Mapper; - -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public class - GetMediaCollectionSummariesHandler : IRequestHandler> - { - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public GetMediaCollectionSummariesHandler(IMediaCollectionRepository mediaCollectionRepository) => - _mediaCollectionRepository = mediaCollectionRepository; - - public Task> Handle( - GetMediaCollectionSummaries request, - CancellationToken cancellationToken) => - _mediaCollectionRepository.GetSummaries(request.SearchString) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } -} diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsById.cs b/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsById.cs deleted file mode 100644 index 470ff51a..00000000 --- a/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsById.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Application.MediaItems; -using LanguageExt; -using MediatR; - -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetSimpleMediaCollectionWithItemsById - (int Id) : IRequest>>>; -} diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsByIdHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsByIdHandler.cs deleted file mode 100644 index 11f9f79b..00000000 --- a/ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsByIdHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaItems; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -using static LanguageExt.Prelude; -using static ErsatzTV.Application.MediaCollections.Mapper; -using static ErsatzTV.Application.MediaItems.Mapper; - -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public class GetSimpleMediaCollectionWithItemsByIdHandler : IRequestHandler>>> - { - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public GetSimpleMediaCollectionWithItemsByIdHandler(IMediaCollectionRepository mediaCollectionRepository) => - _mediaCollectionRepository = mediaCollectionRepository; - - public async Task>>> Handle( - GetSimpleMediaCollectionWithItemsById request, - CancellationToken cancellationToken) - { - Option maybeCollection = - await _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(request.Id); - - return maybeCollection.Match>>>( - c => Tuple(ProjectToViewModel(c), c.Items.Map(ProjectToSearchViewModel).ToList()), - None); - } - } -} diff --git a/ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs b/ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs deleted file mode 100644 index 9c38e3ea..00000000 --- a/ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Collections.Generic; - -namespace ErsatzTV.Application.MediaItems -{ - public record AggregateMediaItemResults(int Count, List DataPage); -} diff --git a/ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs b/ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs deleted file mode 100644 index 8fed6051..00000000 --- a/ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ErsatzTV.Application.MediaItems -{ - public record AggregateMediaItemViewModel( - int MediaItemId, - string Title, - string Subtitle, - string SortTitle, - string Poster); -} diff --git a/ErsatzTV.Application/MediaItems/Commands/CreateMediaItem.cs b/ErsatzTV.Application/MediaItems/Commands/CreateMediaItem.cs deleted file mode 100644 index 01252d9f..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/CreateMediaItem.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ErsatzTV.Core; -using LanguageExt; -using MediatR; - -namespace ErsatzTV.Application.MediaItems.Commands -{ - public record CreateMediaItem(int MediaSourceId, string Path) : IRequest>; -} diff --git a/ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs b/ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs deleted file mode 100644 index f478f58c..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -using static LanguageExt.Prelude; -using static ErsatzTV.Application.MediaItems.Mapper; - -namespace ErsatzTV.Application.MediaItems.Commands -{ - public class CreateMediaItemHandler : IRequestHandler> - { - private readonly IConfigElementRepository _configElementRepository; - private readonly ILocalMetadataProvider _localMetadataProvider; - private readonly ILocalPosterProvider _localPosterProvider; - private readonly ILocalStatisticsProvider _localStatisticsProvider; - private readonly IMediaItemRepository _mediaItemRepository; - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISmartCollectionBuilder _smartCollectionBuilder; - - public CreateMediaItemHandler( - IMediaItemRepository mediaItemRepository, - IMediaSourceRepository mediaSourceRepository, - IConfigElementRepository configElementRepository, - ISmartCollectionBuilder smartCollectionBuilder, - ILocalMetadataProvider localMetadataProvider, - ILocalStatisticsProvider localStatisticsProvider, - ILocalPosterProvider localPosterProvider) - { - _mediaItemRepository = mediaItemRepository; - _mediaSourceRepository = mediaSourceRepository; - _configElementRepository = configElementRepository; - _smartCollectionBuilder = smartCollectionBuilder; - _localMetadataProvider = localMetadataProvider; - _localStatisticsProvider = localStatisticsProvider; - _localPosterProvider = localPosterProvider; - } - - public Task> Handle( - CreateMediaItem request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(PersistMediaItem) - .Bind(v => v.ToEitherAsync()); - - private async Task PersistMediaItem(RequestParameters parameters) - { - await _mediaItemRepository.Add(parameters.MediaItem); - - await _localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem); - // TODO: reimplement this - // await _localMetadataProvider.RefreshMetadata(parameters.MediaItem); - // await _localPosterProvider.RefreshPoster(parameters.MediaItem); - // await _smartCollectionBuilder.RefreshSmartCollections(parameters.MediaItem); - - return ProjectToViewModel(parameters.MediaItem); - } - - private async Task> Validate(CreateMediaItem request) => - (await ValidateMediaSource(request), PathMustExist(request), await ValidateFFprobePath()) - .Apply( - (mediaSourceId, path, ffprobePath) => new RequestParameters( - ffprobePath, - new MediaItem - { - MediaSourceId = mediaSourceId, - Path = path - })); - - private async Task> ValidateMediaSource(CreateMediaItem createMediaItem) => - (await MediaSourceMustExist(createMediaItem)).Bind(MediaSourceMustBeLocal); - - private async Task> MediaSourceMustExist(CreateMediaItem createMediaItem) => - (await _mediaSourceRepository.Get(createMediaItem.MediaSourceId)) - .ToValidation($"[MediaSource] {createMediaItem.MediaSourceId} does not exist."); - - private Validation MediaSourceMustBeLocal(MediaSource mediaSource) => - Some(mediaSource) - .Filter(ms => ms is LocalMediaSource) - .ToValidation($"[MediaSource] {mediaSource.Id} must be a local media source") - .Map(ms => ms.Id); - - private Validation PathMustExist(CreateMediaItem createMediaItem) => - Some(createMediaItem.Path) - .Filter(File.Exists) - .ToValidation("[Path] does not exist on the file system"); - - private Task> ValidateFFprobePath() => - _configElementRepository.GetValue(ConfigElementKey.FFprobePath) - .FilterT(File.Exists) - .Map( - ffprobePath => - ffprobePath.ToValidation("FFprobe path does not exist on the file system")); - - private record RequestParameters(string FFprobePath, MediaItem MediaItem); - } -} diff --git a/ErsatzTV.Application/MediaItems/Commands/DeleteMediaItem.cs b/ErsatzTV.Application/MediaItems/Commands/DeleteMediaItem.cs deleted file mode 100644 index f1a1abd6..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/DeleteMediaItem.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; -using ErsatzTV.Core; -using LanguageExt; -using MediatR; - -namespace ErsatzTV.Application.MediaItems.Commands -{ - public record DeleteMediaItem(int MediaItemId) : IRequest>; -} diff --git a/ErsatzTV.Application/MediaItems/Commands/DeleteMediaItemHandler.cs b/ErsatzTV.Application/MediaItems/Commands/DeleteMediaItemHandler.cs deleted file mode 100644 index 59357d6f..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/DeleteMediaItemHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; - -namespace ErsatzTV.Application.MediaItems.Commands -{ - public class DeleteMediaItemHandler : IRequestHandler> - { - private readonly IMediaItemRepository _mediaItemRepository; - - public DeleteMediaItemHandler(IMediaItemRepository mediaItemRepository) => - _mediaItemRepository = mediaItemRepository; - - public async Task> Handle( - DeleteMediaItem request, - CancellationToken cancellationToken) => - (await MediaItemMustExist(request)) - .Map(DoDeletion) - .ToEither(); - - private Task DoDeletion(int mediaItemId) => _mediaItemRepository.Delete(mediaItemId); - - private async Task> MediaItemMustExist(DeleteMediaItem deleteMediaItem) => - (await _mediaItemRepository.Get(deleteMediaItem.MediaItemId)) - .ToValidation($"MediaItem {deleteMediaItem.MediaItemId} does not exist.") - .Map(c => c.Id); - } -} diff --git a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItem.cs b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItem.cs deleted file mode 100644 index 608fe29a..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItem.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ErsatzTV.Core; -using LanguageExt; - -namespace ErsatzTV.Application.MediaItems.Commands -{ - public record RefreshMediaItem(int MediaItemId) : MediatR.IRequest>, - IBackgroundServiceRequest; -} diff --git a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollections.cs b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollections.cs deleted file mode 100644 index 49ab14ef..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollections.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ErsatzTV.Application.MediaItems.Commands -{ - public record RefreshMediaItemCollections : RefreshMediaItem - { - public RefreshMediaItemCollections(int mediaItemId) : base(mediaItemId) - { - } - } -} diff --git a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollectionsHandler.cs b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollectionsHandler.cs deleted file mode 100644 index b7d78bb9..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollectionsHandler.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; - -namespace ErsatzTV.Application.MediaItems.Commands -{ - public class - RefreshMediaItemCollectionsHandler : MediatR.IRequestHandler> - { - private readonly IMediaItemRepository _mediaItemRepository; - private readonly ISmartCollectionBuilder _smartCollectionBuilder; - - public RefreshMediaItemCollectionsHandler( - IMediaItemRepository mediaItemRepository, - ISmartCollectionBuilder smartCollectionBuilder) - { - _mediaItemRepository = mediaItemRepository; - _smartCollectionBuilder = smartCollectionBuilder; - } - - public Task> Handle( - RefreshMediaItemCollections request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(RefreshCollections) - .Bind(v => v.ToEitherAsync()); - - private Task> Validate(RefreshMediaItemCollections request) => - MediaItemMustExist(request); - - private Task> MediaItemMustExist( - RefreshMediaItemCollections refreshMediaItemCollections) => - _mediaItemRepository.Get(refreshMediaItemCollections.MediaItemId) - .Map( - maybeItem => maybeItem.ToValidation( - $"[MediaItem] {refreshMediaItemCollections.MediaItemId} does not exist.")); - - private Task RefreshCollections(MediaItem mediaItem) => - _smartCollectionBuilder.RefreshSmartCollections(mediaItem).ToUnit(); - } -} diff --git a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadata.cs b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadata.cs deleted file mode 100644 index 4c3c1bd9..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadata.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ErsatzTV.Application.MediaItems.Commands -{ - public record RefreshMediaItemMetadata : RefreshMediaItem - { - public RefreshMediaItemMetadata(int mediaItemId) : base(mediaItemId) - { - } - } -} diff --git a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadataHandler.cs b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadataHandler.cs deleted file mode 100644 index 7bc3b695..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadataHandler.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using static LanguageExt.Prelude; - -namespace ErsatzTV.Application.MediaItems.Commands -{ - public class - RefreshMediaItemMetadataHandler : MediatR.IRequestHandler> - { - private readonly ILocalMetadataProvider _localMetadataProvider; - private readonly IMediaItemRepository _mediaItemRepository; - - public RefreshMediaItemMetadataHandler( - IMediaItemRepository mediaItemRepository, - ILocalMetadataProvider localMetadataProvider) - { - _mediaItemRepository = mediaItemRepository; - _localMetadataProvider = localMetadataProvider; - } - - public Task> Handle( - RefreshMediaItemMetadata request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(RefreshMetadata) - .Bind(v => v.ToEitherAsync()); - - private Task> Validate(RefreshMediaItemMetadata request) => - MediaItemMustExist(request).BindT(PathMustExist); - - private Task> MediaItemMustExist( - RefreshMediaItemMetadata refreshMediaItemMetadata) => - _mediaItemRepository.Get(refreshMediaItemMetadata.MediaItemId) - .Map( - maybeItem => maybeItem.ToValidation( - $"[MediaItem] {refreshMediaItemMetadata.MediaItemId} does not exist.")); - - private Validation PathMustExist(MediaItem mediaItem) => - Some(mediaItem) - .Filter(item => File.Exists(item.Path)) - .ToValidation($"[Path] '{mediaItem.Path}' does not exist on the file system"); - - private Task RefreshMetadata(MediaItem mediaItem) => Task.CompletedTask.ToUnit(); - // TODO: reimplement this - // _localMetadataProvider.RefreshMetadata(mediaItem).ToUnit(); - } -} diff --git a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs deleted file mode 100644 index c6d7ddb8..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ErsatzTV.Application.MediaItems.Commands -{ - public record RefreshMediaItemPoster : RefreshMediaItem - { - public RefreshMediaItemPoster(int mediaItemId) : base(mediaItemId) - { - } - } -} diff --git a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPosterHandler.cs b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPosterHandler.cs deleted file mode 100644 index c0a6f3ac..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPosterHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; - -namespace ErsatzTV.Application.MediaItems.Commands -{ - public class - RefreshMediaItemPosterHandler : MediatR.IRequestHandler> - { - private readonly ILocalPosterProvider _localPosterProvider; - private readonly IMediaItemRepository _mediaItemRepository; - - public RefreshMediaItemPosterHandler( - IMediaItemRepository mediaItemRepository, - ILocalPosterProvider localPosterProvider) - { - _mediaItemRepository = mediaItemRepository; - _localPosterProvider = localPosterProvider; - } - - public Task> Handle( - RefreshMediaItemPoster request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(RefreshPoster) - .Bind(v => v.ToEitherAsync()); - - private Task> Validate(RefreshMediaItemPoster request) => - MediaItemMustExist(request); - - private Task> MediaItemMustExist(RefreshMediaItemPoster request) => - _mediaItemRepository.Get(request.MediaItemId) - .Map( - maybeItem => maybeItem.ToValidation( - $"[MediaItem] {request.MediaItemId} does not exist.")); - - private Task RefreshPoster(MediaItem mediaItem) => - _localPosterProvider.RefreshPoster(mediaItem).ToUnit(); - } -} diff --git a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatistics.cs b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatistics.cs deleted file mode 100644 index f25ecec4..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatistics.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ErsatzTV.Application.MediaItems.Commands -{ - public record RefreshMediaItemStatistics : RefreshMediaItem - { - public RefreshMediaItemStatistics(int mediaItemId) : base(mediaItemId) - { - } - } -} diff --git a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatisticsHandler.cs b/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatisticsHandler.cs deleted file mode 100644 index 7d9b1c75..00000000 --- a/ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatisticsHandler.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using static LanguageExt.Prelude; - -namespace ErsatzTV.Application.MediaItems.Commands -{ - public class - RefreshMediaItemStatisticsHandler : MediatR.IRequestHandler> - { - private readonly IConfigElementRepository _configElementRepository; - private readonly ILocalStatisticsProvider _localStatisticsProvider; - private readonly IMediaItemRepository _mediaItemRepository; - - public RefreshMediaItemStatisticsHandler( - IMediaItemRepository mediaItemRepository, - IConfigElementRepository configElementRepository, - ILocalStatisticsProvider localStatisticsProvider) - { - _mediaItemRepository = mediaItemRepository; - _configElementRepository = configElementRepository; - _localStatisticsProvider = localStatisticsProvider; - } - - public Task> Handle( - RefreshMediaItemStatistics request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(RefreshStatistics) - .Bind(v => v.ToEitherAsync()); - - private async Task> Validate(RefreshMediaItemStatistics request) => - (await MediaItemMustExist(request).BindT(PathMustExist), await ValidateFFprobePath()) - .Apply((mediaItem, ffprobePath) => new RefreshParameters(mediaItem, ffprobePath)); - - private Task> MediaItemMustExist( - RefreshMediaItemStatistics refreshMediaItemStatistics) => - _mediaItemRepository.Get(refreshMediaItemStatistics.MediaItemId) - .Map( - maybeItem => maybeItem.ToValidation( - $"[MediaItem] {refreshMediaItemStatistics.MediaItemId} does not exist.")); - - private Validation PathMustExist(MediaItem mediaItem) => - Some(mediaItem) - .Filter(item => File.Exists(item.Path)) - .ToValidation($"[Path] '{mediaItem.Path}' does not exist on the file system"); - - private Task> ValidateFFprobePath() => - _configElementRepository.GetValue(ConfigElementKey.FFprobePath) - .FilterT(File.Exists) - .Map( - ffprobePath => - ffprobePath.ToValidation("FFprobe path does not exist on the file system")); - - private Task RefreshStatistics(RefreshParameters parameters) => - _localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem).ToUnit(); - - private record RefreshParameters(MediaItem MediaItem, string FFprobePath); - } -} diff --git a/ErsatzTV.Application/MediaItems/Mapper.cs b/ErsatzTV.Application/MediaItems/Mapper.cs index 6680e7dc..59fb7c5f 100644 --- a/ErsatzTV.Application/MediaItems/Mapper.cs +++ b/ErsatzTV.Application/MediaItems/Mapper.cs @@ -1,5 +1,6 @@ -using ErsatzTV.Core.Domain; -using static LanguageExt.Prelude; +using System; +using System.IO; +using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.MediaItems { @@ -12,25 +13,44 @@ namespace ErsatzTV.Application.MediaItems mediaItem.Path); internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) => + mediaItem switch + { + TelevisionEpisodeMediaItem e => ProjectToSearchViewModel(e), + MovieMediaItem m => ProjectToSearchViewModel(m), + _ => throw new ArgumentOutOfRangeException() + }; + + private static MediaItemSearchResultViewModel ProjectToSearchViewModel(TelevisionEpisodeMediaItem mediaItem) => new( mediaItem.Id, GetSourceName(mediaItem.Source), - mediaItem.Metadata.MediaType.ToString(), + "TV Show", + GetDisplayTitle(mediaItem), + GetDisplayDuration(mediaItem)); + + private static MediaItemSearchResultViewModel ProjectToSearchViewModel(MovieMediaItem mediaItem) => + new( + mediaItem.Id, + GetSourceName(mediaItem.Source), + "Movie", GetDisplayTitle(mediaItem), GetDisplayDuration(mediaItem)); - private static string GetDisplayTitle(this MediaItem mediaItem) => - mediaItem.Metadata.MediaType == MediaType.TvShow && - Optional(mediaItem.Metadata.SeasonNumber).IsSome && - Optional(mediaItem.Metadata.EpisodeNumber).IsSome - ? $"{mediaItem.Metadata.Title} s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}" - : mediaItem.Metadata.Title; + private static string GetDisplayTitle(MediaItem mediaItem) => + mediaItem switch + { + TelevisionEpisodeMediaItem e => e.Metadata != null + ? $"{e.Metadata.Title} - s{e.Metadata.Season:00}e{e.Metadata.Episode:00}" + : Path.GetFileName(e.Path), + MovieMediaItem m => m.Metadata?.Title ?? Path.GetFileName(m.Path), + _ => string.Empty + }; private static string GetDisplayDuration(MediaItem mediaItem) => string.Format( - mediaItem.Metadata.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", - mediaItem.Metadata.Duration); + mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", + mediaItem.Statistics.Duration); private static string GetSourceName(MediaSource source) => source switch diff --git a/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs b/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs deleted file mode 100644 index c552bf0c..00000000 --- a/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ErsatzTV.Core.Domain; -using MediatR; - -namespace ErsatzTV.Application.MediaItems.Queries -{ - public record GetAggregateMediaItems - (MediaType MediaType, int PageNumber, int PageSize) : IRequest; -} diff --git a/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs b/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs deleted file mode 100644 index 67dcfd79..00000000 --- a/ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.AggregateModels; -using ErsatzTV.Core.Interfaces.Repositories; -using MediatR; - -namespace ErsatzTV.Application.MediaItems.Queries -{ - public class - GetAggregateMediaItemsHandler : IRequestHandler - { - private readonly IMediaItemRepository _mediaItemRepository; - - public GetAggregateMediaItemsHandler(IMediaItemRepository mediaItemRepository) => - _mediaItemRepository = mediaItemRepository; - - public async Task Handle( - GetAggregateMediaItems request, - CancellationToken cancellationToken) - { - int count = await _mediaItemRepository.GetCountByType(request.MediaType); - - IEnumerable allItems = await _mediaItemRepository.GetPageByType( - request.MediaType, - request.PageNumber, - request.PageSize); - - var results = allItems - .Map( - s => new AggregateMediaItemViewModel( - s.MediaItemId, - s.Title, - s.Subtitle, - s.SortTitle, - s.Poster)) - .ToList(); - - return new AggregateMediaItemResults(count, results); - } - } -} diff --git a/ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs b/ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs index 8e9fd604..26eff7b3 100644 --- a/ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs @@ -12,16 +12,10 @@ namespace ErsatzTV.Application.MediaSources.Commands public class DeleteLocalMediaSourceHandler : IRequestHandler> { - private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaSourceRepository _mediaSourceRepository; - public DeleteLocalMediaSourceHandler( - IMediaSourceRepository mediaSourceRepository, - IMediaCollectionRepository mediaCollectionRepository) - { + public DeleteLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) => _mediaSourceRepository = mediaSourceRepository; - _mediaCollectionRepository = mediaCollectionRepository; - } public async Task> Handle( DeleteLocalMediaSource request, @@ -30,14 +24,8 @@ namespace ErsatzTV.Application.MediaSources.Commands .Map(DoDeletion) .ToEither(); - private async Task DoDeletion(LocalMediaSource mediaSource) - { + private async Task DoDeletion(LocalMediaSource mediaSource) => await _mediaSourceRepository.Delete(mediaSource.Id); - if (mediaSource.MediaType == MediaType.TvShow) - { - await _mediaCollectionRepository.DeleteEmptyTelevisionCollections(); - } - } private async Task> MediaSourceMustExist( DeleteLocalMediaSource deleteMediaSource) => diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs index 818949a2..0e08d61d 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs @@ -1,11 +1,9 @@ using ErsatzTV.Core; -using ErsatzTV.Core.Metadata; using LanguageExt; using MediatR; namespace ErsatzTV.Application.MediaSources.Commands { - public record ScanLocalMediaSource(int MediaSourceId, ScanningMode ScanningMode) : - IRequest>, + public record ScanLocalMediaSource(int MediaSourceId) : IRequest>, IBackgroundServiceRequest; } diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs index 655f5df9..1f98dc31 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs @@ -16,33 +16,41 @@ namespace ErsatzTV.Application.MediaSources.Commands { private readonly IConfigElementRepository _configElementRepository; private readonly IEntityLocker _entityLocker; - private readonly ILocalMediaScanner _localMediaScanner; private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMovieFolderScanner _movieFolderScanner; + private readonly ITelevisionFolderScanner _televisionFolderScanner; public ScanLocalMediaSourceHandler( IMediaSourceRepository mediaSourceRepository, IConfigElementRepository configElementRepository, - ILocalMediaScanner localMediaScanner, + IMovieFolderScanner movieFolderScanner, + ITelevisionFolderScanner televisionFolderScanner, IEntityLocker entityLocker) { _mediaSourceRepository = mediaSourceRepository; _configElementRepository = configElementRepository; - _localMediaScanner = localMediaScanner; + _movieFolderScanner = movieFolderScanner; + _televisionFolderScanner = televisionFolderScanner; _entityLocker = entityLocker; } public Task> Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) => Validate(request) - .MapT(parameters => PerformScan(request, parameters).Map(_ => parameters.LocalMediaSource.Folder)) + .MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalMediaSource.Folder)) .Bind(v => v.ToEitherAsync()); - private async Task PerformScan(ScanLocalMediaSource request, RequestParameters parameters) + private async Task PerformScan(RequestParameters parameters) { - await _localMediaScanner.ScanLocalMediaSource( - parameters.LocalMediaSource, - parameters.FFprobePath, - request.ScanningMode); + switch (parameters.LocalMediaSource.MediaType) + { + case MediaType.Movie: + await _movieFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath); + break; + case MediaType.TvShow: + await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath); + break; + } _entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id); diff --git a/ErsatzTV.Application/Movies/Mapper.cs b/ErsatzTV.Application/Movies/Mapper.cs new file mode 100644 index 00000000..4656151f --- /dev/null +++ b/ErsatzTV.Application/Movies/Mapper.cs @@ -0,0 +1,10 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Application.Movies +{ + internal static class Mapper + { + internal static MovieViewModel ProjectToViewModel(MovieMediaItem movie) => + new(movie.Metadata.Title, movie.Metadata.Year?.ToString(), movie.Metadata.Plot, movie.Poster); + } +} diff --git a/ErsatzTV.Application/Movies/MovieViewModel.cs b/ErsatzTV.Application/Movies/MovieViewModel.cs new file mode 100644 index 00000000..a854506e --- /dev/null +++ b/ErsatzTV.Application/Movies/MovieViewModel.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Application.Movies +{ + public record MovieViewModel(string Title, string Year, string Plot, string Poster); +} diff --git a/ErsatzTV.Application/Movies/Queries/GetMovieById.cs b/ErsatzTV.Application/Movies/Queries/GetMovieById.cs new file mode 100644 index 00000000..7a264190 --- /dev/null +++ b/ErsatzTV.Application/Movies/Queries/GetMovieById.cs @@ -0,0 +1,7 @@ +using LanguageExt; +using MediatR; + +namespace ErsatzTV.Application.Movies.Queries +{ + public record GetMovieById(int Id) : IRequest>; +} diff --git a/ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs b/ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs new file mode 100644 index 00000000..ed5fb72e --- /dev/null +++ b/ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using MediatR; +using static ErsatzTV.Application.Movies.Mapper; + +namespace ErsatzTV.Application.Movies.Queries +{ + public class GetMovieByIdHandler : IRequestHandler> + { + private readonly IMovieRepository _movieRepository; + + public GetMovieByIdHandler(IMovieRepository movieRepository) => + _movieRepository = movieRepository; + + public Task> Handle( + GetMovieById request, + CancellationToken cancellationToken) => + _movieRepository.GetMovie(request.Id).MapT(ProjectToViewModel); + } +} diff --git a/ErsatzTV.Application/Playouts/Mapper.cs b/ErsatzTV.Application/Playouts/Mapper.cs index 93793982..f2167f72 100644 --- a/ErsatzTV.Application/Playouts/Mapper.cs +++ b/ErsatzTV.Application/Playouts/Mapper.cs @@ -1,5 +1,5 @@ -using ErsatzTV.Core.Domain; -using static LanguageExt.Prelude; +using System.IO; +using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.Playouts { @@ -22,15 +22,18 @@ namespace ErsatzTV.Application.Playouts new(programSchedule.Id, programSchedule.Name); private static string GetDisplayTitle(MediaItem mediaItem) => - mediaItem.Metadata.MediaType == MediaType.TvShow && - Optional(mediaItem.Metadata.SeasonNumber).IsSome && - Optional(mediaItem.Metadata.EpisodeNumber).IsSome - ? $"{mediaItem.Metadata.Title} s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}" - : mediaItem.Metadata.Title; + mediaItem switch + { + TelevisionEpisodeMediaItem e => e.Metadata != null + ? $"{e.Metadata.Title} - s{e.Metadata.Season:00}e{e.Metadata.Episode:00}" + : Path.GetFileName(e.Path), + MovieMediaItem m => m.Metadata?.Title ?? Path.GetFileName(m.Path), + _ => string.Empty + }; private static string GetDisplayDuration(MediaItem mediaItem) => string.Format( - mediaItem.Metadata.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", - mediaItem.Metadata.Duration); + mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", + mediaItem.Statistics.Duration); } } diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs b/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs index ebd5a337..bb7265c4 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 717e2897..be9def8d 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 696d4d90..f205279d 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 0a1b051b..50c7a18e 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 54e1587a..fcd8a7d9 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 1b705688..9d8ea1e6 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 76fc0ab3..6071ffc9 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 13105513..e1f13b32 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 b298d63c..4106836f 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 62f55f10..e77e5566 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 7a0ed5e8..dc7392ab 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 new file mode 100644 index 00000000..983e973d --- /dev/null +++ b/ErsatzTV.Application/Television/Mapper.cs @@ -0,0 +1,28 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Application.Television +{ + internal static class Mapper + { + internal static TelevisionShowViewModel ProjectToViewModel(TelevisionShow show) => + 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(), + season.Number == 0 ? "Specials" : $"Season {season.Number}", + season.Poster); + + internal static TelevisionEpisodeViewModel ProjectToViewModel(TelevisionEpisodeMediaItem episode) => + new( + episode.Season.TelevisionShowId, + episode.SeasonId, + episode.Metadata.Episode, + episode.Metadata.Title, + episode.Metadata.Plot, + episode.Poster); + } +} diff --git a/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs b/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs new file mode 100644 index 00000000..303c8aac --- /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 00000000..5c0668b3 --- /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 00000000..87f91eb1 --- /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 00000000..09c73e8a --- /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/Queries/GetTelevisionEpisodeById.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs new file mode 100644 index 00000000..2e6cc033 --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs @@ -0,0 +1,7 @@ +using LanguageExt; +using MediatR; + +namespace ErsatzTV.Application.Television.Queries +{ + public record GetTelevisionEpisodeById(int EpisodeId) : IRequest>; +} diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs new file mode 100644 index 00000000..69a0fc33 --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs @@ -0,0 +1,24 @@ +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 + GetTelevisionEpisodeByIdHandler : IRequestHandler> + { + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionEpisodeByIdHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; + + public Task> Handle( + GetTelevisionEpisodeById request, + CancellationToken cancellationToken) => + _televisionRepository.GetEpisode(request.EpisodeId) + .MapT(ProjectToViewModel); + } +} diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs new file mode 100644 index 00000000..e551e216 --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs @@ -0,0 +1,7 @@ +using LanguageExt; +using MediatR; + +namespace ErsatzTV.Application.Television.Queries +{ + public record GetTelevisionSeasonById(int SeasonId) : IRequest>; +} diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs new file mode 100644 index 00000000..6adea60a --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs @@ -0,0 +1,24 @@ +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 + GetTelevisionSeasonByIdHandler : IRequestHandler> + { + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionSeasonByIdHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; + + public Task> Handle( + GetTelevisionSeasonById request, + CancellationToken cancellationToken) => + _televisionRepository.GetSeason(request.SeasonId) + .MapT(ProjectToViewModel); + } +} diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionShowById.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionShowById.cs new file mode 100644 index 00000000..5d640f39 --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionShowById.cs @@ -0,0 +1,7 @@ +using LanguageExt; +using MediatR; + +namespace ErsatzTV.Application.Television.Queries +{ + public record GetTelevisionShowById(int Id) : IRequest>; +} diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs new file mode 100644 index 00000000..04ba479b --- /dev/null +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs @@ -0,0 +1,23 @@ +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 GetTelevisionShowByIdHandler : IRequestHandler> + { + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionShowByIdHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; + + public Task> Handle( + GetTelevisionShowById request, + CancellationToken cancellationToken) => + _televisionRepository.GetShow(request.Id) + .MapT(ProjectToViewModel); + } +} diff --git a/ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs b/ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs new file mode 100644 index 00000000..937a943f --- /dev/null +++ b/ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs @@ -0,0 +1,10 @@ +namespace ErsatzTV.Application.Television +{ + public record TelevisionEpisodeViewModel( + int ShowId, + int SeasonId, + int Episode, + string Title, + string Plot, + string Poster); +} diff --git a/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs b/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs new file mode 100644 index 00000000..d5781734 --- /dev/null +++ b/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Application.Television +{ + 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 new file mode 100644 index 00000000..885187f6 --- /dev/null +++ b/ErsatzTV.Application/Television/TelevisionShowViewModel.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Application.Television +{ + public record TelevisionShowViewModel(int Id, string Title, string Year, string Plot, string Poster); +} diff --git a/ErsatzTV.CommandLine/Commands/MediaCollections/MediaCollectionMediaItemsCommand.cs b/ErsatzTV.CommandLine/Commands/MediaCollections/MediaCollectionMediaItemsCommand.cs deleted file mode 100644 index 259be438..00000000 --- a/ErsatzTV.CommandLine/Commands/MediaCollections/MediaCollectionMediaItemsCommand.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CliFx; -using CliFx.Attributes; -using ErsatzTV.Api.Sdk.Api; -using ErsatzTV.Api.Sdk.Model; -using LanguageExt; -using LanguageExt.Common; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; - -namespace ErsatzTV.CommandLine.Commands.MediaCollections -{ - [Command("collection add-items", Description = "Ensure media collection exists and contains requested media items")] - public class MediaCollectionMediaItemsCommand : MediaItemCommandBase - { - private readonly ILogger _logger; - private readonly string _serverUrl; - - public MediaCollectionMediaItemsCommand( - IConfiguration configuration, - ILogger logger) - { - _logger = logger; - _serverUrl = configuration["ServerUrl"]; - } - - [CommandParameter(0, Name = "collection-name", Description = "The name of the media collection")] - public string Name { get; set; } - - public override async ValueTask ExecuteAsync(IConsole console) - { - try - { - CancellationToken cancellationToken = console.GetCancellationToken(); - - Either> maybeFileNames = await GetFileNames(); - await maybeFileNames.Match( - allFiles => SynchronizeMediaItemsToCollection(cancellationToken, allFiles), - error => - { - _logger.LogError("{Error}", error.Message); - return Task.CompletedTask; - }); - } - catch (Exception ex) - { - _logger.LogError("Unable to synchronize media items to media collection: {Error}", ex.Message); - } - } - - private async Task SynchronizeMediaItemsToCollection(CancellationToken cancellationToken, List allFiles) - { - Either result = await GetMediaSourceIdAsync(cancellationToken) - .BindAsync(mediaSourceId => SynchronizeMediaItemsAsync(mediaSourceId, allFiles, cancellationToken)) - .BindAsync(mediaItemIds => SynchronizeMediaItemsToCollectionAsync(mediaItemIds, cancellationToken)); - - result.Match( - _ => _logger.LogInformation( - "Successfully synchronized {Count} media items to media collection {MediaCollection}", - allFiles.Count, - Name), - error => _logger.LogError( - "Unable to synchronize media items to media collection: {Error}", - error.Message)); - } - - private async Task> GetMediaSourceIdAsync(CancellationToken cancellationToken) - { - var mediaSourcesApi = new MediaSourcesApi(_serverUrl); - List allMediaSources = - await mediaSourcesApi.ApiMediaSourcesGetAsync(cancellationToken); - Option maybeLocalMediaSource = - allMediaSources.SingleOrDefault(cs => cs.SourceType == MediaSourceType.Local); - return maybeLocalMediaSource.Match>( - mediaSource => mediaSource.Id, - () => Error.New("Unable to find local media source")); - } - - private async Task>> SynchronizeMediaItemsAsync( - int mediaSourceId, - ICollection fileNames, - CancellationToken cancellationToken) - { - var mediaItemsApi = new MediaItemsApi(_serverUrl); - List allMediaItems = await mediaItemsApi.ApiMediaItemsGetAsync(cancellationToken); - var missingMediaItems = fileNames.Where(f => allMediaItems.All(c => c.Path != f)) - .Map(f => new CreateMediaItem(mediaSourceId, f)) - .ToList(); - - var addedIds = new List(); - foreach (CreateMediaItem mediaItem in missingMediaItems) - { - _logger.LogInformation("Adding media item {Path}", mediaItem.Path); - addedIds.Add(await mediaItemsApi.ApiMediaItemsPostAsync(mediaItem, cancellationToken).Map(vm => vm.Id)); - } - - IEnumerable knownIds = allMediaItems.Where(c => fileNames.Contains(c.Path)).Map(c => c.Id); - - return knownIds.Concat(addedIds).ToList(); - } - - private async Task> SynchronizeMediaItemsToCollectionAsync( - List mediaItemIds, - CancellationToken cancellationToken) => - await EnsureMediaCollectionExistsAsync(cancellationToken) - .BindAsync( - mediaSourceId => SynchronizeMediaCollectionAsync(mediaSourceId, mediaItemIds, cancellationToken)); - - private async Task> EnsureMediaCollectionExistsAsync(CancellationToken cancellationToken) - { - var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl); - Option maybeExisting = await mediaCollectionsApi - .ApiMediaCollectionsGetAsync(cancellationToken) - .Map(list => list.SingleOrDefault(mc => mc.Name == Name)); - return await maybeExisting.Match( - existing => Task.FromResult(existing.Id), - async () => - { - var data = new CreateSimpleMediaCollection(Name); - return await mediaCollectionsApi.ApiMediaCollectionsPostAsync(data, cancellationToken) - .Map(vm => vm.Id); - }); - } - - private async Task> SynchronizeMediaCollectionAsync( - int mediaCollectionId, - List mediaItemIds, - CancellationToken cancellationToken) - { - var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl); - await mediaCollectionsApi.ApiMediaCollectionsIdItemsPutAsync( - mediaCollectionId, - mediaItemIds, - cancellationToken); - return unit; - } - } -} diff --git a/ErsatzTV.CommandLine/Commands/MediaItemCommandBase.cs b/ErsatzTV.CommandLine/Commands/MediaItemCommandBase.cs deleted file mode 100644 index 850f73dc..00000000 --- a/ErsatzTV.CommandLine/Commands/MediaItemCommandBase.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using CliFx; -using CliFx.Attributes; -using LanguageExt; -using LanguageExt.Common; - -namespace ErsatzTV.CommandLine.Commands -{ - public abstract class MediaItemCommandBase : ICommand - { - [CommandOption("folder", 'f', Description = "Folder to search for media items")] - public string Folder { get; set; } - - [CommandOption("pattern", 'p', Description = "File search pattern")] - public string SearchPattern { get; set; } - - public abstract ValueTask ExecuteAsync(IConsole console); - - protected async Task>> GetFileNames() - { - if (Console.IsInputRedirected) - { - await using Stream standardInput = Console.OpenStandardInput(); - using var streamReader = new StreamReader(standardInput); - string input = await streamReader.ReadToEndAsync(); - return input.Trim().Split("\n").Map(s => s.Trim()).ToList(); - } - - if (string.IsNullOrWhiteSpace(Folder) || string.IsNullOrWhiteSpace(SearchPattern)) - { - return Error.New( - "--folder and --pattern are required when file names are not passed on standard input"); - } - - return Directory.GetFiles(Folder, SearchPattern, SearchOption.AllDirectories).ToList(); - } - } -} diff --git a/ErsatzTV.CommandLine/Commands/MediaItemsCommand.cs b/ErsatzTV.CommandLine/Commands/MediaItemsCommand.cs deleted file mode 100644 index 10576a1b..00000000 --- a/ErsatzTV.CommandLine/Commands/MediaItemsCommand.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CliFx; -using CliFx.Attributes; -using ErsatzTV.Api.Sdk.Api; -using ErsatzTV.Api.Sdk.Model; -using LanguageExt; -using LanguageExt.Common; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; - -namespace ErsatzTV.CommandLine.Commands -{ - [Command("items", Description = "Ensure media items exist")] - public class MediaItemsCommand : MediaItemCommandBase - { - private readonly ILogger _logger; - private readonly string _serverUrl; - - public MediaItemsCommand(IConfiguration configuration, ILogger logger) - { - _logger = logger; - _serverUrl = configuration["ServerUrl"]; - } - - public override async ValueTask ExecuteAsync(IConsole console) - { - try - { - CancellationToken cancellationToken = console.GetCancellationToken(); - - Either> maybeFileNames = await GetFileNames(); - await maybeFileNames.Match( - allFiles => SynchronizeMediaItems(cancellationToken, allFiles), - error => - { - _logger.LogError("{Error}", error.Message); - return Task.CompletedTask; - }); - } - catch (Exception ex) - { - _logger.LogError("Unable to synchronize media items: {Error}", ex.Message); - } - } - - private async Task SynchronizeMediaItems(CancellationToken cancellationToken, List allFiles) - { - Either result = await GetMediaSourceId(cancellationToken) - .BindAsync( - contentSourceId => PostMediaItems( - contentSourceId, - allFiles, - cancellationToken)); - - result.Match( - _ => _logger.LogInformation( - "Successfully synchronized {Count} media items", - allFiles.Count), - error => _logger.LogError("Unable to synchronize media items: {Error}", error.Message)); - } - - private async Task> GetMediaSourceId(CancellationToken cancellationToken) - { - var mediaSourcesApi = new MediaSourcesApi(_serverUrl); - List allMediaSources = - await mediaSourcesApi.ApiMediaSourcesGetAsync(cancellationToken); - Option maybeLocalMediaSource = - allMediaSources.SingleOrDefault(cs => cs.SourceType == MediaSourceType.Local); - return maybeLocalMediaSource.Match>( - mediaSource => mediaSource.Id, - () => Error.New("Unable to find local media source")); - } - - private async Task> PostMediaItems( - int mediaSourceId, - ICollection fileNames, - CancellationToken cancellationToken) - { - var mediaItemsApi = new MediaItemsApi(_serverUrl); - List allContent = await mediaItemsApi.ApiMediaItemsGetAsync(cancellationToken); - var missingMediaItems = fileNames.Where(f => allContent.All(c => c.Path != f)) - .Map(f => new CreateMediaItem(mediaSourceId, f)) - .ToList(); - - foreach (CreateMediaItem mediaItem in missingMediaItems) - { - _logger.LogInformation("Adding media item {Path}", mediaItem.Path); - await mediaItemsApi.ApiMediaItemsPostAsync(mediaItem, cancellationToken); - } - - return unit; - } - } -} diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs index 6497ebb1..b4c4ae7f 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs @@ -20,7 +20,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg { MediaItem = new MediaItem { - Metadata = new MediaMetadata() + Statistics = new MediaItemStatistics() } }; @@ -175,9 +175,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1920; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.Width = 1920; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -198,9 +198,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1918; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.Width = 1918; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -221,9 +221,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1920; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.Width = 1920; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -245,9 +245,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1918; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.Width = 1918; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -269,9 +269,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1918; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.Width = 1918; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.HttpLiveStreaming, @@ -295,9 +295,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1918; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.Width = 1918; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -323,10 +323,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1920; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video"; + playoutItem.MediaItem.Statistics.Width = 1920; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -352,10 +352,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1920; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video"; + playoutItem.MediaItem.Statistics.Width = 1920; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.HttpLiveStreaming, @@ -380,10 +380,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1920; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Metadata.VideoCodec = "libx264"; + playoutItem.MediaItem.Statistics.Width = 1920; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.VideoCodec = "libx264"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -409,10 +409,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1920; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video"; + playoutItem.MediaItem.Statistics.Width = 1920; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -437,9 +437,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1918; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.Width = 1918; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -464,10 +464,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1920; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video"; + playoutItem.MediaItem.Statistics.Width = 1920; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -492,9 +492,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1918; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.Width = 1918; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -520,10 +520,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.Width = 1920; - playoutItem.MediaItem.Metadata.Height = 1080; - playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic - playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video"; + playoutItem.MediaItem.Statistics.Width = 1920; + playoutItem.MediaItem.Statistics.Height = 1080; + playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic + playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -546,7 +546,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.AudioCodec = "aac"; + playoutItem.MediaItem.Statistics.AudioCodec = "aac"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -567,7 +567,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; + playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -588,7 +588,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; + playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -609,7 +609,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; + playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.HttpLiveStreaming, @@ -630,7 +630,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; + playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -651,7 +651,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; + playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -674,7 +674,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; + playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -697,7 +697,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; + playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -719,7 +719,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; + playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, @@ -741,7 +741,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg }; PlayoutItem playoutItem = EmptyPlayoutItem(); - playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; + playoutItem.MediaItem.Statistics.AudioCodec = "ac3"; FFmpegPlaybackSettings actual = _calculator.CalculateSettings( StreamingMode.TransportStream, diff --git a/ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs b/ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs new file mode 100644 index 00000000..fb824bf6 --- /dev/null +++ b/ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs @@ -0,0 +1,9 @@ +using System; + +namespace ErsatzTV.Core.Tests.Fakes +{ + public record FakeFileEntry(string Path) + { + public DateTime LastWriteTime { get; set; } = DateTime.MinValue; + } +} diff --git a/ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs b/ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs new file mode 100644 index 00000000..3fbe5da0 --- /dev/null +++ b/ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs @@ -0,0 +1,4 @@ +namespace ErsatzTV.Core.Tests.Fakes +{ + public record FakeFolderEntry(string Path); +} diff --git a/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs b/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs new file mode 100644 index 00000000..0e28632c --- /dev/null +++ b/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Metadata; +using LanguageExt; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Core.Tests.Fakes +{ + public class FakeLocalFileSystem : ILocalFileSystem + { + public static readonly byte[] TestBytes = { 1, 2, 3, 4, 5 }; + + private readonly List _files; + private readonly List _folders; + + public FakeLocalFileSystem(List files) : this(files, new List()) + { + } + + public FakeLocalFileSystem(List files, List folders) + { + _files = files; + + var allFolders = new List(folders.Map(f => f.Path)); + foreach (FakeFileEntry file in _files) + { + List moreFolders = + Split(new DirectoryInfo(Path.GetDirectoryName(file.Path) ?? string.Empty)); + allFolders.AddRange(moreFolders.Map(i => i.FullName)); + } + + _folders = allFolders.Distinct().Map(f => new FakeFolderEntry(f)).ToList(); + } + + public DateTime GetLastWriteTime(string path) => + Optional(_files.SingleOrDefault(f => f.Path == path)) + .Map(f => f.LastWriteTime) + .IfNone(DateTime.MinValue); + + public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource) => + _files.Any(f => f.Path.StartsWith(localMediaSource.Folder)); + + public IEnumerable ListSubdirectories(string folder) => + _folders.Map(f => f.Path).Filter(f => f.StartsWith(folder) && Directory.GetParent(f)?.FullName == folder); + + public IEnumerable ListFiles(string folder) => + _files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder); + + public bool FileExists(string path) => _files.Any(f => f.Path == path); + + public Task ReadAllBytes(string path) => TestBytes.AsTask(); + + private static List Split(DirectoryInfo path) + { + var result = new List(); + if (path == null || string.IsNullOrWhiteSpace(path.FullName)) + { + return result; + } + + if (path.Parent != null) + { + result.AddRange(Split(path.Parent)); + } + + result.Add(path); + + return result; + } + } +} diff --git a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs index 45f90a96..62142b4b 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs @@ -1,7 +1,7 @@ using System; 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; @@ -11,9 +11,9 @@ namespace ErsatzTV.Core.Tests.Fakes { public class FakeMediaCollectionRepository : IMediaCollectionRepository { - private readonly Map> _data; + private readonly Map> _data; - public FakeMediaCollectionRepository(Map> data) => _data = data; + public FakeMediaCollectionRepository(Map> data) => _data = data; public Task Add(SimpleMediaCollection collection) => throw new NotSupportedException(); @@ -25,32 +25,20 @@ namespace ErsatzTV.Core.Tests.Fakes public Task> GetSimpleMediaCollectionWithItems(int id) => throw new NotSupportedException(); - public Task> GetTelevisionMediaCollection(int id) => + public Task> GetSimpleMediaCollectionWithItemsUntracked(int id) => throw new NotSupportedException(); public Task> GetSimpleMediaCollections() => throw new NotSupportedException(); public Task> GetAll() => throw new NotSupportedException(); - public Task> GetSummaries(string searchString) => - throw new NotSupportedException(); - - public Task>> GetItems(int id) => Some(_data[id]).AsTask(); + public Task>> GetItems(int id) => Some(_data[id].OfType().ToList()).AsTask(); public Task>> GetSimpleMediaCollectionItems(int id) => throw new NotSupportedException(); - public Task>> GetTelevisionMediaCollectionItems(int id) => - throw new NotSupportedException(); - public Task Update(SimpleMediaCollection collection) => throw new NotSupportedException(); - public Task InsertOrIgnore(TelevisionMediaCollection collection) => throw new NotSupportedException(); - - public Task ReplaceItems(int collectionId, List mediaItems) => - throw new NotSupportedException(); - public Task Delete(int mediaCollectionId) => throw new NotSupportedException(); - public Task DeleteEmptyTelevisionCollections() => throw new NotSupportedException(); } } diff --git a/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs new file mode 100644 index 00000000..002c86c7 --- /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/Metadata/FallbackMetadataProviderTests.cs b/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs index 85957559..6d1a5ad6 100644 --- a/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs @@ -37,13 +37,13 @@ namespace ErsatzTV.Core.Tests.Metadata 2)] public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, string title, int season, int episode) { - MediaMetadata metadata = FallbackMetadataProvider.GetFallbackMetadata( - new MediaItem { Path = path, Source = new LocalMediaSource { MediaType = MediaType.TvShow } }); + TelevisionEpisodeMetadata metadata = FallbackMetadataProvider.GetFallbackMetadata( + new TelevisionEpisodeMediaItem + { Path = path, Source = new LocalMediaSource { MediaType = MediaType.TvShow } }); - metadata.MediaType.Should().Be(MediaType.TvShow); metadata.Title.Should().Be(title); - metadata.SeasonNumber.Should().Be(season); - metadata.EpisodeNumber.Should().Be(episode); + metadata.Season.Should().Be(season); + metadata.Episode.Should().Be(episode); } } } diff --git a/ErsatzTV.Core.Tests/Metadata/LocalMediaSourcePlannerTests.cs b/ErsatzTV.Core.Tests/Metadata/LocalMediaSourcePlannerTests.cs deleted file mode 100644 index 24b21e45..00000000 --- a/ErsatzTV.Core.Tests/Metadata/LocalMediaSourcePlannerTests.cs +++ /dev/null @@ -1,1002 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Metadata; -using FluentAssertions; -using LanguageExt; -using NUnit.Framework; - -namespace ErsatzTV.Core.Tests.Metadata -{ - [TestFixture] - public class LocalMediaSourcePlannerTests - { - private static readonly List VideoFileExtensions = new() - { - "mpg", "mp2", "mpeg", "mpe", "mpv", "ogg", "mp4", - "m4p", "m4v", "avi", "wmv", "mov", "mkv", "ts" - }; - - private static IEnumerable OldEntriesFor(params string[] fileNames) => - fileNames.Map(f => new FakeFileSystemEntry(f, DateTime.MinValue)); - - private static IEnumerable NewEntriesFor(params string[] fileNames) => - fileNames.Map(f => new FakeFileSystemEntry(f, DateTime.MaxValue)); - - private static LocalMediaSourcePlanner ScannerForOldFiles(params string[] fileNames) - => new(new FakeLocalFileSystem(OldEntriesFor(fileNames))); - - private static LocalMediaSourcePlanner ScannerForNewFiles(params string[] fileNames) - => new(new FakeLocalFileSystem(NewEntriesFor(fileNames))); - - private static LocalMediaSourcePlanner ScannerFor(IEnumerable entries) - => new(new FakeLocalFileSystem(entries)); - - private static readonly string FakeRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? "C:\\" - : "/"; - - private static string MovieNameWithExtension(string extension, int year = 2021) => Path.Combine( - FakeRoot, - Path.Combine( - "movies", - Path.Combine($"test ({year})"), - Path.Combine($"test ({year}).{extension}"))); - - private static string MovieNfoName(string nfoFileName) => - Path.Combine(FakeRoot, Path.Combine("movies", Path.Combine("test (2021)"), nfoFileName)); - - private static string MoviePosterNameWithExtension(string basePosterName, string extension) => Path.Combine( - FakeRoot, - Path.Combine( - "movies", - Path.Combine("test (2021)"), - $"{basePosterName}poster.{extension}")); - - private static string EpisodeNameWithExtension(string extension, int episodeNumber = 3) => Path.Combine( - FakeRoot, - Path.Combine( - "tv", - Path.Combine( - "test (2021)", - Path.Combine("season 01", $"test (2021) - s01e{episodeNumber:00}.{extension}")))); - - private static string EpisodeNfoName(int episodeNumber = 3) => - Path.Combine( - FakeRoot, - Path.Combine( - "tv", - Path.Combine( - "test (2021)", - Path.Combine("season 01", $"test (2021) - s01e{episodeNumber:00}.nfo")))); - - private static string SeriesPosterNameWithExtension(string extension) => - Path.Combine(FakeRoot, Path.Combine("tv", Path.Combine("test (2021)", $"poster.{extension}"))); - - [TestFixture] - public class NewMovieTests - { - [Test] - public void WithoutNfo_WithoutPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - string movieFileName = MovieNameWithExtension(extension); - string[] fileNames = { movieFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.Empty, - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsLeft.Should().BeTrue(); - source.LeftToSeq().Should().BeEquivalentTo(movieFileName); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(movieFileName, ScanningAction.Add), - new ActionPlan(movieFileName, ScanningAction.Statistics), - new ActionPlan(movieFileName, ScanningAction.FallbackMetadata), - new ActionPlan(movieFileName, ScanningAction.Collections)); - } - - [Test] - public void WithNfo_WithoutPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("test (2021).nfo", "movie.nfo")] - string nfoFile) - { - string movieFileName = MovieNameWithExtension(extension); - string nfoFileName = MovieNfoName(nfoFile); - string[] fileNames = { movieFileName, nfoFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.Empty, - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsLeft.Should().BeTrue(); - source.LeftToSeq().Should().BeEquivalentTo(movieFileName); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(movieFileName, ScanningAction.Add), - new ActionPlan(movieFileName, ScanningAction.Statistics), - new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ActionPlan(nfoFileName, ScanningAction.Collections)); - } - - [Test] - public void WithoutNfo_WithPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("", "test (2021)-")] - string basePosterName, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - string movieFileName = MovieNameWithExtension(extension); - string posterFileName = MoviePosterNameWithExtension(basePosterName, posterExtension); - - string[] fileNames = { movieFileName, posterFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.Empty, - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsLeft.Should().BeTrue(); - source.LeftToSeq().Should().BeEquivalentTo(movieFileName); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(movieFileName, ScanningAction.Add), - new ActionPlan(movieFileName, ScanningAction.Statistics), - new ActionPlan(movieFileName, ScanningAction.FallbackMetadata), - new ActionPlan(posterFileName, ScanningAction.Poster), - new ActionPlan(movieFileName, ScanningAction.Collections)); - } - - [Test] - public void WithNfo_WithPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("test (2021).nfo", "movie.nfo")] - string nfoFile, - [Values("", "test (2021)-")] - string basePosterName, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - string movieFileName = MovieNameWithExtension(extension); - string nfoFileName = MovieNfoName(nfoFile); - string posterFileName = MoviePosterNameWithExtension(basePosterName, posterExtension); - - string[] fileNames = { movieFileName, nfoFileName, posterFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.Empty, - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsLeft.Should().BeTrue(); - source.LeftToSeq().Should().BeEquivalentTo(movieFileName); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(movieFileName, ScanningAction.Add), - new ActionPlan(movieFileName, ScanningAction.Statistics), - new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ActionPlan(posterFileName, ScanningAction.Poster), - new ActionPlan(nfoFileName, ScanningAction.Collections)); - } - } - - [TestFixture] - public class ExistingMovieTests - { - [Test] - public void Old_File_Should_Do_Nothing( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - var movieMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = MovieNameWithExtension(extension) - }; - - string[] fileNames = { movieMediaItem.Path }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.create(movieMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(0); - } - - [Test] - public void Updated_File_Should_Refresh_Statistics( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - var movieMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = MovieNameWithExtension(extension) - }; - - string[] fileNames = { movieMediaItem.Path }; - - Seq result = ScannerForNewFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.create(movieMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(movieMediaItem.Path, ScanningAction.Statistics)); - } - - [Test] - public void Fallback_WithNewNfo_WithoutPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("test (2021).nfo", "movie.nfo")] - string nfoFile) - { - var movieMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = MovieNameWithExtension(extension) - }; - - string nfoFileName = MovieNfoName(nfoFile); - string[] fileNames = { movieMediaItem.Path, nfoFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.create(movieMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ActionPlan(nfoFileName, ScanningAction.Collections)); - } - - [Test] - public void Sidecar_WithOldNfo_WithoutPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("test (2021).nfo", "movie.nfo")] - string nfoFile) - { - var movieMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Sidecar }, - Path = MovieNameWithExtension(extension) - }; - - string nfoFileName = MovieNfoName(nfoFile); - string[] fileNames = { movieMediaItem.Path, nfoFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.create(movieMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(0); - } - - [Test] - public void Sidecar_WithUpdatedNfo_WithoutPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("test (2021).nfo", "movie.nfo")] - string nfoFile) - { - var movieMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Sidecar }, - Path = MovieNameWithExtension(extension) - }; - - string nfoFileName = MovieNfoName(nfoFile); - string[] fileNames = { movieMediaItem.Path, nfoFileName }; - - Seq result = - ScannerFor(OldEntriesFor(movieMediaItem.Path).Concat(NewEntriesFor(nfoFileName))) - .DetermineActions( - MediaType.Movie, - Seq.create(movieMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ActionPlan(nfoFileName, ScanningAction.Collections)); - } - - [Test] - public void WithoutNfo_WithNewPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("", "test (2021)-")] - string basePosterName, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - var movieMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = MovieNameWithExtension(extension) - }; - - string posterFileName = MoviePosterNameWithExtension(basePosterName, posterExtension); - string[] fileNames = { movieMediaItem.Path, posterFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.create(movieMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); - itemScanningPlans.Should() - .BeEquivalentTo(new ActionPlan(posterFileName, ScanningAction.Poster)); - } - - [Test] - public void WithoutNfo_WithOldPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("", "test (2021)-")] - string basePosterName, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - var movieMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = MovieNameWithExtension(extension), - Poster = "anything", - PosterLastWriteTime = DateTime.UtcNow - }; - - string posterFileName = MoviePosterNameWithExtension(basePosterName, posterExtension); - string[] fileNames = { movieMediaItem.Path, posterFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.create(movieMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(0); - } - - [Test] - public void WithoutNfo_WithUpdatedPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("", "test (2021)-")] - string basePosterName, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - var movieMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = MovieNameWithExtension(extension), - Poster = "anything", - PosterLastWriteTime = DateTime.UtcNow - }; - - string posterFileName = MoviePosterNameWithExtension(basePosterName, posterExtension); - string[] fileNames = { movieMediaItem.Path, posterFileName }; - - Seq result = - ScannerFor(OldEntriesFor(movieMediaItem.Path).Concat(NewEntriesFor(posterFileName))) - .DetermineActions( - MediaType.Movie, - Seq.create(movieMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); - itemScanningPlans.Should() - .BeEquivalentTo(new ActionPlan(posterFileName, ScanningAction.Poster)); - } - - [Test] - public void WithNewNfo_WithNewPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("test (2021).nfo", "movie.nfo")] - string nfoFile, - [Values("", "test (2021)-")] - string basePosterName, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - var movieMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = MovieNameWithExtension(extension) - }; - - string nfoFileName = MovieNfoName(nfoFile); - string posterFileName = MoviePosterNameWithExtension(basePosterName, posterExtension); - string[] fileNames = { movieMediaItem.Path, nfoFileName, posterFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.create(movieMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(movieMediaItem); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ActionPlan(posterFileName, ScanningAction.Poster), - new ActionPlan(nfoFileName, ScanningAction.Collections)); - } - } - - [TestFixture] - public class NewEpisodeTests - { - [Test] - public void WithoutNfo_WithoutPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - string episodeFileName = EpisodeNameWithExtension(extension); - string[] fileNames = { episodeFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.Empty, - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsLeft.Should().BeTrue(); - source.LeftToSeq().Should().BeEquivalentTo(episodeFileName); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(episodeFileName, ScanningAction.Add), - new ActionPlan(episodeFileName, ScanningAction.Statistics), - new ActionPlan(episodeFileName, ScanningAction.FallbackMetadata), - new ActionPlan(episodeFileName, ScanningAction.Collections)); - } - - [Test] - public void WithNfo_WithoutPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - string episodeFileName = EpisodeNameWithExtension(extension); - string nfoFileName = EpisodeNfoName(); - string[] fileNames = { episodeFileName, nfoFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.Empty, - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsLeft.Should().BeTrue(); - source.LeftToSeq().Should().BeEquivalentTo(episodeFileName); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(episodeFileName, ScanningAction.Add), - new ActionPlan(episodeFileName, ScanningAction.Statistics), - new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ActionPlan(nfoFileName, ScanningAction.Collections)); - } - - [Test] - public void WithoutNfo_WithPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - string episodeFileName = EpisodeNameWithExtension(extension); - string posterFileName = SeriesPosterNameWithExtension(posterExtension); - - string[] fileNames = { episodeFileName, posterFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.Empty, - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsLeft.Should().BeTrue(); - source.LeftToSeq().Should().BeEquivalentTo(episodeFileName); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(episodeFileName, ScanningAction.Add), - new ActionPlan(episodeFileName, ScanningAction.Statistics), - new ActionPlan(episodeFileName, ScanningAction.FallbackMetadata), - new ActionPlan(posterFileName, ScanningAction.Poster), - new ActionPlan(episodeFileName, ScanningAction.Collections)); - } - - [Test] - public void WithNfo_WithPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - string episodeFileName = EpisodeNameWithExtension(extension); - string nfoFileName = EpisodeNfoName(); - string posterFileName = SeriesPosterNameWithExtension(posterExtension); - - string[] fileNames = { episodeFileName, nfoFileName, posterFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.Empty, - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsLeft.Should().BeTrue(); - source.LeftToSeq().Should().BeEquivalentTo(episodeFileName); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(episodeFileName, ScanningAction.Add), - new ActionPlan(episodeFileName, ScanningAction.Statistics), - new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ActionPlan(posterFileName, ScanningAction.Poster), - new ActionPlan(nfoFileName, ScanningAction.Collections)); - } - } - - [TestFixture] - public class ExistingEpisodeTests - { - [Test] - public void Old_File_Should_Do_Nothing( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - var episodeMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = EpisodeNameWithExtension(extension) - }; - - string[] fileNames = { episodeMediaItem.Path }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.create(episodeMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(0); - } - - [Test] - public void Updated_File_Should_Refresh_Statistics( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - var episodeMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = EpisodeNameWithExtension(extension) - }; - - string[] fileNames = { episodeMediaItem.Path }; - - Seq result = ScannerForNewFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.create(episodeMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(episodeMediaItem.Path, ScanningAction.Statistics)); - } - - [Test] - public void Fallback_WithNewNfo_WithoutPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - var episodeMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = EpisodeNameWithExtension(extension) - }; - - string nfoFileName = EpisodeNfoName(); - string[] fileNames = { episodeMediaItem.Path, nfoFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.create(episodeMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ActionPlan(nfoFileName, ScanningAction.Collections)); - } - - [Test] - public void Sidecar_WithOldNfo_WithoutPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - var episodeMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Sidecar }, - Path = EpisodeNameWithExtension(extension) - }; - - string nfoFileName = EpisodeNfoName(); - string[] fileNames = { episodeMediaItem.Path, nfoFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.create(episodeMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(0); - } - - [Test] - public void Sidecar_WithUpdatedNfo_WithoutPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - var episodeMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Sidecar }, - Path = EpisodeNameWithExtension(extension) - }; - - string nfoFileName = EpisodeNfoName(); - string[] fileNames = { episodeMediaItem.Path, nfoFileName }; - - Seq result = - ScannerFor(OldEntriesFor(episodeMediaItem.Path).Concat(NewEntriesFor(nfoFileName))) - .DetermineActions( - MediaType.TvShow, - Seq.create(episodeMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ActionPlan(nfoFileName, ScanningAction.Collections)); - } - - [Test] - public void WithoutNfo_WithNewPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - var episodeMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = EpisodeNameWithExtension(extension) - }; - - string posterFileName = SeriesPosterNameWithExtension(posterExtension); - string[] fileNames = { episodeMediaItem.Path, posterFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.create(episodeMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); - itemScanningPlans.Should() - .BeEquivalentTo(new ActionPlan(posterFileName, ScanningAction.Poster)); - } - - [Test] - public void WithoutNfo_WithOldPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - var episodeMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = EpisodeNameWithExtension(extension), - Poster = "anything", - PosterLastWriteTime = DateTime.UtcNow - }; - - string posterFileName = SeriesPosterNameWithExtension(posterExtension); - string[] fileNames = { episodeMediaItem.Path, posterFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.create(episodeMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(0); - } - - [Test] - public void WithoutNfo_WithUpdatedPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - var episodeMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = EpisodeNameWithExtension(extension), - Poster = "anything", - PosterLastWriteTime = DateTime.UtcNow - }; - - string posterFileName = SeriesPosterNameWithExtension(posterExtension); - string[] fileNames = { episodeMediaItem.Path, posterFileName }; - - Seq result = - ScannerFor(OldEntriesFor(episodeMediaItem.Path).Concat(NewEntriesFor(posterFileName))) - .DetermineActions( - MediaType.TvShow, - Seq.create(episodeMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); - itemScanningPlans.Should() - .BeEquivalentTo(new ActionPlan(posterFileName, ScanningAction.Poster)); - } - - [Test] - public void WithNewNfo_WithNewPoster( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension, - [Values("jpg", "jpeg", "png", "gif", "tbn")] - string posterExtension) - { - var episodeMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = EpisodeNameWithExtension(extension) - }; - - string nfoFileName = EpisodeNfoName(); - string posterFileName = SeriesPosterNameWithExtension(posterExtension); - string[] fileNames = { episodeMediaItem.Path, nfoFileName, posterFileName }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.create(episodeMediaItem), - fileNames.ToSeq()); - - result.Count.Should().Be(1); - (Either source, List itemScanningPlans) = result.Head(); - source.IsRight.Should().BeTrue(); - source.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); - itemScanningPlans.Should().BeEquivalentTo( - new ActionPlan(nfoFileName, ScanningAction.SidecarMetadata), - new ActionPlan(posterFileName, ScanningAction.Poster), - new ActionPlan(nfoFileName, ScanningAction.Collections)); - } - } - - [Test] - public void Movies_Should_Ignore_ExtraFolders( - [Values( - "Behind The Scenes", - "Deleted Scenes", - "Featurettes", - "Interviews", - "Scenes", - "Shorts", - "Trailers", - "Other", - "Extras", - "Specials")] - string folder, - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - string[] fileNames = - { - Path.Combine( - FakeRoot, - Path.Combine( - "movies", - Path.Combine("test (2021)", Path.Combine(folder, $"test (2021).{extension}")))) - }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.Empty, - fileNames.ToSeq()); - - result.Count.Should().Be(0); - } - - [Test] - public void Movies_Should_Ignore_ExtraFiles( - [Values( - "behindthescenes", - "deleted", - "featurette", - "interview", - "scene", - "short", - "trailer", - "other")] - string extra, - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - string[] fileNames = - { - Path.Combine( - FakeRoot, - Path.Combine( - "movies", - Path.Combine("test (2021)", $"test (2021)-{extra}.{extension}"))) - }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.Empty, - fileNames.ToSeq()); - - result.Count.Should().Be(0); - } - - [Test] - public void Movies_Should_Remove_Missing_MediaItems( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - var movieMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = MovieNameWithExtension(extension) - }; - - var movieMediaItem2 = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = MovieNameWithExtension(extension, 2022) - }; - - string[] fileNames = { "anything" }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.Movie, - Seq.create(movieMediaItem, movieMediaItem2), - fileNames.ToSeq()); - - result.Count.Should().Be(2); - - (Either source1, List itemScanningPlans1) = result.Head(); - source1.IsRight.Should().BeTrue(); - source1.RightToSeq().Should().BeEquivalentTo(movieMediaItem); - itemScanningPlans1.Should().BeEquivalentTo( - new ActionPlan(movieMediaItem.Path, ScanningAction.Remove)); - - (Either source2, List itemScanningPlans2) = result.Last(); - source2.IsRight.Should().BeTrue(); - source2.RightToSeq().Should().BeEquivalentTo(movieMediaItem2); - itemScanningPlans2.Should().BeEquivalentTo( - new ActionPlan(movieMediaItem2.Path, ScanningAction.Remove)); - } - - [Test] - public void Episodes_Should_Remove_Missing_MediaItems( - [ValueSource(typeof(LocalMediaSourcePlannerTests), nameof(VideoFileExtensions))] - string extension) - { - var episodeMediaItem = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = EpisodeNameWithExtension(extension) - }; - - var episodeMediaItem2 = new MediaItem - { - Metadata = new MediaMetadata { Source = MetadataSource.Fallback }, - Path = EpisodeNameWithExtension(extension, 4) - }; - - string[] fileNames = { "anything" }; - - Seq result = ScannerForOldFiles(fileNames).DetermineActions( - MediaType.TvShow, - Seq.create(episodeMediaItem, episodeMediaItem2), - fileNames.ToSeq()); - - result.Count.Should().Be(2); - - (Either source1, List itemScanningPlans1) = result.Head(); - source1.IsRight.Should().BeTrue(); - source1.RightToSeq().Should().BeEquivalentTo(episodeMediaItem); - itemScanningPlans1.Should().BeEquivalentTo( - new ActionPlan(episodeMediaItem.Path, ScanningAction.Remove)); - - (Either source2, List itemScanningPlans2) = result.Last(); - source2.IsRight.Should().BeTrue(); - source2.RightToSeq().Should().BeEquivalentTo(episodeMediaItem2); - itemScanningPlans2.Should().BeEquivalentTo( - new ActionPlan(episodeMediaItem2.Path, ScanningAction.Remove)); - } - - private class FakeLocalFileSystem : ILocalFileSystem - { - private readonly Dictionary _files = new(); - - public FakeLocalFileSystem(IEnumerable entries) - { - foreach ((string path, DateTime modifyTime) in entries) - { - _files.Add(path, modifyTime); - } - } - - public DateTime GetLastWriteTime(string path) => - _files.ContainsKey(path) ? _files[path] : DateTime.MinValue; - - public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource) => throw new NotSupportedException(); - - public Seq FindRelevantVideos(LocalMediaSource localMediaSource) => - throw new NotSupportedException(); - - public bool ShouldRefreshMetadata(LocalMediaSource localMediaSource, MediaItem mediaItem) => - throw new NotSupportedException(); - - public bool ShouldRefreshPoster(MediaItem mediaItem) => throw new NotSupportedException(); - } - - private record FakeFileSystemEntry(string Path, DateTime ModifyTime); - } -} diff --git a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs new file mode 100644 index 00000000..8f0e75df --- /dev/null +++ b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs @@ -0,0 +1,370 @@ +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Interfaces.Images; +using ErsatzTV.Core.Interfaces.Metadata; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Metadata; +using ErsatzTV.Core.Tests.Fakes; +using FluentAssertions; +using LanguageExt; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Core.Tests.Metadata +{ + [TestFixture] + public class MovieFolderScannerTests + { + private static readonly string BadFakeRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? @"C:\Movies-That-Dont-Exist" + : @"/movies-that-dont-exist"; + + private static readonly string FakeRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? @"C:\Movies" + : "/movies"; + + private static readonly string FFprobePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? @"C:\bin\ffprobe.exe" + : "/bin/ffprobe"; + + [TestFixture] + public class ScanFolder + { + [SetUp] + public void SetUp() + { + _movieRepository = new Mock(); + _movieRepository.Setup(x => x.GetOrAdd(It.IsAny(), It.IsAny())) + .Returns( + (int _, string path) => + Right(new MovieMediaItem { Path = path }).AsTask()); + + _localStatisticsProvider = new Mock(); + _localMetadataProvider = new Mock(); + + _imageCache = new Mock(); + _imageCache.Setup( + x => x.ResizeAndSaveImage(FakeLocalFileSystem.TestBytes, It.IsAny(), It.IsAny())) + .Returns(Right("poster").AsTask()); + } + + private Mock _movieRepository; + private Mock _localStatisticsProvider; + private Mock _localMetadataProvider; + private Mock _imageCache; + + [Test] + public async Task Missing_Folder() + { + MovieFolderScanner service = GetService( + new FakeFileEntry(Path.Combine(FakeRoot, Path.Combine("Movie (2020)", "Movie (2020).mkv"))) + ); + var source = new LocalMediaSource { Folder = BadFakeRoot }; + + Either result = await service.ScanFolder(source, FFprobePath); + + result.IsLeft.Should().BeTrue(); + result.IfLeft(error => error.Should().BeOfType()); + } + + [Test] + public async Task NewMovie_Statistics_And_FallbackMetadata( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) + ); + var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; + + Either result = await service.ScanFolder(source, FFprobePath); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + Times.Once); + } + + [Test] + public async Task NewMovie_Statistics_And_SidecarMetadata_MovieNameNfo( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + string metadataPath = Path.ChangeExtension(moviePath, "nfo"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath), + new FakeFileEntry(metadataPath) + ); + var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; + + Either result = await service.ScanFolder(source, FFprobePath); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshSidecarMetadata(It.Is(i => i.Path == moviePath), metadataPath), + Times.Once); + } + + [Test] + public async Task NewMovie_Statistics_And_SidecarMetadata_MovieNfo( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + string metadataPath = Path.Combine(Path.GetDirectoryName(moviePath) ?? string.Empty, "movie.nfo"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath), + new FakeFileEntry(metadataPath) + ); + var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; + + Either result = await service.ScanFolder(source, FFprobePath); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshSidecarMetadata(It.Is(i => i.Path == moviePath), metadataPath), + Times.Once); + } + + [Test] + public async Task NewMovie_Statistics_And_FallbackMetadata_And_Poster( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension, + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))] + string imageExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + string posterPath = Path.Combine( + Path.GetDirectoryName(moviePath) ?? string.Empty, + $"poster.{imageExtension}"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath), + new FakeFileEntry(posterPath) + ); + var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; + + Either result = await service.ScanFolder(source, FFprobePath); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + Times.Once); + + _imageCache.Verify( + x => x.ResizeAndSaveImage(FakeLocalFileSystem.TestBytes, It.IsAny(), It.IsAny()), + Times.Once); + } + + [Test] + public async Task NewMovie_Statistics_And_FallbackMetadata_And_MovieNamePoster( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension, + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))] + string imageExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + string posterPath = Path.Combine( + Path.GetDirectoryName(moviePath) ?? string.Empty, + $"Movie (2020)-poster.{imageExtension}"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath), + new FakeFileEntry(posterPath) + ); + var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; + + Either result = await service.ScanFolder(source, FFprobePath); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + Times.Once); + + _imageCache.Verify( + x => x.ResizeAndSaveImage(FakeLocalFileSystem.TestBytes, It.IsAny(), It.IsAny()), + Times.Once); + } + + [Test] + public async Task Should_Ignore_Extra_Files( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension, + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ExtraFiles))] + string extraFile) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath), + new FakeFileEntry( + Path.Combine( + Path.GetDirectoryName(moviePath) ?? string.Empty, + $"Movie (2020)-{extraFile}{videoExtension}")) + ); + var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; + + Either result = await service.ScanFolder(source, FFprobePath); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + Times.Once); + } + + [Test] + public async Task Should_Ignore_Extra_Folders( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension, + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ExtraDirectories))] + string extraFolder) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath), + new FakeFileEntry( + Path.Combine( + Path.GetDirectoryName(moviePath) ?? string.Empty, + Path.Combine(extraFolder, $"Movie (2020){videoExtension}"))) + ); + var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; + + Either result = await service.ScanFolder(source, FFprobePath); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + Times.Once); + } + + [Test] + public async Task Should_Work_With_Nested_Folders( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension) + { + string moviePath = Path.Combine( + Path.Combine(FakeRoot, "L-P"), + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) + ); + var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; + + Either result = await service.ScanFolder(source, FFprobePath); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), + Times.Once); + } + + private MovieFolderScanner GetService(params FakeFileEntry[] files) => + // var mockImageCache = new Mock(); + // mockImageCache.Setup(i => i.ResizeAndSaveImage(It.IsAny(), It.IsAny(), It.IsAny())) + // .Returns(Right("image").AsTask()); + new( + new FakeLocalFileSystem(new List(files)), + _movieRepository.Object, + _localStatisticsProvider.Object, + _localMetadataProvider.Object, + _imageCache.Object, + new Mock>().Object + ); + } + } +} diff --git a/ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs b/ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs index 9a9e35b7..c0022aa6 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs @@ -62,12 +62,12 @@ namespace ErsatzTV.Core.Tests.Scheduling private static List Episodes(int count) => Range(1, count).Map( - i => new MediaItem + i => (MediaItem) new TelevisionEpisodeMediaItem { Id = i, - Metadata = new MediaMetadata + Metadata = new TelevisionEpisodeMetadata { - MediaType = MediaType.TvShow, Aired = new DateTime(2020, 1, i) + Aired = new DateTime(2020, 1, i) } }) .Reverse() diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs index d67f4d55..acee730a 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs @@ -40,7 +40,7 @@ namespace ErsatzTV.Core.Tests.Scheduling [Test] public async Task InitialFlood_Should_StartAtMidnight() { - var mediaItems = new List + var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) }; @@ -59,7 +59,7 @@ namespace ErsatzTV.Core.Tests.Scheduling [Test] public async Task InitialFlood_Should_StartAtMidnight_With_LateStart() { - var mediaItems = new List + var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) }; @@ -79,7 +79,7 @@ namespace ErsatzTV.Core.Tests.Scheduling [Test] public async Task ChronologicalContent_Should_CreateChronologicalItems() { - var mediaItems = new List + var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) @@ -105,7 +105,7 @@ namespace ErsatzTV.Core.Tests.Scheduling [Test] public async Task ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout() { - var mediaItems = new List + var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) @@ -142,7 +142,7 @@ namespace ErsatzTV.Core.Tests.Scheduling [Test] public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems() { - var mediaItems = new List + var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) @@ -180,7 +180,7 @@ namespace ErsatzTV.Core.Tests.Scheduling [Test] public async Task ShuffleFloodRebuild_Should_IgnoreAnchors() { - var mediaItems = new List + var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), @@ -223,7 +223,7 @@ namespace ErsatzTV.Core.Tests.Scheduling [Test] public async Task ShuffleFlood_Should_MaintainRandomSeed() { - var mediaItems = new List + var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), @@ -259,7 +259,7 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = 1, Name = "Flood Items", - Items = new List + Movies = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) @@ -270,7 +270,7 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = 2, Name = "Fixed Items", - Items = new List + Movies = new List { TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)) } @@ -278,8 +278,8 @@ namespace ErsatzTV.Core.Tests.Scheduling var fakeRepository = new FakeMediaCollectionRepository( Map( - (floodCollection.Id, floodCollection.Items.ToList()), - (fixedCollection.Id, fixedCollection.Items.ToList()))); + (floodCollection.Id, floodCollection.Movies.ToList()), + (fixedCollection.Id, fixedCollection.Movies.ToList()))); var items = new List { @@ -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); @@ -336,7 +337,7 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = 1, Name = "Flood Items", - Items = new List + Movies = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) @@ -347,7 +348,7 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = 2, Name = "Fixed Items", - Items = new List + Movies = new List { TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) @@ -356,8 +357,8 @@ namespace ErsatzTV.Core.Tests.Scheduling var fakeRepository = new FakeMediaCollectionRepository( Map( - (floodCollection.Id, floodCollection.Items.ToList()), - (fixedCollection.Id, fixedCollection.Items.ToList()))); + (floodCollection.Id, floodCollection.Movies.ToList()), + (fixedCollection.Id, fixedCollection.Movies.ToList()))); var items = new List { @@ -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); @@ -420,7 +422,7 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = 1, Name = "Flood Items", - Items = new List + Movies = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) @@ -431,7 +433,7 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = 2, Name = "Fixed Items", - Items = new List + Movies = new List { TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)), TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2)) @@ -440,8 +442,8 @@ namespace ErsatzTV.Core.Tests.Scheduling var fakeRepository = new FakeMediaCollectionRepository( Map( - (floodCollection.Id, floodCollection.Items.ToList()), - (fixedCollection.Id, fixedCollection.Items.ToList()))); + (floodCollection.Id, floodCollection.Movies.ToList()), + (fixedCollection.Id, fixedCollection.Movies.ToList()))); var items = new List { @@ -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); @@ -508,7 +511,7 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = 1, Name = "Multiple Items", - Items = new List + Movies = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) @@ -519,7 +522,7 @@ namespace ErsatzTV.Core.Tests.Scheduling { Id = 2, Name = "Dynamic Items", - Items = new List + Movies = new List { TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)), TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2)) @@ -528,8 +531,8 @@ namespace ErsatzTV.Core.Tests.Scheduling var fakeRepository = new FakeMediaCollectionRepository( Map( - (multipleCollection.Id, multipleCollection.Items.ToList()), - (dynamicCollection.Id, dynamicCollection.Items.ToList()))); + (multipleCollection.Id, multipleCollection.Movies.ToList()), + (dynamicCollection.Id, dynamicCollection.Movies.ToList()))); var items = new List { @@ -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); @@ -603,23 +607,25 @@ namespace ErsatzTV.Core.Tests.Scheduling StartTime = null }; - private static MediaItem TestMovie(int id, TimeSpan duration, DateTime aired) => + private static MovieMediaItem TestMovie(int id, TimeSpan duration, DateTime aired) => new() { Id = id, - Metadata = new MediaMetadata { Duration = duration, MediaType = MediaType.Movie, Aired = aired } + Metadata = new MovieMetadata { Premiered = aired }, + Statistics = new MediaItemStatistics { Duration = duration } }; - private TestData TestDataFloodForItems(List mediaItems, PlaybackOrder playbackOrder) + private TestData TestDataFloodForItems(List mediaItems, PlaybackOrder playbackOrder) { var mediaCollection = new SimpleMediaCollection { Id = 1, - Items = mediaItems + Movies = mediaItems }; 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.Tests/Scheduling/RandomizedContentTests.cs b/ErsatzTV.Core.Tests/Scheduling/RandomizedContentTests.cs index 8992c6ed..13f3d8f0 100644 --- a/ErsatzTV.Core.Tests/Scheduling/RandomizedContentTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/RandomizedContentTests.cs @@ -80,12 +80,12 @@ namespace ErsatzTV.Core.Tests.Scheduling private static List Episodes(int count) => Range(1, count).Map( - i => new MediaItem + i => (MediaItem) new TelevisionEpisodeMediaItem { Id = i, - Metadata = new MediaMetadata + Metadata = new TelevisionEpisodeMetadata { - MediaType = MediaType.TvShow, Aired = new DateTime(2020, 1, i) + Aired = new DateTime(2020, 1, i) } }) .Reverse() diff --git a/ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs b/ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs index f767f3f1..eeeaef98 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs @@ -70,12 +70,12 @@ namespace ErsatzTV.Core.Tests.Scheduling private static List Episodes(int count) => Range(1, count).Map( - i => new MediaItem + i => (MediaItem) new TelevisionEpisodeMediaItem { Id = i, - Metadata = new MediaMetadata + Metadata = new TelevisionEpisodeMetadata { - MediaType = MediaType.TvShow, Aired = new DateTime(2020, 1, i) + Aired = new DateTime(2020, 1, i) } }) .Reverse() diff --git a/ErsatzTV.Core/Domain/FFmpegProfile.cs b/ErsatzTV.Core/Domain/FFmpegProfile.cs index c12aa460..e1931513 100644 --- a/ErsatzTV.Core/Domain/FFmpegProfile.cs +++ b/ErsatzTV.Core/Domain/FFmpegProfile.cs @@ -33,9 +33,9 @@ VideoCodec = "libx264", AudioCodec = "ac3", VideoBitrate = 2000, - VideoBufferSize = 2000, + VideoBufferSize = 4000, AudioBitrate = 192, - AudioBufferSize = 50, + AudioBufferSize = 384, AudioVolume = 100, AudioChannels = 2, AudioSampleRate = 48, diff --git a/ErsatzTV.Core/Domain/LocalTelevisionShowSource.cs b/ErsatzTV.Core/Domain/LocalTelevisionShowSource.cs new file mode 100644 index 00000000..05515809 --- /dev/null +++ b/ErsatzTV.Core/Domain/LocalTelevisionShowSource.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Core.Domain +{ + public class LocalTelevisionShowSource : TelevisionShowSource + { + public int MediaSourceId { get; set; } + public LocalMediaSource MediaSource { get; set; } + public string Path { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/MediaItem.cs b/ErsatzTV.Core/Domain/MediaItem.cs index 5136d3b8..a17b305e 100644 --- a/ErsatzTV.Core/Domain/MediaItem.cs +++ b/ErsatzTV.Core/Domain/MediaItem.cs @@ -1,18 +1,17 @@ using System; -using System.Collections.Generic; +using ErsatzTV.Core.Interfaces.Domain; namespace ErsatzTV.Core.Domain { - public class MediaItem + public class MediaItem : IHasAPoster { public int Id { get; set; } public int MediaSourceId { get; set; } public MediaSource Source { get; set; } + public MediaItemStatistics Statistics { get; set; } + public DateTime? LastWriteTime { get; set; } public string Path { get; set; } public string Poster { get; set; } public DateTime? PosterLastWriteTime { get; set; } - public MediaMetadata Metadata { get; set; } - public DateTime? LastWriteTime { get; set; } - public IList SimpleMediaCollections { get; set; } } } diff --git a/ErsatzTV.Core/Domain/MediaItemMetadata.cs b/ErsatzTV.Core/Domain/MediaItemMetadata.cs new file mode 100644 index 00000000..4d46e894 --- /dev/null +++ b/ErsatzTV.Core/Domain/MediaItemMetadata.cs @@ -0,0 +1,12 @@ +using System; + +namespace ErsatzTV.Core.Domain +{ + public class MediaItemMetadata + { + public MetadataSource Source { get; set; } + public DateTime? LastWriteTime { get; set; } + public string Title { get; set; } + public string SortTitle { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/MediaMetadata.cs b/ErsatzTV.Core/Domain/MediaItemStatistics.cs similarity index 51% rename from ErsatzTV.Core/Domain/MediaMetadata.cs rename to ErsatzTV.Core/Domain/MediaItemStatistics.cs index 7e725d0b..56b0f59c 100644 --- a/ErsatzTV.Core/Domain/MediaMetadata.cs +++ b/ErsatzTV.Core/Domain/MediaItemStatistics.cs @@ -3,24 +3,14 @@ using ErsatzTV.Core.Interfaces.FFmpeg; namespace ErsatzTV.Core.Domain { - public record MediaMetadata : IDisplaySize + public record MediaItemStatistics : IDisplaySize { - public MetadataSource Source { get; set; } public DateTime? LastWriteTime { get; set; } public TimeSpan Duration { get; set; } public string SampleAspectRatio { get; set; } public string DisplayAspectRatio { get; set; } public string VideoCodec { get; set; } public string AudioCodec { get; set; } - public MediaType MediaType { get; set; } - public string Title { get; set; } - public string SortTitle { get; set; } - public string Subtitle { get; set; } - public string Description { get; set; } - public int? SeasonNumber { get; set; } - public int? EpisodeNumber { get; set; } - public string ContentRating { get; set; } - public DateTime? Aired { get; set; } public VideoScanType VideoScanType { get; set; } public int Width { get; set; } public int Height { get; set; } diff --git a/ErsatzTV.Core/Domain/MovieMediaItem.cs b/ErsatzTV.Core/Domain/MovieMediaItem.cs new file mode 100644 index 00000000..9b2d44fe --- /dev/null +++ b/ErsatzTV.Core/Domain/MovieMediaItem.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Core.Domain +{ + public class MovieMediaItem : MediaItem + { + public int MetadataId { get; set; } + public MovieMetadata Metadata { get; set; } + public List SimpleMediaCollections { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/MovieMetadata.cs b/ErsatzTV.Core/Domain/MovieMetadata.cs new file mode 100644 index 00000000..3c4009ee --- /dev/null +++ b/ErsatzTV.Core/Domain/MovieMetadata.cs @@ -0,0 +1,17 @@ +using System; + +namespace ErsatzTV.Core.Domain +{ + public class MovieMetadata : MediaItemMetadata + { + public int Id { get; set; } + public int MovieId { get; set; } + public MovieMediaItem Movie { get; set; } + public int? Year { get; set; } + public DateTime? Premiered { get; set; } + public string Plot { get; set; } + public string Outline { get; set; } + public string Tagline { get; set; } + public string ContentRating { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs b/ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs index 17405561..d7cea705 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 25363406..ecc12d02 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 00000000..e0dcd6cf --- /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/Domain/ResolutionKey.cs b/ErsatzTV.Core/Domain/ResolutionKey.cs deleted file mode 100644 index 65d3d737..00000000 --- a/ErsatzTV.Core/Domain/ResolutionKey.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ErsatzTV.Core.Domain -{ - public enum ResolutionKey - { - None = 0, - W720H480 = 1, - W1280H720 = 2, - W1920H1080 = 3, - W3840H2160 = 4 - } -} diff --git a/ErsatzTV.Core/Domain/SimpleMediaCollection.cs b/ErsatzTV.Core/Domain/SimpleMediaCollection.cs index 6f7a1b13..f475a4ad 100644 --- a/ErsatzTV.Core/Domain/SimpleMediaCollection.cs +++ b/ErsatzTV.Core/Domain/SimpleMediaCollection.cs @@ -4,6 +4,9 @@ namespace ErsatzTV.Core.Domain { public class SimpleMediaCollection : MediaCollection { - public IList Items { get; set; } + public List Movies { get; set; } + public List TelevisionShows { get; set; } + public List TelevisionSeasons { get; set; } + public List TelevisionEpisodes { get; set; } } } diff --git a/ErsatzTV.Core/Domain/TelevisionEpisodeMediaItem.cs b/ErsatzTV.Core/Domain/TelevisionEpisodeMediaItem.cs new file mode 100644 index 00000000..bd2d66bc --- /dev/null +++ b/ErsatzTV.Core/Domain/TelevisionEpisodeMediaItem.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Core.Domain +{ + public class TelevisionEpisodeMediaItem : MediaItem + { + public int SeasonId { get; set; } + public TelevisionSeason Season { get; set; } + public TelevisionEpisodeMetadata Metadata { get; set; } + public List SimpleMediaCollections { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/TelevisionEpisodeMetadata.cs b/ErsatzTV.Core/Domain/TelevisionEpisodeMetadata.cs new file mode 100644 index 00000000..9b210e4e --- /dev/null +++ b/ErsatzTV.Core/Domain/TelevisionEpisodeMetadata.cs @@ -0,0 +1,15 @@ +using System; + +namespace ErsatzTV.Core.Domain +{ + public class TelevisionEpisodeMetadata : MediaItemMetadata + { + public int Id { get; set; } + public int TelevisionEpisodeId { get; set; } + public TelevisionEpisodeMediaItem TelevisionEpisode { get; set; } + public int Season { get; set; } + public int Episode { get; set; } + public string Plot { get; set; } + public DateTime? Aired { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/TelevisionMediaCollection.cs b/ErsatzTV.Core/Domain/TelevisionMediaCollection.cs deleted file mode 100644 index 3d872c9b..00000000 --- a/ErsatzTV.Core/Domain/TelevisionMediaCollection.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ErsatzTV.Core.Domain -{ - public class TelevisionMediaCollection : MediaCollection - { - public string ShowTitle { get; set; } - public int? SeasonNumber { get; set; } - } -} diff --git a/ErsatzTV.Core/Domain/TelevisionSeason.cs b/ErsatzTV.Core/Domain/TelevisionSeason.cs new file mode 100644 index 00000000..009bbf3c --- /dev/null +++ b/ErsatzTV.Core/Domain/TelevisionSeason.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using ErsatzTV.Core.Interfaces.Domain; + +namespace ErsatzTV.Core.Domain +{ + public class TelevisionSeason : IHasAPoster + { + public int Id { get; set; } + public int TelevisionShowId { get; set; } + public TelevisionShow TelevisionShow { get; set; } + public int Number { get; set; } + public List Episodes { get; set; } + public List SimpleMediaCollections { get; set; } + public string Path { get; set; } + public string Poster { get; set; } + public DateTime? PosterLastWriteTime { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/TelevisionShow.cs b/ErsatzTV.Core/Domain/TelevisionShow.cs new file mode 100644 index 00000000..e50793d2 --- /dev/null +++ b/ErsatzTV.Core/Domain/TelevisionShow.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace ErsatzTV.Core.Domain +{ + public class TelevisionShow + { + public int Id { get; set; } + public List Sources { get; set; } + public TelevisionShowMetadata Metadata { get; set; } + public List Seasons { get; set; } + public string Poster { get; set; } + public DateTime? PosterLastWriteTime { get; set; } + public List SimpleMediaCollections { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/TelevisionShowMetadata.cs b/ErsatzTV.Core/Domain/TelevisionShowMetadata.cs new file mode 100644 index 00000000..914de83d --- /dev/null +++ b/ErsatzTV.Core/Domain/TelevisionShowMetadata.cs @@ -0,0 +1,17 @@ +using System; + +namespace ErsatzTV.Core.Domain +{ + public class TelevisionShowMetadata + { + public int Id { get; set; } + public int TelevisionShowId { get; set; } + public TelevisionShow TelevisionShow { get; set; } + public MetadataSource Source { get; set; } + public DateTime? LastWriteTime { get; set; } + public string Title { get; set; } + public string SortTitle { get; set; } + public int? Year { get; set; } + public string Plot { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/TelevisionShowSource.cs b/ErsatzTV.Core/Domain/TelevisionShowSource.cs new file mode 100644 index 00000000..43f7890a --- /dev/null +++ b/ErsatzTV.Core/Domain/TelevisionShowSource.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Core.Domain +{ + public class TelevisionShowSource + { + public int Id { get; set; } + public int TelevisionShowId { get; set; } + public TelevisionShow TelevisionShow { get; set; } + } +} diff --git a/ErsatzTV.Core/Errors/MediaSourceInaccessible.cs b/ErsatzTV.Core/Errors/MediaSourceInaccessible.cs new file mode 100644 index 00000000..7ce775a3 --- /dev/null +++ b/ErsatzTV.Core/Errors/MediaSourceInaccessible.cs @@ -0,0 +1,10 @@ +namespace ErsatzTV.Core.Errors +{ + public class MediaSourceInaccessible : BaseError + { + public MediaSourceInaccessible() + : base("Media source is not accessible or missing") + { + } + } +} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs index f06e755d..479b906f 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs @@ -66,24 +66,24 @@ namespace ErsatzTV.Core.FFmpeg result.Deinterlace = false; break; case StreamingMode.TransportStream: - if (NeedToScale(ffmpegProfile, playoutItem.MediaItem.Metadata)) + if (NeedToScale(ffmpegProfile, playoutItem.MediaItem.Statistics)) { - IDisplaySize scaledSize = CalculateScaledSize(ffmpegProfile, playoutItem.MediaItem.Metadata); - if (!scaledSize.IsSameSizeAs(playoutItem.MediaItem.Metadata)) + IDisplaySize scaledSize = CalculateScaledSize(ffmpegProfile, playoutItem.MediaItem.Statistics); + if (!scaledSize.IsSameSizeAs(playoutItem.MediaItem.Statistics)) { result.ScaledSize = Some( - CalculateScaledSize(ffmpegProfile, playoutItem.MediaItem.Metadata)); + CalculateScaledSize(ffmpegProfile, playoutItem.MediaItem.Statistics)); } } - IDisplaySize sizeAfterScaling = result.ScaledSize.IfNone(playoutItem.MediaItem.Metadata); + IDisplaySize sizeAfterScaling = result.ScaledSize.IfNone(playoutItem.MediaItem.Statistics); if (!sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution)) { result.PadToDesiredResolution = true; } if (result.ScaledSize.IsSome || result.PadToDesiredResolution || - NeedToNormalizeVideoCodec(ffmpegProfile, playoutItem.MediaItem.Metadata)) + NeedToNormalizeVideoCodec(ffmpegProfile, playoutItem.MediaItem.Statistics)) { result.VideoCodec = ffmpegProfile.VideoCodec; result.VideoBitrate = ffmpegProfile.VideoBitrate; @@ -94,7 +94,7 @@ namespace ErsatzTV.Core.FFmpeg result.VideoCodec = "copy"; } - if (NeedToNormalizeAudioCodec(ffmpegProfile, playoutItem.MediaItem.Metadata)) + if (NeedToNormalizeAudioCodec(ffmpegProfile, playoutItem.MediaItem.Statistics)) { result.AudioCodec = ffmpegProfile.AudioCodec; result.AudioBitrate = ffmpegProfile.AudioBitrate; @@ -104,7 +104,7 @@ namespace ErsatzTV.Core.FFmpeg { result.AudioChannels = ffmpegProfile.AudioChannels; result.AudioSampleRate = ffmpegProfile.AudioSampleRate; - result.AudioDuration = playoutItem.MediaItem.Metadata.Duration; + result.AudioDuration = playoutItem.MediaItem.Statistics.Duration; } } else @@ -112,7 +112,7 @@ namespace ErsatzTV.Core.FFmpeg result.AudioCodec = "copy"; } - if (playoutItem.MediaItem.Metadata.VideoScanType == VideoScanType.Interlaced) + if (playoutItem.MediaItem.Statistics.VideoScanType == VideoScanType.Interlaced) { result.Deinterlace = true; } @@ -132,16 +132,16 @@ namespace ErsatzTV.Core.FFmpeg AudioCodec = ffmpegProfile.AudioCodec }; - private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaMetadata mediaMetadata) => + private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaItemStatistics statistics) => ffmpegProfile.NormalizeResolution && - IsIncorrectSize(ffmpegProfile.Resolution, mediaMetadata) || - IsTooLarge(ffmpegProfile.Resolution, mediaMetadata) || - IsOddSize(mediaMetadata); + IsIncorrectSize(ffmpegProfile.Resolution, statistics) || + IsTooLarge(ffmpegProfile.Resolution, statistics) || + IsOddSize(statistics); - private static bool IsIncorrectSize(IDisplaySize desiredResolution, MediaMetadata mediaMetadata) => - IsAnamorphic(mediaMetadata) || - mediaMetadata.Width != desiredResolution.Width || - mediaMetadata.Height != desiredResolution.Height; + private static bool IsIncorrectSize(IDisplaySize desiredResolution, MediaItemStatistics statistics) => + IsAnamorphic(statistics) || + statistics.Width != desiredResolution.Width || + statistics.Height != desiredResolution.Height; private static bool IsTooLarge(IDisplaySize desiredResolution, IDisplaySize mediaSize) => mediaSize.Height > desiredResolution.Height || @@ -150,17 +150,17 @@ namespace ErsatzTV.Core.FFmpeg private static bool IsOddSize(IDisplaySize displaySize) => displaySize.Height % 2 == 1 || displaySize.Width % 2 == 1; - private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaMetadata mediaMetadata) => - ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != mediaMetadata.VideoCodec; + private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaItemStatistics statistics) => + ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != statistics.VideoCodec; - private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaMetadata mediaMetadata) => - ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != mediaMetadata.AudioCodec; + private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaItemStatistics statistics) => + ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != statistics.AudioCodec; - private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaMetadata mediaMetadata) + private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaItemStatistics statistics) { - IDisplaySize sarSize = SARSize(mediaMetadata); - int p = mediaMetadata.Width * sarSize.Width; - int q = mediaMetadata.Height * sarSize.Height; + IDisplaySize sarSize = SARSize(statistics); + int p = statistics.Width * sarSize.Width; + int q = statistics.Height * sarSize.Height; int g = Gcd(q, p); p = p / g; q = q / g; @@ -194,29 +194,29 @@ namespace ErsatzTV.Core.FFmpeg return a | b; } - private static bool IsAnamorphic(MediaMetadata mediaMetadata) + private static bool IsAnamorphic(MediaItemStatistics statistics) { - if (mediaMetadata.SampleAspectRatio == "1:1") + if (statistics.SampleAspectRatio == "1:1") { return false; } - if (mediaMetadata.SampleAspectRatio != "0:1") + if (statistics.SampleAspectRatio != "0:1") { return true; } - if (mediaMetadata.DisplayAspectRatio == "0:1") + if (statistics.DisplayAspectRatio == "0:1") { return false; } - return mediaMetadata.DisplayAspectRatio != $"{mediaMetadata.Width}:{mediaMetadata.Height}"; + return statistics.DisplayAspectRatio != $"{statistics.Width}:{statistics.Height}"; } - private static IDisplaySize SARSize(MediaMetadata mediaMetadata) + private static IDisplaySize SARSize(MediaItemStatistics statistics) { - string[] split = mediaMetadata.SampleAspectRatio.Split(":"); + string[] split = statistics.SampleAspectRatio.Split(":"); return new DisplaySize(int.Parse(split[0]), int.Parse(split[1])); } } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs index cfb13473..99fd9ad8 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs @@ -76,7 +76,7 @@ namespace ErsatzTV.Core.FFmpeg return builder.WithPlaybackArgs(playbackSettings) .WithMetadata(channel) .WithFormat("mpegts") - .WithDuration(item.Start + item.MediaItem.Metadata.Duration - now) + .WithDuration(item.Start + item.MediaItem.Statistics.Duration - now) .WithPipe() .Build(); } diff --git a/ErsatzTV.Core/Interfaces/Domain/IHasAPoster.cs b/ErsatzTV.Core/Interfaces/Domain/IHasAPoster.cs new file mode 100644 index 00000000..72007f6e --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Domain/IHasAPoster.cs @@ -0,0 +1,11 @@ +using System; + +namespace ErsatzTV.Core.Interfaces.Domain +{ + public interface IHasAPoster + { + string Path { get; set; } + string Poster { get; set; } + DateTime? PosterLastWriteTime { get; set; } + } +} diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IDisplaySize.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IDisplaySize.cs index 7b1562fb..36b12cc4 100644 --- a/ErsatzTV.Core/Interfaces/FFmpeg/IDisplaySize.cs +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IDisplaySize.cs @@ -2,7 +2,7 @@ { public interface IDisplaySize { - public int Width { get; } - public int Height { get; } + int Width { get; } + int Height { get; } } } diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegLocator.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegLocator.cs index 04a18a22..37262349 100644 --- a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegLocator.cs +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegLocator.cs @@ -6,6 +6,6 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg { public interface IFFmpegLocator { - public Task> ValidatePath(string executableBase, ConfigElementKey key); + Task> ValidatePath(string executableBase, ConfigElementKey key); } } diff --git a/ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs b/ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs index f750db1b..e51ccf70 100644 --- a/ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs +++ b/ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs @@ -4,9 +4,9 @@ namespace ErsatzTV.Core.Interfaces.Locking { public interface IEntityLocker { - public event EventHandler OnMediaSourceChanged; - public bool LockMediaSource(int mediaSourceId); - public bool UnlockMediaSource(int mediaSourceId); - public bool IsMediaSourceLocked(int mediaSourceId); + event EventHandler OnMediaSourceChanged; + bool LockMediaSource(int mediaSourceId); + bool UnlockMediaSource(int mediaSourceId); + bool IsMediaSourceLocked(int mediaSourceId); } } diff --git a/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs new file mode 100644 index 00000000..4325b1fd --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Core.Interfaces.Metadata +{ + public interface IFallbackMetadataProvider + { + TelevisionShowMetadata GetFallbackMetadataForShow(string showFolder); + } +} diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs index d2d0bb57..4a698e0d 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs @@ -1,15 +1,17 @@ using System; +using System.Collections.Generic; +using System.Threading.Tasks; using ErsatzTV.Core.Domain; -using LanguageExt; namespace ErsatzTV.Core.Interfaces.Metadata { public interface ILocalFileSystem { - public DateTime GetLastWriteTime(string path); - public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource); - public Seq FindRelevantVideos(LocalMediaSource localMediaSource); - public bool ShouldRefreshMetadata(LocalMediaSource localMediaSource, MediaItem mediaItem); - public bool ShouldRefreshPoster(MediaItem mediaItem); + DateTime GetLastWriteTime(string path); + bool IsMediaSourceAccessible(LocalMediaSource localMediaSource); + IEnumerable ListSubdirectories(string folder); + IEnumerable ListFiles(string folder); + bool FileExists(string path); + Task ReadAllBytes(string path); } } diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs deleted file mode 100644 index 4b8ed759..00000000 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Metadata; -using LanguageExt; - -namespace ErsatzTV.Core.Interfaces.Metadata -{ - public interface ILocalMediaScanner - { - Task ScanLocalMediaSource( - LocalMediaSource localMediaSource, - string ffprobePath, - ScanningMode scanningMode); - } -} diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaSourcePlanner.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaSourcePlanner.cs deleted file mode 100644 index 555463b5..00000000 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaSourcePlanner.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Metadata; -using LanguageExt; - -namespace ErsatzTV.Core.Interfaces.Metadata -{ - public interface ILocalMediaSourcePlanner - { - public Seq DetermineActions( - MediaType mediaType, - Seq mediaItems, - Seq files); - } -} diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs index 8e413e1a..317e3588 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs @@ -1,11 +1,15 @@ using System.Threading.Tasks; using ErsatzTV.Core.Domain; +using LanguageExt; namespace ErsatzTV.Core.Interfaces.Metadata { public interface ILocalMetadataProvider { - Task RefreshSidecarMetadata(MediaItem mediaItem, string path); - Task RefreshFallbackMetadata(MediaItem mediaItem); + Task GetMetadataForShow(string showFolder); + Task RefreshSidecarMetadata(MediaItem mediaItem, string path); + Task RefreshSidecarMetadata(TelevisionShow televisionShow, string showFolder); + Task RefreshFallbackMetadata(MediaItem mediaItem); + Task RefreshFallbackMetadata(TelevisionShow televisionShow, string showFolder); } } diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs deleted file mode 100644 index ab47e5da..00000000 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; - -namespace ErsatzTV.Core.Interfaces.Metadata -{ - public interface ILocalPosterProvider - { - Task RefreshPoster(MediaItem mediaItem); - } -} diff --git a/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs new file mode 100644 index 00000000..cf43a87d --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using LanguageExt; + +namespace ErsatzTV.Core.Interfaces.Metadata +{ + public interface IMovieFolderScanner + { + Task> ScanFolder(LocalMediaSource localMediaSource, string ffprobePath); + } +} diff --git a/ErsatzTV.Core/Interfaces/Metadata/ISmartCollectionBuilder.cs b/ErsatzTV.Core/Interfaces/Metadata/ISmartCollectionBuilder.cs deleted file mode 100644 index 0983b5fd..00000000 --- a/ErsatzTV.Core/Interfaces/Metadata/ISmartCollectionBuilder.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; - -namespace ErsatzTV.Core.Interfaces.Metadata -{ - public interface ISmartCollectionBuilder - { - Task RefreshSmartCollections(MediaItem mediaItem); - } -} diff --git a/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs new file mode 100644 index 00000000..b000216b --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using LanguageExt; + +namespace ErsatzTV.Core.Interfaces.Metadata +{ + public interface ITelevisionFolderScanner + { + Task ScanFolder(LocalMediaSource localMediaSource, string ffprobePath); + } +} diff --git a/ErsatzTV.Core/Interfaces/Plex/IPlexSecretStore.cs b/ErsatzTV.Core/Interfaces/Plex/IPlexSecretStore.cs index f3a3bdf2..cf6973b8 100644 --- a/ErsatzTV.Core/Interfaces/Plex/IPlexSecretStore.cs +++ b/ErsatzTV.Core/Interfaces/Plex/IPlexSecretStore.cs @@ -7,11 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Plex { public interface IPlexSecretStore { - public Task GetClientIdentifier(); - public Task> GetUserAuthTokens(); - public Task UpsertUserAuthToken(PlexUserAuthToken userAuthToken); - public Task> GetServerAuthTokens(); - public Task> GetServerAuthToken(string clientIdentifier); - public Task UpsertServerAuthToken(PlexServerAuthToken serverAuthToken); + Task GetClientIdentifier(); + Task> GetUserAuthTokens(); + Task UpsertUserAuthToken(PlexUserAuthToken userAuthToken); + Task> GetServerAuthTokens(); + Task> GetServerAuthToken(string clientIdentifier); + Task UpsertServerAuthToken(PlexServerAuthToken serverAuthToken); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs index 14b3954c..fda5b1cd 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs @@ -7,12 +7,12 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface IChannelRepository { - public Task Add(Channel channel); - public Task> Get(int id); - public Task> GetByNumber(int number); - public Task> GetAll(); - public Task> GetAllForGuide(); - public Task Update(Channel channel); - public Task Delete(int channelId); + Task Add(Channel channel); + Task> Get(int id); + Task> GetByNumber(int number); + Task> GetAll(); + Task> GetAllForGuide(); + Task Update(Channel channel); + Task Delete(int channelId); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IConfigElementRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IConfigElementRepository.cs index 087e62cc..bd83fc4e 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IConfigElementRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IConfigElementRepository.cs @@ -6,10 +6,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface IConfigElementRepository { - public Task Add(ConfigElement configElement); - public Task> Get(ConfigElementKey key); - public Task> GetValue(ConfigElementKey key); - public Task Update(ConfigElement configElement); - public Task Delete(ConfigElement configElement); + Task Add(ConfigElement configElement); + Task> Get(ConfigElementKey key); + Task> GetValue(ConfigElementKey key); + Task Update(ConfigElement configElement); + Task Delete(ConfigElement configElement); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IFFmpegProfileRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IFFmpegProfileRepository.cs index ac4962ee..c08ef846 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IFFmpegProfileRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IFFmpegProfileRepository.cs @@ -7,10 +7,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface IFFmpegProfileRepository { - public Task Add(FFmpegProfile ffmpegProfile); - public Task> Get(int id); - public Task> GetAll(); - public Task Update(FFmpegProfile ffmpegProfile); - public Task Delete(int ffmpegProfileId); + Task Add(FFmpegProfile ffmpegProfile); + Task> Get(int id); + Task> GetAll(); + Task Update(FFmpegProfile ffmpegProfile); + Task Delete(int ffmpegProfileId); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/ILogRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ILogRepository.cs index 096d24be..a285f45f 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/ILogRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/ILogRepository.cs @@ -6,6 +6,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface ILogRepository { - public Task> GetRecentLogEntries(); + Task> GetRecentLogEntries(); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs index 67e2ccc9..4dd81dfb 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Threading.Tasks; -using ErsatzTV.Core.AggregateModels; using ErsatzTV.Core.Domain; using LanguageExt; @@ -8,21 +7,16 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface IMediaCollectionRepository { - public Task Add(SimpleMediaCollection collection); - public Task> Get(int id); - public Task> GetSimpleMediaCollection(int id); - public Task> GetSimpleMediaCollectionWithItems(int id); - public Task> GetTelevisionMediaCollection(int id); - public Task> GetSimpleMediaCollections(); - public Task> GetAll(); - public Task> GetSummaries(string searchString); - public Task>> GetItems(int id); - public Task>> GetSimpleMediaCollectionItems(int id); - public Task>> GetTelevisionMediaCollectionItems(int id); - public Task Update(SimpleMediaCollection collection); - public Task InsertOrIgnore(TelevisionMediaCollection collection); - public Task ReplaceItems(int collectionId, List mediaItems); - public Task Delete(int mediaCollectionId); - public Task DeleteEmptyTelevisionCollections(); + Task Add(SimpleMediaCollection collection); + Task> Get(int id); + Task> GetSimpleMediaCollection(int id); + Task> GetSimpleMediaCollectionWithItems(int id); + Task> GetSimpleMediaCollectionWithItemsUntracked(int id); + Task> GetSimpleMediaCollections(); + Task> GetAll(); + Task>> GetItems(int id); + Task>> GetSimpleMediaCollectionItems(int id); + Task Update(SimpleMediaCollection collection); + Task Delete(int mediaCollectionId); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs index 513ef76e..44345271 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Threading.Tasks; -using ErsatzTV.Core.AggregateModels; using ErsatzTV.Core.Domain; using LanguageExt; @@ -8,14 +7,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface IMediaItemRepository { - public Task Add(MediaItem mediaItem); - public Task> Get(int id); - public Task> GetAll(); - public Task> Search(string searchString); - public Task> GetPageByType(MediaType mediaType, int pageNumber, int pageSize); - public Task GetCountByType(MediaType mediaType); - public Task> GetAllByMediaSourceId(int mediaSourceId); - public Task Update(MediaItem mediaItem); - public Task Delete(int mediaItemId); + Task> Get(int id); + Task> GetAll(); + Task> Search(string searchString); + Task Update(MediaItem mediaItem); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs index e5246b90..4469f45d 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs @@ -7,14 +7,14 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface IMediaSourceRepository { - public Task Add(LocalMediaSource localMediaSource); - public Task Add(PlexMediaSource plexMediaSource); - public Task> GetAll(); - public Task> GetAllPlex(); - public Task> Get(int id); - public Task> GetPlex(int id); - public Task CountMediaItems(int id); - public Task Update(PlexMediaSource plexMediaSource); - public Task Delete(int id); + Task Add(LocalMediaSource localMediaSource); + Task Add(PlexMediaSource plexMediaSource); + Task> GetAll(); + Task> GetAllPlex(); + Task> Get(int id); + Task> GetPlex(int id); + Task CountMediaItems(int id); + Task Update(PlexMediaSource plexMediaSource); + Task Delete(int id); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs new file mode 100644 index 00000000..08371123 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using LanguageExt; + +namespace ErsatzTV.Core.Interfaces.Repositories +{ + public interface IMovieRepository + { + Task> GetMovie(int movieId); + Task> GetOrAdd(int mediaSourceId, string path); + Task Update(MovieMediaItem movie); + Task GetMovieCount(); + Task> GetPagedMovies(int pageNumber, int pageSize); + } +} diff --git a/ErsatzTV.Core/Interfaces/Repositories/IPlayoutRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IPlayoutRepository.cs index 357fb208..354b1733 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IPlayoutRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IPlayoutRepository.cs @@ -8,14 +8,14 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface IPlayoutRepository { - public Task Add(Playout playout); - public Task> Get(int id); - public Task> GetFull(int id); - public Task> GetPlayoutItem(int channelId, DateTimeOffset now); - public Task> GetPlayoutItems(int playoutId); - public Task> GetPlayoutIdsForMediaItems(Seq mediaItems); - public Task> GetAll(); - public Task Update(Playout playout); - public Task Delete(int playoutId); + Task Add(Playout playout); + Task> Get(int id); + Task> GetFull(int id); + Task> GetPlayoutItem(int channelId, DateTimeOffset now); + Task> GetPlayoutItems(int playoutId); + Task> GetPlayoutIdsForMediaItems(Seq mediaItems); + Task> GetAll(); + Task Update(Playout playout); + Task Delete(int playoutId); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IProgramScheduleRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IProgramScheduleRepository.cs index 098538fc..bb2bf429 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IProgramScheduleRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IProgramScheduleRepository.cs @@ -7,12 +7,12 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface IProgramScheduleRepository { - public Task Add(ProgramSchedule programSchedule); - public Task> Get(int id); - public Task> GetWithPlayouts(int id); - public Task> GetAll(); - public Task Update(ProgramSchedule programSchedule); - public Task Delete(int programScheduleId); - public Task>> GetItems(int programScheduleId); + Task Add(ProgramSchedule programSchedule); + Task> Get(int id); + Task> GetWithPlayouts(int id); + Task> GetAll(); + Task Update(ProgramSchedule programSchedule); + Task Delete(int programScheduleId); + Task>> GetItems(int programScheduleId); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IResolutionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IResolutionRepository.cs index c9f2fd6c..fc0abc19 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IResolutionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IResolutionRepository.cs @@ -7,7 +7,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface IResolutionRepository { - public Task> Get(int id); - public Task> GetAll(); + Task> Get(int id); + Task> GetAll(); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs new file mode 100644 index 00000000..88d7ebca --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using LanguageExt; + +namespace ErsatzTV.Core.Interfaces.Repositories +{ + public interface ITelevisionRepository + { + 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); + Task> GetShowByPath(int mediaSourceId, string path); + Task> GetShowByMetadata(TelevisionShowMetadata metadata); + + Task> AddShow( + int localMediaSourceId, + string showFolder, + TelevisionShowMetadata metadata); + + Task> GetOrAddSeason( + TelevisionShow show, + string path, + int seasonNumber); + + Task> GetOrAddEpisode( + TelevisionSeason season, + int mediaSourceId, + string path); + + Task DeleteMissingSources(int localMediaSourceId, List allFolders); + Task DeleteEmptyShows(); + } +} diff --git a/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs b/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs index 684badcf..7d36941a 100644 --- a/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs @@ -6,7 +6,7 @@ namespace ErsatzTV.Core.Interfaces.Scheduling public interface IMediaCollectionEnumerator { MediaCollectionEnumeratorState State { get; } - public Option Current { get; } - public void MoveNext(); + Option Current { get; } + void MoveNext(); } } diff --git a/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs b/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs index 6492dc83..6e8224bb 100644 --- a/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs +++ b/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs @@ -6,9 +6,9 @@ namespace ErsatzTV.Core.Interfaces.Scheduling { public interface IPlayoutBuilder { - public Task BuildPlayoutItems(Playout playout, bool rebuild = false); + Task BuildPlayoutItems(Playout playout, bool rebuild = false); - public Task BuildPlayoutItems( + Task BuildPlayoutItems( Playout playout, DateTimeOffset playoutStart, DateTimeOffset playoutFinish, diff --git a/ErsatzTV.Core/Iptv/ChannelGuide.cs b/ErsatzTV.Core/Iptv/ChannelGuide.cs index f4f71b88..55d37171 100644 --- a/ErsatzTV.Core/Iptv/ChannelGuide.cs +++ b/ErsatzTV.Core/Iptv/ChannelGuide.cs @@ -57,11 +57,26 @@ namespace ErsatzTV.Core.Iptv { string start = playoutItem.Start.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty); string stop = playoutItem.Finish.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty); - MediaMetadata metadata = Optional(playoutItem.MediaItem.Metadata).IfNone( - new MediaMetadata - { - Title = Path.GetFileName(playoutItem.MediaItem.Path) - }); + + string title = playoutItem.MediaItem switch + { + MovieMediaItem m => m.Metadata?.Title ?? m.Path, + TelevisionEpisodeMediaItem e => e.Metadata?.Title ?? e.Path, + _ => "[unknown]" + }; + + string description = playoutItem.MediaItem switch + { + MovieMediaItem m => m.Metadata?.Plot, + TelevisionEpisodeMediaItem e => e.Metadata?.Plot, + _ => string.Empty + }; + + string contentRating = playoutItem.MediaItem switch + { + MovieMediaItem m => m.Metadata?.ContentRating, + _ => string.Empty + }; xml.WriteStartElement("programme"); xml.WriteAttributeString("start", start); @@ -70,7 +85,7 @@ namespace ErsatzTV.Core.Iptv xml.WriteStartElement("title"); xml.WriteAttributeString("lang", "en"); - xml.WriteString(metadata.Title); + xml.WriteString(title); xml.WriteEndElement(); // title xml.WriteStartElement("previously-shown"); @@ -80,28 +95,35 @@ namespace ErsatzTV.Core.Iptv xml.WriteAttributeString("lang", "en"); xml.WriteEndElement(); // sub-title - int season = Optional(metadata.SeasonNumber).IfNone(0); - int episode = Optional(metadata.EpisodeNumber).IfNone(0); - if (season > 0 && episode > 0) + if (playoutItem.MediaItem is TelevisionEpisodeMediaItem episode) { - xml.WriteStartElement("episode-num"); - xml.WriteAttributeString("system", "xmltv_ns"); - xml.WriteString($"{season - 1}.{episode - 1}.0/1"); - xml.WriteEndElement(); // episode-num + int s = Optional(episode.Metadata?.Season).IfNone(0); + int e = Optional(episode.Metadata?.Episode).IfNone(0); + if (s > 0 && e > 0) + { + xml.WriteStartElement("episode-num"); + xml.WriteAttributeString("system", "xmltv_ns"); + xml.WriteString($"{s - 1}.{e - 1}.0/1"); + xml.WriteEndElement(); // episode-num + } } // sb.AppendLine(""); - xml.WriteStartElement("desc"); - xml.WriteAttributeString("lang", "en"); - xml.WriteString(metadata.Description); - xml.WriteEndElement(); // desc - if (!string.IsNullOrWhiteSpace(metadata.ContentRating)) + if (!string.IsNullOrWhiteSpace(description)) + { + xml.WriteStartElement("desc"); + xml.WriteAttributeString("lang", "en"); + xml.WriteString(description); + xml.WriteEndElement(); // desc + } + + if (!string.IsNullOrWhiteSpace(contentRating)) { xml.WriteStartElement("rating"); xml.WriteAttributeString("system", "MPAA"); xml.WriteStartElement("value"); - xml.WriteString(metadata.ContentRating); + xml.WriteString(contentRating); xml.WriteEndElement(); // value xml.WriteEndElement(); // rating } diff --git a/ErsatzTV.Core/LanguageExtensions.cs b/ErsatzTV.Core/LanguageExtensions.cs index 537c1756..b181bc05 100644 --- a/ErsatzTV.Core/LanguageExtensions.cs +++ b/ErsatzTV.Core/LanguageExtensions.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using LanguageExt; +using Microsoft.Extensions.Logging; namespace ErsatzTV.Core { @@ -12,5 +13,24 @@ namespace ErsatzTV.Core validation.ToEither() .MapLeft(errors => errors.Join()) .MapAsync, TR>(e => e); + + public static Task LogFailure( + this TryAsync tryAsync, + T defaultValue, + ILogger logger, + string message, + params object[] args) => + tryAsync.IfFail( + ex => + { + logger.LogError(ex, message, args); + return defaultValue; + }); + + public static Task LogFailure( + this TryAsync tryAsync, + ILogger logger, + string message, + params object[] args) => LogFailure(tryAsync, Unit.Default, logger, message, args); } } diff --git a/ErsatzTV.Core/Metadata/ActionPlan.cs b/ErsatzTV.Core/Metadata/ActionPlan.cs deleted file mode 100644 index b10ab95f..00000000 --- a/ErsatzTV.Core/Metadata/ActionPlan.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace ErsatzTV.Core.Metadata -{ - public record ActionPlan(string TargetPath, ScanningAction TargetAction); -} diff --git a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs index 5320ad3b..726708c4 100644 --- a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs @@ -2,35 +2,58 @@ using System.IO; using System.Text.RegularExpressions; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Metadata; namespace ErsatzTV.Core.Metadata { - public static class FallbackMetadataProvider + public class FallbackMetadataProvider : IFallbackMetadataProvider { - public static MediaMetadata GetFallbackMetadata(MediaItem mediaItem) + public TelevisionShowMetadata GetFallbackMetadataForShow(string showFolder) + { + string fileName = Path.GetFileName(showFolder); + var metadata = new TelevisionShowMetadata + { Source = MetadataSource.Fallback, Title = fileName ?? showFolder }; + return GetTelevisionShowMetadata(fileName, metadata); + } + + public static TelevisionEpisodeMetadata GetFallbackMetadata(TelevisionEpisodeMediaItem mediaItem) { string fileName = Path.GetFileName(mediaItem.Path); - var metadata = new MediaMetadata { Source = MetadataSource.Fallback, Title = fileName ?? mediaItem.Path }; + var metadata = new TelevisionEpisodeMetadata + { Source = MetadataSource.Fallback, Title = fileName ?? mediaItem.Path }; if (fileName != null) { - if (!(mediaItem.Source is LocalMediaSource localMediaSource)) + if (!(mediaItem.Source is LocalMediaSource)) { return metadata; } - return localMediaSource.MediaType switch + return GetEpisodeMetadata(fileName, metadata); + } + + return metadata; + } + + public static MovieMetadata GetFallbackMetadata(MovieMediaItem mediaItem) + { + string fileName = Path.GetFileName(mediaItem.Path); + var metadata = new MovieMetadata { Source = MetadataSource.Fallback, Title = fileName ?? mediaItem.Path }; + + if (fileName != null) + { + if (!(mediaItem.Source is LocalMediaSource)) { - MediaType.TvShow => GetTvShowMetadata(fileName, metadata), - MediaType.Movie => GetMovieMetadata(fileName, metadata), - _ => metadata - }; + return metadata; + } + + return GetMovieMetadata(fileName, metadata); } return metadata; } - private static MediaMetadata GetTvShowMetadata(string fileName, MediaMetadata metadata) + private static TelevisionEpisodeMetadata GetEpisodeMetadata(string fileName, TelevisionEpisodeMetadata metadata) { try { @@ -38,10 +61,9 @@ namespace ErsatzTV.Core.Metadata Match match = Regex.Match(fileName, PATTERN); if (match.Success) { - metadata.MediaType = MediaType.TvShow; metadata.Title = match.Groups[1].Value; - metadata.SeasonNumber = int.Parse(match.Groups[2].Value); - metadata.EpisodeNumber = int.Parse(match.Groups[3].Value); + metadata.Season = int.Parse(match.Groups[2].Value); + metadata.Episode = int.Parse(match.Groups[3].Value); } } catch (Exception) @@ -52,7 +74,7 @@ namespace ErsatzTV.Core.Metadata return metadata; } - private static MediaMetadata GetMovieMetadata(string fileName, MediaMetadata metadata) + private static MovieMetadata GetMovieMetadata(string fileName, MovieMetadata metadata) { try { @@ -60,9 +82,30 @@ namespace ErsatzTV.Core.Metadata Match match = Regex.Match(fileName, PATTERN); if (match.Success) { - metadata.MediaType = MediaType.Movie; metadata.Title = match.Groups[1].Value; - metadata.Aired = new DateTime(int.Parse(match.Groups[2].Value), 1, 1); + metadata.Year = int.Parse(match.Groups[2].Value); + } + } + catch (Exception) + { + // ignored + } + + return metadata; + } + + private static TelevisionShowMetadata GetTelevisionShowMetadata( + string fileName, + TelevisionShowMetadata metadata) + { + try + { + const string PATTERN = @"^(.*?)[\s.]+?[.\(](\d{4})[.\)].*$"; + Match match = Regex.Match(fileName, PATTERN); + if (match.Success) + { + metadata.Title = match.Groups[1].Value; + metadata.Year = int.Parse(match.Groups[2].Value); } } catch (Exception) diff --git a/ErsatzTV.Core/Metadata/LocalFileSystem.cs b/ErsatzTV.Core/Metadata/LocalFileSystem.cs index bb353a3f..3042e8d1 100644 --- a/ErsatzTV.Core/Metadata/LocalFileSystem.cs +++ b/ErsatzTV.Core/Metadata/LocalFileSystem.cs @@ -1,9 +1,9 @@ using System; +using System.Collections.Generic; using System.IO; -using System.Linq; +using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; -using LanguageExt; using static LanguageExt.Prelude; namespace ErsatzTV.Core.Metadata @@ -16,52 +16,13 @@ namespace ErsatzTV.Core.Metadata public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource) => Directory.Exists(localMediaSource.Folder); - public Seq FindRelevantVideos(LocalMediaSource localMediaSource) - { - Seq allDirectories = Directory - .GetDirectories(localMediaSource.Folder, "*", SearchOption.AllDirectories) - .ToSeq() - .Add(localMediaSource.Folder); + public IEnumerable ListSubdirectories(string folder) => + Try(Directory.EnumerateDirectories(folder)).IfFail(new List()); - // remove any directories with an .etvignore file locally, or in any parent directory - Seq excluded = allDirectories.Filter(ShouldExcludeDirectory); - Seq relevantDirectories = allDirectories - .Filter(d => !excluded.Any(d.StartsWith)) - .Filter(d => localMediaSource.MediaType == MediaType.Other || !IsExtrasFolder(d)); + public IEnumerable ListFiles(string folder) => + Try(Directory.EnumerateFiles(folder, "*", SearchOption.TopDirectoryOnly)).IfFail(new List()); - return relevantDirectories - .Collect(d => Directory.GetFiles(d, "*", SearchOption.TopDirectoryOnly)) - .Filter(file => KnownExtensions.Contains(Path.GetExtension(file))) - .OrderBy(identity) - .ToSeq(); - } - - public bool ShouldRefreshMetadata(LocalMediaSource localMediaSource, MediaItem mediaItem) - { - DateTime lastWrite = File.GetLastWriteTimeUtc(mediaItem.Path); - bool modified = lastWrite > mediaItem.LastWriteTime.IfNone(DateTime.MinValue); - return modified // media item has been modified - || mediaItem.Metadata == null // media item has no metadata - || mediaItem.Metadata.MediaType != localMediaSource.MediaType; // media item is typed incorrectly - } - - public bool ShouldRefreshPoster(MediaItem mediaItem) => - string.IsNullOrWhiteSpace(mediaItem.Poster); - - private static bool ShouldExcludeDirectory(string path) => File.Exists(Path.Combine(path, ".etvignore")); - - // see https://support.emby.media/support/solutions/articles/44001159102-movie-naming - private static bool IsExtrasFolder(string path) => - ExtraFolderNames.Contains(Path.GetFileName(path)?.ToLowerInvariant()); - - // @formatter:off - private static readonly Seq KnownExtensions = Seq( - ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4", - ".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts"); - - private static readonly Seq ExtraFolderNames = Seq( - "extras", "specials", "shorts", "scenes", "featurettes", - "behind the scenes", "deleted scenes", "interviews", "trailers"); - // @formatter:on + public bool FileExists(string path) => File.Exists(path); + public Task ReadAllBytes(string path) => File.ReadAllBytesAsync(path); } } diff --git a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs new file mode 100644 index 00000000..323aafb0 --- /dev/null +++ b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Domain; +using ErsatzTV.Core.Interfaces.Images; +using ErsatzTV.Core.Interfaces.Metadata; +using LanguageExt; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Core.Metadata +{ + public abstract class LocalFolderScanner + { + public static readonly List VideoFileExtensions = new() + { + ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4", + ".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts" + }; + + public static readonly List ImageFileExtensions = new() + { + "jpg", "jpeg", "png", "gif", "tbn" + }; + + public static readonly List ExtraFiles = new() + { + "behindthescenes", "deleted", "featurette", + "interview", "scene", "short", "trailer", "other" + }; + + public static readonly List ExtraDirectories = new List + { + "behind the scenes", "deleted scenes", "featurettes", + "interviews", "scenes", "shorts", "trailers", "other", + "extras", "specials" + } + .Map(s => $"{Path.DirectorySeparatorChar}{s}{Path.DirectorySeparatorChar}") + .ToList(); + + private readonly IImageCache _imageCache; + + private readonly ILocalFileSystem _localFileSystem; + private readonly ILocalStatisticsProvider _localStatisticsProvider; + private readonly ILogger _logger; + + protected LocalFolderScanner( + ILocalFileSystem localFileSystem, + ILocalStatisticsProvider localStatisticsProvider, + IImageCache imageCache, + ILogger logger) + { + _localFileSystem = localFileSystem; + _localStatisticsProvider = localStatisticsProvider; + _imageCache = imageCache; + _logger = logger; + } + + protected async Task> UpdateStatistics(T mediaItem, string ffprobePath) + where T : MediaItem + { + try + { + if (mediaItem.Statistics is null || + (mediaItem.Statistics.LastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(mediaItem.Path)) + { + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", mediaItem.Path); + await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem); + } + + return mediaItem; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + protected async Task SavePosterToDisk( + T show, + string posterPath, + Func> update, + int height = 220) where T : IHasAPoster + { + byte[] originalBytes = await _localFileSystem.ReadAllBytes(posterPath); + Either maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, height, null); + await maybeHash.Match( + hash => + { + show.Poster = hash; + show.PosterLastWriteTime = _localFileSystem.GetLastWriteTime(posterPath); + return update(show); + }, + error => + { + _logger.LogWarning("Unable to save poster to disk from {Path}: {Error}", posterPath, error.Value); + return Task.CompletedTask; + }); + } + + protected Task> SavePosterToDisk(string posterPath, int height = 220) => + _localFileSystem.ReadAllBytes(posterPath) + .Bind(bytes => _imageCache.ResizeAndSaveImage(bytes, height, null)); + } +} diff --git a/ErsatzTV.Core/Metadata/LocalMediaScanner.cs b/ErsatzTV.Core/Metadata/LocalMediaScanner.cs deleted file mode 100644 index b07a042f..00000000 --- a/ErsatzTV.Core/Metadata/LocalMediaScanner.cs +++ /dev/null @@ -1,289 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Images; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories; -using ErsatzTV.Core.Interfaces.Scheduling; -using LanguageExt; -using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; -using Seq = LanguageExt.Seq; - -namespace ErsatzTV.Core.Metadata -{ - public class LocalMediaScanner : ILocalMediaScanner - { - private readonly IImageCache _imageCache; - private readonly ILocalFileSystem _localFileSystem; - private readonly ILocalMediaSourcePlanner _localMediaSourcePlanner; - private readonly ILocalMetadataProvider _localMetadataProvider; - private readonly ILocalStatisticsProvider _localStatisticsProvider; - private readonly ILogger _logger; - private readonly IMediaItemRepository _mediaItemRepository; - private readonly IPlayoutBuilder _playoutBuilder; - private readonly IPlayoutRepository _playoutRepository; - private readonly ISmartCollectionBuilder _smartCollectionBuilder; - - public LocalMediaScanner( - IMediaItemRepository mediaItemRepository, - IPlayoutRepository playoutRepository, - ILocalStatisticsProvider localStatisticsProvider, - ILocalMetadataProvider localMetadataProvider, - ISmartCollectionBuilder smartCollectionBuilder, - IPlayoutBuilder playoutBuilder, - ILocalMediaSourcePlanner localMediaSourcePlanner, - ILocalFileSystem localFileSystem, - IImageCache imageCache, - ILogger logger) - { - _mediaItemRepository = mediaItemRepository; - _playoutRepository = playoutRepository; - _localStatisticsProvider = localStatisticsProvider; - _localMetadataProvider = localMetadataProvider; - _smartCollectionBuilder = smartCollectionBuilder; - _playoutBuilder = playoutBuilder; - _localMediaSourcePlanner = localMediaSourcePlanner; - _localFileSystem = localFileSystem; - _imageCache = imageCache; - _logger = logger; - } - - public async Task ScanLocalMediaSource( - LocalMediaSource localMediaSource, - string ffprobePath, - ScanningMode scanningMode) - { - if (!_localFileSystem.IsMediaSourceAccessible(localMediaSource)) - { - _logger.LogWarning( - "Media source folder {Folder} does not exist or is inaccessible; skipping scan", - localMediaSource.Folder); - return unit; - } - - List knownMediaItems = await _mediaItemRepository.GetAllByMediaSourceId(localMediaSource.Id); - var modifiedPlayoutIds = new List(); - - Seq actions = _localMediaSourcePlanner.DetermineActions( - localMediaSource.MediaType, - knownMediaItems.ToSeq(), - FindAllFiles(localMediaSource)); - - foreach (LocalMediaSourcePlan action in actions) - { - Option maybeAddPlan = - action.ActionPlans.SingleOrDefault(plan => plan.TargetAction == ScanningAction.Add); - await maybeAddPlan.IfSomeAsync( - async plan => - { - Option maybeMediaItem = await AddMediaItem(localMediaSource, plan.TargetPath); - - // any actions other than "add" need to operate on a media item - maybeMediaItem.IfSome(mediaItem => action.Source = mediaItem); - }); - - foreach (ActionPlan plan in action.ActionPlans.OrderBy(plan => (int) plan.TargetAction)) - { - string sourcePath = action.Source.Match( - mediaItem => mediaItem.Path, - path => path); - - _logger.LogDebug( - "{Source}: {Action} with {File}", - Path.GetFileName(sourcePath), - plan.TargetAction, - Path.GetRelativePath(Path.GetDirectoryName(sourcePath) ?? string.Empty, plan.TargetPath)); - - await action.Source.Match( - async mediaItem => - { - var changed = false; - - switch (plan.TargetAction) - { - case ScanningAction.Remove: - await RemoveMissingItem(mediaItem); - break; - case ScanningAction.Poster: - await SavePosterForItem(mediaItem, plan.TargetPath); - break; - case ScanningAction.FallbackMetadata: - await RefreshFallbackMetadataForItem(mediaItem); - break; - case ScanningAction.SidecarMetadata: - await RefreshSidecarMetadataForItem(mediaItem, plan.TargetPath); - break; - case ScanningAction.Statistics: - changed = await RefreshStatisticsForItem(mediaItem, ffprobePath); - break; - case ScanningAction.Collections: - changed = await RefreshCollectionsForItem(mediaItem); - break; - } - - if (changed) - { - List ids = - await _playoutRepository.GetPlayoutIdsForMediaItems(Seq.create(mediaItem)); - modifiedPlayoutIds.AddRange(ids); - } - }, - path => - { - _logger.LogError("This is a bug, something went wrong processing {Path}", path); - return Task.CompletedTask; - }); - } - } - - foreach (int playoutId in modifiedPlayoutIds.Distinct()) - { - Option maybePlayout = await _playoutRepository.GetFull(playoutId); - await maybePlayout.Match( - async playout => - { - Playout result = await _playoutBuilder.BuildPlayoutItems(playout, true); - await _playoutRepository.Update(result); - }, - Task.CompletedTask); - } - - return unit; - } - - private Seq FindAllFiles(LocalMediaSource localMediaSource) - { - Seq allDirectories = Directory - .GetDirectories(localMediaSource.Folder, "*", SearchOption.AllDirectories) - .ToSeq() - .Add(localMediaSource.Folder); - - // remove any directories with an .etvignore file locally, or in any parent directory - Seq excluded = allDirectories.Filter(path => File.Exists(Path.Combine(path, ".etvignore"))); - Seq relevantDirectories = allDirectories - .Filter(d => !excluded.Any(d.StartsWith)); - // .Filter(d => localMediaSource.MediaType == MediaType.Other || !IsExtrasFolder(d)); - - return relevantDirectories - .Collect(d => Directory.GetFiles(d, "*", SearchOption.TopDirectoryOnly)) - .OrderBy(identity) - .ToSeq(); - } - - private async Task> AddMediaItem(MediaSource mediaSource, string path) - { - try - { - var mediaItem = new MediaItem - { - MediaSourceId = mediaSource.Id, - Path = path, - LastWriteTime = File.GetLastWriteTimeUtc(path) - }; - - await _mediaItemRepository.Add(mediaItem); - - return mediaItem; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to add media item for {Path}", path); - return None; - } - } - - private async Task RemoveMissingItem(MediaItem mediaItem) - { - try - { - await _mediaItemRepository.Delete(mediaItem.Id); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to remove missing local media item {MediaItem}", mediaItem.Path); - } - } - - private async Task SavePosterForItem(MediaItem mediaItem, string posterPath) - { - try - { - byte[] originalBytes = await File.ReadAllBytesAsync(posterPath); - Either maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, 220, null); - await maybeHash.Match( - hash => - { - mediaItem.Poster = hash; - mediaItem.PosterLastWriteTime = File.GetLastWriteTimeUtc(posterPath); - return _mediaItemRepository.Update(mediaItem); - }, - error => - { - _logger.LogWarning( - "Unable to save poster to disk from {Path}: {Error}", - posterPath, - error.Value); - return Task.CompletedTask; - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to refresh poster for media item {MediaItem}", mediaItem.Path); - } - } - - private async Task RefreshStatisticsForItem(MediaItem mediaItem, string ffprobePath) - { - try - { - return await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to refresh statistics for media item {MediaItem}", mediaItem.Path); - return false; - } - } - - private async Task RefreshCollectionsForItem(MediaItem mediaItem) - { - try - { - return await _smartCollectionBuilder.RefreshSmartCollections(mediaItem); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to refresh collections for media item {MediaItem}", mediaItem.Path); - return false; - } - } - - private async Task RefreshSidecarMetadataForItem(MediaItem mediaItem, string path) - { - try - { - await _localMetadataProvider.RefreshSidecarMetadata(mediaItem, path); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to refresh nfo metadata for media item {MediaItem}", mediaItem.Path); - } - } - - private async Task RefreshFallbackMetadataForItem(MediaItem mediaItem) - { - try - { - await _localMetadataProvider.RefreshFallbackMetadata(mediaItem); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to refresh fallback metadata for media item {MediaItem}", mediaItem.Path); - } - } - } -} diff --git a/ErsatzTV.Core/Metadata/LocalMediaSourcePlan.cs b/ErsatzTV.Core/Metadata/LocalMediaSourcePlan.cs deleted file mode 100644 index 2ede9678..00000000 --- a/ErsatzTV.Core/Metadata/LocalMediaSourcePlan.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Domain; -using LanguageExt; - -namespace ErsatzTV.Core.Metadata -{ - public record LocalMediaSourcePlan(Either Source, List ActionPlans) - { - public Either Source { get; set; } = Source; - } -} diff --git a/ErsatzTV.Core/Metadata/LocalMediaSourcePlanner.cs b/ErsatzTV.Core/Metadata/LocalMediaSourcePlanner.cs deleted file mode 100644 index 91cfffd1..00000000 --- a/ErsatzTV.Core/Metadata/LocalMediaSourcePlanner.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Metadata; -using LanguageExt; -using static LanguageExt.Prelude; - -namespace ErsatzTV.Core.Metadata -{ - // TODO: this needs a better name - public class LocalMediaSourcePlanner : ILocalMediaSourcePlanner - { - private static readonly Seq ImageFileExtensions = Seq("jpg", "jpeg", "png", "gif", "tbn"); - private readonly ILocalFileSystem _localFileSystem; - - public LocalMediaSourcePlanner(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem; - - public Seq DetermineActions( - MediaType mediaType, - Seq mediaItems, - Seq files) - { - var results = new IntermediateResults(); - Seq videoFiles = files.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) - .Filter(f => !IsExtra(f)); - - (Seq newFiles, Seq existingMediaItems) = videoFiles.Map( - s => mediaItems.Find(i => i.Path == s).ToEither(s)) - .Partition(); - - // new files - foreach (string file in newFiles) - { - results.Add(file, new ActionPlan(file, ScanningAction.Add)); - results.Add(file, new ActionPlan(file, ScanningAction.Statistics)); - - Option maybeNfoFile = LocateNfoFile(mediaType, files, file); - maybeNfoFile.BiIter( - nfoFile => - { - results.Add(file, new ActionPlan(nfoFile, ScanningAction.SidecarMetadata)); - results.Add(file, new ActionPlan(nfoFile, ScanningAction.Collections)); - }, - () => - { - results.Add(file, new ActionPlan(file, ScanningAction.FallbackMetadata)); - results.Add(file, new ActionPlan(file, ScanningAction.Collections)); - }); - - Option maybePoster = LocatePoster(mediaType, files, file); - maybePoster.IfSome( - posterFile => results.Add(file, new ActionPlan(posterFile, ScanningAction.Poster))); - } - - // existing media items - foreach (MediaItem mediaItem in existingMediaItems) - { - if ((mediaItem.LastWriteTime ?? DateTime.MinValue) < _localFileSystem.GetLastWriteTime(mediaItem.Path)) - { - results.Add(mediaItem, new ActionPlan(mediaItem.Path, ScanningAction.Statistics)); - } - - Option maybeNfoFile = LocateNfoFile(mediaType, files, mediaItem.Path); - maybeNfoFile.IfSome( - nfoFile => - { - if (mediaItem.Metadata == null || mediaItem.Metadata.Source == MetadataSource.Fallback || - (mediaItem.Metadata.LastWriteTime ?? DateTime.MinValue) < - _localFileSystem.GetLastWriteTime(nfoFile)) - { - results.Add(mediaItem, new ActionPlan(nfoFile, ScanningAction.SidecarMetadata)); - results.Add(mediaItem, new ActionPlan(nfoFile, ScanningAction.Collections)); - } - }); - - Option maybePoster = LocatePoster(mediaType, files, mediaItem.Path); - maybePoster.IfSome( - posterFile => - { - if (string.IsNullOrWhiteSpace(mediaItem.Poster) || - (mediaItem.PosterLastWriteTime ?? DateTime.MinValue) < - _localFileSystem.GetLastWriteTime(posterFile)) - { - results.Add(mediaItem, new ActionPlan(posterFile, ScanningAction.Poster)); - } - }); - } - - // missing media items - foreach (MediaItem mediaItem in mediaItems.Where(i => !files.Contains(i.Path))) - { - results.Add(mediaItem, new ActionPlan(mediaItem.Path, ScanningAction.Remove)); - } - - return results.Summarize(); - } - - private static bool IsExtra(string path) - { - string folder = Path.GetFileName(Path.GetDirectoryName(path) ?? string.Empty); - string file = Path.GetFileNameWithoutExtension(path); - return ExtraDirectories.Contains(folder, StringComparer.OrdinalIgnoreCase) - || ExtraFiles.Any(f => file.EndsWith(f, StringComparison.OrdinalIgnoreCase)); - } - - private static Option LocateNfoFile(MediaType mediaType, Seq files, string file) - { - switch (mediaType) - { - case MediaType.Movie: - string movieAsNfo = Path.ChangeExtension(file, "nfo"); - string movieNfo = Path.Combine(Path.GetDirectoryName(file) ?? string.Empty, "movie.nfo"); - return Seq(movieAsNfo, movieNfo) - .Filter(s => files.Contains(s)) - .HeadOrNone(); - case MediaType.TvShow: - string episodeAsNfo = Path.ChangeExtension(file, "nfo"); - return Optional(episodeAsNfo) - .Filter(s => files.Contains(s)) - .HeadOrNone(); - } - - return None; - } - - private static Option LocatePoster(MediaType mediaType, Seq files, string file) - { - string folder = Path.GetDirectoryName(file) ?? string.Empty; - - switch (mediaType) - { - case MediaType.Movie: - IEnumerable possibleMoviePosters = ImageFileExtensions.Collect( - ext => new[] { $"poster.{ext}", Path.GetFileNameWithoutExtension(file) + $"-poster.{ext}" }) - .Map(f => Path.Combine(folder, f)); - return possibleMoviePosters.Filter(p => files.Contains(p)).HeadOrNone(); - case MediaType.TvShow: - string parentFolder = Directory.GetParent(folder)?.FullName ?? string.Empty; - IEnumerable possibleTvPosters = ImageFileExtensions - .Collect(ext => new[] { $"poster.{ext}" }) - .Map(f => Path.Combine(parentFolder, f)); - return possibleTvPosters.Filter(p => files.Contains(p)).HeadOrNone(); - } - - return None; - } - - private class IntermediateResults - { - private readonly List, ActionPlan>> _rawResults = new(); - - public void Add(Either source, ActionPlan plan) => - _rawResults.Add(Tuple(source, plan)); - - public Seq Summarize() => - _rawResults - .GroupBy(t => t.Item1) - .Select(g => new LocalMediaSourcePlan(g.Key, g.Select(g2 => g2.Item2).ToList())) - .ToSeq(); - } - - // @formatter:off - private static readonly Seq VideoFileExtensions = Seq( - ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4", - ".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts"); - - private static readonly Seq ExtraDirectories = Seq( - "behind the scenes", "deleted scenes", "featurettes", - "interviews", "scenes", "shorts", "trailers", "other", - "extras", "specials"); - - private static readonly Seq ExtraFiles = Seq( - "behindthescenes", "deleted", "featurette", - "interview", "scene", "short", "trailer", "other"); - // @formatter:on - } -} diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index 8d03cca2..8a291d2a 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -14,64 +14,136 @@ namespace ErsatzTV.Core.Metadata public class LocalMetadataProvider : ILocalMetadataProvider { private static readonly XmlSerializer MovieSerializer = new(typeof(MovieNfo)); - private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowEpisodeNfo)); + private static readonly XmlSerializer EpisodeSerializer = new(typeof(TvShowEpisodeNfo)); + private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowNfo)); + private readonly IFallbackMetadataProvider _fallbackMetadataProvider; + private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; + private readonly ITelevisionRepository _televisionRepository; - public LocalMetadataProvider(IMediaItemRepository mediaItemRepository, ILogger logger) + public LocalMetadataProvider( + IMediaItemRepository mediaItemRepository, + ITelevisionRepository televisionRepository, + IFallbackMetadataProvider fallbackMetadataProvider, + ILocalFileSystem localFileSystem, + ILogger logger) { _mediaItemRepository = mediaItemRepository; + _televisionRepository = televisionRepository; + _fallbackMetadataProvider = fallbackMetadataProvider; + _localFileSystem = localFileSystem; _logger = logger; } - public async Task RefreshSidecarMetadata(MediaItem mediaItem, string path) + public Task GetMetadataForShow(string showFolder) { - Option maybeMetadata = await LoadMetadata(mediaItem, path); - await maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(mediaItem, metadata)); + string nfoFileName = Path.Combine(showFolder, "tvshow.nfo"); + return Optional(_localFileSystem.FileExists(nfoFileName)) + .Filter(identity).AsTask() + .Bind(_ => LoadTelevisionShowMetadata(nfoFileName)) + .IfNoneAsync(() => _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder).AsTask()) + .Map( + m => + { + m.SortTitle = GetSortTitle(m.Title); + return m; + }); } - public Task RefreshFallbackMetadata(MediaItem mediaItem) => - ApplyMetadataUpdate(mediaItem, FallbackMetadataProvider.GetFallbackMetadata(mediaItem)); + public Task RefreshSidecarMetadata(MediaItem mediaItem, string path) => + mediaItem switch + { + TelevisionEpisodeMediaItem e => LoadMetadata(e, path) + .Bind(maybeMetadata => maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(e, metadata))), + MovieMediaItem m => LoadMetadata(m, path) + .Bind(maybeMetadata => maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(m, metadata))), + _ => Task.FromResult(Unit.Default) + }; + + public Task RefreshSidecarMetadata(TelevisionShow televisionShow, string showFolder) => + LoadMetadata(televisionShow, showFolder).Bind( + maybeMetadata => maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(televisionShow, metadata))); - private async Task ApplyMetadataUpdate(MediaItem mediaItem, MediaMetadata metadata) - { - if (mediaItem.Metadata == null) + public Task RefreshFallbackMetadata(MediaItem mediaItem) => + mediaItem switch { - mediaItem.Metadata = new MediaMetadata(); - } + TelevisionEpisodeMediaItem e => ApplyMetadataUpdate(e, FallbackMetadataProvider.GetFallbackMetadata(e)) + .ToUnit(), + MovieMediaItem m => ApplyMetadataUpdate(m, FallbackMetadataProvider.GetFallbackMetadata(m)).ToUnit(), + _ => Task.FromResult(Unit.Default) + }; + + public Task RefreshFallbackMetadata(TelevisionShow televisionShow, string showFolder) => + ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder)) + .ToUnit(); + private async Task ApplyMetadataUpdate(TelevisionEpisodeMediaItem mediaItem, TelevisionEpisodeMetadata metadata) + { + mediaItem.Metadata ??= new TelevisionEpisodeMetadata { TelevisionEpisodeId = mediaItem.Id }; mediaItem.Metadata.Source = metadata.Source; mediaItem.Metadata.LastWriteTime = metadata.LastWriteTime; - mediaItem.Metadata.MediaType = metadata.MediaType; mediaItem.Metadata.Title = metadata.Title; - mediaItem.Metadata.Subtitle = metadata.Subtitle; - mediaItem.Metadata.SortTitle = GetSortTitle(metadata.Title ?? string.Empty); - mediaItem.Metadata.Description = metadata.Description; - mediaItem.Metadata.EpisodeNumber = metadata.EpisodeNumber; - mediaItem.Metadata.SeasonNumber = metadata.SeasonNumber; + mediaItem.Metadata.SortTitle = GetSortTitle(metadata.Title); + mediaItem.Metadata.Season = metadata.Season; + mediaItem.Metadata.Episode = metadata.Episode; + mediaItem.Metadata.Plot = metadata.Plot; mediaItem.Metadata.Aired = metadata.Aired; + + await _televisionRepository.Update(mediaItem); + } + + private async Task ApplyMetadataUpdate(MovieMediaItem mediaItem, MovieMetadata metadata) + { + mediaItem.Metadata ??= new MovieMetadata(); + mediaItem.Metadata.Source = metadata.Source; + mediaItem.Metadata.LastWriteTime = metadata.LastWriteTime; + mediaItem.Metadata.Title = metadata.Title; + mediaItem.Metadata.SortTitle = GetSortTitle(metadata.Title); + mediaItem.Metadata.Year = metadata.Year; + mediaItem.Metadata.Premiered = metadata.Premiered; + mediaItem.Metadata.Plot = metadata.Plot; + mediaItem.Metadata.Outline = metadata.Outline; + mediaItem.Metadata.Tagline = metadata.Tagline; mediaItem.Metadata.ContentRating = metadata.ContentRating; await _mediaItemRepository.Update(mediaItem); } - private static string GetSortTitle(string title) + private async Task ApplyMetadataUpdate(TelevisionShow televisionShow, TelevisionShowMetadata metadata) { - if (title.StartsWith("the ", StringComparison.OrdinalIgnoreCase)) + televisionShow.Metadata ??= new TelevisionShowMetadata(); + televisionShow.Metadata.Source = metadata.Source; + televisionShow.Metadata.LastWriteTime = metadata.LastWriteTime; + televisionShow.Metadata.Title = metadata.Title; + televisionShow.Metadata.Plot = metadata.Plot; + televisionShow.Metadata.Year = metadata.Year; + televisionShow.Metadata.SortTitle = GetSortTitle(metadata.Title); + + await _televisionRepository.Update(televisionShow); + } + + private async Task> LoadMetadata(MovieMediaItem mediaItem, string nfoFileName) + { + if (nfoFileName == null || !File.Exists(nfoFileName)) { - return title.Substring(4); + _logger.LogDebug("NFO file does not exist at {Path}", nfoFileName); + return None; } - if (title.StartsWith("Æ")) + if (!(mediaItem.Source is LocalMediaSource)) { - return title.Replace("Æ", "E"); + _logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name); + return None; } - return title; + return await LoadMovieMetadata(mediaItem, nfoFileName); } - private async Task> LoadMetadata(MediaItem mediaItem, string nfoFileName) + private async Task> LoadMetadata( + TelevisionEpisodeMediaItem mediaItem, + string nfoFileName) { if (nfoFileName == null || !File.Exists(nfoFileName)) { @@ -79,71 +151,103 @@ namespace ErsatzTV.Core.Metadata return None; } - if (!(mediaItem.Source is LocalMediaSource localMediaSource)) + if (!(mediaItem.Source is LocalMediaSource)) { _logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name); return None; } - return localMediaSource.MediaType switch + return await LoadEpisodeMetadata(mediaItem, nfoFileName); + } + + private async Task> LoadMetadata( + TelevisionShow televisionShow, + string nfoFileName) + { + if (nfoFileName == null || !File.Exists(nfoFileName)) { - MediaType.Movie => await LoadMovieMetadata(nfoFileName), - MediaType.TvShow => await LoadTvShowMetadata(nfoFileName), - _ => None - }; + _logger.LogDebug("NFO file does not exist at {Path}", nfoFileName); + return None; + } + + return await LoadTelevisionShowMetadata(nfoFileName); } - private async Task> LoadTvShowMetadata(string nfoFileName) + private async Task> LoadTelevisionShowMetadata(string nfoFileName) { try { await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read); - Option maybeNfo = TvShowSerializer.Deserialize(fileStream) as TvShowEpisodeNfo; - return maybeNfo.Match>( - nfo => new MediaMetadata + Option maybeNfo = TvShowSerializer.Deserialize(fileStream) as TvShowNfo; + return maybeNfo.Match>( + nfo => new TelevisionShowMetadata { Source = MetadataSource.Sidecar, LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName), - MediaType = MediaType.TvShow, - Title = nfo.ShowTitle, - Subtitle = nfo.Title, - Description = nfo.Outline, - EpisodeNumber = nfo.Episode, - SeasonNumber = nfo.Season, - Aired = GetAired(nfo.Aired) + Title = nfo.Title, + Plot = nfo.Plot, + Year = nfo.Year }, None); } catch (Exception ex) { - _logger.LogDebug(ex, "Failed to read TV nfo metadata from {Path}", nfoFileName); + _logger.LogDebug(ex, "Failed to read TV show nfo metadata from {Path}", nfoFileName); return None; } } - private async Task> LoadMovieMetadata(string nfoFileName) + private async Task> LoadEpisodeMetadata(TelevisionEpisodeMediaItem mediaItem, string nfoFileName) + { + try + { + await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read); + Option maybeNfo = EpisodeSerializer.Deserialize(fileStream) as TvShowEpisodeNfo; + return maybeNfo.Match>( + nfo => new TelevisionEpisodeMetadata + { + Source = MetadataSource.Sidecar, + LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName), + Title = nfo.Title, + Aired = GetAired(nfo.Aired), + Episode = nfo.Episode, + Season = nfo.Season, + Plot = nfo.Plot + }, + None); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to read TV episode nfo metadata from {Path}", nfoFileName); + return FallbackMetadataProvider.GetFallbackMetadata(mediaItem); + } + } + + private async Task> LoadMovieMetadata(MovieMediaItem mediaItem, string nfoFileName) { try { await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read); Option maybeNfo = MovieSerializer.Deserialize(fileStream) as MovieNfo; - return maybeNfo.Match>( - nfo => new MediaMetadata + return maybeNfo.Match>( + nfo => new MovieMetadata { Source = MetadataSource.Sidecar, LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName), - MediaType = MediaType.Movie, Title = nfo.Title, - Description = nfo.Outline, - ContentRating = nfo.ContentRating, - Aired = GetAired(nfo.Premiered) + Year = nfo.Year, + Premiered = nfo.Premiered, + Plot = nfo.Plot, + Outline = nfo.Outline, + Tagline = nfo.Tagline, + ContentRating = nfo.ContentRating }, None); } catch (Exception ex) { _logger.LogDebug(ex, "Failed to read Movie nfo metadata from {Path}", nfoFileName); - return None; + return FallbackMetadataProvider.GetFallbackMetadata(mediaItem); } } @@ -162,6 +266,26 @@ namespace ErsatzTV.Core.Metadata return null; } + private static string GetSortTitle(string title) + { + if (string.IsNullOrWhiteSpace(title)) + { + return title; + } + + if (title.StartsWith("the ", StringComparison.OrdinalIgnoreCase)) + { + return title.Substring(4); + } + + if (title.StartsWith("Æ")) + { + return title.Replace("Æ", "E"); + } + + return title; + } + [XmlRoot("movie")] public class MovieNfo { @@ -171,11 +295,33 @@ namespace ErsatzTV.Core.Metadata [XmlElement("outline")] public string Outline { get; set; } + [XmlElement("year")] + public int Year { get; set; } + [XmlElement("mpaa")] public string ContentRating { get; set; } [XmlElement("premiered")] - public string Premiered { get; set; } + public DateTime Premiered { get; set; } + + [XmlElement("plot")] + public string Plot { get; set; } + + [XmlElement("tagline")] + public string Tagline { get; set; } + } + + [XmlRoot("tvshow")] + public class TvShowNfo + { + [XmlElement("title")] + public string Title { get; set; } + + [XmlElement("year")] + public int Year { get; set; } + + [XmlElement("plot")] + public string Plot { get; set; } } [XmlRoot("episodedetails")] @@ -187,9 +333,6 @@ namespace ErsatzTV.Core.Metadata [XmlElement("title")] public string Title { get; set; } - [XmlElement("outline")] - public string Outline { get; set; } - [XmlElement("episode")] public int Episode { get; set; } @@ -201,6 +344,9 @@ namespace ErsatzTV.Core.Metadata [XmlElement("aired")] public string Aired { get; set; } + + [XmlElement("plot")] + public string Plot { get; set; } } } } diff --git a/ErsatzTV.Core/Metadata/LocalPosterProvider.cs b/ErsatzTV.Core/Metadata/LocalPosterProvider.cs deleted file mode 100644 index a90d79ae..00000000 --- a/ErsatzTV.Core/Metadata/LocalPosterProvider.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Images; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; - -namespace ErsatzTV.Core.Metadata -{ - public class LocalPosterProvider : ILocalPosterProvider - { - private static readonly string[] SupportedExtensions = { "jpg", "jpeg", "png", "gif", "tbn" }; - - private readonly IImageCache _imageCache; - private readonly ILogger _logger; - private readonly IMediaItemRepository _mediaItemRepository; - - public LocalPosterProvider( - IMediaItemRepository mediaItemRepository, - IImageCache imageCache, - ILogger logger) - { - _mediaItemRepository = mediaItemRepository; - _imageCache = imageCache; - _logger = logger; - } - - public async Task RefreshPoster(MediaItem mediaItem) - { - Option maybePosterPath = mediaItem.Metadata.MediaType switch - { - MediaType.Movie => RefreshMoviePoster(mediaItem), - MediaType.TvShow => RefreshTelevisionPoster(mediaItem), - _ => None - }; - - await maybePosterPath.Match( - path => SavePosterToDisk(mediaItem, path), - Task.CompletedTask); - } - - private static Option RefreshMoviePoster(MediaItem mediaItem) - { - string folder = Path.GetDirectoryName(mediaItem.Path); - if (folder != null) - { - IEnumerable possiblePaths = SupportedExtensions.Collect( - e => new[] { $"poster.{e}", Path.GetFileNameWithoutExtension(mediaItem.Path) + $"-poster.{e}" }); - Option maybePoster = - possiblePaths.Map(p => Path.Combine(folder, p)).FirstOrDefault(File.Exists); - return maybePoster; - } - - return None; - } - - private Option RefreshTelevisionPoster(MediaItem mediaItem) - { - string folder = Directory.GetParent(Path.GetDirectoryName(mediaItem.Path) ?? string.Empty)?.FullName; - if (folder != null) - { - IEnumerable possiblePaths = SupportedExtensions.Collect(e => new[] { $"poster.{e}" }); - Option maybePoster = - possiblePaths.Map(p => Path.Combine(folder, p)).FirstOrDefault(File.Exists); - return maybePoster; - } - - return None; - } - - public async Task SavePosterToDisk(MediaItem mediaItem, string posterPath) - { - byte[] originalBytes = await File.ReadAllBytesAsync(posterPath); - Either maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, 220, null); - await maybeHash.Match( - hash => - { - mediaItem.Poster = hash; - return _mediaItemRepository.Update(mediaItem); - }, - error => - { - _logger.LogWarning("Unable to save poster to disk from {Path}: {Error}", posterPath, error.Value); - return Task.CompletedTask; - }); - } - } -} diff --git a/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs b/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs index 9d1fc229..9be90b17 100644 --- a/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs @@ -15,14 +15,17 @@ namespace ErsatzTV.Core.Metadata { public class LocalStatisticsProvider : ILocalStatisticsProvider { + private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; public LocalStatisticsProvider( IMediaItemRepository mediaItemRepository, + ILocalFileSystem localFileSystem, ILogger logger) { _mediaItemRepository = mediaItemRepository; + _localFileSystem = localFileSystem; _logger = logger; } @@ -31,8 +34,8 @@ namespace ErsatzTV.Core.Metadata try { FFprobe ffprobe = await GetProbeOutput(ffprobePath, mediaItem); - MediaMetadata metadata = ProjectToMediaMetadata(ffprobe); - return await ApplyStatisticsUpdate(mediaItem, metadata); + MediaItemStatistics statistics = ProjectToMediaItemStatistics(ffprobe); + return await ApplyStatisticsUpdate(mediaItem, statistics); } catch (Exception ex) { @@ -43,23 +46,24 @@ namespace ErsatzTV.Core.Metadata private async Task ApplyStatisticsUpdate( MediaItem mediaItem, - MediaMetadata metadata) + MediaItemStatistics statistics) { - if (mediaItem.Metadata == null) + if (mediaItem.Statistics == null) { - mediaItem.Metadata = new MediaMetadata(); + mediaItem.Statistics = new MediaItemStatistics(); } - bool durationChange = mediaItem.Metadata.Duration != metadata.Duration; + bool durationChange = mediaItem.Statistics.Duration != statistics.Duration; - mediaItem.Metadata.Duration = metadata.Duration; - mediaItem.Metadata.AudioCodec = metadata.AudioCodec; - mediaItem.Metadata.SampleAspectRatio = metadata.SampleAspectRatio; - mediaItem.Metadata.DisplayAspectRatio = metadata.DisplayAspectRatio; - mediaItem.Metadata.Width = metadata.Width; - mediaItem.Metadata.Height = metadata.Height; - mediaItem.Metadata.VideoCodec = metadata.VideoCodec; - mediaItem.Metadata.VideoScanType = metadata.VideoScanType; + mediaItem.Statistics.LastWriteTime = _localFileSystem.GetLastWriteTime(mediaItem.Path); + mediaItem.Statistics.Duration = statistics.Duration; + mediaItem.Statistics.AudioCodec = statistics.AudioCodec; + mediaItem.Statistics.SampleAspectRatio = statistics.SampleAspectRatio; + mediaItem.Statistics.DisplayAspectRatio = statistics.DisplayAspectRatio; + mediaItem.Statistics.Width = statistics.Width; + mediaItem.Statistics.Height = statistics.Height; + mediaItem.Statistics.VideoCodec = statistics.VideoCodec; + mediaItem.Statistics.VideoScanType = statistics.VideoScanType; return await _mediaItemRepository.Update(mediaItem) && durationChange; } @@ -97,7 +101,7 @@ namespace ErsatzTV.Core.Metadata }); } - private MediaMetadata ProjectToMediaMetadata(FFprobe probeOutput) => + private MediaItemStatistics ProjectToMediaItemStatistics(FFprobe probeOutput) => Optional(probeOutput) .Filter(json => json?.format != null && json.streams != null) .ToValidation("Unable to parse ffprobe output") @@ -107,12 +111,12 @@ namespace ErsatzTV.Core.Metadata { var duration = TimeSpan.FromSeconds(double.Parse(json.format.duration)); - var metadata = new MediaMetadata { Duration = duration }; + var statistics = new MediaItemStatistics { Duration = duration }; FFprobeStream audioStream = json.streams.FirstOrDefault(s => s.codec_type == "audio"); if (audioStream != null) { - metadata = metadata with + statistics = statistics with { AudioCodec = audioStream.codec_name }; @@ -121,7 +125,7 @@ namespace ErsatzTV.Core.Metadata FFprobeStream videoStream = json.streams.FirstOrDefault(s => s.codec_type == "video"); if (videoStream != null) { - metadata = metadata with + statistics = statistics with { SampleAspectRatio = videoStream.sample_aspect_ratio, DisplayAspectRatio = videoStream.display_aspect_ratio, @@ -132,9 +136,9 @@ namespace ErsatzTV.Core.Metadata }; } - return metadata; + return statistics; }, - _ => new MediaMetadata()); + _ => new MediaItemStatistics()); private VideoScanType ScanTypeFromFieldOrder(string fieldOrder) => fieldOrder?.ToLowerInvariant() switch diff --git a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs new file mode 100644 index 00000000..0c402e75 --- /dev/null +++ b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Interfaces.Images; +using ErsatzTV.Core.Interfaces.Metadata; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using Microsoft.Extensions.Logging; +using static LanguageExt.Prelude; +using Seq = LanguageExt.Seq; + +namespace ErsatzTV.Core.Metadata +{ + public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner + { + private readonly ILocalFileSystem _localFileSystem; + private readonly ILocalMetadataProvider _localMetadataProvider; + private readonly ILogger _logger; + private readonly IMovieRepository _movieRepository; + + public MovieFolderScanner( + ILocalFileSystem localFileSystem, + IMovieRepository movieRepository, + ILocalStatisticsProvider localStatisticsProvider, + ILocalMetadataProvider localMetadataProvider, + IImageCache imageCache, + ILogger logger) + : base(localFileSystem, localStatisticsProvider, imageCache, logger) + { + _localFileSystem = localFileSystem; + _movieRepository = movieRepository; + _localMetadataProvider = localMetadataProvider; + _logger = logger; + } + + public async Task> ScanFolder(LocalMediaSource localMediaSource, string ffprobePath) + { + if (!_localFileSystem.IsMediaSourceAccessible(localMediaSource)) + { + return new MediaSourceInaccessible(); + } + + var folderQueue = new Queue(); + foreach (string folder in _localFileSystem.ListSubdirectories(localMediaSource.Folder).OrderBy(identity)) + { + folderQueue.Enqueue(folder); + } + + while (folderQueue.Count > 0) + { + string movieFolder = folderQueue.Dequeue(); + + var allFiles = _localFileSystem.ListFiles(movieFolder) + .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) + .Filter( + f => !ExtraFiles.Any( + e => Path.GetFileNameWithoutExtension(f).EndsWith(e, StringComparison.OrdinalIgnoreCase))) + .ToList(); + + if (allFiles.Count == 0) + { + foreach (string subdirectory in _localFileSystem.ListSubdirectories(movieFolder).OrderBy(identity)) + { + folderQueue.Enqueue(subdirectory); + } + + continue; + } + + foreach (string file in allFiles.OrderBy(identity)) + { + // TODO: figure out how to rebuild playlists + Either x = await _movieRepository.GetOrAdd(localMediaSource.Id, file); + + Either maybeMovie = await x.AsTask() + .BindT(movie => UpdateStatistics(movie, ffprobePath).MapT(_ => movie)) + .BindT(UpdateMetadata) + .BindT(UpdatePoster); + + maybeMovie.IfLeft( + error => _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value)); + } + } + + return Unit.Default; + } + + private async Task> UpdateMetadata(MovieMediaItem movie) + { + try + { + await LocateNfoFile(movie).Match( + async nfoFile => + { + if (movie.Metadata == null || movie.Metadata.Source == MetadataSource.Fallback || + (movie.Metadata.LastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(nfoFile)) + { + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); + await _localMetadataProvider.RefreshSidecarMetadata(movie, nfoFile); + } + }, + async () => + { + if (movie.Metadata == null) + { + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", movie.Path); + await _localMetadataProvider.RefreshFallbackMetadata(movie); + } + }); + + return movie; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + private async Task> UpdatePoster(MovieMediaItem movie) + { + try + { + await LocatePoster(movie).IfSomeAsync( + async posterFile => + { + if (string.IsNullOrWhiteSpace(movie.Poster) || + (movie.PosterLastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(posterFile)) + { + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Poster", posterFile); + await SavePosterToDisk(movie, posterFile, _movieRepository.Update, 440); + } + }); + + return movie; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + private Option LocateNfoFile(MovieMediaItem movie) + { + string movieAsNfo = Path.ChangeExtension(movie.Path, "nfo"); + string movieNfo = Path.Combine(Path.GetDirectoryName(movie.Path) ?? string.Empty, "movie.nfo"); + return Seq.create(movieAsNfo, movieNfo) + .Filter(s => _localFileSystem.FileExists(s)) + .HeadOrNone(); + } + + private Option LocatePoster(MovieMediaItem movie) + { + string folder = Path.GetDirectoryName(movie.Path) ?? string.Empty; + IEnumerable possibleMoviePosters = ImageFileExtensions.Collect( + ext => new[] { $"poster.{ext}", Path.GetFileNameWithoutExtension(movie.Path) + $"-poster.{ext}" }) + .Map(f => Path.Combine(folder, f)); + Option result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone(); + return result; + } + } +} diff --git a/ErsatzTV.Core/Metadata/ScanningAction.cs b/ErsatzTV.Core/Metadata/ScanningAction.cs deleted file mode 100644 index e09b53de..00000000 --- a/ErsatzTV.Core/Metadata/ScanningAction.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace ErsatzTV.Core.Metadata -{ - public enum ScanningAction - { - None = 0, - Add = 1, - Remove = 2, - Statistics = 3, - SidecarMetadata = 4, - FallbackMetadata = 5, - Collections = 6, - Poster = 7 - } -} diff --git a/ErsatzTV.Core/Metadata/ScanningMode.cs b/ErsatzTV.Core/Metadata/ScanningMode.cs deleted file mode 100644 index e38fd6b3..00000000 --- a/ErsatzTV.Core/Metadata/ScanningMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ErsatzTV.Core.Metadata -{ - public enum ScanningMode - { - Default = 0, - RescanAll = 1 - } -} diff --git a/ErsatzTV.Core/Metadata/SmartCollectionBuilder.cs b/ErsatzTV.Core/Metadata/SmartCollectionBuilder.cs deleted file mode 100644 index db84fa64..00000000 --- a/ErsatzTV.Core/Metadata/SmartCollectionBuilder.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using static LanguageExt.Prelude; - -namespace ErsatzTV.Core.Metadata -{ - public class SmartCollectionBuilder : ISmartCollectionBuilder - { - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public SmartCollectionBuilder(IMediaCollectionRepository mediaCollectionRepository) => - _mediaCollectionRepository = mediaCollectionRepository; - - public async Task RefreshSmartCollections(MediaItem mediaItem) - { - var results = new List(); - - foreach (TelevisionMediaCollection collection in GetTelevisionCollections(mediaItem)) - { - results.Add(await _mediaCollectionRepository.InsertOrIgnore(collection)); - } - - return results.Any(identity); - } - - private IEnumerable GetTelevisionCollections(MediaItem mediaItem) - { - IList televisionMediaItems = new[] { mediaItem } - .Where(c => c.Metadata.MediaType == MediaType.TvShow) - .ToList(); - - IEnumerable televisionShowCollections = televisionMediaItems - .Map(c => c.Metadata.Title) - .Distinct().Map( - t => new TelevisionMediaCollection - { - Name = $"{t} - All Seasons", - ShowTitle = t, - SeasonNumber = null - }); - - IEnumerable televisionShowSeasonCollections = televisionMediaItems - .Map(c => new { c.Metadata.Title, c.Metadata.SeasonNumber }).Distinct() - .Map( - ts => - { - return Optional(ts.SeasonNumber).Map( - sn => new TelevisionMediaCollection - { - Name = $"{ts.Title} - Season {sn:00}", - ShowTitle = ts.Title, - SeasonNumber = sn - }); - }) - .Sequence().Flatten(); - - return Seq(televisionShowCollections, televisionShowSeasonCollections).Flatten(); - } - } -} diff --git a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs new file mode 100644 index 00000000..d1fe84f2 --- /dev/null +++ b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs @@ -0,0 +1,361 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Images; +using ErsatzTV.Core.Interfaces.Metadata; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using Microsoft.Extensions.Logging; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Core.Metadata +{ + public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScanner + { + private readonly ILocalFileSystem _localFileSystem; + private readonly ILocalMetadataProvider _localMetadataProvider; + private readonly ILogger _logger; + private readonly ITelevisionRepository _televisionRepository; + + public TelevisionFolderScanner( + ILocalFileSystem localFileSystem, + ITelevisionRepository televisionRepository, + ILocalStatisticsProvider localStatisticsProvider, + ILocalMetadataProvider localMetadataProvider, + IImageCache imageCache, + ILogger logger) : base( + localFileSystem, + localStatisticsProvider, + imageCache, + logger) + { + _localFileSystem = localFileSystem; + _televisionRepository = televisionRepository; + _localMetadataProvider = localMetadataProvider; + _logger = logger; + } + + public async Task ScanFolder(LocalMediaSource localMediaSource, string ffprobePath) + { + if (!_localFileSystem.IsMediaSourceAccessible(localMediaSource)) + { + _logger.LogWarning( + "Media source is not accessible or missing; skipping scan of {Folder}", + localMediaSource.Folder); + return Unit.Default; + } + + var allShowFolders = _localFileSystem.ListSubdirectories(localMediaSource.Folder) + .Filter(ShouldIncludeFolder) + .OrderBy(identity) + .ToList(); + + foreach (string showFolder in allShowFolders) + { + // TODO: check all sources for latest metadata? + Either maybeShow = + await FindOrCreateShow(localMediaSource.Id, showFolder) + .BindT(show => UpdateMetadataForShow(show, showFolder)) + .BindT(show => UpdatePosterForShow(show, showFolder)); + + await maybeShow.Match( + show => ScanSeasons(localMediaSource, ffprobePath, show, showFolder), + _ => Task.FromResult(Unit.Default)); + } + + await _televisionRepository.DeleteMissingSources(localMediaSource.Id, allShowFolders); + await _televisionRepository.DeleteEmptyShows(); + + return Unit.Default; + } + + private async Task> FindOrCreateShow( + int localMediaSourceId, + string showFolder) + { + Option maybeShowByPath = + await _televisionRepository.GetShowByPath(localMediaSourceId, showFolder); + return await maybeShowByPath.Match( + show => Right(show).AsTask(), + async () => + { + TelevisionShowMetadata metadata = await _localMetadataProvider.GetMetadataForShow(showFolder); + Option maybeShow = await _televisionRepository.GetShowByMetadata(metadata); + return await maybeShow.Match( + async show => + { + show.Sources.Add( + new LocalTelevisionShowSource + { + MediaSourceId = localMediaSourceId, + Path = showFolder, + TelevisionShow = show + }); + await _televisionRepository.Update(show); + return Right(show); + }, + async () => await _televisionRepository.AddShow(localMediaSourceId, showFolder, metadata)); + }); + } + + private async Task ScanSeasons( + LocalMediaSource localMediaSource, + string ffprobePath, + TelevisionShow show, + string showPath) + { + foreach (string seasonFolder in _localFileSystem.ListSubdirectories(showPath).Filter(ShouldIncludeFolder) + .OrderBy(identity)) + { + Option maybeSeasonNumber = SeasonNumberForFolder(seasonFolder); + await maybeSeasonNumber.IfSomeAsync( + async seasonNumber => + { + Either maybeSeason = await _televisionRepository + .GetOrAddSeason(show, seasonFolder, seasonNumber) + .BindT(UpdatePoster); + + await maybeSeason.Match( + season => ScanEpisodes(localMediaSource, ffprobePath, season), + _ => Task.FromResult(Unit.Default)); + }); + } + + return Unit.Default; + } + + private async Task ScanEpisodes( + LocalMediaSource localMediaSource, + string ffprobePath, + TelevisionSeason season) + { + foreach (string file in _localFileSystem.ListFiles(season.Path) + .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))).OrderBy(identity)) + { + // TODO: figure out how to rebuild playlists + Either maybeEpisode = await _televisionRepository + .GetOrAddEpisode(season, localMediaSource.Id, file) + .BindT(episode => UpdateStatistics(episode, ffprobePath).MapT(_ => episode)) + .BindT(UpdateMetadata) + .BindT(UpdateThumbnail); + + maybeEpisode.IfLeft( + error => _logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value)); + } + + return Unit.Default; + } + + private async Task> UpdateMetadataForShow( + TelevisionShow show, + string showFolder) + { + try + { + await LocateNfoFileForShow(showFolder).Match( + async nfoFile => + { + if (show.Metadata == null || show.Metadata.Source == MetadataSource.Fallback || + (show.Metadata.LastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(nfoFile)) + { + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); + await _localMetadataProvider.RefreshSidecarMetadata(show, nfoFile); + } + }, + async () => + { + if (show.Metadata == null) + { + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", showFolder); + await _localMetadataProvider.RefreshFallbackMetadata(show, showFolder); + } + }); + + return show; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + private async Task> UpdateMetadata( + TelevisionEpisodeMediaItem episode) + { + try + { + await LocateNfoFile(episode).Match( + async nfoFile => + { + if (episode.Metadata == null || episode.Metadata.Source == MetadataSource.Fallback || + (episode.Metadata.LastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(nfoFile)) + { + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); + await _localMetadataProvider.RefreshSidecarMetadata(episode, nfoFile); + } + }, + async () => + { + if (episode.Metadata == null) + { + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", episode.Path); + await _localMetadataProvider.RefreshFallbackMetadata(episode); + } + }); + + return episode; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + private async Task> UpdatePosterForShow( + TelevisionShow show, + string showFolder) + { + try + { + await LocatePosterForShow(showFolder).IfSomeAsync( + async posterFile => + { + if (string.IsNullOrWhiteSpace(show.Poster) || + (show.PosterLastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(posterFile)) + { + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Poster", posterFile); + Either maybePoster = await SavePosterToDisk(posterFile, 440); + await maybePoster.Match( + poster => + { + show.Poster = poster; + show.PosterLastWriteTime = _localFileSystem.GetLastWriteTime(posterFile); + return _televisionRepository.Update(show); + }, + error => + { + _logger.LogWarning( + "Unable to save poster to disk from {Path}: {Error}", + posterFile, + error.Value); + return Task.CompletedTask; + }); + } + }); + + return show; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + private async Task> UpdatePoster(TelevisionSeason season) + { + try + { + await LocatePoster(season).IfSomeAsync( + async posterFile => + { + if (string.IsNullOrWhiteSpace(season.Poster) || + (season.PosterLastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(posterFile)) + { + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Poster", posterFile); + await SavePosterToDisk(season, posterFile, _televisionRepository.Update, 440); + } + }); + + return season; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + private async Task> UpdateThumbnail( + TelevisionEpisodeMediaItem episode) + { + try + { + await LocateThumbnail(episode).IfSomeAsync( + async posterFile => + { + if (string.IsNullOrWhiteSpace(episode.Poster) || + (episode.PosterLastWriteTime ?? DateTime.MinValue) < + _localFileSystem.GetLastWriteTime(posterFile)) + { + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Thumbnail", posterFile); + await SavePosterToDisk(episode, posterFile, _televisionRepository.Update); + } + }); + + return episode; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + private Option LocateNfoFileForShow(string showFolder) => + Optional(Path.Combine(showFolder, "tvshow.nfo")) + .Filter(s => _localFileSystem.FileExists(s)); + + private Option LocateNfoFile(TelevisionEpisodeMediaItem episode) => + Optional(Path.ChangeExtension(episode.Path, "nfo")) + .Filter(s => _localFileSystem.FileExists(s)); + + private Option LocatePosterForShow(string showFolder) => + ImageFileExtensions + .Map(ext => $"poster.{ext}") + .Map(f => Path.Combine(showFolder, f)) + .Filter(s => _localFileSystem.FileExists(s)) + .HeadOrNone(); + + private Option LocatePoster(TelevisionSeason season) + { + string folder = Path.GetDirectoryName(season.Path) ?? string.Empty; + return ImageFileExtensions + .Map(ext => Path.Combine(folder, $"season{season.Number:00}-poster.{ext}")) + .Filter(s => _localFileSystem.FileExists(s)) + .HeadOrNone(); + } + + private Option LocateThumbnail(TelevisionEpisodeMediaItem episode) + { + string folder = Path.GetDirectoryName(episode.Path) ?? string.Empty; + return ImageFileExtensions + .Map(ext => Path.GetFileNameWithoutExtension(episode.Path) + $"-thumb.{ext}") + .Map(f => Path.Combine(folder, f)) + .Filter(f => _localFileSystem.FileExists(f)) + .HeadOrNone(); + } + + private bool ShouldIncludeFolder(string folder) => + !Path.GetFileName(folder).StartsWith('.') && + !_localFileSystem.FileExists(Path.Combine(folder, ".etvignore")); + + private static Option SeasonNumberForFolder(string folder) + { + if (int.TryParse(folder.Split(" ").Last(), out int seasonNumber)) + { + return seasonNumber; + } + + if (folder.EndsWith("specials", StringComparison.OrdinalIgnoreCase)) + { + return 0; + } + + return None; + } + } +} diff --git a/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs index d2f64d40..1bf7eeda 100644 --- a/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs @@ -16,10 +16,7 @@ namespace ErsatzTV.Core.Scheduling IEnumerable mediaItems, MediaCollectionEnumeratorState state) { - _sortedMediaItems = mediaItems.OrderBy(c => c.Metadata.Aired ?? DateTime.MaxValue) - .ThenBy(c => c.Metadata.SeasonNumber) - .ThenBy(c => c.Metadata.EpisodeNumber) - .ToList(); + _sortedMediaItems = mediaItems.OrderBy(identity, new ChronologicalComparer()).ToList(); State = new MediaCollectionEnumeratorState { Seed = state.Seed }; while (State.Index < state.Index) @@ -33,5 +30,71 @@ namespace ErsatzTV.Core.Scheduling public Option Current => _sortedMediaItems.Any() ? _sortedMediaItems[State.Index] : None; public void MoveNext() => State.Index = (State.Index + 1) % _sortedMediaItems.Count; + + private class ChronologicalComparer : IComparer + { + public int Compare(MediaItem x, MediaItem y) + { + if (x == null || y == null) + { + return 0; + } + + DateTime date1 = x switch + { + TelevisionEpisodeMediaItem e => e.Metadata?.Aired ?? DateTime.MaxValue, + MovieMediaItem m => m.Metadata?.Premiered ?? DateTime.MaxValue, + _ => DateTime.MaxValue + }; + + DateTime date2 = y switch + { + TelevisionEpisodeMediaItem e => e.Metadata?.Aired ?? DateTime.MaxValue, + MovieMediaItem m => m.Metadata?.Premiered ?? DateTime.MaxValue, + _ => DateTime.MaxValue + }; + + if (date1 != date2) + { + return date1.CompareTo(date2); + } + + int season1 = x switch + { + TelevisionEpisodeMediaItem e => e.Metadata?.Season ?? int.MaxValue, + _ => int.MaxValue + }; + + int season2 = y switch + { + TelevisionEpisodeMediaItem e => e.Metadata?.Season ?? int.MaxValue, + _ => int.MaxValue + }; + + if (season1 != season2) + { + return season1.CompareTo(season2); + } + + int episode1 = x switch + { + TelevisionEpisodeMediaItem e => e.Metadata?.Episode ?? int.MaxValue, + _ => int.MaxValue + }; + + int episode2 = y switch + { + TelevisionEpisodeMediaItem e => e.Metadata?.Episode ?? int.MaxValue, + _ => int.MaxValue + }; + + if (episode1 != episode2) + { + return episode1.CompareTo(episode2); + } + + return x.Id.CompareTo(y.Id); + } + } } } diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index 9a8dfc44..16d70a2c 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Scheduling; using LanguageExt; +using LanguageExt.UnsafeValueAccess; using Microsoft.Extensions.Logging; using static LanguageExt.Prelude; using Map = LanguageExt.Map; @@ -17,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; } @@ -38,13 +43,31 @@ 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) + .Distinct() + .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); @@ -56,6 +79,16 @@ namespace ErsatzTV.Core.Scheduling playout.Channel.Number, playout.Channel.Name); + Option emptyCollection = collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key); + if (emptyCollection.IsSome) + { + _logger.LogError( + "Unable to rebuild playout; collection {@CollectionKey} has no items!", + emptyCollection.ValueUnsafe()); + + return playout; + } + playout.Items ??= new List(); playout.ProgramScheduleAnchors ??= new List(); @@ -67,7 +100,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 @@ -100,15 +133,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); @@ -117,10 +149,10 @@ namespace ErsatzTV.Core.Scheduling { MediaItemId = mediaItem.Id, Start = itemStartTime, - Finish = itemStartTime + mediaItem.Metadata.Duration + Finish = itemStartTime + mediaItem.Statistics.Duration }; - currentTime = itemStartTime + mediaItem.Metadata.Duration; + currentTime = itemStartTime + mediaItem.Statistics.Duration; enumerator.MoveNext(); playout.Items.Add(playoutItem); @@ -166,7 +198,7 @@ namespace ErsatzTV.Core.Scheduling // is after, we need to move on to the next schedule item // eventually, spots probably have to fit in this gap bool willNotFinishInTime = currentTime <= peekScheduleItemStart && - currentTime + peekMediaItem.Metadata.Duration > + currentTime + peekMediaItem.Statistics.Duration > peekScheduleItemStart; if (willNotFinishInTime) { @@ -189,7 +221,7 @@ namespace ErsatzTV.Core.Scheduling bool willNotFinishInTime = currentTime <= durationFinish.IfNone(DateTime.MinValue) && - currentTime + peekMediaItem.Metadata.Duration > + currentTime + peekMediaItem.Statistics.Duration > durationFinish.IfNone(DateTime.MinValue); if (willNotFinishInTime) { @@ -285,14 +317,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()); @@ -300,7 +333,7 @@ namespace ErsatzTV.Core.Scheduling PlayoutProgramScheduleAnchor scheduleAnchor = maybeExisting.Match( existing => { - existing.EnumeratorState = maybeEnumeratorState[collection]; + existing.EnumeratorState = maybeEnumeratorState[collectionKey]; return existing; }, () => new PlayoutProgramScheduleAnchor @@ -309,9 +342,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); @@ -322,12 +355,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, @@ -348,8 +383,40 @@ namespace ErsatzTV.Core.Scheduling } private static string DisplayTitle(MediaItem mediaItem) => - mediaItem.Metadata.MediaType == MediaType.TvShow - ? $"{mediaItem.Metadata.Title} - s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}" - : mediaItem.Metadata.Title; + mediaItem switch + { + TelevisionEpisodeMediaItem e => e.Metadata != null + ? $"{e.Metadata.Title} - s{e.Metadata.Season:00}e{e.Metadata.Episode:00}" + : Path.GetFileName(e.Path), + 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 + }, + _ => throw new ArgumentOutOfRangeException(nameof(item)) + }; + + private class CollectionKey : Record + { + public ProgramScheduleItemCollectionType CollectionType { get; set; } + public int Id { get; set; } + } } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs index c2c793c4..40616253 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs @@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations public class GenericIntegerIdConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) => - builder.HasNoKey().ToView(null); + builder.HasNoKey().ToView("No table or view exists for GenericIntegerId"); } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs index 66187d03..97b1a673 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs @@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations public class MediaCollectionSummaryConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) => - builder.HasNoKey().ToView(null); + builder.HasNoKey().ToView("No table or view exists for MediaCollectionSummary"); } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MediaItemConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MediaItemConfiguration.cs index b0681a22..dcb5df39 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/MediaItemConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/MediaItemConfiguration.cs @@ -6,7 +6,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations { public class MediaItemConfiguration : IEntityTypeConfiguration { - public void Configure(EntityTypeBuilder builder) => - builder.OwnsOne(c => c.Metadata).WithOwner(); + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("MediaItems"); + builder.OwnsOne(c => c.Statistics).WithOwner(); + } } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs index 58cb49c4..5bed7f58 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs @@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations public class MediaItemSummaryConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) => - builder.HasNoKey().ToView(null); + builder.HasNoKey().ToView("No table or view exists for MediaItemSummary"); } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MovieMediaItemConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MovieMediaItemConfiguration.cs new file mode 100644 index 00000000..b1a5ca66 --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/MovieMediaItemConfiguration.cs @@ -0,0 +1,19 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class MovieMediaItemConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Movies"); + + builder.HasOne(i => i.Metadata) + .WithOne(m => m.Movie) + .HasForeignKey(m => m.MovieId) + .OnDelete(DeleteBehavior.Cascade); + } + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MovieMetadataConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MovieMetadataConfiguration.cs new file mode 100644 index 00000000..70a6ffdb --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/MovieMetadataConfiguration.cs @@ -0,0 +1,12 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class MovieMetadataConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) => + builder.ToTable("MovieMetadata"); + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs index b25a39e4..8585e920 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 08ef8482..f3c72b35 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/Configurations/SimpleMediaCollectionConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/SimpleMediaCollectionConfiguration.cs index 66d11f58..5f79f77a 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/SimpleMediaCollectionConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/SimpleMediaCollectionConfiguration.cs @@ -10,8 +10,21 @@ namespace ErsatzTV.Infrastructure.Data.Configurations { builder.ToTable("SimpleMediaCollections"); - builder.HasMany(cg => cg.Items) - .WithMany(c => c.SimpleMediaCollections); + builder.HasMany(c => c.Movies) + .WithMany(m => m.SimpleMediaCollections) + .UsingEntity(join => join.ToTable("SimpleMediaCollectionMovies")); + + builder.HasMany(c => c.TelevisionShows) + .WithMany(s => s.SimpleMediaCollections) + .UsingEntity(join => join.ToTable("SimpleMediaCollectionShows")); + + builder.HasMany(c => c.TelevisionSeasons) + .WithMany(s => s.SimpleMediaCollections) + .UsingEntity(join => join.ToTable("SimpleMediaCollectionSeasons")); + + builder.HasMany(c => c.TelevisionEpisodes) + .WithMany(e => e.SimpleMediaCollections) + .UsingEntity(join => join.ToTable("SimpleMediaCollectionEpisodes")); } } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/TelevisionEpisodeMediaItemConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionEpisodeMediaItemConfiguration.cs new file mode 100644 index 00000000..829316bb --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionEpisodeMediaItemConfiguration.cs @@ -0,0 +1,19 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class TelevisionEpisodeMediaItemConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("TelevisionEpisodes"); + + builder.HasOne(i => i.Metadata) + .WithOne(m => m.TelevisionEpisode) + .HasForeignKey(m => m.TelevisionEpisodeId) + .OnDelete(DeleteBehavior.Cascade); + } + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/TelevisionEpisodeMetadataConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionEpisodeMetadataConfiguration.cs new file mode 100644 index 00000000..08c84c14 --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionEpisodeMetadataConfiguration.cs @@ -0,0 +1,12 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class TelevisionEpisodeMetadataConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) => + builder.ToTable("TelevisionEpisodeMetadata"); + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/TelevisionMediaCollectionConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionMediaCollectionConfiguration.cs deleted file mode 100644 index e2345daf..00000000 --- a/ErsatzTV.Infrastructure/Data/Configurations/TelevisionMediaCollectionConfiguration.cs +++ /dev/null @@ -1,17 +0,0 @@ -using ErsatzTV.Core.Domain; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace ErsatzTV.Infrastructure.Data.Configurations -{ - public class TelevisionMediaCollectionConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("TelevisionMediaCollections"); - - builder.HasIndex(c => new { c.ShowTitle, c.SeasonNumber }) - .IsUnique(); - } - } -} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/TelevisionSeasonConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionSeasonConfiguration.cs new file mode 100644 index 00000000..8ebc89e8 --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionSeasonConfiguration.cs @@ -0,0 +1,19 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class TelevisionSeasonConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("TelevisionSeasons"); + + builder.HasMany(season => season.Episodes) + .WithOne(episode => episode.Season) + .HasForeignKey(episode => episode.SeasonId) + .OnDelete(DeleteBehavior.Cascade); + } + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowConfiguration.cs new file mode 100644 index 00000000..75464a52 --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowConfiguration.cs @@ -0,0 +1,27 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class TelevisionShowConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("TelevisionShows"); + + builder.HasOne(show => show.Metadata) + .WithOne(metadata => metadata.TelevisionShow) + .HasForeignKey(metadata => metadata.TelevisionShowId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(show => show.Seasons) + .WithOne(season => season.TelevisionShow); + + builder.HasMany(show => show.Sources) + .WithOne(source => source.TelevisionShow) + .HasForeignKey(source => source.TelevisionShowId) + .OnDelete(DeleteBehavior.Cascade); + } + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowMetadataConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowMetadataConfiguration.cs new file mode 100644 index 00000000..145dbc1c --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowMetadataConfiguration.cs @@ -0,0 +1,12 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class TelevisionShowMetadataConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) => + builder.ToTable("TelevisionShowMetadata"); + } +} diff --git a/ErsatzTV.Infrastructure/Data/DbInitializer.cs b/ErsatzTV.Infrastructure/Data/DbInitializer.cs index b2d9472f..1d01fd9c 100644 --- a/ErsatzTV.Infrastructure/Data/DbInitializer.cs +++ b/ErsatzTV.Infrastructure/Data/DbInitializer.cs @@ -55,14 +55,6 @@ namespace ErsatzTV.Infrastructure.Data context.Channels.Add(defaultChannel); context.SaveChanges(); - // TODO: clean this up - // var mediaSource = new LocalMediaSource - // { - // Name = "Default" - // }; - // context.MediaSources.Add(mediaSource); - // context.SaveChanges(); - // TODO: create looping static image that mentions configuring via web return Unit.Default; } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs index bceb67e6..a7c6f5dd 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/ChannelRepository.cs @@ -38,6 +38,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(c => c.Playouts) .ThenInclude(p => p.Items) .ThenInclude(i => i.MediaItem) + .ThenInclude(i => (i as TelevisionEpisodeMediaItem).Metadata) + .Include(c => c.Playouts) + .ThenInclude(p => p.Items) + .ThenInclude(i => i.MediaItem) + .ThenInclude(i => (i as MovieMediaItem).Metadata) .ToListAsync(); public async Task Update(Channel channel) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 66425451..812a63a7 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -2,7 +2,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; @@ -28,17 +27,42 @@ namespace ErsatzTV.Infrastructure.Data.Repositories _dbContext.MediaCollections.SingleOrDefaultAsync(c => c.Id == id).Map(Optional); public Task> GetSimpleMediaCollection(int id) => - Get(id).Map(c => c.OfType().HeadOrNone()); + _dbContext.SimpleMediaCollections + .SingleOrDefaultAsync(c => c.Id == id) + .Map(Optional); public Task> GetSimpleMediaCollectionWithItems(int id) => _dbContext.SimpleMediaCollections - .Include(s => s.Items) - .ThenInclude(i => i.Source) + .Include(s => s.Movies) + .ThenInclude(m => m.Source) + .Include(s => s.TelevisionShows) + .ThenInclude(s => s.Metadata) + .Include(s => s.TelevisionSeasons) + .Include(s => s.TelevisionEpisodes) + .ThenInclude(s => s.Metadata) .SingleOrDefaultAsync(c => c.Id == id) .Map(Optional); - public Task> GetTelevisionMediaCollection(int id) => - Get(id).Map(c => c.OfType().HeadOrNone()); + public Task> GetSimpleMediaCollectionWithItemsUntracked(int id) => + _dbContext.SimpleMediaCollections + .AsNoTracking() + .Include(s => s.Movies) + .ThenInclude(i => i.Source) + .Include(s => s.Movies) + .ThenInclude(m => m.Metadata) + .Include(s => s.TelevisionShows) + .ThenInclude(s => s.Metadata) + .Include(s => s.TelevisionSeasons) + .ThenInclude(s => s.TelevisionShow) + .ThenInclude(s => s.Metadata) + .Include(s => s.TelevisionEpisodes) + .ThenInclude(s => s.Metadata) + .Include(s => s.TelevisionEpisodes) + .ThenInclude(e => e.Season) + .ThenInclude(s => s.TelevisionShow) + .ThenInclude(s => s.Metadata) + .SingleOrDefaultAsync(c => c.Id == id) + .Map(Optional); public Task> GetSimpleMediaCollections() => _dbContext.SimpleMediaCollections.ToListAsync(); @@ -46,109 +70,101 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public Task> GetAll() => _dbContext.MediaCollections.ToListAsync(); - public Task> GetSummaries(string searchString) => - _dbContext.MediaCollectionSummaries.FromSqlRaw( - @"SELECT mc.Id, mc.Name, Count(mismc.ItemsId) AS ItemCount, true AS IsSimple - FROM MediaCollections mc - INNER JOIN SimpleMediaCollections smc ON smc.Id = mc.Id - LEFT OUTER JOIN MediaItemSimpleMediaCollection mismc ON mismc.SimpleMediaCollectionsId = mc.Id - WHERE mc.Name LIKE {0} - GROUP BY mc.Id, mc.Name - UNION ALL - SELECT mc.Id, mc.Name, Count(mi.Id) AS ItemCount, false AS IsSimple - FROM MediaCollections mc - INNER JOIN TelevisionMediaCollections tmc ON tmc.Id = mc.Id - LEFT OUTER JOIN MediaItems mi ON (tmc.SeasonNumber IS NULL OR mi.Metadata_SeasonNumber = tmc.SeasonNumber) - AND mi.Metadata_Title = tmc.ShowTitle - WHERE mc.Name LIKE {0} - GROUP BY mc.Id, mc.Name", - $"%{searchString}%").ToListAsync(); - public Task>> GetItems(int id) => Get(id).MapT( collection => collection switch { SimpleMediaCollection s => SimpleItems(s), - TelevisionMediaCollection t => TelevisionItems(t), _ => throw new NotSupportedException($"Unsupported collection type {collection.GetType().Name}") }).Bind(x => x.Sequence()); public Task>> GetSimpleMediaCollectionItems(int id) => GetSimpleMediaCollection(id).MapT(SimpleItems).Bind(x => x.Sequence()); - public Task>> GetTelevisionMediaCollectionItems(int id) => - GetTelevisionMediaCollection(id).MapT(TelevisionItems).Bind(x => x.Sequence()); - public Task Update(SimpleMediaCollection collection) { _dbContext.SimpleMediaCollections.Update(collection); return _dbContext.SaveChangesAsync(); } - public async Task InsertOrIgnore(TelevisionMediaCollection collection) + public async Task Delete(int mediaCollectionId) { - if (!_dbContext.TelevisionMediaCollections.Any( - existing => existing.ShowTitle == collection.ShowTitle && - existing.SeasonNumber == collection.SeasonNumber)) - { - await _dbContext.TelevisionMediaCollections.AddAsync(collection); - return await _dbContext.SaveChangesAsync() > 0; - } - - // no change - return false; + MediaCollection mediaCollection = await _dbContext.MediaCollections.FindAsync(mediaCollectionId); + _dbContext.MediaCollections.Remove(mediaCollection); + await _dbContext.SaveChangesAsync(); } - public Task ReplaceItems(int collectionId, List mediaItems) => - GetSimpleMediaCollection(collectionId).IfSomeAsync( - async c => - { - await SimpleItems(c); + private async Task> SimpleItems(SimpleMediaCollection collection) + { + var result = new List(); - c.Items.Clear(); - foreach (MediaItem mediaItem in mediaItems) - { - c.Items.Add(mediaItem); - } + await _dbContext.Entry(collection).Collection(c => c.Movies).LoadAsync(); + result.AddRange(collection.Movies); - _dbContext.SimpleMediaCollections.Update(c); - await _dbContext.SaveChangesAsync(); - }); + result.AddRange(await GetTelevisionShowItems(collection)); + result.AddRange(await GetTelevisionSeasonItems(collection)); + result.AddRange(await GetTelevisionEpisodeItems(collection)); - public async Task Delete(int mediaCollectionId) - { - MediaCollection mediaCollection = await _dbContext.MediaCollections.FindAsync(mediaCollectionId); - _dbContext.MediaCollections.Remove(mediaCollection); - await _dbContext.SaveChangesAsync(); + return result.Distinct().ToList(); } - public async Task DeleteEmptyTelevisionCollections() + private async Task> GetTelevisionShowItems(SimpleMediaCollection collection) { - List ids = await _dbContext.GenericIntegerIds.FromSqlRaw( - @"SELECT mc.Id FROM MediaCollections mc -INNER JOIN TelevisionMediaCollections t on mc.Id = t.Id -WHERE NOT EXISTS -(SELECT 1 FROM MediaItems mi WHERE t.ShowTitle = mi.Metadata_Title AND (t.SeasonNumber IS NULL OR t.SeasonNumber = mi.Metadata_SeasonNumber))") - .Map(i => i.Id) + // 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 +inner join SimpleMediaCollectionShows s on s.TelevisionShowsId = ts.Id +where s.SimpleMediaCollectionsId = {0}", + collection.Id) + .Select(i => i.Id) .ToListAsync(); - List toDelete = - await _dbContext.MediaCollections.Where(mc => ids.Contains(mc.Id)).ToListAsync(); - _dbContext.MediaCollections.RemoveRange(toDelete); - - await _dbContext.SaveChangesAsync(); + return await _dbContext.TelevisionEpisodeMediaItems + .AsNoTracking() + .Include(e => e.Metadata) + .Where(mi => showItemIds.Contains(mi.Id)) + .ToListAsync(); } - private async Task> SimpleItems(SimpleMediaCollection collection) + private async Task> GetTelevisionSeasonItems(SimpleMediaCollection collection) { - await _dbContext.Entry(collection).Collection(c => c.Items).LoadAsync(); - return collection.Items.ToList(); + // 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 +inner join SimpleMediaCollectionSeasons s on s.TelevisionSeasonsId = tsn.Id +where s.SimpleMediaCollectionsId = {0}", + collection.Id) + .Select(i => i.Id) + .ToListAsync(); + + return await _dbContext.TelevisionEpisodeMediaItems + .AsNoTracking() + .Include(e => e.Metadata) + .Where(mi => seasonItemIds.Contains(mi.Id)) + .ToListAsync(); } - private Task> TelevisionItems(TelevisionMediaCollection collection) => - _dbContext.MediaItems - .Filter(c => c.Metadata.Title == collection.ShowTitle) - .Filter(c => collection.SeasonNumber == null || c.Metadata.SeasonNumber == collection.SeasonNumber) + private async Task> GetTelevisionEpisodeItems(SimpleMediaCollection collection) + { + // TODO: would be nice to get the media items in one go, but ef... + List episodeItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw( + @"select s.TelevisionEpisodesId as Id +from SimpleMediaCollectionEpisodes s +where s.SimpleMediaCollectionsId = {0}", + collection.Id) + .Select(i => i.Id) + .ToListAsync(); + + return await _dbContext.TelevisionEpisodeMediaItems + .AsNoTracking() + .Include(e => e.Metadata) + .Where(mi => episodeItemIds.Contains(mi.Id)) .ToListAsync(); + } } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs index b419ff7d..add440fd 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; @@ -16,13 +15,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public MediaItemRepository(TvContext dbContext) => _dbContext = dbContext; - public async Task Add(MediaItem mediaItem) - { - await _dbContext.MediaItems.AddAsync(mediaItem); - await _dbContext.SaveChangesAsync(); - return mediaItem.Id; - } - public Task> Get(int id) => _dbContext.MediaItems .Include(i => i.Source) @@ -33,81 +25,29 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public Task> Search(string searchString) { - IQueryable data = from c in _dbContext.MediaItems.Include(c => c.Source) select c; + IQueryable episodeData = + from c in _dbContext.TelevisionEpisodeMediaItems.Include(c => c.Source) select c; if (!string.IsNullOrEmpty(searchString)) { - data = data.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%")); + episodeData = episodeData.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%")); } - return data.ToListAsync(); - } - - - public Task> GetPageByType(MediaType mediaType, int pageNumber, int pageSize) => - mediaType switch - { - MediaType.Movie => _dbContext.MediaItemSummaries.FromSqlRaw( - @"SELECT - Id AS MediaItemId, - Metadata_Title AS Title, - Metadata_SortTitle AS SortTitle, - substr(Metadata_Aired, 1, 4) AS Subtitle, - Poster -FROM MediaItems WHERE Metadata_MediaType=2 -ORDER BY Metadata_SortTitle -LIMIT {0} OFFSET {1}", - pageSize, - (pageNumber - 1) * pageSize) - .AsNoTracking() - .ToListAsync(), - MediaType.TvShow => _dbContext.MediaItemSummaries.FromSqlRaw( - @"SELECT - min(Id) AS MediaItemId, - Metadata_Title AS Title, - Metadata_SortTitle AS SortTitle, - count(*) || ' Episodes' AS Subtitle, - max(Poster) AS Poster -FROM MediaItems WHERE Metadata_MediaType=1 -GROUP BY Metadata_Title, Metadata_SortTitle -ORDER BY Metadata_SortTitle -LIMIT {0} OFFSET {1}", - pageSize, - (pageNumber - 1) * pageSize) - .AsNoTracking() - .ToListAsync(), - _ => Task.FromResult(new List()) - }; + IQueryable movieData = + from c in _dbContext.MovieMediaItems.Include(c => c.Source) select c; - public Task GetCountByType(MediaType mediaType) => - mediaType switch + if (!string.IsNullOrEmpty(searchString)) { - MediaType.Movie => _dbContext.MediaItems - .Filter(i => i.Metadata.MediaType == mediaType) - .CountAsync(), - MediaType.TvShow => _dbContext.MediaItems - .Filter(i => i.Metadata.MediaType == mediaType) - .GroupBy(i => new { i.Metadata.Title, i.Metadata.SortTitle }) - .CountAsync(), - _ => Task.FromResult(0) - }; + movieData = movieData.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%")); + } - public Task> GetAllByMediaSourceId(int mediaSourceId) => - _dbContext.MediaItems - .Filter(i => i.MediaSourceId == mediaSourceId) - .ToListAsync(); + return episodeData.OfType().Concat(movieData.OfType()).ToListAsync(); + } public async Task Update(MediaItem mediaItem) { _dbContext.MediaItems.Update(mediaItem); return await _dbContext.SaveChangesAsync() > 0; } - - public async Task Delete(int mediaItemId) - { - MediaItem mediaItem = await _dbContext.MediaItems.FindAsync(mediaItemId); - _dbContext.MediaItems.Remove(mediaItem); - await _dbContext.SaveChangesAsync(); - } } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs new file mode 100644 index 00000000..29b7a3a6 --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using Microsoft.EntityFrameworkCore; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Infrastructure.Data.Repositories +{ + public class MovieRepository : IMovieRepository + { + private readonly TvContext _dbContext; + + public MovieRepository(TvContext dbContext) => _dbContext = dbContext; + + public Task> GetMovie(int movieId) => + _dbContext.MovieMediaItems + .Include(m => m.Metadata) + .SingleOrDefaultAsync(m => m.Id == movieId) + .Map(Optional); + + public async Task> GetOrAdd(int mediaSourceId, string path) + { + Option maybeExisting = await _dbContext.MovieMediaItems + .Include(i => i.Metadata) + .SingleOrDefaultAsync(i => i.Path == path); + + return await maybeExisting.Match( + mediaItem => Right(mediaItem).AsTask(), + async () => await AddMovie(mediaSourceId, path)); + } + + public async Task Update(MovieMediaItem movie) + { + _dbContext.MovieMediaItems.Update(movie); + return await _dbContext.SaveChangesAsync() > 0; + } + + public Task GetMovieCount() => + _dbContext.MovieMediaItems + .AsNoTracking() + .CountAsync(); + + public Task> GetPagedMovies(int pageNumber, int pageSize) => + _dbContext.MovieMediaItems + .Include(s => s.Metadata) + .OrderBy(s => s.Metadata.SortTitle) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .AsNoTracking() + .ToListAsync(); + + private async Task> AddMovie(int mediaSourceId, string path) + { + try + { + var movie = new MovieMediaItem { MediaSourceId = mediaSourceId, Path = path }; + await _dbContext.MovieMediaItems.AddAsync(movie); + await _dbContext.SaveChangesAsync(); + return movie; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + } +} diff --git a/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs index 5b24b0b8..42e3657d 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); @@ -55,7 +58,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public Task> GetPlayoutItems(int playoutId) => _dbContext.PlayoutItems .Include(i => i.MediaItem) - .ThenInclude(mi => mi.Metadata) + .ThenInclude(m => (m as MovieMediaItem).Metadata) + .Include(i => i.MediaItem) + .ThenInclude(m => (m as TelevisionEpisodeMediaItem).Metadata) .Filter(i => i.PlayoutId == playoutId) .ToListAsync(); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/ProgramScheduleRepository.cs index 7cb8150e..315abeaa 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 new file mode 100644 index 00000000..8b7f3d42 --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using Microsoft.EntityFrameworkCore; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Infrastructure.Data.Repositories +{ + public class TelevisionRepository : ITelevisionRepository + { + private readonly TvContext _dbContext; + + public TelevisionRepository(TvContext dbContext) => _dbContext = dbContext; + + public async Task Update(TelevisionShow show) + { + _dbContext.TelevisionShows.Update(show); + return await _dbContext.SaveChangesAsync() > 0; + } + + public async Task Update(TelevisionSeason season) + { + _dbContext.TelevisionSeasons.Update(season); + return await _dbContext.SaveChangesAsync() > 0; + } + + public async Task Update(TelevisionEpisodeMediaItem episode) + { + _dbContext.TelevisionEpisodeMediaItems.Update(episode); + return await _dbContext.SaveChangesAsync() > 0; + } + + public Task> GetAllShows() => + _dbContext.TelevisionShows + .AsNoTracking() + .Include(s => s.Metadata) + .ToListAsync(); + + public Task> GetShow(int televisionShowId) => + _dbContext.TelevisionShows + .AsNoTracking() + .Filter(s => s.Id == televisionShowId) + .Include(s => s.Metadata) + .SingleOrDefaultAsync() + .Map(Optional); + + public Task GetShowCount() => + _dbContext.TelevisionShows + .AsNoTracking() + .CountAsync(); + + public Task> GetPagedShows(int pageNumber, int pageSize) => + _dbContext.TelevisionShows + .AsNoTracking() + .Include(s => s.Metadata) + .OrderBy(s => s.Metadata == null ? string.Empty : s.Metadata.SortTitle) + .Skip((pageNumber - 1) * pageSize) + .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() + .Include(s => s.TelevisionShow) + .ThenInclude(s => s.Metadata) + .SingleOrDefaultAsync(s => s.Id == televisionSeasonId) + .Map(Optional); + + public Task GetSeasonCount(int televisionShowId) => + _dbContext.TelevisionSeasons + .AsNoTracking() + .Where(s => s.TelevisionShowId == televisionShowId) + .CountAsync(); + + public Task> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) => + _dbContext.TelevisionSeasons + .AsNoTracking() + .Where(s => s.TelevisionShowId == televisionShowId) + .Include(s => s.TelevisionShow) + .ThenInclude(s => s.Metadata) + .OrderBy(s => s.Number) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + public Task> GetEpisode(int televisionEpisodeId) => + _dbContext.TelevisionEpisodeMediaItems + .AsNoTracking() + .Include(s => s.Season) + .Include(s => s.Metadata) + .SingleOrDefaultAsync(s => s.Id == televisionEpisodeId) + .Map(Optional); + + public Task GetEpisodeCount(int televisionSeasonId) => + _dbContext.TelevisionEpisodeMediaItems + .AsNoTracking() + .Where(e => e.SeasonId == televisionSeasonId) + .CountAsync(); + + public Task> GetPagedEpisodes( + int televisionSeasonId, + int pageNumber, + int pageSize) => + _dbContext.TelevisionEpisodeMediaItems + .AsNoTracking() + .Include(e => e.Metadata) + .Include(e => e.Season) + .ThenInclude(s => s.TelevisionShow) + .ThenInclude(s => s.Metadata) + .Where(e => e.SeasonId == televisionSeasonId) + .OrderBy(s => s.Metadata.Episode) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + public async Task> GetShowByPath(int mediaSourceId, string path) + { + Option maybeShowId = await _dbContext.LocalTelevisionShowSources + .SingleOrDefaultAsync(s => s.MediaSourceId == mediaSourceId && s.Path == path) + .Map(Optional) + .MapT(s => s.TelevisionShowId); + + return await maybeShowId.Match>>( + async id => await _dbContext.TelevisionShows + .Include(s => s.Metadata) + .Include(s => s.Sources) + .SingleOrDefaultAsync(s => s.Id == id), + () => Task.FromResult(Option.None)); + } + + public async Task> GetShowByMetadata(TelevisionShowMetadata metadata) + { + Option maybeShow = await _dbContext.TelevisionShows + .Include(s => s.Metadata) + .Where(s => s.Metadata.Title == metadata.Title && s.Metadata.Year == metadata.Year) + .SingleOrDefaultAsync() + .Map(Optional); + + await maybeShow.IfSomeAsync( + async show => + { + await _dbContext.Entry(show).Reference(s => s.Metadata).LoadAsync(); + await _dbContext.Entry(show).Collection(s => s.Sources).LoadAsync(); + }); + + return maybeShow; + } + + public async Task> AddShow( + int localMediaSourceId, + string showFolder, + TelevisionShowMetadata metadata) + { + try + { + var show = new TelevisionShow + { + Sources = new List(), + Metadata = metadata, + Seasons = new List() + }; + + show.Sources.Add( + new LocalTelevisionShowSource + { + MediaSourceId = localMediaSourceId, + Path = showFolder, + TelevisionShow = show + }); + + await _dbContext.TelevisionShows.AddAsync(show); + await _dbContext.SaveChangesAsync(); + + return show; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + public async Task> GetOrAddSeason( + TelevisionShow show, + string path, + int seasonNumber) + { + Option maybeExisting = await _dbContext.TelevisionSeasons + .SingleOrDefaultAsync(i => i.Path == path); + + return await maybeExisting.Match( + season => Right(season).AsTask(), + () => AddSeason(show, path, seasonNumber)); + } + + public async Task> GetOrAddEpisode( + TelevisionSeason season, + int mediaSourceId, + string path) + { + Option maybeExisting = await _dbContext.TelevisionEpisodeMediaItems + .Include(i => i.Metadata) + .Include(i => i.Statistics) + .SingleOrDefaultAsync(i => i.Path == path); + + return await maybeExisting.Match( + episode => Right(episode).AsTask(), + () => AddEpisode(season, mediaSourceId, path)); + } + + public Task DeleteMissingSources(int localMediaSourceId, List allFolders) => + _dbContext.LocalTelevisionShowSources + .Where(s => s.MediaSourceId == localMediaSourceId && !allFolders.Contains(s.Path)) + .ToListAsync() + .Bind( + list => + { + _dbContext.LocalTelevisionShowSources.RemoveRange(list); + return _dbContext.SaveChangesAsync(); + }) + .ToUnit(); + + public Task DeleteEmptyShows() => + _dbContext.TelevisionShows + .Where(s => s.Sources.Count == 0) + .ToListAsync() + .Bind( + list => + { + _dbContext.TelevisionShows.RemoveRange(list); + return _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, + string path, + int seasonNumber) + { + try + { + var season = new TelevisionSeason + { + TelevisionShowId = show.Id, Path = path, Number = seasonNumber, + Episodes = new List() + }; + await _dbContext.TelevisionSeasons.AddAsync(season); + await _dbContext.SaveChangesAsync(); + return season; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + private async Task> AddEpisode( + TelevisionSeason season, + int mediaSourceId, + string path) + { + try + { + var episode = new TelevisionEpisodeMediaItem + { + MediaSourceId = mediaSourceId, + SeasonId = season.Id, + Path = path + }; + await _dbContext.TelevisionEpisodeMediaItems.AddAsync(episode); + await _dbContext.SaveChangesAsync(); + return episode; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + } +} diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index 6e881c60..688a8e88 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -19,15 +19,20 @@ namespace ErsatzTV.Infrastructure.Data public DbSet LocalMediaSources { get; set; } public DbSet PlexMediaSources { get; set; } public DbSet MediaItems { get; set; } + public DbSet MovieMediaItems { get; set; } + public DbSet TelevisionEpisodeMediaItems { get; set; } public DbSet MediaCollections { get; set; } public DbSet SimpleMediaCollections { get; set; } - public DbSet TelevisionMediaCollections { get; set; } public DbSet ProgramSchedules { get; set; } public DbSet Playouts { get; set; } public DbSet PlayoutItems { get; set; } public DbSet PlayoutProgramScheduleItemAnchors { get; set; } public DbSet FFmpegProfiles { get; set; } public DbSet Resolutions { get; set; } + public DbSet TelevisionShows { get; set; } + public DbSet LocalTelevisionShowSources { get; set; } + public DbSet TelevisionShowMetadata { get; set; } + public DbSet TelevisionSeasons { get; set; } // support raw sql queries public DbSet MediaCollectionSummaries { get; set; } diff --git a/ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.Designer.cs new file mode 100644 index 00000000..5951589c --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.Designer.cs @@ -0,0 +1,1224 @@ +// +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("20210219165123_TelevisionExpansion")] + partial class TelevisionExpansion + { + 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("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("PlayoutId", "ProgramScheduleId", "MediaCollectionId"); + + b.HasIndex("MediaCollectionId"); + + 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("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaCollectionId"); + + b.HasIndex("ProgramScheduleId"); + + 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("MediaItemSimpleMediaCollection", b => + { + b.Property("ItemsId") + .HasColumnType("INTEGER"); + + b.Property("SimpleMediaCollectionsId") + .HasColumnType("INTEGER"); + + b.HasKey("ItemsId", "SimpleMediaCollectionsId"); + + b.HasIndex("SimpleMediaCollectionsId"); + + b.ToTable("MediaItemSimpleMediaCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection"); + + b.ToTable("SimpleMediaCollections"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionMediaCollection", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowTitle") + .HasColumnType("TEXT"); + + b.HasIndex("ShowTitle", "SeasonNumber") + .IsUnique(); + + b.ToTable("TelevisionMediaCollections"); + }); + + 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.MediaCollection", "MediaCollection") + .WithMany() + .HasForeignKey("MediaCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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("PlayoutProgramScheduleAnchorPlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("PlayoutProgramScheduleAnchorProgramScheduleId") + .HasColumnType("INTEGER"); + + b1.Property("PlayoutProgramScheduleAnchorMediaCollectionId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId"); + + b1.ToTable("PlayoutProgramScheduleItemAnchors"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId"); + }); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaCollection"); + + 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") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaCollection"); + + b.Navigation("ProgramSchedule"); + }); + + 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("MediaItemSimpleMediaCollection", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null) + .WithMany() + .HasForeignKey("SimpleMediaCollectionsId") + .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.TelevisionMediaCollection", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.TelevisionMediaCollection", "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/20210219165123_TelevisionExpansion.cs b/ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.cs new file mode 100644 index 00000000..581b0537 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.cs @@ -0,0 +1,453 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class TelevisionExpansion : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + "Metadata_Aired", + "MediaItems"); + + migrationBuilder.DropColumn( + "Metadata_ContentRating", + "MediaItems"); + + migrationBuilder.DropColumn( + "Metadata_Description", + "MediaItems"); + + migrationBuilder.DropColumn( + "Metadata_EpisodeNumber", + "MediaItems"); + + migrationBuilder.DropColumn( + "Metadata_MediaType", + "MediaItems"); + + migrationBuilder.DropColumn( + "Metadata_SeasonNumber", + "MediaItems"); + + migrationBuilder.DropColumn( + "Metadata_SortTitle", + "MediaItems"); + + migrationBuilder.DropColumn( + "Metadata_Source", + "MediaItems"); + + migrationBuilder.DropColumn( + "Metadata_Subtitle", + "MediaItems"); + + migrationBuilder.DropColumn( + "Metadata_Title", + "MediaItems"); + + migrationBuilder.RenameColumn( + "Metadata_Width", + "MediaItems", + "Statistics_Width"); + + migrationBuilder.RenameColumn( + "Metadata_VideoScanType", + "MediaItems", + "Statistics_VideoScanType"); + + migrationBuilder.RenameColumn( + "Metadata_VideoCodec", + "MediaItems", + "Statistics_VideoCodec"); + + migrationBuilder.RenameColumn( + "Metadata_SampleAspectRatio", + "MediaItems", + "Statistics_SampleAspectRatio"); + + migrationBuilder.RenameColumn( + "Metadata_LastWriteTime", + "MediaItems", + "Statistics_LastWriteTime"); + + migrationBuilder.RenameColumn( + "Metadata_Height", + "MediaItems", + "Statistics_Height"); + + migrationBuilder.RenameColumn( + "Metadata_Duration", + "MediaItems", + "Statistics_Duration"); + + migrationBuilder.RenameColumn( + "Metadata_DisplayAspectRatio", + "MediaItems", + "Statistics_DisplayAspectRatio"); + + migrationBuilder.RenameColumn( + "Metadata_AudioCodec", + "MediaItems", + "Statistics_AudioCodec"); + + migrationBuilder.CreateTable( + "Movies", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + MetadataId = table.Column("INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Movies", x => x.Id); + table.ForeignKey( + "FK_Movies_MediaItems_Id", + x => x.Id, + "MediaItems", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "TelevisionShows", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Poster = table.Column("TEXT", nullable: true), + PosterLastWriteTime = table.Column("TEXT", nullable: true) + }, + constraints: table => { table.PrimaryKey("PK_TelevisionShows", x => x.Id); }); + + migrationBuilder.CreateTable( + "MovieMetadata", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + MovieId = table.Column("INTEGER", nullable: false), + Year = table.Column("INTEGER", nullable: true), + Premiered = table.Column("TEXT", nullable: true), + Plot = table.Column("TEXT", nullable: true), + Outline = table.Column("TEXT", nullable: true), + Tagline = table.Column("TEXT", nullable: true), + ContentRating = table.Column("TEXT", nullable: true), + Source = table.Column("INTEGER", nullable: false), + LastWriteTime = table.Column("TEXT", nullable: true), + Title = table.Column("TEXT", nullable: true), + SortTitle = table.Column("TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MovieMetadata", x => x.Id); + table.ForeignKey( + "FK_MovieMetadata_Movies_MovieId", + x => x.MovieId, + "Movies", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "TelevisionSeasons", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TelevisionShowId = table.Column("INTEGER", nullable: false), + Number = table.Column("INTEGER", nullable: false), + Path = table.Column("TEXT", nullable: true), + Poster = table.Column("TEXT", nullable: true), + PosterLastWriteTime = table.Column("TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TelevisionSeasons", x => x.Id); + table.ForeignKey( + "FK_TelevisionSeasons_TelevisionShows_TelevisionShowId", + x => x.TelevisionShowId, + "TelevisionShows", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "TelevisionShowMetadata", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TelevisionShowId = table.Column("INTEGER", nullable: false), + Source = table.Column("INTEGER", nullable: false), + LastWriteTime = table.Column("TEXT", nullable: true), + Title = table.Column("TEXT", nullable: true), + SortTitle = table.Column("TEXT", nullable: true), + Year = table.Column("INTEGER", nullable: true), + Plot = table.Column("TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TelevisionShowMetadata", x => x.Id); + table.ForeignKey( + "FK_TelevisionShowMetadata_TelevisionShows_TelevisionShowId", + x => x.TelevisionShowId, + "TelevisionShows", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "TelevisionShowSource", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TelevisionShowId = table.Column("INTEGER", nullable: false), + Discriminator = table.Column("TEXT", nullable: false), + MediaSourceId = table.Column("INTEGER", nullable: true), + Path = table.Column("TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TelevisionShowSource", x => x.Id); + table.ForeignKey( + "FK_TelevisionShowSource_LocalMediaSources_MediaSourceId", + x => x.MediaSourceId, + "LocalMediaSources", + "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + "FK_TelevisionShowSource_TelevisionShows_TelevisionShowId", + x => x.TelevisionShowId, + "TelevisionShows", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "TelevisionEpisodes", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SeasonId = table.Column("INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TelevisionEpisodes", x => x.Id); + table.ForeignKey( + "FK_TelevisionEpisodes_MediaItems_Id", + x => x.Id, + "MediaItems", + "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + "FK_TelevisionEpisodes_TelevisionSeasons_SeasonId", + x => x.SeasonId, + "TelevisionSeasons", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "TelevisionEpisodeMetadata", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TelevisionEpisodeId = table.Column("INTEGER", nullable: false), + Season = table.Column("INTEGER", nullable: false), + Episode = table.Column("INTEGER", nullable: false), + Plot = table.Column("TEXT", nullable: true), + Aired = table.Column("TEXT", nullable: true), + Source = table.Column("INTEGER", nullable: false), + LastWriteTime = table.Column("TEXT", nullable: true), + Title = table.Column("TEXT", nullable: true), + SortTitle = table.Column("TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TelevisionEpisodeMetadata", x => x.Id); + table.ForeignKey( + "FK_TelevisionEpisodeMetadata_TelevisionEpisodes_TelevisionEpisodeId", + x => x.TelevisionEpisodeId, + "TelevisionEpisodes", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + "IX_MovieMetadata_MovieId", + "MovieMetadata", + "MovieId", + unique: true); + + migrationBuilder.CreateIndex( + "IX_TelevisionEpisodeMetadata_TelevisionEpisodeId", + "TelevisionEpisodeMetadata", + "TelevisionEpisodeId", + unique: true); + + migrationBuilder.CreateIndex( + "IX_TelevisionEpisodes_SeasonId", + "TelevisionEpisodes", + "SeasonId"); + + migrationBuilder.CreateIndex( + "IX_TelevisionSeasons_TelevisionShowId", + "TelevisionSeasons", + "TelevisionShowId"); + + migrationBuilder.CreateIndex( + "IX_TelevisionShowMetadata_TelevisionShowId", + "TelevisionShowMetadata", + "TelevisionShowId", + unique: true); + + migrationBuilder.CreateIndex( + "IX_TelevisionShowSource_MediaSourceId", + "TelevisionShowSource", + "MediaSourceId"); + + migrationBuilder.CreateIndex( + "IX_TelevisionShowSource_TelevisionShowId", + "TelevisionShowSource", + "TelevisionShowId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + "MovieMetadata"); + + migrationBuilder.DropTable( + "TelevisionEpisodeMetadata"); + + migrationBuilder.DropTable( + "TelevisionShowMetadata"); + + migrationBuilder.DropTable( + "TelevisionShowSource"); + + migrationBuilder.DropTable( + "Movies"); + + migrationBuilder.DropTable( + "TelevisionEpisodes"); + + migrationBuilder.DropTable( + "TelevisionSeasons"); + + migrationBuilder.DropTable( + "TelevisionShows"); + + migrationBuilder.RenameColumn( + "Statistics_Width", + "MediaItems", + "Metadata_Width"); + + migrationBuilder.RenameColumn( + "Statistics_VideoScanType", + "MediaItems", + "Metadata_VideoScanType"); + + migrationBuilder.RenameColumn( + "Statistics_VideoCodec", + "MediaItems", + "Metadata_VideoCodec"); + + migrationBuilder.RenameColumn( + "Statistics_SampleAspectRatio", + "MediaItems", + "Metadata_SampleAspectRatio"); + + migrationBuilder.RenameColumn( + "Statistics_LastWriteTime", + "MediaItems", + "Metadata_LastWriteTime"); + + migrationBuilder.RenameColumn( + "Statistics_Height", + "MediaItems", + "Metadata_Height"); + + migrationBuilder.RenameColumn( + "Statistics_Duration", + "MediaItems", + "Metadata_Duration"); + + migrationBuilder.RenameColumn( + "Statistics_DisplayAspectRatio", + "MediaItems", + "Metadata_DisplayAspectRatio"); + + migrationBuilder.RenameColumn( + "Statistics_AudioCodec", + "MediaItems", + "Metadata_AudioCodec"); + + migrationBuilder.AddColumn( + "Metadata_Aired", + "MediaItems", + "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + "Metadata_ContentRating", + "MediaItems", + "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + "Metadata_Description", + "MediaItems", + "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + "Metadata_EpisodeNumber", + "MediaItems", + "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + "Metadata_MediaType", + "MediaItems", + "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + "Metadata_SeasonNumber", + "MediaItems", + "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + "Metadata_SortTitle", + "MediaItems", + "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + "Metadata_Source", + "MediaItems", + "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + "Metadata_Subtitle", + "MediaItems", + "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + "Metadata_Title", + "MediaItems", + "TEXT", + nullable: true); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.Designer.cs new file mode 100644 index 00000000..ee59dd78 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.Designer.cs @@ -0,0 +1,1289 @@ +// +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("20210220003018_CollectionsRework")] + partial class CollectionsRework + { + 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("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("PlayoutId", "ProgramScheduleId", "MediaCollectionId"); + + b.HasIndex("MediaCollectionId"); + + 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("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaCollectionId"); + + b.HasIndex("ProgramScheduleId"); + + 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.MediaCollection", "MediaCollection") + .WithMany() + .HasForeignKey("MediaCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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("PlayoutProgramScheduleAnchorPlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("PlayoutProgramScheduleAnchorProgramScheduleId") + .HasColumnType("INTEGER"); + + b1.Property("PlayoutProgramScheduleAnchorMediaCollectionId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId"); + + b1.ToTable("PlayoutProgramScheduleItemAnchors"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId"); + }); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaCollection"); + + 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") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaCollection"); + + b.Navigation("ProgramSchedule"); + }); + + 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/20210220003018_CollectionsRework.cs b/ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.cs new file mode 100644 index 00000000..c27d17cc --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210220003018_CollectionsRework.cs @@ -0,0 +1,212 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class CollectionsRework : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + "MediaItemSimpleMediaCollection"); + + migrationBuilder.DropTable( + "TelevisionMediaCollections"); + + migrationBuilder.CreateTable( + "SimpleMediaCollectionEpisodes", + table => new + { + SimpleMediaCollectionsId = table.Column("INTEGER", nullable: false), + TelevisionEpisodesId = table.Column("INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey( + "PK_SimpleMediaCollectionEpisodes", + x => new { x.SimpleMediaCollectionsId, x.TelevisionEpisodesId }); + table.ForeignKey( + "FK_SimpleMediaCollectionEpisodes_SimpleMediaCollections_SimpleMediaCollectionsId", + x => x.SimpleMediaCollectionsId, + "SimpleMediaCollections", + "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + "FK_SimpleMediaCollectionEpisodes_TelevisionEpisodes_TelevisionEpisodesId", + x => x.TelevisionEpisodesId, + "TelevisionEpisodes", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "SimpleMediaCollectionMovies", + table => new + { + MoviesId = table.Column("INTEGER", nullable: false), + SimpleMediaCollectionsId = table.Column("INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey( + "PK_SimpleMediaCollectionMovies", + x => new { x.MoviesId, x.SimpleMediaCollectionsId }); + table.ForeignKey( + "FK_SimpleMediaCollectionMovies_Movies_MoviesId", + x => x.MoviesId, + "Movies", + "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + "FK_SimpleMediaCollectionMovies_SimpleMediaCollections_SimpleMediaCollectionsId", + x => x.SimpleMediaCollectionsId, + "SimpleMediaCollections", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "SimpleMediaCollectionSeasons", + table => new + { + SimpleMediaCollectionsId = table.Column("INTEGER", nullable: false), + TelevisionSeasonsId = table.Column("INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey( + "PK_SimpleMediaCollectionSeasons", + x => new { x.SimpleMediaCollectionsId, x.TelevisionSeasonsId }); + table.ForeignKey( + "FK_SimpleMediaCollectionSeasons_SimpleMediaCollections_SimpleMediaCollectionsId", + x => x.SimpleMediaCollectionsId, + "SimpleMediaCollections", + "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + "FK_SimpleMediaCollectionSeasons_TelevisionSeasons_TelevisionSeasonsId", + x => x.TelevisionSeasonsId, + "TelevisionSeasons", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "SimpleMediaCollectionShows", + table => new + { + SimpleMediaCollectionsId = table.Column("INTEGER", nullable: false), + TelevisionShowsId = table.Column("INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey( + "PK_SimpleMediaCollectionShows", + x => new { x.SimpleMediaCollectionsId, x.TelevisionShowsId }); + table.ForeignKey( + "FK_SimpleMediaCollectionShows_SimpleMediaCollections_SimpleMediaCollectionsId", + x => x.SimpleMediaCollectionsId, + "SimpleMediaCollections", + "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + "FK_SimpleMediaCollectionShows_TelevisionShows_TelevisionShowsId", + x => x.TelevisionShowsId, + "TelevisionShows", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + "IX_SimpleMediaCollectionEpisodes_TelevisionEpisodesId", + "SimpleMediaCollectionEpisodes", + "TelevisionEpisodesId"); + + migrationBuilder.CreateIndex( + "IX_SimpleMediaCollectionMovies_SimpleMediaCollectionsId", + "SimpleMediaCollectionMovies", + "SimpleMediaCollectionsId"); + + migrationBuilder.CreateIndex( + "IX_SimpleMediaCollectionSeasons_TelevisionSeasonsId", + "SimpleMediaCollectionSeasons", + "TelevisionSeasonsId"); + + migrationBuilder.CreateIndex( + "IX_SimpleMediaCollectionShows_TelevisionShowsId", + "SimpleMediaCollectionShows", + "TelevisionShowsId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + "SimpleMediaCollectionEpisodes"); + + migrationBuilder.DropTable( + "SimpleMediaCollectionMovies"); + + migrationBuilder.DropTable( + "SimpleMediaCollectionSeasons"); + + migrationBuilder.DropTable( + "SimpleMediaCollectionShows"); + + migrationBuilder.CreateTable( + "MediaItemSimpleMediaCollection", + table => new + { + ItemsId = table.Column("INTEGER", nullable: false), + SimpleMediaCollectionsId = table.Column("INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey( + "PK_MediaItemSimpleMediaCollection", + x => new { x.ItemsId, x.SimpleMediaCollectionsId }); + table.ForeignKey( + "FK_MediaItemSimpleMediaCollection_MediaItems_ItemsId", + x => x.ItemsId, + "MediaItems", + "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + "FK_MediaItemSimpleMediaCollection_SimpleMediaCollections_SimpleMediaCollectionsId", + x => x.SimpleMediaCollectionsId, + "SimpleMediaCollections", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "TelevisionMediaCollections", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SeasonNumber = table.Column("INTEGER", nullable: true), + ShowTitle = table.Column("TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TelevisionMediaCollections", x => x.Id); + table.ForeignKey( + "FK_TelevisionMediaCollections_MediaCollections_Id", + x => x.Id, + "MediaCollections", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + "IX_MediaItemSimpleMediaCollection_SimpleMediaCollectionsId", + "MediaItemSimpleMediaCollection", + "SimpleMediaCollectionsId"); + + migrationBuilder.CreateIndex( + "IX_TelevisionMediaCollections_ShowTitle_SeasonNumber", + "TelevisionMediaCollections", + new[] { "ShowTitle", "SeasonNumber" }, + unique: true); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210220220723_ScheduleCollectionTypes.Designer.cs new file mode 100644 index 00000000..cc378097 --- /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 00000000..22c5aabe --- /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/20210221215810_RemoveScheduleItemsAndPosters.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210221215810_RemoveScheduleItemsAndPosters.Designer.cs new file mode 100644 index 00000000..43d14bf0 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210221215810_RemoveScheduleItemsAndPosters.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("20210221215810_RemoveScheduleItemsAndPosters")] + partial class RemoveScheduleItemsAndPosters + { + 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/20210221215810_RemoveScheduleItemsAndPosters.cs b/ErsatzTV.Infrastructure/Migrations/20210221215810_RemoveScheduleItemsAndPosters.cs new file mode 100644 index 00000000..d57bdccd --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210221215810_RemoveScheduleItemsAndPosters.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class RemoveScheduleItemsAndPosters : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // delete program schedule items that referenced television collections (that no longer exist) + migrationBuilder.Sql( + "delete from ProgramScheduleItems where MediaCollectionId not in (select Id from SimpleMediaCollections)"); + + // delete television collections that no longer exist/work + migrationBuilder.Sql( + "delete from MediaCollections where Id not in (select Id from SimpleMediaCollections)"); + + // delete all posters so they are all re-cached with a higher resolution + migrationBuilder.Sql("update MediaItems set Poster = null"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 7f072691..ebc91a0a 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -22,6 +22,8 @@ namespace ErsatzTV.Infrastructure.Migrations { b.Property("Id") .HasColumnType("INTEGER"); + + b.ToView("No table or view exists for GenericIntegerId"); }); modelBuilder.Entity( @@ -39,6 +41,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("Name") .HasColumnType("TEXT"); + + b.ToView("No table or view exists for MediaCollectionSummary"); }); modelBuilder.Entity( @@ -59,6 +63,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("Title") .HasColumnType("TEXT"); + + b.ToView("No table or view exists for MediaItemSummary"); }); modelBuilder.Entity( @@ -256,6 +262,55 @@ namespace ErsatzTV.Infrastructure.Migrations 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 => @@ -315,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"); @@ -414,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") @@ -426,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"); }); @@ -458,20 +533,224 @@ namespace ErsatzTV.Infrastructure.Migrations }); modelBuilder.Entity( - "MediaItemSimpleMediaCollection", + "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("ItemsId") + 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("ItemsId", "SimpleMediaCollectionsId"); + b.HasKey("MoviesId", "SimpleMediaCollectionsId"); b.HasIndex("SimpleMediaCollectionsId"); - b.ToTable("MediaItemSimpleMediaCollection"); + 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( @@ -484,21 +763,29 @@ namespace ErsatzTV.Infrastructure.Migrations }); modelBuilder.Entity( - "ErsatzTV.Core.Domain.TelevisionMediaCollection", + "ErsatzTV.Core.Domain.MovieMediaItem", b => { - b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection"); + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); - b.Property("SeasonNumber") + b.Property("MetadataId") .HasColumnType("INTEGER"); - b.Property("ShowTitle") - .HasColumnType("TEXT"); + b.ToTable("Movies"); + }); - b.HasIndex("ShowTitle", "SeasonNumber") - .IsUnique(); + modelBuilder.Entity( + "ErsatzTV.Core.Domain.TelevisionEpisodeMediaItem", + b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); - b.ToTable("TelevisionMediaCollections"); + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("TelevisionEpisodes"); }); modelBuilder.Entity( @@ -576,6 +863,23 @@ namespace ErsatzTV.Infrastructure.Migrations 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 => @@ -613,61 +917,31 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); b.OwnsOne( - "ErsatzTV.Core.Domain.MediaMetadata", - "Metadata", + "ErsatzTV.Core.Domain.MediaItemStatistics", + "Statistics", b1 => { b1.Property("MediaItemId") .HasColumnType("INTEGER"); - b1.Property("Aired") - .HasColumnType("TEXT"); - b1.Property("AudioCodec") .HasColumnType("TEXT"); - b1.Property("ContentRating") - .HasColumnType("TEXT"); - - b1.Property("Description") - .HasColumnType("TEXT"); - b1.Property("DisplayAspectRatio") .HasColumnType("TEXT"); b1.Property("Duration") .HasColumnType("TEXT"); - b1.Property("EpisodeNumber") - .HasColumnType("INTEGER"); - b1.Property("Height") .HasColumnType("INTEGER"); b1.Property("LastWriteTime") .HasColumnType("TEXT"); - b1.Property("MediaType") - .HasColumnType("INTEGER"); - b1.Property("SampleAspectRatio") .HasColumnType("TEXT"); - b1.Property("SeasonNumber") - .HasColumnType("INTEGER"); - - b1.Property("SortTitle") - .HasColumnType("TEXT"); - - b1.Property("Source") - .HasColumnType("INTEGER"); - - b1.Property("Subtitle") - .HasColumnType("TEXT"); - - b1.Property("Title") - .HasColumnType("TEXT"); - b1.Property("VideoCodec") .HasColumnType("TEXT"); @@ -685,9 +959,22 @@ namespace ErsatzTV.Infrastructure.Migrations .HasForeignKey("MediaItemId"); }); - b.Navigation("Metadata"); - 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( @@ -770,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") @@ -793,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") @@ -808,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"); @@ -855,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") @@ -865,18 +1130,82 @@ 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( - "MediaItemSimpleMediaCollection", + "ErsatzTV.Core.Domain.TelevisionEpisodeMetadata", b => { - b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + 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("ItemsId") + .HasForeignKey("MoviesId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -887,6 +1216,57 @@ namespace ErsatzTV.Infrastructure.Migrations .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 => @@ -899,14 +1279,33 @@ namespace ErsatzTV.Infrastructure.Migrations }); modelBuilder.Entity( - "ErsatzTV.Core.Domain.TelevisionMediaCollection", + "ErsatzTV.Core.Domain.MovieMediaItem", b => { - b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null) + 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.TelevisionMediaCollection", "Id") + .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( @@ -975,6 +1374,19 @@ namespace ErsatzTV.Infrastructure.Migrations .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( @@ -995,6 +1407,23 @@ namespace ErsatzTV.Infrastructure.Migrations 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 => diff --git a/ErsatzTV.sln.DotSettings b/ErsatzTV.sln.DotSettings index 59acb62f..b3fe1e0f 100644 --- a/ErsatzTV.sln.DotSettings +++ b/ErsatzTV.sln.DotSettings @@ -1,9 +1,11 @@  + True DTO HDHR SAR True True + True True True True @@ -13,6 +15,7 @@ True True True + True True True True @@ -35,4 +38,5 @@ True True True + True True \ No newline at end of file diff --git a/ErsatzTV/Controllers/Api/MediaCollectionsController.cs b/ErsatzTV/Controllers/Api/MediaCollectionsController.cs index dfda3dc4..5c2e6c1c 100644 --- a/ErsatzTV/Controllers/Api/MediaCollectionsController.cs +++ b/ErsatzTV/Controllers/Api/MediaCollectionsController.cs @@ -44,14 +44,5 @@ namespace ErsatzTV.Controllers.Api [ProducesResponseType(404)] public Task GetItems(int id) => _mediator.Send(new GetSimpleMediaCollectionItems(id)).ToActionResult(); - - [HttpPut("{id}/items")] - [ProducesResponseType(typeof(IEnumerable), 200)] - [ProducesResponseType(404)] - public Task PutItems( - int id, - [Required] [FromBody] - List mediaItemIds) => - _mediator.Send(new ReplaceSimpleMediaCollectionItems(id, mediaItemIds)).ToActionResult(); } } diff --git a/ErsatzTV/Controllers/Api/MediaItemsController.cs b/ErsatzTV/Controllers/Api/MediaItemsController.cs index a0ae0620..a6f3779b 100644 --- a/ErsatzTV/Controllers/Api/MediaItemsController.cs +++ b/ErsatzTV/Controllers/Api/MediaItemsController.cs @@ -1,8 +1,6 @@ using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using ErsatzTV.Application.MediaItems; -using ErsatzTV.Application.MediaItems.Commands; using ErsatzTV.Application.MediaItems.Queries; using ErsatzTV.Extensions; using MediatR; @@ -19,14 +17,6 @@ namespace ErsatzTV.Controllers.Api public MediaItemsController(IMediator mediator) => _mediator = mediator; - [HttpPost] - [ProducesResponseType(typeof(MediaItemViewModel), 200)] - [ProducesResponseType(400)] - public Task Add( - [Required] [FromBody] - CreateMediaItem createMediaItem) => - _mediator.Send(createMediaItem).ToActionResult(); - [HttpGet("{mediaItemId}")] [ProducesResponseType(typeof(MediaItemViewModel), 200)] [ProducesResponseType(404)] @@ -37,13 +27,5 @@ namespace ErsatzTV.Controllers.Api [ProducesResponseType(typeof(IEnumerable), 200)] public Task GetAll() => _mediator.Send(new GetAllMediaItems()).ToActionResult(); - - [HttpDelete] - [ProducesResponseType(200)] - [ProducesResponseType(400)] - public Task Delete( - [Required] [FromBody] - DeleteMediaItem deleteMediaItem) => - _mediator.Send(deleteMediaItem).ToActionResult(); } } diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index 4ebab80a..96955e2b 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/Channels.razor b/ErsatzTV/Pages/Channels.razor index b5da26a8..c88e1c0c 100644 --- a/ErsatzTV/Pages/Channels.razor +++ b/ErsatzTV/Pages/Channels.razor @@ -7,61 +7,63 @@ @inject IDialogService Dialog @inject IMediator Mediator - - - Channels - - - - - - - - - - - - Number - - Logo - - Name - - Streaming Mode - FFmpeg Profile - - - - @context.Number - - @if (!string.IsNullOrWhiteSpace(context.Logo)) - { - - } - - @context.Name - @context.StreamingMode - - @if (context.StreamingMode == StreamingMode.TransportStream) - { - @_ffmpegProfiles.Find(p => p.Id == context.FFmpegProfileId)?.Name - } - - - - - Edit - - - Delete - - - - - - - Add Channel - + + + + Channels + + + + + + + + + + + + Number + + Logo + + Name + + Streaming Mode + FFmpeg Profile + + + + @context.Number + + @if (!string.IsNullOrWhiteSpace(context.Logo)) + { + + } + + @context.Name + @context.StreamingMode + + @if (context.StreamingMode == StreamingMode.TransportStream) + { + @_ffmpegProfiles.Find(p => p.Id == context.FFmpegProfileId)?.Name + } + + + + + Edit + + + Delete + + + + + + + Add Channel + + @code { private List _channels; diff --git a/ErsatzTV/Pages/FFmpeg.razor b/ErsatzTV/Pages/FFmpeg.razor index e3698837..c4860533 100644 --- a/ErsatzTV/Pages/FFmpeg.razor +++ b/ErsatzTV/Pages/FFmpeg.razor @@ -5,90 +5,92 @@ @inject IDialogService Dialog @inject IMediator Mediator - - - - FFmpeg Settings - - - - - - - - - - - @foreach (FFmpegProfileViewModel profile in _ffmpegProfiles) - { - @profile.Name - } - - - - - - Save Settings - - + + + + + FFmpeg Settings + + + + + + + + + + + @foreach (FFmpegProfileViewModel profile in _ffmpegProfiles) + { + @profile.Name + } + + + + + + Save Settings + + - - - FFmpeg Profiles - - Colored settings will be normalized - - - - - - - - - - - Name - Transcode - Resolution - Video Codec - Audio Codec - - - - @context.Name - - @(context.Transcode ? "Yes" : "No") - - - - @context.Resolution.Name - - - - - @context.VideoCodec - - - - - @context.AudioCodec - - - - - - Edit - - - Delete - - - - - - - Add Profile - + + + FFmpeg Profiles + + Colored settings will be normalized + + + + + + + + + + + Name + Transcode + Resolution + Video Codec + Audio Codec + + + + @context.Name + + @(context.Transcode ? "Yes" : "No") + + + + @context.Resolution.Name + + + + + @context.VideoCodec + + + + + @context.AudioCodec + + + + + + Edit + + + Delete + + + + + + + Add Profile + + @code { diff --git a/ErsatzTV/Pages/Index.razor b/ErsatzTV/Pages/Index.razor index 1001c7e3..17533ab7 100644 --- a/ErsatzTV/Pages/Index.razor +++ b/ErsatzTV/Pages/Index.razor @@ -30,8 +30,9 @@ Media Collections - Media collections have a name and contain a logical grouping of media items. - Television media collections are automatically created and maintained, while manually-created media collections allow further customization. + Media collections have a name and contain a logical grouping of media items. + Collections may contain television shows, television seasons, television episodes or movies. + Collections containing television shows and television seasons are automatically updated as media is added or removed from the linked shows and seasons. diff --git a/ErsatzTV/Pages/LocalMediaSourceEditor.razor b/ErsatzTV/Pages/LocalMediaSourceEditor.razor index de192ddb..e69f2d04 100644 --- a/ErsatzTV/Pages/LocalMediaSourceEditor.razor +++ b/ErsatzTV/Pages/LocalMediaSourceEditor.razor @@ -1,7 +1,6 @@ @page "/media/sources/local/add" @using ErsatzTV.Application.MediaSources.Commands @using ErsatzTV.Application.MediaSources -@using ErsatzTV.Core.Metadata @inject NavigationManager NavigationManager @inject ILogger Logger @inject ISnackbar Snackbar @@ -17,7 +16,7 @@ - @foreach (MediaType mediaType in Enum.GetValues()) + @foreach (MediaType mediaType in new[] { MediaType.TvShow, MediaType.Movie }) { @mediaType } @@ -88,7 +87,7 @@ { if (Locker.LockMediaSource(vm.Id)) { - await Channel.WriteAsync(new ScanLocalMediaSource(vm.Id, ScanningMode.Default)); + await Channel.WriteAsync(new ScanLocalMediaSource(vm.Id)); NavigationManager.NavigateTo("/media/sources"); } }); diff --git a/ErsatzTV/Pages/Logs.razor b/ErsatzTV/Pages/Logs.razor index 23e43dce..d8e3980e 100644 --- a/ErsatzTV/Pages/Logs.razor +++ b/ErsatzTV/Pages/Logs.razor @@ -3,23 +3,25 @@ @using ErsatzTV.Application.Logs.Queries @inject IMediator Mediator - - - Timestamp - Level - Message - Properties - - - @context.Timestamp - @context.Level - @context.RenderedMessage - @context.Properties - - - - - + + + + Timestamp + Level + Message + Properties + + + @context.Timestamp + @context.Level + @context.RenderedMessage + @context.Properties + + + + + + @code { private List _logEntries; diff --git a/ErsatzTV/Pages/MediaCollectionEditor.razor b/ErsatzTV/Pages/MediaCollectionEditor.razor index 2bd73f8a..ca417b1b 100644 --- a/ErsatzTV/Pages/MediaCollectionEditor.razor +++ b/ErsatzTV/Pages/MediaCollectionEditor.razor @@ -1,4 +1,4 @@ -@page "/media/collections/{Id:int}" +@page "/media/collections/{Id:int}/edit" @page "/media/collections/add" @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections.Commands @@ -75,7 +75,7 @@ Snackbar.Add(error.Value, Severity.Error); Logger.LogError("Error saving simple media collection: {Error}", error.Value); }, - () => NavigationManager.NavigateTo("/media/collections")); + () => NavigationManager.NavigateTo(_model.Id > 0 ? $"/media/collections/{_model.Id}" : "/media/collections")); } } diff --git a/ErsatzTV/Pages/MediaCollectionItems.razor b/ErsatzTV/Pages/MediaCollectionItems.razor new file mode 100644 index 00000000..a5f5569a --- /dev/null +++ b/ErsatzTV/Pages/MediaCollectionItems.razor @@ -0,0 +1,168 @@ +@page "/media/collections/{Id:int}" +@using ErsatzTV.Application.MediaCards +@using ErsatzTV.Application.MediaCards.Queries +@using ErsatzTV.Application.MediaCollections.Commands +@inject NavigationManager NavigationManager +@inject IMediator Mediator +@inject ILogger Logger +@inject ISnackbar Snackbar +@inject IDialogService Dialog + +
+ @_data.Name + +
+ +@if (_data.MovieCards.Any()) +{ + Movies + + + @foreach (MovieCardViewModel card in _data.MovieCards) + { + + } + +} + +@if (_data.ShowCards.Any()) +{ + Television Shows + + + @foreach (TelevisionShowCardViewModel card in _data.ShowCards) + { + + } + +} + +@if (_data.SeasonCards.Any()) +{ + Television Seasons + + + @foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards) + { + + } + +} + +@if (_data.EpisodeCards.Any()) +{ + Television Episodes + + + @foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired)) + { + + } + +} + +@code { + + [Parameter] + public int Id { get; set; } + + private SimpleMediaCollectionCardResultsViewModel _data; + + protected override async Task OnParametersSetAsync() => await RefreshData(); + + private async Task RefreshData() + { + Either maybeResult = + await Mediator.Send(new GetSimpleMediaCollectionCards(Id)); + + maybeResult.Match( + result => _data = result, + error => NavigationManager.NavigateTo("404")); + } + + private async Task RemoveMovieFromCollection(MediaCardViewModel vm) + { + if (vm is MovieCardViewModel movie) + { + var request = new RemoveItemsFromSimpleMediaCollection(Id) + { + MovieIds = new List { movie.MovieId } + }; + + await RemoveItemsWithConfirmation("movie", $"{movie.Title} ({movie.Subtitle})", request); + } + } + + private async Task RemoveShowFromCollection(MediaCardViewModel vm) + { + if (vm is TelevisionShowCardViewModel show) + { + var request = new RemoveItemsFromSimpleMediaCollection(Id) + { + TelevisionShowIds = new List { show.TelevisionShowId } + }; + + await RemoveItemsWithConfirmation("show", $"{show.Title} ({show.Subtitle})", request); + } + } + + private async Task RemoveSeasonFromCollection(MediaCardViewModel vm) + { + if (vm is TelevisionSeasonCardViewModel season) + { + var request = new RemoveItemsFromSimpleMediaCollection(Id) + { + TelevisionSeasonIds = new List { season.TelevisionSeasonId } + }; + + await RemoveItemsWithConfirmation("season", $"{season.ShowTitle} - {season.Title}", request); + } + } + + private async Task RemoveEpisodeFromCollection(MediaCardViewModel vm) + { + if (vm is TelevisionEpisodeCardViewModel episode) + { + var request = new RemoveItemsFromSimpleMediaCollection(Id) + { + TelevisionEpisodeIds = new List { episode.EpisodeId } + }; + + await RemoveItemsWithConfirmation("episode", $"{episode.ShowTitle} - {episode.Title}", request); + } + } + + private async Task RemoveItemsWithConfirmation( + string entityType, + string entityName, + RemoveItemsFromSimpleMediaCollection request) + { + var parameters = new DialogParameters { { "EntityType", entityType }, { "EntityName", entityName } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Remove From Collection", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled) + { + await Mediator.Send(request); + await RefreshData(); + } + } + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/MediaCollectionItemsEditor.razor b/ErsatzTV/Pages/MediaCollectionItemsEditor.razor deleted file mode 100644 index fbe2df0c..00000000 --- a/ErsatzTV/Pages/MediaCollectionItemsEditor.razor +++ /dev/null @@ -1,121 +0,0 @@ -@page "/media/collections/{Id:int}/items" -@using ErsatzTV.Application.MediaCollections -@using ErsatzTV.Application.MediaCollections.Commands -@using ErsatzTV.Application.MediaCollections.Queries -@using ErsatzTV.Application.MediaItems -@using ErsatzTV.Application.MediaItems.Queries -@using Unit = LanguageExt.Unit -@inject NavigationManager NavigationManager -@inject IMediator Mediator -@inject ILogger Logger -@inject ISnackbar Snackbar - - - - @_mediaCollection.Name Media Items - - - Source - Type - Title - Duration - - - @context.Source - @context.MediaType - @context.Title - @context.Duration - - - @if (_collectionItems.Any()) - { - - } - - - - - - All Media Items - - - - - - Source - Type - Title - Duration - - - @context.Source - @context.MediaType - @context.Title - @context.Duration - - - - - - - Add Results - - -@code { - - [Parameter] - public int Id { get; set; } - - private MediaCollectionViewModel _mediaCollection; - private IEnumerable _collectionItems; - - protected override async Task OnParametersSetAsync() => await LoadMediaCollectionAsync(); - - private List _mediaItemIds; - private IEnumerable _pagedData; - private MudTable _table; - - private int _totalItems; - private string _searchString; - - private async Task> ServerReload(TableState state) - { - List data = await Mediator.Send(new SearchAllMediaItems(_searchString)); - - _mediaItemIds = data.Map(c => c.Id).ToList(); - _totalItems = data.Count; - - _pagedData = data.OrderBy(c => c.Id).Skip(state.Page * state.PageSize).Take(state.PageSize).ToArray(); - return new TableData { TotalItems = _totalItems, Items = _pagedData }; - } - - private async Task OnSearch(string text) - { - _searchString = text; - await _table.ReloadServerData(); - } - - private async Task AddResultsAsync() - { - Either result = await Mediator.Send(new AddItemsToSimpleMediaCollection(Id, _mediaItemIds)); - await result.Match( - async _ => await LoadMediaCollectionAsync(), - error => - { - Snackbar.Add(error.Value, Severity.Error); - Logger.LogError("Error adding items to media collection: {Error}", error.Value); - return Task.CompletedTask; - }); - } - - private async Task LoadMediaCollectionAsync() - { - Option>> maybeResult = - await Mediator.Send(new GetSimpleMediaCollectionWithItemsById(Id)); - maybeResult.Match( - result => (_mediaCollection, _collectionItems) = result, - () => NavigationManager.NavigateTo("404")); - } - -} \ No newline at end of file diff --git a/ErsatzTV/Pages/MediaCollections.razor b/ErsatzTV/Pages/MediaCollections.razor index d34c2523..c1c43bfb 100644 --- a/ErsatzTV/Pages/MediaCollections.razor +++ b/ErsatzTV/Pages/MediaCollections.razor @@ -2,107 +2,50 @@ @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Queries +@using ErsatzTV.Application.MediaCards @inject IDialogService Dialog @inject IMediator Mediator - - - Media Collections - - - - - - - - - - - Name - Media Items - - - - - @if (context.IsSimple) - { - @context.Name - } - else - { - @context.Name - } - - @context.ItemCount - - @if (context.IsSimple) - { - - - Edit Properties - - - Edit Media Items - - - Delete - - - } - - - - - - - - Add Media Collection - + + @foreach (MediaCollectionViewModel card in _data) + { + + } + + + + + Add Media Collection + + @code { - private IEnumerable _pagedData; - private MudTable _table; + private List _data; + + protected override Task OnParametersSetAsync() => RefreshData(); - private int _totalItems; - private string _searchString; + private async Task RefreshData() => + _data = await Mediator.Send(new GetAllMediaCollections()); - private async Task DeleteMediaCollectionAsync(MediaCollectionSummaryViewModel mediaCollection) + private async Task DeleteMediaCollection(MediaCardViewModel vm) { - if (mediaCollection.IsSimple) + if (vm is MediaCollectionViewModel collection) { - var parameters = new DialogParameters { { "EntityType", "media collection" }, { "EntityName", mediaCollection.Name } }; + var parameters = new DialogParameters { { "EntityType", "media collection" }, { "EntityName", collection.Name } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; IDialogReference dialog = Dialog.Show("Delete Media Collection", parameters, options); DialogResult result = await dialog.Result; if (!result.Cancelled) { - await Mediator.Send(new DeleteSimpleMediaCollection(mediaCollection.Id)); - await _table.ReloadServerData(); + await Mediator.Send(new DeleteSimpleMediaCollection(collection.Id)); + await RefreshData(); } } } - private async Task> ServerReload(TableState state) - { - List aggregateData = - await Mediator.Send(new GetMediaCollectionSummaries(_searchString)); - - _totalItems = aggregateData.Count; - - _pagedData = aggregateData - .Skip(_totalItems <= state.PageSize ? 0 : state.Page * state.PageSize) - .Take(state.PageSize) - .OrderBy(c => c.Name); - - return new TableData { TotalItems = _totalItems, Items = _pagedData }; - } - - private async Task OnSearch(string text) - { - _searchString = text; - await _table.ReloadServerData(); - } - } \ No newline at end of file diff --git a/ErsatzTV/Pages/MediaMovieItems.razor b/ErsatzTV/Pages/MediaMovieItems.razor deleted file mode 100644 index 185814ec..00000000 --- a/ErsatzTV/Pages/MediaMovieItems.razor +++ /dev/null @@ -1,8 +0,0 @@ -@page "/media/movies/items" -@inject IMediator Mediator - - - -@code { - -} \ No newline at end of file diff --git a/ErsatzTV/Pages/MediaOtherItems.razor b/ErsatzTV/Pages/MediaOtherItems.razor deleted file mode 100644 index d944a4f2..00000000 --- a/ErsatzTV/Pages/MediaOtherItems.razor +++ /dev/null @@ -1,8 +0,0 @@ -@page "/media/other/items" -@inject IMediator Mediator - - - -@code { - -} \ No newline at end of file diff --git a/ErsatzTV/Pages/MediaSources.razor b/ErsatzTV/Pages/MediaSources.razor index cef3d2ff..0cdbee15 100644 --- a/ErsatzTV/Pages/MediaSources.razor +++ b/ErsatzTV/Pages/MediaSources.razor @@ -1,13 +1,15 @@ @page "/media/sources" - - - - - @* *@ - @* *@ - @* *@ - + + + + + + @* *@ + @* *@ + @* *@ + + @code { diff --git a/ErsatzTV/Pages/MediaTvItems.razor b/ErsatzTV/Pages/MediaTvItems.razor deleted file mode 100644 index 69703faf..00000000 --- a/ErsatzTV/Pages/MediaTvItems.razor +++ /dev/null @@ -1,8 +0,0 @@ -@page "/media/tv/items" -@inject IMediator Mediator - - - -@code { - -} \ No newline at end of file diff --git a/ErsatzTV/Pages/Movie.razor b/ErsatzTV/Pages/Movie.razor new file mode 100644 index 00000000..3004229c --- /dev/null +++ b/ErsatzTV/Pages/Movie.razor @@ -0,0 +1,76 @@ +@page "/media/movies/{MovieId:int}" +@using ErsatzTV.Application.Movies +@using ErsatzTV.Application.Movies.Queries +@using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaCollections.Commands +@inject IMediator Mediator +@inject IDialogService Dialog +@inject NavigationManager NavigationManager + + + + +
+ @if (!string.IsNullOrWhiteSpace(_movie.Poster)) + { + + + + } + +
+ @_movie.Title + @_movie.Year + @_movie.Plot +
+ + Add To Collection + +
+
+
+
+
+
+ +@code { + + [Parameter] + public int MovieId { get; set; } + + private MovieViewModel _movie; + + private List _breadcrumbs; + + protected override Task OnParametersSetAsync() => RefreshData(); + + private async Task RefreshData() + { + await Mediator.Send(new GetMovieById(MovieId)) + .IfSomeAsync(vm => _movie = vm); + + _breadcrumbs = new List + { + new("Movies", "/media/movies"), + new($"{_movie.Title} ({_movie.Year})", null, true) + }; + } + + private async Task AddToCollection() + { + var parameters = new DialogParameters { { "EntityType", "movie" }, { "EntityName", _movie.Title } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Collection", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) + { + await Mediator.Send(new AddMovieToSimpleMediaCollection(collection.Id, MovieId)); + NavigationManager.NavigateTo($"/media/collections/{collection.Id}"); + } + } + +} \ No newline at end of file diff --git a/ErsatzTV/Shared/MediaItemsGrid.razor b/ErsatzTV/Pages/MovieList.razor similarity index 73% rename from ErsatzTV/Shared/MediaItemsGrid.razor rename to ErsatzTV/Pages/MovieList.razor index 84a4ca52..add94084 100644 --- a/ErsatzTV/Shared/MediaItemsGrid.razor +++ b/ErsatzTV/Pages/MovieList.razor @@ -1,5 +1,6 @@ -@using ErsatzTV.Application.MediaItems -@using ErsatzTV.Application.MediaItems.Queries +@page "/media/movies" +@using ErsatzTV.Application.MediaCards +@using ErsatzTV.Application.MediaCards.Queries @inject IMediator Mediator @@ -19,26 +20,24 @@ - @foreach (AggregateMediaItemViewModel item in _data.DataPage) + @foreach (MovieCardViewModel card in _data.Cards.Where(d => !string.IsNullOrWhiteSpace(d.Title))) { - + } @code { - - [Parameter] - public MediaType MediaType { get; set; } - private int PageSize => 100; private int _pageNumber = 1; - private AggregateMediaItemResults _data; + private MovieCardResultsViewModel _data; protected override Task OnParametersSetAsync() => RefreshData(); private async Task RefreshData() => - _data = await Mediator.Send(new GetAggregateMediaItems(MediaType, _pageNumber, PageSize)); + _data = await Mediator.Send(new GetMovieCards(_pageNumber, PageSize)); private async Task PrevPage() { diff --git a/ErsatzTV/Pages/Playouts.razor b/ErsatzTV/Pages/Playouts.razor index 7acb1daf..91406323 100644 --- a/ErsatzTV/Pages/Playouts.razor +++ b/ErsatzTV/Pages/Playouts.razor @@ -5,58 +5,60 @@ @inject IDialogService Dialog @inject IMediator Mediator - - - Playouts - - - - - - - - - Id - Channel - Schedule - @* Playout Type *@ - - - - @context.Id - @context.Channel.Number - @context.Channel.Name - @context.ProgramSchedule.Name - @* @context.ProgramSchedulePlayoutType *@ - - - - - - - Add Playout - - -@if (_selectedPlayoutItems != null) -{ - + + - Playout Detail + Playouts + + + + + + - Start - Media Item - Duration + Id + Channel + Schedule + @* Playout Type *@ + - @context.Start.ToString("G") - @context.Title - @context.Duration + @context.Id + @context.Channel.Number - @context.Channel.Name + @context.ProgramSchedule.Name + @* @context.ProgramSchedulePlayoutType *@ + + + - - - -} + + Add Playout + + + @if (_selectedPlayoutItems != null) + { + + + Playout Detail + + + Start + Media Item + Duration + + + @context.Start.ToString("G") + @context.Title + @context.Duration + + + + + + } + @code { private List _playouts; diff --git a/ErsatzTV/Pages/ScheduleItemsEditor.razor b/ErsatzTV/Pages/ScheduleItemsEditor.razor index b37b9187..ceb6319e 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 @@ -18,12 +20,16 @@ + + Start Time Media Collection Playout Mode + + @@ -31,12 +37,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) @@ -46,14 +52,28 @@ - + + + + + + + + + + - + Add Schedule Item @@ -74,7 +94,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 +151,8 @@ private ProgramScheduleItemsEditViewModel _schedule; private List _mediaCollections; - private Option _defaultCollection; + private List _televisionShows; + private List _televisionSeasons; private ProgramScheduleItemEditViewModel _selectedItem; @@ -108,7 +161,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 +185,10 @@ StartType = item.StartType, StartTime = item.StartTime, PlayoutMode = item.PlayoutMode, - MediaCollection = item.MediaCollection + CollectionType = item.CollectionType, + MediaCollection = item.MediaCollection, + TelevisionShow = item.TelevisionShow, + TelevisionSeason = item.TelevisionSeason }; switch (item) @@ -154,11 +211,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; } @@ -169,9 +225,33 @@ _schedule.Items.Remove(item); } + private void MoveItemUp(ProgramScheduleItemEditViewModel item) + { + // swap with lower index + ProgramScheduleItemEditViewModel toSwap = _schedule.Items.OrderByDescending(x => x.Index).First(x => x.Index < item.Index); + int temp = toSwap.Index; + toSwap.Index = item.Index; + item.Index = temp; + } + + private void MoveItemDown(ProgramScheduleItemEditViewModel item) + { + // swap with higher index + ProgramScheduleItemEditViewModel toSwap = _schedule.Items.OrderBy(x => x.Index).First(x => x.Index > item.Index); + int temp = toSwap.Index; + toSwap.Index = item.Index; + item.Index = temp; + } + 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 +259,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 +272,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 18677da3..80398576 100644 --- a/ErsatzTV/Pages/Schedules.razor +++ b/ErsatzTV/Pages/Schedules.razor @@ -5,44 +5,46 @@ @inject IDialogService Dialog @inject IMediator Mediator - - - Schedules - - - - - - - - - Id - Name - Media Collection Playback Order - - - - @context.Id - @context.Name - @context.MediaCollectionPlaybackOrder - - - - Edit Properties - - - Edit Schedule Items - - - Delete - - - - - - - Add Schedule - + + + + Schedules + + + + + + + + + Id + Name + Media Collection Playback Order + + + + @context.Id + @context.Name + @context.MediaCollectionPlaybackOrder + + + + Edit Properties + + + Edit Schedule Items + + + Delete + + + + + + + Add Schedule + + @if (_selectedScheduleItems != null) { @@ -59,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/Pages/TelevisionEpisode.razor b/ErsatzTV/Pages/TelevisionEpisode.razor new file mode 100644 index 00000000..fbf3af3a --- /dev/null +++ b/ErsatzTV/Pages/TelevisionEpisode.razor @@ -0,0 +1,82 @@ +@page "/media/tv/episodes/{EpisodeId:int}" +@using ErsatzTV.Application.Television +@using ErsatzTV.Application.Television.Queries +@using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaCollections.Commands +@inject IMediator Mediator +@inject IDialogService Dialog +@inject NavigationManager NavigationManager + + + + +
+ @if (!string.IsNullOrWhiteSpace(_episode.Poster)) + { + + + + } + +
+ @_episode.Title + @_season.Plot + @_episode.Plot +
+ + Add To Collection + +
+
+
+
+
+
+ +@code { + + [Parameter] + public int EpisodeId { get; set; } + + private TelevisionEpisodeViewModel _episode; + private TelevisionSeasonViewModel _season; + + private List _breadcrumbs; + + protected override Task OnParametersSetAsync() => RefreshData(); + + private async Task RefreshData() + { + await Mediator.Send(new GetTelevisionEpisodeById(EpisodeId)) + .IfSomeAsync(vm => _episode = vm); + + await Mediator.Send(new GetTelevisionSeasonById(_episode.SeasonId)) + .IfSomeAsync(vm => _season = vm); + + _breadcrumbs = new List + { + new("TV Shows", "/media/tv/shows"), + new($"{_season.Title} ({_season.Year})", $"/media/tv/shows/{_season.ShowId}"), + new(_season.Plot, $"/media/tv/seasons/{_episode.SeasonId}"), + new($"Episode {_episode.Episode}", null, true) + }; + } + + private async Task AddToCollection() + { + var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", _episode.Title } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Collection", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) + { + await Mediator.Send(new AddTelevisionEpisodeToSimpleMediaCollection(collection.Id, EpisodeId)); + NavigationManager.NavigateTo($"/media/collections/{collection.Id}"); + } + } + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/TelevisionEpisodeList.razor b/ErsatzTV/Pages/TelevisionEpisodeList.razor new file mode 100644 index 00000000..74743c51 --- /dev/null +++ b/ErsatzTV/Pages/TelevisionEpisodeList.razor @@ -0,0 +1,122 @@ +@page "/media/tv/seasons/{SeasonId:int}" +@using ErsatzTV.Application.Television +@using ErsatzTV.Application.Television.Queries +@using ErsatzTV.Application.MediaCards +@using ErsatzTV.Application.MediaCards.Queries +@using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaCollections.Commands +@using ErsatzTV.Application.ProgramSchedules +@using ErsatzTV.Application.ProgramSchedules.Commands +@inject IMediator Mediator +@inject IDialogService Dialog +@inject NavigationManager NavigationManager + + + + +
+ @if (!string.IsNullOrWhiteSpace(_season.Poster)) + { + + + + } + +
+ @_season.Title + @_season.Year + @_season.Plot +
+ + Add To Collection + + + Add To Schedule + +
+
+
+
+
+
+ + + @foreach (TelevisionEpisodeCardViewModel card in _data.Cards) + { + + } + + +@code { + + [Parameter] + public int SeasonId { get; set; } + + private TelevisionSeasonViewModel _season; + + private int _pageSize => 100; + private readonly int _pageNumber = 1; + + private TelevisionEpisodeCardResultsViewModel _data; + + private List _breadcrumbs; + + protected override Task OnParametersSetAsync() => RefreshData(); + + private async Task RefreshData() + { + await Mediator.Send(new GetTelevisionSeasonById(SeasonId)) + .IfSomeAsync(vm => _season = vm); + + _data = await Mediator.Send(new GetTelevisionEpisodeCards(SeasonId, _pageNumber, _pageSize)); + + _breadcrumbs = new List + { + new("TV Shows", "/media/tv/shows"), + new($"{_season.Title} ({_season.Year})", $"/media/tv/shows/{_season.ShowId}"), + new(_season.Plot, null, true) + }; + } + + private async Task AddToCollection() + { + var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{_season.Title} - {_season.Plot}" } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Collection", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) + { + await Mediator.Send(new AddTelevisionSeasonToSimpleMediaCollection(collection.Id, SeasonId)); + NavigationManager.NavigateTo($"/media/collections/{collection.Id}"); + } + } + + + private async Task AddToSchedule() + { + var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{_season.Title} - {_season.Plot}" } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Schedule", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule) + { + await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, SeasonId, null, null, null)); + NavigationManager.NavigateTo($"/schedules/{schedule.Id}/items"); + } + } + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/TelevisionSeasonList.razor b/ErsatzTV/Pages/TelevisionSeasonList.razor new file mode 100644 index 00000000..e7b11e38 --- /dev/null +++ b/ErsatzTV/Pages/TelevisionSeasonList.razor @@ -0,0 +1,117 @@ +@page "/media/tv/shows/{ShowId:int}" +@using ErsatzTV.Application.Television +@using ErsatzTV.Application.Television.Queries +@using ErsatzTV.Application.MediaCards +@using ErsatzTV.Application.MediaCards.Queries +@using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaCollections.Commands +@using ErsatzTV.Application.ProgramSchedules +@using ErsatzTV.Application.ProgramSchedules.Commands +@inject IMediator Mediator +@inject IDialogService Dialog +@inject NavigationManager NavigationManager + + + + +
+ @if (!string.IsNullOrWhiteSpace(_show.Poster)) + { + + + + } + +
+ @_show.Title + @_show.Year + @_show.Plot +
+ + Add To Collection + + + Add To Schedule + +
+
+
+
+
+
+ + + @foreach (TelevisionSeasonCardViewModel card in _data.Cards) + { + + } + + +@code { + + [Parameter] + public int ShowId { get; set; } + + private TelevisionShowViewModel _show; + + private int _pageSize => 100; + private readonly int _pageNumber = 1; + + private TelevisionSeasonCardResultsViewModel _data; + + private List _breadcrumbs; + + protected override Task OnParametersSetAsync() => RefreshData(); + + private async Task RefreshData() + { + await Mediator.Send(new GetTelevisionShowById(ShowId)) + .IfSomeAsync(vm => _show = vm); + + _data = await Mediator.Send(new GetTelevisionSeasonCards(ShowId, _pageNumber, _pageSize)); + + _breadcrumbs = new List + { + new("TV Shows", "/media/tv/shows"), + new($"{_show.Title} ({_show.Year})", null, true) + }; + } + + private async Task AddToCollection() + { + var parameters = new DialogParameters { { "EntityType", "show" }, { "EntityName", _show.Title } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Collection", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) + { + await Mediator.Send(new AddTelevisionShowToSimpleMediaCollection(collection.Id, ShowId)); + NavigationManager.NavigateTo($"/media/collections/{collection.Id}"); + } + } + + private async Task AddToSchedule() + { + var parameters = new DialogParameters { { "EntityType", "show" }, { "EntityName", _show.Title } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Schedule", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is ProgramScheduleViewModel schedule) + { + await Mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, ShowId, null, null, null, null)); + NavigationManager.NavigateTo($"/schedules/{schedule.Id}/items"); + } + } + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/TelevisionShowList.razor b/ErsatzTV/Pages/TelevisionShowList.razor new file mode 100644 index 00000000..78424d02 --- /dev/null +++ b/ErsatzTV/Pages/TelevisionShowList.razor @@ -0,0 +1,52 @@ +@page "/media/tv/shows" +@using ErsatzTV.Application.MediaCards +@using ErsatzTV.Application.MediaCards.Queries +@inject IMediator Mediator + + + + + + + @Math.Min((_pageNumber - 1) * _pageSize + 1, _data.Count)-@Math.Min(_data.Count, _pageNumber * _pageSize) of @_data.Count + + + + + + + + @foreach (TelevisionShowCardViewModel card in _data.Cards) + { + + } + + +@code { + private int _pageSize => 100; + private int _pageNumber = 1; + + private TelevisionShowCardResultsViewModel _data; + + protected override Task OnParametersSetAsync() => RefreshData(); + + private async Task RefreshData() => + _data = await Mediator.Send(new GetTelevisionShowCards(_pageNumber, _pageSize)); + + private async Task PrevPage() + { + _pageNumber -= 1; + await RefreshData(); + } + + private async Task NextPage() + { + _pageNumber += 1; + await RefreshData(); + } + +} \ No newline at end of file diff --git a/ErsatzTV/Properties/Annotations.cs b/ErsatzTV/Properties/Annotations.cs new file mode 100644 index 00000000..928c91c4 --- /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/Services/SchedulerService.cs b/ErsatzTV/Services/SchedulerService.cs index 8b91872b..4501d0ff 100644 --- a/ErsatzTV/Services/SchedulerService.cs +++ b/ErsatzTV/Services/SchedulerService.cs @@ -7,7 +7,6 @@ using ErsatzTV.Application; using ErsatzTV.Application.MediaSources.Commands; using ErsatzTV.Application.Playouts.Commands; using ErsatzTV.Core.Interfaces.Locking; -using ErsatzTV.Core.Metadata; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -83,7 +82,7 @@ namespace ErsatzTV.Services if (_entityLocker.LockMediaSource(mediaSourceId)) { await _channel.WriteAsync( - new ScanLocalMediaSource(mediaSourceId, ScanningMode.Default), + new ScanLocalMediaSource(mediaSourceId), cancellationToken); } } diff --git a/ErsatzTV/Services/WorkerService.cs b/ErsatzTV/Services/WorkerService.cs index 75538af4..caf56afd 100644 --- a/ErsatzTV/Services/WorkerService.cs +++ b/ErsatzTV/Services/WorkerService.cs @@ -3,7 +3,6 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using ErsatzTV.Application; -using ErsatzTV.Application.MediaItems.Commands; using ErsatzTV.Application.MediaSources.Commands; using ErsatzTV.Application.Playouts.Commands; using ErsatzTV.Core; @@ -56,28 +55,6 @@ namespace ErsatzTV.Services buildPlayout.PlayoutId, error.Value)); break; - case RefreshMediaItem refreshMediaItem: - string type = refreshMediaItem switch - { - // RefreshMediaItemMetadata => "metadata", - RefreshMediaItemStatistics => "statistics", - RefreshMediaItemCollections => "collections", - RefreshMediaItemPoster => "poster", - _ => "" - }; - - // TODO: different request types for different media source types? - Either refreshMediaItemResult = - await mediator.Send(refreshMediaItem, cancellationToken); - refreshMediaItemResult.Match( - _ => _logger.LogDebug( - $"Refreshed {type} for media item {{MediaItemId}}", - refreshMediaItem.MediaItemId), - error => _logger.LogWarning( - $"Unable to refresh {type} for media item {{MediaItemId}}: {{Error}}", - refreshMediaItem.MediaItemId, - error.Value)); - break; case ScanLocalMediaSource scanLocalMediaSource: Either scanResult = await mediator.Send( scanLocalMediaSource, diff --git a/ErsatzTV/Shared/AddToCollectionDialog.razor b/ErsatzTV/Shared/AddToCollectionDialog.razor new file mode 100644 index 00000000..b76f9d3e --- /dev/null +++ b/ErsatzTV/Shared/AddToCollectionDialog.razor @@ -0,0 +1,58 @@ +@using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaCollections.Queries +@inject IMediator Mediator + + + + + + + + @foreach (MediaCollectionViewModel collection in _collections) + { + @collection.Name + } + + + + Cancel + + Add To Collection + + + + +@code { + + [CascadingParameter] + MudDialogInstance MudDialog { get; set; } + + [Parameter] + public string EntityType { get; set; } + + [Parameter] + public string EntityName { get; set; } + + [Parameter] + public string DetailText { get; set; } + + [Parameter] + public string DetailHighlight { get; set; } + + private List _collections; + + private MediaCollectionViewModel _selectedCollection; + + protected override async Task OnParametersSetAsync() => + _collections = await Mediator.Send(new GetAllSimpleMediaCollections()); + + private string FormatText() => $"Select the collection to add the {EntityType} {EntityName}"; + + private void Submit() => MudDialog.Close(DialogResult.Ok(_selectedCollection)); + + private void Cancel() => MudDialog.Cancel(); + +} \ No newline at end of file diff --git a/ErsatzTV/Shared/AddToScheduleDialog.razor b/ErsatzTV/Shared/AddToScheduleDialog.razor new file mode 100644 index 00000000..1468ccfe --- /dev/null +++ b/ErsatzTV/Shared/AddToScheduleDialog.razor @@ -0,0 +1,58 @@ +@using ErsatzTV.Application.ProgramSchedules +@using ErsatzTV.Application.ProgramSchedules.Queries +@inject IMediator Mediator + + + + + + + + @foreach (ProgramScheduleViewModel schedule in _schedules) + { + @schedule.Name + } + + + + Cancel + + Add To Schedule + + + + +@code { + + [CascadingParameter] + MudDialogInstance MudDialog { get; set; } + + [Parameter] + public string EntityType { get; set; } + + [Parameter] + public string EntityName { get; set; } + + [Parameter] + public string DetailText { get; set; } + + [Parameter] + public string DetailHighlight { get; set; } + + private List _schedules; + + private ProgramScheduleViewModel _selectedSchedule; + + protected override async Task OnParametersSetAsync() => + _schedules = await Mediator.Send(new GetAllProgramSchedules()); + + private string FormatText() => $"Select the schedule to add the {EntityType} {EntityName}"; + + private void Submit() => MudDialog.Close(DialogResult.Ok(_selectedSchedule)); + + private void Cancel() => MudDialog.Cancel(); + +} \ No newline at end of file diff --git a/ErsatzTV/Shared/LocalMediaSources.razor b/ErsatzTV/Shared/LocalMediaSources.razor index 163e4d28..d34e114e 100644 --- a/ErsatzTV/Shared/LocalMediaSources.razor +++ b/ErsatzTV/Shared/LocalMediaSources.razor @@ -1,7 +1,6 @@ @using ErsatzTV.Application.MediaSources @using ErsatzTV.Application.MediaSources.Commands @using ErsatzTV.Application.MediaSources.Queries -@using ErsatzTV.Core.Metadata @implements IDisposable @inject IDialogService Dialog @inject IMediator Mediator @@ -90,7 +89,7 @@ { if (Locker.LockMediaSource(mediaSource.Id)) { - await Channel.WriteAsync(new ScanLocalMediaSource(mediaSource.Id, ScanningMode.RescanAll)); + await Channel.WriteAsync(new ScanLocalMediaSource(mediaSource.Id)); StateHasChanged(); } } diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor index c5536e26..e0d55ba4 100644 --- a/ErsatzTV/Shared/MainLayout.razor +++ b/ErsatzTV/Shared/MainLayout.razor @@ -28,9 +28,8 @@ FFmpeg Media Sources - TV Shows - Movies - Other Items + TV Shows + Movies Media Collections Schedules @@ -68,7 +67,8 @@ Palette = new Palette { DrawerBackground = current.Palette.Background, - Background = current.Palette.BackgroundGrey + Background = current.Palette.BackgroundGrey, + Tertiary = Colors.Shades.White } }; } diff --git a/ErsatzTV/Shared/MediaCard.razor b/ErsatzTV/Shared/MediaCard.razor index 44fea3a4..30d59868 100644 --- a/ErsatzTV/Shared/MediaCard.razor +++ b/ErsatzTV/Shared/MediaCard.razor @@ -1,37 +1,88 @@ -@using ErsatzTV.Application.MediaItems -@using ErsatzTV.Application.MediaItems.Commands +@using ErsatzTV.Application.MediaCards @using Unit = LanguageExt.Unit @inject IMediator Mediator -
- - @if (string.IsNullOrWhiteSpace(Data.Poster)) - { - - @Placeholder(Data.SortTitle) - - } - - +
+ @if (!string.IsNullOrWhiteSpace(Link)) + { +
+ + @if (string.IsNullOrWhiteSpace(Data.Poster)) + { + + @GetPlaceholder(Data.SortTitle) + + } + +
+ + + @if (DeleteClicked.HasDelegate) + { + + } +
+
+ } + else + { + + @if (string.IsNullOrWhiteSpace(Data.Poster)) + { + + @GetPlaceholder(Data.SortTitle) + + } + + } - @Data.Title + @(Title ?? Data.Title) - @Data.Subtitle + @(Subtitle ?? Data.Subtitle)
@code { [Parameter] - public AggregateMediaItemViewModel Data { get; set; } + public MediaCardViewModel Data { get; set; } + + [Parameter] + public string Link { get; set; } [Parameter] public EventCallback DataRefreshed { get; set; } - private string Placeholder(string sortTitle) + [Parameter] + public string Placeholder { get; set; } + + [Parameter] + public string Title { get; set; } + + [Parameter] + public string Subtitle { get; set; } + + [Parameter] + public string ContainerClass { get; set; } + + [Parameter] + public string CardClass { get; set; } + + [Parameter] + public EventCallback DeleteClicked { get; set; } + + private string GetPlaceholder(string sortTitle) { - string first = sortTitle.Substring(0, 1).ToUpperInvariant(); + if (Placeholder != null) + { + return Placeholder; + } + + string first = sortTitle?.Substring(0, 1).ToUpperInvariant() ?? string.Empty; return int.TryParse(first, out _) ? "#" : first; } @@ -39,13 +90,4 @@ ? "position: relative" : $"position: relative; background-image: url(/posters/{Data.Poster}); background-size: cover"; - private async Task RefreshMetadata() - { - // TODO: how should we refresh an entire television show? - await Mediator.Send(new RefreshMediaItemMetadata(Data.MediaItemId)); - await Mediator.Send(new RefreshMediaItemCollections(Data.MediaItemId)); - await Mediator.Send(new RefreshMediaItemPoster(Data.MediaItemId)); - await DataRefreshed.InvokeAsync(); - } - } \ No newline at end of file diff --git a/ErsatzTV/Shared/RemoveFromCollectionDialog.razor b/ErsatzTV/Shared/RemoveFromCollectionDialog.razor new file mode 100644 index 00000000..da643724 --- /dev/null +++ b/ErsatzTV/Shared/RemoveFromCollectionDialog.razor @@ -0,0 +1,43 @@ +@inject IMediator Mediator + + + + + + + + + Cancel + + Remove From Collection + + + + +@code { + + [CascadingParameter] + MudDialogInstance MudDialog { get; set; } + + [Parameter] + public string EntityType { get; set; } + + [Parameter] + public string EntityName { get; set; } + + [Parameter] + public string DetailText { get; set; } + + [Parameter] + public string DetailHighlight { get; set; } + + private string FormatText() => $"Do you really want to remove the {EntityType} {EntityName} from this collection?"; + + private void Submit() => MudDialog.Close(DialogResult.Ok(true)); + + private void Cancel() => MudDialog.Cancel(); + +} \ No newline at end of file diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index c50781ff..85210d13 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -171,16 +171,17 @@ namespace ErsatzTV services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddHostedService(); services.AddHostedService(); diff --git a/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs b/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs index c9f04651..df676355 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,48 @@ 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 => CollectionType switch + { + ProgramScheduleItemCollectionType.Collection => MediaCollection?.Name, + ProgramScheduleItemCollectionType.TelevisionShow => $"{TelevisionShow?.Title} ({TelevisionShow?.Year})", + ProgramScheduleItemCollectionType.TelevisionSeason => + $"{TelevisionSeason?.Title} ({TelevisionSeason?.Plot})", + _ => string.Empty + }; public int? MultipleCount { @@ -41,5 +87,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)); } } diff --git a/ErsatzTV/wwwroot/css/site.css b/ErsatzTV/wwwroot/css/site.css index d3f31315..58ddf384 100644 --- a/ErsatzTV/wwwroot/css/site.css +++ b/ErsatzTV/wwwroot/css/site.css @@ -6,9 +6,11 @@ .media-card-container { width: 152px; } +.media-card-episode-container { width: 392px; } + .media-card { display: flex; - filter: brightness(100%); + /*filter: brightness(100%);*/ flex-direction: column; height: 220px; justify-content: center; @@ -16,7 +18,9 @@ width: 152px; } -.media-card:hover { filter: brightness(80%); } +.media-card-episode { width: 392px; } + +.media-card:hover { /*filter: brightness(75%);*/ } .media-card-title { overflow: hidden; @@ -34,4 +38,18 @@ right: 0; } -.media-card:hover .media-card-menu { display: block; } \ No newline at end of file +.media-card:hover .media-card-menu { display: block; } + +.media-card-overlay { + background: rgba(0, 0, 0, 0.4); + border-radius: 4px; + bottom: 0; + left: 0; + opacity: 0; + position: absolute; + right: 0; + top: 0; + transition: opacity 0.2s; +} + +.media-card-overlay:hover { opacity: 1; } \ No newline at end of file diff --git a/README.md b/README.md index ba76e270..1532e857 100644 --- a/README.md +++ b/README.md @@ -9,28 +9,28 @@ Want to join the community or have a question? Join us on [Discord](https://disc - Use local media files and optional sidecar [NFO metadata](https://kodi.wiki/view/NFO_files); no need for a full media server - IPTV server and HDHomeRun emulation support a wide range of client applications - Channel-specific streaming mode (MPEG-TS or HLS) and transcoding settings -- Automatic creation of television media collections +- Collection-based scheduling, with collections containing television shows, seasons, episodes and movies - Powerful scheduling options such as chronological collection playback throughout the day or over multiple days -- OpenAPI spec for easy scripting from other languages (available while running at `/swagger/v1/swagger.json`) -- Command line project for easy shell scripting ## In Development - [Plex](https://www.plex.tv/) media, metadata and collections -- Published Docker image ## Planned Features - [Jellyfin](https://jellyfin.org/) media, metadata and collections -- Meta collections to logically group other collections - Run as a Windows service - Spots to fill unscheduled gaps ## Preview -### ErsatzTV UI +### Television Show -![ErsatzTV UI](docs/ersatztv-ui-channels.png) +![Television Show](docs/television-show.png) + +### Media Collection + +![Media Collection](docs/media-collection.png) ### Plex Live TV diff --git a/docs/media-collection.png b/docs/media-collection.png new file mode 100644 index 0000000000000000000000000000000000000000..a05877b83ca6063683d74285665b012d490c2a19 GIT binary patch literal 222469 zcmbrkbx@q$(c?Gc&zeLK?ef^8;F5{ z<$;TdUU``=*NTCGiJ=A7Q#?IAJw86h$H&Kh@B|z4(az59!NI}({k^g3%Hf~G?d@%O zd3oFi)K5r=sA*})$jMk(SROp3WME`mT3+5gJ_7a@;^hGG0F100BlS z2NP{Q89^32TO69_%Cf>N)FfUaJY*O}Qeu1;S2uEW&-4t<Lc2)E4PgWWu zJJ8X_v;?bKBFtjw^M}$*jihkR%cH)-g??3i-lTl5tWwSI)jBoxX-dxK)gD?3OkrmD z%~3qerL7|`9#*fFIKT0O)xJHC|BCoj=Wi@wVnpog>%-mG;*4v;r;Pc_`gQa~hSkqt z`9cA%;x{08s$_-}Yf$6P&)jo%yuQP}5Z}aFMT>;~5L=j2ltJig%SVeZALWayd*Fr1 zx{>!zSI9+bxc0XnS>h)rCzpj#EPb^;?G22`QaD%&SE?#Idh5HBm9+OkcErhSO(VEv zdFFBxZk9-U;+XudT}MctKgH*<-$Y6oL78R_WrROtLXC!bW6Ijo5i6w}l#fbzw!`Fz zs6sD&)*Q=5lIo|hSxZ;1R3K( z&j`s_hZEC#bR7ag0g;x;j~`)*SgTZrim^>&Jjxt2k#0Lzg-EVwN8X}%F*+81b;2+( zC`q9YF%WPV#v33IfMErW#rx0F59fMHQj8@|sB72j7~lhpnYd}=h%9d$^!6W=HN+dN zwjZ*YaJ&D$s5p#!Eb|pfkHErs#c#*Q$pmH|EQvtB!%ICD z59n=e)3B|^V`-OZTM?iKKO)O!>V}LL4pa8!rS#l90Y8!k2^Ha9*~zncgE21bpa9g+ zb%A)BUPP6?Aw1JQ_Hr=|@ocB}_-Cn@fSrs{tJ_aYH>6|zQ5~I)GEUh}%fh8#lqasezueg=ZL(LGSGTIx_!|^PSr2ajFTM=}`$ThNth7 z-9B0hLC_||Sb#*NhR>u~vmwYzJbGQH&#QGhdzz6<$&{AKmHs+p2Z>Qk5cPdBC_VnL zi%=1nqfd1hAvB~GXzOnzu)JZA7A0k*;;uCsV34Pm@x325Q@5@t1>lG{5VF$|gFxI| zP=bgSTIe>4&3rwtu7XCewpH?wuf=7>03qb&PXR@gxpoX7gc=tQ*`1^v@^9Y(-Gquv z;W1IS85zyHb7ybAhrkB9Tq2ld9LyY!I$(u*DW=6-59VHhnO}aXA2n*^krjc%iX50eXu`+h##ZLWkTteuL?L2b3M2{E;~Rw!=`50W3Y}z)A&AP z#+Cj#(9Th6n#WEI6^8)TFtIyG9RoKi5TG(>`l*}Yg-1I`IHP}2KaU6k_J@6BpdM=y zwiN&b)dMT&Zp(!XOk!_@x?$HEL33wt7$UuII*~^dQ%J;;FWn=yCNM57drZ^L?hhlM z#*Q7-j`CP}8+N_yuIp&vU~FI@prXfvK)%n<7q#OA;50wH5P<^9vM>E@xxcg(Q3{e( z{|=cuW8o5=k;+e@`pIi~icwA8$clD-{Pkjpt=qceEVq zFYDZErqqo{Z%j2#@BKl%WD6||^ABN78B?b`-iggbHG9~R=}&IMw*9Pns#woCQh>|X zant%`<;>*ndD{6u)<4zM45v8yj9AYKBF1koozpm`%;%A_qbA~ z(-_+AeY)8ct&rCfz=60r@=tp zO9ul?r(S6l{|8WO_78|@ zl;#T+4Rs3E{8C77cRTz}>++(C@)%Lxl9S*Ne=E%;x)Ypg3D%mL?>SdAD_XAnGe3UA zZMu$%&BOyh9|mOu)s0f3gB{r`@qqs@$cV`z?&jG}KLMQ`=cHlPkA|G0e#)`MdI={a z2*82UFb#9K{Q_J~*PKk0XzEMZ{gdMwXUq@4xWR||klt^m4(qh9*F>f!UTQxZmp+OH zK3MXltZ|}19JHS^9Q}G`i(kkI?LTwJLbtVc1kC%bhDrK+*nNnHJ@Qak5%_m?0(MO% zn_-!lR;cDbf8YMM)m37no814>3jcd|{I3uHghIh?)UoNBpI7~+a2=Y^`~Q4H&|(dA zf^r~Wj5h{Qjp8fi;X^cT*%+YY(9b#UTHv)cfACYtQ;B6&w@lu!5pd)y$;a|MV?)aP zGw?~+SN=#48Xb{6@{{22Qct0CB{e$Np}Uh@8OWA1jwwoCq0^B+?!A#Fj;G77QvdGl zV5*D1opKO{Y41nu>{`6iM=4|!Sy)$4C3yN0C(V;B#5XT|QyBpxK&g0LY5u#nl@l=@ zb*ALx@f7=Q&8TtXLJ1GG%lkjHq03kS$Gwze3GWgV!3IzM!l;gKJoG`^L|}-PpeCyl?&HJ<(CPZ#}2|f!%KH32ZyIsL6|}#-!|;&#jk~BisBm| zD`h*}!(o~`j7IG<&&LD_?;f{E1_kZc{V2er4f zw6@;fBW};9e+<@ zYEX%q&QUxmz8hi!`>{+MK_IR{*Lp97_S&HG-!T1#*>#{^JY$v-q=Vv*xEshjGr54j z{CZg~^FA`{^4Ey_1|j+og-J6OdjpDrtfBg-b;UfNUs+otCtj)v<36-E0HK{CYtU8& zos1P9Lzq1g*7QVsjXK_bg2BfXW0Ph&@x$SP>vmW$AUHZG0^kW9F=){=!Qv49_quAWsh|I&-Qmu*n;e;%$4{u?NjEoFw$oK43bBUqDj(BvDcn=>wga@f0 zO+XSl7(h8$**CDNDC0@27zSBnyFM>7jVbIAaiWX@*zhjhMdA(2TAb-U78nz^H&g-q zJ8G3QGBYjsxxxsFKb%n(%vkjNQi1ph{71nm)}Z|M#=~gLhj1*&&`%bMN?CAN#xrAi zEXu5ZHYUeZ7vVE*FtHT@RWA8om$|41V9q~y0C@;x+ae?3WZW1nCqy>on;(|1N9VJ$ z6X6n*M*8<|<7JSNcrX^#@w4)~+tYJSd}4d`@1yKjztnqeWWlUqLJf2hcB9C0xj?in zvPgG!Ylk76*@} z5kJlYNQeZm84r7XSt~SgDwE0XEz@DtbFdCEG2@2v(-dX}d;1ezl2kR`j>}yO*)4*$`rmWA!P&FUh_DojO)17InxQwT_I#2F z<3jxpVn2trG1`&yh4ur|A^Bh8h{Mx-Pd4>jY+aZ(L0`aK{{&V%!$tHOn-}u?Zi(H9d z>)_d!YgSyw1%rLHH#R1Elid0eHh--3pA4MpC$YMq(FP7YV5qk!er^|jg;BcPz1VHS ztrr&}qBA-5il%qHVzO%)@n^=&dGN@b;!V((eD|buxB8(RJi^E8oMp93QNA6I83Dw) zP8{arJoUn0VMj<^%x51OV2omJ`t_pY*Yd;-j<_P;jB0^pc9c)w=QK`nQo$-@vXVWX z8sMR`&N>G~Cx3lo=Zi4V(=^Yi8~h@S80vrf7&^qN|5 ztZ{fct)p3v!@=0A6NE;u$hF^+6#YSZ*vXK~%OI0m&-*PM+9TZ1=V zAy51TvdNR*7_{p#2(%R*utrUDpfmtHc8?~?H12uEu8nX>{Tq8^ zca0USKX5pu^BmSDGY^m(z<28f1#AD#gT~|&z z_@+#0aHEppoml3dil1`=iRm+Is`d09{-QdxgT_k($9fNt1W;DZ_>6DjJtv3?U)9&_ z^2WW#G9T2!9%Q1Ebz>Wt@L}EX0-j@p;Pnx!Dx)1XEN$KjBL__7@qYZllP+0j4%4gn zT0v5O0pSdiBeodsO;zdJ<>v&_UeYp?u-etTsU@2YtGQ$)R{z#(x}gbXJ>8Vx5IC3+ z2q8<)Yc?Swnl__35G3QU-iR6WhdU0Cr~Igh^>J{o)Ia+4sTcaYVQHu$=?w-@CJkon zwr#TK+Vg2o66bmCK>^)&UjutiP_4_yYos;m^%1aZwcyOYV~C*_076pU_IWaJt=M&>4IA3^9;or zgyD@EorUpt(u}f5&1#@iift=}n%;8%@)BvyAtmDB4$GX@-4|Qd(VsmJggGo%8C{jYB1(w3^(w_B}Fa5ILdpS{jJ4}5JbMa}X za4`nxU0m~0y(A=LU0TmbjKxQn!tHP7DX-&LUFUumu>elVJ<3u7-I}mRJ!V&PPE6;V zm6H*i@zJsnDN%!DkNl9cCdpIfkNv(1Kbn2mfRaS=>b-po5b~Bb!0ZSoDfNx_clVv| zp0lj3sQ3gp!)~yeB`)E*Y!Jwxbd3*PsUnCyfAG)@j}oL2dT4ejd;_!!S9TZS_y68f zPX-$yA4_&UMuC2mxZ@d_04{O91mm`{-?>3_hW}K&6S*PD#^wKBoGR_}qfU%e!qZnAq*wfV3)AV*|R$>(7i#a%Yu0wzTlVah0WG33eU5bP*R= zL$+xpkXvsVehzG#5E5Wx{YUG8oxNkQQr`~xEx1A?_$fdQ) z%%wkLKjt*O)TTYA1mwM_f*i|+@1DCT9-Lmq&W~_DH4?$iY7jeFPy@`&^blHl%DM1| zAb9E>))rxlPa)L6P4P0SOQ~TeES#G(d|?6=Alyj-lhjt&7=PGeg(Br z*gUWln29S;n>9a@!}sy{N^SSeMTTH4l=neX6aY( z#F?a@GnnTUwMU|@N3rnyb&K?MsY$2zKa8K=QTEnoX{*s%n7W(H8qto;{3$iM|A~A> z5OXWR7v=w}S%3Y@JG)?2Ox@?CZ!WdrYhKj=Dh>byUnnvvmFPXTViru818c?d3GDvn z_?RMUS{c*qq*cuBsY+X$(X&iLfX9nIiux8+zn}g~tIG$($Ah&V`g&CU&+L`0-7112 zsg62rzk@@V=$D<~ErO=!*S7GI!j?1;?ggUqI{i^fYi*CNNCxum}ZsH_Bl02 zkZM(HjLpW~WdBbCv6@i%Ow_NPiyz{U-96@W0{BTg=wSyIWD-T6w9H*#XrYcMB(zUQ z)tvQfMygwgi{I-fR|CH+x(ex%py@tgY_3I?8BCW_EHH4FxfDzKdsDuH)QwoM;Uf3S z!vCpfTIL~pNY(itoOj!`W=Yx-@0F$wUb%z~d%4%EQK!-UmQ*&Duq2AglU1TG9J8Ah0?{VYXh=8kzghsQLVZ;qXAnM?_F0RMg!B71+Eg3G5 z(LboC@)0I+cse8N_@t;lwgeu1SL` zneCycN`W+`(sP{%q0SK`Nx{D0M(Q-COw|tCfVQWp+^A2;<1xk^lOf(_iQ4UiBWKb0 zyA=neVaRLOn1v&X2skVqg*`WzN6(FWTpp*wVoC+AR8{&i4l-x?Qf9EPZ1z$@oD8aZ zZ6+Beu_U(#o1mvrD+ff}4=-3*h|zgGg6=sl?59dAbqj&M(T)9fC(h@&hy<#~((UO% z2eo6K4#LEL;~094ey?&N8F3lt^(*5@`BoCI9wAy|)por0bEdjx_K8(}PV~1Mm)+8| z7chHgWDT{U^z8lIr{-LbqYfXiJ;KN2uzFsR_T*dsTe=sm279k0O@z!+Ae?~%W#&}S zrY`T0oNOSSU{PZxoFYB@9VG!0SsNWl;WV>Hcuuk}IRv}*=5k-eNzs^xN4AukE!zR^cFxqzU6772+ZvLFVtW{_QrwmS zL4sQQUg@oAT}shejW&ATTI7q31MzBXQBkk^oDxu`P+;qZZeHf zP8?+;1(E~-2Ol>0r z`mNl7AYzEACad$4B79x*8tdA&wqGWh#DG};BLuRCZOG*+gUF>v$X#WQ!C`Bb2DfIO z9796deeZJykZAjKh$sQLqaEV%B-}F%IPutuznzxA2WjIl{(4}+D7bCQ^2Xs8L;vt` zUuS85&#nKg(f4Ax?yHh?wm?bcY5gMxFiFo|*9U$P5(J0-|v^ z=AN|I=TnR@vS`K4F#RzUi>X@^EAEyjAOnY~QttTF_2|^2%v)7Im_`~{nLSId{aJy? zaw$HF<<>1246C@CMg)G3OkzKoNFU7 z#c0~%UDjq4Vx3&&{oJg*|Jv%BM<94G`2N(hebSzZA^!A1>{GvKUUlBLJzWzg#m&|HbXv8q%`luVPRq& z*Z{3=i90rPtWk%4_7(I|>;T7e2Y8_ll=$fi-jIlX;T+bNn8d_n?eAKFj~vy?q=$_) zq9c1a`;0veDtq-j@~W)RL%C8a!6-Fb>3uHfBP0=2#f-P>JvFrQcR=1znNQx>+*s9v zVfnsvQz+tYFclD!)2tB9Z;O}oJ>!T3?T4H!ASRuvYH3_wDs^y^>c!}Ie=RULExY%t zdP?MW9zzkhQIFguZfnZhZ+UntT)+_wsa4KGyYY>#pq+SOTbHgaIKu-w7xhjE!Scx} zr!qS(SZ*w8>qeV=k6O3g&_)8M|0QD8=35sN$Eh(!5 zZiH{KQv57jjs$u?@{})%TB1USP)rJHXXc!GZT7SFoHqHJ2!hZ%G?5>{4Yt2BAx_^k z@t60ywx1mDKLup}8apr?u$2}y{fjFFo!Pp>l!8m$EwPbZYCR|fj5O;yyUd_1c0~yM z>Qi6{sE;;TIfQxwapF+@`wz#{qyrVQ5PZH^Pc$1|2OwB>V3dCw$El#a16*-F-k6{F zc$*5@mv#J7##&FHGM_gKmp-i)IN>8?P*u5hby?7;#mD;>F%Swd6aNo^{udhkpJ|8z zUE#t(k5vCF<^J2=|7VW=Z%lu~3@j=jdp>wlg`K;d5x!@=Kw8cr!VXRTIrRj93T3jdw*Zg`h74%*-UHcS>!l z&vH2YmEM*i)a2j6!1}FHmUOMa_S>Acrfcb7--J80t>FvOM@zw?fGCf`7rGp5Utu_; zw}2u=cBhs^_y6x(0EoHTIX;=kMsj&Ww4$Ph&2C{!P-tAKp4eLGfgO^q>M~D#o=+yw z^B`++qP;!9PlY4!a^;HzA0_FVFZ_0WkzFa<%xtLflND## z99!a4$6Kgtv$I`Y)zD`5T(|9M(G2yig%;UZypE?-o^rSEng$~Vm{v%~;~l zOFTIh$qrf_DNMFE8(LWPL82@x9$Ruz^lk6O_zG>f1Pp9fOKP?mB3cmVYtq+7rAKXv zIz5^7A#HE}A_5lS)9k2)(|Y5D`;oQF8yb^KO7!ep8G=j}PCEik${0H#-&cSua#G(_rJJSNWPX*n zHla-e=Av;J{qU>E)st$EGs$l$I+?&k5436HfT$m(sewUz5#s06P~Pd80lnVR1-08> zfiGXOv}o1a{)A1TC}zYPIQwn`BY_XZsG-RXaa1ol{dZFkFnCO9v>2Z8;k(55POW7; z1%W|`prfjYjMVmw44+4ik&bN=;aAoF+oeConX@ZIH~7XdN1D2rUr z9*_N1KIsCl?1HlZ&-0{(w!TKgIzw;q97)F)bgvD7MMG-_DmaHi?C za%Igm%%xLqHV-Kn%uJr(U|dMW`j4*m;~;L%G}Wh0THgX0f-jA8ZKO9#PPSaGHc~2q z`5|*&ukX*StdOWsmz4cF#LaAe_V86jZo64ltP?MJ42^f}8d&_mldN+mcWN zncMu<8vnS}meq@R-yJz{ww# zXXa+wr(C2Q9IX2zf})j?UB;J6MAj(kJ6>7hNPiDVh_aqp$>2g$Wm4ul_Z0S8LCFDq zr-f>d#Wl(a4k4hFE5PJ;*1AB8e9?12r!b{{^P2y(M6%?ZNlpvB(A3NnfY5Vn4Uqel zB=%14YcMVBlS~tD)yr%pZ0{|JqZ1*pw}G4PnWp=4-Uq8TTr4@pjlRgf3lf0(38vJy zs6GdUk%i+ET@`vyM(p)>DhQ~fY5mE1;^#B~Am4zxSy^;qIo5DXbZXyJ&TmNTYF@3! zuev7{W%Oz1B|$$ed9l%zg?W-JN6%07&($?5L@BXf=epaxC_x<6$0%N7SofrhK{qQFDu(@BM+U4#f@={Zhv6s(pP`G8t9+oe`A{$HP{7-$GwL zIZks=cM@v)dNGUfk8>QXM*^Aaogl77Ps@vMW;YT)B(J6QL9<>>zto9$v4~bU$_@f% zz`kT|q0|!G`UEuno7GZ$W^>M%rFp*dtH0!Aj3#OM(;j$2cE&x`r+q0>wnmHv$xDa1 z&G~Pby%>Q9<>tPt{As3Z1KdSlDz5#$ma%kUDw>_UJh9UaZxse&3(1h$Ue%jd_N$6%#?=i!O)E2BV=0`)Cd z+rwjXi@^1S(I4@x3WfiXYA-!;%o_nSJ>zvjHQ}m8`}t6N-0oC;D6iBAuM=fsE?S!t zeH&SGeh6zUvdXVG*w>d>t-N>=nEX-o{%#;2zDb<>0!?vFNc2=*NCEs=PIVp1tCIiJ zRXsd=;NI_5lY}ODT3{%d)Xvp0>10lRY!CR0hdu^{_C@w{X`4OInUj90^U=ljjXW)6 z{d-2@W4J%^6IiqU!`xScvEeBB*-~F8Uhlw#8HRCfe3zc`n}&HMxRkUCLBf`ip)USn zL1*h^m}P@c`_@-~4;Lb=jlXe7gz*?OjKjUyxVLkb2d&PHJv|f8=hxQMgjdaKz}l|e z+r0g@O)-^1q+|Qk$#K~gQh>Kc48Dr)`td>My6$*GE^WD5mY%nhYqK~TJ8fMc6*L>s z9~GXt&3IP?)wz)B3T1tj7#ho3A@sF-k~x?aOgJU!s`K3&=X#T(G3?9HR2`n4r?-h8 zvXG(Ti4Qd8(%E5ASCPSXnGF8d#%J&~Y>o+T#OY$D#%q<2d=V#eIX#br^4KfgP_c^6 zS=}{vQIe>h)eRs25TN4s9Vigw~kX6+)demAY)Q`Th+PTTS8&H zVeVEHW^=KxtgGw9E={GfTf%$v41<7tQ+EwnSvt29aJAZC^C!!F5TA7-Tr>@#6=!F! zewvpI5+C>XpI-rNzwn(tfON+1(hi)kNKVlTzN$DAY4T?CWe0lO7kkTcB{-OCSDY1X zwd8Kdsy{p}HGb{-pyRI#ytwd>M_XD}s@(knX$^OOzCO38J=d7A`r$Aw6sTzJo{a4| zU>4({w%AV=p6|OJFAHvB&vj%k0hiXkGYv#;kcF?ah&-Al@BUd~1fa_$AQJDt zEdTcv^o1>YBPt07eTxFkHrvU9(e408z(le9-*E%oPW}}p0vyo25i%VjA=MT~i!D+^ zPQNK%^SZ<%^=zXBKe>nzp3%xY-da9Sva_@; zYB!+IERDU8l8cX;Hr(IJFT2cm*SKIjgOA4T)fP29CAx84OHEochq`7?YuxIOm66;M z1P<5;2EN;(n#tTb{)=u%9Eg&9muI#v^7I(e@%DVJyuD#FVGsyx{V0zgpj^ZmBRCe> z{~TB19L}ONlveD9lSgr)23e>id~J{tB`zw7@95>1Ya`qo!%xO!>jA%nPbSl6-e2+YnVZmvQ z3zxu90BmQEAjJJFO6B$T=4QihgU=7%W)ge(L~ikET)u>9}|PbvtMSPlma%=Wi11fFk#iI2?&xy~i7UaS~c+!PQM z-Xy7PwnUB(ymU)AspHpJ(J8P0@LcAZ7`8~ygeK*=?_Y;Q>L`Fx2S>2ALOGiS5GqCy z=gY~(x^xHakCIIfJ$Ky~0{ijP5T&G(Yu%8knsyz zf}^hmSAoY7lE_|vms@Hf3&G?%ftzm%IfScXc*y~ybX-BI5meqEav8jm4`Wv>U|@6= znT>;CDvFW}H>ne3Uy`51atmhG<{`l|(j_uXt+w3$R4gj(H;lv{pfplY1EqIdaAq^mupL+YI?c&5?x~$PrLYpJ zh>oz^b_ycWIhmzCExtO!Di1VbL12{?1M4s z|2o`fzvsu@Gw>eUf!{F1XXEnDANiRsu9#zxKUT3xN5B&n=Ji(92bMExRw(N2lO2lr zPkdh_WY&K#i`pJBv%20K-+WK4EK~`yRa?ewlU4flGkB13= zU48BIE!z@%%re!@7`J-$EZqUw%aJDeSP4`cVFq}SLiYMLqN6X-*?F8)|GJcKO!^;J zpT+j4<#O9_%1nAL^J^8w!75Bz1Z&Z7x& zYql+>Ea9$K=?UYEGiEn&vfD^7U`sF&A{QUafteN@b^sMQAk ze#L);4sF78I(?0Qe1cC#_?P8Td^pa$!}_GIu8yv)pql%|r&ppg#J%NiHyyg{U!A3Q z(D*u`b%i}hQ*#9XqoI$djDIVG%Z+W zqElgi>tv6!Zo<}F>Ca``<;}ev?J)t`CK1=gAD`K?V-%=s6k1sH!~4o(eiEVhy0bFp z2mV#{wFg|2j@>L~A%;HE^qL1SR~xRfXH9{s&gQ`(m~X4nBhQLgk4; zcFh~WJ({a$Cfw&fLMBR?vUA2!ttI%<)e*Uo(;U#_Wd8Ki$8FjswJeUPXW7spBD6ty z*a?{u$ajJ$l$9Gt29b|jtli>=DQ*iE7h5yi7^=h8U+`+!=dbKkMT6%nf%mot&L*?J zhr}$nCcPdpPkXrQqq+-eR%{EqhZ4UciwNK(4Y-SB|_6SJfqmZV;2 z-4BMbdFCx&ON+lc@QdC@~M?AmAo7L)d+$R_TvhaW27={`k zs-_mp@U@CZ%p_9vg2wN6Lutwe7|lS3&xc>2+24W_b4gDK*u}bnCI72MFrm@L>b3oJ z$&#vm5F^ohuYonp7h3c0@r>18+A{GME;X2QCwabv?(Z&9r3mfCDKI5W>m>LWf2VJ2 zi_J)LckMUYuK!M?lIq5!ss+uy9z z`+j?-UR7isKYATIE!KIP)WtGVoC@WWObKND{{7|0<9rGQFo}aO$WG7S`(Wx|B7^&W zLS&zdVJ;^_n$kSQSj8Suxr`+dok&|Et^qcRFGaRV_tDIvwa2i!1=8$du9!ZtvrdOhaoVsE8;Oi4 zZ%+{fl~QI$A;t#Y(-eg!VaNi!Al#aF%l=O!Igl6*;wnypa`_H?WA8uGvBZ%Ep3axb zzD^vT$?j?KTz_EyST_tRI#cPiG@_dxY-1)35g^1#=@U{UB}8I#%N<|#6%+lnO%Y_* zfRkR`34Qnob4o{pxwC5pKLHQ^Y>~jA4~mFsu{BdMv*=ViwW;1F9S~(6ODpBTuP4p> zLzpCf@8#j4fsVC77j#3mq15jwC@1Qhsb8;yF*#-0@Moz=xG(DqwF`Oe4%DY_`7_X; zw!FOON8h-c1m7;vy5&dh7!$7-Ug&pDxW1KHC<{S4IUl(1NKQSi5wOj3neM7|<*-`0 zVR8`AT$p2nB)|N+42%I0y*JOngOFPTxsI1~68VT&f!Lb%qsotn+i(d)Vmt%m6}w4riK6o$igxbx&Fpp;${&kIAxU$Wp?i-Je!`@-+eeEqbL zHWsuBt`(sDvaKHZJD6OiZ@4lj>`(Jo(1sp-+aIrExBq>yUq;0ID?>95$i}*>A$RvH zvtJYz%OZ?o#Fm3y=6ws*H0t)9Wq#m#2C259cCV-?sT~0j14I)rtCP?2>jTGdW#X@A z@F4a7_^esBxfEm0HZp6&`lI7|&8E&p*Eb>hu_7^S0y3c54LKvlj!YKpf(xQT-#@pZ z=Qrr^D;>0v`?YMNvE*vdP_ZB$z80wY2#1ZGw}fom?fI(1!RY? zharko&t#E7oIec3;M@aSQ~_Wl6rYJ)Pz5LWOO9Sl~)me(-YUgL%~u3`wjuv!ScjgC}ds)+1)cIb)sCi{^! z2O}0$kKisZNzQB-Z4nayYiHzH*J~M03hiK_G~uJqe?FtXB=`mAURr%E>gY>9_QxZs z-sVxtq)orCfLxzm|EMQy)Rz9wi?#=2{Og~{m>4M%E`vhk|N5yH>hppd*&&#}Zf)o{ zX#AW@=0Uof{I1~4*ey)gY(p=kpX!f+K?u`@$lOAA)k7_2-4~n#J}BS>yQr_d=4050 zbOH6!*ZziS9p5KNnkS&Sh}66km6*$?AFNkg8FmqR@S7{t-$c*{0mS5{U5tC{J~sfvObWcXaDmn&MIu+i0^(hgRd{){E@f}!4N~j zS7Rg~u#b?xd%}F<4=HK7SWTpVM=DLQSk8|D_a>U(NP>xuyA4)!E}J|};K&}INOzz9 z!Ml&;v@L8>Gy5jw6ZK4(a3f*HQuWK$rGD99N9w3?-eXy**)`y5z|km}`6#KB>EL&h z^6jtPacl8Bz2#aPPLC(p+>4mP;_;*(_SPC!QG+{%p{#$IybN5QRO(h3_$9meTIsdl z&V+}XScG7CSL^y+|6U+6UPk429>>COi>;;6H9jdVfs8`Gm*L#^EE{eZ}oLHr#WZ+iaE*0)ulrF z+9LxRCSDSf!9oK1Z}zP{%ZHW~Bl^~PRbu-&63|SG?kTtMJ21O$_(P-5OszPGs12td z)AE!%FC+l>fCHL)_GoJ3%;W^DRaR!fIaUQ4O1fddb$XS*MOf7zSpvbPAg=xSQ)yCZ z1VUdw0)29$%jQ_<&i1HX_51wdmq5X%u-PZ7bSb4u?!@TCF$HWq2G3NNuN{?c$E;pr zyZcLm?B;?bZ05c_PwsYYOO|6EsZA{X772O5W;W%okpUa;)L^Q}>+yQpz{ovCUK3S0 z=;|r=DrA6Ouu`_h15hwO^NL_z^bkfK1Nh>=SSIuO81^ecA~Zc)sHm9Wx}ZE(?LOlzIEmt-1UY#qg|+g2kz0FS;u$^k9o(4yQo7f)k9t7uZEl*;dNC6#8d_C1kV3F;98 z?*R+@{ubA-sXcn3P6J`v9hME0DMO-@mAK1pT~I_C?M6qo`r64e=t;^Pctpg7n8y70RZmM7ijG>2f+Cy|G`CTUbk|dT|gQ3(&?>~E8}81sHok$EAP*2lf^TjLXYE*$8pC?w(~6wjQxhJYU^J!10}}kZ zDpEz+lMGEW?x28U1l*<;GvLu_RA+jy1`(?U{YqB48F{PJ7l$2`$_mzcdaaqtqxDSu zfw!6HO0!jWGIq~AKL>4MI~MXLo8T#mf(!p?z$%etnmQ?eE2xrX5+ib<6dl-4CZK*A zfv&VqfwRhB&u#c;(%~BeFj)$hn*$g?J@o!@Ko|12Miu4SZ&kKce=GcT5*SGB6t4xX zBr0-R3BtQpNcU#?75`(QMkY3zxB$Am#?Am9e9aR`u|FMw3?L;_L;8jD-xeMTWJ9f1tsq(hOI>X${eY%%XfYvo>^+urS$i9m8t- zHE9=XdgQN6g9)#YU7rQ%PKI1#X2yu8RqARvG~EA)4SgwlUr__!`*e(nR6tcfY#E(QghcZrf@ zf(Cn`e^vUBqySN`UQ%T3!x4*i^?#7W50EGhFQv082fPCVFdt9)4vIz|U6JEA=6JMV>ptQ3x`d{d^^t!fOPVS8liqx$i@`u`svHu2`Ki5=Z z7NPf+$h{+1;~T|e^gODH`n;}?HDJ?lcyV${+wQS6ITA5TwAk)m%!5j{I6;RrB`gW469pbpy0vf{-*Rz1t-B1DwL=K5@~~-Pqk?`f zE||##c-}KjbTf?2NQ36?_pjUAuM<^Ks{a3Q#ST)2IefE!8i;`O!)}Of7bX#&bSawr zNgS(glRFjXBK7A9lcJKj)&WVjq|=;2m!M49l)?_H(OFPHb*R_H$))Z^6y3fvj)b)I zLg@7#Q8Dq!z+UpdL^OaPMB@79?oNL@@Tq>tHl59$)y$urE0A5rf*vhg1)whae21ST z5MA#`@n;$X62&jMkpM5lcVzRsH&=ZJ!^7i;b6ISi=RzVtzDPUoK!G1xZkKaNs8 zfjQsCGj80)u(dyg_#mduVBk*h1rE{c?D8?mki(v1U(~hb>6l*l`U~N=eiv?ML&dc& zJdaoVz88IFj*l-`+ODf5u-+G&Y-N{|SXz;z9x%!Z|HDMQUn^&v>fEH26W6LWGM`os z_DT6^KGbjO`zaxjj~`oZL+yeK85r*@+81t|jr0dQmpBEUulnE9SR8EYn(f7`dTzXf z$;(SBBO5|TkZFeZF)-2Le3*ePt0+MCdP|{{=|nkZDoo;2Yxd%*K|6yBr$kE|=1C24 za^gn35?f^iYXRriYIZIo0p~lMM5?E;rBY4OZTln6ovTMBZMn@@kob*5PLpAEd%Ork$O^cu^2I*Xt@i#kAy4Epn|#7{cy5EGGfnIy4=VjhFH7!a(0Fwc zi;7Y`Z8~ihZT3fFb5{7766*LeXvsXS#jLF~-IQDjW|zCv_B%d{;IDjY;CU(#)b7+@ zY#9`K^)OO=|B3?&oyu=ZU5{y#!OjagSuNsxLv(HONbXqcBeS`387lsXOEv~@V!F&`hX0uUP3h;iqT9iD zQ!;^J8fHfgE#_oMo>ssEH?j*d@_s`lU1HsruTY1r zq<#CopN!N0PzW0s{@y16cGOTs$hW4;(6Z6Ie4WrwuMDLyTjN^y+HFT^Ut&VyjjiZI zlOmQB_;~a1fVnu*x~yRj3hnEzBRt8nluV~0Q0>j*>u?+ObP&J&8v=E*=exB5JgRzK zyGVnrGMj|zn>wE?fE?y*t8~6@Q;SXaG1aWKjs7aE>AhZz+v^C+6-)GBgylyd97_Pv z-%U_>DV0D~d>u`sV^RG5CKbjJ7&AL$eTPD&^voFyES6wFJZ6h3FKv#SN=pmIc5*{~ z_SQYlf^}$jnK6it^$JuuIu(R#X1O`XCh!v*XN(3j-@b)a?0EWG*;iQCPVxwd(h$C7 z`X6+?Wn7z0@GguMX>lv=?g5Gww;}1NBw9om! z=bR7cYm$3*?rUbQ-QVtJu7Ur#3lQa6)_tN~o22#S6YAeoAp<-^OE;a`o|C~z?m6np zu#5pHS&h5V;Ms`Np}!9y)TdV02%3#SBX0$FSNv$^qMB1O{a!H5KX>)0=B7iJwyKMg zhU|B|@eGyp4A8u}O(`*aE!e=xzDKItBcRyZ)isLgl0$Lz3$Z3)QUUjjTNVZc#oxRa z+++H$5EzF=O+jq6MGs*YkLlo=HDhyX&*gIA?GWddm|@+V4#rNEPv_$zwP}mVc=161ETCFXwag}sY&YZ2OTD*#fL%YX%ea&QH#0T;LP_J8`5%T3+Bi>c zZ&z9US)Ow?M-6nf{UD>-j}gmpSm|u3w=bJaq(VZd0oIf;qGa-yzW@NdN<^qjoQQk# zL%RL(IV@5R)b$0I5r__R2J0ri$^^@&K|IqTWa*I#8cU+iZ?#ihN4On0)k9%}lMAv+ zvcORXZRjQZ{}WKAEp(dkbteItCTEL|8E znv@cKs71PbJd*8ai;vr$tAp_ArQAQ67~Hn62({nPR%PaQ{-&6k{49huLat;24R&b1 zr-uVYDv|w#2?)^k`@^McU1K!dJ-IHnt$~nqyNjOx?Q(hcp%E)P)zGh9k066yDloQ+ zLhlFOK6WY|?fUTXuVUNHm{T)hdm#I7^UzdRuYo8T3z>j*kftY05972d6OSj-4?fEC zrOFJ}2yk(s%PYsZ!|BumCG(gM*T&`xjEV;yA_VqspL!3n)HUe zb6y4=7UD4K|sF{d#tVSsiV#?xLx_I(5 zYtrW|0i>pufRSH;0}D)|6M~Vi&`4c*lRJIIrcBebxaw+Q2Uf;>4i21!K56=@vO>sa zq044MKVI(!I|?_~CSo(nm@XUh@z~LlQ%A?cLodD%9_g!Ae;r3@(U8IlC$6%=Js50z zW_YP_y71=O3;~ain1e_z^1BOg@cc1u?ufwXX~K6xkU+Et_71;xg@x>k`&_drQPd)| zV6#hS^!ZH~kc`cij+|tRtB6+Ga?YImI!vh)KTOoN85o?ehwvI)+w3aHNG+ z*o+VjoLWPimlp-UA%NolEfEQjedS>ZLn3N*ak+K&PJ=K8R$H&BG7#E@3`y_=B0OhN zt*|IiB$rBm@QSG?qTU3Tn1q;MSBYt1M4q96Vat|KB(quUPzNU+{)7sYtr8D$;Ih`& zcL4UR_P=w&OI&CGbL}!EvQHzc3#Ef8nI`ikF5=+Y%NeZXH?>++hve?Bcoda-0kvPI zW?;o;s=uIvj@*o-a<;jz`=ixAfTZpT`BpIQaIapYW3)szzw&p;`gS<6-mQOkvuY*{fUK$VJwB0@SA81hX3+6!oEM0X%5Jb-$L2s&C8{4 z3v-UFSfU1&qwX-kt>jCp)1KnMd}nHo;q9#M?q|dU+2>%I!~i%7<1oG z^JFj>9*WPz^h0Y5y7~RW#Cp&+Bl1i0SJ+U&O3AvH^&xz%6AoT zG}3#)Ko#Ec=n>Csx{>6V4sGxarqrqb4qOM->+k7=Z_Vqp&z z^_-Rwrds%;(xJ^~i)b?tLh2!BXc^U5B$*%W+V(-rL!4(SsUS~U3w3qcZ5CFs z7D7L{mCu#IT6E>-YR#2$whq_y8f)2arV89M$}lvFDd5)L zq1QW=XJf{6R_4k0g`utdkMtI1`0nJ0OF6fwP~b=a9Ujk?EbK4-1h`ZC$q!^-bG^3^ z8J!cI`MqQ~ysIBq!e2=xJW}0#4N#=>7@H1EL;c*0JvAZCY*MtW3{$-N7U!U_Mk<&( zRXp}5IJ?sze8dOmn>QDsDW{_>Zh^<%RmPT;jMCN-d2Mkqj!Oa5M@}9k*ZWN3wJd|a zQt=@Jqto-f3bZYGxOg>XvvYkp3(3159fZub4(`cEJpoS%rxq5{c%v(2zwQ+%&=G+& z*NyC`WpE&dTeASXx9pGr3Byv}1GawyOb-z8hJ^`so8tNt{!};;Naq3Y?^96VIleE@ zyHSH%#J)ONL;t_=9UqK#)Gu zt6$bF`DYVqo`3Ndim>)oQ2%oXaD83FNMGinrJKqVu_Ayk$%wtwt;_|WT_9=$B#-Tw z5heWr7Y;rL3*F+E3?zFD2&;Zt&{_7-?%~$4=g^*48(P28GyyT7qXT%%qY=gn-o6|t z{TqQLB0>n3DaX8b5DDz2p)VMbA30d5x=`i@gz(OOJ{g+JvZG>XmGpp31yc_W>E$y2 zJ_~`VFnv?OfnIEwe5|*pBaVCnaB!@KeM@*SFdqn zz-Y7ynZqkbSxHr3HPq@R;8O|j>Y=$q{G5_Vn*AMxVwB&@y#s^)aB6H{2brN-8Mw)z_q?+dxz=()Eg!3F~~jA9Cn{UDq_!|=@y{tGoD0I?PLT`=ym7_-c<7h_2M zV&W@-w;+awY&B*b-~a^=G-|-CE=whZn6Xe0rkdLro<__9aB9j;7N$7nGDAD}jB8Ju ze;XD!*sH4e)NQd~0ToR$72Kvv^78Ojhyg1p#eho>jj>z?jpRGw@fWl)hp`U143*6U zQ}S-PU4IVBrAMT-E!}*K&>j7?w{8*}thg0nX8|?1)o|}NXE0$&i)h9opPpk++XeIx zUueV{EuzOY1IcA{l*v9chEe(p^Zv)G+$rM^Bh>?Ba`ET+^62FlHL&DXsIPX8FzTC;)FdLu<)K`Y=1Sum1KsB zfnne{dB;ZxQJJTO7e?_8>%6zG$YXy?H4ddQ+D}EtaSh{YUznmqe`RrTsQy;LpoHf?xhKv_iA1E?w zy7h)zmGxN;Sm+R^%YkwDIB>rCLR6!r5;)(S*~mWtl|!68|7;vP+Cyz^^e6#wyn4?n~4m6tls@AfRR8i|$21X>NVhi}x{&*+Q(neM+zK(K~K~QMl$XVW6 zRBRmr^*R6D$3Niyj0ouTzV=rDveBhG6T4D^!B!*5=*qT3%l0WLd=6~K`IozP13Jxf zB-i5|K9BBXcYEF!YOmkT9OrtbAMEF%#zh>ZvZR)+e5^4HwZ3qYK#J`BG}0 z49i=V#F<3}>&v5q$^}_&z^hldyyL-N1^t16f!4!Qg0AFl#ug{n?({Q1<0395O}|}h z_s%SxREEaTLY)~3iZ1%NG5Lk&!WX9NltE7>H*dmk4_hCfR%*qL{m&n6`Fm@3o-Iy% zyq09gxQ-`auhHBs2A!BIRENX^x)`t&#PQ;LW$Wa^4eEk1k)3gJ*gskY?u)a&Rfb0q zCWz`!btTJL$a(XLuz7bn#O2qA-QrzMafe6byVt&JKYW=MO|c?*;@Ed7P1gK8D8}v! zU&r`hRRlEhnYn%U&eSJ4 z!3s66HbRp-5IuJ)P+@r!rM4erUSDS0e#Wcop0)sC4Yrey+Y{ucP!CqgrVG0tS@+vK z>vu%}H2MlEa=1uDf)x}pJo6x=|))jWE>x`pgkKFch^z$^X4+W8Oi zY`Fy;LysO9pGU#p|3oz&yg6&M(XjS|u@J;aP%aa@t zPCB?2|F{}Gpa8CWHhu5MG~d1(aW|!77^rZNWTJ|1!SsNkYRg+>+d4q$@sT2ie6zT+ zqAjE3>-*CjNM248L>sa;0%(8J0!WQ%02q17!9jfoT8Fq7V1RfXp=*-?M-su}w8dpy zEMnNHh8*4c4;o0~&uyIA`GszW1hD55wunXPU}>A{`0f)wK>x#eR8f=5j7%dGsc{G# z;|{2%<)&gI!F+|Z9}3>(u09I8&FrQR4n0?{XPP|#|7vhPOx!g2S}4oU>iL=Kdza`b zaN70ACLDI7U8AVx7ut7}tL&KEN;Z-WXa{60h|N8-tTBUFnC-TGVWc40bFESYP(vPo z&H;5heweY`DFj6V1Mt39>$AzSQXKPJL9cK9sOH_M8FXn2C~?+sa}T5-ke!zlaD@?? z4J34NOr!7#A4~LfG8!gLr@KVbft4j3AOJE*+;B;i9$Vrm)KPB^6a$JwOZ~Y^^p#k~ z$#y3B|E4FY63l5eTbjBJ4WliOc+`E^u~d`pNEad5hq6KC*QHU~G63W}%!E7DJ=6G$ zf_Pxnxktb?TJ9nbyYSO0mHJo*l=z@!XMX6QwDhX178y2tnGn#DD_1SbeA#{%3bwas z;>$lH`8?mqKG06jFs~d44YnjD9ieWoUAhPJy~_&x8wFXs+V`>jS(~V)f_@qyI9hLg zQ$EPQf-WoOqUgU>O?E%gd)3MrS7KKdj$W)xWM9@Lm<9tFR)O8nZF$Hdiw%^mEO}9* zofgszeLcKqjFLO(AYzvgT}@yh7*6Go^)0ujX#uvGL&CaK>-M`sVeZ7aOI-3pH32Ot zpfFh?^b3*e^ibU2WH|R{S-aTy^XwwRP+j!F>5n6&qUE5&pVI&{^ofh^Zs5t(rrCJl zZ(4vfm+~g%EH|E})@cn=wdTyT|XGi|FFw)Tz=E1pod3%gc&g36Ie^r*^uI4G#x5N#^m_Z^?!o~I9&k5g26T=q;fwRbK|vAN;OfwR?#HE;62j(KD0CRcO}QgesIDaH9_`QK=9b)I&we{k_OXQ`dv#Aw zo4ZC~R>eC?2P;W=dEO2dS;`4Pb8T<~x1-d`3GdBX?EBWSRfa=+h=|v&d`HH)r%*t@ z9suK} zT9B||BQVa~#Kgh}c~|uP|DQs*WD#hpr18W+b=#oB>v1BAbV_0WCxlnbWLyzYvNXvq zMcjt*@$sdc357rn~=kxhm=i}A;;4SRfXF5pzW?;OX z_KA>V+Ia23`AF@{YVzmfPH*8S3sO1@n+iX{PNCa>E6U=`V1x^-I_r ztsQ@gdR)~jckM7aJeT@S_Ko!T;I{3U;_Y120_e1@yCYjAsTnSF9C@TAw1B|_VBwnOl$>9;Nu679)Ym*`CZ|*lt@)P)Vz4Vw02}9N-uG* zj@O^lqPA~_0M6x9AY+tcjB^|%63W)&Sd%$!5!dSV)NbeBG)(Y?j)Bl*K;sHGE@ka} zV{0qIGjTyiXS+P`TB4%}wa2+!Ng4zxld#C0tn+y3$9I(J7arB%x=~8rWjmJSRBkXc zXUuBp0^g}tn{g!MnA0S5@z6xdC}M&X-uw!r@CR&ojoLW9ra$MfAO|!(#k*G-W?+9D zj!R8t+(yH#mCyHq*RlzDHTjAf-~o$taPdzG)&ube$V(-DxHw zmD^tdxwY?Bt?GYF$E3Nst;%5U`h4sb*J>|Xr$}X?2@gp6QTjQ0B+iHcnagQK2d6Bb zd)D~7{HLSQkrBLeq=+<`$qYU)@QHAbi7IeHkDr=)bX8Q>Y&%J0_PO2sd;JvMlWqJr za^ui1>5sNpxA*xL`1t&|Q zLb$cqO}eLjZ)4W4rL@3;@|@*?T|^j*22aw?BL0exUY5z}YJ6yiXsyeE|PD{wCnh zd=#%j?*V+Y0UnD${-Vf?%OgmKu`T{ufa!-*yppBnb@9H$~y0>u$&&_TCTu$im=s zQZrT=Pe6ZR*eh=DZ(e+b;-67T_x|b=AcPno(0n50icHYr6lfM~0Q8=(e^X@dL7{?( zE&}He2n~fAt^n9p&+SAFbm$Shklid-IfWZ!*&27#3z0Nq;w#OkS1UUY_kYOwKQ;(~ zqtC(3nUI$GUKj73&sE-@o}g?RKwt_SOqvV||6qaJE|UPFHc_R~M7rSB;oQJr1=Z*m z>)v2*7|=EW@3X6ztLx)l8s*IGw2ET(8o!GrRTumc9n!}mpp2tj6}2d>bFU$y$?l8? zUO+G}ES?UT)B)hFdDmY5Po(!#Z+(+;@~+t8}wH$%k526u9Zp zJQ=3V`~eVoMAYE|__xja)0j&a5QtrtQ`7QBl}Sjy^kvD{BA;P7BGh{`)<21T4GyJt znkJGAxau1ad5^;muIBFp|LRx*R3l4g%qcU)%Z(V7W(-!A>cJC-_^CRa(}?i+C* z_#i}IO0WI!?&ec6QIJwwvRqroSSXNK&Aqk38|3rYgTDpiy8M)r!exE2RGW`jTggAT zWI@$$Xg!nZ(>ShfK5iYDdi@O+2)R`tz!#e0+&x^gS@)w2`s!p$Rt`%4N3DGj!p!VR z6tQ5{b`dM}8TJ}vQxz_Y2w3_WCXw#Yt!@+EQrgh>SI2MiL?to}_fcQ|6-+ zVgujYZo8%jEB&M6gIT}ndoCT>GEd| zQh;Rb4?w43rDEjL#7ye|R7*@Kt6~tJC)U7l3I8E?dQod^-|1O-X30kX*EnewU4|0$ z_?S0%qh&(qYhh3`{gglovx9UZS>pRFhxY$6L=yyRZzM_>5^PcGCf)!T80Q8uWu4+9 zoDmtBwT~SWv2TrcSe+~d{v^n%s8d}WMPWsxg3K(H!0GFXPhd2{kzR@f@8OubXE4Re z=jI283-`Du@**)xrpcw(tg}U&`JUGKp1owgkRXUKh>-!iOgfA>75y#DNfxiKTAQTT zc^5-5L;FQ_S+s`HWmu0n4wMWs{?-fsy3`h1tQ(wd0{lPp znOsf{uF4o&D;WIgZ!~p(e-F*i!zwIM*7OcEmzUsvWVs`7{W%*NofL`?5fN6s9c`3^ zGt~pChCVp282TsNV`*z&H@d&=6drsOTSEmn4aS)*5s>GwEtjF82~vg9dO9&wF4K7u zrSUfYz_<@&aS70^MK7qBJe<5eAE?UfHT-$%G#}Tad=bmHGLIqHqzT25Jmf-4qqbI@ zlQ5ONHLLr+d0>r74m8!%VfzL?X~jCpZgFpV_E=jn6khJQ_qn{_3y+{^4ggTGhgF7u z?Rgo^ary)nzn4;9y`?r$5f z-@g4W)=3b<-1WMSV3d#+Wt5qQhu;e5He^bu^)1m6+DB_v@USs)%OxD^*}gR6^l&#@Cq-Po&xWG@N zieIJiZKH|;j8nT&#ov?G6`0H=r6DVOS$WR1TG9`!`|0GNr;KKJR{lo~=@*53bN;IL z$I3&ugDhcxPp_8Fz1*k{3QS*=ncI{Bl%a7sDN7_=33FZ#u8EF?3&DK+HkbGs01H5M zix>2QZ^ld4d1%|s*75fKi2l$pJ!9x@5Rw^lEsYWiXN1Bup$N*?c)`4+tX(mMa**o) z73cb{K_>ne|7Yg~%M+htKc|6`TqQu~o=Kl%fSBhIU-MD+wl=Uc&>kS>;K$iK|d zy+y1!Luv19yZ`ch(ARgu?*NV4qozyn*PaIGDqdLizq|o5OHwhH9`Oq9js(n=@Tlw74Uh#++ zTU-9G&x|SAXxFoE03U|NI(_{GFD3(lD{kl(D$?mQdStL@;~_-s<-| z8{3n~T5FMM zRhTYK$N--m4jZ7k!`7R=rF!i$AL9JzEMS}X2@T+~H*kD#Q|UmIi*GE7r4;hnm=Z0% zf`#=~nu{2mbpD*9?Li9(>H8URdwccR8a5)<&JHT+d06oOaVLdHTCcQ97Y#piD|k0w zX2+)*D%og=u0NSpPu4-+B6^b&G~PaNi>ISJ9eT_qnbXr745Ku1?Jke!PXe1%z;(SD z9&*jUoC&u!IYpTOtE6i4Q*y470VWTf>|K>7q#`x^4JL6Weg_gHwdaU0WB8glC3z*6 z{2W-O z8O{*@T27{!Y4+Nf+vDv)sZYyG2KR(FL+u>Q3Z$4|Tt$PiSRV)xYaDJAl)SH4YK;Ctts|@n>rvdSKJOhrG=pUC`x7Fwu{Kt5 zfjH`82Z)YP=VL5sF3Dt5HPJ0Vv)33i2e8>5Fp3!CGUb_v#f606xFq-iPo`(`m;r)A=464)g5pdp zJO}H92x7bP8vK((5&LpId*UCzX}(_p6l&Z#F|GJ@CA`&SO>JT^lhdQyn~#=OBAw@n z6QUs=PfGrlqOU^)NPRi5YWkulnV*tnsZ%vcJB%Hg8@ZC^SY;YVznp}27}fHO5u!v$ zi6#^Kqp;)t;K$**QP^@>5wHiXGJ~%vl9o2_lVCX_tcCm4(Vg_jFgsf`IEoHJ$o04& zn9UQDq`8t-WDIg*80S){Ln>G4s0KFFpVa7u?Xgw&KJ{L(y$ z?^4};uTJ8DE+18#kik7`jDiBxM`t1vd_hlfm?9-5l@_ ze|JUv_U%sO8=B)`2y~p?By1-)+ikuSSz3EV2n;O8PAYt5N6(-7-az{2agUp}r4;1K zlY1PFA)qwd*7`qpCk!{o*0bVYlLj|21nCP-S8;SnCuK68I_o8V4$G?4H(BPQZg6go zue;cH-?>^h5mKJ2kgz&-Ya(1|ge;%=h|IAEDZ{@qvB`=Zu4>aI@POQfY9ua3mUe!hshTpRME`Q^W>eIjgQCV-+d3lQa$#@8RtFOKGo^izpY_ zp0l-PjLIe^jr0{xRZqUQdEw@l6perylqY{Xp7}N;+Z^_LL-rzh1diGBj@k(c(J9P@ zV?RNq8%q28%O)Y+I(>!6X+0%lyBOb1;vYc_x)o&-{bcTS-!Ov|6)J9%$G@dc1bDl^ z>OVRxKXy(*ixXQY|8q-Ykv6v2DnEw_2v0M?EL8PcpnG^~)4=XS+oD0)`v-Dp@P)el_(9C`)v=BC0r!Ip?hIne#o}) z+s;21Uy4J!o{pRd*`!Hep++wtkr5TOCEaDpqA&W=6P-U*a6`Mo$GR60TKO7}W(Sr`aXX0jhvTsrbfAIb43-Q3bc7f=KS zmhAlcJ4wG-lty``1MP1J=&*eieUV?3MKlX&3{Z!wBt!~)oY6)+@< zfQNZwXgD`p2WAeiEpjfwH>^!u*k1W?&>yRor^K_g*Gfnz3kwmNZ$}+%K5=n-t9j80 zUxj^Ui-@qtu(>qYBmqlN|G~6yl^-I{g;R1Sor6p!4R2;22&6le&Sh9*%G82vxxmlJ zmvHL(jTUpx{+EpPv3q{$^UXdh_gSS=bRcXJb)uH-*Q5Q?WGVgqn4TN@T0S}`-$ZqP z&}pnMK~F`5=L;nza&6Ew_>`_$`99l-Sy8;RoQ=JN5`E%FY5kjy#h7mVbGVPo_knk$ z1~Pg^;~b;)R^q(f^u(Xk?Z@2mBZ{v`P%hQz81UIti_83sZ18m+%Jhh3KJ22r7d}g) zVn|nPiYBK+H-mgrs~!7C!p+d>KJYRV*{ejnO^U~a@;m>*@3T;>lgV zvR1?8;vFXB{FRq;=Lf?Lxp9FGue2!|ES`-7*f4^;5>?e`ccNt35Ox$POn0GnOXD+y zM}P&IE4hs0xY?t`LSR4Lf^B1HM9;+$Iv>-8i`4e~HImonkmcmsbJg8#v&<)y&STKG zvte`Q_;9fAZ2i7%ywa(dM<}0Jjqhf@GjTDnv+nvf0OXD_qG&jru|eHX_2`FFn;-{_ zqydn6#_sKywiH->i*2s=zHQ4^By6@#nZC<$!j~(*3z42t`-2%5fkUDuY8K>?G!mqS zyVbn(Ik1?50Kjf9Ca0@vOm|Hij~uU9HA6K5l_$fO0rAb4B6p>ef%fNfyK-v=H&9J( zWzry5iT2DXdyCAmy&u>WYSvulsF@QQIA`l71=Y4*1Nk>ZI@$XUoD1+-evG1sNEjZ- zs^^DBGIcl9c}8Kj!Nw=s7gG>TACKC!TN0O#Q z+>9n0vN`A?VwtR#>oj>lkaz`uLU)8`MHQE;@J4KNNJ5QNm6oNE@a*bSg<`8Ymg)Sf zN|Ex2liQ`9K$}@ZmL#*LF#Fo7dAfgIT4->LFy=~@3`|y9+0ml^b`P8klqo{nx7o=X znxP>K>E8@&f(moxu!Y+w6}lGfa?VkAtEOM`{#I#C6xxT_NU641Z_-VD)oRMPQ~mR& zg6^NMbP(WgaUKC7_lXKr-OB3>-k)wC4iH*Jlmhh?az$4}Kh_KhGK_Jm6FUgT(oGP) zw%KMJJ?F5%UMgCqU+Ug9K&IB!x2|gz);?4SYdxN%`sb>>YXW_)S^FfuHKH|{u-=y; z#~46^x>c=5M3doSx6-dGLneTP#BvT-JBPBE2mo)nJnVO_kr5L>{Y{^oBG2jlroFlA zK+&A3hO#Q1g?dDBpN#x6ftYSO(uZ`0@WW4Y5EoUqb6y#Q$Dri$58SbGmU^$5LckI% zc<)@O86Y^V?Fr0_zL6Et8$?mxr4K(T#CiVM;4udo6!XLYrpSvaag%c8V6wBO{X%b~ zT!aJ#bcHyE(Ou>5<#_!791=gpjefqqLIc>%vwO261#zQ}*)fiL7o?&e?e5PSm2JOM zGYoE)9Yi#F6g;Mi@c3s8qWoJ9x0`4TzkrV1>6{29nt7KUcj}S|pzGR)L1IoB5zOmCE9E^O3~2O60!N0@h2je{YSqV=7y*v*X?c zp*IVZ5mu-WuIc9eZB;BJDWp&0%2ibqO*0p5U&-k(g5++993j|aXidcVt~hyTB@(=m zn}JfHvhozUWJR60Gh)w4uvBcGnQ18ff+kPIUd^i2&5#LR_4Fi(3`}<5=LfEjyWqG! zpM8aF+*YcUy@Ga6~TOcW?2KLXwHC^D&kQa9tHWRy5bZZ(HO^AhA#G1?UKg~eRB3l!V&U+9rX62+f+6S%kLI%h{7_@Nu;@yoZ{BG zWRejN;f*?obXK~ia;FXZL6t|t^HUhxNdDY__Dn4slZ<;^Sz=J2fAw54=#*?q93-Ef z1pez=P33YWQ!2EPeklaH+A%?-f@6cg?Nvv-I`>|YOET(-O#tYR2so$+xzh?@+WhFl)C zEgXI=(7}t)LMPMd22#x_X15W%85|L#;N~SoitG)iz9a~ZQpFdv5@`kRWh9_~RxuD| zexaWd9H_yK*p$Cf_X~?b6CC#VisJ@nKzDG4VFbkJGx0f#H_-{vVLUbCK%1$Uk=%oK zNu(N`-AaniYte6>%-?6+7(6Pyl=7r0-?%A*GaRwCLvu!jQ%baDHY8-Z&D9z{MI}jH|fz9s)VOe zr0%`PSE7l@-E=|VQDI|{E!~l*9^^t<%m{Kd%`{;+cj~$Q2XQ>u33utEKwR}H&T%DS z*!}6Ds{!(`Mp*Y?$ciT05!^v(5-uFUwyIYo?gN4%42oF$`ls)gitEVArDxoKQAdm* z9%qzYq7$P`RAGYvguaRtwM$Vk-_pM6gQBt$bUjU|ol9_~?8_T!7~xEj+uhY+|M#4? z-@Y~Y*PE^7Uxh|r&gz((J$HzS~#8>Bcyx!*dw(Re`zB0>Y?e;d|RE= z^OjiU14d;jouEaZHR>@-Ov!aH(*$`;=PWKjvwnf~mK_8)e%P&7yRA*)M&v zLhLCGzMB1{i){_VT-qXrVcO?t%TmDk`EFd9Y?U^-7+%lsQ6cgvq{kV-f6#~GxLD)2 zD$uwH2J=nfiq;ECC)vHN7gE?T(IHoy{rM;TG2#LBO3%)P={<6H1 zhjISQc)7bRrvLJJBKT8Is{hl#0iIt2QzVoQ&`HPS?|XLf5K6!E*!k@0u(P`^Mwuz% z_p}jI^}H{8uyR0tAh9N9FXHe06#ZT7#h(8q)W_xOX7t{hYP)=DvNkWwyYk8HTHyW4 z-IRlhLtC56Pp{L9*$CfLS~0)Bt}I3fo}WG4#^i-DQZZy&9R1kZJ^w!LKqh)~wM;Te z?)UKGNG2rXht+y27?K*Y)a!^Wc2c=l(0_p2tgZv&#Mz>|R!rZ5s{M$AG^q#R{oG0H!1w2nLE>J9{URH+>!hpt_Bim9D zy>aBLosgI^f2xF=oOX&~F|9Og7jV7L$K#$eSJLE4d$WV9t?uKyoObTPyIwc%1*56O z@fu(6$6s9?tGA0#;f~Awgc&T$@fWUffo)u0>y58n*$F>cIK&P7CDA7YK<{J9tfYY6 z;@B+T{k>vb&7{R=U(a$8?;|}%GG$?g9_P2o8-}&G$&GhQrM-?nLVUnU}%*IL=*+ z7aGyv#_IG(crwWEm-rieH97?6tyF|9yiFTd&{CIAE%1gc^sU^^NX0W0Vu z@T4o(kI!<9VyLmJ*3OJ^AFOs{unVt$HMZT0%@>Zr#tz4x;>q5-)v=+<(h z;;`XGP104Q74=0C(%Tp~YmD;0)h*$ohx9RDx~0FU1@!x9$24<`y`$%hv|af_$y4s% zVWYG6qD2XSKrW9KSH4%)^NbF>Y?k}6P*kcYiIj%^_oa#a{^^y4>Fx8oTY-hUy$S{H zy@xIM7g1GWDEOH|4Pt=KyH`}mkku&+7wu^b^oqgMUp&etzqR&V8e>u_dB2vyK>ngM z893?jHpbKNONW%{9RMQ#;-jZAdV5g6L5ViEH2|XU_XDmvkxGUb-Am!q9Q-}a8jJ*k z(-`LOvo#AfgWL5ZA|P1GtIMN=>T~da7C)Y|%rty7?=3}rXH?`x+mp40Q@K)fi@+XL zNbm!As$=hQw92)e=jmEgb7?&qeQ^VT04+&G;QED`%x>mUzmDv`s$}^-E8MAAksT{n z`Px*gb|G>Oue811-j>g~dHJL4_yBtUu#(V=C?=w@wAV+HDfQ`mS{e7Ya-;fsUh`C6 zY2w;*;_+=lEKFS-(sj^DWGO zzbMi+8nfW^I7{1)Os(~+e2^T!9DWfb?I?KZrLG(MpwX_E_ZwrV`dmQ^<66V~q%Dga zP1a91VVag!>}7zp;Rqm4YtirGTj?6a3WoZTDe%)FjmkKO&Gp0~%}WD*hmrJ3-~Oaa z0Q|2iQl7s$x&O(T`h0%%e~9J&-^#cznecy#%KvMK|Nl@){cG%hCs`L*#12+adOWg! zf9GE1F|$%~=JevjdlK~`uGnjfe%nXxS?W@k$1^QEtP}O~p)r*CdQdc40Kj`1+#_cH z-gV@Rv7%Mvi*f&URK z%U(p}jbV=>aksqa&%+KL6fRryHeuptlm1$a*H*yy6>KLh|Hi-?1gG)yTD-mDyTa$+ z_6q{Qzo_1mxH!DLUi8A!i*F}dljQoJKGVhA?)GQpzHdCf4&eX#NXFdIK(dxLMfxzRQBM*}_-|O_8 zG17D+Lvsn7(C^Ch`UQJfI3Sf{-^p5poQc=s{LZ{xo?eH=Q0ip``#*O9G94RP8kz{l zGbWFYWMN>|Pbe8BfiErTm-Cg5&0yOujdgCKdS0oK<$2j}ki+Z`@|zdBz}5W)gs)!W zW-$#`lq7SxO#7S~!z~36Wq+LogDtb%X<7T?p?^G}iEbhjOtZ50mBsA+yn@t^@9e7R zmGr4%&;Yl3`K~9qh=*@8i^5c4?sma-&ydB9FH#~1xUL2+xV)Q2yQGw|+4vcW@FJAU zV*@O%R3hFfz7@w<0{$QC1pcx=FDhD)ZT zqc!hhX;-frI9aa#i~_vF;HwWGbY^XLVzzt9MIs&$@H48+@KkLyGo|Z8;r#|f9lC;C zBi_iFg*8lKK9YA~9e)YS(T$g$C$3 zSHA-&dv(N_g)@Tsj&usWwoYWyO6xlKh~HUEk}T?G&zJvyoV{gJU0o9`in~LCyE_|q zcXxMpcPF?zY}_>vAh^442=49#cMV*g_nz|y&Zkv1t5(gmy1ILG&F(n{fUN;X?faR> z&$fr5iSy^=QU?bIP}2zI`hR#}xdCLh64Q(%8_{p&It_eyRK9DUO%K3xJv8!)S*lU2 zZO3+U1X*gCjnFv?Mh^q?;nUVOae7+;SdCHNv#CHmMMf{f+3ZC1>*_$=rpa0ubGA`@)Oevt8Bb9X_dZq0jg&@zE zPIk8LyjE_GW7|@$KqGZ3&_I35Ar3dkAWhZ9-g8zza#bRFq^$YBy!aKH7GPFCKuZ-4 z>psPHo9VE#+9X7n!79%)NP z0&SWLH+shMRRms_tLI(%wWe+~Crc6kR{H{6t76+9bTp!RIa)AAf4&%x#}dZ3I|kTE z=ox8UItt_%aHH3FER>lxV~j2}!Xhro65Qe^9LN%64yJQ|`St&wMf{sv*D+@$rXkES z^b%uzh9eWM@>~&2pXNKE?fjl5 z>Yn+rt*wqRtjW#u<1WQHLwwjT*o_Bl(`Ngzf1wy?&`KWm3nAp=Wc~vh0M!??eFg59 zgvi&IVf?QeN;HN*`)}<(kN^Pscc-jw|*SEK~9Y(UP;32Ej zr!ubXul2EbTEWZ!$d{0ikgl$Ig6Qt=v~PpLUY@<9Oi`RC9JiC92Thpzr@bo)U*sbA*$gN}zYepJ#psU#uAP-{Jc3$t@zS% z@z6CPn)(UHJN(C^8q-qfa~2EFUXWJi5@Xlw#_z6t#m3)O-u^@RP(>()I0(y&I6CFy z{y25GkvN~n?PxBmjv|vO-~lz<|25OCcRrWzZZS&^A@5?Ny+le$$;jvVa9|kIN=Q(L z0N+jMb^l6;+xH&Qh5G61fx176P_Vd}X1UenqsA%LJhYLwhX0pX>OD92iv_DKEgIm( zZuyzq^&TZ>cf@FRRJ;B`JHMo&>>dLMU55N3R&T#+(2F^mo4bHH>f9g;oI2ZA82}x^ z41}i2c2PS)21tdy4jsJ<7y%K4#CG%1in{2rF5j}d>cp!8AHdtdEZP#_b2b+yzj14qI%g~9~!=c!okl?rUQx~;JNDc~zGV@|uPZzawA5dYB z0&sI#0ePJ%hVoJRY|W7~v=(p@k$tqX8Qxc0jp{!J#^ z-@fSCIYc5tyjOQcHc|qaB)st%vSuC5(;+BOKSjuzoTqk_MEON!!z!z7(`4%pnS$$k z8d`t1YSlrqd=7@Rn<$OavZ;k5%Yp<7fDg!g`B8f7)1v56b~tn@ne=X1?j1?8Vxeq1 zHBDAWk6E@Nrv^e9gWqZxpVPvVE9nzwfzo%w7rJNcN+&V-vtKGf?D!iNy(%=Eq zLs%M;a(_WRP_U_F?_K%8H7;k+0hi5ddd}#1NGn*6EH|JinyYBvkY8#-PSp_Heh(&| zJoU2J6beK8*mTS8k;5P~Ql?i?WFq zqe|Wt+7kgS?g0F!q9ZlgRNI6}lxQ5pQ7{+&i+!-pvuxIC$i;gL0~eC;`G)aV5T+1J zAsI-(8L%ucd24;ZF4)j%ghF&X1Y-34n*U{+^mTQgL`WgLuk}E}U26H&0-oJZJ3CwF z20^%5tFv^veQNUK-$88llTpq@^-rGDk;l2u8ljxX-5tOF8Oa=l0SnaSKa8e7{y>is zsMKAdWyDG$=j>tg9JPtL_w$c(ms9AY9G4;G{gO)q){~;bDk$tSWu>SGf#r3l4SJsz zTHMkQz-LV!xvmy1l>s`x+6n$lrFgSr!I|jRFz!gaoI55kR4jf-CnkE}nYr<>_d4i6 z{F$3RF&eb2#-S;Pg9;3C^FAiq5K2muzy;Cc{N=$uN=h0ZT+8 zLywAz87i4hKHznLO^au zg!bEa5@eZdY~-hgPZ&?l#pxkunHRkIusF?ef2BN_> zh{dNYp*pjbh+1%hm`lzmKywq7-xX4uW*vs2jyVE6&}%5ZzB;_q2N91hj20Xjg~>x> zyh1{EPzV%ak01!A6rZe~qyEN74XzNB3dOz;S(WHNaN+g<=`)p)WE@aHcXL-STi1xhpHdctEP;RnPkfhI&<5hhp9buW0>A*RvhBx zPyDIGrE6G24DGE^HvXY$dT5o1XUPLOCa(f9XWTW-e~;?tvHr{9Pb>aK|G@ZVBI1QE zm4eGSO8hzlKl2Px;-5;6z4>GVqu&OIletJX2EBfUMO~R4 zxP2WyiGmou1AcjccEz{99JP7uT~gy#$%1W^3XFmjqrx^irm7Eei6dS_P4i%5+TWgz ziW0zZd?lgGXF7H-x4%IXV94rh3GJAG#wIPAjozwX9pPM)BpvM+mO(&F=_;|$jJ7w6 zku^S_fs{>mJ9G9Ft;N5=%lAc>2rl8Qcx*B1A_{L^itUN-2!N6s&lOLzx#zK{Do zeczJeh=>t=^|3qro%ye?q7Oj3wFd9|ct{rXP`LwQO7x2SY9pNm{^=cGLySaXeQq^) zZfWVg=t1gOCU^-2(a_83-JB6xJum53V15l^ybYW50|;|I)R+=(tY;n2tw$#P4%&Ks~M*1d0|Wzr70VG?X00ao)!f%Bda z#vKDNHfwf#uAax4Bfs;chjc{pL>raMXL%B*R-?J6X=;Cw`qI1 z(~Pe`dS3un6i{K`lm1k`_=_&l9?$5ipo@wLy3E8*o%or_8z=Q0^^R#?x3BP9bujMGFrHnbXt-! z7{{!zAYvUk!E+m3ueZg!%N(Ot*gxy>`>9+4wd?UVWFA$eC*)yV_h(?70Ult;*T z=?a3S5!bA+#2RAbZ8&j~VG@)kC{JT6U%t%&+BP{6yD@Z6KdKBR^y=VZ!8MyEuKwz zkG2u67`i_cCl5Pxm@=*EwbKSP3mbgEqu^88ZQG{;ytdD@vX7@idt&>B=mL;aK7d^Tr?rB^Df#t3PpodXp{7aXq<<}!K9 znRuxVdrJ6zxJNte*AKmGg=%CTEkO=f?Em z;o)6u@i>g0BZF?NGaGq?{Fk?8RuX6oqU(y>j52_%|4J_*+(%p*4 zSxGP@*bb@L5QUX~Tl)N3cYjO#bl*uHor zG@@Y6n>>#-`k;l}9TW(@nhmnp_ja4v7NqZ|J=X;}?f|D*1VfG9uBk1p|7`;h^u)|r zB2yCQ`Nbno%*Tf^dp;VsgWlN>MW%tmxWp9UQ;6 z=KDWC-*^chMN$Vb!!1Y zZ5_5B;?Bq&5XYF4CI3FN+*AZ!Y(A0`i0Mk||Kfi={UanoFF+@ttIw)nnPnqRRoIe5 zH>6gW<%Os4$FAeo@|Dn~AMB+>EKbO`I?1ATVx31!!VX$oDq)v9LJov{I8(3ziGXwAM}WelAMRhGKD{{W<__7w_llB=d_acPD3Pt&&&8 zZv`}tt}T<7rzWRcI5$3;Wrf-D%`W;bxf7BjfMEct`efKEW!w(fe~b=XulCW_B?JSd zq+s#!@y{S^|DgMZ?GLH@RADI%54Tl1SKY2X$sOK~jKrEt(FnhUqQ#p@rbz7Moqa$p zmhs`-OT9XM$vChk=czM+wyxi1F9e@gPqZ1RhD^z0a& z!;d+UPdzkZeU0L0Fg@qu&>UJfPA^J;{CYRZ5>VCct6 z7w_}vO~XoFG+(`GAE?2uB(>mlOuP}DlnVY7P{#w+hb6&L7iG&oJsTY+{Y(rFke9NS z@>a}}(V1w2A;So#K+n!4?c{hW?;hIvpOpgT*Hfm>N=xmP?($f8_iKFQ*G>NDs2^4N zGSY8K8j49yc+Q@dRyFW<^bJARDeC3e`Ou9*(uYJtoXF84#+H-Sk46z}2k4*HX^Fc?Q( zmWTf|@3eGJnfrBHX@E6%IDSkKx!j$bA$*YhhWWC0dwZQa`DNrzf)=)J(xO3qjp@y` zJy6SsP1h(=xs)$P^TmJ>-ycBkn^e$V;&>8B7R8# z#k6JND%{=O0FQBN@pX=UH6de?lDv+N2(Y84FL;#y>Pq#tJ`sa`@qNEFQz!1DrDVz$ z1NWq!R}XJvqvfgCNZ~M1xxUB<3{T~bQpe5N!@ACMdN`9Gf;KjGFY!#tQj)T^*DzlH z^(W?ZcJNLEHk7X=gU(UvZ?E;dk%TSJcL!Gn0m4V-PQ=MoVOhW_@c{U9_eO9HjU8q% z7Tj+hYz49RiAq0M3EMb$1&N|@s@MDn)(m4i&nW4o`Dn}_T0}rFfV`n5Ey1Xsp%Z=j zWT5K2#!nb^#L$PFktsi14FZFWX6>h+a&n2?Q10b{By!>tO!Pu_UTQWi@;|jNMKIAh zC0cw1bqx#*Xb~Y`_A#b*K~mXCDPra+(i!kjGgGivwI;msV}pO>Uov)Y^bCdyztlS4 z$#e>m+U@1+)*$ckd_cTBqj+QNDB1A5G5gi@BbEbmo?~M#wD%TBBLEGV&=&C!T3KOP z!0z3PrPHfFDgu|Euceh$=0w$a-X=1|(c5y6LC}Xs%>MHzrHaPgthw#Q8EC0Qo*^|l z9(iN<#jzVbDl1iHX_KIKdo__sF2 z(+>U+AayCPQg+pDt(q(szj_Da;w7W2$-bhUo}3+K#(0jdz@PkDejO|pANbqbCoGi| zbrNBlKq)-f6J^~xI1)^z!MDUCr|$=vi5qLOmIKM)k|V+>>N&p`Ic=_3uc{`@Qd+O; z^AhloBjHIRdDu$JJxqCBJ8c{;eA=G_=+%?N$c0>LWOc`k1~vbSsVJ(=Y0*}->VOm@Y)s#|-I zgU(F6Vo#sS4^cf7h-|89EB{lMp;I2U^Sl|?*}ZZt>eSXnqb-OaKv*}tyrZ>Z`1I99 zSAg;3@5J2NG$;S1`i1D@cE(h)Cz1WbO%gM`w!yziFC}FoOBLfs*m_VZn9jJfx^X38 zVKgT7+PaEGi;?YUxU6Iv2CATegWJDL{;X_xJhdw(>d95;Foe6GzL#WX_G|S-+OC|D z?T2u40{hKiHjS#9#N?+k7byea41hzh5N=^Nam<`zD5?D$mI9!U^4OGp?~53Fb{y;6 zK&_q7zxz8u61OL7ucwDc&#JVT3M>v{Q*y64wAepWQ{P}=#`+%m$L%%IXaA*c`}jJC z0XL0K%sPunTZHgK!2(6`$e>9{3JdbbK!1qISVA&!1+s9-U3f_D$naMAbTEK+yz>eJ zepI+qgZr_v7(b7a&ra_<_f65AqZi&E9DFyVHJkA1T!Cby{9+pSV5i~4cJpoo|8w7O z>3SUKjS_Qb9=tskb*Vr`U!C^mvV4wB4^uvKkL-Dic<;A(ym=hbJ%KlElg!QG$@9odFz*QjCLqy^e2Ssabd5wGO+94d}wV@&qM8K*Oum$EW}4vT7LBw zbBm@47Sxp#Xu`SyzrCwP* zg{iXm5%WkiLNI4e-|F%|W99&xz4tgoJ=|JUlDFT<0K2&C0b#83jgO^ClZ;0cy!0+el!pT+E9K7B+Faj=;G0m5 z{=yg<4mh=(MhN8Yl!Hy(`44bWGV9$g8kh5=Z-PuP;%&r_HHHX|q<`0H-o^ApaOfpz zO!3Ue>?B%@iW^#Tu%T12?)p^7l8}Js6pI45xBR7FElmB5?=Cm#`JT8?P&|#gUyq2l zgC~Yih8N}FdBrl*hxUed=i3z+L=B=YhT z%E=gh)lMOjmVAuX(!9UZ72YDVx_LZ!FGhWkuv{e>D2}DEFsHh*xgh)S6WLg^==`Mc zs8w}P$HoQ&9*PAYCLXc|-#J61K4~NstPm5mSB;prchoN4Ykq5=HTsBqR75HeFnx?# z%>zoQZJ>r zhUW00eh`nn!SSrR+31!f5#-xKq#U`=a7+M>d z7hQC5&qz_O^6s|1#&kykFuQo~&#;=aMiD=5(DdFrpU$Wqll)ckdo;``K~&IIadAw% zf*ph;8OYKKcj0Q{8qmGUVDsZK^b!^7{bTH07d+l^CaIv#|Mn>;uA}hePk0 zcM}_8AH!9HGdzHVB)9#{^Flbqh%>~}iX5n)Bj}r}i&3Ecib7^hJ z_?JiI%1N^kxrqq|FgC5ds7)#L2QKyP+luAd9f@=SW{_iY+ufnqJ@WVy= zy>{9GqwOZEe=fYZfKjb2hY?VB5Psj^;_XnSY)L2JuU+QhWQ|?4wPwzjp~;VUhMIQO z#nl*j<0&M6Oq4&FUwU^QGKEq#3+=C1!#cIT7pLwP{YoxCy)nyQkJ%jiaSTM^6gx<3 zQN+NEhVOg#41nDisIH``u8#Uaf{3dFgRk)z*ZJeuEIL1wLH{fX1||uD-Z}48idj}? z)?BDuB9gt6uY;x~9^Sn5WdA#GaPu9=FAYj?m$y@H%>$Sj9?Ns5c!H{GnO9tE**11O z_KDWz%~h9tVc6p8ld5j4yLFb`Yby^&AJIbmZ0r%^r0JwM1#Ldef`~yTgwCZJXH!(Y zL((>1j4R^;$t+-OtIXTdp|_oP^iiPY8(jE+q<`O3pzix=6QT)DK2vjyI$_74a^9x^ zc!#m!3lK{m8#?W&(0N(1Z;-N46EyqvrQ#5Q^W<6-RVnR0&NDRM&&oy`)qQ9cFSc${ zgD3P?0_T9(JM-!81*qnw4Alb>hHDNB=70Z=#9Rl6OS;|7N6GV>8OZw%oufCV0trd4 zGK%-xroX3esZqvo=G2|FAndKk;d>@;9Z0xQW zsW4FjHJBFkAZ~z<_|I%X8`IID&WWc1(xpA2)05ABp{u5gw4_cD5Q<;E{nj-lE)GqnUb*siy(w$HRlPFeRQadNsvW^G+ShRk~- zJI*qrvDa!G);q#U)b?~5GJkLA_O*RU(r#}OXAmbTOEBihStDpchWOc~otl&`2gmSA znj3pGd72*bA%0>D1+{&9rq%oTL6!z5GY;;>O1kST9elV$@)(FOO>?JoMM&Hmj?8{! zx+@t-hNp0ho*kY(Aj_#VbT3`xO|kd6fSrVK367db>=baevt!(ack@i+6KoPVn(&Ne1MFj90;?DtA=pwisM zaZaIxA9=>oPfrF}?cF~cGS6!%Cg$7IHe-EOm~of>`Ps-{y|pZtR)`)$B=m1~w2PG` zLL`!t>XV%?Wf31IN0S=|U{YiOF7b-KvG=r_0=CPe~m!f|K?r$O{*JOTQP8*Gmu*navF^qqFc~E0aO3f!8*c4v1y1-Kx>d z`IC*W_WAFL$cz%dP)=Uz5otLk8CYY(c;fr#!0jia53;j$!rk{HW4129G}K8CfS@3pEg?MyLU`XR@*X`;5} zlURvA+3lxT{eDS-hyb1F^9W3GXp#Gh%^0Gs#K(Q@jmreNse>8G!HU!H#37J(1onoo z@mdE_p5=gz+PwNHUB>aO1^=fxH-vRQhiTMo_x?S>1MQt$Y%f-brCa5-GF~S05$I%W z^=CxaqQW#OJb(QGD71U0w2yOXsa0~aJSv_j>{ zl0&U$?&=@vpKyRU(d3b>Y}pia00GLr;ukAk&pwLNM^q8_Dg@YBvC^>}-i)PJx6hj$ z1(hBJyNdbO85gAj{a+GhxnWsX$LSImHi9i9b1{1LsCQ6=hyr)8H%u!@;tW#5gzdw=syx zR7)>CA-sekPR4BwvzCyT{*sWm3aQSGw@f{p}|`jNM|12 zi9Q}WHZ=p7UlyFKN}Ly{d5XU{MhX1H8?t5Of_8Uz-!{uNbC;FKt!!eae+^b-;5Noj_fhTzbeVEf=Ku9aVb`gGKYKGhJ*l%!EAcJSX+Id-pqFw7 zHA4C5(A&C=A0%PGNszJ8hygAKhAP7ZVqrF;aVd^=S4?ojsKUg+a_Ll=F#-Aa9af*|MjRjM0%fRkG-I;PAyd zJ$-LYDJQ4p4cl+)fUGFoo2snY58eI4yUoqSU7Z-WFA3>Vo3qZ;cI4y_zH~KmJ{xxG zcvmfcQl`7kU%$mbJrn9mdkwK_z4JRfle|Y*(-5pB2l_Rr^XI*gXG*gt-<*?0O7O>&QBipbe6lwH)TQEssv7ehm zixV^(59IE<(j?IET}((|Tx+`QV8SC}}{J z&IOnXkv0vR6hPWT+zEe5?6KYN?UGvjHp`m^Q>0d+sDy}8xG%yg67OAC7!%hY5In%W zz2@>yrXjhzur(pglbxC_FPXLWwFhTE!Vb5m!%H0G3sW&BV#3bAR=xQkRrz!)d*0l~ zt~2^VooQf>AlMlqKV4{LXR<9l(cfj+yTK)}IqEV$KabsVZS2L%#NE{*IDOjJ zbC{dnX}LYFMs_z8FH{Nmyxtje3mGSx;qk|;ajg%(!uzXAYHDhT&X|_LTCCd8hmnBL zIAc&OC!uvj$-u;A3GcIRz{6RGvv^=0yX};7NbQG>%Qh~K8COh7X!AW>8bg&rS~ZLd z4UVQB`aKZ3C78fiJx#Ca{j02g%ht>ucEKD8j?n)X`TIf{EwB*Go`fWY(l>Sg}uFw={A5o{tkfWn36or!2jl1CKOu zSf1?A;~VJj>YAdLuWMBh7YtvZ8vmsvKA&jVy%q#Jk%d0L zSzexle7TU6p1qPd@iz4CZ~B;&;&4D9W3X&w(6EX>`0}3jJ~g{(>hg5-4bd1Aidwvw za{?uKxAeiC_q2G)1+%`O1R#VUiUy2Vs^udKp*41>ni;!NWk%kCZ-8jflLlZyu%eD6 zr?B$PM4v=ApR-oatJkm8n|1huwseNgKL*M>S+=J##v4C?QC3FF@LL3^!4musxR40v z&i%(?+5-_`=DFfno({R`;}LL*G|FRX)q+A9L9$ zcJ{Y_Mt=d_UD@}T<<%o~CBPwF&wy2cEC-d6dG!Q{RQ974XceA^B9cXE!GSubU_gpgGWY-mAy&9_^bAx8 zJhp#KofYCtfc>CVGy_UFvQbpqPvc9ofr_#0Szc>fylK8cyvIRu$ZIihFJKcsG2-cv zVGh)Z9CBiGjo!f+g=w{|x2DugCloC5ojK}gOZ{$ zpdmtQx)oPCbNzxDqqKm6Qef@dt`Tw4rNQ@lSdnp*HU#$*N0kx}DSBoec`B4_mIilB zLBX6Z&?sUdS@hX!o;Mm?hhJ-%V3D+ zX^I=Y$%I{(%c*s}IKD9UyRS!;W3I}MTC9EJg{_&m-`!4(76g3RnzFq0r)+nQtyQQi z!iWP2QLX1X+KekZM_x^NipHJ=2W}h$+Zo)HZ$lf^ic}p4wJ&tnW3COJS-a`*6hv^k|G7hZc29=-!{GUY<& zHo`fsh$c9Ya@gil6b*=;K?w{155zJ`%}rHXEZh_Ei4r`E_;uQYP`5f%Hq@Qk%5200O_>>q*MDO` zR%2@b|I)y%I;pxhTA2=1{Z^7Ahge9<#R+T?zbbyhzal1Q5it?%X472Fp=W^F zyF`yWiZ5>{X0{sQ)?=s?c4wj`l}O!G^fvQ2#2Llt_Urj%z%<}}KddJ>yxVSAj@0$9 z?2lB)G=t4zH0UFy?W#}$2?G9L%&*(wY#=jD5g|+g;vl zRbtlag2LwGgZc2DbjRz@2sxmXNF?y@}hk}2yk3kc6!p2_D#psQXAkCfl3qm8-;K#_xDP8 z*QG-EJS}TXZVp)_Z7-FtxMyNc?V3$#o3K>u$e%wbD9+%GRZ?vGF}<2cLs04?8f2+- ziebeHSt+|DSjb7G7UcT78UpFv*zNnRPKn1O`!0Vd6N~Fn5w$jmv>;yA*xi3ydT|m9 z^3S+%EL>=^=6Go#;)CR$AO89s9Qw@wQ4t7@%jd%ftjbNkYj4d%0XC12^( zfap=3yHR?TJDvtFE83>AM@gLCHG!IUk% z{+EQX0QeUn)>~`_Nes#oMy@~OVR-y)8o`aUjDBSS%=lL^gsCbCaaX}=j6$S9R|ZxUAK5Tj(_}c*Lte39bI~K)TWPW~2iLDn73xxfR)IJyTkf_NM1iy7WKazu9 z{~a+#Ozkmav1t&_31t;){m`b?Lplf+V;uZP(QZM3K_q)gI zO#rX6TL8xN*1t`$ky5t9(Re*xQH+UzJTzJ;<9<%1)C&vJlbF?)&dtraP_o4?@=C@&Y2p1SkGMO<9Uq5(rs*QDI#4>FJ zP0XtA_FVIq{407LjD8jY;G$1D)K|s@7x=4g(^p=f*5{W!VBKCAg!4hDx8qN5eA{l* zN}p}7>ghRk6cQ>w>=>%Us~FX@OeB?16HD-F#D8E&OAQgAyUnE6Ef{=fzaVK?SjJkt z%{nbRZJ6RheR{I1XxAl4js+d10Q7Z`eOYtlB*|oWM`{jJcBUFDR?RP(9^n$9X%_?^ zj5rR1t%#m^=S9DGdLH`SK$gJb|z}D6Pd-ymL8e`Y2;()>O zSXmlS-#WO_5rwb@v3$Y^r`pF**s~T&|9xwZ%)!P^cqJ^EjdYM~9I4CE3tT}f$r)OZ zR%3Ov4RB+{z`+=fZ_<;O8@Z^te}gS88?!q&qSquhoe}qlt~aleKgWY=GYClP=aEZc zn9NHEW507srrjQ>5+S9Gdz)qn?&*-t9mv}+LYvYOGbi$Yr$!lk2i{clHH6wNSu`)J zX~6r1?aUUN{gL*@zz=LPta~b7m(3J4J>2Z<6(BX-Q#rax4PsNv|HMSUi_(8VPoH6% ztzB?97Nz@!;%)tc*P>gl+PkK^IUqe>I~*XtXK$7eVFJ^dF(mQa#6dZ~ayir25hw%3Oc8BXcZ}<6<0UV4=^5gdAsWnC`E#y^ z$j0H;uHm|-!MPgS;Z)d@%r)+X4DAz?64e|mR^Zz3-NGWElOveOue_Ey53PS;a#EA6 zOTwP6BGR z8*AJ~foo`gYi0h|o6(?vmmhd)$@R1l|XBRzmvRcMir{)9q_Lv!|9)0AS`6ocNH!BRc^6$-|WKmGgj&BNjvuk~&&p zyFGFikJ#xo?N8{j1p2qQU6Z6-JcjGG*FjsoU+s6Hm!{y~?0#}(~4y=?gvc|RZJxc?mhFE-RYJ5Xdr0;~`rgAd#5&7!=CHl{| zWoXUiH4bI(hf2gjFNYOQLf-_g->j?G&OFT(NZa9i+rm$VRA2PvQvK*)cz1&$hIi(@ zb_Q+YoO|j&rp%hQ+i)*Vq9~}+gAx$VR=8fe`G_!%sntXQspI~JcXWnHjW+y@3Mx71 z@0%_?pC9g_bgtZRbCX|9=UjQPbx zdAj%DKKI2;JQ9H0*R}K?A;tErFy#cWx!`=M=()(zI~3-7BjLioAS?yINdln9^hMR2 zrZ4{WKINjG;(8&ESLq?Fx^8(GQhH@wZuGQA`S`5!l#9sE0CVDFJ@4Y!k4U|5#V#jy zJK>GFV|T5#Fl0D$vs|d1*rZI0JEth0`cs(e7Lb$2*Zni50y4oe4mCSZI{cNz=T(`hdxwtvl*ERN^{f@se4gpiluJ9BR`p zcA}S2b=p5|`^DS<+)u$&9w8xSVmL=98Us27Wj#y0OCDRXAE=ZTBI;@DLX~*dhmI9x z)Xz4DEh>yw9tDL}fEPbIyBcRfvZE*<*EGQ3eW&h<#qFB=h6{y2;llvhx{3;;kTA7< z#hyYk3Cb*C595%^AAxy506%)i`L{3xq;&hdpIO4cLz~eFP>R`KNyze5BAX)jm2>wu z9gEF@%KnLt5+OD7z2-|6{q{-6QRGt_tlf%CBrQR7)vYa}F@l-Q7h9qxdGy}M zqzE!XlZeyzxpv_;edjgZ;QE}QR0TtBCLAgS`oK}Nj3)+FJn)htA9>!fw6Sch+Rm8v z$HL6&UF+t}Fa^hshECXan%}~_XZnB$x99pBg6q>kCX(~=unLG4a;m!Qvr(^IN82YsJgI>j!o|ZOG)xaaTK2S626WzkqH(XJwjE8(VY^n)3ILwpQQA z+bgGS!zQJ}qnvWA8Ex~_sAyqLS<>3|1&-(V*?UO9uyZ{`ALvR_iLzjQ zM&9sd|HOc@a7a9x?Ppra3fZQ^m!)vj)A_qUh&7C}A_2fK{T zWdeIEG#}0_57k?XiLMA(W_#Bd9o5a4pvJ^(no-NTG-!X&?c5Q@TyQLS1PdyL;q^N% zTUPY}yr=sS|4M=xHl#4}l--h5bBBjDd z!v(Y8Sr}`4Li3gBD1!(1na?eEEb7%&9j8>*qJni~nHhNJ{k!HwJ4$~NYS?;WMi=tT z53w9+{vP8WJb;5v=J{XX%t_F*QzB`Dhhxrk%chj7P)$& z1IO!qhz}?Q!B?XTcJRMTWl%!Mpp~uqnW-^FhD*9Nscd_fek#bHrOiF_ElnLdguQ=k zNiPdKjOizj3v^84iA%Nk``i2bH@dvk#P5=U%zjWto{5xe! zz1WtKTeXl(Z{m5ily0nYrHpg2T6RYUp3^^J4{sw*(5dI9*M&jQB`N~YXd@x+ZMJZKz)Gj-bpp#HLU}EgE{u^_ zzl4tEv#~p-9A2V;D^R-lUo-L@2GG5f8O#B#OGfSZzav}YHO4IZvz4Y3RW!J8Ahxc_O(N8ppe{Ea2UaECV-KYmH_rG{!nvw^kzRP^j@D-Ca{QS>lD`@NPI zncLtn&@^ywiz@W<@lvhlCet$Tmtif(B^V1=@1}X#?Y(VBF*(2j?-E5EE^8%8CiF;m zD8An(?4^5mhv$3X#P~4@0M*jcfp5IC>pdhXnoUUirAYl)f*OTg0$bu%{LhTy4U&?b z<#vmrJ#(bydk42`Up`k(onx?Pn-XNjv5BHzL>G&B3%Rfv3*bgw1 z(cNqBwJ*&a=RB2*OFKWDnn&(DeHwdgnvl}6%c!LLO=JC|cku_6(6*MY4mDkrUcTR| z7Kqhx&7|^>Q2th{Sjmn5I0BLjpT9psJ}I*UMWj7{{6`BW3kMFo zu^!c3H@q|kD<%|`R#s}8JADQNbvY_ZHAwLJ_LTAcejZqCy=`nsNcX0!%=E0SywyyV zQcQY8N-!h5%*;5<-in#Y(V<_hrlD~kff`dx7F?~5>I`X`UC`o98WUW{Pg15XoFwwj zkYPyWc@{3(qcafr{{gH(Q@Lc<+}z4dIv_V!=TU&*nqH6Z5Gt*zair#oAl1WY^#tgI+w8zZX80F|35f5-;2 zCn+$;$`3km1k3wZ4-T$(nuz%J$P4+*ADG1Kr!h%e^1VB3(_@B9%nV4(mMqW=52;H5 zc^MY;O2!vL*Gn~ZT8yy)MiUqKv`Qtj zc~&a#7fN+BV+VCf;q5Zq*^*S3!%4&3Aqa}M!(=jy^aL9)<9+!%E7cLI+fNFJn$nBWTW8#K3NAp4ZtfICG#iC4utT^nm z&Wb(VkX*KQcwk^YsQe6w*@u`Nae1AG=Q9kStgXExGKUV5$wXxzcX;~&n4~NHmb~QS z)_%p0<%V%mmyDl_i#LFo3k%8nvqQMm;%di3?gA!zZvR;_)3$4@GseTPX=}Z#cEo74 zH>YVIn^4`-iJme!>P!5BnLf4h4C}+p@CrG*f)5!cF{4t5YfJ!U z=Hc64kDtA*YV&Lo9_EjiSQjnaf6_aRR5o{!|;P z@zDHauhgs`eJGrMc-SgRf@m$+2xDL8Z@JWuS;Nu5--{Gj~fkzmEfIfb#``a zVuG#rAtzxDPMgjo6C-@Znz^hoyDHOSCBr5~gm7FCd?8=x-SHovK7Beqek*vz zU~*y7>B5NI<8ituFn2oLm^<)zG?4;x!7JE2+7+CG!3LukM64LXf;3Y!(=2>wLr7lV zZSS2mS;kRm!NpXRh-cZ1i6C^Dn^83S9oY#-(`dfJaM?I{|N6y?*Y81RTG&Kn){ffk zqoWV)7tj7uFyTMX43NEEL4Wt|JRIG=`{aK#9f_827@@BsG3VwC14L$yl9{YtE~CID zWU4@K*Kk}C3}L$hce^}OIE%>&Kt+K6M43`3n z$p^$75VLPoUS&FShN&SuGrVe!@h_69dbM^xnMeeeV&jV%FOS2uMoJR*b!%)RE#@&b zZn}%LA6!%;im`>Iug2!tejpmRgRvZ z`SeisisRap2UprU@yhizPQm~)-EOxw>5hz}^$~6O!KyhOQv~?QD?m&fUMwh%db z9Gi`kc75;7@$s9VDyG+GGl@4UM(B2U_3n?qbA?Hc!a&RvNK6E#k<-!XjR)pgm21qn zQ+46OO3AyuZRpn~?(Qm%NO8H?Ko-PLE3=KJJ-~nLjz)pA;p~MUX0$w4LUJ0-0DSb{)$r4kTATcBG zz$h^>nXFCI_hpZ5_aVmH6BD+VCHho9V$#}iL;vs2@PUP9`(MpsZ`PG- z4b+#fU%c9Z_!^M;9#0pUho=vpz4`UDV*0GDmXuh)eo^`HySxAVg@%1_Z^bVpF;lB^ z1|V||G^UZW+Sws2SJ0m`J_2$1$gtC+M#D(V=xue_qXF;PIt;c@V`F(k+i*I_LTM}Y zx0T0c^XH%c&))gHw2`l2yp={UV%yW!Z326Qp+h1%*5HW;wQIXMSmji=kuAbPFAD8N_n2G=d+FU?_xjwP_j$iF$wbrh z7i6q9D8ZTNe0<*bd4IgSD+8Dkr!gg=F~y={sj|Pgp)ni8tm~C$&G|)UCA-sdQL;jY zF`shl%Fyue_0U&Z=T~2~x3Uf1$>DcJW43cSCKHt2{H-38Vk+s_(+5v){dlU`0b)8U zO_DCVXf(?qZFQPLZXPEl56AorFP*g}d5u9{1Ic5ZKW_J6RbvjiT)FMl7>Ud%-!@Sq zB?KlukjBJK)Wyx&k{XFffqwn=bdNC-6&e$Bg)eKkZqM-3uN~M~!IiG&X0#5sZL6fa zwpE}`9uv{GF?Zu|^YFvQhg~CDiPKdM%KQ2#8Ygngh{%bRe3Z(D6bzonSPFWFx@-~$ z{Xc-uT{w5P{hH#JWK9@98n=~u*atyzIhq!EK_n&?<@3dvfA1}b@qL>3 zsri%L7_qdqw*{R!(Kg}NF5`lVM$fbR_xpQ`SYIxSMkl|iEQLvqRqnjrT@WZUuwZLrSQ<^%{(Dg7r>WG+ zx6P+8O3meR9#f2!X`EGcX$#NmET42b9RI(~yw(&qy&v0GvbE)!wxt%VF$tMSV@Bzo z@#5Ygl?U@jCy7ZK6MaAEpP|oFu-+dI6EVZ#NR+~Ia{zm=UGsQIR^Mr1O=+!?bRO%a zs!sAW3!P5({17np?{A1``_m4Q5)v~M6}}GvlVb9&F%Wa^$tH=+5{UWf;WxUkn^<`c z0<)>D4NG=Rhvdu|3eJ((Y!8L}81V+1Tgg`-9r`FaO$tQ~w&}>J~89_Awinkh#2!lqNCLMPFV` zd%ekCuh-h24vCqhH(ABtO<9iN6&i8pU%i#UBGWqGQbWX0qB zLl;qcLt{3GS$B3(N5%88eK7BF5dvqOt{g8v^M_i6#B7C(6e&!hEjb?V?6mZyDKM3E zCXHY2Tdq^hmo7PMY0K(J^pkUvn3Q6>#Yv6nbl92cb{-!W=eC*THCxflqN0i?)!8NIejiep zW)c(00okkWezP-~`2DC{of2?7I`aT8buZWi~7@6DKj7-JrtzC8L6;=2?a5@^Frz5@=r+If1D}1Tnw7*wQ?B>GWw!KAzl={<-C7_41e%WV(-S`Ki<& zW!-N3mb8k7&Q=@D?OpVhRhNTr2TtZ`7(Rj7o-dO>rbshDh^4%w!M9a54&AfDUFbdyiOUXnP zHJXX~2Dx71fZM<^ac)&4ki-`!{|HH*4A|H0_QpDG?7mr-h@@JkC-{ocIE8Z zD`$gPV>70XK~Igr70)9e^>EUU{uQ~Z92#@>-J6A#kZjo@!X}|tMKvX+TI5YR9?#Gh zr*c=kor&d%bh<+qzk*J_cf{LR?DXT2*uFa)X!+VxH(LH_dXluzM;i>O*lAMyGa-4kX47u>SQ6T_$KAFrbo=mO2VE@^na>v$nB>uDCKA<=5G)1zU1oF%4MVSz=G!^2ZKXUD~knoOnJw%yoa?U>}VaMW>r z9v*&TbqVEI`nWH=az)=#qp9VEm^N_J*W1@i6i*N~Wp5ymIM_Hy-0StKz9tID&j*R5 zB#%c&|BizqTiu{go&wk_f4Oq5?Oag!!W?(Xl0c>>B&xl!WP*2>C5w0W-u?XZtY1>A z2R-ICO`*itct>ph;ENVqfC zWHe4@;`4uEKN|YV4LCkw;w1qy3|q@|ez< z7k%b^<~ns=X@39SvzI^o=W#s4mFjeGe>_(5f{#xCt@sq81T3LJILG;sCd4{3@$DsRS?)iF(fH^)MO~+Gly4fGiliFds zoNi#%7y~bjaXlP>fE$?}HFQ-D2G3s(iY1f8kD=kL%lxh#_6$e02h_6ZouatOae|m{ z-vX6KOw8X8=5$exNk^>k{Q35ZLJ_I4d*k88#(M+mEZ{YE4ibTYEWs$hpn2$2_0f^X ze1Ms{KCa4r1L!6s4ccur{7C)%#G7QEqU>l03m!?5aXzy+vz1KF?7e&QZYH0Hp-h)O zW?9PC2bk<2rsjEY=i4t%pKc*wE^$aczsAQt7WL2@7M&3?;hS%77apZ`!Ah@9W6J#! z&yy?OjpNA($vGA`65#|Bb377zM9*I;EUm8>rlty0g_OkN6uQtWE&j5(^S)eufAH(~ zhAaxF@^W!@VSRQ2l|=(LnV16@eT-3_FQ2`9`CEJ!BmBC|+$D$Pe$)05m)Ia?gP3)k zA5mXzHl|qctTsvttDH0@@0ST(yWYxWn4O&_C9Se`6QK-DjF3>GItAsK%v?N`dV0%s z`Vt|t#bx88yC%)(JUPqwn8vIJMtA)hBQ9mnT4UgxqAcHD55VvIN5Wnr(1G+P1?3q6 zV?>X{7hljp5_693UId2(P1R5(lpBpi0=-5U#;!g-F3DX%w3J^C`h%J$=()a<`_V?s zstZ-Ej}i&YihF*7m>-YIe%lZ>WUR~JB?=SvHj=#9fe5~7C>b#5$A z9DLZ=iN}@hvzO0y`=dJXa{BQ;Bpx#pj)_6Y1T)VyH3cQ4G5y4w{(iJM_V@JYnamc~ zuZtySc)az~PyhUBW~*ot($rrb)p|sS28UTvhrYX&ZE?8(%omHV7`uqy;ZB3+UTiCi+=TSJqQu#TZ*1T(>i7b80eR+Pmwc7q@x5_G(YqzNS2 z)H;lY+KyHRLGVFn9~`0&1t0si&wZZPJ?H%H&5hcYo%h7rrYS9z(oeqM^XGe}rjAr- zE?zQZ&aY(mf8F02_i_SdESYMsX)yurk(lAA>c}H*qNUF*2u8OQNMy@ppyftpVY4yAB-$mZVrAT}niRq?Sa|_Ear(?J-3aA{QlIJicAMfn!Y|d>8@r8w; z$!;@~0ar|UP3MQJ6SGdt&v$+RmNMfd8?9ylWut=sBy6syftc7Beef*>xpga(n96o! zH;dt*?*W)P3f&1}8FMur9#5>T{m^pz_Fr!`E7yLsHuW`GX!x?!{My3LO?_!-x#_&w zxwI=9lYNl@)9Xvjb~9No%pW%r`v^xqd1X2{3+xDXQ4JLz`|pfde_ z9IrOs}E>eNKOe>%=^-T#QvprPmkl&Px?9rDU$! zf{E?ya;ks@!S%wM_vG7wn1aP=T76T(ou8FtXD1L-Gq+$o(%VEh4vrkGxG}<^yflJz7P#;h$kR+_ zc{i^hlT+e$sPPj?$nN9%7&XUL^rCEe~y>haFG%;ELR zVcr`?zwtJ+ZRjo|rdl#>s(Nt)L9lsuUf`?@FthaO{9SO@woCUa^4r^m zWO!>ED}t{pA888nk>E52y(Jn#nqOIM@BBxbXyLs6$*CgiRv%S}nK+muS%8=x_;B1C zF^-r7@6^-d6Y`+%zu##86`H9z1`0I>RZYK}}rNOyFQ?*H%bpWVxJ~ z*xJGyx^!wBH^eWCz)RfWIVw^ua~@mf+hL>SO4kSPXJD^9W(8jF?t(zZ(xu?$2$zE3 zv~Y=S;AJ|KL0ta*@!A7>hob{;28&E%QW}ZL$in88cLQqva5>JEbak{lKwDx$JeD)* zusV2iY?a4cKmIsqwwvu7{XtyKI3`!um~~=)u8yhduO3ZhR+*`4&9r`faL8pI)ahLJ zQDE{^$;8$;uS!hCocHmxK8MK_;L2@*BmnbSBJrDi>o%UYt@YQ2>XC_RlzNL&1h{f< zsO8T9vwAo9uDBFM@ET>P1$4uJb4y8`4j;Y6{h6l zxDlhzNDXK~@CFo&j^HBDHytc3lXtm>*078z*Lv|PFtrKLnsIh~tGZ6ib6{rW^*lK5 zkEAhiKhhR1WS?=XNX+e@OO+F>a$bzZ$KK38VH51{qg+F8TL;gD{FBAv($d)__Ffhj zCl~!pW%>p@jg5_-Mvq9ue01~_{prr}=3FQoHe=fd#};EV3%LEWaC!(5^Yrx9uDVYN z{86FFLeZ2R8DRNF=EaNj#`0E*u8tRr|Ao&#ibP9#_@~g9r>JATY;zh}NS%sI%BvG& z4jeO&Sq>ylK;=llbXuHFDUm6c(*Vp&TF(41sB3G$GeZrn9gLU+J5!Fa6Nf8tE}k)y z;||Pbu^nCc+}s=+8csx?rSkbq^KC$; z{(7FelJ`+P)nm{OM;g?~^p?++I=`+l8ISPKW8EE@rGOIs;gz(qKc6NY4Zu{sUidxw zaw3T{H3=|hz;{bPlFZN-@gM7JTQJXHaBZBijLNY z1kBvR+k@C>ZszdS^H--Yf16Rfx1zz+nx;|hwvWKS^I$<IJ6b&tJs5gCXB{544l{Vp4G(*A)47~W9L?<> zh_*28pH|aJ>Ke07%&#PI^z_hF5%Z~xl=w}Hkx1+HLxV2ceciV%zOPS7S*IXOkcomCcpzo=YP)G;JL=E;iJ)7#rAasja90L;s!C54zr>?osG z@#T#k>xj<{cKrBXV{=mJm+vC8v)FgR$wl(x2X-Ll^TSt%r*BVBcbR(2vGGN2{Hk@iL-<9b7;DL-9D0nTvHi1h#qx zz?2Vo99E_=XNNtW*3p9lUW|!3_<={EY&NQI4Xo}^uVdDU`Pm6}nI#I#%$v(-T9w#F z`1}dK1D9z7jp@R8U>z~DkJ2%_Rh6mqeK7Rx9@Cfr%n&ZAtR}}3sZ=UIvEHm_;ci#* zfmK+_hN_@s&#Jc}Rj9ToFMpkx?9OC$Am6|m|4KNMtRCm+(s)T9RLDf2HfItc0Hn|B zjW2;K-(zEeh)ho6d`L|AMv^n&2V}b4oEy`^M6tVr5fi5|^LoAVR?x&IifY#})w<;s zT;3$+lPB?r^aO|r((yH&-(3xI+>@1+U%nrsiNc++O)%7(n~~qy%qAl9WM1;Lw{`Lo zD7;G`=9vb}Nmj`8hBy?N4IZm|=lFQ%`0T^$i_(V=C27E8Pr5Mh#}T8|ANZI)JUm@K zT{vBc#aLgYTFSH=bBYdAY^)tzc{#JOyk>M*oyNeVKOk8KaJ1Y>)Bc>%IqVtW0#oC_ z%)OUe3C^i?$t~>e2J(}Su?H?ynM_|NnY8+jT& zr4bs@1WVfpZD8myQ-qm;j0`E6CTvnB)EaU@*y-R%1~Yh+>q({5W_v*pNfFbIij81S zZoL-ka_-)y^biXB;b6a1tREKkQQYpUK3HT*WRf0?({?^*B!_J}zYP3K}QDw^tD{c~&a`hqug)pV!vbfM5olYyG$@ ze`A1nGU2<>ASuh~#t=S{G=tOfw5w`Y>cqVcW6R5`9LfwJWMWYcfZ2sH7{IE+ zux&4l@rGMn#OxyGX~e|CxwJ|qA-?#=^Ve|;$Ef2t>%cMlkz;bmF^Q8f8C70Jhh2B> zLwqO*U>cNTavZ8q05r`dQ@kG`K5v26>^OKzPdVHvNpD~J9&QV(aK6;O^aIO1Sy8n0 zdT7(oo3pqiGwK!Oh@HV`FcP``QZEz=rfsCNx#j)&2NW<3jf&TiV=^q!uWJSroQZUn ztW%PrJd7-pOQmXm{k|`F$+JrK=Hjhq(MB%a(27eh&T>p$PjRs_kIT|WmX3}lBL6Ec z0-gM}Fz4nrQp7AB9vvSa{&Di@_|q?omOM110%9Hj#{|6GcM)@8;oyP2Kp<0-J2@`3 zH2r$H%1Cg7tQu*C6$HI1#d+d*3_m9(353%q)J&k6#hNQ%=iA?~mDy|(bIvj)pAyvd zR$=Go*K0WPi8z}IO+G`@bqbvWp&?bFZA`{*WKxnDLw}GIhX!_7>epl8-V5;c7A*>PmIN_dRITkAhtE5I9{&b2(ST)Q zG)4c-s|p!(5JPgeHm888#1JqIk=m8Tlo)zPh%d(;Pxtf$uq6W*5#|D6|p)T^%*8SMEU|3uT&Xb)T>p%Lv0>~MY$X)%JGzN z@Iz`pGB;1~X|gs6pJi1+4Xa@VYVueL=ROXS%e#S=0P}MPTt*2)q;+q0-i0{22yZQ&t|hg$E|cc&WNbbEf!e5^W=FI0hFN> zv)*LbZ&@M9>IJ!AdqDy7&`h%Q+`P;?05$5)O%%; zwV{@Wu$m9aq27!PkH|4YVO7N_VKixShg;wJ-t_d9b`i6Sm}fX9Ucio*Uf~+t27@hf zhs7`;=B9W{yp1vCB*f?PO+^_|1sbdfMJcJxRnpIlrSy~`B5HCHoIp85U3|H8-|2}& z?)II=$#X|J=K$*PSN{NY^lX#CcW#lN0#3(oLpbhs6FdOP3`G9qY;t^3U<|mzwMEpf z=kf)LGe|MLpy_I{FW|@6rk?IcQ? zlo93r$pD-WfnAO<9)700u?u zP@BU|k#AM3v}@U-VljV7hSRIdOBP>PGBNL?pfQ$hYw4`6S50&X&fC(qilQ!8hRL!CT9|v% zBCEEvY%&1Abs(0Q-+@7sp-kpR1`2dI2H~|)#GL)^p};epN0(!E5%XNcbPL32wAHwz zs`b{2xdV_HSxsSNMGA9wNm8Vu2nFU6A+abU<4F=c@_iXZ>LLHkicZq7bzD&@%#*^0>^ zQXnl~9iS3x(M^fr%;)n3!`xXmfkm2HTC?qZHe0AwFn(Mc%{3m_8X$Hl zS2T6EZ^^r*IkkHOU`Cj3?$w*{ZTL)v=cVe>7OBa9h)mxic&|En)kU_w_txSf8CKce)@BWSdsw~4W z9d`$|+sEu8=Gi``*Udww**5L*5@@g^=r}=?u%{I2Ty*p1ZQzv*a0viJju{yN!ifbr z5wJ_dC>-0E7Kep7mg%~rkp^mdyY zfBBd7U^H^Qt*zlTPW@;!?|5G+0=GvYG!8U*)u@TIQqXC27{v?4U+J*F}jbhAn zR?7j^)kgC~%brgsM(u_zd5FpLeiu`Pho;`FH)r4NHFfx&az=pvV&>*F(XC$@A8=hn z2I!zXf}(Umw2{kfT-Rn_xHn1 zK_)kb+3%_~$)CEnL1q^*yNG#?9+R4JP-RiH12SEWCO~G-{V{@=aD0nAQ{}6sW*C^| z0GJt5Mra(GGJ*+9d|h>}sjv2Fz@CIIFX+4!L(({&A}hHuC*vxuV# z-Zhl|)Hmasf4f4SQ(G6H1(+>ClUtV4khF6-=I-vw{{E9EWJzW*Jx8hKhYxSv1}0)A zLCm?iL=u>xReV|#1SvitC$skN4cz!?d0 zh`OZrKVh)0%CK=#>+=GD5(#;jHFeyeX~n2Xopq~EJkig*wzV<8xLB+JBhMz%$SLjI zXaYCaad-W}!NNig=k<9z_kZ@zuQjc73**L^fRRK`OfnG~VUvv(K{r*g28|L8+tP`M zN9e9ZaZ|-X+6gJe)+0gi1c#ulrKA?7WO7={X`o3smVz+Qi$HFAuwE2$dA`D2&+S?3 zefKuoB%M#-w#{ELV&Io&z3W-eVrZ*emY1xb7Ufn>IA|YiYyhZP*5=ZL{f;%qv!6+) z{F$DXu- zWB+`8)!;X*V}x?MeIWFIwK6jClOZvO#JpnWM=@f3-b^;0GZT=xpl#lF1pUis$kfd) z>pXUDJ01aCCOIyN%d$N5Fw^GYBsZo=){CJ_#v%lT;Qi;S`El%qTv=70C!t?r;ly9E za$;Zw#<}NRuHhBd@qSOMNymVi%AW`VbAEnatAZEt#lL=yqkSlv!z5u|T3JERC~}N6 zlSf^d*@$Fqhd&LNgr~FUR+gU4tIYF8y7#Q#?U7BG(tLnf>X1t%`DJ3(>-B#ZKkpao z#rm7wH!-RwH_1fwrt(A50s>-skDrLHa6lA&2_Pm0Q({pJRdB?ehMK&b4*3EJ)RkV2 zBXJy>umR*KU+I4Rc7j$89uqW2(@FO5CH+n^LGGjYB`VT&`S>xkI5WJcC@P)A_TK&sTE!bed;CQualrT$K9>F-2teK?7vFOQZI$ zIhPe^DIqKo@Z#Lkv z8>XkGCc_M5))4{QA>NISY^*zQ)L`8n#_G%8vBL0{8AHC;s;N^ zH}mthzFRLAi{Fa%`kTY(9!7_gY)29&ks#VqLMDD8tnqD$F15xg<_e)iwgW|ZCGEp0 zYOgos&UY&LbO@(h?5;$@M?CDf8fVqz=W|^s%)cG^5>`Iq+anR>i9i5YS_0+u;PwVb z?T)f3oa^Iq@IVt~W+Rg+yndab%f@-PJ6hfgm18kLXQfb}L7mZju2Obmq9;|DHrZB$ zz_bG~1<~qqwKss6eo%sjWrG1kCf4MjFsJlnCnj3gao}fBe?xzRs7#N}X(QQjK9-zg za!y1hfcNF!w|ZOMUJr=*^)wKMKLl!xz4r3uPED;D9Yr5M<40nyV(2^>9+G&;dWITv zNX+kPOr!b2>7`L|@Me@$rg3aRtG(^8=z`a2uiS4Jwv+B8S{1pZ16Rdz#A)QmZG)&V z@t-IPwNxq&b$QSdd^V@jUMQZ=H;S8-k(?&QtWSR<(09%*NxTtx5c#Q_E+ryK-&Q`f zav$0R+5D>=A0L?+QO%6py7}T>riL?+xXu-c8JD6sh`s2JL`*K3lp?}WWCtxy+`jt* zdR3Q9TJspyu+2Uy&y~9@F%@T;<|{R3@mu{HygMvb-2g`VNHw#62J{>`RTCD+ZC=v*()uW@~#Yw$g$bCUiX4kv9 zS1!LNTf;~L9-(=FJIdRw7NCq}E??3)X6t-L#({ZBm{^42B_J$g?)UC;tN>r@R0>kG zalVjuQw)j7$Y9Wd$g~16 zlLAjI>;fMLV%F%0`O2%N3QA;YwA`fZaSxm_(l!mXIY|=pDq@ZeC{VaRg-_FLGAUPL zD2smO>RstB^0r?f<_KOhs#~{IFMh41wzhIxd{!g^GfIkbG#4=;Tv~w}YeSJF?@qdT z>eIA9U5I_#S={2TyQ{*jXtcvbU)tv;*@|FPh)v~@@gj#DeUXX4v<-ped9h~1zh)FdjFYpAIDfnhE>43WYT2rN!Oz5TyJH+Lbwh38h5}KHl z)wSs=V)l7yrZa~NB(!b9G%zzHOG??V*;K5sL|DS5rNA6f-MR@H6RL7TN|I3o5;NIX zn4Byz?})T+#|4uD=LBFPF|X^EmzI{-#wN_BCLPnqf6q-cG7m$OjLNa{{$SPRMB(Zo zC1(BeVYPm^hfzD&-@%<;A&_Xyod8I|5FZgUZ{Jo0yWQhbk3yjxDU{363U+;fnNeIs zl#b=jz92Diy;ul<$tSI3i#di<2#xM%^ki0WLS5=4$!DYOjRz(8fAHbtcsFfr~aTBv4n*e%cJD-}RyaOA71OF{ncD!FK+Lr0vojWA zJF?ls8nxaqyZr3uM|$cif_M7A0X4~OY7uBjWahMC7V2_bt4R+sSr&?Nd&yt&)KZzp z*egFh&7OYx@i-9>pX~owf4954W7UNDqh7DORhm>!xm@iJ4UYBPp~M7K(nzEsF^9zb zp2ob4%%_9IG|n4GCLRGWbsnc@7SA!6H~=Gp5okUkP7>EGnCnV3rjQiTVid=COeAJ9 z=s;xN9aD{+Z=$8i8gyE`KLRN)l%O6_nXvw$;10_R`?;Y9Au zR zb^)0G&A*oqNH?Vs%!HR|w^myZ+O6MCPEJl09pr(lrHto5Vj?p^OBSltOsatECqP;j zC@`@mkEY$xsHiZoE5?2#VtQ#R2g}%8z;!*8<&S37diCTY6`15bt;`p1=%;RAZ9YYj zi3L6}u(#M!$?jTpxh$#7me8`h7%`ANJ?6!&qy7Can4h#Ggu~e<-Ci%9ZMniOwsC6p z!cVlxE81jW4v9G=<`oWh%qKF+h2m4`W|rQP>z}v-=lP3!Kt(4IlZ=(K0$RHxpfasO z+#z6C7BK;HgqLswk@zkJwhBgKtf@b8(F4JQDZ)v>BQYh}$Ru)fv=Xy{ zo{=UPdVcI#eBdu?P#jSeRZ^l5?@pOaFu!*t_2D z^E~hSp2W7Ze?d-c)U=8T^yByYKF{-gQq!O@i~8&U2r)qB)aQM~Wc>sSANb>P3yrx?26$@xb>&1uw?Kuoxs%cpR)cpkejL1nJw zDLrW(U3oHRYs{!UTrMLqWkyWi+b$z99bGqWw&Nr@9$WUiSYr{3;h>XDxX`QPgsLoj zIjz3{V1nB&7Jib|L}Kf;63Yd{)()<8&lAPEj(9jU=-VOPK)8JR*<32eY1PjhC zfF>5oQIMfI*HQ(XGlmB#eP{HWo<1kvY;%E`rn4c8-Q=2)I-lwy&0AmCCTi>gy&gf# zyM#=S#Ks?`y2fl_yCTK0XbVf~v_&TIk^yt#(xuY#z12*94laZuJT-iFEOJ90yiNa= z#&a4)bm%BB5tp4*_(5WJ-RPcdb#5RiB_S_qW?!Jw6pwjD`U(89_=K2#WOVVcf=NF` z+y$|k(G?Pz_rrdyuMH`g^5H_|#rn?T;#6@7L?+HY<`I&oIP-HTVvFG8Qy^v%kjbK$ zjBwcSsScMC9Ez%mn6Xb2M2O26n^i|(M#3ugWa4tiZ(kL2_15l?@7L-;`oaPs5_Bz& z_r%BJacsl{$qC5(00NV0<pP?MNVVt(w(IVkH|lS@pYG?7$~L1eZ_eP+r0nMH3dKMU(^}!WIWT#>RTQlmL%j9DqPm>K!#^VC zhCOs7iI8B4XR8aR?qtI|Tw?x(Y&nS2kLbljm(su zW(qj#huzpz^3S2Z#4c{|ZSY1CllqQygpNYM>_&~*jWW|c=`=S?(_^wGCR5xRLpu2< z65py}eoRcvbMzG!c6Tca`smZ=CwqJ4@@^(ZIxvPv@eKtkmBqvL^@mgMj(%6Xb7yG` zkeSv1lV^y`M~8=OW5p=~a|JYJHakzm95#$(GAn~(RTJ(`MGb#X6PFLdZ0M2_ks{X7 z(La``js4)@-f^Z7F1GW5k@E)`sKpS#3Oy?ep+Cx^gn*^DFrz zmdY`4U2YipA!06pD<6+gGdp$)$qJsgx}&{r>cMo0ydqSdS6$+Av${DVlfDnwa`;le zuQ0U(6s#UqtH-{Cdn_^5m5?>ds-0W#EGh6Uoi>P=HLH4lUVHmi!!S>Fger>yP?MNV zVt(q%fA+92ou54o1J4pKp$pOJ{n9C!t);JTjRwG%x8LZEhJ0vl5GJV{_IC4SXtNr+ za_TI~p~x(zG}4#>J=8w=`^$ZjN8%hIZf>v~A!^7p`EVoEn7PIjDpP1o(~i!luOXK_ z7hE~B@gbR#1ZBr^ZDStu5;T%P2#Wcjj#gYbq1HAto$ zp6Y95y(=u@hdXN?GW zbtR0?Q1OGv?C%YAxm+C`fJz68EaJC*(P-ql>L#QrYyduvwj92bh1-ikK-OwUIUujQ zt3y>HhE=Uu?Ds-AolY3lDy>SwfBDN>Bh30lLvmANHi`LvDnIp>K;9$xjI<_c%xA)) zQ$M!p>@)v58WS826gPanHAAK*s(s#=6%4#4p2lINUQGkB_!hg(Tc{wq8$;j5MXviCP`3f6OB_Ce#0a(H{GXr-MBQwv} zfS7q;W=ez>QJCN0xk}dCMx9;vQu6wS7z%(U$3JLS_7%RmL zl}Lk!N9kw%n3%ypW$y%ZWw|_;Tg5?1L}qz2py1AJ%okD}?o8!y{bl*#Vbv-Y4_?2% zcTaUBf^*y1EEa#>zkBzdq~*%{cW(%p!*K&h22#_=7L0H#k?41KcDTDbmGAd~lJN%* zSjQ&{&J_yIL}Es9R`Q?aHH|Na@YQL_7VN++#MbKseR?J{ozH*BPcLi9)N*op852O3 zv4tJT{Q*U0GC4cW7C+Xf%K1A%Ox!G?1cMRKd)UN&vt30U=^%-T3bUVpiBaq>5zIm7 zn(oe!?aJXYj4IQQwW<$Cg}OSr)L0~IRTq=|OP^|jrd932`Mz}WR@D5!I5$|oYRK!Rs}IWpxz z9UE`-Krt22aZvKf-OEl+L9bndXZ2#cP|qM|vRaN9G<1HRlnq%IEIp0Y$~|@$J2&Kw zlayb!)QddKhvSPj!sO))yWoAPv|G@Sn7GL&h~ewEZzC|HQ3Phx@6$D{m`X8VQq70m z({-c!CjKBM70aDo0Zh~6F5EuaWL@=)n9ffUg;)1LV7@HB+%1Onxu+)t%(cx`9f64< z>Q}B@+1kXW&BI44+2GRB!M%emwX0vvC5`gE+0Nu{%**B=POjA75lnbn2e%A?J-qMGRXVlvv{ay$Bo zl>|u#Bc{v2qdDCCPeP6IK}m`uHmsd<49Bz1jw7^5t)sUSkeL9`Krg?nZs%F9XgVFP zlDf3en(q`rw)^b`m#mMyNz5iOKVK`ikxApu5*L{sVrB!)ByWqi^z|f$2GdNWk4`eF z^Cm|d7U2zZF+r3KpfW>^ISV2)5YTbg_C{Y{OM|TBTQV+e&8V*eM%5_UjX}=Op;A07 ziSBietuT45M{Fkp7v2IlY$C!_%gfi=E?)xkeHs2t2yscMoFS2UsZ`p_q%xVz6K#AJ zj}$N%6Jv{_(Wnwt{BlsI$M*&miP2*05O?d=DwW#INQ&LhYlD>+HlPJ zDKX{M6GSGM@?1I{*hOAe0GOMr0qpFC%i$Uu3v5;@3ya4EO;#~8{f@h%KM{-F9|mHs z;1MPObAK7`m*;UMxiKD(7)I7ePHRCm@jv#?=%uYZi{mz!EJk9o4Xp``f#4-6LT(^e zvX*;I!5h+Try=7Cn@S@IO>ps{r7{yI;|4(_B8f)D#-X;OW<(vTtsPc`c`Mk5mY4_M z{S#W)w|(7n&hK|`)VA{%PrV84%KHF@$~88aYJqB|_$tG)9hU zb-?fsxcFiB<&V{B$*VV9yEOsE)uwikDfplMnv6yNM?*!a%rrSUild9rkkb{7+@?Qu zQC)J4srZCcKABI<9=v}28pqrvy;A=|dq)Sn$ZqN+S2ascBC|1wou6p>FbvN$rdLSU zPmq`+BelE_CvT|XtG1la3;$YW76up^lGmx{e5C&2!`Z$ZVsqgI#jCF%Y7(KIB%@nD$20h`?z!+S|$xMa6R~fHSrP{~+riOvNK9f7;I~Br*cegr}N{wXJj;O_EDP9~F&`@&(XHLO~~mR74qOze$>2hf-!HQG-3b0rtV89=*lD!UQCPD$!|5_FXr zWVSruho{2C4B#?D>*S(i8C06lv9;(V6@7r7qd-iW>~X=Dv!)+#I z-==)GR_}G%8gthzBsAGfVScxqyO_wN%8wwYaNkGm1Vm;;YQ=rMk`>EbURlOB3-2^E z)R(WP>#x#jP@3y?nQgZ8>N+z>GP6m{CNVD&^P^oGzhP(l$>({|d|p?ZtgR0y<}|wO zX49%i6ay$S-SNS}Jnmw_krfz*p`Bsu7jZD{qZ0>p*`m=BA-TM~v8-Ji zK+Jq8B7$BfyU7XxGzC);6wsKZQXGIuo}5WcE;dnO-t1q%ZcUAujk-@+Bc=vSMt|cS zxLqdZ(@m0D+beUW(ge8kVln$FRZ7lnt~{H$6;!O&j;moqt3`6hONkj#GFTq`&<+sn zR;9vVZw8CwqX0}$m`7`CN3q_W9Wdj)K+G3%EE!G2m9Qn;ft$sw7BwWZXb~Om(PN!$ z{e8C^4nL?oe*7lq&3V17c1*5Z6Pf&X1U8~003tIvHx`SHtz{yU!_l?OWDM}R2G4Ya ziRmIV!Yb0wZQBUwpl_y?y#_e}6v}5>{7lwA&08KH!aPxq%hS`E?PQv<2Ng zK!e{RwHnsI>-E~PPo*Iqsw(+%IWNEx$0iRw84C49x!#Ux}HP({gk#S@H)J zTu7^0Z5;;HD~}UEo-kmJKN%-to}B&Xmt6S4o5znU{A`aZ$UUq8#A&GwtpKU%_Xh-b zVsknY8=D^E8Z#EnP}3wxOr&NemXdLF+<=bDKIX_tbYkTvoz8D>oqjq4ZJ3t(3jey- ze$Ao{;ZQB-Ou6L?%(P0(mJh)9kW=|uZGZjK2@o@UtBNC-**S#d zlaJTeVHT_{BmACmlrM zL`;s%VbYj>(wKCriO4JzMDZI(o6!Ugx$Oeb%-VxlPnhF4b<~RubL~98vCZggKvP>u zi7Imu@s6D~W{jtUs?>pr3bTy|F8|)bOFhfL&*hfC5ix<2OS6Yh4`-J)Uc+_pe5Qn_ zvuMYIN)U}F&La~*UArKeiJ_-AL_~|9HM!sXlwQ5U|-08+4wjqD8laamz%d)7oLM zEhOd05j({MN==PTjZXgY$7JuoKyNQKmQSy2CIlrAwE29JWb071X|yTo9L|34ClMB^BM;R-JS8AAyg{3<5DHXp>JAi5UfA(p-)h)0jj|UYI#I9#cyf zg@Bm7=~#AK1EzcRoBkV2Uus~~{kXx=gy<&oo5>fPzj$CVrya*G;P zTMbtgtJH)2`628D7{Z@`6ozjbOYz=JZJCC*nJ#J)^G`HpBVgp>B9qzkd3_A-JWpm~ zx1{O%_u{PpP{dOzl%k~)n;;pFq*&J5Pw7m$V@NYaAOgcRE#k@dF zmzfCa{7O?f|6EP2VuX``=_;GK3nye!RvoOli5YSP=6Bz<03%I~a(TnCj7^ct%fCH8 zeEagJm%ks*K7T%YID0UcXS@suw2}iki2_rB50rq%<546@Vak%1eQa8_+}3&RM*rO_ zm|^cScbU&AOzy?$0JqG~Ztu8ADwoS&n)X&Kf-gVVni$)Ef4W`FW~Guj^ z!fI%h+$BL(6>F;@T&d*3VUSsdtE)48DOUH1VdZCshHOwq{YjrZAdgg6s!4xH!AVm- zMX@Ri6M6Wn%VZ*~Vh#?Saye&bpVp6k4=R<4py!ALO=M~jBZf<+!UGu86cZVMWd_Fz zXObD*o4%G|b#pw*oQmC+LH@ZLrW`b;tlgeq9+LdQQ`&6c}F=h<9gkPXz57} z<7i<7=CI%GzjgOvTT7dX=x8z$IZa=pr-n=?ycn$$oDEpl*QPo7ow;u_sY!>!?6mXK zL#LTxl7uFXaceVzL3ZFjeX8@2zT7DPwzTvdd^mhS0`vEmKf(3#?cw3uw};iad>kA( z9@e4_M`HG{OX!5m797h!T=2y~<|yk0Wj$h)mqdoy$oUixhB&(y^%{ z7U&tB>}AZvT>AJp5Hq$1Und=+z9Ap{330BPOeB+Yo11I5@h}s->PYS6J>2$>y%H@C zKUW+Vdi1`?9$qNd#v1wA#LRdmIvmNcR!Q{anP`jw6H8ICG!j$b!N+qdlf{b!(3o}b z;4CRD$_~j*m(4Oyg|Ic?GG5>uJScqkPj*4hcC>H5Jc`;$5`>%_dvUG!(ORGZhB*(~hRW@Y*ZwmTO4 zKMlX2yU1d+MdNsmIqVVAgRy1KfJrlw+&BqL^yhG#?O9$UQz^YF3_GVc6%&`5yCkPd zsi6)I24+x!O-aNC&2=Ao&6G}bRI4=T*kL9}4INzy-5z)cHJK5KiT)uvwFEJlKgq9U zGjR8$TmoY5!Br}kkILoIWj{zv+{fP00=lw;oR}R}BBmXHsbei45t2H1%pPE~{NB?i zPoMrnO^eyv;Woo2$7ujbaIv@5cCW+V3SQmf)Lq1Un<8LtZ5I{_7-AGnPcFyO>9`Bm z)F}A$ezjV?x;n$~b-A36IP5)XGw*g=fta*z7ew8cf6veTLCgeXh6V=ug3;m1ejo%2 zuFL2EAV%W(%8$P`jPEGB7lxTIvsY&UUfe-y6aG#QM@O62oN5|dU}Q0oVc>^-*C zOaq|Mt0GvcY8q1`4dSSYqDqERY~jFXgO`A)Vx%>A4PGUC6Nh(%FO|tas*&XpFO5M6 z3Z{e&wLw!OCcZZiFcYXI%O{CMB3~};5iLula=3g9%nXl)J)pp>xRBjJLCHAjW5GE| zyZN4r*SUz1e}Q)am`$RIy`|;eliuDZ&)?ugC<@3nA!VM5Z}##sVx}>OVO$2kn8@EI zCWy*HA(h+7<=~_6t+2kZFdLi2F`#@t&MSbJ?!6u0<<(WSf?Fs6lvazcbn>wW_o)vH ziRYNgG5_KZ2F$+B;6RtZP}x6Q-@kBnTR2Cf&f)5bB{w%WXJ=De#h8VN(j86FDQb8H ziMeYRW?(ujVhq|1XiBTZIMSFvOmv*FR+EQlDe!WR*)geNw%pX1PDV`9n3q>jl~;ni zqUB95jru&sORJTfn6=tmaZO{&QDmaV#Ht_uu{eiT+aKX0_;-jz`hl1e6K;hY^Pgz< z$LrUD;ck}t(IKwRkTU3MeV{rq>%{!NDkl{wl;y&lWb?J@ax#T~V0C$0GoBo(R?WB~ zGCB#sOjDcOhnc$+At@YU2Fb{d4It{3`)qHf8X8o3-gGIk zYGq8+11C`{>hX){qjmud4N3!x(oh-Nn%lFPEZ(#!#bk9h4v8W&qiSgQJL~yS84kQG z0VF|PmY^Chy+d9umr-IK9UYZN_f8LK`GFO{iGki0EA60l*v*b69n+ZN@ujXNI9r7r zc3|f7XD@$}wKcSZw5$a)Wf(Jwl<=#ZqB2D{joER()$Zbbh1AYqco4*F%x*QuZN~J*_I43SU0Si?elZ_cQ(%bB>%24> zrEqp5v#lBjh?t$7!A{y^86aYkfgJ-nIP8cUrPdFfW157ya@3fC3OYg0qg@JlOK+>N z0*xW_`!mez-EtNKhn7sJ%Z;4b#rIF3xRh)AK}t6@qR6CW>|>0WV_oZC_rt+%(Ng4; z)}~9Ha!Z|wCN|AtR`{OrZ&^b|Ox z7xW`k?qnMR_Ow3$Vz#%pgQ7G@rXm$Kd9sUHKqfkhud_$a)$ckpQ@oj%wLm%^n(6I!)7yF*9BWTTPSF4hLZfXp9%{P|ulK=Zv? zrco(bH=IE;L(pWW2J{L+8l&1Ry}M1!!S&IF0?zzw12GR4-g<(YTU%J7ZzYOrW{_9> zA+ZG)j$c(zyT`_y(X?k2P?ul!d#1mzQ_Oi*od*D!eIPNN&e`9$cbIu67~)*_n;aIm zb95)QzYlO;_WNUr#l$QP7;S89&ftQ@l|!44<{E1txrs~~Cp1c{(~KNe<#Xem1WZq7 zz%wT_euiF5kDF{C<0#dyD?0kdm{bHBb0847`1bYd`GwQ1XuS6$TGuhm?T5z{#36Uz zq^3etnG!P_tq4qz*+xFHySoCwbX$43R>wE-SeEndF&4|* zJfd}tStsTlRXN)l`KRb$e{^GtQRq?7T`Yz56r$cR^{gj8IXM}gW>q*6a!?eXLRHRGqF$y)g{fD21!RhWmOx5mt^w~Z zsZ|-=(+bGUQdQ2r9TL-E_?%r_d$5?nt~h8>6m=ygErFK+%$XS?W(k;CLWM~eP_ujt zVl!MWkCq2Fw>Q(c|IG}lOxM)Zs?*&9CUY9mHp!NjpK*J|lP7Q5WipHb5xZ81%Z!9` zy$pbvSh zxApli@6A@}*wBf`m?oUJF?CtHO}J3)KVRR5gKmYQ3BBtiE*piAxBsItS-TwFnm=gW zR-x*5MRs4kI_y`tjkq$7$AHYpM1)r`^ZIK`IvDtRzOx)DKAm${F#xVCfmE&ekJjmq6=?z3C zhubTX8nr=&eQObRBq&+r8d4A?qn@U|)$F8`G$!#Cz(UWUuw=l5PkQ_UVz59=wG1_8 zyGTs@QS?(}vLyuO)Kp?>F@u()&lwbTjF@oE%)EN_Y6dT2W{FkhNB^pE^C%4Oqnp34 z#nW+{&1T0#OK~i6LafhgzyxCc6*VRi^XapH{tcT~kC;&^lOYA1%`jpbP+?}W`Iqn- z?joj#i){ciQ-hNSg|~%;WKO)2tE&#TD}BIBKo{qi)vELL1r*yCp@j_qX6)DL`7fBg zL1j7a*BSO8!kqE7rKRQ21rhT!Xyo0_i(?e2=jZDwP?b5T`HM;aaxOWWOvYV!4v2}u zyQjOmTrDfzR9omRj!a7Hd0B{&XJo`fc^@9E$zL#Y(a^AGpli%6Z+zDN(00A-6~C#) z%3I^f>AujfsmC@B0el2$z44NVl z7Z7t|#Q_@gvD@wLvUGa}_rFyJM}0Nty13HPb(5NPV%CXylbB3FN_j#8EH&@}YyxEB zeSBoVIvR^#_%9^;Df|9oGB&xe@P!=gLQyVWL3Tqr(~PFfkr97FwS@CFdZr}Bk{Y8i z)Y3(e)06yS=a^`z{2zPg*V4wG#_>ejEYY>Pv9&2^As8}Jm>EbUMl&YZvC;LkXjeEn zVkE&Srxe%F;x;ujT__np#KbywVzA|t@AL2b;9rJ2@zP8p^Q!6O5~`$|h{Hfkz!9b~A@DUBneU}Bn>C}T z#JV^jGxN4SgRMjW$2i`Im{E4bftjUJ=`1nvC{=d%=Bp0gIXgQnxGZ`MZn2zFnBd@(_V&H#?eFR7@44GPWCew3GJ(d-w6rg_zL`RBTdnwx0lyFH>#1Bk`U7Gv7ef3P zCnVLv%Yc&DEW|b!C_;z(Qofj!WM?{XfYy#!T0TBLoJ4=Nqk}{ulupN9V#x=c-ZW&6 zpvWBb-k&m#W|M)9W4um@*kpbiFG8Jqya2pJbGc&3CM>JKED0wh%PSKT<^9rbw9M(d zBy6YrIBZ&A?q=@745Z?C;C=u@av1`tJHlIdM1{S$Xf5B;M8N zbh^;7NtT3^7}?r7I7o;*cWMvt+3q5C=7N|P#5_w(n!jcSl32&3F>I})hh-G4tapd$ zL9*X9cN^M=9e3B30hv_$K__+=kK1a<93+o4tpo&7d1}o%YA_nE8t4hwp|g^mIzcBv zwH}2Ty@ac%ObMGzX`V(bW&`WB(vP|RCONRTylz2a8fv!cSxgnKRdde~nIP&~XL9jj zq$LC92`}NHR65N|9h;SEWkq)J!mDXVlSS{ov@n4=p__4ByT+kQI{m;-4 zyyM)owzRh*E@#$eNbAzl$|0k*wHgOx$D`j7Gi1lQuH*=c9#13`3%|@Spx{J}Stz7j zmw11A`}5~_zyJA#L635pUk<%CE*Sfb5fhLJGV=igrs56E8%BYclUpBfQ-cz*j|D%X zzQoY}!&4RXEXd5H+coD91RDggwDTK?8He|fb($@&&HU)<(yn`Cp483-kdWhWKumt` z-U!6ylau3vxTOez32bX_@C#ziX5D)tGI<{t%EwgD$Z5&h-)U*a&5<4YwhF{!$Eksz zb1}NCmCR*c6!E#mV0j<@0k;nhC%t^oZxhl^*PJAyOCIkJ!CS(!XR*R&EZ}2o^yv9Z z{!EwX1u-v(d7hX^9IQ9vU<{?&thIy6Ev)Eb)KY>}lZKwp0QWnP!T^O|`}1NKq8L zRyNOLHX6E(m)ZW^n*dBJd~MKxX|OI%0WfpRZ4Xi25i#Q|Bw6)0umAuc07*naR0mpp zjeaFo5e>jr#W1DHOWZ9BEw>+P!yo8#D72mJm2k%zRLG$utDX2ZQ!VIuYK?=L>~}JU$i# z;14HkgHn9_I}U+sQt|kB%j@&mLXZCaD~9F20<3_UBO_gka{I|`_3h|iMzb+R`SZi) zqXQ+v$pIoJT=x0X(?T9#m0wD^ng+GI{7J7mr0n^5n*Fi@fPEOAGL?IXrMN@7WC-E;i zLP>S0xb&vDQ{0Jhyp|mBYFSmrOo`Lv?*%b0hW0`i6x-uDB@mvNFM(RpNA2mpfM|_wdE=Ra}{ot=nA%AHaEK)npgr8?-on5<&@bQReh*X zeut3xqPGVdGkfl~UA}6%+1~narV6}-pIn`P^V&)pv)kIf7ROgm`i_{zpv+5d9&O#y zp|r>aL!nqfRTtEk;czTA{`m2jJ-EHEc*n+kk;5YfOdM?F0I~NTQD+env+G?K$V>Dr zdUAhpw0kuBc3JUGe){t9KoNO4vOyODHYtypSHmH{3p;Y^(0y~D4h=pA=HCBvefy`S zLK=r-Y`5&XwzTW39cn2}jE7V6!%dguF(3c45D45$JODhYDQp-e8CWOvxB;1dM5bO} z(Z+3fpQz@;*re}tOH5)CFFUC6qq_+IxB^t0F>i7?MDkMNypin#bQGT4kCfB!rX8LS zxp)b5yCW4=3x%b{onmok$>t(8>EX^Ec3P&v-MS#=1u@SPla-T=tgy^RWlZePSrf&| zMssbUH=HwCK@%EU?)G&`k~2vCMJLTgs4;D|$ei)gMjL>HpnrCeC}}We0d(-7NzGbN z&OC+S+Wr^Yv{k+atjosb&L6Y# zV_YzmcDU+fw~z|*D}oq^shgXRx^8=w2u?gI%J$ZQGA3V&Y#nKl`O(IRPnJCoeq+Q0 ziP`n;ckdHCyiY=lXgTsWsoYjZwhksEqDwCS11~)Ou(9#<4-lBe{C&>lniv2@Hvj^& zk$^ccH@EV>QYg$PMIdGknEK=-CNkZ2X2pT$oUdm3>(CxVcD~9blQC6Q*Hl5nB}l?1 z38cCNaX<6Wq^5zT8a30(A;S;}O(N!HB&HsLo2ji5@0x3yxyDb^We}NkS$+Ey z5R(<8GZ>|t%!Vu@=EJpWTeUj#uoa0p)izzJLYQ8OVv#%^{}%s-kvZXV@8jNoiI}Up z&RhjxPDr%F2R>>ek#!r>joDNL_a1}+_-%9f?@!8XlN1r}@`1JAVPqElbRTX?P z5EC(3E>D2W#6cNWFMpp-Q+`mlF*+BPvrOhK&Yx@Rzk-UgP8!bTmX}p^Sz7iOIAJHxM;y+sOaN-r2RZwPtbLNKZh6FKC*0 zpcaB5v4t4OE@^b{#L;+CPkM&F2-QI|hU{`=&`3+#G`4o21cMmN3yoX$#MrbKT2h^w zo-i=bi$Hr}YrQD+(yt)B?A3u;Ydt#}Yv&u}QBsYGZl(P4UvK|K!0ci}s!$dfoUyRCfvBwl&@Qltc#1~(E`}PmN!+bonX*^D3 zUR26JHDcC?d6Jmq=rgRktWqN8W7*e`#(FadHM*Z@iPMIO#=5#ky-vV`V6hm`jDs|$ zNWiQhljJ7w65eo6tj!eCrI~^6Fu4cS;bL&y+E{mV&BLlXb`QHU6==-!KdF>Y2(6?w zk2dtE7%KspjTpXz*i7oCrUEIQE=%?3bQ+X%nw;Dc*+48Pm*lZnpfiz31*K3?lI0Q+ z6L<;0q~N?zD3`Cke~-k>t)jvtY8Kw)RyaXPQdXD8DJXAKw*2Vz8cjC?CWuQ?n0=@( zdwVtakeSP4pmIxPF!+SeV29`P}!!e7^0E!|90&fFF(%R(1@I8&9sT z?|u?Ts0W*O@OuZJ4%Wqyt#EKweFe=GNH)98sV=A)B~ax}A;&u2yr z)`;Kt@y}N`;E!WG9zQx6pV#^^Ys5T7Otu@0ZR%FUp0+{B6{ahz!$6o}<8>mL(xH6Q z{YJz*tG5Wo(HrqNYsX{_nFb{?Cu-VhEZfxMJ*P`IsF0S*ng~kbT^=6B4jX3iJB+-< zyDlpmaMeGzpI6h}?DAAvisH$!jgA(}=|4BBI}+{5WFnbN1%k31n~KF^!IB)zCgF4d zF0ZGb0YTy3F?kH@)Tyb`^4#1srW7F{Mr9eALv(`Bd&4B=8%1OSG7GB(P?(Wcq5JIL z;SGZF=4xWLvb|`EFQ(F21Yvo%rW+LI*}v$|_VqfQPGqJRG%g^MwSU6Th1fisjiCWZ zVQzIUcbb?U9{;XA{gL`JL!3oG^Kk!@_Xmfcw|E*`xhB%K?E}&E>G0r4V!Esg6IdC) zhJEV~eV4?wL2(d|2*iBxBAFq4U6K}bxa!!PH1W}hxS z*qZnI-@VI~i&pD=G(10%iDK2~#p?{m8LhYf{P@o+IB;LdWGZ6QQnBo;Y0MfiPZ5(+ zhgi_5V}Ut*j|Q~Is3Zv~avbAvqIL~CVDt<^e6 zTGNIr)OtdXuys7GZfwy0`fCM<42~*wV}e%jAeXgT?a>zyp^TUWQ?0f^t35VQhsQ8v zHlA;9PvBg3Hk+LSPD;h1gbB;?bXk&Pfk2jds;6QyQB*DkV}WcymO=omX?U=A_P5!u zQ~;Vmg^88%@*gZH&!NnOf8c97+&EgRV^Ds2(5HE9i?~Z{6DXq(a;6O9VK^$Og9f(R& zG`j5Vx753`cgjZhA#DWbpeT$+nyIN??4@+JiFW&us!#~)c zZiY+ohi}+2*$0K~xYcZlyo<$VanneAVlo;Aq51T6w#RMueEIfq?G6BQkmWBOERktB zVMA9VW{sHNX-urObhI}%)Uoo5Dh=^)JHSdsl^QhqNhH*%U@4eQWlPh=>C)Jvfq~vW zg_x~^!8GbooP=?l9C=AWxz%K_NmGFUQ7fqn1hDg`QA@njYB2;?B{1>WiNoL+RR5$! zHln^9#%t+l*R^*r;j4t-tbDIhBqk`<#!el!tD_51iK;4IHnxKYVqk3Jd5NKKDN~{Y zLNi7~+}Ms;EPkbSq7Vk= zea=2iV^VDH?Co_v#?OE0y9XH6^iNCWa*-6~%q*K^#~TFZ>YLM1dE7iYin?pO4Tw3^ zthcwEIb(SL83!W|xAsKGUEbyJ?fXQ(U))#fNfl|v)#frkg9c+o;m$T}?@I z6)%lOi(vAeOX|G(mP+4GlhEJaZ?X%V`-C+ch1E)xpW|&HX(k&zumM^=RS*QTgDgai#dYT-Y3Mffs?Da;#{Jn9WGc#js~S z6N`le*TdgG{uh9`wnj}we1(w?%B1ppk@8dq?B^<#b7_3JoZ)9C`%W#XfmzzurS=u%B45w_) zjRk{a*xjS8Q#In8V46zUfHf9Er*ygmHKVIl%xWkJG)=(O04dq@BLv>$ z=Io};-mU2!a1t=d+U)`|l7jNKBj0s>Hxjd3<8)H_2R?XAIm`ja@k z54-~|Z7WnKk3>v1p)$yL5|}4u9gV=tR6}P-kA$mo`mM)ZL{*}&sfAGxkSVls9B09O z#&{#xiJ8kU(^6ZTzNS`TI93L->U8&DqUy zOJrw$egXt$I36!9*jioJHa6Do+yP>)@w|g)?U#&}^v)a22D7C`%o;Jj@5f9e0*|Cp z08@d<1nvbxL8wc1JgY2M)B$8L^G0P0?{a8h;IXrt0aLGUvj}#a9cRe=8^-6iZduuK zpG+*9f&;QHL!wW5RGP|mc4iTDz{spzW^5}0*#Mj|pkEfK*a=Z~0H<_fl#EO!v%z2x z?jnhpr5QtVZ%YMAgv4@zky7bHCw~p4CQbfrRH4SE9+a6fmS5)RfuT7sR?59|U#Y1G zH03cc0hz0SH=AAK1P(ei5S0@kFPtp=u(l$Qn4r14oxM&C?fLm1duR8WR-VT3!Fsj9 z7aX#dW#^(8)b3ycp5mIQ$rxQ1980IoMW(15sikFzD;?6tW7}LvNGU66QyLS})V>UC zEjGz$+AS1%A=ryd(_GArS9`mcCHoHy?DKtozoW($_8;(^_!47IjzT_upO@eF8Kd6_ zj5%dD`vQHwj6r9{u&hWfV#nDcI4_s;WkzN%G5y4^P}mS6W}<;Rj5RnsMjXf_J?RUD zbiVZXR7f|K`USw;+uQo#;>C-nldxE0Vv4{#d5Qva5}Bt87_fG2zVPDOTiNsevZ<?do6!hhvK^5Ylj}aa*r!Kh+SS(G5^#3nN-PTP=>bmuQM^9y?OKI*^j<1i7Bq=yYcZpG5f?kN=yK4jsBH6 zWWBI*=kepa%%)Ac_k88~<9l~DNuzO983pC&+*~Xe7#nj+V3OJ*WZLaK)Q2sF&!>2? zPilHP3!ZJT4<)u66w}vd5iK_wh>#gw8rfI|%E);`#L)HnDxVZRbFB_73jf~*h1vis zA$1c+XuJ}KfgLbFn9vRx<$W&ScHF@6=?IP4x>yw%m8vwB$)l@iw+cwEw}DCDRcdZG z%H9OOfk7*4N~^iOHqs)qm?uT9lGqp=bqZi|F4GCaObX+_8Kek3fX4zcLNwmUJg>+E zF8Na4-K|!8iK!TLe!JTf=1y`DQ+1cpAB0yC6k>8Jq%Uj6!1?WQ8Ww@dib+u{ zW|!F?oQnl4Mw{&q0H#1|$3s}J@zK!XrM~khcNme#bPzEo)82IM-EOvqcTX3Vb*$l~ zrNt-z__nAj2DSO}+S_N4n9tt4|Ly%V7MWZucZpOcz?mxciPeXtlA^p>7%QG(%xR&6|YN}H^ zJO$$-GR6l$lzB;atDE4H-9^6Jvk_#IWUlk@**QGgHRR{lP>F@YD|OC6J1-byLs^qam-b#m72gD3CjPF zLfpq7UGF#%Q}HDa_V*s`6&_qXO^Cb>Vy1yi+Ak3_fy~MDWa{+knWs;`E#&44$@KnP zI?{T&RPtLeWsyfaa4Qiuv+A~U-G?|@neaH;CLE3qGTU~N{vqNeZL^o}eEIj7*#vEf z31mmcJ$4rA805-FaceRuM!6U=g!?9=pH2vnv>8Pb6G3^;g2?}~*+9$_D0QWqFojl4 z(3sq08dpn8iN*QbGut&)DXqW!_-hjG!Vig&#F>viF!02 zlP&69IZm-MvY092fed(=sbtGI3aLziOu3WBES22$(O5JX7}TodvK>?AGtJhxRSn$w^PiOa14wm>rF2&d=Fy!WKb@`@*%IQytC6M zW}lcxi3xC2GHt`NIx>o@uT}xeMx8j)sA`zWlE!Rt)dwj$tKS?qYbw+4!O9*A#CfPB z{E8xxVh#dO{(fDcE}w6;35f-YOpgxCe5W zOEd7hEZwq225B}koDo}DTi;XFh*=hTGgGNlGSO&2_7HWi^TD0CsFZ7&OMHy*2MN77 zPL$8tBQvt2G`JsqTN| zVEQ1PKKQVYuY(j3(-4~cq447Ni^2mjKa%S`j7+&j&O}b7C(kC%lE7MAoLO9qT=7M= zuALiE)RNnyAj>C!sfM|;$fK&5C>H07r#Bk!;L0nT7>Q0qB2bUO;Q zX{MJF)7HM~-h}G3TE8~Zk90PRQLxn}viAgH+4H45TLvbG1=5n~~i%)kR_lemj=22pbe_;5Kmw}Ob zV|A5-;d)idHw<+UTx(^5vFIofU`%!vF)>dHWZI{hlVXke`RPb;`&=%UV`6sdGH?kx z1R3)!rejMWOcX*Gmn}M-mL?~KgJlcK^H4lP{GcV+SM6v_f?c*r=bxz86JTkA!tZDn zSjx&ESbiu(`MQ|NF6CoPtxQ$|6DCEXL@uexTP0I*Ta8ECFfFK)i#m|SY;_I;B%vwKq9k=xcn1hn%o=rpWI&rF>5ufzlkeN zBIe!!S}>FQ2mA22^o4Xby>1#FX@$aTVZ07rGBP8a#T3YlM9wDWZ(X=>;iua(XMIyw zys2Uh4(rn+E`@zC#mGgZF;%-8)|gluSyNqVcQEddMn&MI-yU7Ne0ODbbbvk@fXsl? zs5Mb`XlXc-bJR(54WKY&3yHg&*yP=0u+Up*GZ_tfgH)dazHMG=wr?GU30mrp*2~eu z?gKEvtgxF+1@Utn`()`IX)$k$t^}c!AmbThc~qNd}rsoJ~8{m z{2Vc*0^`PN#LpB-vN3Nl#>cZcg`-8hczaU$#MT& zzC$He$eFgz%#gfzsZMRXi)EaL9s3B z1W&VZP_(UMq9lQ2z+$9`To@SGP&jM8N|BW^x>kllq~V`4=xJC?{Y-WF9alQ^^Pi=8+`kI-X7>NLyZh zew>i`WIl1;JLOW*+A}nA!lfuG{zkLOSxhDFnW&URU(wYH;&vf1#R76^DeQ3lX?Xej zua1B5=iko-@IujlW#KTpgSk(PEcG&rsew}4Lj1*#S)5k3-q0D0;%VKI((sujk{pri zkBZEY(3ojN<>{O6)=QaDbaK{eoX=FJi`87SSy5Fbgb@~3FpT%%!ykQKM>{AV_nx_a zCH3>;ot=AqV)lvIOH2tZzU9=~GN8$oUlDaaHOib*MFbkpv4{g1u# zduc0A!#HEJ7fVPC8PZIldm)O+&XANfMf?@T#(E)2(ZXhmkw`5a%A6jyOvaoun-)q) zkv+IxG^r?N(3SS=+FGJEh29D2#rB66f)(a^ugmP+-0u6l@At%LyZ?bFiNqw0Ce%-! z_kEu4`|U`gD7Q$@P?53g=e#W8hAW}F@f>Q2LHxMtY$Czm<$^zJ$ zN`I^JTH2UAyOEAfTpW*SD^I{2(V`4Y$Lpyh(;`|CGo#Ec0_Kk7)lw1 za^wFHGg>Kay;xgIB@0{aL&oG}a3}`B*Ahu2`UwUzd!#aF64-?|R+zni|Ng=9%Vi}p zC5Ji9XVRHh#%>e}>HO?${?YY?rFpCpT+K)O2$=>Nl(E=QMb}D|XFEWq(b-w5*tT>} zZM#%zZ)x;<Y4WXXF(GcvCelUQY?>1+H}c4@6EV4q2~UV#U{vGwr9~$l zu(XqQCCLwvl|I{QCAkw{uh-?e9aEnSehCyQg$Z6FteJ_&GYD(?!vstWHwyYBiiADP z37PI-kV|ry=Vu`! z_;`A5^&d-xmPn{&`a%m~c+xg)qxSR1XBE`vynkfLkj7?3$Hb!JZOl;Q!u<8$VKMEO zXKz0m{LkL}qJ)%RS zhn{g3{ak|abE&tttM=nA31&iYLdJ0<2RF;5VaT@)0Gsw@{P+bru?jY7Z_|AYR-M-E51i7Cr{0!m^AgHA}X zZy;)oTRBS+GrP0%dS_>Xh`H%#OzlvL?yBOWx=E`l>*>%Dnf$#uqP8vLQk?%S|0W8a z+{r{2P^-aqMnVW307(($dC@qEZ)E}TGZuhLKoh*g8Lx|=wavujVMV5+E^AIzUe=8m z>&l4uJ#_xYFY_>wvBa95K(jC4D9%Iib49^ zb(Qty%F3m#E>)2eF)!Zy?d`!j0dswS|CE@g#5_q%b8CZ%sWCCjytv2Ix!6UKDqgzw z>&R#~0rQtI^inY3v@wxup=>9ejX}hmg)B-32a+Br_gzrnqq!RFUW)BIid_q#wDt9 zOA~5pT8*%^2;WDI5mp+LN@bh@OtUmpXQkLaK4*e(7djmZ^ZkfrE;d0zAn?g+bkKeA{t3}VtpM$YO_Qaw-~9Q z?!U^!_&U9O>*Y(5n1W1hW&R}nOS(*712Iz{=#=`9CQ#1J7OqVXkKIUPxSI_$mTbJ- z_t?myKL&Xz@l4j5ni&lp4gJMn@<^y~{qJ9WdHvhJ5-#KDM)>`8m!@8Hdyem5rXZ)V z>S^p{{;9c@DC(szbZ3AhCbXrCfaz~$;ixR1R!@WL<7)b(mxS5R*O!gCHi`KCdqJDyKXY>F=6)`}W}R`uh6g{r$(M#5^VDNn&mg zF{wnX*!4yq#{^Bx)LASGF?I6?2+X^`i0K=4ymHmWRPImbK*1{HhKG~GBr&&Hxn*}r zWJ)9Pkh_?UrgZ43tYZg^jLeK^T4XEXEu%`WGEu8slB4%o<;s@zBf}oV6q z{}b(L1eER;aFJ%|l9FZt>8_<)LJ8^aj$Kl^OG>0+>7~27q+9y(``>$??$deAd_Q~U z%=>)p!ZVQ-Fr-E@*?f>TlgWgsx%@c=cT0FN2iu-cdrH1TeDPElBD?kG}C?U z1T^Rz$;m({YC=qt1gWThmb)l8-Ga?${lp7q%%t`c=)@Z-vdkGfEJapNP5BY)@bSPl z8RCrqtpGw_hq6Y>*!Z3eYKv2{2NQuGzWqUKyUpbOKBRc5qKQh9uV&}X0l9$6OIa_7DPabC?#aNvaVU6Iii*lXQH%%LMQf*XT?Fa5P$d> z9=Bm88a|+Y!xMYl?kWP|p4MHLc3Wyuen*qw9x5{5l-0!Gkj6k%RSlfaY!i z*VFQd8~QR;x(z<3AbUs2>@UBfI4B1@+b07S;waDZwz@I?!2pW5w7Kngm{fwI#szFq ztqEZ;%}L7qm!U6Crxd0uxglJV3R-Gs_WEdC{`=QHHUY}wyvC(9+`jZ9E&%pwS3rr3 zE>JSh&Fk?!@7&}KssJ`y9zQ-G-6eSFlwpcpUicklb=gt46Zex;JAw;zgUS>+`nri( zUBuOc?GYku)H%;V=_qKA1DIgvbZ z*ho9S99)K}FJyy19SPQ)wyinI&@(>JSj(ic^(WBxKJP!QKD6IbDyojFKjVq4tcrXj z5y2-^-Y%RTv)Md~`4WEe&d4*6ougm2Z_EbN^mpshQ+hZ2^p+B;hC;MUI_uN^+r@le zhrYPVlcC7s*g7@uF@ncZVe$7Bq}zq{@yqbo^3nAR9dL|#f_2jUmB_-d`$E6V_K(W6 zF%7d)jS~lZVEYa3pH_cq7c@B85k~YMZ0R|0u(leelx3w}%z(4&a4?ok!+sL03Pm4J z$h3vlJ*NP01=8G&bz|PLp=~{7@X;9M6`*=aHaPZn{RVn@YVM&;#`RL?+0zck6e*!D zzcJ09*oSxo>C?VT&bPoEV#;4&xn(9QhPL)wzM4uoI%IhOLRx@P#~=g&%DwcmwZ0^J*Ou(h?F zP6^%JdlO9f4u_HAv>}6({t>Oj;`J8JrzRZK) z%fa`9#fIMF%?pMp(ADfv~MOu;p+{+-@Y15)7UKQCTu|3nEkYD3&n?plyaoC68#{ z{MpYD7<5VasZaCE$H_1;Yj;hifu=*XFUpGF-*@-5pI=!|$B7qscAGl>CDC;pJEwcE zJMcHVXz|Oyi>E=g8`Ng?7~DUMUKjP1#v9LHmz8}$l)O6?AtAXJ_!(#;HGuRfglMG< zj}rshIgI||)hPlk;39Dixx+#YA1+52+Pnrc&~4;-~j=L{oFb3cm?!|Ig(q zDbW+Jd5c+7Y??0D=gfz73J0ERb}`!VDrkp8eCbs;22e1((f zr^R;-)$OBPLzEbs4f7%?$=odax>hk4KC*!M$H~jvQ|Et6O3TPE`+NW4T3_q8I2{1d zx$F8mFJT$7V8lFK)%Ml-ep6oq@(QVGw&yweU>Vu5(U8ofRjih8>I_bj1nd3Gcj;#o z%dDW=Q5cjyFn_OO69WE4~1hR3gGZJd0*R~BLY-jwAyj9dX8D+k$ttV&e4hzAYc;<0!NP`|m624FQc z3xC@G&G3NFz5Z?(UC6KIf*xJnv{fZxhtsA4Z8(~ar~lP+l$juU`U6K%cf;zty~}I~ zh4fG)rks*e1Mm36*&@9h(8GT4SzakoKJ)43akp=B^1fU9YG!B#PWKQ{ zhA-PsYrg54$x<%HmaJbDtXPsx$*P?FUOv-ZHl2V4a%W0c2Rga4B}4pRvCeqliI2P@ zwV~&@av<8U0koR1hpd~YkR%QHQ2ovTe=+7@OUdVWvD^$fP`y7Ar^*ryW6^ff*31sp zOer(=Ql~1u00D^)5DHc*NW6QqyGIIqksR>8TptPY)rlphr`?^`%Z=B6#EjQvcG$c0 ztU%jQEO*zw43QKSqlt*WQ{zY@(v2r%$_OEyEz_~hH{ugc*NPDwCQ^;-r8`H#>=N&! z=e&F73RMhH5NAy|7&QO$E$5sTXC=swl*egEjaeG@meUT)~iBVnNyTB>DDLb zf&q0WL0-DrI_sAvUnLRcu+&(t5yz{1Eb~`4M}$N%@gKrNRRQl!llO@nFf^63ov;KoOfJoWY z&*ZPNCC}qt&3?`(tNqC&>-&=fsU3v}@_!MAvFvohD{+(v(#ihBG$j%}f?tOy<_g5Y z^;JPK$KUI#J8Zn4j(5=+CV>c))G#=`uxIvQo zGNTzd0sRGA%|A0G9a4wf6vVQ@f)+qziRw1%<=jHnsh-RMnDqA!2`wTLQpO+yvshu( z=6>bv!@1!H!yQGudZG~{9izs-?dl9A7CeCQhQVHx{sBzXLl@TWn-)_0c=2VT;$S+Am z{j%Isg}*yiRjt2aEkK50hwD#~kZ7dXPEG%OlUOCKv@VbpLwDhLYjbOHn_pv&+n~FI zvv{<@K(yEwmnqRLAd8%T&;E|A&-9%3LqngB9-XI4j@*LBP3QS8L@{fnovfO9=PEN3UsTA*gg_xFG zI$rsWj(2m!-}_lbz6WGa`B3;2PIxl|2OFC_0zns-HoKEW)a8M!D^pINC&52YCMBH3SgL?zE+^d ze6mKEc1T#@<}^Of1rx3suOy@P&%fq2|e(EvH7+x9D%-Yi3hs|6r%*1 zDVy0m!D%7D$D2B5!UI47@4Q+eJ{s88N+ghqGw#OK#b18o_L=SGCB#Zq`Fw-+t@&&{ zSaGe5Q?`|3BfVgt)|vd|ejaHgJdEPsHpd6o+uP8o)lis-EU2z>vW6$KV)%~_v+TBU zaZh7&WjI8BHSaW^9jI<(ezBBCdJ!i5*39OsxsbYqd3Rf*MxMnFyK*c>=%=MlMOu?I z7Nw++o$nL_bB$CfgTDuyepS0!R?&Omi4^_Cw&@S}w&;Tk`C&}Q6%enIZ6cGD^CKGS zk%_=P_OJFOw#gtrz=5g}?5swh%@zH3NCFC2b~(fa)-k zy_3KdBgi^uOwLbJiAbG4zr13iyF>;%+S|j~MwFCa1^7A#B^ISEf#7y5QQGk8&kRGX z{r*;X`Mc)Py`7MCzV>+hrXe14GF+frKe`-xB&j4$9Zv}XL&{AB$6G$W-2fjph-cF! z;&(A=v2LT6!}hrJ`iFO|1h7!+Pl$fc)x|GFHKxGyARGD9hhhVlKGE#rSaJVuD}>85 z4s`Vt!3hXqnmSKA>J;~Hi@pFj{Mk2ADfG_Qo27V9@-!kGZHBxr|tuGdgTaI!Q- z*>@NP0?~TUOa`5-3z}%Iv4>XPi@Y5zhQl-j<>8s4q|0qBhh2^Zg|||Ec#%8Aeh)gv zYSR!7=jkw#mZI?BptoZwCjUM)juu8TDcR-tW)iq6!)0Md+(79r@_g<&Rz&%}74fOI zNKAuJ$zN*JUP{&;U7o@lvs*PP+RQGujl&pw+-FD(=T71@bZ+c*b6?1QU3?EV8lhk! z!x-bIf0n0dMY-$zJJG1HH{bU< zp0<(js4+Ej<@g0&b{Y*|b9CXpc75V7MLiPT%0oL0Byu+_CHB07~e@FwRG~%`rMx(v}flw zvHWFhn(NvQY1es$dj@j0u%pG2;ZO#&I^wq{{m~@&sn4O$MUyQIN1pIjJJ(h)B%rvkID6BM}Cfr>}2F$)n`2i@?zo z0r;>ZiK$@h1Uj%kiwqp=Nm*iC8=(yO{6p1zK79yv5MFgVp$k10jy-K+mD1 z>|RCfi!YSRm6;1KO$-OUM;st|Mdi;mh<*ifbbO5g%G0EyR)S;)O!fBsu9}-_cl)y- zKn%;A9{Qw!LSm6+3mOFRp*6xZMhvTG{!;jQ#i&?f`}!@wJ@w?!i#Eixhb2v1R#q#m z@o>*cD!DOR6``e^mN@dN!KH2Aq4vZz?(p!q zT^$AYHX)3#Fm|BeZoKRFwzFLtKf@y(sthus@f0V1pAXWmliNBH{~w|U&$2UR670_g z@9I@a*?O6-nih^R$ptgiK!LLU{=syT-A#ESe@erK2H4Acw!A zS{ll*O#TuoHyK15=~ep^P9ml!fNUu@To4kmsrER_H06sJ(HJ)2J|FS%RC8Q_Y3eR6 z$14r3L63 z;hR`8vU{wPP;FlSV8>aNQm38JBb3x*u#2r2C4K$d_1nBpX7aD#5d?`ZM961HYiRa) z*i}W`yoo96A2zry?OEi8Qg!T8=>K*WRxs&GS@>v~rH^N_At+gnn*o(r|4p2x-{U`~ z5k^kvbCRmw0z5&5c_o;ke^+`|aTSMlW;VEplvMvA7m!}&j7A7D?wWzM3Q=*}y+WP` z*6mF@##p{L&Iw|tF((N}wN{-Pzlj-inLOZ$82l~@W`wx=I3!rbq8c3kt?^vte7luB zW?dhpPn*PLv0%?Q*RB&c%!uZXjWUtG@Il(?`ir0&x-LbQzdQS@!2^T?tqcL4QMPw;Fjnf& zr@KeoOoo`jQBuwdI4Mm`(FCYC&Nk=!Lr}n%BvxPt0yJ8w^tEVQ5w!YeK!uYdt6$b; zameZNfL%+GY%H`M36$K{&VYnwGSMqsiz*83m}prnFS?%?%nUa zDOcg2db;~=`fo5I20%qdw|6>j>~Ajfbfm0|xY+At$7+QFMQP~pW`$B;zO(PC z_j{%d!bXDKSx(C$f;Rpmg_C;j&&UB<*`yE?u$qDI=LEslbw|%5RQf2P4kSr_CrW%( zuM~Id@C)2eGJn?tAvOi#2KGA}Pa;neInY5F-cH!U(YI`InV9Hsn^s5yFL5veFA}2XwM6FSnDq&sh#0Sl%u8gE9jwZ+KO(|q(g4Z8C*c_ipZ6sn!>jb&vKxS%?vPL8v^ug=>> zy}JouxO{;|`{z&`2LXz9ga=dh;gaQK=&+<2{*Rz=JB$h0g~>sTibUn~tCb21Z{K|- zeYhcqsz|oa<*pLjr9Jn+Z&0B*f6?&Hy6A`6`fj>Ez%(m{-Jpem1ps~B*;Z~`fWa^`vq6Qb0lDTpt>%||mQ}7$%c8jru zDY9@Wq!g|9UHAks;r4v++c)1roF)2AuyH&u*EfH60$e@WwYZICDLb{Bb0WII^W9t;2Jpq7CJb(_hJr1c zqVTpx?e)L5_Jo5JY+2#No=U-l7!{by*ORTqAtuyeJ53a$ktQ1vp503%=Kd~xa;%siN?T?7~6;@X=8QINPB9?6fB@jj3TJ=vna)A*{;j@ouSA zfJK)+Z&VyZfE_&!FL_6r|D3niwp zafjYk>h;{j{vmH_+WU=*zK!1nA94moelk^>Xw+i2=;B_YCYa+~CiNHs0RLoux1#en z4F|&ms$Tvx&nR(^YznybtcF)t(VmPL1xPuZ^7uF48oQBlemC|1RFvFM>p!e4E(*GZ}W?S7X@Cgjb~uvx`LiW!X=mO*pk zGG*9R5_l|<`2cyi%YRf5?U00d1Hv(i9@lp?aUdVLzJXW40Q{-$ZFRyi%2u4;2Bbq$ z>dG^RJWj{x~qI+U88T zP=lS=u6MCZqAy2jMqPX?Cmyo2WT$3^^y+|3Zp?%W&|-dA!vk_3-m15I&(I)vgg#Ov zQ}jjfBY+4qZzdwNSe%`+I=tjx;e2@(ZT!+iMLs*yE-g6n-EZ)xj#tHPH@D3Lo<`_E|94m}idFiY)Q$NA92Uu4F_+^TNTBA;>2E*??{>z0! zk66HmfhvN0kpo^eo=i$dLt}c*7fgKDRDM|@YoicoxXo0Z7&bn%%{%NFsHBVHE9xKQ zA{7Yvh~c!@HiOW|o1trBae(3|^p6hK9p~YwOCehk!cAsk3J5CQX?(^tlo;it6bO5_ zPKND%DFz0g-RV+m`#pc)iTWqWw$F-w%EHQ6%^i(t7Dj;uXnovZRj|BD|ohW70 z4`d0v+AYf1g=VqfcNUSmxCO3JY7gzX|;xtti+hIop4F`>8%qz@biM( z(S-K^#d$!xlol6o<^ku}KQ(Ih{&4FfF2S44f^=yjcwh?nk9#z7QKqonD_9DjYvx@k zPZG0?sx!T?+;UJ5(u}cXTEAvu8eWW3I0Qg&vZPH|*ug?+2N(9rX-e?bid`;ayNK~; zy7k&}_8?mQEleF(e&Z+duzPo@y)z4FN~tzzMFTHpiLyOU<)Q6U<~XZu{CNfbEhD0o z$TqYQdvP(msy0m#(4V3S!OXvnEW05(x*gedC+h`#~qa9ASXABeNGTv?_Za99(e(Cni6;7_Ux6%0eF zEq(V`L`8qE;N%~*u|sudwO!s4ATmZUd0;HpC(XlR#REFI%0}F#J4AqGGAQh-CcpX& zeyJ*&%EN(Er&2p!DrUa9B_+2^e7o)8pVnh~0_vmtk8d7Atm+@iQxSX)P|ST-yq@sG z1rOA>PFMeQ1&j%J`Z916d?-pfv9WbI_M;Jf`(Mx)El}|T<@R{+u>4KI!XRJQA{gId z*hJi9G;;A9_KZB~qPe=ET{TThyWkgGc~L;| zE8D}1i}LM-V7PH55>^m6LD|7iMoHwGouOKug9O}%Zg&XSofxbAa5MykAgGYNE}_8Q zn(zm~E~(~m21Z*G2H_!biTtE)7F#c+T8a zTcXM{JRdF9S{Gkbe}Jap%P5ff*}4v_--f&7vo`HLASCj+Zqwv4GIB0~?^ z{U-TEhcfiq)hu4a!$5zEUf@KMQ^|Ux=23Rv8OWAghOfU35!>*7Ag)}wl#@Pvssdsk zYvZ=#b(57bP}_IJVoXCucD0>HOpp(uUqrD6;X<8T`&Qy&^T&wIYeFneA!O4L%zKvwV?C5|LBg2Oe>ifNe ziKhaosrEkldJJ4NUzh4c6KtOIjl#p%cFsseri9ruznbZD`F&g1DyW}<3F@K9NitMv zRHl}p1~m|}-n&E+dk37{=B7D#o(6QsytRe1umTqQX#w~Ib)r8|;683URvZ#0Cx%&6 zK`6afOzaM4vTtSbS3~5gwJM zzVxWFwI6jb=D=lgd$%hm)krPKh)Q0xH5VIjlJMrHrjA=%8s1$pJCMSYNiN>38F9~l zP92jG&*wP!Bf^P+Yu!s9?ryBzcjBuL%DFXy_OmuPrHWx)%w`ZvSL0RLWHYry^U1)c ztmIOrn%&0IAymYqXX>yRj_dwy`)D~ZBRze4gqslY@C@dGTW)G>q20eZIWfx*^6iiL zC4-OiW<~Hct9%WeSZ0FPg@4VVaS?-;wP>pTn-C<9QB&1f^c?#w)q@M(H*na!Do7J~ z7?HIfC387r!=qvv;$?%^0OusTHt9b2sYv(r3Z^?5}Ps#k7H!6d2}*%T_B zgU`Ki(0SHv>1{Ziy{ocG$6l+&q5h@>2z0}!dR$6*Yzl<@c#r~py1c}c1`u4njTjtg z72NI;ehYHjX_a4mLQCca7p_Hb^r3=BtR zqD^iEkb=NUteM&w*Pugxc$Vne3Q{M236wo^)g71%H7oM&QqlM$ zx71F;4%d>)*^cTo%59T!_{-~^;0}%s{Tmf^*yUJKz z1r4EzGPb~R#N2?<^DVkG${=|l9iq1@rtQYIGy9DsA(o;2joH^L|~)OAShGtbWfeQWA(zp`6C;#roh5Gl8%=h z!^j;Ue#+Ft)zIeB#aD`iW$!AfrQhzSl1I_5*5mzas|$#Uii$wGpX$qg9z$7C2- zigJL+`t*f@3M|>vks5vA$ zV&LKp0QKhb>7BJN^o!Px1rrY@+26Drw(sg3_MeA%@4SGC*mDIRPB=ozra4uEX^ z>fVo%2P^OUdd6E>0Kx#Wvg%B39CV!hMKrW^Q&iN)2jMbH^Y;_nP`Qj*d(o9;5Wra7 z?fxB$t?aT44kW_~C{^Iq;+$k1YT{`E<7Q8UGjl%JX4DR1UNUgvFd#YR&*f3Qvm zS`E><97`ASNvzQ+x4ZBJjfn0f1FV2Q{C0n4hI=w5=8wOBKIqzh)U%{*{uI;5b>n-& zUSI&zx*xlh!MB3<_u+-D%Jqp& zkH>gu%>jxhMB3LS`YIX<=IKc8>dr-8>c`rL6cQjL>fUK@TyC-{>GOt<;1dsj0rDZd z$u|(i!@Si@xd@64d@t@|3NaxLRlMXB2dVH|EEJ`rzDFeu5yz2q6~o-(k77U*;IwBLtQ+u^|Q{a>Xd91~cLPcSX1Rr|HO5r-3lR%_QUCZ8!kzpcJn9dLJ9xCX8JUG2Eg!Y88Jbsd z9K#)s;m7;e(uYv+`xXv`EepM?{qUYA`dP?m6{DYT=sEIQ2dWJy<-~2Fk{I4D^~JFY zrcr&7cnNWnG|xMUTxHY0)Co^2THjf?g-sI`DJw;1GpPOJtyXNo%k>w@%shGXBC&pm zf!2@8Ep5R5l}RzW;4Y~1aV{GXbJ&;*DE7szU5NbTKwgHtyS!Ac!0O|`eIj>J`{HPh zVb#j9_(rg=$i>~@)|OL$|CW-Y5;4LTgwfZz*PeT3uO$Z}OQ|bxbKM_te^T&HOcYg@ zQkD?HN!T*90TTiuKvPALej3oHtP2Gm*S)xXrZ_izc6sy04|8uIBqW6|uV-=SVSK6d zK2C8rH(?JCOB7TxxlZKPLc^&>Bzi-*U*YD^#tto15`DL_8qeO5^K z!O(-0Nww}ZF6XJI#$j9_#O38iN%tpvSXDlkq0j!;^s;#+#jfo?JlFkUwjCDQng$!5 z2vgg8o&6E$q)^27T*whkAzvuLiG+pXAF?Yiq4EUIuCzs~O4(1zBdO)WQF}FNjZL>u zh`v7eIOIV4pl|_7#=y1r-TWI~1r2_d@SEW@9W#6&aOIY*QI3=vmn&Tg)LXWE2#CPg z+M~8i!e!IMo!YFa7xLiMV=1a#8pc}wW|EFMzrzRgR?&n&^h!R~;8;HU9!qT)wY4wp zU1;CceyYfAujZI^rXiS4`s$#=jMk+XJAB>wXae3;01!08_qRVr-)ML98XHnkk;;YS z6l)cqxPO-lOAl66p04oY^09Mw=1ySBo>J9|qcI7&?K%t(mAJA``Fw-;^&2a4xm|_W ziq|vUH$^W|``8{8L>4p3z<$z1QD{F8>h|QnT=(dy+&*QR=0DvZS-IpD2{@EG3*wR} z3V<2mui0(L!iR-Xmr;}bTQL^ad+i}tszOhuO)G1yXA+k`c++HRQ20LZuH-dgToFDN z%+Xp(n`wo6hCo>1D4cci>8IL~Q|ZXrXWZy4igfgYip#x9(RjpoO_T$<1nSts(*)4O z3LMQsXYJaSY=Sq6zjq3c-}y^gpan0(d!;ql4{T62q2B3IjM=-vebeRs0#nul!>-Ti zD>6Cejy_g)mXl; zHK zeqHMQVc}$kx9z6FrNBs~O5IN(B?0i44-iDY{ArNPWXpAcGo%T{3Wn2s$;3e3Cu%bo zkrt+xv$#0ROMtYrA5QE}$wp=LMompkUB2e1J;I%UCw2 zv5?+k{PktWwP^{`^ZxIwk3q|7l+`O)IrUkw)tqKvN7^Rj&DBmK&4ik7NLre|jbnVp zF7cq=EJTTci$M4;>T;^?GOD95P1SU9ocEE($E{vIN04L6sB%KOW_sVh|MM9GFRSWc z#ntPXhz9fgYf~wJkluMj1e1d=j}9Kh^Hxu6JY_mHIEz5MnJz2y8w^GE)N={)IHb(1 zVHT-&(ZPUYrwZ+(#rjT~(L~&g52@(^alPWLa{F*1N<>V)zF`Stsj%OM0QzuEYo>+-$nwP-qul)6LcDwUH{1r>s#h9Z#vlan-aL`tN50_MN&>s|Vq7k_ieC&dY$!@jg>q77Ef_fn2Y0Ld6SZ2i17$OA zJ4T&b)30G(!zAFgMrB7Q{e5pOCiT8|W23hBxSJ`ySq)w-@|qI%Q~WnKfbnaMbSysHNVjBk!n1lUxh;8F%BWG>mFdd0=&NeNIFxq> zgK|dO3asUq)}I`w=U~M+kr=*^Kp*4j#V4Q>p7WUVs>aK^jcp11-m{FjlK7ex)fPdp z!zUmSC$}3X`1kCM867zYwy&y&;*`4iWv~k=B2}(Ya6>0k;Q^>KVcjtGi_;w$6aC&9 zcx&|%=Gvr<{BU>fL)~ys;LXf^2v3H4Ui-< zuU-WX0XOayRQ=~50M|&?s6ougt5>(a!%hAI3%os-UIrv75pN%F*E5+YX9cRNQI{Hj z3$EXG9MNKll#MYKFm&Am0zD0J@h=zZ+&yCDVIs8JzD#JtcI3c2=7hcqKROew?$TQc3oN#6PEMe+3%Y20O@mzZX?ntRqvp@Nqv`bIxFUY_nlMGj#wvH% z;@X4kD@Ez`$_OspUmPlH{Pu11eKDHRcGP=45yWhf(1Xqxo<&!o;z#J>JU0uKAvAmx zuFx+JCKOy7rU`oXlkD zQo%ZipU<@Y{mM1jo@jvwvpqQevN;*2Bac_%ckvMSXt*5`U|B0@$_oo}fg$qie#w>K zR*wq_<;}r+=eaB76Zx&uOY~kEc}2LlA}mu>jTdlt{%$rA>o_+XwNjg^y`+yk2`Wzo zoiM4t{%~&*p}@@wWD-<1aJMB)qy>C%uIUpIn1{J4iVU>c+rjIwHiqQQP7r{IklPf6 z&ji#k4_EMe7e%NN$NrM-HhDq`lxAiP4=(E#3++ zhJ*!gDL6vao@)0XS&mA?wrp4uGO~gw4aVN9OL~KMX~43smNgu-`9a{+qv(r{X$n)> z4rHV%0{-^KxQ~;OK!tm*Nf7>~cUSdovaSfB0`tZfK0da?dkcdeK=G@D}J>*1-RVJ(nauyO9*Zb8Q;UyG(5r#3VLE zJN;@a@}&>h?bsR1XIv&aMrqcSs)O2O;r--EChB`jS%x_$fpWqGE1RWec%1)?BECAj@UO{L04N8k40*AcB92wlcFuO%!k47!;kJ2 zJ;_~7+RsSvI@ZUFME)`^yLoBivSp@%SXZtWn+CPprKIKu7^7;&+y%3!0Fb#O z^Q3B!Khh`&6-On=y*b_eY(6AmW@#3I(rV#+6ei-iYi5&=J5)n&WeKi*6z$1jOo1vh z;LoSYpFC>-!~I#ZVj&u$O8`{#9D+#&sUjcZ~ae(rOx9wjF}r^A^%ysL(SJ!*Nd zcE{Qa7crttH@~)yF-O52CL))n8h9Hmwolk+Yg${XO}eZvSj=pQn~RHU+jsCazJ>d>Hw!|oz6T*? zCqDUl+7Iju>8|Oy()sn!#{%WehWm%qg| zJYWVkB!M)|(-|CofMpe%Cax#h2S1z zs&}AhLz$wk>*V?A;LI!Sfz>h0z1sx$AM#&lEtlod4?VE09fNQQll=Zr?fTp3NVQf*&#%nkfIa(k^ zkLe6}Mt&EE+p0BN312>D^q zC0urmQ%8Z{NwuG7*$-{&`>6m*fv|!d@fS(3&Q_iiJ3;xkj zY#lQs4m+A~$oe3KFnI(8IgpVttyb>jMu^pU;J#3ELPokdl?JrUbvQDWV zgGo9uW|tk=ICA4xnk#<}cnfHsWNPi_NjK->x^M^IUc7;$&3ON+KNQBIOX}};SL_cf z9PY8-Q!U!?S?c}&UVt*r5KN+oBPqDF|IW?dzYn)j;$P@2xiUHIDT!RGJBtJRl4YYJ zOujLRT@kB1R%mMrilom@X}RPAyXM<<^m~-rf4Ut|yb=NZoWJ4Pz@2lPGII~Gq>&J5 zbgub0ZH5bs$q#=_Sv3i?`dHRZ$bK4m4S4xeVBzjtqet(wWsFVdi*<!2&!TO-V|&lR*FCDzmA9KVU}y z07CJ-@VkeznaEf&5Oy_2fV^f7x)N8X{Kdj1a%r1e+U-3=s&skY5c9y58@Q$pS;8au zYWi6{^-8bIA-Vp~7k#jreE12HbG{siJ*~cO>qKg(#Tu`wGFXk`U# zvBuJtcg%J$dhE7JdyPNb3rx$Oku%d`-tM8$;OtkXPl24spQ6MjsRTapovG&*SB4SW z16}mxM97zP8IB1V^BLTM1UVQ%5~%_Xl;OJ7*3rHt!2YC!Gt|+Ne#xa ze*d(E6{+_LqbEhBjdd;_y3DVtsUPUlQQmvxC#>M9)q1f9`n%-0;uqY2;cIsrESlhp zSrp4ce(!-nRkS{dYksYc854Olpz8kHPwwg=u4ijBgsgE$II}h@Vl;|cD7^wh9vp$# z$0?JRqn7UkL&_3KSrTB0xwP)8toiL810s~a@EU4=)s~s*mSK~cS;YRJ8B+QxE++wn zZ<5^W&5(Xna{mNw!4#L|nK8U&%mV{O3?B}P8*-c+Z*2A`ZmcG=H_o~f>%7x@+7g$I zq&8@;sCNLj#c|>O$#H21|g1|ux7VjKw8)Eo4c&W+)FX`#I$t(Gp^L{FI zQ8REK^4RI18!MH2kG-^o!9+Zo_*>9n%TgxP@dG02?kBH7rh37hQZWmX9-7}zSW@&w zhAOa3w5rVW%_eK3563AJ1MCEmAc+gsoGmN(L_;&`M&C$kZ>kOhjoIE9E+C9m4yWjs z<(IXghTM6KjZOc=SF_%t|H3umOLDTUYu5McK#Ff-9@Vy}E`)W5#OLt~GgVq_91hk2 z$WppB{zEYZ;t1zyjgGV>9XnozIJ|(HsOE$E`PcVATaLZ2b?(g&6>Nsf5L-^*mEX}PH-W;QOEppFLv!=4$8x-u>&$s%c@MW<*hlz|th5ot>L zeoHRj5}c|B9GuoglQ?s1t&!nCyQg7oGx&JaYxEimoOAnWkD>N~X8V)Gr}XFXNtO1o zvFg-I`xxin;`rR=6}N@l4~BHq5%fFcamAKdeM5?lhGS9*TZEya+^Ziqc*wt5GO#c7 zx6-U8q;En!9{se-?6`4Rj=JVY;>p$kcIUzSWfB(S`JZNp~Y8tpY7tT zIi)`T@a*)J;hvv_9m=#OcmriQQ0UV|83$p>mC(GaDSFE|NYn0+XUzj^XjH+nZ^Gud zEV~*cvgbpiK_k67twb-+wa-CFXsA-q>yHz&EjG$s0)14GTP;~?Hf@pZLr=ybi}1wd zkMW>~-RtNK)t=7ukPoW|GXMJ~8K{3n%8J%!@-bP(BcHXpbh}a85xV-7z=zAVO zl){S3?bDbW3qz3{8jRe&xuwayjm-k^BaNnQY8s>QGlgHt;=M2teOpny+sXXmbV9F& zqoyB3pmi`j9J+)AlzzEiy*ZBQF13*a+~TXk6}=g3BQpSzqLN_1@RbB&_-n**Sz;fn zW_gHac3SeKXwFE@W4b1Qd<{`4a0d(R{{ZJe7{BJ)S|aKTQcUYk&F3da$2?D;hKVF3 zDz)Lr$;Cx1`EJYOC&lGgH6Hh|8o=!HOwUE*aW|oG0JsdvBe_Y+GCobn^y)G*iqKr> zOI$WpwGN`kQ+2r%o1FPE+lo5ecDwdtb|f{y)zCoP&9t0go=%2q< zy}8_FRL?9Q9PBQOouk8%t=!PDAAOkulf1-;naRu8wVuZ85%UXk<+1auo+-?3>@xNiwG1ILQB>E7sTvg`z3{`tbScW;HaEe{bx6%$^$1-L%}a0F z0`iUhHgySL)>&lIr-95BSVuPWZNwE&Q*7LX$OJK)KVYRW{CBy#=ujw3#$|~S3CD6| zF?o9&7H(FWAm%pQj#_(2XtHbh(J#jY#qmcE;Xh+*Z>+RysE4^^oxDp{o5Eb_)|ePA z$1Yx#H0IXPHUXMb3Adwir_&e7M~H3E(#9`MO#yFY-V+2jtg_i>qcPbAD0Q%Ndxjf0W3a_Bn$jGD(?5B69<|Tu$+f<(`Ja zxm2;$_*i(6-vU+q1bi!aIr!CAIE5<}4wzkAc}|cPLhCKO64qgFeRk4YT>OGmY7q(&XC^h#82w=VermY^EEnM$4hl z|GI%aruZ~XOEaYVs;V}1?cRV zKoh;&c)~5QaYrSXt4wq@5>%d;gqI2^hBoG*1V}}Oyv6%`=2)tcXoDa zJ0$|y8#JZA=A5Qk0D5btFZcN&y(%g!N9*z~R?%~==l|ACFDez;j(OBvTH0$q*@rR3 zVU2iMeN(LwE8oAb{iDX)HVKixV*@7yZMJ@Pfm3O;kQ;5Wk>xgQED@CFkA4~QPY^L3 zgLiqzQ4IU&t(62*hfQ=A@eE|gT)BwTHKi>JOkeZcY<9)QDPoVd#Z~|RL(E7q72)2? zbhDf|K}nphB%~(cvfKnPxl&$0`6n$DW{rSJQF*CaFHaeqWQn;dH71*zLu7)O7%~Si zlWWP8cbG+H%AJZt2K-9S<59xla41)7o-`UO$#=OhS{MEOHf+4$rRb8Fp?ef}M^n&_ zOhY;Yv-Ckc>h;E_gD^!yhi04$<$+)dPxA?t8Jf}jcnN{OnpJFhvzVqnmTpX1x|Y1P z--4K41m%3Wp)uX=)6>)48UG)DD%JnXIcmqY zH*DmXxTE$zdiMQ}INVvz;dyr>rVzVEo|Aoj2#%1MKqfvHJ2%$Ekv=q;iM2~BY)gs# zM}S9d9lLYk`U*T&LC6bn&gylhBp;jwBsW*;Ws!azeUV^qF5|KVN%!)Sdx+~ z2xyj*f}Zqdg+2*lmM81)YsAdbGy0xO>=rpCK>4{lE*ItUG~4mYEauwUE2q~N3`S9B zj;11WPHpUgM+-mASp$*QN#i70%n?3weuqN993<=vnqhfp;6Xf|BJCI#U_^u-k4L5# zre~dTQkRgKQDixzFx(9gF6mDa>CV8&JwG(JT_{Y8jQ1_=0<)uUUD}d&IF{>MIQl|9 z0_Hrza=N&Tb$WSuni*QJeZ0K6`E;|JnZYg-(v}COBsYne(J3Dfz>kD1)^M0*CFUdr zEk)J5r!jlP`~op6pe9|FSOsZJBBtxEh^`gI>|i$*7%8ZhJA}rRm2CgF~|Q(QMo|ovz_(Qda1;t;@S2ZkHqB@otfuZ3`P<%K}~ZqNUYc7ud;8NG~8gf zb-26jZWd@!r!_V&3SvmKLLThTaUcQ`zucSpQTc>5Y9rd!0!zseIa z(`iA;0T9weiX#Y*8|ZX&arMW=ZLexH;y=o47kN z9dTxm#Uy4fi$S2MFX+t#-LMj;193NS4}DhRM`E!`KOgyfTROLIv@hbYn6>h@E+Yix z^ZallEQiDdF(akyDu1+`K~Qd)$pD$Vh{_kz^Iq&~8rCc!YgleD@&@D)BT5Pd(oZ0 zY!WIrH>J=N`!2gr>?~HV)GH(@1waAJmCBQILvwO1Nc^QM(pK$v`I#p~Oa^8wi?CY| zGnuRamuC!2BIFgWiwnNx`_EUO1u@wrJqcifnE9pR(yKcz;^o8yc410n@){Dv=ZMf% zRe(@!TZ!4#kg1rvi@2HwMMdvbzomGDx=)KsOt*>o3L2B*X931?fmJ3nC27sV3Q?19 zl8HiLZDC=dz>@ks9#uZ|^x_xKH>mxHayjC0CgwDu0vyAnOq5jS+|bm!`Re9p1tB+sy97); zb$0fiX4Re5th{~QV(t%a+k@Ga(Ui6EwgF0{3WigVm^w4jm3eu2dGI{Bak2W%>E}xn zd+51rIJXNAxS7mCp^T-V8SltUC}iX2BE7vx?&I+GG-i*O-NXcB7@GA;rBKN}abiKP zYDY|tsTKju{3MZR#!7)L&MU&2w}L%!ARf_f1?40`%pca0i6n{4vongp&lr(c zA3j}uLST-2`tXX7d2JArNJ*d~W+s}6=1U^x!*OWLZ}Gi}wReitn3#HWsB+7)t}W>+ z!erOTZ0bAM(YxLce+vZW`1x6|C#l6X`Rl#kZfne9aq?9@PheaToGci~Tqze=aTdxj zE>9*pAwJfSHjt8OOh`vA0KpT;E85fpz7$t}qHcjy_tgK` zJHMB<@-&Qt^)BQGfsmWs3#rslNrj$JQZ?yd+*8=B1QSrgoJ^!GLRl7+T~BPt6hg3w zVVQwMhIVX76e~1lnguBc-eoRoom_Y^cYC*ZzkkBM&-;E)a-!~kkdqkW(OM(z=RD8* zb3w0;bd(B}oakTBuHJ=3BLA57@wy{lZ%d?d<@B5K zOBt%Qudnt1lulQkP;!@-AIo;5tv)?*fKXu*{E$K$b4>yQOid6((kBo7DA7^hSO~wx>HFGC7nf$jtt;RQi1?y0#WxhcJ9D$edeQ z)W_OKuU@t1wKiQl3J+#SM!a5*GjeLC*Ye|bIkmAV5R>yhLe254j_d}1x!-Ip% zi|*>{&!>A#OuEGsGE=F&cYL`ikA>IQ!_naGM6U?556mGkhs3<0F+mFPxW!t$h3qbx z9wj9XhwV}`cr2Yc$yVIwht2fJG7BLl2QvL$p5Af2y&T+<#v;=~rdc#i(?-l3pJQNV zO@`(EetrK_eY;WzF}Ia5|H}8i%*?MQkS~A9138GAi_sJhOjo{~m^m}27aly^Y#(jT z&+nGY%VS!M*ysi?o!pCaM(vh* zn3$;A#1ZD{qw5*E8s)Nuf+_iOGSzI%!N0bA$0H znvxiK&ctkrE2k6x?OvQ8*Er)OuG}z;Y|BViw*n4w<&OhunGwA}CML%*T^@&!(kFeQ zYIu`uKOiJe~#Fo^whbv3ld zoknowP?_QI8XDWzjfP3r-tycY5p(nCBsam*Qj2Pq$Q%-LNX#3#a%fCamr$9cF?A+p zB*2ghNVL*wrB;sIJIPuJ6*oL{cv2Oq`_(uOpwr{B4TzI6_K3sd{tFTFEsi%*V$|5i z1UDQCnsJrjiHpEQMkj}Eo*Ia0P1M{u+(@<(0Axo`TpYudU;diANGN0$5XK}|P8xH+ zvTc$r2QhanDlE{CBFGep*<_t* zGC1+PNIxmt^ZF<5`C(%2*C8=kWnyU>NlYa&`CUHG#N_->(JbjG`Uwj%l`o%}39lI2 z+s4u6X1l$qk-~(;bn+-=0p=)9@X`t)Ce$P8OB4{r`t3NeSagE3j)(TSsvA9tnro{$;TqC<^2B<8KeY^ux;s2Pv1V0MJ>d!N_u^;>($ z2mklOX<}+oz~4Kqv=xg?&;ad~H%7iW9DetMXFr0NCq&HspZ6IQ^SGe+)<;ty6cW6w)V@A((y2mvx$~-1wlI+x%^+JgU5lca4eY?cOyc9s50g$-& zT)CKX(wLB#9L6k{^>T}VN!GmdATa&-@wDR`$8;d@NLFTom_k60Pp06>*gHqS-{0(R zFD%l;nyXxp+bt`Sys>r*bBlzRoz5R`YRrX#wacf;z$A%D4@zm8W{vo1B3F*=Pr>B6 zqB=L0;|Z%wv-oa7WM-ad$rImB^W6C(!!i>u$kM+^&j>S_n8^Mh-K`sD4)bl5Wg78Ig0koR9H67 ztr?Y4mb}CYx%AX{=cv5g)0o<@9&<>{+ljdmPm01sPP&-{FH33az$!*Mx z*H~kc$W-}s+1*>@kR=gQCt^w*Q#9tXUN%e_;7;uL^l86tf|xs(0!+j)C6IYtV_wj8 z1H2^Ny4-=tGz{|QPaF}Fm<}Hsa-YiactkXEX^|Qi%IndrhVp(#xq1&?av;;g+b;OK zXtAT#`z~sWL}uQWE9d&6f&o|pk@RTty~zfhpMjXjUlfcwx0T~MK@X@e+TMOkdi33k zyv)YrH9$Br1k5~wnWQx{+%oxMB^-@fzFais%*;xeG`4ZH+dkUWxTD^~J(8mWOe{nj zVPa}yONda0k?;v>v5{+#8;J#%(mc}$jvNYeX^eQu{+^+$8p%at#+;%sM+W;NuaDs9 z$x9XTj73*6A~ExH<3Q#L-ExPAha@t)du!n!{b<%Qw%D2?=1RR{Vm*-#mmb{RY?sGV z;3V5}ZKyGa#JruDT!{Ha$}*GBgCOR#f6_~$i7P9wd&^!D93qoWOnSNGX0ktx1N}e# z8-3QOt9kT?AK#puRI5N{1@-IkYPG6FW>%1ytkF#p=-A3z6ODP?OO|tRGY(=VGIY-r zdbx7t%1L5Y4i1=@6+&i3h`FN@KUd61n=4luvwLw2f!XQko#l>B#Kb~yW9vyIGCduM zd=ro<^K{JiH$@UbBppPT4u8Pmvo(_T)VMsjq`b)ML5q;Ao77_E7DT2p<+sH#7fEGq zF)^Vh=`qQm>#E) zFg@w_65%-gtM=5WV5Y6d+~?~#Pn8?~AA9HbnpU2MaWLMwIUWSpo4pAMiQ8bq<_sZd zez0+|Fm8|-*iblfB!+=9HyxO0S56B-QY43AFJdQjx?>|lhNbFsOGBWLJA2WfUIgZ{ zw{uz8KVjeJeZR+J8s|UooD-8++Imv@@p*qeuV?udjkvQfGJ39a=|@}n`o;5FrBbQ^ znsTJ8H9pW6nmtfiP2#4^{4`x+mg?z2`5YDWy-fs_sa&R8$0CgDOCn~6p2!n1k9c#> z(dijexnesPL`+v?Dv3F)F@NjzG4|7H6EST0 zz-9~5n)mM*8t*y6)^q7BQkF&DH2G(v<6htTFf#pvu$*4QMus&eOK4Rrr}N>3)g zFt>tox@>=^7xVDpzrQGalSow>l`36k3_C8@7XCXm<;u%#RW3Ki#2gdzGh&MOPfv~h z5HWRVObW9D@v!az(_M==)XOemxm1Ht{h3#jl8{j1-R=#TLt~N%)4zOrUU_4^DOIur z$!fzr(lvE*^(C7v(nGqLLx+KWokktxu;rl|aZEy&DVq}jZEUujGtrq%fAr(alP z9yO|5`LPr+c`#Ct`B9L`-(3BV{#W(;?RKAlX*)zrgyjVG%HbjrlcMrSfU7=I6jw;Z ziZcOb7#q{*?zEd`^=`RL8jHAI9-7qM#l^-NZtSG(_G6Bi`8oa1YYPoVmej|KMTy9p z9F_|_r76?gv+XUD)0g7KfzNbdrXJ(tgiP*Kd?%HWcjb4YG5O@D2$@?O%V6e4A#pFC zFF<6@B{oQATF#@`vlaHKV__+eJfJfPjhS4FEziv$BuCf`=~{ilz;UcX?^BHPKt zG-p1QYNyY{#_jm1 ziAikRKG=sVk|HLz6OpjwsUM60vBadK(<^Kv0e9+VFZ0Picz0YoX~ z$#>(RUr7>K5u48#zDX?g3Lny z?Z=NF`)zBdNJ5u)soX-!)Wk5fhV3VQ*C;A9oT-1CT0;D)8vk#rh+EU zGzrWi3g>FJ9I^XWg-tk)MPhRG2TOK|n5WeTn;EVk^YJr)>Gfyk6k{@bdzk`4@dO{s zbEE!ez zPf03bj!t3z-in_Ak=O2L+zc)2OwpL-T&8t>ef{avzG?VSiFn3$u8`S$%=Ats#I+eAz)9-j|C9+sFP?PiCO%3Ov~ zEavV^C`^vZS%*b1G3i;cyTpu!f{Wijeg2#kWwlW&3Omnh=jUk!(QJ*NsY*dv{@h}F z3_AH4y3q1|V99t7NlLrU)u42VJJ|C%LZ(H4KMWgaVH!XZ;Sj4&@*A!1sa zDVRnUSYrN+@bxQ^n9`q_Q`yW1d$|o3nJAVoyFHqD+cXyp8TJW^qp-?3BBjY$N{VEY zs)(){er_W(8Kba0MiMjlH|;O1F{hP{@U#-Hu9@5Invbd^-PncZ)yw_IITx9#i+!BO924_% zVzy3T#7o3{fBAmDo-V~mDaE7n@yAi!tM!OR(zM~I6T}RM27QM5!166j=yaQHh88+R zq5(a)@->EjgqhFJt4eQzn(l#gCXum{6`@(mR?vUM&tz+*VyTz~CzzNyQj|ILtE1lE zmYJV?KHq6)*{p*evLI&1K4=m>FE5#w5{X|iFnd>5UG~Q$jcIoXTqyqFckJYMoI}(6 z=iS*jO67s@YCOE;CNafn1IFPbGUL&JN@YfrBgycZ3iSh1gOU`H2YGQZvKfA}*v5Oo zE8DS06EkJKKE~jWL&S93ia<-gotrdeZhSJBXBa^LMX%-fan;RQbv>|2yV#iyJhWR{g^{vwyk<*vZXUayz% zPcGfPyS~EB?(|{{nF75>cH8Ene_~>qZ-R)~UYMD~_DN&gZ7&*Y%rP-<5%c7v^|p1= zYO!k?+}^ZEC>{+zj)z@QsfE0CV4A0$#5~v~Cd@f>uZdkIC2tT@j|L)|@#XFdW+q=e z|5GK=iSwL38uDe8Sn8F9s&vw~=lG<5(y^bP>eN5i@fsyUjJLA9&Clx)ZgZEZmQp zh~90x&r_27mrVeYRc4QX+56D@Q2(`i0cTA5&yztgP7<@#MZtV0zp${gL$Ue#Y#dG; zt5G86pz1@XrW%H#0p_JUAgZiEZyth6_G%Bq?1)u&&`6pU_|lcEHk?1&<_Kci$5n31 zWQ_@23M?D+k#2HW-sEDrNe6M2h$-swSSmor)ncVsJq0x@Tj(7pW)^rdrku$P#wW|N z%OdAyGxy--!&yvRa}t=_mBSSXOO}{E*JFsKAv>r|Cx;8#e#OaYFC_O*hE^8oXQcs{ zT=mgp!LD?$2L~I_NAhxbLh`d+|wd{S=5?_2urv|;T$ix~=;_{2hUOw2JcKO^RkC%hr2{(k@cWj%d#n2baS6Ofp& z=+V8QAJLeao7Ch;JEGL5>tQ6u6uN}vx;G~oaub<<-Tl`e;U|2otc7>sExh|;_&DBu zdc1o~*Y4BPr?}+67Y(=_oi-Xxw}U-Jv_Vk_(~X;(cy1@vaoQBT!&W4H0E?r(eR0ri zeyG=5-HX<*AJ{{fklAYyF8fcqPx=JRPAAi8|Ja4t>pQ7@VQPw)xxT)>`hWJ$x23H# z4dZa??28TgQ%sAe%vII7XnOrc3#B5k*!z`3cxN)$?L=A}b<=gWA%2sLx%v{C5HEGPA zlSE@C2$O-#%s^p2ri~7S%*k6LE2i^x)XQz_u1`8?#Ze|^{DSl{lJPRCJ?ch;y~E*| zSx?mC(IGJjNY0S0&5WF*s z`^7xjwiOB|2la!NTuKm%sAxO4>eqMqjF!@=8ZpsD%Y{K*EbxG+P2j&>vq!=zoU7s z#;jNtMm9MrcLXvCmjq0Dw?N258q=ZyCGmoo2S>`mNZ^tW$K^s}jwe#7rKQCDe3rs; z6^}z|CVNz70x}aC^D&4SqR4zCVL7HirgY(8S2h?94Y{&X_F>AH{DO8Nu0nEO_|D?u zjK?GXVro)jauO2_nHSNNshwBz>09jM{up-!kc!enDIzA%HYts{N$@=R<(E&LbT(5m zE2bR<bq(rE}1#dm;+*-OUzeo-uF$tMIZNn z{=vB#_e@VephPbMpaL0KTtJ!%cF9I&HArtS;=<%{2a`J1h+ z?d|QY-m^{jZ0|_7;hjr+mv(5=__H;-2wtwq3-$vY96BVc8W3qPY1@YtFpAs-zWbOa zFU4IQinaw-(Vw2A*t>utPo-0#cFuM(*-oTs)TcoG>$Iwpkq-H=V z?chu}7(Qk7M#S{FdfcZxIArMzumo_2W*w())h)+vT4uf8Y@Sp3shAesCYhL&!z@dI zc_^f24!uQoF^{mkVU|jz+CPpk^dnm`YvuBRZRKpMoJ)x_zM3*it7LzE6DdE`l`%32 zm8mKwv;~M{<~pfMl9(Z($wpR6_KPtrt+=ScQg{^8jb4?$yx+2IQ2*^s~_$y4Q!aq4R6yLuWRV_vA9;fM+CynNQV}PtQce=?~sN{^`?y zKBcphH%#&J%o=g>S~A|5;vdO|78lP3rE|le4uGibRH{`%ro^QbnFC@Dh}lQXS5jl{ z?~AB>f8nqakIe{)$sW1ax*Hd|$mIY3nV7n->&G#CT@mv-!gAdgkQ*mSjDncM!%x2a z)vrNEW+pf(uB1En)CwR3>&h68oR08@%qQPL2~FxS&)ds+E)Y>dkTy*;8g-fxYVawv z$=|45fYNLu$w{A6K|Mi5@pc6kL2b!doVp2fOhL2ei}7gK17c2dV}8#rA3WvYRFLN< zd7l21A!y&Sa?n#JXH5d8p_N)r-pbpK0kW+&Zk zGfEa7nM?Cr_+gbPWaf=jZg<=mLL25N6ebLZ`fJ)k zCRKj?(sQ#{Jbnh4y-aqyG$|^NBuHC6Py_T}=4SH8&Bu?k53-ZDzZtVl0w(gX8*dub zP_;V8>4f<0kxWPTj z3pRRp=YmvDs@KTJ)pA`|MIq&2q(o*Q=+RYFF1|4)z8V#Qy7$hd-)=!nVt++9d#M0P z$;ukngvjMcotk^$Q+A8%n6JcCIx!QgD~=hS6sCiXBCy58c!nzJ3I3ps0YpUY49Z4b zXui5d7`9abe>O_PJ~QF4$4ytdAvk3+gEQP85SG+*bDaTf-ZkJ`4dll>;9faKJ9x?3?xEIqQ!)VxHdECk&x<;tZ9&quNsJ9iiLwV9u8 zUPo^L{#<&dgv8X?c4JiT_Ca8d_U|52*o2C_vSXL(J^9X2)#udl8eiLm;++= zcNa--ppXB?mfPpf)ySQv`ZcU7*8|Gg(Ty_QSxLFzMh2*gnBU3C7>La7J~bat{ph;i z^X=Bo-ri1k2d_E=NGg|8qfx8XZ8vgu>AKr$U2fGb*S=LKMLpg99_UTk#rcsUba`Hp zfP@1Oq-48YaaE-W4@}EyTJ?N04^oOtQ$Aq1d?@HV8>7Z`k8I4Bfq9Ren;xdjC3&!u z7hDwFRNE!HKFs_AQnfr7#3D7{aNKYXNnSd(dIo2SSphKxFm2h4xgc@*R<*I`mW!|x zwuB(2WTt%}fr<5KY%7d{dD$wZFxM@DQ_H%)l!4p&Yc#XJ=P7fxu}~+uBpy^TQ??rY zF_o*Ce~5EAhr2SpUhS+I8;9=DnLyN*-I~bxHB&~k;#$ysC=ByNX#CCjvgSp zqLYb98dGiMkQ(zf0dswQG_FNT=7e`HM$3ceV)y1m)II0bLX|=?O@m^$;v3(;scsmt z+3LM}sFjPOoH7T*91ydQn8(nV6qL)+^6}`Mu+DwxU9UHvPkpF3z~e8vyVU}wZ%Sjr z{LQ0F>qxl;CyLbO6DcOCorpxF(>go_NXHS%OOOPhYzbC=*d>iwtNrQO!!yCd6U!Dqr73Pc2$5$Kap?g**ov2} zL-E_~j>V{bXu4lOV$Tm<-R^*(jLpGICBO zOxqOln1RVzOmVwV7Ps^%dW(wUHz$#a;t%x~!Q+k>N**2Lat8-Fi~872CT6LW5?a)@ zQpC*po9x2-KCR}l<;kEo(<%y`$$dtAJCd@+OCpg8A4~5UjY%^<2+75pS3F<8H=uLZ z`w*6@jmq7T(sJD|Cw?^Td;<=}M0X;n>%JkYK{!TYiR=HYF_YrSB!uR?`TD2z_1}+< zim*KtiAN@)KCgF2(YFRUF72*DNz7~_}W zDcm}RNnlIx>H||tGO`7`c3N^lpuBWNR*U|=wX;^EZ=kO{dnkiT=DlOx}{|@>d|Gw8ragTQzzFCTixG#3x4! zpD|R~J9OCW&RJ-Yus$a$UnvM;CQPdOAcu)-nd?7gIa^-jo#k9DFO@+|vIWJdvBbm~9yW3Qa*Q zjZD$Pj@YP8IvR*zl%T>`A=zt@NTqBX^rCSi>41cUU{E7T1KBAyv5*~hYi&UWa&0eW zTD_ag`5tpS&*5F~irSe^kT>UQY_a(B|D66$!t=|p=*Cpu?)R-hOg{kmS zSQ#+^F?ES4Q5WhPMZbD^$*PZU}TelA3yxn)MRi3TKu}0*q#@DjzS-6ULy@cn*%L{$xhhC?wB|a$%Exs$?^L!hiNkTO+zl5 z=vmdK%EcB*k;KH+ySU4kB&PRvnuuA4yE8H&sox;x0ueKxSCYvbmwJeq^;{jx<)|^S z9JI)(%qX>Frjn-una9V4CK}Ku70P46BUXM^9sx2lC^D0vIx#k(R!on}Lo%PK z%&$!A6F|%9ttn0TNN{CUn_{B9Z?Q;>3qcXpw*;gd{%a|vdVqmg8TS$thduCX zAfYBgYQX0KTt+U>W9R2;XdtkJ%|!vqW-fI!W`~$JiMgc{6L)rp)64La{nMXC7mdA5 zWQwHbLkq8-B|5l7z|I|+H21=zj0#=O0Lcpojhh#sa0xX1(Ej7Y<$+`@K4Q6nMCOA(l_u@1HawsLcie&TsJvn zlT-I%=dPB?5KpaE3$_oq@Rx`sXed=dTxwHI_z(Gx##HlZg@~zFd~(!Xlw*x48jPq| zjuJDKQGk`$ka^0~gilN>h(}6fA~FA>E@z_fXCOP15UH!!o!LNYUKg1mEuRU@OogUB z8Z%dvc;T+!N-G?VhC!Mz)Y*1hj`-My{P4eTHA~tV2(Q9g6K+Pck&!uQZIhVV`{ir7 z!(88FF8w!*LG4*k1!6{?&-1i=L~6`PB<2gGPJa9CESpZLVlO#+s}B$R0s&8AK+OY5 zeTmrLzL6cvc>>dyK+284#s=Ox#Ox6Be``!^20sc%2cEbf*5uu>7WuR974fE4`@y6> zS9P=$C;J%*k?DGf-WwLJvQAnP6PY&iT%k|&tz+jU-KdwHD(Fsrw%-HMcdxs*r>pM{ zoNiQ?;@^CDtNTtL(v+x*S89M4<@ahs{9v5rJrv=fJnXBc9 zm^H*>CbRMgE(<`+Xc~!`PN!z3XJ~SgER7nYGCJqF`KZWeD$&q@D4F{nMJt8M+!HH8 z&v?xPSiG{@ZGZBcP+$&n3zD$0WSI%19Mq%q!M4C&i@w`@+|f_2-PTPa(@e-TvPotz zI5Ypet+|LRKeU%UmPqZ^KcCgZiW2hqHaE$`ja)F1p8?-5uSb4_2s{ysU0z;%yF5?e zCvDOShGg2wcL+iEOVzIc3sD$frwOFjSH#=1e zalhUF>ecx8I3_9CYh4UKQkDT3I1aF+bx2&{lI1e_+l8!$t>j&V2E^R_?ln9pm9VzI zD5^h;;hWt>BO_`^N#{ds!|`6W7V#sluz-*}xwFMtOzM@&XKKykI7H;2FXLJAeFH8g zad~+HS zM&`A2X5p#n!IP+?m7A8nnYvNZlCNHxvn>F(f zxqVlBRiQDLrk`)Owzt1-^Q}dm^DTIcH6pw3iq+eL5}Pm`6CAlqyQ3dX5XHNG?sYqzZblwL&wap*q{9P1aPHmf?d`?8 z1M%Vgp5}g&KYkz-Y1N6R;3URz0ltLBaEk!Vio~Vf_zuiuMTfYo!(|tQCNmS~*kuPj zT?>^54*x5^pK_HN7O4}J(23s{*SLunbucdJyC%KynlRD)%g3nk00N@eS4Z*O{tS_HUswu?gua+M`8v%>ZjMQyjUjaeEPV?8ByptZ4*&O;HFyBu18E)$*h zhZ;C|X}4oys9j=`eGwpIZp|5Y5;HLe8HeZYd;R@Aq$~UT_YZpM;&zKq(BaDC=WdMM zolcqxiet%VeO+%dN`J;y^v_#dLds_K2%S3P>-Y!?w1&WZx%;yI@+AUv7Xxz|VLz74 zj90U>{&8GdF(J16*mU!GUPx}Y@yI#1wd2w>>5Qil(<_a7>{vG9B^KUbUOB-jPvwN5 zi7ogyiMhx6Q&sAU!ifdaYQy}vXVw>7v zaA^{WdHtY7Z2apT0JGUV-dSB;p4{|#KmC5<;Y21N({um+z;G~V4h92}*yY8=hjZWb zD837YW=CUoi1}S&(i$Bs`k=}A(PCH`8`Ijbc@LsR`bk{LWopE11E!YA{K@5dNW>hP zu($7{ec~{U0Wtk|8I!%e07NX;z*9sfhT)(qiIh&HB1*_AUFZeZcHpdK*9eo(DwVqx z_%SV)e@>U_g8f6Ew?1PdI<+5B6==P!!h~|NimStRft6prd_jGOsQm)P<~)pAPY+U+KGfs@^uawgI)X6hN3=wOaE9S%-(1c$M<4ZQqt z0(fWZq7ZNB>eIpm&i;cB|@{IAyY2`g(9mvwPFo4>5Ru0*zko`S6{qXou6M#=4$YLbFMvm zwjM7OYPoW9FY~}=#&WnuN+V}6Ne&LO$TS;yGW~i)e%o*jnB1?)D$^jDsmV+;&G5iA z81#*93yIkdOca^_+5LF9UoF-^hO3?pAm-wty2LAdP%qhV{^C~g7c=Ol?=H*c`oF`(o4MevG<+{Oy-PT4-iA))mv+Q!1Y}y_NrVS2C z+W*Vm*}b%tr*RzGUajP%5tA-dAt;S71X!q%bN$dvRL!WKKw=B`ktv*(fDj zkdP1>)C`G*G_#uIq9dhFEl98Cu6r?1`yb5R{0Va>-!G&903ZNKL_t)sxBGmb=XcIY z)cFr`a&k-*Ut;OU@ALM30{@_>oFZCj4{zd{Gd7*Ci$j~I$x<;fA)z@Dz_lesZ{|bqiBr^H$P4w1O zvG|S$bx07?_g}s?Dz}JPHCaxIq}*NSlb%|`k-1aD*eFz?F$-GprHqefe70 zuEwM3L^AOrnUJ-6bPR|&Am(L_$$NCriOCYPp?^Cx@_oQ3<0?)sruz~%*@wqzo{<1u z)}t{mzX^yXrWzR(A0v9loY(95C)z9b_t}v{O5-?jQ^*|k6Ey{#Ev6=dcLL_-Cgbw4 zH7@y>@8?X<<|YDk;$oZ4G+A11_LR!40G%D<>Ya|Y6XR%`MP~WRk;!~Fzc?#Vx%?5} zVmQKZ;BUlB!qF@8(;;ThGGZ%>X^C@LC5JTTt!jgShgo>@JzX1rPRxdps3r;!le@dy z7Brccau~$R4HlSiYfnTPqAgDjYN(fI+TCuu$+)aEQTx#nny!_Im?ntHmGsjr*F1jv z6|QI^G`W1vdCk<@-ZD`3;VQAQ+$1Ggsr;V90_0VoGe_rczVb)AooQ;`IFs$RqT)^2 z`y=PLRDWES;Oyy3y6Kn_sbL;zF2V-{ja`b?yiP&7#cKm+7$jr^nGVXMRJWmq+i2wf~2)X?qvhod#SO z;oprHb|g&9c-%PWZJOIm&2?S{cZHaGT8Np@1(~K$lX)q_%%V}OoEM896I}DS_~n8p22)w3G`R|tO}!<- zIn8bz$<*}H^hUnR3DR!2`v-EEh|WLZ_9>S$b7oPC+QD~!>HE56X||Z`r=^kZL5lFDm!+4gUx+d*AP~ zktyY5;>dhJZShtc(uO9z0r%A7eIm_D$ZjGh+ZxFzy&AN)M$g?+_3@~IA7#Go8~4O_8XfE{+TON* zhU)xuh2A(A;h*}rq=n1~6D$!D2vL`-BZOWG3+ z#9Y%#=AKE!Y!-`+LN-ajy!h=)>g!ihn_q~G*(`?6MQWz9vg!gIn$y!s5~kAV+5J$f z`OEFy-2VueZ{EDgy&-0rk+`)5$km5Nr}g~SI4yUVI~yPg9{7MDD>q5BQtuhr0<*^KDgnEPDIi| zS$kG=K+FL#uMpFU%dr}K-Jqa6@C|Cg(Z5I4vTdIuFMGmrw6OO^?Y>@-$<9U~=7^Mz zB`Q~mOk}-vR2yH^HQWM)LUCwuhu{+2DQ*FZyM>^|-CK&gLxMZSwYU>J6t@;B?ohlG z>6_p4z0X?jTHl)gX3Zq`&biq+ch1>+Z>;xYGXI$OO^iY`04+S?I%9ARtc&Dod9#n` zS0O@b=?B*y@uMAIitFk02vfNws7|Zt#r7xcLUvlH7wqR*w>kK@#rAzeCwyGJOblVU z@alvk8kq&kIt25N86j4iK?VtS_AW^v3%JUS5QYa2A7sDMWE9)BrFzD^oM?Q&>FbgK z#Yd4FN=> zFY8Dx{hn`cTGwVZsB;%2WOiiavzXT~z4K&0{xz53%9oorx2~2vb8E)pRwGfa$GDC^ zW&sA{GyuK|wc{GF5YgZbAK3iW{lU}LA+p&4YUap0=wGSeTt}5*)v~(o$@MI6Hl`Q?eilb!MwYs=P`<=> zaY4=1McL@NC%>$#a;2HYCvPbxdQAf9oM5?ypRcbf43#jv zNMlo`-Nck5U$fl9DlgyRq>8=qgu=Enig0-5@WV092+;v-#A*3CkFV16xons|c3npD+fXMA%BjSR3nVqg z)PjmNUC!QKO(>EOCzgYajSGKo!fGQ2lC|epezR?tmX^|FX26?F5kK;a>IYuY{B>Id zPo++0ydIa@W#Q4_xW?EK@^1yLx|@hOMziXUujPyK<&txPjZ{5qdmFOC(wBLYu*xhuUl$7P{}J;b$$qSt=0!yDXm-<^yLU`ld3` zd0p&aS{wy>z{TgO46GjGm<*77Fho_2lu9Ht!SqGyUFE{D`cUMPFu97UQKow7><9Fr z2?Ho@Ngl-=3CTWlY0v3f48#~68QtA-OaR>Ni}s{Ek5~3~G;JcoROty*VIaSMTlX_= zzi=bnPlvtHSUp^CV z5T1gN-d#cKJ&DW|i|?4_chY`f119vae@1Ods~uaQij69FFSetB2ks72&??B{BT{ce zmNqBTpxWRBd?NkTKJzmi(FSuJ3_1~9mLK1P21R<43GrJ=;;}z8W?jaL;=LOIa%9%- z{8qM-TA?v})Fm#-_d4gJ(tJ-nj$o9&aAl#yV`A=92ab{WAJH5T{P6c&+1Lj0YQIQT zmGr|M#VO`AjAowt$-P`Y`0Kn?N??x&^T&B;dwBb`$&XX@at0lSOsNZ+Zb%&KJ6t~F zbeQ2P33J(4w||0|E}fP}gPwlzH|e)aAFsI#e$9~~X{CSz=?Qx90H(dsO6A1!O~-*b zhFuYSo)>8WYt?hu;}=PFV!PHPo0Qqyi$bv+JKyUp_#(&PUr6TSgW_q5TQDl&)lzC| zDx_Yo>S=Mtqhq1gxa=K!OX9=iTm1Zs)1lpwuFy+?9JK-f{nf(ui17n9qLnhoCF9L1 zM;aB*#JPdIGdm(6A&8i|{d-OUtQ4Ig(+V-`&AmlHznYgl`3nony(8|YPXyk!GMWt? zDYjid%Zk;W`?rKeXBGPS&HU*4~A;Gf~Qb^qFVZNgoX@&pPqx<2kP-qu98Sh zm4aiqUtclwkJWYsEI++y78fhtqz0K|@wkUm)&@s@{ELzR3;immv>3lBtzD=Q?}?%> zukbk)Q`}Kg4xxQ2pZq(4Cv_bV>6dfkQhcc}gF8=XM@F4}-pb)CV zPJ6Zg_d?U-gTzLaBQ1}{d#lJ@_L#H|17*|KeFqdR!n8*Uyh9gSh^1)U@ujhv zz}#DtRkYl18D)I066>A+TTuJdy?wcL^huszK^fE=pr9LgCM6L-2LkR&E6rjlcUijN zgeX!#tLe##2LS5ysrl$7jP}uO;`AoBct#w6_U~T=sdQz3m|Nfyz*Yn=OV~v!p!@C0 zjC&^VT!VsCr78I~jJabX)!;6yJed};NdG(G965ZJ(O7<(6(=$E!a!7{fO6%&kjOv;MGiWQ!(^&Ic{^jtE= zVJLh}jdUl>Zu;kJ00CfhJ6;#W!pzC37Om08n%8t=26{_PgI=bTqndtru?i>~F=XE- z_ZOs4Z%pritdl+r{El-Kr<{afr9gAl-A56h^|7SIt-{8Z5%I*wM;@DONB!0;QNV7X*v z^)xx<$f>MJx%4togU!yet1~3%El=Db!b*P(h6O4ig{9^|^*ZZDX$yL%Ec)1hk zxbBZXx(hi_HCq+R{dI|E*2v@d&~9?JpcB-{RW^OHjmTNVB~Q zz(rX*rQ5-a#^spqNQgKZ5h)A#3y4$s`!?Ujac7l15=|ujaHQVkZCQ%vao*6$0bX%I znr~W`+o&`>9i9+1<#f;q@fH&3jTH@ExwxIa9v``54f9bRqR3{@aC373NHkKIe=@Uw<=M!nS9#7T1L*7LM~v;> z>CtOnTPDnpF#%1#g8(nJclZFR2!ixO0dLkU*ziqx2Aj~yD&Z^Kkw|9Vz6!bf{bh{ z75tZ1B4<`5O~BGURRy~;#Ec>eR3W_C{u=a0Kc?`Wc{x6MQogdGiEJ^d_G5`(bRng^yAc3~d_~t)|mX33VY*fpK*Df+y-W8S? z<#KKOJUq4*M!UlIF(fD$I(LQqdyzho{AYveCvA(7ACGY+Og=*SIqYi$Vs6%+Yjqu4 zzV`+`2u?4093T76glL*cIG*9C43R#P)J>NZ(+{r73y+B z%tL>}wM502oO+ul@)#uyXpN+&B%Jf;6g>0enTO&nw+m;-W3;tNL?AmbP43McT0aSe ziavjC&*@qBo5zHOluW0Osg%!>vA1aa2kw8;7+*Bmk|O)kbGw1Mw*NYoW_r)6pTEZN zIt!k3zfG^&8Q$EJ+mm(R9+$W2uG|m`Kpc;EVwg7Y4D5lA`!wO-x9R!r>tQZG-;fd{ z===p->$2jk>=ng}_kjukhpIcJETEque?+=!nSavl-h*BYt=cd&|7HX-zsnl~YBcAy z-YL)0W4&OytskuUS=UCyM;SBsEdez}bW&WJQL3tQ`btghUfaJs_Z$;EP$wx>(a93g zp|!{JGMhY5hQoBd>b%7eLWs&aV@NzjytwYVh5=_1g-90vSXx=J%*T>qGND`HD#RHf z-y#&SA&>+7x^7;1xZP;gR~0Z(=}eeMD}aO^P~WA#ZMt=K?g{R`@37<}Do~Cd$h=4= zyl^MlzF5$wP_0r;Bnh24`IkM^wcB}?(x*dpJ|zR`7&eL#Z)KB?LfBd4r01ldIT`Ma zEZRQ=ISL+ylJqB%%g0AkDU7M~oylkY2QcSEP0UH{iE#cHIZ*F1ebqRB=~JzXl)D`$ zyQo9Lh>Ho}Ws9QH)$%5*a;jr9R&pcJ)H#_^J(&K{a^Wvu57|?~ z$K_{7kpL=bL(MdmEn(Oshci-RHwEY(X2-c_<+-aDitwdI2fWESZFy^?G&LQV*!)xi z)HvzU(V5MativMY>LitAzV0PhHLq!(PK&u{9x zhqkr06~`7$TY3SC27}P36)Dl9{4?{w`>BP36#0amYm;|Gp#ns{vxjzoscdr-$7OhR z!L$jk4X-NG02Az0w-HKsJNAYD2O7gccHT6i&e-RXb+Yc}23t2uN5kcav9J()bJ?LV zPVz-oStR`@D`{Xd-M!pZ9$Y7VXI;p?iZOZJ$j!tbreZuK_j*?$HB!L$ty+4}yR67p zH=ll7N%U^G*bvTjK43tKltB*sLxGnP`+-v;v&pq8yUPR+7H)Q;tHQDk%<(5Pnopwv z{#-qMVQJ-FOx(KAcy4o@gbl|Lr>EzrJfXvAHjTZwon5MEL(>1$IfB7h5cgO2B~esT z4rl6r&b|tDGncO*mD~p1lUoAOK54R4Mn(Uu#;06okczTy9l&8Vm1=W^e>U}#Ppc7` zUSCbSAF=pUxczDw<+BLphj){&>+d^Rj+g{5it_?BlU={B5E{bKiBjJ>VEo(p@=xB@ z-9eE2_BL0(_}vfxl#Jyu%i=pCwUnmK9nTa9e-v2^6V4jG3tQRas1kU#by_vwIcmA( zW7hWv=j|^6)iF>Nf8b~6wrT}2Nig7*xH5zgs9o{4UB&&4)dGQy)zxNd`}HF(x@&U- zLxKhCt=@abvzO4w>Un8|?q4p#gx?V7Vxt}(1-FOAUKeL)^+o^@U+RpaCsDZjk^tW~ znRk6%?e?qa6H^zkKg+8Ve>!_P!9O{V_Ii5^5R#G4kM{I6fvF)9M5s8hmlZ4|dErkc zN$<-X@5x9HH?+~!>=5m~y*)aB>zlR?%g&C53;*$1MS23E{IQboK|7m0E|%`t|0IP@ zmY_I5ZPNX6RoVfDcKKPJ$KKU?Tg3ICCrZIOU-p{$U2#-&d>1{(-M}Nu3=_D#X13Jz zN{1>j69pFdEFXXKl07*IVulhHJkMF{oxNtl<;#Fdbfngwc9n+G=wOm`72rImiNdPQLT(XsvTtGJ@-Dq>{(t3lxGxX z=wfSIGbL8jv}g$XY$z%2IE&FyRh8}#9S*q5$Yd*#T^dhsjsM5*>`c^GdfY)U*BqVa zC@9Ju_=-ovIcyk&_}T((K+_cHL3Ho++C*o@8pZ^X3x`^h$r7yvFL1=>Z}G(!$8VGlNChX1 zT_7EohK7D(@*#zWzt{|9S5dIq?~*rZ>^j;{(Z)aVePTFv{%TMtg90t@fxx{&Jl^HN z$A3q<4N5fT23XEm(X8p<;^Zq8p|;k;m)e->Zy^s~=0XyP6S<<4^x5PPt*$k(zs*j7 zKBBn6Tu-j$M!Bu2K&~%DT(W^t#M~XUR@hRHpThFO(q3L zbbOrpk)teKjsMrw0BB%d;A-qjUfoj0Qqt^K=$X<6%(SEoa_bL#;7jX)6Su)_jRKa< z?+=^aMYHc2NO3*M;NRZ=a_^kGe;mgBNG2q!DMCktTWplm1pTMgm6j^7WR4?%TsWcp z0AYIaPwI(6P^?7yN(7xprhmc3$Xt&2mF5Lso7CjSu~k%(pa|8?U4|lk;a5uq#IW#Gl_e?f2|Af@a z_&B+J)Tm|sF{%kc5tbJ%_)(oQ92a4eN{95-uq2sc&xpHBjf_=_mB=m&ePG6M#yIlB z1Fo!agWYPuq{LHmH`j+{q_z4x!0(=mR66%$B#T81Sd%MuXke~%9UWY7ZQ9Ytk7ECP zh5l{!pY`YC{wO{T&7BfgKCfN_gWd1Futt&J_kj$a-TfE4;e-S_Ifc6Yq5@QpH4X3KSY)7UIBVV zcKCR7a?7`l-}uPy)nD>4?g=^U4sFkEv~@rJ;ZJ)sO#2c(jm;i%m>$^0eXzhCMTW2@ zmvHkls4>gIRd!)c^um*WIqP{!dYSO6m6fMlGM3FqFS&!Jfq??EONs7ir#4Gc%uq|v z18L`ZoOEza@E9JTqBp1fO>drh-9|ejLuXE;Qe6&Ff&a8c?8=<#Zpt|2$VWd0HF1H6 zV=Jc2jUBGw2f^jOjr>-zw5bm#=1iG}k`~!3o?d(7cBCZ6(wiO&J{RaAWdH>vr)oqi zPHV8jcRkTH{407q>QzU+8wL2>Sr8+3Hlg`Cfx*M7zIQC57+iP+uvjaazn<1!yRx}a zhUN87JAeOr2Mr*0%w6z#)CQ?(T35RQf_uzHZI*~?#ptqY7aH&r!}zj#n#X^#n~4W^ zfkx4^^AQchfh?KFf3McWEmVkhslZ2;E=!TEU$kiq?l0^r{H|i3qoChlzF4Qps=4v; zZn9JOY;$Ds^ZZGMV&*E7fgWwh1&_2jriW!R+x%}Y000fZnitPXzc=Nc5wZD}^WTqc z&e`q^dxnCqh@SQV2R+8RO6XpF4#k_dKsAr;l;oC*ziyAk2WY)4)Fg=ueOQXbVdNk3 zE3HO0*T5poPy}PW48$Md$%3|#qAl2`8g~D?t@l%61Z^$6DWQff)268dG8r+gPkr+U zl@S{$dK?(WAy*(_@)P0~`b&BwF?dZ}*2b@FqQ1}yJbCu>@w|S~_V}9SA>_ItG$X)-mzm0MxtfTFn?2L3 zLk{bDZ9H;8rOJd3a%M?AQspJI?d+JdWa0sT4xVL#sJB^#4|K#fUK-F7=TgJ-wFA6N{>_qwY>g zNX$HiykTuJ-+cNgHvL?`mgzEEr}|xEz=E!!M%X(2=*G_0Lx2L{Gv=I_|5&%T3Vqqr z^@?`;S4xe@AZ}Yz(bHKbUS?V)jDs#qlDcu zbN`?V$dfpBTclhF9FbO+Ily|!HQmS(NIROI$Pcq?biyUBe7AW&`YQ+12kB+Dy zY~h=i9#`>WC|$vOn{ckMQI7Q6iQS->**l~6drL+UI=g;_Hb1X&?=fkKcJs+|vYf#j zo63TsTC91~5Ov1A;b0R47b^4()E{`qTXPwAm&R?XW^8K>TZGp}i^0>za;SYGXl-0I zldAixcK5X$V}{*lmYZaX3?5VZw={~p^Okr8C5WT!_xQ+4Xxciv_WV*J;FN?{pYT(e zv)u@%#&i>lXv>znPWL%N;c0~L(e@Qegjj^)Uy`Q^DJcwAM!p_odrd)L+^Vv>XY*Y{ znr+g+`qp%B-t%$`)Bcy$@UF4wTM116A!FM1sfJC@^p6r4_N}RGMHX<&SH?pH3y6*iEQ2^k-Tu8A&ur3kZJ;f?haZRWTYfWMKL-2D_;N-KmO$o z*e99O$s2Ne&s4GYyn?%&Rh^2Tm4QH4vYUPH>as79*k@X}7DS#*zwK5j6#TfLFz=`N16AzUrh4rh5HR1{WlKM;z z>+yNj%*A0D#)i~%!&uK;^j6o2fO7t5Y-P!Q4ue?xnk2oaFNM@81$U zt)AV$$;;QD`8KTpZ0G2qCQL}R#B6u95Q`EV8JA=wMu+$~Y=sui9>s-#J zmaQ#r<;vynjtNCx4txv(i#u4b<$@?E8PY9^ThitiUfN~pq1aDop@%LyQ}8YpUMYz zZY)7d@Q>RAvQ&lTWHB@DHba-u_e-DceKJ~sUg493=8r#>3Sj@=3L1G~M3Upx^RGB?`s6ed+vt!MCn|%4?T2`xG}S&?go1u_7IO2bW8$Ui zR6j>`3F%5!J!vW9-tX6N@=8y~6-bcplN+w-4luX=baM7r8z&#Le%*<#rvJWeM>xuP zk#kGH)E0`X*w&C5rJSz?QJulyPVHaE1~&$c;bmt7wvje4W4^pz>DZtap1aU2G*I)O zd&`?h-I)@P^iE|NS`UYeVo6$nBO>zPdon$!On5%8D+2)DdYC_PJTs%#Tl#g6Ei>9r z%P;cbb9_(ibDY%75e66E?C`bwM@s^Dbe`nt`Pp;RztO|KL%TGkKgKhatJA;AW1Lhe z9`^pU_Q<_e&w{FFu9a}H{Ctrv3-I31F0H_l)n3M@VuldzTy0<7KXy0O)I8T-KMl-2 zKJ`M-fV6Ho^#5F|*=tF-*?`wIt(D#MvYB*Wy68)yt zR*fneLH2DXt?vvlIA=enIKV^DfS>S~VqH_BSb32mwednVAfrYW`4O1eAR zR#WOC`_1lhON~8D^|@zKRL2{_+Evi^KrJ0VQlHjkH(qFL#+kpxg8d#BX|-#?n`9(; z3F3f&aZ;ToMM@yUV{M=^E&VMXf$}ovX>0oW=AA31AE*{K!WM6qevGl-4fb8PI!NAI zMw!j~p{7XO;6i=NdtVpd`84PCK<;uYy8&PYOT)#VN70#{+%=M|E4kKAAX%4n!dD^} zswDyxYZSk;7kDTRT}Jaib(pYlP~GllVqLpfvgX%|@RNb7h0us#Xigt(y*53(_On#6 zK7D5cvw_K@?eE2f-kB$G7w3A3OJIUyZr>P{zm3fMz=+N;&Dr@uc#?B2hWEs~dmu8u zV~v2=#G@J}i1yLX#eKrV9W(VwJHyb*^&JkP(`Uq!sfDl7FS-u znXB{*0_t1QuU*p>OkC0YOgd_pbwp)0-`fQ1)wA8(&7=s3CfR@-e-v(1y7qA@LNZI z%40MZ$W<|yEmrn_0r{@h0@4ut)?94T$V`A88PxBM7K+>Y*En`pk{+bBbnu?pjgFgG ze#&*=UC?2Le2y`^H!&-{1^+O4j$_T&k)39c+?ex-q|sQA{1 ziA{TQYQbaSds!q-Abm}6w}FH`cZIBw-??+Ez*h4|!LYftzM4-tGiAg-;~7w3_08|( zz`I{gxcd9ulAX~$l*x-DW!;R&mGD+wHrI~&Z3#e|&?Fv!n3a^h7}w}~v5)TcEy)JU zaK&V2(Ih{S6~dMjK4+Y0b;)TQujFKVr`Y9big(i7`UeKacJyJNljbVyDPi=xtFft3 ztXq;h$zp}>Teu3DRwgumQP1N6mi9QKqd>Y!_`m4q-fzjnLsp^Z z;!omQnR$D7`uS3)DBOYFKUB(us=tlP35&7qM&8QN^#k6x2~&BTQ+IRXVFZ(A_!3E! zV+7#9J&)k?y054ds@z?oK@DoETwmHNB7^xM<89xyEPFn*JzwwNcl!L(ZR?p|cdl|r zGjI<}e^11Lm32XG^lVpj0s-Sv>LAgIKL?hcCo9WZ#+0jy7u1iQ@Yoi!HVxs`oEez^ z7HvR3YZ26t~FaG4MV1r8mQ|(SdG9OmZ0=N{+%IcPV2bf*hO#3JV&8P>SEAgd@UoKRQa>$UFHN=Sw9UgnM zvy!21b>w?GT$6*}kL`SGFO)1{yiwPPDCT&~7;U~@{t>{ds=BVX;)3AmvXoc~wB2o; zbi^4imEDf|;Rvv{=!}NmHo~jf2J2RtUqf^{dBzfy*z0Vd;`TyFC59k=? zW=4$))S1b8Xl({!k1naUPyUcyb+LehkKl|}Hvd8j`FbDTk08w;RsSUvqCQNPAtEKX z&rkpCeaa<~M`+|h(=&qdsYDBg|9MgVb<7_U_j52?pYqd$Kh8%hm)n#Mfh&StB6gR| zThoH76xaVG-+tIQnh|`r(X1g}Q7h-Yx)Hktw=ik_O zGC}7bw!Vdk|8ELA9#^Q;G=E08pQ&h8x^#k@uZp7XD~Pml1m-`;5?nEmv42X(N;iI* zYUMf$4BBh=^<#KAjm^sC&RkQBZ{=?!$*LyGesZZFsnMpmoY`poyYTYAl^FQ5*xk*d zV6Ih!$F-u2_(a0qwlYxEm26Ci%BQij4%*sIPg06p%kopj_5_jzNUrsxWl=sMr-dky zi=A1w|NHqZz=suBc0X!-T0O9;s_G*Y$`|w`oM?X8y|uVB_*e1>vW&xUwa9vWOC;Lj zXr+fmk!rI`(qSV}CPrPI1K*9w?++q@uYNFVr!S-5JNpS0rZ{XaNlhZX_k9Sf2a4!C z%lPiD%#@D?nGT{hV-P2M%|vcW4*HP-#zt^*$-dRSkpDc z=G!{n&)M1VKb!UT`t?Xz?KaI?J)=?+!O?(I6ILl!g)$=522ar0V;-OgN!6#nby~S9 z(Zh66_aZ-cihNQk??b=tT}p9+Fa1~Sx3tiJyG?B3fGA9GUg*e9!{y`M9TRJ=)$S)h z+ehEI2kDi+x8;9xmkGu9Y!?<=RU@ya{4S?An15U~5CF>VWP^7TX?*nvhfn;M3e+_I2sI z$rriM-%t?|8ts}P1z&|~Mw{O&LFYFjz94)e_+tu|NZx&d`J@o$iur3gyq2OYGtiaAxodC=GfonhK4%yqp;=e@bO| zjeJ0b6C96zUyv80Nh61dDUFYuw+#OO-2VTZ$;)Ts+xn+Ce_uJ$UO?2GX|F2WrUQZVFiVR*xWcZ+tULVo|rD|731 z;|pVY8(j(OZfWScd$?30WZ`x3tuzS#>YDMR0>9cD7Ky{Jx3Y*Mw?n_U#0o7VLST9g zAwUO7SvKhW1DWXxHLuteYkAZy*{~4ZHi!|9LV~BCyfJmF0zm}_kxdX{fw+?9ztAi3 zMt5!O0fq29o)}?D@_;h;7LAT;l`HwL=2yXiJh^V-G9c-Rg;#uSlu7aTV51k!KNLDC zk#{~Q^Xfv>)y;n`t~8~n3qn4A0@xUhk~2}Y;<2;8`D-auF|WeKgb#Jz&vLu?U}ie~ z(=FNmI4y0D*}9I8;}tMD)z;h;Z8dMfy2ib@J!3B0>!r{FM+}a>=h~<{)IDRa6*=aZ z*LoRvR;T>m=6u*E6cUz3^_OlsUO|qF5wCy`NXjxY8m!$4Og46PHrinw^V^d)fgfAC zZ{=*>*}D0;=@;IvZku$9`O7{My#^h7O)^va#!h-?XuIfcq<`fX+eQPf{C;0f*tE@M z9o{lXnYj?2_LiP2##zmwYactG6O?PAMI2nfqLY!G{JD`!+=lPQw}Gm~U(Ty)#syu8 zaL!(y?!O4vGN;Jd%N`m~Ut_xS{$Rr_G8-Rfq^7obec2T6JVH(}cc%A9JDVH$rqs;O zErI-#yU&FvjovqL98axGm(~cmn&>PI)?IyiIlW}Vky8i%`bg!3$C9)dfC0;k<6ac!%ann>=iV(qqR24{W%&dp|4pO3777Qt`$ zB^du(tPZ#j(^MZIolN2b;RzV0j2t_vfv!$H-mu&^F_MNe<>?OPc2gz zq$Eop8{wJK_GDsk#)vXMBecY8gWt{m%AlR1Y_k7?em1fiMP>=(`?k-t(wO0)F&fBm zj`b(qygDjR-99p`6jrq3L8$#!(re*2_u`~mq1uGi7iiF(RhcMUCoTTmqmj(w?a*~- zHBZSck73QFr`Y-h)TSy>U(-Hban7X6P|`-Bp)+#oXfgsh&8E6w(AgXtH~Dy#CS*$>yt_SJpN}{2H)PE3mdkSo|iQnwitm^7Wn$^p8 z>-5uHv=vDd%9f{Isz6*x9xXbmoE;wHYXcR7S?qo2$2*)82zA96q>j}X?(cv?Hpi9|Rw|d2k4uJNmotqOrM!ad8D#7n})Nw0ZfS3;`t zkc|oGCV`6lMvdgkadSlpuWa>;wkavGVEw93*gomk<pqAx22F%$>y+yY|rl=dK zI^xX%ub2;G+Xu4$7b?MDp#O`P{}(*}pELiz3y~;`_P-zt&O-(29-zE-yZ;pZCd#ip zTKN+(b>aw4ZZXRXiT3C=6GYo*Xl4$y^62?bXC$*j-OcUk&&Boi^)(1D8PnS$tCA8X zBh67L`9$0ky1ZZE;Jc0(0Madycg2qX;7{>}xbN>t{o>+RokeAcMyGhctnvx*aqUHx zSZ^2pmQTU|Cb)l&eSK0BRa6hoO6b}+IG76!9lq6c8`8q*{q*m7sc3`@Nv>c|IWi~s z2z;Ap+tl-cg%n#ozrdnSrvC#=$!`+Qo}}|A@t#z9ipW^mzi^76GEy_KF5nqcyf1Ut z`p50E6pn>I|}A- z?j1))DoEttv^Rh2u$*6t8T|1%`#UTC^nrYaCF!YmfUUi|d!X&IM|%=7lFFb&Z$eIE zJmUa7H89{DaE{esU1X(<+z1j_us{^-#~@f%*IG8!uJoiMMW%Az`ttU8KXgxEg~i$x zJd1YSP6S-`bjz#3zkl88g9Oa5;udkE{IN7eJE_6%5JPNAqEe-$eO-@ybHe`b;f_xK zy4LWM(z4%}%reod^t$nnO^g4k8&2LOHM<0hwU!rayg@1r{`}{SpPIZvsJlvjEgXGF zR)68T#5S){TKXzaz{qh_96hVw{^t=FcW6wBwbBg`_%>hTThN895=WU127l9ifVLc> z@E$f;!6Xej_gNQOUs@V=!4A5vdAgaYQ7nZQ&;kloe`^uW`Q2XM)cI!2oc3gq>gi^x z;#B^q2S^q(9*}ngJ2~VX#e5bi$*j%FH{q2q$%1BKwmos3I?w}8FlUij!;f4fA*#%5 z$|D`07&8i;U~YTMz1v_O#ThR@O&MWYtk(B9^}Ta!Y$>%aGXeez-n9eQ(fwD4r=c0! zdk0E{rPVV8^OKu$Ti-!7F(?9jSNtlpS(Fl$x%jeJd3d6=V0NKo1EUad<4Bj&g_hCy z*X_cGRC9?1@HHd>2LT#<%o0M!H zwsq+se{#Ex72pY>ij?=p{=2__hV$lgC#uose}k^B{;tkjS$V&Ids%#5uGlE7#A1`) zO$go{zoCoAE)*0eokssMcm2iA@BuY+P~N7vK4YlpkRcZ0*$y>lM}Ncy2@!d4x{!k! zq36R$9i!iWz2-hufB;c`Wkc9GedDbunVgsi;3ZUf6F8D`-Q1uE&_?m7rsXJW&eZD^ zw~DlBY?>{xo^=*1rYctME1@BiM%?|gIuA6PGguOq9IWs-v-9@f6SdEgk%@JkD5aRf zm;M?n13+^F3Mst{p7eR`t5JuHI}qW!u% z$4lt${-S3)PC()g?>E(_c;=azU*-AdSK{uq!=&R9?uhR0?h$Zw7iAHQF_i>z-~v;w zJWyS3vF4DH9uY584F4c09R4A=A6_yN94(biq%q)_hJM)O`$j$;h*R4{J~LB5pj;meRo;_Eg#dD_NQX1{#_8C(_R_@Iou`{U(p&`bX z71x;2_AZ9$MiRJb&1$G0qKxcq*BJ=~P!kB9dpI8jG$+y~TziWaVx^d41Z4FEh-N#J z(V|d*n!IyvM0(|3-uEvVgjE1g>%;cc7y|K5wYmPxy}l_!B~ZccC#lai^t@O1X6y5r za1+9-f0(;FnfMdt#g=(|SV?aCe?)A@JY^H2$A07@V_=)*T<=$1;c=(+3&Wsjy?y=9 zQ4AANRC~`In%IGimCRSc>qPDHt+tL2Wqo zQcTSfb^;R|OqLd5hGFG>jf#dM`zC&+WU`qa1?s)x-nVL>>)=2ETJ9?LAvyD0uWbag zH@d3zk~51FUE+Co#C)T8Gb2Nyt;1~`RF!Rw$kZCViW_X4Dk@F&`WfHEGrpV?R(aHU zA@{|X%|YCTKf${t@yJbg;TtGuvfwU>iqDyevnfbVob2}#i(BS;dJavviNK& zBuV*!j}cJ4z25VUaSv%$t5KF=a+0a7IR+ULzl(yQwOp|VejkbZ(1rRDD*bb0k`uAx z3@t>-H2W|H8S^ycBgmrmmMt3qhCrKdpD~Zkd!Nu>FgZs2kRZ+ubvG@EP(@Di=~ zZ=rJkR6-CHLYK~+)g{Izye-#!iT}4304-B8pEMG6GXqtl?E2T{G+Li`@N)A|4FO|>sN5b9+#29_z6A`6GorP|2I*~uPVu^ z#Y5s5oO2)MteYsP7>7WcCQQPNfTOn?l(i3=o4o!+i?xB#>R0}CLF~Rt@W7-4xs?bJ zQO=xa*8Hx$<$3wUhZFtIKE%5c_tAawt{e9!M&_fxq zQzXpgg3oKt#}^Lqe-HXofaj|rYV*K^?(NevwT)hw(+>3*x}oHj=dlItt_lucvhKQ9 zTJ`t~{1we1R_tC6cKP?JI4}TJsGF8*l-@%x5c{wN0OrEje zCtyhVI(`N(9rVs$70eYaqKK$)Rk6?TjC2JRFd3GS5OaC#TVbW5u6tBmLajyu$qpad zqzUGV;~Co7Z=o;(i!Ftr!4{vGsh54%KacCot^tFVz~0`DOVsN0^mGh7>eb%Fd6Q48 zc}q&d01n>2MUY?Nnqxy6tA(Jm*oz{;$V+YP-5!dUfYijc*Ortt?-<$>cfY(WoV8xS zwiAi6&=!}FEV4_FjPmpH;43w%iQ4rM-W$O6Q=a9Wx5Tw-lC8Py1D=j@X!;TGu%@B1 zx0BSmiYdbAG&LsOMK0XG1F4#fSJve$BnJ$79$xeLQ8V)WoEQ|%({Xk&j&BMk!9DS7 z8nn-Wl9@fqNT!Q^e@9KW(%?K_^{$Jl0qSRASLvL&9gouDM@*p#wVy;VdRbIq>MU$Z z%vXo1NQBnC4#BJt$L(Kr0F$0up^;FpoEzwS9^V?ge+d6@NQ^d*$UrJHJNdD z{)YvcVD%xwuAGSau?WokJ&y=L_MEQ@oU-1k#nf=vai2bg%&gISY!o}4|4^$1Ss<#8 z43H}CBf7g-w-a7HtU+NB5)}N^C3B21us6s|0BKzGYwS8yoKI<<9g5F;<*oi(E=IZH z5kn%1Z#-c=`tmU^GN(W2`nnUVmEcIF?ch19MYJ#FqZY9vJWJ@U^@{2H}YtD4WK`7rig z(`&L>$GC|X=&P@DmdV_5?AB$^jK^E)3}d5|s=s+LL*5#v^6FCLk&P6f7mj>37|CD$zh^pL-pNfaStRR~QGo&%?tM+a=2R-8 z)!E%)Va^wTzoq9mGP1DbY?SQUBvAgJlQ)B11$^XyjLqDF3Ea2XOgscI72h}|1y8Yqhf$c<%(-SSS=&4k#LJHG;_^Fwe0lEfLMGJxWat`>DPj z1+1c!UN<;`pJstLPJAoN&Wu(a8T@Qs3<@sbtW0|y<9U*Ee$5D0FO2JxR-W{cStIdW z?`qsk5&NfymAUf&AfY`8+Etd4Ll8K7^2QjTX00-IjRF~ed2PZH8T)tFaf)a)&0ksE z(k|u_5K7c5KR>)abv=LA8!T^z{?XdXYxiZT2a+8WhxiO5Sh_B%SjCIB{h1F;jfhp3 z>g{|!iR{(f8RP!@!s-?0%d{;cvjWGz?&cjfJS%5!psl9)j8}Lo*X{2uE*%Lj3CovB z2%CC&(lc=xh&C*3yoU@liALZJZ$N#kgio$28?!w2zgSM7tTttodiuMdWOYyYI8zZJ zv|^hckFYfmGH(H3rL)O2>a9(r=Eu=`>Z#{36z(tnf^khd?8# zHU3n+ccDxZkTUj;U15?KRE0iGBD-ifh(VStbDndA?v@Fd34g=|MrQ@G%c+n39{|-r zD!)wOnLx}dIbTErnZf*Js2T2s9n%YWrc1D7)pX^qH}@4rZ-B%+{rYvZDrvd8`nUaF z@5PJ#Rn*LTfK04t-j~P>E2fws7fo~O*vwcN^oV@<%^xf>@@$bG>XJ)fVvu(S!SV$v zB_qZDLfsTCb1DY-qEUW<+h0B-FsEV?b4+jZovC~QG)pX+v&v-)`{SZT&R_OHjxjK2 zycAb~V#+?bO@o+n5vaflrxbF}^Y z?;k%FcP^`DDut4HCofqr@e4m#evQ>@wJKPp4bu2OcFylLjdYFU+IA+)L^$P`9UQ9$ zDq8}UoN}N@0;MM$4mcvki#C{E2n_@!K^Ed=IBWx5|Q(${@u;_`lhbN%%ou?O*s9trjp83%qW({OTfue!$?xS2b(4>3d`AG zWT`+*J%-WyN5mzD=ErDapJ~h)F|Q=)5)k`s@pRnU6RSFPkC+)qZ4#*` z(wOkU9A`*Z6|O7ndc)z_*_BcGo9X7BMnGi3y+YvWu;YjP zm;n=}4@z0#|G)|p5C(5s0rWxm-FQ*hGx1#(Q+q2*g_4q0!*ki>&iu`#d`;KLt*&pr zSeakRmrPAH%@i;*#*R!5%XJgtZEE{zB+V!}jo!4-oY`WHX~h;aO^xY_Fm7_a{Qa-L ztuD=oIV0v5#KgZ>TFz+Xz|9c0Gsh!m5Sk=3yA+nod>vXc?+1*QxZj*F`oe}I>L+a+(2@W9ir5l_SCqd^EQ^tbQc_U-c|FsqaK%<2>}tDMM$ z2Qe%!@C$)Cu)TjqaQPj$@?XniN(*LVdg=!9l6>G>O|J7Je)%)!NHC?dC-SS`o)NF^LnO8G(}hVN3T73T95IF+3XRqFHKt?U?TObvz9 zXfkDP?$z`8go%_)8B{IDsuAMyP&ZT5G=6&8Xqq}r;CO$EL3*pzfCX2~QsI-Ty3P;7 z=5i+Hzh-&N88N>gCiBv9@XvNc%)~Q{PJa!3rP&o>IfCJx@U`Ff#3XKrzgv!|WTYuwODfA{6f=U#SmT4PFNQqvMT-vOFL%L`kCQS zAS#D+-SMiQj4qAJFEM1w7V?G{k_#shmY}&cl1%Hw%n-wpu_;_cBA4ka%v=b+D2ExI zu&gs15tFj=Lw8gaM5g7t*eL;E8i{9b4x_E9u zlz@nls))<6L{2G873C#tQ-e7yW3NEKWOETs;<&R8z!?&|wUVY1QxwxYXnOPBuu<~pA2VXkh&koQ z6iV{5LrN2vDYPc;KKsm+GLs`o49j5?qEk+jUp}=PW*f zpf(tsBQ4Ld-zXE0FUI5XOlG^hfAsX}FBqU#ajYf?M*Lkp56fNr!cB@>h{OkM!$gHS zqW?y!#MmYnbbpiEEOvj z6;q9w5S1r3vGx-dC2;JNBsjil9asoO_7^o%!fQk_6Tzw|9KGWx4yu-~&NSwXm{Y{0 zQteq!gy0@9X@T;T#B^9{4tql;CR#GPT)V*HMK@6S74-~07Rx5UdJnU`$NG1XmTf$= zQRvyP_iteib1@Q$Tw_IQqE zPC_y`LpYoZU&2e7wNwe3GMT9oGkN){^b-Q}0rHYFnXudFY%D)qt|tsjGTGv&oMUo= z=7)f4YncUnMfHB{7VJGDdY@TWr@ zmtNE=HN(_mrcq1OaDEXIlcRD)p=KY{m6rL5G1+RGib(^EAUZc=7M6bWl$^$K%EX+x zi)O^Us;?YLh$@qWCjBYXF=@}wB2t%Bfb%`13g8o{Fd&W&k)^f}phZiHX-l)DM(E0^GRPL_vMXiH~zA^vb3}szwQU+R4xa6-IHD0T`B7NOg`JT zr~0`Gg&+rog2XewxBG)J&MlHLeb>ehLYi@W+(BGsGUTF-X0LC3qMmXH&AG9B>jPD;n$4|Gs4y2&EfAO$6nzWXFc34542QE>vtsCK zSk=&lSt_iq?KHGp4nA4*)mou+sOk!iXU`ft`4<&kg|NJmC?&BGNQ-f`T!|X3MumpE zS!`TKWHv1Yk8zOEff-w%u^lP#Ls5=gZ}C zBnV9EmW(X!_XRISB;0QY=TJ_H%Rx^HLHsm`OdzIvaxxyYWeAOLy+@Ju_~HnypnoMt zXgQP4WXc)QrkRegyXn@GCqF)KyW=r}a_A0_mSav$2AMb(wxaQPJOW;h{{(7ItmGxc z48-75J>@@vbT&IZQfq%m1xF6R?QIGd9tB(#kGpS|;YO)E{~_;HKYV=}{bG***n z0&aK1hMk2S7O`N9^rFXu1WR`Y(~H0g;S32fkX%ZcAPX`OEt!kpUhQ@Nf`$&Y3G@ZY|_ryeFV)W2{{65d~eV%t%I7DUwBQZSRu27wS zxIGk;>ENM^ToE-U5L43vXcN*jr?IF9jTb)4~_bx4p9jnhvRrAsRdJ3adLqWJz}nTQ4Jw%rAa_b8T&FeSL8;BFshH_`}3JVLpHI0ht~B1TQb9FVh=c7&ZMZ8<2()o z<}50o5%Wr7uF_@I9-*6S#K^QII0=~`5L+Fq+KX5qMlb-X^2a))K@mW2TU;S&P!!)_ zZsL^_i<%r)|3t)Gys=^$FCz=_SbXi~7u@|ro=mIp2cmMsW8;LC;nYku6O)aW_}XZ+ z%x~g5K>y_A0|e!wG{Xr@Qr7(mm+K>ub=?b;7-r5-ceWN+gvh)$FOW$(^M}5C-H6L^ zjn0R;y0we`3DB7-3Ae2?g};Qg^DBtCG0jO!Viv94&%~w?9Qi78=*IO+yiALnC8sTE zM3E*mrp<@Rfw;>m`uJQVGVz8j7H49*2&h=vHjeewgD*j03VBJ)JiQAFGo(ZvV$czV zF)`FQ-BJcOWnZg)6z7W^*eizQ? zh&nv}d@7bm?fOAddb}Yx@O-L)z<&yI!|uz7aBo!Clgj{+EMv>|4OBPBCH{2yGUkWT~;&M}U?s zmUFB`TP#EoTR_YmW)tem$XYD6c9TNmd)KJ;f}EO{arw@ppClVaIL-C$7Q^vG6vtUu zj#?HvA%bpC`X3toc6HL{(z!@M(u7e(cC%&*+~`iB9^ z$?7yIYfU;}^)MuFPn{+{Go6u}Cb54+OnW4L#T3kn@L~E2q%mn&o2cicMdjyu+G_35 zWSNJzHp9VgBK2uXOmkxj%3ZQ=ojmL+9=u#CsZ8bQ$mQaRMlS!c{_fp_QwYi>Fi$uC z2oh7zmGOlLhr5q8X40f1|8=hKGrl>8DT!GLYNFyF)jWP9Eh+4~etS}dRo)-^;NwGf~)yOL(Xi;Ng zA~WFR*2$Nz0hmAk7oa&K=8TxX5tYN*8+4mJ_rUG`m)q?&<>rVpmLQ?1EL4N9qqi+a zXj=xNi~;=8tui)k%PvqyHtE?Kjix9nOp=e7m;}X}vAc5%3u})PiA3!7Q}+2REfG-R zULfX=C$g4fJqqV3O=(HR>jqnUxHN-b0GJb!en89*cpKjX5Nl%$rO%(Dp9p=R>p;y@ zqj`$A*&RxsBs*`+gUbBJMYA@Z(v{tAS8}t~?h`Tj{P0^2yoF#|7ihVBhm^+Dcpk7%lf1kx(wI{zOxBoU+y+z1aXBXEu^Ylt#C9U8k6Q(Dlb9+d zI#p+A`3(Ydld_mV&2P+*+>16(QkNDor-4b#gg-@fDTfntm_ST!#W@6G!fR2zp`gRz z3_3L>==VBdIdE^?a6UE{-v@en%DAm$$S{IVZ#I`NAw>+_emc?Fr+$;FQ0yE7^GuK1O9`}uiB%o#B+5p&g+!(82~_lBCb-Q4J2T=cr# z3-{o{eFo?}tBax#N6QYcIyX6x(>lNg-=K1=u8mRs(NR^P)ns?|?LLe@X5tV*`tX|{z&ASJPOyVW^ zFz*5|-(*6nA`+McOA(#R1Zr9~a{3(nv=vXNQZ$MctX)&2j9ou2fBrbw%gCk*E3?yi1`U(vc`0GeVpDZG>3%(T0b_rK+fKU z+kG(rcEaVYyVVfRpaxfhszOl;jS0LQqvB*@+GXZ=syf^@6Mt6xzshz5|H1BN>C;+Auj)ZK*Su|LUMUVFo#jE^UmbbA5e#P zqBHkJx9i@!Qe$phwyRHXO+lWVA=gbbi9=!$4hklbiLK+Z?xTnwu%sWq!Im)Ao9itL<-d+|Cs zZK_pU#9pg@#8J3R-XH9&M}8Ms+=}bF$E7d-{MEgC_a5H4^Y9LNBZ-;^2M0?KlRsm* zi0p0mq;f<)PO1};E1-3$AOp{9j0b=d1m;A54vE)fVh$&4qR0Ns&*93}?S=J-aDd)e znWyJq0u#p@f7`=%2$`g;2|3-NlrM$fXC&4^vV(Mo)opgWpy-De^}l~ADi@@*HY1lg z%tJYV181$9j&w!L*pP=9x1ou>$Th_pN+fpH)uw1k12B^`-OV~UIL8$TxVT)7#DuFp z1%dfs^WEv@{Zov`?*lT?hpGR&993lBIg3moFSY5(Nw%#tf;1MRh@UYouN=b5a(InQ z7H3_CGRuAkZc2eB%zWZ@>bw|Kabq+kThcRSPuc5n!M}fOLeG{1E)TYrXFW8+9iA^n z<=D@|t>uSFPeBrMyO0#S{gACynAo|L{t697Gh)t&d37F>mF8+4G*7oV^bMO$pKmy% zggh$FLP6V>3jw;(t-E`}X78d`?O+9EFlYe>sP-(3Mg%7-hbe(LZjUF`>e#|%%>=2a z0>u4v001BWNklt2x!1jQcP5DEtFq^~iGfZQWe;vK_3v-Fv5wqq) zmQ182TiM~`>;JQNem`lYX&gUnmm`NZ%_OiCs!*co#0Y83Mu*W%XTw}403+iWJACG!W^*f z#48u>t}xqdPQ_$H64M(#C<=l%ukD$7iNA~ zAhQVCv1maF*=ShkBVH}T2b|T>h49RzLZVtlX#*eoG73+0+4bqT|NUQM-x#sv36*A9 zdBPLwEcOYwt5C=f zZ{>Mi(Iu@OlPB&`W6oJ>wf)9^t+BgH8nRx;3o^1yj06a0#P<+UqOFaUPljq2YE0c7 z*O1Bfd?!Q|I86}dt8~Tu4Mt64-MYj>nCH8DOnlzD!uufk95)n~GQZ0;rmQexDykVZ zCcH~?{1JvQOOd_;vGPs<-SbFfKD=UL3X{bzRqm6Av;up>VoM=|abgh?lS$0-5(YAP zL8k0lnjWUX>ry}t58n>%EqUy?BTaHl|L(Id20B)72O0p?F@5E#8(=g`y!@#}%oZ_!QDahtkr#6j z#UfoHK>?u+6iD#NWrM}wvzN(aL=hd9Br|BDVJlCu_zYy#!LHFzZ=LO*YYjA-UYW$4 z+U>KWd~hZ?6!Y~&#vWI|zDbSr^(~By05U;i#xR@S-g_$keD(XPC~E?p$!Upat|Ygs zLS5nyCuKp*^7aWMCX<+kba(mW2t;On@cJbTV-|)7V}(2?B};20zX*rv zDZsMP!1zVEoG7y%KmcVXfyXNo;(No%4}J3{U@uqUOQ%}=WsPZICkG~J| z`|RvwD_EF_lv0(-;}qD)*x15IYBDlGPPq?=i9Y$yyuPVUCk9O5C%V7e!tv(NG~5+W zZlhODh}}M^qaS}}u%q;K-{3V~MBiNy=^lp5OIigx027IMM0%X7N!3oplG+ywMK^$v z07-bh&I=J5B;+GLB(k+K<{0Iyv7PrOnpl@=mz7v2IV@9?^!!mZacE0V;zC;1W} zJC~RNZ5q5FCvQ4ptwUMz$eCNdM8pJ*xoS&>9cJ0%c4JwQ+3KNXyf}m-;8-5Gd^@;2 z;|9lF3MfvGduBd3F#ITv`EiqMbJW5jiH$o!)6)Y?{+yy6 z;fdSE&c;S`%GZ4rdy78l#x=6t-GhTv4|;>ON*aK9bNt&n00JQy(i;&`R8l0K#}&!xxp5yG_f8=dkp4zVC&3h8ECvx_^wXj z#JZw&LaI76sg*}%&}6wEnJG>rDauS)HKR|yidhs2WUe~@b^N;YZsF}hU!;%Xm;g=m z$%n!@9>b(L9je#~WH+qOAYZ(Vc&SbH#CYP;`sNbTnc$YMXI(&gXZ%5jLjkQQq4dAX_aL;cKFNYm2o5v|hR!7`zbA+eohn8nnfauQn^m5pYE7L^g$HnW5*Z?EF zh#iOt|K#0l*5guLIjfVUi1uE*_<<%1w}{yy=6Pazn?jk*nAGy{*Pmo z%DZ>(7XC0fImx7EsWcIb_2j3f(&?$|(W4`fuBU7)&;eDUqcsjJyHB554jK(0ucjCe z7!!>~WZc-GtBr5{nsqYk?M+X`e82wHwSi#pa?E%2Y8S}NSS%mIoKrhNlk(&M%$vP6 zUYG@tBnl*`Yv(0OZVU_~Bj@_(@WN3^hmVe;_d|_&j+Ta>5^QJ@Lnew%zNf^v*9vc z1-l~M)4_qE>F~PCWp^a7HF8V#i#$hlt6BWU97x zjhOsVENXJwJUJ_C`+5HSB_)4a#B34sq6&6zGcLX2q>D@KPP2)ZkQ-ktqWg}@34poS z*-PY!GDb2OF@)0f8ZEyi(`mKH`Rd;|-O7JCQuz^xnfmVU-=tEN<0r?ZJD~A^n6Y#^ znx0B?60dEbgxz5S&v%}Q#gRKZ8%LUCL@KWNn@oQvox*zY3@5zBo153Kd@>Lmyyoj- zNph5wQ&Tt7<7uE|JE=>qFx8MfQ7r=}UxL*R-!~31i&hlhFwKx;fescjrt1$i=10_} zQDeSAW_obAg$s!p3h2OW6A2YIdP8akrYKZb`8+qRn}=eVDr@)wW~yu~5)nrwVgfR; zgGMnaEAitB67%iCLIgEtp%3-t#I5C|$IR=AP(AW4NOHt|O)9JLuXF33wtjT;BBbt=@ z!3uUWNK8J69W*9R4(7njs2(v^c-`%>;xtVyTRywlB4&%27j{W{&jC}<1{F1bya}54 z+2-R^&?nQk)rOIO1*GVZ|W~7F_D!)IUERcishZ)$Fey_OgkUFAK@shz zbyBMjdgm)oDz8%*#ROo2SN@h9^GKnz99KgBXYc%8(@N7geq`|!PxoS2vu8E+oDG&h z+jPK-W+elG%`8h}2s;U^!3YzGY_Ks%TXUy$J;*LHU`#UuBE4ylxk#9c+-;!{2Ir=b zt5D*?TsQ>0^VBdt|l6eWl42O$SvXP)%Ctx}YFO%dX z>?M=H%19*XTGrv=3K0{-@yE}ef6@D@iwW^psx<8tFu>mJM=L4#W(oK+I}TZj@BHo!y})8nMTMF_6<*z{}Ng@-G^77mbK{ zjmDgCX-oztN=r0}n7*`#iSAt7;G)M6NI-&#lN5Sk)Q><)WE5OX;&u8#yado$h|Q$M zC+mk+tF_tbATU8>KKlI2SKkpat>?H!5{Qa&4^DI>V~`;1a57>OQSdM_bcJnvItNH% zzBruvY!L^eHDMhl(IOL`cG60|`4$d~$BCFl`G(lyap-$9;nJ8nsV*6ySMr!QtPR_J z-R?^+h2mTZa>7WP4x@i!RFK%UZ(1SkUV|wBCgPENaNJA5^NByDgnjX}GdOB1i zGZ306>BYHcKtq$;)lJ;l^Y;4!ZIq08<=k>2W*`tVxt9KZunulvs z=q)^shQgT39M4#s!32Q`5)m6(-*X%)0r8@Grf zlWsXaMYe-5JV4GL=Q0m>T&e^^3Z7M2&IVGN*lk1Mske(eWxOg^glMY|mdN+^4jTJd zjKhc=LULHE7jPE9RL2~h%DjxlZ2e2Uq0(F!V)6=m$m8$Q#oM}KpQOxVR)RiJ{>RK@ zSvRSWV-yw_LTLkE&5~ji=I%YXy;##d5Q@Vq1)ScV-O&ROpQjSFkWL<%K9zA>e_xFJF-OE4Mod?ce1c8iPHnLW1s*z!gCNn8P&rXIgFQlIUaGMbCkS4{(O9nv(fI}{mox+p?eV) z$S0}2NpxZsi_V8fPP`xD1BKroH#H}&XiU2BD>deZBQaTFisB9%0oeL--Z2nnWV*MH zBP}s6855frxeVm%R?6w@+iW6{P3t;Z@|tF~iu#-V zO?xmta7ZI)DctkK$gymmjfd+58qA*2VP6~7V;X_J9Cf6k~Y5j+=+0TEDzOe=EpieFNem##^P8F};J-SOfY4n-UP^uY>lHp24H8nrfMGUH;XWYLwf z$Vt&H-W4}5Cv1)B&SMUDd%GbfXD?x|bhkhU4#iP%(Wso_!=3A6b{%11~0XG{B{pCwz*N=*myetCh>(>X39`$>X#)Po^QGPYwBw%_J z8V$s}#8BG=9a%D+UGL--;+iWdo1&ysW~DB6{MfsG#-d9;y|xq5eOM!hqfH3L`SoDJ za%*OGZhNPY+TArY#iMA|UF>0>+fEoXwLHaOC1L^!gMNc%S3ocxO559haQ9S2>@g}~ zRIaL;X`1zPVe#G{?%y8~b41MRG^V{o?((Z5U}I9W(A*J?+IM6eS{#I?YZYY@6Qgng zOqt32T2y}Eo-4&mQ@Wk-ABx+1pulgU)KA!dehs6A@v-y}LCNL8vCS4?niO>XE zuCK3CbZ@z#8NQOXbl+-4Cf(Q!$DK&t`tieYGc!K*>C}Uj<#xNhymENhY&V;SByMRI z5v<9${JkAvIqiVc6{<^FIiy1LD~P#4w3I_6-Qljy%4pO!!W72lihF{O2**id#x%D2 z1y}++(plhT>$uf!!MP3JTdhuSzwxqxOE+Ncc6WmZFOQCZm}mX|N&AF)i!L6uQDH9c zq*O5}jd>X>QFvbh-9}uz6_vGoSNUf?${qK@j=ftEl^>9pl+#4}BA*^AWKEBPhHwBq zzXt3Kczp_czHJvkZ|)j?ui^=&(s~tp*tfR}x*E_-tpE5`CMM0`4ayCW-5@(s4t&ej>b$f zGWmGyG!o2|>suEmMI6qh9@n=)tw|+*dS!3kSQb!nwb^X9UahXIW!9Rj52n`WHNex~ zVGoX?0H(uBsWEddkvX6)ool(FwelMzX60`pD0hIVkkQ+`ZB(QPW>PDr*c~Z5EkQ+% z#jw(Y`VuMiHW4~MKLZnYjin|hX6X|Cb6A8G-$ zP;*;WqGUpXVHF2v?Ahorf z(9l`~%RwM8XBM|YX}=frpl8Wrqzpp`vihoewzh@Sc_9kN(G_GuEDSOcm?ql_LkP}W zedK_&a;T6}}{6Bl=7t&Uq#qk?<6Ufzvfuy&&OKv*cVU}XEgA9gH*pBo`Bg$G< zB@+fqLTo06z*w0+Bru6eu_-Zb9S9-=ioJyfp?#|ZGd`GyW$9BNdSPJ6R0#X#!?seg zFzvACoZs(vZ`9cN^PHQg!P?|r!B4*De9s@+a)JHp^F8mqcfL!^E-^c5OkR}vK6Ub8 z5)y)OvW0suSi2Lg#Woju`LG_LGA&ft5|e98jz=AtX%~`ng{fOYlj_-lb#aDFUSm|O zn`n}6v^Jij%52@hLZgjOr)Ottq%n&uBxfSi^4#G$c>U6`klg6+WS19D+T=iYL#Ryl zwf7DcFw#I^j8jrBLW}5mx9_L*zS-vx&(f@iwoV zW^pVEOQn6-B#Xzn#Pqh!5B73f+zbDPX*~#`371Gq494+>;kZRi>KfSIyoWwd6GQao z(UBkNc?3eU`S$JGr$S;ja@TJAHK%2ebDinL7c6J<^CR=Paz~KeI$K81dt7AFjx?3` zNg_BE7Mvq7Y2w?sD?751Qn4gm8kZ6o6qqT6_56(cq7aB<^}6i#ttJv0NKi>aw>+Qq zutcVl9oDuV9PSq2V}+N?H2w{BZ8@M~Q714nprxu;&z(PazDvw5F;C|)MM#cS*g(DD zOk|dr$Rr|7icTA<(F`JkSRNAjM~Tha zYpgD6Zfu~&gy=k-8yFaxn!W_coSn6x$tsgr7#{1_5xV`cP72eo{hr!ntv!LsBXyla zQ`k|gFR0(xGxw30t%12ufS7&z`?Yu-`#XV{R7fAwOB10^%sgfLoQ=(QYs@lf%rf`N zg~)6RY{hzAY1jg$R}^bTth)<4v}yZ<^Kd;2>T+*y&wO)WzQMfZPbSM@wvx%#4NQd~ zF&}ONC0pqx$xC?joE`x%TTdA-0ho;?#no0*6rnC8d~tO>70qW>=JU%AHdx7CSYb;} zKK%|Oa}3LsAC+}Q6eqk<$^m}Dp+uvKN*(4E|LEXoCc3NzRGA#0l`DVFD43;G z-3}^ZuV@w7=~1%&sN$2j$i$VBd>RLBC>QH^{n+-S5;M!5fQmi!F4?JS3I9mX#f#^< z#OxCD%q~e1n5BXa$_Z%s0o{7rN9fW9MZW2#_ktCg+a;P4c2ut8O`i?s z(~lyVc|u#(PFmw_8q?0M5-owih7QDRH1+`{LtlI{{TC!=yujukq#7@ZhI0s1mlkIxa5{3EbvBNng?*h zyGgi&58!38m41S<{w@)de*cKcgCne42a-Pj4{FS(Ib`P8SmTfIn3^i9 z?(y+J_3FnTf3%XS_}o4j{szXwnm?1R_-NS=?z|w8$*mt*j#w>`NKBB}`6$O{Hnk4l zM2+bpV?8^6rAy2%G0*hMsc8N4&nJTD_aHBOKe!-hd5oFji%4gR%60)vf+p+fE=&ed zV^X&whUEo7CTlEbgqpOPDD@zjHuW`N(QbT~L$g>jkB-vhm&5&U1Gz>rH!#(Q!!fDr zN7&@;8WTmT-k}WBIJ0`4h2fJ1dgPOqA%$KP&{7W?M$Bv`DPsL*dKRaE?7xDu#33Sh zZJ6qe1q)1FpN`2x?ZxXie>*W}L_HCU&Mjh6o1{%l4ooV{WXQB<21l$(MF0bAlabu2 zLIM-CB^DFCG3_I5rqgM_<-flFeg}!UxxKZsLjtqfG_ee`_4F+}Fq1ikhk0k!fg3G_ z>7$}S4yr8xF*6qGK%|Qws_c@&LS^#Hl-f%*y^+HJr7sl%ra5Bf69J%W*pu}`bnSrW zat%A20UYXGp)q;uk39~9H~!DUR2`8S#}os+H-Nm=3#E|V40W@uc-@wmzg=U_h#0(WPlfexaYiIE`5a+h zATD@G3Cv+C!i?}FrjLk;orX>X=9>c&m~YxdCi3!ndShW{=gTke-hFWYF%a|d_SV+d zTd4F+5TDphfXMtAhj}gZ&wu@SHwHq56Sb!#Gs-mWv$FT90H-o6=n)E3>OH8 z*M}9MV%YF-1dSpJ%YW1Wn2m9*JDQ`{?nzE*7>V#m7001BWNklY znm@?r0hAD}E7@#z-NW*jxO)y<3l8qK5T zM!LB%mdiB;23pB$Q`3Y?CNX1HV`Yr>SjGvNoqHmUV|_+KTZ-@NZ7t}!C}TH3V#XV0 zx|wd8ucxr2C^ohK3Wy0W6AaR2(ig6-PsHS*UtyB}vh!mNTb~hM7TH`$TVx7}DUz6& z#vFEd`TRECJC23iZotZ+LNWFM`QvmnIWe2puP@x%L0Ud|aR2`ON1ONVJ$$^qy}gB@ zd2%d=5>trG|F)hyNvD&aRTHX9SU#-F2NWhsOJFAJ4~HYg%PV?-H|1%oD_X#&ek8ef#Z;H2~(H7%(v=Ka-b#6gsoFmt-bGMhJ-r4@wx6 zGhhZ;ArVuUC^1VoWthTf228AI#~3^w->p+z4tI#nch^{4UI&?J9yQY&&82HYxv|{9 zz`#)7G~$wm7g-(=?-)X2uCd8+opGrjv(P8o6sBSKpIb(LKWa?Fs5Q)18ucvzvo<%? zXuPV|h?sCu1)S^&*B>gK=z%(2RCM$A5Oe0(V7Eo)lJlYERwC2dE*9oFOtzzf`g|gM z@YFQ6>v@jphFwe@UfK9liB0it+`XI+N;*ST+-aa%3h_giXiPCn=xV zUtpz0*5qJTqs|J~k(hu-RF`ogX1u=kVy(_nnelqDZXTIq$%W>%p(NV2IV>Ze#Tl6( z_u@3>Eyl~%V@zVM#qF7HjH9Qca%0jUiD{)Vv7ubpD`6 zz2>VrNm&w?@D|W-=OsFgcY6PKYD|{5DHb9vv)orZRTklPZ*PuOmA^t@=D%!7R$_`mBoWY zrUxnQz&Z(LG0;j3rIL^~O85HyZ71r}zBc7=tElY~kbg`S*R+A3ogp*EYct=MjbFk2YCi zrb67begmDD0Ol(Yb9O5K54Y>cTVsxsRD^xHaLNp*`y@9#+|7yiA-+6bfU=bj$|@vH<8cga@)sT6p)_}8pKOYRXuLon9WO3 zc)U<7#(hhfrL+grjvo4PRl8lzNjiCV1?f!V;-?plq(U0h2of@lY?=b~WCRoS^ky`b zm;KQ5Rz`c% zE{)Cy7D|@cy0mu_`Al@K$721^m=y*QR3=>2MU)=fB4XweiOJan1Jg7mF=6)xP!5Ve zK-$FtnI)@OzGZnDaln3Pu5KyKE280OP|#DF9r5_4Wsyk&v%fr8Ho1#?dZN0uwRQV! zl{99w#?zbw_@Vo+uouVipu7{9A3Vp2$yrODhvY<`>=Zguk>;r%n@w)B6p&Wh%ic^&ZoUi>Gjq+{WUije<(eSo#x~A(blU_m1uzL;(o- z-R7(256#`F{6@jq_SKl%zrGKN$#XuCxt4{|KJuGPPhIC+=9nJI1aU_Be#xgeh-vgJ z3c8hzd~T8(oI-?5pN6hY3`zR6Y{rO_H1#4CtNDT#M@b&eYdWbyY%V7~sU#J5fmCL8 ziHM0wA6-eNp;DQcNKcabbt@nt5!2&Yo%ey4zBCr3k;Y5{n2Lvq>2P@FeZej0EF`9wh(z({gA!9R6UoeeiOhjYh2+-)t1syoEcE4`g-m2Fqat&ODh)A? z_=qYwv$7^HW+sK|HS_iBTq?D;w$^;yG^a<#IV$h#qoABY*)L*w@sR0>VtKS}A~$Ho z>#~a-9PDq0=1fE=&$9RiF8S#29Ml2pi6Io7uHCv#+^iuZAd?MO#P5qQkIamA1m(xZ zBY!tB|H8L9X;;x<$Sa9xb+EUQ`HgV8u-Y(@ybM5F1yGyW*XAQW$V*FLn&zl^zh0lr z<))^ZQ_WcdXFk6UV1BnL4hQ8A(T5qDHm#QRA6u>6cdrPUyR-R?O|L^njcbJIYBO=B4z-B8nePuusUevb&l9HplL3)HpcMJw7B}k7ky1Ole(alB*64KJr0;5Mu zgY*C?X-QxH&-1+B?)P)gx#yhw3(gF6jlKD(9h=K;@hKBF9H-31Jpt5Il6W>S$1P4q zw5DNcYxNbg;U5W3-Z2KpgsO_-I-e-S8DWp+A7(c{^%z|9E}g@>059P0#q7TsTWn?L0zUR6QzrmmfD)lPM2_qy z{HsZk(lPhK;U~S%>(bPnvY#YQ(~{>HXmq!@q8L@j*|dZU9}P;NMp|&fAVOCSJWBe7 zX@+^if$oJ`+%_+Mc>h`nX&zJ6VJd*}$C6#RrrxN_&|kj!uSnZxyj$}|<* zsuNMKe{-cZJ^$<}`}E+y|Jndvh)pFU^eu;i5@gS~5FwP;6{mR_p3g2$vKy zPicr{a99huyHR^YRre^IL2c^QGqn4^pBK1BO!yf*H#z z-Oobg>3|nBa@UZ_jG1QZDn3Gg^1+R@JBmLbt6u{Epjloa1Wc%*Q`HZVcB z{O`cm5Z%;X8jx-LV}t{{^fb)=Cgr;>JdnOQxcx6Y z)XHdvMX}1F9T@#@An-(6`Q#ZkdN_iXEdw3P&+PKYlYI8))_H$xtBeeRJ|e%IAJnVV zVyee5U%0SXp8Uy2iU_BL>Vr>dvGh*UyOk>1aC(+bGB8d1ulLbzk2+VF5}Zkdfx_$B zU*3$Od_?d;aPGL%9wjTr;Ba=+=wV`(=UPHl+#HVJbnh=RxuvS_|LZm$6q9%OTLPOY z?Y4AOd= z#%v}te3U6HjNee4_tRljl7Rf5Mq-T5d%|tjJjIddj< zWU+VLPpYwb#8o`^Tk(Z{Gx>2;wk+4Dx~tO(;VaU2^XlBmIo=)}A@^s2ca@Hl2XT(~ z>{P`_&BAfL)XutlL?Y;iK$*i?-!P*|&2U@D<)OtMuDRq+{o$cC^G@WlDHf*qL;KHa z04FkiR*H}YTR{~Z>DIeBd8$992(AIW{^4bbOyEEMt`ALM>n`?ZKrOiz=|988Kt9i6 zCfU{fh1gab zsBSqaT|S66P7rmu=xd1G>T}yd;RuuNy5?f`}_Dm zgDy)>$a6m4b+S9Ba~L2UGF|5QNeNsLzjN~xRpCLSkFH%I^WJ6uB1M}Pm+5+{lZ?QM zoWW(Sk4dk5&OR*mvBU6Llbou^2{t4X6NRLur@QjIy_om>YwK0b+aBMpcw;Yegu{~D z^DeK`*T*(2ggGT6^1-<{S(cB&Sy#sRAYh^|>IuBC(4`Tt5s%dR4AyMZc)8!$BsF?I zT6rJo0ouMYIK494ZDf0Y@{LZ*T!$R^FK~Nwg#dYVzJ=wo`dz&9Xkqr{>AY)!)v}jA z(LqHexuCs0kNs?h#Vzv$XA5sYGa(EY8sA`1)-57wMie;CQ#BvB|H{@pxoy+nY`tG8 z2@e(0(5ix}*xnfWUG~eluDKyVe+?5{a+ps;oy~U=Hz!f5A-XM$D~7z*(&zs^Q3Lho z=clyERn|8EJT zM|L_7z~WARD0}HweaI=RJb5fs?x-w=M~)|Go&eb-&y!@j02ov?9HznHR}=NNQTjn) z7I3+fI^HkT<5UP_z@tszbV)}92XKD0t#^8!l@Evi4v=8XRXl!j1fWlOYr59*J*{DX zTl8hc!L|^Q!ik@r7HBP3JivsxDIH|lWx^?0$&xwl$>tz((U&cvqM|Z|<1&fZ@FF;B zAB*VQ^aJB;5oxHwA|Ll_ID?z>jY~dLr)qNNqr^#UBt{I*$lljO@=bn<|L9rsOT=|< zb;l{}lE%SY3jgc>brn&cR&2B6qQhG1uR#J&0kF;2S{o$V__^9N^%72h{P=v})6`1= z0wnV~+c#|o;inZdGdldB+bK1Df18TUf_1f{-}^7g+rNrth$_1)M~M1cy3nOtQa*T1 zT;7a=ZtKwBW~I~_WbQ%OS0krkN2l^f7|gx=9+X^ma=wk4c7TVCPEpb{stbpj$#8wHy1=^l(i){yA_FB?|(huM3}us0f;>2>_pP zXR*AYSIFUsL$+JJv>1Ir3db)I3(OFkk#LZSwb7CkE27<3JfmN|aLw30Qb;cT*jKOR z(r8a1Uou0%lI|_p%#2%!K*+VD(3GPB2L0D-mH8-xwjp*d3#7pk+4lR}zr_~KxouXr zcuHvN&S`N{+_gjSk$5A6sYzDjFjzl%XxcexK8m8J;;jfp`L~?oAbU}cu5K~}@TD3M! zm0ZU&OsfV6?_P9cabI>2Ag+ouq+*l*MS4qqMBNfS_I79bf%-ajmIQtR_lOKb5x%BL z=-wsbg>c|3tnd38!V#8aQ}0NgDy0Tgqm9ZD9#HF!phjI(NY2f-TIuTJ+k}+D`kt-h zKqevIXHN8*AdMg6KcdQ^BqZuO`&{pj^*_8y30ny15Cz0g`HXB;@X?`(qym0xu!^v^c0%|!aQ;M#y|0)T0N5mIVtLEg9BYg|w^jDEi)f-KM| z3}-b%a&coDm=Caa zE3;ijS!9U4kZ1t^7E#>tO+k$J69Vd@u{9+tz?UogUG#cCR2wYI9a&*`7Z(n1`UEJ& zm=mV9yi&9z&~vPEPg?RFA6uCo`}nX%Esu*_7Vq{2ufXS^xll#VD!4MqJdV~W`QUD&Ub0QyxCKdH_gF9 z)`d2|JLX&(j_xbzL!F0n5-euSE9lMAY#4Nbm;7}0)-@h2s~B6q-$@lrN3z=(9`HR7KMs@@c=nl`3(8IJ zQ2ffM;~wI7F~46o{n|)4JWIbS?S!+31O}@m2GU9$%cX6?;lZX#XpOOdhcOrG^)tfBOf>-EB7j;OEp2o_A5DUs<^;zUY<$sbH8T#>SlXkOpbz$EVY6ip4_= z{4Ll;A?gOiL4=f$a@)@;7SY6ge9|CIsxCy*r(uTm zag2CAGGitZle2+;P>ai*-%N{8R(_MeXTvt5F97U$j|>3#tCjNzCL*aRPC0aW4V;|~ zHrg%n$9J~&td_r%kBu%Jwu<|`!m8u{NrZHq-4x}U%KW#RL7@6#yF%OgjYh`Z`sdme z=~5TatJ=4wq|yBrq>)n&>aOnYlH+_wH>bS{`KDuSoRAB=t&clX)RwuT)&E%9y`%=k zCUrQE^ZA4gU#4%w6F@MYc-{Drl>Xa~?rjZw1`B*{YnT#AT~hA2`MJ$`O`)L&nl!%? zP8!3NXZ{8*RTSlr{^i$JZFZU0B!YQ#-Q!sSZ{#SaBU&+dcM_cdF_U9TrNDQuu8x=_-B%k23q&_rL8o5k1X(pTy*@$dy*=OcF^n_#=>G zoZ`XvlOkxOO92%v%U#%mvQ%Z(FcpNC$*!K&a*2Jm`@c#@fH?BkceZ`ex4*j<>syFB z+^1LfRDoMEi=xi=x+H^uLOo-9*#HhUvxSUmJk`Wq8&7pZQ?6_Pw0BkP*NfDCF)$Wb zw4?g-^C!wX={l6yPm@y-Y)B`8^%wdhcBz6nENqMiNrU$-toF{@{n0OL#bFjs6G1N= z95#Omt-u#kggpfqG?l_?Bv2D`aTU_{+^yT$o4-ef+{fllKu@Zv#q|9)Ckwyl=9YS_R z_Lm5bTOQ`8$+1|3(e@v0aTL)>hi^aWFDqlR3;tq^U(c-U-~H{$@+}+OeN*&Li57YF z8w((o5Xk(*Y?5){Qv3?6EQp7t%X6Cn+kay?i|?fhKARzr-F*wU5dNLH$X#4Qs8;Uz zn_PWZB8WYPd#0cZrOQ``Q_|kQOG$?;>(oI%|F5H5zRAgvNJe~%xO|heXThT$Dt+)_ zSl7%=>#biS{k+Z4Q^KYeN@PrJn+}^DQwbt{MYFI2^kHIj9amuQAXn6N-3{uVs3Qj-oUZn9fkif2tuYvuy>elE0#&l4CCOnN4z_hTubMTyke zwL%!#CWzMAXm9(8T|7=illkmS2m5Bs5#6t#2Y%fzSc7iGzKf-xe1L{hC1ItmE2czi z#vsfyV(!0tKxR5l_f}l5?s_ZN*Yca2>6Vw{lp*Spo{0~alDzIYaJ$p9wKc)1g{4h^ zF|j+gA8`DlZBO!5(wC!@ku5KOX8Al)kLbx+yFId$MC1x1%hw-B=ftTc8XTNn>RnJ3)21A$>$b5hb@X0=!e4sd?OeLkml)AN13nByVLm7WFMQ2 zOr2_s31%a<^euU6;7XtpZO)FZ#s6y&#DLA%p?o1<>KY_S%%k_r?d?wu%zQ*US4C=- zqv!J7^zI8Mm;6YIE4o1MG;CVH9}q{96?&z7)jcfHkyBK))%k2cH}b7ar;tneM|63$ zqM{L~C7UEi->$&w>LMnyM!UJhk6FuQQy)ZEDlMCIfjw))Zm(>nj!Z<6`>j6#wUaqP z!I?zsh3IVAu-L$g74P)9RHoDZ5_l=W;`O6&%*J{WSrSk+p^dW)RG7MTCmK?M;z7D) z{D9%uC+!fPK7r`#bhPqooqpKq>dNUugZ|6{SU^|gfr@X!zB`_UjK4|5&+Dhp+m_2It1-c(Io8Oq-<3xUrQCo z4SP{y)+(H|KN^ifXizv}YZ>-G>3Go%>u$%P$HMs7k40t^I~1fk{_fX28BHL_?~abv zw@@>KCHpw4xL3oq_6?SomsP>jF2;#C_TQl|zubSTqk7WPL$3c6$KpY+@E<5O-l?O` zb|O)WUVQz$TXjR64Za{tgFfc-Xkohktyshg z6N~obXxBqr2XXK|GplLC5(JrD$tDuW4lmQn6$tt7(%1FT)3|T)UG+%;segU|fLo@P z&RpWhAauwcCLWo^h2?{SkxmN%ewvxFBX{j>4TZkj&96*boo#%VJi+9q@3;UW?<$ub z$H(-4BbV;lr8b@uCkXBIuTU#2_S6|wqS>QB+!QiMvH`Uhx!$2Ce~mY=L0GSW8R>^| zL!Xs>9UfsU%cMK5Fge7gguQ*C&xa+zT)=JO&s3jK#m7bL=&68_n942 zQ?DBgmphg<<-p2DnH<{d`P!%_9YtwVJy6>mn^!IKduuD}TrtE3a$zOC70^F)7)#~i zKVg$($XzfZYS;H;g~_|M)3_q`c#q_;G!u(=4GMoYs9IblUh%+k5G)kldrtix+x8DE zfPkNj&4+hCHPJ&1TlS6G5cX)M0fAfdM0T<9UhkE;?Tz)?Y^rSW#t#s&1p!&Fu(us9 zvw*lbx2EGJp7?jkZjE1J8GMk4QC&Bp|Tx>$~bW^ zHw$HiB)GA~U-J$~X>kWvl_#orKO>hpBg#M!U&e*p8d zocjO-!Zu7#xWqMTls0~fZLqjL2+Z8Z%e|hw>~_hESSn6{0vJql5Wt|6FYjh%C0vCH z2w>^~f9$h<3`%vYoQ3;9ZEU>a_*R0&M3nFmr9a2V6IZvktw{7GTc3>CLwV)h zy%)9yWRla8A^FkfvLF9H7J!Rf41_w<_hs-Sj2N6D#c<|y-;y81m!(COiU4)8Bj5$u zoEW}GS8j10pdaRJ=JLTUCwFg-mk)*3Uvrl*!zMY&1Th{^Fp-oRk<6=WvZ;!^w2{U^d@@H%d*S4 zvEIWavQif<4RnTI9btpeqru|^}=Ryj#92I|7mwdZp_F?oQZqhQmq4~$dZ|H<5^|e2W7phUhJ4)j$?p0+2Q{T6p%QS;a1iKn-o657A@NDo%QqrCb3T=ClgJcPN4BR*q-Wm@)<0A8r6>ckR{w4!K5BG!@Usm(af#Mn;BmB zUuYCKF^|bphDN?{!OmTvm1q|yflg|3u8*I&0N7jOh^A(^CZioLj<|huhi|hF&MvmJ zd1Xv)0c*gseu-YD?4z^5u~46ADEy#r^=&6Uw@#K9P%mKv z#T8HPLfZ#wXwu$$F&>uSN9YFup~w%#L}2ieXxOLRz1azc@a8z;QYT0>?X^mm+Vm?% zqxxG+hS3(yueOD#!Q4OcB9r?Q#AFpG<*nC~`Z57&INOt$nB4sdq+kxty(QV{IW z_2y&W62m+Ln?o>DuRB@Da8nT@frU{H35mujhDWlU$5^mng7U=H z>^Jo zh(ZxDvZ+F?_HEs(K5oWWo4d#6x+Rep07PQ#!NVMFFXI<545}seFVFEjJc0pz|5+Jfl z|1!^9gOmLjt=??%jE#)+Zal&euiO25gyJ&f2B9ammp+ze%GR9FVbpveUZ7Fem&`mD zH&MASV^R_9?YyO4M!Gl}h&Rs})u}by-D6VD?ULUjm2^4`QYEC)iO5Adg{dE-|Lx1O zSeVd;|20wK&7PuG*WjkUq<`A}=@XqRI%_h@M|C*1S;Dq1BI?ttk;TvpfwohhPk(89 z{ZWo0H}ZtVr=SGgzxRJi#B3jJ;?RRQ+V};@bzR1Tzr&g6)cqR%%NtViI-ego^E`TC z7;|7lC&Cl_>*@>AxI(r|ql$6jHn9=x3@4)1ZkvM(GeLN1vGagTn#ct;3W}YD_ zXeQg?{>;CFQ=AlVdLBVkeF*?I3*$|Y4fx|oKS@G&TRp3;wT8fKWnb#5NxdCJ6>6LD z#627G5<*LAXN@BsU}I_6h%*;0@qf@FarM zgNNp|nG#s7Xkj6``2<%$t_khne-za4jWCK{G)(ZJg(XaWJ1;jE_W|4s0hLb>bdJQg zN#c0&?#5EVnnx5ab~QRQX$L(&YLkhTReKIkx<0Gq=G~A6?xAE=!BDP(k#8Twi(41^ z9LwnbJ|h~`0$RFe5J97Vf7hTeQ1)(gHr9aAh=!~EasL)``4{JXtxAf){DU+5;mii+ z(EzvqcD2lC&h_`>kh)jy_u?cgvT{$E-spfsj-K;noxVOT6!}o~8FAy;eD+2Aj?o}i z1H+fDR|UCwTWZlQ7>O;OlcIETTNa6`ZatoxF^+sxEChUv9twK#=#+;kT>>F%m;n&^ zr!-5dfDdKn#+1)K)VX^zz z$0>oxg4%3O?u@PD7tvmtU@Xk!^%9L08PI)cr!)M63Frr-wRHg}3td9lMY!?MvDaS9 zQn4pIYS$-df3D3;9#WY(U+*2ABms)>#hM{qD5jiFUt=W!r|2CX?rU1h2iyj14Gw1n zz0#$Lx)RWt953TFh9Gl(2YM6jqVZJGhpm|-A-l<47giUe|G0k3CTx89qC(Hv^o;xU z=1_sLfQcc#@`-j7Bke?&U!tZ0WB-$1dsji73VBH2VyCryX0!hDCpW8C*C!zz`?<-# z0|e+>pTbSClCRlKTQtcoa=)(#U^Z{b3YnUdZ#DtQkOd7v-L2Hg+5KrcAKVz4~{J#imYT8 zE}Tz}96fosR9WcZbn5k@Gp!Su_S|AjNw6G2%@jVYbOy4oAjR`*&gJ)HjV#}%)2Q`gDE|1JIv3GIc}gXx+1w10iK(FL z|A}R!(?{3@DO89{&8?KqQ5e({i!{9}VKf&5nnErJ-JAxXf8Hi0BbA}lF}W~Dyk`;} zDTn%@8&2MzciMC2WiH$Iyr|OBx;PxYreB9!B(ekT7E=^9JT6G)Q36!nUP;0N)N|AK zT*F%PU50oH!BAYhsOIruF;7;!j&ANm z1KD|jwb#i}%~xlmw|&vA=4ix3$+dtTfyu_#c)H(~sduYBk% zl=iAunHq-1=Jvx{f-;KJnuOf!<_2Vyd&o%iIGslM7GMb5Sz9${pV;tT*PBy^oK&)r zw5gr}+cisprJ}o+z*ImT-N!r6rz`_f^P5k^`FMPr&|!e9p9^ zI-f^OKj2T6YhM&&n#MaY)QXvx6z3`|P}{fR+ucXJmiBHiv~_@ zNDCiytQIn?lt-zCtQb0%G9&V#MtZD@@N_0Apwux<>|%AgxL(<0RbI?kHji+uMgN+| z%4h7p9xSJ;vXJp_ztnwK`njSdBo)5P&c?ThsLgXo9-tIsE%3|1yst>Hlj1PgCwuT@ z+H{)uR)4cUp^EA5`}T2*fE%Wf56U|bEwu&Kbat3=a66*ggS92^FR#zX6gt0a{LJ3g z{&Q45eRX>?_$(n6QJ!^MF^XO3PF5Ef#|E$w-$n<(qQb88R#qWem?V^xuFlk_( zT=2}?%ivTvvU1GW5&4@bJga-DHt(6!bgDYcy;dND-Bm)EQ5cvR5;JK)FmfxOtZLhNzfpxT(xfZ%TFF$X-28-$;JTWxFe*3J}UNR{* za3Y^=-6HbSU5v0J8iVO4dhoMOIQrCM?*c0!&*-giYm)I*6k7Obp7oIMwva$3gS-!3 zhXBOO9X3n8$GRb3NNLlBNABw~6{oO$uP$q9#^-uAY;n*F^pDZFWB_LbTouBb9Fcq(b*eoO$q>2h5V3BpuFxKEjvkkO8@G%G}Y$pu}yZA zRM@Nd(7%Obp)T%>F6|4Ei~B8KE!Zq*8|Y&LXEZHW(&o5sr?EU`j71g`7vE8$zbSm{ z2-&ON^3=we4O$oU49A&ppWq!v^SbPlbn1uwnEJ%&*IEpme>2)2;QHNPm;E7&o&uXF z>P`bxT(?-{=l(IgZrp(w?@V`x?76*aX+wivBSkq4QnwN_x&=&^ZYq7$hun%|cc~`a zmwGBvxTKQwSq<_8oveI^;qtb!4zMKZ&mdVPtcJJGP&}t@>MYfAdhM~Ve(Ys?t(@} zH|!%QQIQ)URw)k?U^Gb8{;pTMq|c>FXs=y;En+3d9PV| z#%#vCE9EAGGx>FG@DBzPE2~al{?!EfIFdX`GW^L|V)D7!Ue~p#xQ>(p)E&u}k@wcY z!YftQb&i*BxzKoA-eM2-B&Rx%M)^IL3LA(q6mZFRCe$FHS95c#Od}?js6tUkgE!o{ zyQN;tn5ObNY`B<(O>oP&`0HSnJ{Nb?cRS>-LSeohrP|`+Ypg5&N$#y6-$#YrF7A7V1UBTpYbnM`-J1BLWL962Iun6cO?JlYxDQANX#gtp6cH9kQy^VJZChK7>oy)L!1+%6e$hp#QN z!I#OOR_N&|TpyiOQVSdHzL+VWWBODfYb#C-;G2RIhYL5(d@wO14U*#f$fU@)2@il0 zCC~jA6>lP-RlCyxqs!OZ_0zj>oQH~$IoT#})-*eng&2Iw(Y`Q{N^c+pJz2JxAW?Pt z_ATd_OS}hsX4#=lq)n%sJ4fc+-wuz1Z?R_)FTeo)>mQ zpXzVF`tyA<+D>?Z%34z3n`x8(-r0)hb&B&Cup(o&44 zlGDBRf)xA<3l9&^PQ}cJnS{@*-k4_U&!pC&hd(IXoh>VLaKPk4G`Rxpw#fIXraxK# zb<}$TE1S}gxGj7HsM1(xTc1&@s!=lB{^%|=+}dEc_E@{AbFuF^n_oIWH1Z7|4-HM8 zuzX&YwmBmYgoI0)h9+qkIH&nQ_J8g}(krxRmioEJAvQTVc2dDOU5Zu_ymH|zlha{T>5Zly)ks+{b!MH?mo2JX;15fFJ!kvjz)Ce*!_DOGUX_fv|#dQ!@FZU zdpoeMuYLzsmA*k=$C81#qtK4OhxdYy1*3VfZS>>!C7mAu(H{DrP^U$udLv9KKiFc4j4I@N2cWvJLt>=((MO@$vV$#DJ3 zzc|`efXr+jQTd{g`HIDP@WIplqiRP>RaD6Pt8*Ubqg(XI=xDo%x#@gc?cX-8T_dAr z+zn#M6{~cCyFWNGj$kIXT0(@sJDHICFHVPb&kZchTpj@Ed7RNXE;%`I{K!5iC^Jgx zoE9aHoL^dA;PXd(sSvTE-qi-KvBq7I@F>E2Y#A)9+IH__|R4 z6E#>pur8#Wv|wDDy{=j#5v1&GLJl~8aRL-7IG!42P&z$*_hFrJzq)3~i%ViePA5!aZwJY-;cyo_YdlY!P`+RC z7VP;-|6Z+t!zh`Pmp17z0Of;1eaINHlSt5*d(T(m~ny*jkpRv_h;^MJ}xpFmEUZ!;W zkgLLUOVLgi=nwM*mY)u#nw4i6HuEK{>z2kpfEr8>F9PtS-qNsp!!T<-k%ay=cb{LVU-7lF--roim42%{$9{8BHvmn07A#Nh{3;oT=LpHdNCwILp}NO|>b4O~*4-$Xh!?d_fp zK)+a)p_Yjw9@xq)uU$chpQ!pBvAHu`4L<=`x<-=7`Pwh{=e_+C;Ao!~$%NAL_|Bxh zTS;-u)UHNjE#nWDcu$=U=gkn~ey7dhWNmQO5FGIc7XJ=z$f6<{J5?r8Bt)zaf`hd) z$=fCcwrBK;9>*aIf`a}o_J~!(ET_V?0X;oEh6&*}5^0+>!9m>Vis;raSRNW>1m~jpnTD)yRbp<+DR(onainRpZ;)*Sn{?CK{yDOl}m&88El7 zmfIqXLX7b~*C~X|fDPa=EBb%{=El&Y7`xTDqXv5$MDc4^pbIY1E=`$L6p|#T?57_L zAj$ABDKP`l&;S+D?mC2rZHl4zvms}qfz5$B3dsO(>D&|vfl~X1q1QR|me#UdYsz5o z&y~EdMS1vVTnQz04Gij0f7u_l8~TX5@q#Rlrm&Ve+fJ6a?1hEI&PGLaq6r}p%LKQi zwx7oS^clXkX(;=^@cmSUUM#JE zqOMYKC95;h-ZU^JATWbDICT_#S+J(k1`P%MxIOGL;2UPC89*f`JOWtc<-*BLu5!Zy z9pX~-QP7Ah5-;J0rtb{b05UnDub=K+!1;QM>XRhhnbcPyf2?=U-0shq4#Spp(EDL> z-hV|VXVb^eq@Kbo_v0Iw8Mq+tLs}MX>I)ZTz9{LDoj->R5trDO%VfGM7&JLfrR#Y4 zmNrXZmn+e?#&kz9O;DxjTB-NV)!|}dC|p_GSK0gqCcZ&mCu)6aAP@WHkdXm@2*exu zQ%DAx0sY3_%0X&=`fOZu}vr`Coe z)|my1lNzB^z&riq5`1x$3q&@(@~c0B_+Rpe?YX%+_2!b2r|3v!z#t=)a$hpMrAn}wksS^ln)5oXxK)r-tr*!f)2Mk7${6~TZz_p${=1Dv z3)hg?zKv;Wp@Q=Jo`9qagBgS!q(jN}J)-matL5>lO0AjdrZL+V<10wjGtv22Y-hQp zD^tskc3D;ig{3noUNLo#ZXA_egYUY@QRNYKNiDsc)`YY%qWtjUWDduwWkaM|pw6xY zcT?CVyI4V{!h7&w(Pthv0{&y^xp_?s@An8s{m0f}STXF6QY>jZB#L8{ANtV8?Xt&A zz;X|zVgm57sOa#C!iiBaJ7z+1>r15?I4BtS56c!ALo}#_Jv8a+d||>;VR^N``k6*8 z(9B()$<0N!?nqJCL23})=6Aaghuwa2!SqNWlG$YY+3T$Y9HdW&gPS@?zP^0pHf?Xa zjaFYa_094#1)nH5yd_(rxNPWE#<1Eufpi06H;G|j>j(N5htu@>eZHgKkd0xxuOqvgKO;e#!BTUs^`$=j!DB=-NI3fVTvY2OKwu zf?iB#EpD}KK&DdMzX^(F{hm>S;jB4)?>FxV4W@$tP|&1j2e~(6xpLTIMz0R5mpfU` z{h}mbI$!(zQcR8TuZXAltfL2>mi~>JrZ-5udZ&8i#ho?JFIf_k>Mhc_je7nBU1Me| zYUr~U>1o+d83E4%liY?%qS|he3eZt8Q)2JVRE!$^X(2JPd|^xPR~1k)W+0?SMCS7S zpQ{k+5&SxqgGN!m$`pF==;X>J?`7!efnkALb-4_-qAqh@c6QryHT#u@Py-7K1B0`N zZ}jnc>4ZmUcW(QRKr|`@{KNb@2=cmLp~}4Z%t&1NYnf}2@LG3`nQyq}1M5Srb2hLu z@>eYGlmRUb^26`F_TOKso}^gzfUH&>@Pn>OU7jxxLHZ!of2!bXQr<}beh81Q_y+T- zm+lAsSUj--=@`6*XstOf=$Ch+ql)s&y8jeEPCfqn&7e8HlR#HbJ7DpX`ldGQVSlGO zj{HJsG32Gky*C)(k<=`=1pm>jNKcUxHCVo-DJ&WLiBQns@HQ20s*JWRqMtId#stku z$D~~J=0TEn`E*hZQJeU`?1zW3`CvdhkN=Ma;2Aj+2zu_p68SkW-1>()n!e%Afh(a| zD|tdifwZztwYZ!RDI;rOSOK({DdU9klVh}hh=)%1Kll2~bqXwCCHNG*CgKL1uiojY z8F;2JR#2Cq|FzLrIZ-*_5i;zUu#_*hw$r|PP$w?%WD`_=b9!I48O`3%Ty|Q=?t9hU z9o~^`W@3(Xf5#I!&(}m;T~6-D2J;eO{thwaEwgU09 z4rjTM(%k|vJ8MI+jA7!cp?m@2i*=9S_sSFdMIE6!vFPwV^l2uJy+Oql3NEZGe*QGU z(${|7^}yAW6pI;fY^~PNvpdW^85g&kuF9?`5y*Nd7r9DkV%GwC&AsqIq?nJW&w<%2 z9jWmQxNY3Rf$ELAIWazy>#0Iaja1~dwvA3ibrR8)DHWAJ!Nub*wRIX7;}Iqb6kjZA zsH7XAT{&+|lqTn2diq#b@<(*-zSZaKwj41k?HN#Zf0$E3|6NV`&EK?zcUIP4toM8W z@*X|?Y53aqm3|5Ak5=3uA)tCo)5u%J)(>*f|Klb}Mdc3_Y}9#;A@9F0ja}3}#`5Zh z=Rvn%5HyKM3oHa05KCV8g$)mDPavM5%o8e4ab)JRkRj$;3N_DmX}&Kf{I?k)uP=N6(x;}+!tA;d3e2x0YUNwAvoB%~c#A*YY9Wz!C(HFRJ zgYt}INat4qU^kaGXyj^ra7!QRv@Wwn z0Cl23w$ba*u}5m^*Oo4YU2rIW7LuVERLvs}OPYRRdCU_)EfyVWUFo=^qjwTCi}4v4b6yPNxSR8*MWRtkaeaYET(snO+Hkew3E$J-I!xc zNOr^Nfy5K}8uwmW{l`aSf2cZUNpgePEHx4I-TV(3kY`j+)%CfJ@3YgzdO$5!b#;cC z)QQ;350P z@y$-9-i(5&70(M%QDmxig`n-C%-8;mm$?emC~<A2O~_3d8nnpVz{BTdxkqQ{xZ55d>Df%*r47YM1q9kzTf6LE zzP5K?xs20^*SY2n{Wxk`!lM=5+r2QeFoPv$b~@~q(D|`d0gTp^>|bs1G18*k zzUtlr9I&y&UQzGMsG7PliaJqQx##sxPMr#4prS&@js^xAmp^}EXD98xQs&Ns;aJPh|_o6IgMQtHT2<&SEbNKvRVQ36M_y>45+(VYi~`$9j?~HhYWSM>b8MQj|N9l>X6OB&_XWt zSk=1QnU)udF!MJQd`(UMJCEf`xvgrO)~OAw1SGiiUGsq{OG{BpQhdOo(P7Y=zop}b z`;!RSnn9Ec6+K|;MBjE{`b~(6)^J=uLp8LFwB6Raz`_1TN=L~5IOY-rC2X~o1J>;+ zA}zmyr$4C(U!)JI>Lzh=Yoir5T6;OsUOB3=*GS zQ-Hu(#5QuX*JZ3gct(t%AZE(d^*|Ydg%}x0he3)Rjy0_1-anPmj zZrUx6UblPuhgf6onip5lVfRQXZMYc@*l*6?Wny^NQZ|Ox_3KO>|T5t2M&^y+t`x7 z^I4E^t1PW;`NJeWr6Jv4i5OEgt-@(|Y+=*LiWw+Ofbt#+=QL=P>&3S+l4SL}_ENS^ zvEm)_d^gMepJ%330<)~a3S;8=vh}sQR_KU$HZ5@1xH~x`&?yQB;BB~?{BH2b>S!P3 z2U%1_=lPr1^=aBKG#{y$D_1lA_-I&OF*kil9{r$__}X}DFz19b4$1E(HXo;gGGhOh zTry26O8qJZI~x*td(3Z|DJF7&*V~IBR%T&afG$MV-ewd-?zcUMBRj4q^4^d--~i9r z-!_4Tf*`eqVW3~Oq9T`+$bT^i-d>x;XHm@;cPGY_&(&-B^kme1!dyOiDlbZS8%;xo zxvikNj&v@;Kl?5b0wP&lqImz0rt@%T`+eVjjaI3ewJCy7J7(?LR8V`bAof;!w59f* zv0IcHr6_9DjG3st_egANZ=QU<$MgFSa^yPh`@Y`Sb)K*Db61=~aa*pDQFMpQ);9T5 zOijvM4=pVY{rC!Iox%!8U87+lna}T)y)!a1e<12z4caOPdY-nBzEq6vye6v}yj#%J z0wyJv5ISU8DCLI`r#l*1IhOG8`dH+)FZ&`()$L`55rJx_gZL%L;h*9$Vzf$+mt(SX1;=d>b_LNN9L5aZ=9NviOSDt)j_VS{ zzam34Ra9YtS~yt+9=-r{gob<_kZHZczJX+^yT|p9(_t@Dq!eqphf|L1J)7vq6>-7C zmwV3ho47RGypH{+h_A@Q?VozmKvUu(3fNe;RPzl;)2-xy-D;TW!S-+HOMv%&UFTG{LC zf$|$x*~bzPcwE?V_3*dv&9uDF$-G%|;Ji|5|9IuC4H{zy-m}KAzQz^PiVEct@l6Q|?^L5KonPF{D)TQ*>48 zizbTA$`jG3T(bbJG;(@k_u`z0`JK}+Ta1wM6^l1w*)!SyswMPb=T_(~gdpAj6+!&A zcH9}@jex|f6-tDV5=)F{qLxO83l$)>+|wTOBYB2+3SydDKXQ7) z6NZw0w<`Z`y*S0kN@Yc--ybZQlRQ1i4Ms!xPe@2l+T1+H?83Nc5lAbiebq1#4P@dj zMOAw`b5qkRT_E+_wG@`V-;6)L7$qPv9xHL8TFZjhMfx;uj~GlofOBQmP&xtJZ-9+q zzg_#lfqgv(HQRh){C)_6$1gI+gSOsHe!WhnEz8{Gi$n(dB*%#Vpg&Re{I~QnyqT67N06 z*1GwfcQ5dpGWsB9McoHtP|e`K+xOS(n40vYhv-imy7qSR@898vto!3mVg=;P`PkNX z%=dCIhlxxj$kyDy3b?3a7&bK$IxDeWP;w%epu=Uxkv>0FRWy<@&S_4hPbkO&Bhen< zm=IT5D;BnR^})vx&BR*Vc@v^6vFFzi)i2Y21jth@aqQS;Nh2~f$uC3-v_ zvRL17_y&hG=6b!Vd$z5Deh@?*KCCN>#9tXAc>(FsRK#uPkx#Rla*)3}E7Nh%HiO%t z*Ap8X`y5muPcu*0HJeRH+Fxt~d{DUH#+>M@r7H^`pTa2|^ZWhYZn_newy*Dn-6n4` zMWP}g`FlF=bwKIoq{@2$aOBWP8d=RP-65eiKVkD1%aY=kDGPi z-zb8#HPNsL|MF*oYA_RlhK%=63?tdRfeR~aPJ^emjf@TJ@7)9!0|FNTL8s>Az>Y;$ zCZfl39_x9qdI$y!hj(FYZEY^)p)2+MZ%a&cI*35NQCW)CfP~qNC4}INKSAFCk6Ch#CMeyFKJArV z2)F4qO|l{obh0gOmFj#oex?8kR>#DeGntZtVq@QNVy2IP4^8!o+PtWLBF|u+8p(Eq zymM(5!%R?BkMh*WdQ{x+wvF21Tf)0L{I*7UNJFBfjh4Se3~C(}X?R1~YU^Mo{G-9~ z_d1>}V^&sDwk>@>_naK~R${AY0{g@}Tq>_$GRK9aZ*7y$9J4CcgyN@g3w#6z9QjS6 z#&do-=JCo~6Er%JcpwAssU5&TAoiXg$79g=CX9#21=GIt<@|m}6n(X|D(?|8`zO5O zCU%(D{{vW)TM4@^i+AQYW^k@w*gX8BQ7gyY>)Zy%`EqG?MI`6q2aYU4dn2oILr2I` zj%A>$Yf<(DD=;|w6i9(5rU$hLw<<3P`F9VUt3z{zppgumsUNnV@GO=F2CBM{X=_gV z=C`~*DOsYZtn%oUF9uy07=9!EGp$8oK(*GGCAt+aG2n$4K~S^bXMehySxHS#4}^%X z@$Ml<=ospSM_^Gj%r-APA4B~m1HXj6X;~lqUzyCWdt6+v$|~l2jsU2GJ({z{!9^2I zenBeLl3~!UHv9#K67DH=EVbfZhc)UQUY(}T|l{Jf(SGg1wWn91wX*&I}xciHQ71iE1b>hs4IfByWwyfQF~ z!Z%U{obM`T3}?JF7VI%{Ik{V2oM$HG$Zjctrh~2Gm+lANe9YYP0iv_g~F;uzwigOwkUHh6m3H0jwz<3;!pxEPdAkbhyn9X*X z#Xw^j;Pb79Ygs6P#cNbEfbs^AQT!(wRtr+T7|sRpJFdmA#N%oNXbYEn4&l&MVJ5k` z3dw!OCSDu;K_T}o-=-!qme$0Tt@VtjC_*7=7o(mfSoT$8$Rc|3S{NI}tAQO7FU zFa#=J<=3}gzfL!$baZs2SmF8x@)fKD&~DF~%IF!3W|80p9{)0A z0$j}g20&oLe-_yD&PuUrsu&d@_S3;raUlyZj^QzfqkCBiKccer8lVm=DoFJG*vA(IEmDHi(>#cuyt#DVj zen#bWV&cQoE9gxyR?*h=szd(&N~RA1v@cCGWJWbrXuO&WsU7ywf#$txl}dEz3`Yi| z462Hc6~C}qRA;1e4ZsIun1CR1bf%dYd7YXFTC&{$YP+MFYMuiWf?`Rz<#Z%t@L3f6 z%qNAWqw7+zC}MiNG6Ir3R-6b`4d`yMVWzC{{IH{b5!d{>hLnmwhm%ET@p zF86Mh%;PQns0ef%fh7*W6~c%ey0UL?Q`75lkuM8Ttcw9F2oqzQcvX0-l++LsSD@Vx z0tpcZ9g#-8;>Fm*>FooGRp5^CN>5=Ir2)<&$|SaowWx-jHauP)Qk~>59tZIg4i+ya zshF^KB)z!fL}!FS8KLoUGw|-aeji1V+zR z4J83V6H1KD-)gM>aJ@XSw#>N@@Hs5x|NB#6emE%vdD!9nTee@cgmahRj4*Pz)2gzzWI~71gc%m|f z(`exEaByX6tLLY6Ma}7lo5Om84z|TMtk>k>WfULY7K0FjZJOdl_w;0S6~zh-D9;Fv zNiHITs^%#-2iFRp za8qf<6VFmL&q$b2lqy*rL}u!Cf)vEll9r>~^v5dgUqfw@r?nh(8jIuvj{gyf3SGMG zo;$9q*PP0iVllsGkH#iZYtf-yL0D3g^JYxd znh5XpY~4MTmviRYWACFHZyKJN1IbJ`WgT*tSG>Rhkx635o={xTDcIKKcGs~={K~hz zejOJX+T^-O_UWhu)tBv^IuQey08gDQ#4{6Tf9RRr(H(m=M)GiELx*9HG0L#V`x85+ z_IV;d2i@%rl3Gm%*G5UC=?`R!)`fZn|M{Lq zE>Kz;s(F#fc*ph)M3cqH&^?wXI6j_dCoX|mk{JM!ZU5Gs{QYv%O4Qdbu3WXC)L%(- zjjUv27Vz4RT=}*!lJ@!7_5GQ>Om%q&R*7usI~1C-+`HT~y|Dc2PN|&rU_U+e2%W9T z5^(-_dFG0#uA3L49N7aZ&0~5iL0|k$5O5Ih0HY(g)VIS+s=j5?^-@@tlNddUyL4 z$j~&21)W@{yg_&Ebs;iiVVX)5Yb-Fm`=eyb9QH!Mj;3> z)2zW7Y69^D5YyGitad2zYnrc?TPqXT)2P$L+<)WuloNa*YPs$Nfuj0tw4-z*)d^?9 zpKGyhhOamsrgLZsLoEltrlF4i*`@xA@jvCakO!CdSas%KQymP_WR=nId{r*xC4b+9 zi7I2USOTR)zhX-zAIPwGa(Z?o@RsdrV`OCG+khH*klB*ivI`1VXyys~)zqd45LMfY z3vyeOn07y)|I6`1gFU@1UTfeEz7PB+J}UQF46`GpD<#@hRB92?GR{df@#I9H)Y)}W zE4CO#NqQN=`zsUH)JwR}YmMJ53ApV4&>QKp*5&>@WtF9`6>OV^(4=fl3j>wR;mQ~F>oZkok$(+sOXlI>b+^HuAsWsFMnOG27 z4#l3GU5xVqzVCSV-2eWz(l4zByH`LzQ@V*wyHk7@n|cx@lGaZqsD_M(pH7;%1aiS( zhh>B&x>;YPO{VL-?(&A`l2pGXVw8ngK;{#+G6ag9rN}n|uOU&=2wibJJKK{PTj!}| zF59so0)0fA+94(_xXuY?8UH6}&NGN1#ArF&MfB>@&yEKp38~oR)@2Wj?@!5Tu^qAo z`fbereuC*}szL~$5Zxt>oy<(R=bpBIrPSj|8d@#G=jbhhNav|uZ9Ah{28Gu`R13;L zGKkZDB=z;JYl!Q?ed5E_5u?`$-Je+PpS*9i-rwIF`K(>KLTTrhzB%-X&NAtkTV+;Y z2x0gaxN})##;{^gWQ0ZyMGIZdp5OB+L46|D2TFwEa?stGyewE!78#*gY4_L9xuK*4 z2n3qWGE+u=^!(0Uj3ry!u@jDN)X)3p)J<8p-GP-lYcDj;#UCMW zE5<*$U|!x`EZHm2q`V(5O2$lK9^-JOyoQ&+spC!TfRtj~FZ;?jynZC)6Soj99BJUh z_kHO~Ls7Bken;L)7D?t@5lMgFwm|P(C2!k%yd~o*&76Ao8saESSHrman5Cr$^YmWE zWzKv&b7sU8?F`;}=I`}qyrF%~Yzp>d43^`dI@!B==@cpRWEFnYIgUpRe+zt9e|sBC z=NlOrSuKP2%aX74_xE2Qe>S{dX~HE->E&KE89Mm!W#G^mKQqG0*g@Z)RwyO&s=tFE zZgw&MBqw)l^dMOykDZKe{z&Fe@%&FLlEKG?qn+Ju^@)uL_=|p_f3F9bUO<^8p5wrH zr*jm$^bO5F0U#MIekt{i-TBF{nwIwBKZuE5tlTdw-1gDgp|G??Vha^>Cg6ll76Vrgejj;{ZRWc))QtIrz*x3$InN8_4<`WDjNYgY8 z2PPVF(v)Z(95$|_Gd^MCR4aVV+Q>@EuUgTN>lU4=5{EZ?N^jeQGO}}U(JaIxrF7f6 z^fZmW9?1NtdObD+70H@Mx=;eiq?Fk|arymG;c0z+X{t??A}K8LEjCL(O~j=4Ha9RQ z(DB?d=HT@8u7pxsYmi>W{*TJ?rJD7;m>rgMpM3+6EHWkNnx49UfNLO@;->y6iI>kV z5WmIajmHke86HykA#*0M2L2MA2lg3=3$Oj}qQn6mE;&{;ir_tV2}e18M#e%mzFy@cF@N=`;Db&P!i(ka~1xhUdA4_h=&R-nHa_U>MWQ4(*mGxuPZ!R zGlqv^8$ZGbRsE=vdy5md`^qpM2z7=()GpXAHKZj6XdD_Bx8E>+M+Pq6-nl(00f3a-FI(%sPO@vQt}=i2e@dGgwVjdj2HJNe zlY7W!NOq1P!lmZS&v9FU-_f^bTq)6%zI%a~R~%`tua~-K+D3@5yu91+=0G=?rlHsy z(#z6$ORAJhPkeHF@7WlyZEf(s@$63EDCB9#C<0|rt*`$ytG3t1 zCg#O}XIl^O38~Oop$KtQ6M^zIwGCwO%?tQ~*0d9d>P4CEE-%aUJVQ#E_Zill2CKO|^GLfToX&d{41KYRDsp#;^h zL+K+^R$m9x5v*59l8r{(=6!X*n*#m+{zRFV3}hSMgnD@GQfa$hl8215|JBU|tUz)Z z^FwPicl|AMy8UC%~tV_%3GeB_sh`XF0> zo)J_en_D6!Ma2#oGl4>4&H(mr7nIhjxgKp&$0CiU+Sb>3sa>AFlQZ?CA}7lKe0`k7 z2%Q6MEDx2wtzFM81ex(tRUm~xp=?t#GB^zWSYGezpN8J}o#ZkCfoQ1;IQa2sziuFd zY|E(k?IU*w@ujKr;F-=Ep+k1dMBbz<1x9iwrOhJM@;=RcT)t>nX_c9^eF{=?`VC`& zfPhwN;?mbdt@43X-CAE|Hs6X#+HcAor^0GOs+`CoGC*fdt*&xED>$B$8i08=c*~cTCzM&s5nqC=Rl_)^orl==3Lj9S@sF%s+aO3dp#Rld zw}_H@tsUC!<{4(A@%K%2S}7Wbvs}8+=Z;?24${sEK4xvmz%6qIk;&vwB`0`^dU{5m zmJDBMAl6CSvo#SQ?&;oj<8YG~PWx!=SSji_YZje~_u!kJ;#`Xim=UCk4LYsZNSw;Z zP35xK%*JvFuAka);Sm)Dg3HH>j)g!nm>Knoso|H}+z(d-{V5DHcv-NZcSMi_sO}C9 zc3wd6*AZCxuU_y2U2=u?JRK1gb$AumLR{u&2MFX>H@vudr5zD!mO+<=H*4?b?C!p| z0{a3~E>9f{N7NvMm;U;u(B|LRD^SwX`R$ychVSp<+WvjuPCTQLW}?k>%6ylVJ&E-6 z*X7-?fo2plZK3aBcEk7lL%yiQQY=i+iOw)teN%h?z~Oe>Oth>Et3b5D!>`s6A5LYN z@$m!|KFX;p{#rKwwiXJ97SjwJt&sgOMwH60z-7SHIk|oo6MAbaPXdmlWA_(feNSk{ zY6Hm{r#e#|8ft?!P6S_v?&Upbj{Cmsv(+Q#SVjJ1RI7gTB|*5b=QQmb;nWL3v6gH3 z=SC)DzYOEg<|fqOd>$i1El-T$a(5{a3E>}?1>*j4AE|v3>2+z!r^+PprOg?bLWH#W zC4)l+F9O<@3Nk*eS#*?!$i53xV)SoolPY*(de2li<}C6K%gJec*W_QgiSryS{oNil zMY8nhcR~BQ8Ryr<&JJO%toFuRwzZGt-4%h+Mo%ExY!;6h&mAX@*_p3yC3)D|19>a0j9-=7DANdOSGuBDE z?Uo>>*K2z#N4A~6Ubgm{Hgi}12u>6NV%0s>2)ASAd3_8zF5HVp&_5AhH}~Itv)ye z&f{NTf_q^8xuF&Li$2u-6l(x12PE~f_;22qI+78bZ0IO}ogDp`%^u@+Eqm+cC?G;Z z7uO3ds_6vVI*&POhb(jfPHHlQ(L{fbJDKdnT_G@tIFaetGwZ^iugK%3_6%ES1!?|r z^T|#ns@x=h0D}>-NGU_^xv?5ik=5zm)Uq^RiC&pZ@rK{$8oAe;$wJgQTG@@UW-Q-F z_azoz*&dlq@N)Ucl*z4ql{z-B2RAK1)#GhBs$#rJ%r@Ojz z@TS4nm#z4o`?o2nO2UsJRVE7HH(n1&WM`rKFKqtI5m-^XLMiFD)B)&A$T>#h9xQ6C zqx}{hM+U;zF-m=P zdG?(@JheD&zUNLg4aG43hdjh?ftK(&ee!gIxanO@O)@DXbM@D$18j%(`Rni76(ygBsxCO?SYidm(hE=!D0=YI;*cM;Sa|!me(qo3t3X=JwryRV z9EMPQMYSYi%*9^pkD_Tva86puBTZd*k4@w$Xj_fUYf9Ggri%GojZ#Io{cr?&Vo1q# z+AjU2Z|n~iL^t_D{Q0UunieZ`$MI`9&(?k{QZ+Z@f=WwhOFf$cv0QCqatm4wq9Q~K zC6A47f*|6-wVYCF$oING_yA~qAo><=hu$xvC@N~VpWT$~rc(+sdJazJk&NzL{4$Yo*MDBjK$JuSN?#!Hk;a+EO=CzomBh86GQ| zGOQ7#aBRgS!S__1?jTwxWJwNDrL0r29_5us{uHp;{X&2=+SSp;l9cnRqH|@=33o_$0?q7 z?d@yMdw=F0_&lM@UEj`2wf@-icv9^RNvaT9zg59%>D3K2Go3mISejG0n}n7eTd*Tg z(NiYX;YuT!4o~W*4$kZ4A2a88YW)r-MSgUAwmPdrx{k|oEGIpC3mDW=D8}DH{CWz( z)|;LG{$ zX}l@|%em(Xu1VEpflt%Lj#BdR{LGp{Q*F?j54?{~Zds+2TjjY`Z&V7j)eqdM_wJrx zldqbb29f=to#7eT~BAfVG92X@oyTJ+5;1h{*E#$=1IA5rY}Ky26gE= zA85uxW}Zwm{jXwcMpp`ZQS%87V%0y}8-lKSFJpQ%x~`fMnSfZSQCE*gJu4Ol_V&V) zM;#JW0s$ZVvl~HDSRy=bg#Wt7Jw5czf4|RN){Ly?6WVN{5I3tBC>|Sd+DFBFBk)?i z@Oei5kE*WD3m4ffYFF?~f@muJthP2SwEwnJ=I8F(z)h@4qRPWrQ8 z9=@Gz@)fys#$R>iqH*7G8rGBFiKM)rBY$>E+bC--E$(l5F)r3SnLhHmEm&XIB-Qm` zG(!fdhM@OQ2y6Z!Rfa8?oLg3}( zOR&&W&UEE^S=8(bGn8|F4z2;6QF!k^Sw4v=;3_oSy)ISG>RPENMaJPf`SgY#G!ecw z^?N+hIPbk(U<=DZW>k2J%m&>@<2|>4P${K1fz)LT=lu%BuBl%Q%IROy@Y3;Cq{$6@ z(c#poI4T*pFdL+4#V;5Z%}`On_u!aaSg?#O3LCJJF&ywNEG;twq6I(}ENJ@kSn-{W zyTWPiX#F-VU)~Ye@dXO9$3pH!0>|ZVN)?NmUfCYke4kIVy&*zBbA4+sk46q^<<_@?L=DZWIRnDD&%ChmtN;SGX05X zAoI_(ogjJZ!%43X9v0>}JwYpH!8Akt-geNppLFL*^hhD=Zk-gqUnSjKZ*&;0%}qzA z(T!5zS}5aW!-jgF2yQDVL357RDOpSCtt}8gy~M9eB}=bGIw{ zgFP>LldKn=Y$y{#boZjzK$n?CK6poW!2LakD@(mtW$zBD{tWpfCZ49Mo-@aTgNEH^ z%{-;J*=x@K0^?D}wB}a~eSfTz-UKcsBxwKauoq)`VS)yjs6FL+=Khg({28TV0FS;k zMHgQP5mH;6iSX={Ab=J-#V^DJ5Lr4)?C6aG-msvV-6RrxLRCjMqM zigynm+hk6k$*Ib(g>9ak3BMe6L~>&w5hpuYN6F`HkFJiFcL0KR<0Pw%QAqmWBUhqE zVtpG7Ti4&2&tfqbukeQNUVY}a(40&L@EJJDrQe zF7*6zwk}d$=GOh(nE+;{or3Gq9Lg%-u}|w$V3Cb4R#Bm9C7#E_yV1YMGc?2_4f^>K zr)l5#2<3Dj{27zt4m3I{1ibddhURsyIB2jcSU2OFSWs@DjDIrh`$`bJLGsxRl@`^Z z1$2yzCd9V^5ILI^M};$6n@_F2Th?9wZ55T(DmP|Eu^@-+ zIbzwDr3$)=bg*eNvUbfs&d6e?vP@ur`+we*}7cf=Xdw%DA;ML%mvSAq1?c zYyTx99M7T%UWFuNM>7M)wVWG6zXPeZSymhBHY!2B@w2+mZ;l@_@MU);z08W){2UM? zFr+$Nl^^3Nain7iMY(4fU})aUy?fE zM>f|%tODyBh_3=PO2r>D%sZlF&VW;7xm=~1D)wzNmmy(h{@Txi!;D~|GSlPo{oX0P zHkO4yZi`PJNcb3~>BcrG7L%yarKj~yc99Y7E^dr8jMDNQxAVBoLGL&Z%9W^7PruQ< zvQ4p8RX=IMk8!EzO!4Pi)d)IJ{lzx-Iwgz529QMahDp$Jr5QZQ-9#qi!pZTf-wuM# zHnhmk7bfRd^W@1m5tIV(e=C(#QJz3qDM7YgtR{Xja8)XopcGdA@io6ZyIe@)OHPU*E$*WOSe1k4R6@;3H_^YhJ z&bkmn0=sO_Cq^`B$r~0r8DwhLBNgndU~Et;0vG_qWo}~QaQmEm{0L)_VNq=NT5_(DVA%P7_Ok}jz zmfX)zdg8MoBA1n@XkB7FaHt~VZ(uM@a{Z|{@zAaGP=KqZ=3Wi0!TSK7+gGvpPhR>DhxCZMWWaq`xktGQKtHNroj4i&Z;5zrK_DT47<) z&jripj3om8nH>7gSGeh+9;*yu)rlG)kBfqh(pad)Qo-pzupOeF49w2*sa`;%v)Ir; z2qpwcN$IxJlIdNe$!7tt1|Ow1D^26p2picF?AFQH4@7NNQ^8=sUZcD^Rp@@IsZT*xb~T-!^hy4cJj z;k*VDVx=2icYEB4V|ZNM@s`d@=RoH#J=}XH3$trFdh$BS=xHStSF6_)ySzn7V_kZL z69vfkPZfxm$$)n|pS(gdB&fFedUvwX2@EJ%-hE&6{t0Kw2L;TwXilopcSuc|dQy>V zTYfI})>9x$;r>a-9u6pQZuoO=pQ`0j0#h8msWeOSD|oAyvQq1Fvt3%+on}u=09}9o z{qT8WIG}YKJ2*N0{p-N3sTsqG8ACO=!s7(d)iMgnwRnFOOV8<&Pw35;L6a*WTuCgY z5__cWlM)Fm3v=^Ne?18)M25+Di|S6G&LpCmMup4<6)7+V+&5y8lRttlr_##kSt5h7 zh2QcqUj$2wh^BgrOeKj$$`JoH?X=wD%sMdSrmc76c@oCw%G)hBK;vSW6f#K#gF%t~ zLY*1o7eax;zf_2NA*R{^v`xo0c6R@b`&`-3?i>BmawQchIc%bhN$qBkoeX-h^%wla zW!OF*8@IC4Ti=Hw&Z+3g$x*PePYmY&!WocnwP6%wy&HbT$+%`^#=o4{<51MCRsrU} zPjFh+Gr#pt#EBLy{tmVio7xxZ$XG6>8;p4=6%d`5{7J<`X)sb4h|nk=RMjRs?@yOk z^So*fD}se1AqQKlW+<}G$|N<}6BcDH7t+L_)aR|+$ABd^e>qQ?zAf=1B1moTY3fUp z(Tofsxe^=wEymwZ$9MlkYEF|qsE~VYKh7D9#;Od(u+Kh8Ny+k7$0;O)2Hu{(CX<1K zk2XY$vu=BP>ZU2FYD$7UNONE^yYfXOe{M}VCblbq!8Bhm>O1}NSjiz*K$9|V$wmMf z>{;!b%okMKju_=+0M<0s*H#s~lKDlN#G!0Y9eA#CIlU2V_Pf8hTS#HG9pZgkiANFk%*UO5wqHVeMa-~s>)VWL9$V|3E}~^b39wg z(TMER^=FK>JXhlZlAdto!4|Q>fK79fy45%GI2B+OFlz`CsGAieb~e6TAcAs76~BsQ z5O^Zk_Yw%15|OY(_@?y2OO+U4j^XI3RYfpZ__Nu!4}L?uHCm4acXwSd!VG6c9k_y< z&8vJ!NKM$n@3%KE0O|+tGE8G)$FPRzSxCKPql}VQ2e(0gf~nZDO!V)aflvxkuY%?F zg}np%8B2q|(Ob9|Lth;49Jy!Ar_$8@%{b8`~^4uPAo-$yAvA)Mqf z{H@eqT$kjtA=fVw?XdIojGwcKi9D-7Ts5Pvi`D)(>9yBDbrekE7#wAlGS%&9p5ljY z-KMdNjIAQde48l9Q}Yk+;-1E-H4|<-;n)L+`;q=Qb~-q(W-FCH&0oLBqNy9S{F@N zOE?EM#Lo#BLL`_s;Tf6O$v%;0&p&Wg@zn0Nj2l|Jigs`)f?tOCKaCv#&Fn+p+CYDM z+;#WLC#-fd^b%yiOcTqL`JN?zC%A}ZuTJ7|t)`$(2P9>Aq8yS3oI+l3%;w8m-=lco z&HzZ&O`dCYgl!|8S`4h14?n9|qCOp$bi5nB;i=BYUa`6sOf9~_YWd{yH@0^9c;jId z6FIWAOCDr3aJcj*@b0=NHa0V{aOieRD-NsK+X1jqDedcUKlNi*@b4o9s2KxkUDMpp zNJ0?+aebFk4)RYykK@v5%t&}0&d&4TzxkdoK+hZ8{g-M&e3UD2p zo)BychYnsfoJAC@C{^^^@(=PUY+nywX8#us^Y3pzK7; zsZ?{9?RRz(CcPcNf(t36CbvQw9zM)Uv=jhOxD%e4sd6(2k#MHzm+&<_@0k)JpzM4S z#fM1SgFru$QTE|ks}FdAl1AaKH63WZhOD4}PMOMCM zpg09p&lIcnSuGDZ$`qAZBXzW`xIdqYmfGRT`V;5KFw6?ZMIhuCr%Rz_KGTls%wjfk zM(1j47AFa`sl}ONs-YE+(4@+4%F2A(U?t#KG~jwJ_y+tPB`>jU@m76Eojgf(Ax+g3 zNFGu>op5mWp}n zr>_Xeha?2IT3KDL2mmKPnSdW?BvcqL0dRLXzd`rQ*tHv*q%xhBktV%hK38tl7Mg!v zz>>-4oRxGloujYil98}@LA#zx%vcE6Psc+uuPi)BXLU>cLWFuL?fpns((g@uQrEw<@com3e_^ zmdpbT)Rt7h&4Pjb;}s-me`l>{JXG?zcA-@tPWz%Yw5~Nep>Ckecth(Owwo4qTv{na zu9n_h3~n2yBgQSD7?(`epFq84Q84#avE!Wqm9K1s9J7OGfWQ*Fca7LHM5^=M2;5-?YsXE%?+N{uI*gT0~LLgW?SeBR=mgy)|2+}#} z^j^)?v)|$+PK8Up=4bMWPlgBeAbk05J3Q#4_Qm3|B^xZ>rH9478Ds{K;Aeb|gL{AW z5(f5JHP5We%2vtmWGmItG3-%1$rJbCZ9U#sLg3rv_F!=oI=!^Cu|b=fL!X4g`(*X3 z53DT7IJkZ{KIMZn(<|u-(Nn@J$!W)p3-&?f%9W%?+H9CQAyp7^KCa40942&*0$?OP z?h$JuATjt6xt}WI?n)~8_dypvjDk_%P5O3#>_Whm*;o!>3ec+WPIjzEvehyV~gG&5{4Ew&tE6dqlhjQg5*+kSeVCo zQ|k0ogytf>ee#8}1y->ssIq!H;fBW9+1bxM*2RM<0v2qo|2tQ0qHM2PJ*5vU-MT+& zELn_}4d6dpf?g^YA#dC_iC+LmSdIOlzd1u6+n9eilJCa_)f6W4iGQ4=p4#K4&>+&_%#EN|_n643uPs1_BT| zS?$aDFow>9gO`oVk=mrL{w-U57$zjrIZ^5B{L1a6-$vwzs|$58R^{d(lty@M*6cDR zQbGK%JCe_RsckW!t^Vx#?3hxDpJf1^o1fnjx;bSK>*VA#oY8*2Q>V6kx9=O~!KoJ* zC>4B=FP+#}*6DOHkDnC-H`Q{F9#FTo;b!{unlhJXr-+_h_OD~|hs%&;T!(?x;l+Xi~{2qCH+@wp)DS~2kPoGVU=KiYISM$_MATD7w+&tAIKTSH- z)2*S3Hey8w!qY&8`CA{02-3G5EL(D;uZ{?bgoC;r6?X#mzVs9dNs;l(C~N(_fJM?| z6>);_s4M80(C_>>kZ^i3_(qv(oYVBKz^sQP@hk7*kG_Xv!!`Yd6V212w4q|X{)bOz=eAG7Bqp&Vh_H=L~?w9 zT5in!)ftk=R&zX?ITI22Bur+Sp{Ck06hx->N*qb}7NkRTv3c>6d%t+A9Yi zvTv`K!nkFFG(N|`xykC1U=LWJWfD+WA-5d_;#?vm`dg)L=u<)N$QeHtxmgBHbaW4f z4Ro73{u%}|;Qlwt%r7b;@>g3gK~1gVqcYp}b_lb8*(z8ozwVdZ#xv{pP4(C^8O9g7 zP*>g);S!SeK{j?RF6hTE^i0vwIf&l6larr_?JPyrrG8VZzfoua&h!fIfRDe|;~F{& zUd-!;f7qHoW3CK)ajVb>{Ua*`b%ZOH)HZ)lNX{}c1C`_c+)P6^#j-VG2$Y&6>bx`Z zXdNEPr(oF78(_KT^7VK@_|3^{IVQ_^BMr=-ug;Vjtx87Tu~AoFUw?a&=$%pHeL>^{ zcn0~qXwiJKkCDrVCKfzQMw43*=D7Tk^(CI(fGRsxOf%ZMaT(yEW3Z{9*sY<(XTrz;vbJ}mgI-F>c`4>N9z-R&JsAjS4u-&|F(cvaAhgX4hCHw zd_^pJU>8;aF|i;dbFFU*JV-tZ$rL)~FD zqTs@RKtybMgHje5&rXj~+z2;uU_xZO`D7TRZX=ayJU0hYLtUJsDjUT)y&}Wm3yf%k zpU>nn;}f}TE0jwY^vHzOu7Gu?`cz*5%=o)60f}Uf;l2_Bng~nj<-lm?#v%tV;xB6C%mOz?He=P# zKEm|s=LE>IsHH_yJbzp>2HIVWPX+gSP;LP~b+@WSObE+aVpFn%XxNH@=TE;R4;Idi zJcT_4yBBs^ws9;K{$TAPU1w8_2(MwGP9HRB{Zc-_sZK$vnB5eemSdyMQWKlBuva^m ztwNd^*fxA3(TBF3m04Mr<8Z`L7*t-~+`ma*lUfM-vfldcwK_(pc0qD7sj-rNUn!Q% z1Z}1O(M4sRYIZ*~v9qJ>lI0mWz+>s-xX4YCtf$EfYx*Uocam4#sqBAv#_7Xiiy8g? zka9#P{6Ct`!mr8qf7=QQQX<_AqhoX9f!GdHw!{-S>U%x~}(e9%ohx#-x~5Q30*kDl$ryWU_wCE^02yFS7p$Jxu(wO4h%f zivmtr&og8Mk;vDm_M@7?#R0|zzM)>v%;q!}P&w2t1Gp(yx6yvgS221d2MP8ff2%>8gFHy!*E<9@fh(h zg5+1rGekPJuPm3KD%}k8j(|13P%q1#$o6rXuiQ+@da1=F-uG|7Ag zfq{K8>n1ld5G)ARJULi{P6Yk=X@E%4tM`=2A8=||pL0`?8g_@Qgr5E*}2Yf(cfx-0)}2g*bD{52Qj$oL$)T=^91&%o-B+B+nMyF6+>RKWUx zH{a@C1K8>Uc-@!+avD&~%$2|87j+Ko0ldY6H1 zhjWj+%6;!71}x}6e2EQEz^$U}dAp`>z>UVgG#D1H^S zWQAEacbOMi9Q7w4eSfxnD#S~BK`%spM-QW#7tY>+p5*u%B}yN`5^2OdS?h zF)yE!o1B~*9?E0Pf?u!pqlA3-^d_hS|0^omTQx}Zv#R9hoLFz%$`dbVd8H}(p=9Sb zjwKevS_uK<>FHDXFYgypW~ZM;9oW95L&y~Z?K1tSJ^E8wRJ*lV$w(maPj9KpWiMF$$)BH#V^ zI8@`K`RuC5$gA1z(qY5;{74^8k=9Om{sT-4u8a2aH#cpd`7YU+<=fPyR5+TPwSGD5 zpu!|&S%IrT(@6W`uWn_Lmxhu2r?bnCBRZS7<45{71_~}kipzH`k~efL(u@lcqN6^F z5F~r)8 zpUl3ibX)N=-gvM+NTW4vB#5H+LM`qq+>VQy=1$}0AY*n+0S8nZQzPd;gIMJjj%Si? z@82`(MHbUAuvmZU81SIKxNzEkiU1=OnUI1*f+mfD@iK!X<|Ms4>?&*LmsRdMrz>|F`~ zk_B&vwRWBdaUTTTh$h=Yrq_h|O)7m9HyI5AK5}&O5rU$npMo$Mu}lll_(kJI2FuSO zy7y0ScYKe)@|>_%d|UGrq>|#p%4I-b7ae?J`ST!|b9(_0js6P1lhn$WAdnE|$8`po z4U0lyFY?Kz;Jkkfr@tYQoOoFhCWPR#mfdnl^bE(xtto^t20jqtSI~Fb9+}7g-hXMU z3zZV}v&$T$|C=xE%LH8OPtf-D{Yu>Le53gQ?zd0dUVaueaxF#0nL$T@5Ts{@SZQm& znSr){wPHc{8*>2vU5KIeAGL-JeJ(rEmp6ad%1&aEg_`(Vx3;xN%8Q7|e@M;8|7&k- zJlnSK>b7p4wy_srwelCfaX?Xrhu3P^K z`{R1l*VkZaELnjKLwr@@8mjLfGC0DeMdOewz!oYJA@uKlQQ!=6`88@tb4lhM)eFRV zLsLd*a?vIVwJgRcK^H}YEWn0vF&=< zFLT5U2rsbI5*Hj;OITMIYc=|W$7$C-ot5#5!on_Xi(U=+PlU6GC|4XFy!hLm_K%OP zg!q+}dRNThen6nh^Fb~~(v;E|MGLf>p1kKWBf=d<5T)lx@rM?NS8vmDY@$ibpKFAAGw~y(6(wWi{&| zfzRBJQw+;!3LD_O^d^iH$oK57^SYY7yOY&$ej!djWly4jr#*C{=Au^-TKsyBp-7TY zIzY1~8gE%=boqW6GofKE_W1kfg@!_- z?W}tG3wqgm&mZF5D5z|29u%3Y6&($y1Wo1|XNtKc4K~o1151)=2B0xevySQTLbz5` zNh*S{@)sq*Iy>7-;Qgj*^^6zqXu5c5I$4;g*gCgF&dk1!9|9ynw^PzcfV`7*sAyWPJNsbd2i(-g1{A?He=6&1L7hMwuXImr**#oz*5rOT$|2_ zn++Xw{-Yr(gA7Ra&$xOH1~=t9DxV~JYg)wE?}gC2^2u06S*F9|5DgW6mS&NnOB1eBfC7#yvGi|ibH-X zy81wzS97(05h5Q0LoO8i7$rF!1MwOk>_}!qEU#yYMu_Gy&`cJ-;kGr(e!6>EQh;A1 zW*##Vjvt%QQHeb>&a>-g(x;s{`sxTiVzXtGoNr<#O{LhWwZ}qbKPnBFo%4sRwtBBG zMvu+yB(D7-#<{xapQtRXzC=|RIE5&*pIy(-qvrE5ib1bSS=_KriBmhmpajZT zHeBSV4RG^-Y;$w^Vj+vP!Q%S(F_a?|l*CIh|L)(4T>C^{jxfe*jS;KT)3H}9SZMkO ze|tlT(F$(W(GSalkR6gzq?%=FB26FLRIr43Mca(bk7oL;|qCfrc; z;e$Ue?sm10E;p)Yq4!ax_stiQuC=IwZ)1mvq={%Gip+ORZf8I8Ukvn`<1ra+eW~uH zdxLh;28WF_Y3cd%*1un|;TX~rUEmB_wttSalep%U*8twBuYCvwk-85GBL?_r zs*w{q9E8zENaC_Paw#pqgJcG{yU9Qs>E}m_9|eCu@XA75$A?U0Y*dIZmWavvroz}r zDnudD7Q09jorxnb&i0E`SqciH7hPCchl-GeV8pEzsJURT-0ZD|)(fo1rQgf5d)~o0 zeKtq?TXUAVg!(flucaNpjd#WM)8qMl2wM9!3)izy15)y$WHFDJKwu~Pq{Z@`6 zB_as=Rr0^9-)FU?HdUT_ZmGgsk5e@K*=5{1tsTy%PgSb+W;0k*@dDxe081}pbu&g) z^+XGhbaHR*(rPF%+kw@dE`EP{M>ko2q(HjbB@-dehx#``&WU|7$`0a}{MH?#207mn z=++~GnLm@%v&jttldFKq>hGRu$Cua-r^pK~{z7Wzjcl!JGCDWo^WtTEcmBa3rnHh* z&Z&cv1xk>JoLtS-D1#wkG)(L}xOGZJQe+Day_#{!bWGyPsPBA2>UQ#m$d23dz0QJy z(i2?+vY6Yj_c4PgvTd$#o6cbK$qNbb=k;YNR}poC%3l3^ew6BF*4uM$VZ8tYyLaXi zP6-Hu^zQn1aIWNNIQ9z!+EuWw!_qU;w~C{4Jilxd1FJ}1ja8~o zOB{`Ha6ZkM_3|O0>5*)(kF5HQkH?Mg3pBp5TPc7lP@u0Kt61sc&&535)US#Pmv^Pbt{p6- z)bakJR3yOcI2d9e7LL;B8jM;zXvjEPuFu@0500tj#d$~D8_rJgcShaPW&%guW%~C0 zAm}`THc}IrL$i(}qrU%=yKSWyNTOnw;8$FVZ2-|`%O^*V>n>u7mRWfxd5q86bP3Dj zKjQ={%f&3~$v(@-bIyYY|3{}ZFLdb)uAj_x5QyHxi*wA6#Q*f9eUXXFcnjbNmTC_F z__3k#j^Zs|s&{fGQd8axl0hsRIl-dv$tQ!}-1uxFX#n1t&_F>+!79=R;Ufp*k2Yhr z@zG`{l8^!|;*AykjJUyr_22j?>U1|pDU$k-JaWK33jI4ZbrAK-U_FKW3J&OAV<$Uc z2JpP$9<@Vv!&)LjJ_2wHoog|sgU&slL7lHEf}BwTMGpNAc0>^uFG22Ch14Z z-=SK}c}>LA2|-UzR)>B8pz5q-r5RoLL%rfwVAq*HTqAgJO*glfJBuc6wt!q4An@2S zPsV4fXcLwMgWoI2;1}(sHmYxIHP(*Vs|^;AJ$u1TmL#1rV(#teS?wb#D$_HXfj?DO zP_U3Eti$X@Xn*|6QG{&f5hz47!R1@K+E;V?Cn6$(C%=%oX*yctPfCwK97+y_%$yb6 ziG9VwRFstRcjq@hm7SNUZ}#6!%gF@bV+Kq!6^Z0J#?$sV`1APJivuvHw(>i+V`F^Y z>%J7UBs2ww6dpC&B764VN+<%Pf!9!D>XO}Zjw>B3?rCSZRFWyVmHA0)zX(qiAND8s zU|~3?(AUH;yeA`Q;r42@e&hIl=;;1_sBW4FP<7#hy`fvCg^#e`-UsHHp-Z{!nZwL4 zkC~^<9H*f!L{$D2f9*+`lm7q{dN~aIL?@t>Yey3ypqL=ug7o5 zNb6{NXxIv>Zl6FC3~7`%kWpZUL?8QQuU@sn)KOn#O_V64u*sVGh>eZA%+2gX*mB`R zo(V}wObndK3(wgSQyV*XtJfa$IDHGJj@7;BTuxAncMPF9`9^opg|kueDo&c}JbZs@} z_nY2I1xg9^EyX`ZZ{@vN3OI;HJ93+>TfHcU94R7#J63og1B=h4jU$=)dNoiq%5okz z4pExr8Y!8@qD9rt%hAoUK=gl@tEy+7Y_b`MWe0arwsiEQ7Pc1U{)Bdq-y)4H1`fYE zX&FMp|1pCjN;`80C0Qi?w zpE-`XqLy8_tQ`6n$;5rPvj(fGjUA;zy)568)PpF8)O0ZK51hVksk2{`9okQb@QsqlC-T2uf-VftC=(76a*7$lLP->g>b$VW zVL{eV)N_oYu6-_aS{wO;13A3iMvmUjOu>kRMg zbV?a<@B~W>wFccUN`dZv6;6LPbveEa4!d-`t&3R=!vO$n@vNsgPf7;?oI|g2FCkbi zz;-G{2++SNJ_D$9b|k?xCqsDB7^?_Z*1Ged*D!VmD|T&6cA zvOJXi)t=J-7V#W}fqLmCG##eAF>c@8we_lv!)K#2-H0CsIxNYo1@4u%GwX-h9h#D^ z0P-5_q`O#gaRzKI?3sZ}unZ&l5L#%O|B8IOXKP~Dx7%{@8aA*r?Sa1%C?vRc;bH8Y zFnw6vaEBiWQp%+(23!SooO@garRzRt!@dK4_1)*l5cC^&l)%erwfNsMUC2&u-4P%wQ6uMiW|^zW%jZFe-t4 zq1&t0p5dOGIHKvyrbXj_`@W(;lE>)pnDgK7wbFjQO)noKj~@XMqF%6Jz7HhqQIIA* zc`sPd_2dDIL(1>1&n%?1;>cnmgE1wzSM7q%1)+ z+{NPy`|Rz-CclGKBmu4LgAFp#;D?1BSKoU`fcOsEY$5xM>zg9Mr+kP^#uqa?rv+j@ zztiH&d3m5IZ6Yt?hn&9PmX=a(^!6SV4MCHN)0qJ)TSsTJUOta2IQcqWmPsZ}_KY$L zK+3%6GIf5$3iIJ*a>72-LqicU{e(jnvTH$qXHTSh)q2h{=Jp)}C>iS2P;ZcFJ?A1o zJlyeg|5E#%%hdJ1)!IA|q7egjR`JW1Kp1>2t0(O4F56|tf#7&&vu$E{C-#s z|8?vWAUFA3Lt2JLNwIxa6IbD8cyUI_0o)YHtIzMhL;XDNI|ui3QX<>wxahr*impvE)Rm=S%0rbrx#L$%%a0cB)>D&y4LvV3!&7&iW}`l{gGl0#rcuwp(o{-F=>;d(iA6wZWJIEGIYhU>ugT+L{nZQSGn=RE3>^%DagJz{ARFTW8M~(x?1)OZax)1fU zr<{)sLs;Qv|8Yan$UE@G^&hG)@XM7Mzi(!#i7xZz)jj2jyO&;=xer^a#O(n^?DgR$ zm6U@(>X03QITO3Vqk0w_pqtPkluW^&ISyyU0UY}d(Mp(Dj_vP!4yzz6|I_m_k#<(Y z{t=|Y59FQkw+>8;B!uO@c?qaV+oEt;Sjs`OfJp_Ko0{^%1Jj`+|Ef!be;x&R`AW$^ zf~C;<#?-K{f3OgcWW5TSbdw`rYPX0ey9j0qG^%ED3m&Y}aT)82@(S|uVhd}To9)H^ zNh|Z!qxLkT=J^4OTZ3!e#_xAmWb$KMb!-w+keDGO&*r7I@NTir#D`&-S0Hvf85y_d<&QG*o_O`JVI|BygGOv`4D?!xvq zt{^s9JiE>@wTD9B^O!y;N?G=KFR~~K^Td?Fl%&35uRmfq26%jhST{ODuiyRv6`VV> z?Od?)QdFOe#oA}&`q|s-?QV4-Mjd%Jls#6^)s>M!$v&+J(ra5~=BURKyun5O52f?ufSJt`J%~Df{`}*Go$MI#O#ve7Nh=$!@9SvsOX}M8kg0 z$vkLkbRpp=Ugp0-n_wz7gPn*bGG9#V^&7_*aS_rjmboUc=ttE7-sB0{!c^yPaU1QS zfKFZGou&m#+{52jF41-m$H~7BgW2!TcpstVSBmW(GKf+s(6kenK~?0x+xz2++SJ#a z5iPd~k8=ZGa>x1v{F+W=$kYJfehhm8I(Ah#-3;W|^h>`G6&GujWrpw%95hO{xxaos zyzM8A-B!&EjMDi%-Kk`b^|hOT0Eh+8>P3=wbhLhouc)MrA1E9_8g6JbWZ#|Pih&$5TgWHuq%xC4HC1^Oj}HG}4hdEPG#x0-;|3AlnCoXVW5mr*1Wt>8 z|3_7_L3Y|vm9(>aU-#TyJv;)ufDo%yHD5gpin+1KTH67b){Xwwq09qe6o$bUFob~( z6P4T1f^Rb0ndNF3&OU>l+7rq5aBeM}Z4k)JkvsFaaF}yD)b#%3XvG~T-apg;$8a25 z=QWT&<9!Lm;M;)J1WGJ&JcKPE<>j2-aD-fT=eVZmBh=T(huwXCb<`Z6e@8X0!Q5W8 z*>|JF;bj(bk(&B2o9dJmt|9WL2gz9-Mk(*%2N|~nu}Au96o+xx@?Hf|Uq ze&~EPqMJ63Y2hL+KhpDxB(kRZi*&$tP8J^~o>b>snFXuhdLaC_k7=hWzht+Qu|~RpXA8dRdSL$|l|ar3k}~qwSxAyLVQ6zC znzxr3c^MIJ$lrXMl*#4Odx4gS&W^k@WKkHgQ(>Ied zT8{P1sqz1qvWY5dGGSY#o`zB|xj5&?DGrh*1zF*TC0Q%?zwI`^h*NJv=-h27j@}lc)J0 zjLUu?HXgx)zd-SOODbfT3R0C);)O5X)Tu^^E5Rg>m$v^w2+pq z@rVe%{bKe4z>G}=A1$E1VBrCXnE2+JH80A5b_7pMdb+y{0p;$johTVc3C31p^r52( zR2}`FvBHfjzb6bh<3`(WQ2RO_PGsSMPR<_gCx2rBiEJN6o&eNWQI>{ASRq29;$*{u8{OEEvAnsB;=xffsqTfR*$v^kj{bhnU_d=E6s+LGv(EZ#o0eYSD^29a3Vrg>tXCAsMQAgmZnX< zlWI6;RfYkd1lSFH3)`n5GYDhIBu$rz#|qH1HFWtUtnWg8ZbFG7&p;yI$I&M9>|*1Y z4GZdTt30T(qA8GsF?g9VFgXUAh?(YgI%2U{+JImBlBKiUGMr3=3NM{k1sG;_Tn|zEy$9w=fmz;?Sl`8k`=7@aNQDIjZ=-R*VPchfmJ`*^GzN4tg)vl*IS&rNJM=xSxm9tu6}fUclY~>*6h0Nkw>yCCR); zNC#M48m!(<4M&?5zi6{k5}7H))2lE^6|Z>f#kvi~uv52P@{g8meqA}qsN9k3K4|l?-QZ}E{bEbV@W?8DB_paVus*nnW`R`)H4imnw0kZLJy6k0vQJ@0(r^3yVw+pw& z*~;s2?_6e|Gybq2*D*twC8nc2thTu>ry-s1 z9-&E*VK-@FJKf5gB)52*N=^XP8k;1WtEksSE8o9(S_Vr0abHKfr<=)8sYAzuo9h)5T;kOax{PfFB;E&{pJZK*$QDL0e$LV2V*Kp8hyySZha4 zI=tBsd@4_?P@diRSs9;T^Fu zk>#UDLUWH$LZ8&ria8BZfqR>Qa`J2Rzi8rMvW{!isW+F0wY6PC;c+_ZSY!g~4vBmd z0+$K=q(G7a+$S8Jm{u){ryvqUb7IsW1z{lLQ%x?-RqJJNdV@pm$o=a(0w*E5Jc(hS z7eBUNkUM|yFMo-`0$wqf%He4Ws+M)$oJ)yBhb<8iSfI1|;;*-_e-kvO4!-%*g$#jB4%qv`0-t`wdoCRr0ZA?8rj;rOI>e#T*W zNN%#5Tu$Fsm#V#h@Xx98rXjHC`R#46O>1R_NnJx#6@7Jwso6*zU(0JWY4&<{cj-yXUt6au*zcl~v-mR#h zw@Ew`Q*R6uIT8EB{r=1XRNeJ3%29MqW?`CEPVa`y+J)(PH)o-q?TG<{b6tx8C!zf32B@+o>eYK@OGLgPZZ zu%*3f?yF}Gzx%EfkviXxWHhR zqVj_tkyr38q6+@?Gtr97T~`*tWJ=@o7*{T?=9tK^O>@zZ!;^P`s|ym|fj>loRxdb( zQqv%BIoYWk4I^$BhOu=VQrLHhUd${kxDYJ48=M+t_cO-I>>oUD16>w1^7A_h{jevJ z2_1SJRmFSgH(}F?<&732p-@3DyM`39q@5sCpvt~{9^sUgPIO8}8+ySY(>L3rnt@51LXN1C! zqe9+cdD{5z-ey!Tlfr?Vk!3XJ%nKI|79 zJ?t=9=+vBlF+R8`U2{IUr(Wm{M)?#Al8qEAqwZeX|B3TingYJ8CMj8_Y|dL+Qs(#l;7*3Jdk+wwu-5EY%;kRKbLHeAsl@ z-skfPw`xzYrg9!$$Z$rSAIuj}DS#bl4IUY&qvBAY@Al^kFqqXEUQuoGOgo`AVuqa6 zfd82%sd)o;velup=$>}QPq-ThFtb#;XcKvt>ZW(F5pmzs1^aE-!myd7i-<=l5M(!Z z4UB!LPKsFFi;rK0w*oatXLevwee1d%YmoXSlh=u^Wd9r*qfK5LBo7;Mo!^EMAFYJG zoV>ky@cOs9J(jt)A6e7goqx+MCnwp($W(s)-jwFq`7@&haUr289RHDRh97|$f@Nyn zO|3y5cYhh;6+-N-=X!c#89l5_JEJR{S$}CMAl)Hu%7l zBWcOLcBq8xUlROC#mc_$`#jX>*OOOGPjo-HUF7C)>_T>#?hRTB1 zkzX$tb>yS0jyXF!4VVJ#3;HotY3-TfGotYyF8C7JUKvBNdtm{VHsgPPf)hKTpnp6hFGH{g_mk32(P>eEi4j$l*Q0 zbWt6^&CLxIzS{=a6YlKj&_cR18|2@TWi_^Cw2J@c;mt#Tq{S zk_LFjXLpn8_G_Dt)_{2U>)>Zhjh2i_@UwzVFR{vqa^9|b?gc1 zX)N!8v#>mu=-Lx}ww_^%)cBdCv=AjILBTqgeh;9r{=QAK=_<;;Zcs_;?(GU}-qRkN zMM@pic6)R2Fgltw1y=&Co1-iQ(_bCl0uL(H&dT;ZIEq0)Y8Bv61H zLi2R{ufPweB{jdvpL@hHJ48>9gars?X@}rA1qavZ*=$2Xq@Epv2t}QYDW{mYnJsyY zs(@}H;f9m%CB2lPlVx&p7dc~L=R%GCPWuzDBLkst_xSF&W`6f=kbfN* zsU8{F8)#h$UvipEX+n7o>59Iumf#92`QO| zh*>qY#Rnf?-!67GKkjSZGXeq%mVdtsKhIem4mD6iU1s=TKR@R}gJ1TL|2&z)C>{I5 z#viS$j61yecI@u=yxjg{>$_Z71-Y)$1hCeRpOx|KT=Ph9(^ECSD({ujs z$93N-@v-}?7+Z+x(`4H^?loK)(u0)EPzo~ZGa zJWU5I1PF4w96)_|ZoT`|oM^*OdXL>9k1nrEN};<$w)>1tI?cG%0y;ZwN~fPipMNF7 z7fiyfn$^C)-)Y@ZF*D{HmIGgZjkJs{)+d?HwBHkrKAO~bVfJ;?JOxdSnyqMbgtCvQq4kD zPB|tPew~3d{0d`!J*MPQ-_}`$Viu0`XM2vn{GnVtPIM=v2vdF}I<`fJ#PrpAj*ZlA zlUs?CQ4yexFuALoSIWn>5{40pzb!I;^ehX1^Y&uqC@BR>%s(g7yBVVjEl+>_v9E51 z)u2^;zv=vCk&I$soGub`B}*CI!N*tig|~_gl2_gn z|4e6p>Q`!SZ|{6bqM$S-6milE&&_3EV7L!HC&?`uAAg_8@v9`f$<5C7=<=|l`gh;5 z#2Jq*{r|B5&AT_H&COR=%_7D8y`yL54U{h8t5w_G_5{>mQ}ACUpEbHflKVc$4sL`2&~ zT616}lAIP@tw0a4exDf7$FaiVLh}hf)oU-{nens_dSH866{2P=y)0m4Nh=HwkYB;7 zFatwi!B996{LmM_N9p+_#91I^DvjYlTJ&qkN|}4d%BMlT9qBfl=>STE;{cA)X!h5k z)ZFD>k4EO;g_a{z9bw?H!n=a(T0925)&wX`)YEp3{2}AvCuPdKI`)N$wIAf>eu<+t z1EqMJz^^a{wEe?_6?+K~m_w%KnBlj({4xPjX_9u<3ymWkmQ^^=%kMM=mHE^i**SXmHyx%P_o~9s@*_h(+*7Q{j>(z z>0I~<9~0SnL3=&DJq;S}K7l0k(J(k4ri;V&x{YmiU5}hi5G~ZWfeqpfJh2s(^mXQ7 z37;emcmCq4R2A;UuTfgwakZ4s4kWhjke+W{<%Ox!%LVm5zMh!4*zY8TgPTi8Z#N>Q z2GN|%zusqgCv*HQ?iaEBiA!n`@8L<0;_Z9ec|ULRkHx{b=(w`ES)U7}ZQrGc3UmRP zuwB_#rsx-oC{tIfY%6mj;Lhf_R@{6wn?@x$<>lr63mlkcjN0BLAq)C0iqv8edn=m= znEBNK4Gu(IUH#2~cc}a6;MtvU4$yT{3DIngpjAI{TT=0nTN! zCgmc#?Fg8ufFUSYgd#hkqC2DoDW|AKFg?n9BK(D@KfFPoq!7K~tpbSdF_xs{@CS#Orvb9;m#YrO^^+ z|6V2B!os$Am9B=<^b$McmYoll*6w$7B+yBiR5EL9B4~YgH5b{27LwH{clS6tWijJ+ z;Lu%Ax|*6X`@_e-%~vYI*hk0|`Gf;)!A2fupd>#VPIb%CJiQm%y-Mc0l&1mFNVv8o z6isYW#(p+d1ZSS2eG!p6tpj5(#n)0-OVzCVcFF~LYHyirTBg<5(l;z&5d5aK&G%Q( z_30)xP0oMY!7i)oiu&3v*#Z20Vsqlw*~F0rshtw#5BYkaru~P^K__l5&$;3<9<8sl zr><$qxX(#MMqE&~Q??U`R8?d{m>SdQ54A-Bn-^AmrNu8&D6=b1&%25{*vw5mxEcRl zUVp)!u$8AmR&B}dFdpl$S~DIT+@Vi5hjI^II*pMTybl7k6G50Q`6QUh4wWUzH4~KM zy)(@ory=-+Z7l4SDao=!b#ch08Z`06X=YZZH9qY>kYC#kU-Q-ef#_yFRtLww;V+1|36BV8($DOc zdDlsLck|b9?JN0`ZDNVOy>|mSi69G>I$jf2y0=znoGyK>B=1l5T1;(Olv!Jl17kA=xvDfD)u)Do|w%g5K= z-hMzA^RxY{N4zjY?Y?rx%47!IC7AG<*x~E^!&Woq`5t6$ZQV81)V8~!Z7irLy!9>T ze>|oRDDpz;O3JBm8I~bZUQUY8Hn2BGW0=~v%9`e)J6+CJM4FV7uqHNT6f+Ci%DSCj z-55~4p25HwC6$!q@{yLfGenJ8hA;sZO&d^uZr?bFiIfIydA6IW>_h_dx`>k16>lac zs%0%KEGE0Icg7xNTene&toTF&RD#Z>jCh^?N$mE#{U$_O?a7lg8VnL+aH&c@;3Dv& z3BTuTM@`+CWTs=vnBdn2!5{;BTI4yjmoZ7hck#X5k=Uyh+)m|a7C9w_8*9K$|C|Xl z>m|fqbkre?R?cFZpD3E=)q`$%!=>N#!tWsUHKnz~6gouv!HD=Qx`C6??Nvtd+l3LT z=nXMo!VgD4s-1Ccs!$#~Y1Sa!Pi-OId-`{`ba(y{ZM_<>ZAYtctMEG5gR^ml(%1!V z)Y$mA%if82Joo6h*0(dJ?0JlHjZxgz{8Q^494eJS8YV5U92+c7Rsj+sgAC4ZsF=Xh zHeE(kYMPs7;~Mv#RZK4wf#mH+rZjM>JJ@eD04Dj`~G7Fa7u z#hn0aOLFrRDNTGN5e<6e&@t+!c3mO`BoJzlX!rgL;K9s$Wm#kL-iU zo_j08y4z=hE$583cdUIo z+ZJcz>5j-nl#7-obRk)1*buP|beW^-)|7isK<{4;ud{$vtZhQa8}onD6kiq24c0t{ zug2;|Q_bua=sI05j1K=8B16zg0E8=!lqQ`k%VbDA-6+fkwYxZB@L~yzm{lKg`P{cv zTc2`%!SIO(j%#O2r>A4375Y=2nH}PC@kSV|@5Yg11aHM0wiO(uc6;LZhG*_u1KS05 z0ihODPGlvFtUIIQ*D~lUaYIXiBA)~FBcaA~->(dcQX{C#-Gq|}PIF_s!unL$ZwlsO z0tdr1^3Uf&EZ*@p#nl~{s0G>yX14MRkjzaEskeI;d`qx=nqBvd_uve0-|0*e}KF)t{k4) z=_?)&KjTz0iZK;Y9`(WB7Z;x|Cj~P7KLGeZ2fyy_X1{&%>dDsD_K1s{1w94L1 zl~+b9Nqb(ndb@Apc6U6{lS=ha_#Go$8oX>uLKjkeo}B4VyiH82Easck%ds9_tJm7Z zx{@z-7?c%AUDmO>tfxPtrll<&XIwHX+b&-G`lFc{G4?pr&+>9}kq0923B~1K-Me#V zK1XB{F&7b*YiV4pFZpp5JJsdTF9VYSSu1eJHxD$Vq~e@cJ=6Ivj^bsAgY&$`%oFn` z@|ZriySc!AkiVqb@dFT$#&i|um~fQkhTgKbhzA363O_l}gb5spRl6jn(wLqwn%F(g z9J@E96Y0jp6n&Snx5%kNal6yC2bgLlnsh{W4U8#!6^4pJ(3nI4k0*R48h!LS5^01& zweiFdCyRjwLMDGeg?wr}5_wAD`qjRvv=;9f?a|t`_L`<7F_Z3{%u77rKGTmp=E4Fx zic(`e-R-eX*uX(tHkhmoNnln8U_w}4sv%gmwSktHGw&b52J)HukLtOFT|#qts9$UAdOT$poozFdt7rwueYI;G);#Nsbh*SZ~k7zgArjT*npH4uP5ChtbuK}b%|g#&x< zSU`vom|p@!fkfwPF1S%jZ>PdIwz zladRcT!}P_b-iM%8fN(EH+xT4rg;=QJw8=ZGIfQZIez;_PrH^F{UW8cw70YhLVA;Y z#|P1lslCZ8EZo04HJ)xiUtU$(&`{M4E4bK+QprkO{t&`&I;9NDSW8Qrrm?4yHmqU$ zI3cngH(W%^nwsUIA!A46GCOc&7BVZF>l;i=OD0nwb8#`Q#h-(kjLVpTO90cLrZ}|{ zltx5Ow>blj5|mz3WGa=J*O+-?9!SiP-`7AA^H71+!`xyY#0@+yE)XA}G0SoacqTVYwsi$UpyesO z5aX@7ACxSVMIRQK#8L-oOo!)8y2iT@$BYBUMSdl4AS%BpQnV;3RN4#Qtx~U(X1Kx5d#1 zcQXt3=jU&&tgmcrd^In^@)e;mWmx`}`D@V9;H8Yn%?AUQhQt)fO~uDrN3F+eJxXNO z@|7oMo|t=yi5z`35%X{X>&!j=>&D2B@Hi_r`p^|J17+6k^}QOi%px(x83_bt(TfSj zFfr#yV`4X&gTSyF7R0n@Oq6@{Rb384Dx_n4T$al1ZqsfpmzXZOG8mS}vy-!k&9?LD_eC+*jx$Xcoec4yZhSR3H{FRnK}`pcjGNQa>rJ<}MOD*3n%ZtB;FmW``6fTl8y@=a@t))KqaiZ)6> zvo2|Z6M(BpJV>R_r^h4ZwN+K5E32mN|F*Atd@OyQh>6rF!LpQKc?qy=*MwUp&qua$ z$2w@4`KL6t;67GZ+bioR+v- z*%h(=b>{$6ZCIDQBw}{Wo|-*130L9mU(Zf{NyH?L*?X#&yV|q6*>|tL-<>?g)q9sq z)x6zujpWgzg$@t$mp-RljON>`F-?n1k=7I~0n(zxB<7kVrVPrZ#vB~{{K)^=JG+;* z@-&Wv^dd zLO*_=x9@WoH0GacYfO>69n%JX9R_Yluock1gieY*vrOaONjU#vOu{NX_flCl?Tw3uvZJ)pL-R z5B5rrOIr^%pWNBU9TvbyK&BL#wkGz&Z_$)^AreW&Bk?#OvrJ=l11r0$nHdbgYuQ*N zDH`Zef;BW|k9Y*@#HH!i}Uv%KL_mV>+%#1vn^ z^^;z1EdtC`5wmabsEKx?HQFqNfs~hT_K}vfRHPs#YfPIHhQ)cGxw;aG3BW{R9^jK= z7ayyuAO3~J{D8#l3b(tQA}GghO!DC1eY=i;ZQ#dgUbZ1g>BT{Xb+aFnewl>PCg}x7 z<${>w)M6A~EK(j5h>1b@Z)#{vPGgFWOnP~diP_q6+UFE)1iltvWvioA0+Vu?l-vZ3 zd4U$4!&`5C1DHIExI~GG9KFEpOFVIjrDN7+1k6Y#9wk~15iilH2=!(!1<~2*lt=yh zUskRy=Z(CkscP=tx}mDNs%qFwl*;F>8d(EPibi87R^`|a9caR_5sgePjDhT2h>*?Y zXm#-SiOg@Yauh-33doEn;)#@ImVFY$OPS4- z1t5GT*qD$BO~PYO88W5J91!WvIx*|StR&{p`SSow;k?mk8W%NMTYj9J#*`Y<7mxy zo@%yh%=U@xZ35(AXup zAsr1KsVU9V+mF1A$35tm0V4CG*^eaZ5TSE(QC-g@)#XQ}_2v5jNiC^$+@CM#iHxdi zot+xG5YHP~2$-?`of4M*{!W_yfgeK6mH_NL0#9Em7k@ICclSdk~!60+l2%iJbgUg7%`2Jo#0f zm~~>76O-#cJ|d<7rS(j>W73Ll_e^*@wn_#9S?VT!xQ#{^(9i%94xQx z#KC?=AOblmM}uL>KZwd(LIBKAE3r{DWLgJ&l2-;_l-}(4-o8G-;;jJDa#t~zR1TA& zB(KI%RW+G6^i(XC%y9TT}NK2NO-58yllCwwl9+|wf$K~~?yiUxT z#2h+*1vF+uLxbhK-e@lRks{N{LqE2t+-32y-Q}tgmD|Oq2vKF$lbkq!n7c>lj43rH zZe$qbJm&vBd~P8kQDZh+Qkr?( zeLuyCf-_Q~rqie~abpB1Oe7|m3D1PY1Q6GMgnyCgUgRc?X%3W2VzPA+&6LxWWSDx4 z#(8hWr!@0(w|cHzs7l0LKzIi88XpCSiItuZ)q2`OCNhbdZ6cEipr?fw*yyhCbWj>t z8K4gV(wNxhNkc%i>w&i9d{KliT4lM~`xv}{m zXBb94nK3r^_BOBTTCbJ2q;5>WWr|rDPw7T(o(enw%hJ~Rd@irYBK>o6?8ii=uw4gW z{z&C=J24MQWS*QHzkIp>@srQ?CnHf2mxI#Osx>Cu15^$IU?MELWo*uz>@le+!gHku zFY&5Q%sMeEhnb4;~->`Sw2(GnR1?Rq@kkzK$Eq z(@LOe*Rm640}fd-C#iW38;k(qKBs72Zx;)GFpWusWalC{y&@pT(0oyDlO&BTM8}cI0z~!n~nov^an$fz(6y3`|L4chE+3Bbw2T<+XLABdMyMUd_lMVf8#luWe%>BlgCZeuJPnZqebfTj$~cPJ{C#d1O>Fq4UCZs3LQ!5*W-!>9Yi;6q)RZDQo4(ONOSa z-P^T&Ac$GqW?C*1GEr<6Esg0uC(@&#T~lMC7~4%NX{%O_eFI{o+(u0wdv{^aK6jt)H!LAIXK646A|-wQE#+|!?kbL|^v zCnKp;K4+vdx*m&C*bQ$eY?VrzhQBw3GdMbYg=*!fECH75oUue! zi;g`UBZ-N_<=2^+RQaJy<}a^);T9tt{`pZ7Qye?I@n`?(@x$YXV^lD&iOX(rC%M@p zBJ*GcFUvYI2Z93_sw;@jIx+vt-ub*Xou_epX6+7)$%3mi*2XcLCNGUAWPz)aCHX=(| zdYaX@^Z9=Bqxuizn;Ki4>44yC-p})Xp7+xtW-T#KkyCDQ zeuR|z%ny4%P?dMOEXGfPPwmoM(>7*fRo-3uI18jPnU~ziv{hq0hR3p`*|vwq{G%eK zU8zhl%k4E*IsX$x%*vac#}!nR6)wmTnDkX;^z~1_zV4!Px_D2QlqwU`Aca@8nHsau zDG3}-i|uog8=2^>7YLr_)IcvIlQiZfQ)DjUrc{~A@;x;sQqWK>N?h&%I0_+=p7b-; zLh(ipg&(dPepkT*YV19kDZg&rRrV|4RKM`qa z;w~yp89+~#hO?=OiGg%3Juooxx7^k7@$s7=OP2?tv&CdqSc$M)!C(LWAuG$1biGL}^6MveLRZ7#`4WL9yxrQM@Axp!SIYvoE}T5mblXiP?CClE7e z5>u7r@J7Ed`uf+O-cp*dOIFvl#)Q8(2400|H*{H!BC{Z6xiZZ8IvU%&Dk{24JY=jQexrWTas<{ZSsx+E$Io8?G z{JN&X3lbZv1fj*#QEC`blE;eL!TM zrB}->{j=D0VrkJ;8tWB#GjV=7DWfMi?8x+BSjd!ZNmm^)vx^~1>IaZLO&IQl;%CW%Jg z!-Rhs!#6q&!VQjz_Gn^K%A3XY;Vnt}vk3@2tplj#AcK-z@+)$Em+YV3u5yJoe)cA& z9u^`oiF)z?E<~CJkF+e?B_ET&;um%@@m?>*4O1>A+u?&~^dKp>P*={+11evxy(D=F z&|J9u*?q^|P#BlOvl*|?>xZh`8;0^6q-IiS%;H9A>eUo3HU9U}&84NO+%TQgEzaVA zdKTUAJ2*Oo$`W^YmamRv-O+?nmU6=a1SWmEw0 zcJt3PE~)dmp>pE#Q|nV>*lo=u$^EG<(A{p*nLRksekLQlY=%s0P-bf$vqj9NRXJ)* zk7`|-JLS^PwC__Iv(ua`5;zJoo6Z(lQj08hj|L`H_SDBTXyM1Gwj(jOV@S-l4_W2* zPEW1xIuv9>ruMd|pm(O``)eI5_i(V;YEwg}K`c8Z#4( zdMW-AMP|I{<2a5~?$!GGt4EL4p)QZ4=qzs$M~Bi^Sy|pkJN)6j2g_H}0{~k(i*_-A zz|><$ATZ-8WajnA@X}JG$Z2^{BlUF2#m^{H~_-(o>H$-C=n46XX_LCThhfC$1+_GR~mI2Av zzHWF}+*VAD*^HR1FArMY0xUR1R?5hc+979RlD*%9X1EP^$_H6U0t)A8c4!E{Mp?OV z`G(`rkt4?(GYboAFV{d}VuaDq=bta^cg*+q^#L)%8L!vtkNcy(4AL|1EhfF7G-)KW zm@47Sd4zA6gHTKjjI1oLeZyrq)ZyRWzO%eMJ`j&4lSLVXr*hof0|Iy^C-A$)Bf0go z&gWo}87Z;(BO>$N^PlO6#cqB#pSK_rsTnWQ2U2qr#A=#1CgEkS0J*u7Nxe*epUUTG z$myu7%hk^RBUXV|x6)j(Y}-mENVjp?eLEut-Sd;L~9`aWo~lt;hZp^CgLR(bif3PJzpPFFX5 z4-%Nz#$=7j3a;A2A-t$C@f7)=3jx`e=3Pv)kLl9;<7%(GHX0LuHop6gfBam_*AmU&sZ{ z*<}`!h#6pwi6YalymI(aFd8-HNs7qS#B{l|SAL1S_)29*zY#E{om$b%p8M0>UK;ZN znc^~Rc~~!sn*x)ki)3cKCq|cihDb~DjOEaf2+K97!WU3Y(lfnAB{{;9SP4%EuhmW; z17h0x&YtzU0hrzlAk!PphO%zbm{?5Hlgy5c{3rJ!_hR|xod>rkS3B>sSv0hcU5H*ZXVHM_{T?eK#2=vnR9rT#^y<;rBuBg8bS1-d&{MPjND_V@QP?VYt&dCf6N zyBWWZzgyjULx;mF6#}MoF@t2h05Dk-7hEn*c(?ObIl4Po-r|yCwWn%nVg^iRV&FPO z3IjDg8klyKzkaE=3&iCp|93~}O{SXwmcg=xm~+jDc{m{ExIr(Axa2#Mbm)h)C0SH9 z5|-@l3~u3xatgo1xgn^*CmjyQ3?8QdSmGPX%20?P+20Sa3~l%0b#L4Bu|tElGlWcF zrO%Hqyh)MqMZI3yGS6U!@5(r!^3H5otwd!5mtn{5AizmUicfAudv6+k9*KUaNBrJ!6^ zjp=NnF>6=No$8Qz12Tb_kHz^ZATcTUEoPXw+(TowNj#>m%1w=F8b4Zs#{Tv8689|W zDOc3=x*{g&U5x8=iA?oqVxq|O_o$^GsmZx6C;x{;rU(9PJUGGb(FQ$j8uj*Gxpqx3 zt$-Qaser(J97OlNELj}oITV?V8uPG3T^?2*j|>W#yFh|bWrh#(ayU>DG~_g@O#1Ay zWB3e$_PKs|owJ==+ut$&m-B!90+7jf zZ+Ij{Ww|fv8(F(OdHVsd^4{uKpWgrIe#fd~W^H~bbliX91lc=@Q+TBukEA6|V4zVd znVpD7Pei5a=C)=c;m)QKvr(MNOhnlag5N^#C!R!=shIifiISP`e*X6@>|x^R3%q`z zn2BTcBvTQbP^3rFQ@P<1#u%dul;S-0j7@miC!v|R_Cvh%zQ-?bY0MTen-SAsWO^>x z)i_b3#DAWG%XVOj^j#2$VDVQSf6n;BA=5K(5CKGSruh-`%w#E$a8N}pi!n#7K!(Dcw;u159xTSC`D5)P&mP%u~LVDzz z8_dMswmuy^<%vnyx_ew?Vutsg6D?2n!k+z~PduLIKks{hPL0fmtu~|S;H1;p;KIvX zK~z?ymO%&8a!$%DvvX3I4imo3aHMCCiI^XL@*z|pjCwXoken#g>COjfOC0I6TnE4W z#p-8wKg&+U)mBJC%EH1>uHf@6_efry{A6+C^e(=q=OZ1!0;d5j8seCLG zqbIz6S2X7M^v&tf(OWlvPdtny5=vhxOYTdsb!WdWgWeO8FDK*vY|`j3SI2!ckspyK zFR{QpQX2Ca%H=nd&ZPU{PR&w6wB_Wdz*zhE&N(3r$ba_8yv>c{U9 zF@OEXbUYH1o#O%D^+Ex}92a8#`P*;a|KNjKrcP|w8q?LFF%5-jJ9U@eUMJbWOd=*DbA|KJTwF9(cY5DBhr?c4;nw4enlxs^ z%*>9i(AeLFnDZy|Cof)DBGZUt?)qKQ?sI`B67w96zP_@U-yjh)Xz@~4WFlE^_e%1z zO(QcVFBoIj5e4Ac9|CF*SAu#x1Z+)AxNSCZ|ngO}M{luo(>n7O*9=0tKLitJB%^XhxsL`>&j zQ;L|8!GXnSA(vYqf1V(lzE~v!_xG>PX5qtQe2})xC{)UG=u84(VrF{!=3o9cz0N*8 z6DSlC>+6y=&u7?>>-Hbj0JOm+ndmTA#MC;oL=tmn2i5CGh9d_sC77vH=7z{jE^8mX z|J}cqM*&POFA_hl9s`3)WWIxy>b%VzdMaxlC~{xies+$n>v03 zn3jFFG^RPW-E!rY#taED37Bc8*BS=WQfPMCYkurg9(!IGvoZ=$*71nefl8p06(+7Mu52`!o?H6Ow&U(scfTS* zOUS$&iKGS=tJNxq83=%ok$5~l67P*o#ipi&ml1_Zddl)rJUJeTf!y!Dyys&o(k})A zQpuiCostoQm>CIWipGS>Je(0^s)F|a{Bs8zdP!oE#x$XcWAbo=6F}>d|JfL4XqI-q zST1p|`GC@xL`jwS(J{<`QF|$vEX%FT+bi0xyiLpsoW83JakuK}5z=8`cMnO!C@ zrL7!2k}D0P-b7;BMK-R6d2l8#{r=(MqG~Hga1f@vR!=8`UZalwEn?GJT47|$?OeIm z!}8HGhzVrQKR7u%JL7`L@cyf_lX+2iw{iNF#$#PkX=gU&%vYD~`*wnV13JfK zIPk{=%qLH-F0J(wFRR4L=mckVu(M2LrFG{I$us`g97#_st;iuN%NT=_Fv_^J?0Ci^qHkyj&Rd-O)Knh*68M^kigm_p zk(o%Swx4lvilD+q)#}Jt$zHL@l<|;u;-%9-CQpF0MW#Kv=zQMCrHJXay|~NO zQe*Ct#@y8!GeoA`F;-8C#_a5TSJRKJG4)V4bD*$S^}owp^p)DzyJ+!79PL)i|Tux?z_epp&wo)(cQr*DBSaFa#m!g*Wn zrEE6yzA%rsfERES|?qxJhx!cJ6a2#{R)x472Wd`(`HKq~9 z9NsN9xbm@DFrBPBy^Xz^Es5zhHKr{xWl4`6bH%r=7w_=yNPrTe@A&m=dLqO`EYk&I zDjVMs3Tk4qD<8v#OV*f7OwI~vUoMj}=`T+5KK6?y4AUPt4`PzABu4Teb|z*K8`8qz zVMgY)pG0GxBj#LoVub$7#^VZ?P?ymxXLnG4juOiyR6!zA`EvFixo}dJ5SE2>UGm_- zBTmL7TsR)!WNt23&7IOCC%mNh@rh9)W^i_G)7gFFM$e6&9u}E|%pr2+g#{nULZvTb z`EFn@#870#$Rg8bCG(QhB+*h`BrqFs$r4kk%m{dim6fD1CpQF_pr%dCXGdG0@)}82nS5^(vrWwNnV(1| zbR`pFjcKRJjh*4{E?&xQcOKd^*jJkJKDTX+c`l5phDI79vskIscFQ$MseqmJWYT%b z-pbb5xKz?!zRPOR&X?=;w_TPicj*##*UE~2mq*Zf!jIl4f+oI; zN9dFMiMu4v6Eg)j9!Fie%J8tTL;?E2_MJ6Uq^p7kFBF(=Z&Z2xAwyXO)eJv|^MF=L26voJIWVgi^wd2cM{r8DDI z2#mxUjwx~AQ|#cGmxhbSr_;Z+2;x8yl_z46DLa0Bk}sd+j5+4N>B;QeOn$V*xpIAM zX=akhRJ0t|0Znxr&TwLAa=|Ho0x54HW4!=mi!6_GP@cI#XIcv zyKb-eUrR?drF{(jXqa-pU5?r0$~g|o#8h$25b805AZAk>v-54^F$I*y9F8uTwB$C| zqPcnG`>q;-Uc`k35|vkHXY-%xK1m^FhjjdObd`glE+%G;hnPcS$~sBWm@3_)>Wg&p z$7}pF;U5hsAQFu!hX-qgDnDHPZ~gfAxT2dR`MB1CnBAA14o6zD;Zl>{zl|=()dLoo zOKW$yWGJhW-4juTN}**vr>L1z*d#Erz62@-mhzSbrplU=I72T_`IS`9y1VV1T;WU{iHm!!Ic{;Ehe?jR3fuEIo_+V3EA21`Dqig zB{88Eys>W1{G0=(F+0s3poT!ERlx3UjAL37(``jDTLdy)md4b`l%5~u$}Lj1wB=|k z?=V)QS-J8qwJg};r4Un)X&xZvp2RVsGX3-u;R3%>SbmF`|6}ibTH8vmFdl4eiX16% z>lUO2QD4axQslUnYW#PE?Jy3;W-t>wW8(@b53?3SK-0s zEhc8d)P531WZFUO6qesi`3u?bn+|gG`1ZCIeh8m0Uw-%Im@ceE z>oAEU8ZFP+jO7=PAO8`qAxt46cEVtJfP`gPjp%Afmy)<*lVfn^;iqqmW zCoy++EV?{55bq&g#tGTu;~-|4f_-Sr{=yBAFHZ3Dig9|(j1`KkH+fT3oUUSA%R&na zcNeZNICOOnoSO*C^J;c46ed2IDCqLZG^aAr9)$i%7>#&3yNtnnh0y#}piWM@vqZP} zi>B+kk7!y}xnU=fb9iIna!@Q2FU~qVhn6JghN;tA12vveCz+ zIYCSVOlVrDOeW@=H_vY)AeTuTNMrUy(C7$a4m3TZF`LqaIGY`4LR(@KM9i{E(=8bo>6c?8f-pvp;-udV2iLhxgaspPii@9X}16%fvJjG@ksHo5Ce$ ziHs!*;mZ_8oA`T9?3G4R^6SPbJe17Ih6N+wxx%41BXecdImArhwj3QBiq7=MWBvUo zI7OplC4+?0^z8i$#*n=oD#83eZ8!is2+YMJh6r zI)*)^B2$+OjSdbzg1rb0KbS4|*W=UEqocEr7r*$|+4-p`-)koMAc3CpwlZAE2Uy zsJtJv0|UXu1#f1Cr6!?~q+~1(M8@#X0L|<4#a((OOfo5%mVEc4@*}LwGcOG^Z6dQi zkJ}&*ysVp9>yVU=#zdiMX`sO0j@MF7vum>`)>&+OM$(v7IV$7aS1#7eV zw?f2J!_b$HzeL7ziOuLX^44^viNJk>rJK0q(`Z9ja%?W7Y%??E`qa;H($zruoF^KM zOqM6PK#%Dhx|2S zw4P6-8JU91!$rC^H**F(6C_*+=qCM6yYycv*n>_VUCeH*%{2s7() z^)#`to})Lp_4+zJ~L( znxI_D*S@zXQ2Nn=y5=z-ZSqKA>G^53=D4psU@wYX-Bz9oZ!4zZ1k~(^59@6sngT%y8r0g=H~7D z_qVsVxw`Z87{vVXr;BT^&rXk?hR!c95%b_chFR_|k;WU@K$~M(Tf(Kfqa_}vz|#uJ z$F{O@Wwq0_y80Pj+biuxFx36N001BWNkl)=YvX?#Xg=_TXS{_p36+ z(lI*1L4hU(+$t?`$K)BTbuKz*yH(6tDANx7W6PmxwW*_fUTT` zCGm1(V?=JwRc?r9$iQ-Bwc}Y~X8I4@#7}c}bgFG-fv{ z?-H{EG4m9T_e6T^GReRaUVjIK=TRE@d$h|`#rpvLUI zq%k8`Xv~!J!pibyzLE%POan|sW`^Itc=2M?RQ7e)P@flLVtSUC&4C;S7iDu(vr*$N zLg~l2i*}TlK&IOyDZltAe!JVxwtZew2u>pN?d$Jfzr6&^OT=U_K2X=th8A;_nyd14 zn3L_`+cq=Og`|Gj1}Pha%38HntDaP=C$+8G7DlQ!5X5srll_?-o<|3kpDfQElnX#! zEGEh^9>1=@4D(WH30NvG6`6TxOtrg9^SR;@uA*V)rG(?iT`mHZCD0PHGUvIc2QjuR z0TSE#o=8kvKi6!f1m!RiEp|AdW;&e%F4HLcP(W_dn&LHD_ha(ze9SH}I}=kirn7$6 z=Vh<5$M50xD?gd5C8pJP!ATrRhnUIC8i~y2{!WGW*_u3NYs%{l7t148X-q$fX{tZa ziGHQrNK7zujgy&-%(WLE-v9H%XvVH2!nilhWA>#g&3qpdGYd0jG~1-Ge9HPUjmAtQ zt;FP+!YLP->|o#A+0Hh3retUIW#l|Z_&)mjq%;x+2{=8 zFV1<)1uHR`mxh@phpCCnB7riG-kw+iuUzcG%we0tBwqd{H!+ceqo}Syvky#z%Mz+T z>K$TcqFfG9Nw{=|XdW(>Oi`SRKZ`6eH~71trP*X5E+eO3G6!l7ujkV1X`RuLR%d&< zt1-L8>_AM4%6oh0^3`P|oW~5V)UjQXb^^N%%0WdfR9lU?u*$R^2@m6)amBTndN8+OUw$gnb^Hc5;L03 zHrHEIwpNZjoT62r!CV<&!tdN3zFm&h69c)}2m>IY+v-{OD z5tAbAy9m1DsxPJF!^qSN@i=H9X%$O%4b3svQ*Oo`z9S~kXV~WUhnn#a^-?*C=)d|KN|uP{Eb-7;xRVoZW3S2Gd%S9_#! z$6(oUBI8m8q%_o|Lu^q8d_4=3i7yN!Tb0@h!z>K_1j5T8xL=^DTS~~PFq@W?KsRFo zhD;c;aW)F1i@wh}=Y8LECAnXqdao?WLL~B-`Q%XUp&u z?_8x0B%^Mz=^-Q2FOw6$`TebHi_|c5o-02Nnim9`xp}FVv%tK75Ina+(z1kdPf1mJ z?3w1tNP|od%yPXvh?oYMT=IrhFlkH%rnl-aPphu+lJ6ZdJ0e@1$aM4NHa@2-&tpJ& z^Me~w3OR=wb4biyVxkdqbi|j*ec>mu0m3B=ZLk}We(Y(-)Iqr~miO!~XG=JCFA}r2 zAmB8nA?Br=4a!g7B$?J9Xk5H7O;}y}w3OCbPc{?LC#HwO-23OxpTB(f=GSN5JUw}` zwsx|1V$MJRb@I5-GU2%S99WsomTyL5o`c534)8b(NmPcOn64i)6}W|H{MWDp``x<+ z&g*?AJEvbQ$m3owF-cF7vfRMaT935+VpWq;5ZT@Kr|U?`sBCYUi{OZZXFKf|?c=sN z^cv%`wy{y?owd9N#{hF`cJkb6w{vdN{fqBsB$QP#22UL1kWS04{BeRk&*{Nc4 z`O0%po}mkM^C2;Z#Oxy`W`4#-Y*e01WD+rReiYF$GfHWzH(%iC*O+);p?6NwYs|RS zM@(OO8raB=6=X_WGOH*mhr~=LMW&xHr!f7=yEk;M{Op5^N3Wm0e)aT~IY&pY{swrL zg6QubKO<9 z<vk7VxvrR5?bDcGWro*TP*`p* z^U|Vu91?R#%pPKvXrlba$cR>#NsZ~IYn*lqSYswKnKB}23qQ*5t#|0hmVV&B7PnfV z5HrS}G}f4QMssWsG;MQH5}WjQ27YCIcRZV4*nX>8)TmiCYP1@oC5;%hcTrKTS%ea+ zY0X%TRa;_~7@)mZcGK8y<7|`N?AUrtaqQT=$rsaTlHl&k93thLCgxCMJt>=V@Gw3}p4@Z^@vP$jY~wi6pxE z%)WuNkpUy#{6DN$q#W0!0VuB3*$OWMeD!7jVXqthezPYqKUhgzk~MrVGvLo+0-oG? zL#5fDJW~s_WI-L1GSPjW%e{6_ZTnr8{W?`KDBZ>|>$r3#R>4Nx!lnQ~Tum%r1Q}96 z^}1VZ_Z^FHAMx}Q2lduZ5u+A%&>i0~X(dPbN3fnG)utTCbM_Bx_CjmEQj6zE$OiUC zU(1H194WX*#Vr!Yws5%pG2F^k1*wWn<$4Vz*&%UkZ7{LyYuU`wBfd$(z$I{4*P5PK zn0dLS&9AIGxk7#tQ|gl^jl*Yr*Bnipf4(2c=MyqUJ&YI(>cHv2jg}S#OF=Hg&-3q> z4657pM!EmEM}8Q4kqlz>1gQv7@Og@3APHdbMk=k=Sq%M$uI`t28UJSow#b$I2C_^jorT6I615D>=h7=*<3 z@Uqsbh|x$gg4|-~bs#AThdP9Vw;in_tOa9vpyFL{#uyfj-#_%`p?v=`STd*II&PQ5 zKXQyK*4VG+!`Q|5Wzu|mbJHQZAk<4T5spODl>Ng$;aTmNKb!ApwOyN^K0CwX@yn*X zS6Qm!oY#e<0MSGb7KM&wJsB}GeXJxI^Rb?j$}mCrbn>~H^vlGrH)F5M(P$G(11a=} zH;J_W;V5x6@u|ta?{6<+T!&sHu|6d=X>ms6M_XBpztGkLNgUx$-3+^2q8yzQB&}M= zaYXi7st5^t#e>el++t(%ZU;j{jzd9)>OQp&OYw`cU{}1 zy#j%~!xu&^Id^_O0$q_RL@eJb0?|JJ=_LxULJaNVGhKgoe=-i@z3hMXSWVrb@bJU1 zNMro{Z&CCOErHk8XFm!S9=FH#F}|`#lz43zg0j*Z?WF)~puaBRSElq7CAv4Seg>su z)%D=G{hAT-zEoVJxRQodYD$@yOT_1xhcmO@fz&~ex*Z?OH~N-Ll0(#a)cuYwql<_z z)s9~K6nNf5IxGc9(IRIUn=gA*RbA|te1jeAy<=Ct14q_!ZH&8cse!c&xO@uGUUD^D9-yw@m2a0Vq_ ze*1PrO4n)D^}i*n=^ZHA&~(i)^9OLXxctK=k!`k7!j*Eua@ivHX8i4@dzJSSzdfj~ zks1?P)9cmC2%!evw-4R$fG_XG7ZEfXm#eb?moXL-NaOuQjs3v;ca4R$j;+XiiAf|6 zJL|qGxl?#%1LYS^X}FVw#fyP8=%8Z(`(27@A*uvR^g{my?^~bAcMIsj`M5DIlAf|j z3cSM!#Xe!5T~r)bDErtU6rSeJ2MvGO=3%!?ME585O0_pKdqOME$2mS#hkLWIJ=FOd z?$~4;tsHm#r!{cds+FzwiMW6HGHjqx*$kEY@Ka(o^Brl<4pR9eFxZGeqxhfZJ-38n zgTWHGIEeEhGW}Ob!BRm_A@cmpEDeKe{x=>^koC#DXg{y%@S~3Tm@MqM{UVf+v;Lo{uM=)zUT<7Hy^7#S;FqVgf{dmOLI`@BSL3#l(^HR7X?G-UgSiS|6NwPJ z6At*U%pL2J9K4t=fq{z55O=onF6&?VeJ3E3qF+ ziw`6@Xra$4qzVVOizy{(Z&5p|*+oK+_b9AirPQfSgmc={s98vJrtCUtsY43Xw@HYs zm(KU(?`-!xWbqP}+N%=;L>#Koy2#o2rdq}oe-^T`A=T%=(W1D2FME8T1DXFwn1lnj z8*OQoZBvUvD@?d0g;=lp9M+=L_-se)GtM#O$6SgQ35mq{pUZw*c9{ zW^9$)@PEH|73Q`T&F<&DdDkSm`lxcQc|uE3;Kz>XaRac8AvK_tX<`5N-95O|LcivI z!w&IG?>N=%JgMQy*`chG3%+gc9ZA?VGO86{>6=d+wRWd{bf zbO8lcwS)opz-bk;*~j1mHEGZDzJga`WaU#T-?qk=sswri$S8NyKRn0!+`4Gs-cj($ z%Km!}Z@z!~_3>{xUmwwipfK9|J!<6pz*ePXIyJYW@UlNa{_zc+%cwk+GgD-_SK1 z;5->^ZOi-v?|PUP^vdV0^in#iI3jY)EauNMoU_Y()3hP#z=@HOCNO%w>&C=;Wo0SF zSw#(~T2Lv?8F1o%0xLuf&;vKaRKi03!O+0HFXG3$+I6KI;+BE+wSIgJ7T=5uxevIw zuc7E*>ffW@n|xK64(3vYFp~pI5I{lij|z&`E|tDa4&I$s%KG(YJ@p~seYI%Zs}`}c zRS9bY_Q0C)ps{G;)MYMh*Y$vw(L)n&tDHwsY61Y%v5aH``MA#v_*oiL+TflE<=N;G6qY}IKcEM`3nB(xop(Ma)+=GAiRKgplG9s<{ zBSqn`AlEAEbm71w9_SlZfjCew;bjiU>@;fT4#f&MLSNm70e*oENQTa7PMcDOZc?5Q zeTeT)zI>bdaZ;`ClG2OZB8F+z+bB3M@0u8!Q~NOfKOYv@>kkSvdCH7LH1@Aukg{IA z#Lx)pj7RLNzt+x_;Zm}yFZ9$~{WyY0`J=$sQ!jUz+hgr@wcCek`r19Ge;4j?NBUEa zNVi^v$>+wDBZz((z$24B15?0yxWjYgHU10F8hF9NOEZh1fO~7$&dyFr8{hw27pp!|G}rSHujufy*__W*uu+&kC-x!eT=Pt`7W>MqT#`-zp9E z0gxYO^{nuEj=v`-OS0L=*ALi5{<7!)xKYDX@Mhz=6eeZgEjWAq%)|Bk&rVP2-?z7^ zDfM$na)ozR!-Ml*l>OxO3=9eiGFrt(v2Ou2f0obce>BDjIPUP)IvCG?u|i=TZ9GA7 zy)N;FuSd;PA&Gr4DcN(*uZ{KjJok&^>xtNbCJ_)3`?Kg(6bdOcJAlid-QO*fa^Z(n zT*Xm?{t`gwM6$u?0Ft1~_4*F=;lBwI`8f}6OP8?QH>471a?klklHHRJQ3!|uuWlrQ z0If15IRka`+G=@?plLBF3TIxOR=?fsWECl}{Ee||@^9ApmCvY@APT+| zuq=Ei;0T1(Z%IgI)YG$ps~WKe>|`vPljhW7J`1yvYMh|iPj5szd-R4*C%efW8JjEd zgs`hv$dcZ(;yAcS*+RV$tp%~WNCOo)0I<43 z0@$INVk5te;s=~cjbU@Irk`eG)>>9W20GsmH-`#`GUNcDijyTd^ERQOfnvU2n?nGL zIp(sIe#$Z-%RgqRb<(n6fU<3mB&yPx|K6(_y?L=peIO*y%aRxUb!*tia8Z+k8so`< zcwtvtzFHa(8+zCh@p6;N&Hk|9UfuSA+b81#Yy zDm(wuyJ~6}J3KnEYKo!!ovnZ3;E&UZnFYnFrWHpc^e7)m85D(KW&x|V&V zlcOkH!xq{^xwu1fF^QQv1j|f)VS`SM`Tq~VLMbC5Sb;LbF$pKEDtAl zdLhaTpOHZrfNVk2AIVRxS2U?cWVlcI63YL+Dv87GVb1*BG(UZ_n);lxMqo9Uzs=Ai zpwhCw(sG3q<2;GWgQ{+Ncce<0qNY2_VPYZROA(1p*5}8Sv54r*r5YJ&Ny&W=2qY2( zcO5Hy#6%34EC0xHr||hyhyL0Xf5&fY!opq1?lNb6Ez}f=>+-WR<4+vTV3rGYr2@(f z_x}PKts8G*tm|2ga4X%R*AY(>I&e~cYkHifJJaBDobwUGbH8n;J#)BnH<1!EmoSHV zLMgk0ijy;6#&(6nYZJLAMp_%!o|q)quJhJ@ZE_{-LQwbMbTL=f0s$G zv38PV58-VZy`SPnGhrQure}d6macdo#a6xPy{$=5R?=7|@xMGNB|UNZWtS&}AY`15 z=4QtqF{q>xX?9^jTo)GYOJ>zI&7VpTnrj%H6gVZWi#x`gPmO&K4_OJ4X{+|tP905nXXVWsMahVkEH7=FzRi# zHqefaXV z6OL`BTB+#0nd=>nE#Q{1|GggzDDjAuE<`ti&Ga`fw97kat=^zuE%OQ) z-CQelE5)tS;pVHdn_|)7nme|14w5hivwpb<$ed+qi9=Kmee*XFfmPDSTP%GFdJv{L zT99nOto2AgnDXcv!=eZj&#ee!j`ftXlz9C(_xLGNx{W$prQHR4Xoby!qU_cjcIBB3atAR(eicSsKsN_V%kbT>l?BHbZ5^w8ZycMsiN!_Y884fXQ) zyua`LzVCYfJJ&w@tk~;b>t1`G9ik{N@dAew2MrDFg_NYYG8)=b02&&m@pFvF5eMR- zmuP4hXo|9`-_Fm^*Vopu(4XwV;ispkNaXp+@$tjMgO>CL8XEe+{{GhHCTwTt*>l{j z^;JAf%=`O?ySuxM)y377RWjmt2n2$N;Ekw==;6WP0c`6C4qIKCKRr4?Z$aSWy}Uu) zl$MrKkbPKMTArU@oSvSZo1KM@4qsiOc6JZp`zPloM<@I6iShB>t@X8y9coId#kuLt z9XNb<7j<*b!^urY%e=d@d5JhZK0KlN#CUvqQBzeV#4osigg8G(H8nM~wzqZn^bQU5 zvokX%uPXWF3(lu6?=!ELu1pD;@?gXCP^>V`f^akC*q0w;!M3kt}Z?!nT+H1O!@|nIKNP?sv<|NEfJUGjZlzZj{ZMJZA<4@GcBR2 zfW8<uU(_tq_)VlTtSI_|E&9@mphvOtV1;-FK zN&_HbdkD97hr0_QtRLm2&I|!(G5u+I-+`zq42KjPhXCOw8atVPj}ZL?w`UsP^5zDL ze7yCx6QQ+;{=Mg>a_kw%u3-A!HYAOo6^KzvHUUVOJ>*@Js_%&$rL~5*ZAyIKzd}R% zfF>m_qUyeIkUk>6C{HF4AT6L|XsD`Yh9#XfUs2?yiY?_P8PXr7G#CAWYWrEq!&CQ7 zx#Ow#cFie=0-W+sMw9%5o^iu4KXcvF^=!ti3UazK8d>r3@|Qe8D+wEX3$MOU-h6?! zO9*^|h7t#%`2ogh(J~c~*#EDm|GaD$E`a(_K2B=Z1>Jqv%~3$}Bi^faBo@T%(0lry zVR2Bx;#b70B^F(jeR`kDqk!G&{?Sr(SOEL(;Y$=IntK_*?U{C3z?xy^3;sC~5E>eE z5(aNKQ%D8W3M**rRy!WwT)t6q@D+Bp1+3n*fIK@Kj?Zj~A9c^bt8MZuWI$hhpB-+a znkmvUFx|948sJqgpSBq{VJoi;U}KXQ2(7{fWsT)=F||X!wJG6rkGB-r`=UEThj1hH zCTAl9o85ct*WN8bOXAClqt_@*zta8YupcFIpT>WP{6-iyOF$b?t7# zNkuUF!cTa@yqONzTU^(l^4q27p-Na56p#Q{@oq-k-FPa2ux49E)#Pq(PzPV`tE0@} zoUd~lZGX+r4Y=hLNsCAIwG0)apCnD1k7op#U62A3bD`6`$L#K{{(yAi&)putohe!o z&eukcCEq{pOrdGYpbi)mo-3p$`Z38I`xKIN zr-WqYN#QB59B70~Do)l+DmJPMT*gzLgq0N%AERs!K9ai8FN(B8RJ2rl6&vo;|-*rw)>e zrON_wz_-7Zm1(E3IG*Q!oX#eL%OgPhoGg3FG{uu(>(FG$+`E0Q?XdHKJJDlwVDP;nJFzS|L3!i&4OgBsT8hsM|*4g5~8kPm2i-s^#Zpa!#BOM)C$$|NdZ z|FH@Fj4Fxz&SBl(Xk&h!9Ofjo+LGK0i$awIjeAaj2}fEh1%Z3-vnT&D2PKXci-5}I zKsjl1_j}X1gmz^9g_u%#8ALG|dz`25Q`ZTB`PU4mR_3DUw~YW5W0}pS%2ty->f-a{ zU^d^jhQks70?4!vF>loCLrqzg(gJ(USil@`^&P}+(s2!6+f+A{2IZZa^+^ZnOnzP< zr_vUW09iUHfvQe(Kir9fCaPO5S>gbO0dP)s3jH^c=W5XODrwNqpqgc|Wy{w?vnZq`tFvgrn zAV-1w-KUgs^VXz5DNuzJa4)jvo?4*~H5l8#1e6Ce5!a_rf|bmhD;!d_&ADymaiT9K zkpGb%Sgy=@^}{HlngPf{=bXQbV4Vi^fh;+`oWuKBq$${y9Rwf6GBjIP0uK2@A7v{6 zdvh%J1|)!m6(xwWGncT%QTQa2utwOoBuR?^3rh+3VRB7kjGI=%CZ(3y7wJCMaj+=J z*K|wB$5FG2tI9GVgQ7m|u-ROH{e#dnIGuOaWP;XO6{H=jr0*4f(JDs69yvuVJc=;T zjA(LnU3`iPhsdCg#*uq4hr{ZoGGw8cdA@2&FT*&yv*{t{luXx#PQ-Qn7>q1mfu8oW zq7$<7TP?JLmqQg)X|L?sjJBc4~;rMlS z8|UQ6$}s-4mjM`Hvpu0w*@82nR75}ceZ~C1(j35kc0X$gaumFtonfU#;o+iGv#FBy zI0<3@Sq?+s0{w@^Cy-4GL^5%Hh&DB7SpH8`y0z_Mkb}`j%Jaqp%PlTYx<#F~8_L^} zzEQ<-#ESzW*tW|l0`lD~%8`1lmts^a^*2Q5tya9RUxc~SUed-X-R|UYllkMiXo4nE z@s|7DRq<^wMpP%m-Zs)!&2IPG@l+4gsE#8Wq2%it+pc=kaX zPo<%YD-9h=*Wl1e@OYB^)&d4JDdRsA=Na>no-STDWB#)g2AKn0BMPyW@9O9I z!o#9K=_n}>n$<&<{HjaK)bp`YDMgS1Jazw%FKynnPequ z0K&rn=xdZy;ACt>Abf-_vE26P<-7<6 zp#NY*^0hp(xW<7uEEQ&48oc=r6>Ind(9yjmFE&Cs(~569MvK-yg!FJK2DvW%hsI9`fHMNj2oAyG zhBl0cow!$rA~2CQo@NSE3og~OyHt7mB8}!tuRDxl=uChx?<28SETfj3+O}6!m;M}^ zw`9G{78WLV0|aL+mY)kijK&%Zmhqeqny=4VE+==L`da2F^=BRpu&xDI9j%Ci+5&f1 zgY|M$+L(j1lU*)l!3tD9E-x>Zp`y2~94KoyKq%^z3{9O47j1G_RUx!Aj$CA5e<@{OI`mi-G}nA2Kw z`n=YOGUK`R5l6ho-Yd;ciikDUwZtRa%Xi7z5#8?Z3HXj{F6L+>Wrar^TeE;G#YBFQ^p%%KG1QQXh_oO7nh@ok zJkQ#c;xY;Oi@ZY_y1ANVm|qYZGz){w&7RujLiH0iT|#(jF&}foPw@QRe9s4#>7mu3 z$CO0tR{>F-?kMADx?`dFb)LVoctHke$)Ek{GHzy|GZtL&b%DbYbCBNV`IW??_rJv$ z&Qmy-bmHdtN!mO8Cy6X6i3PUdMr-2i&Vzi;K4^(Yl-!9sC;ch8cqaIrJ1h02XD z(3ciVubGDeNi)}3q=&*&lxtIzDIV?l3WDc-B1@2w2pywUv!W{{O+ZZrI8WJ;1Qpd^ zQT$W5RCAdu28vM*n)nD1eQcR`5$7mpMJ(2p#d(0}p(WqHfFj1hq?P+V>F-s=^7KIw zb*}H*Ctg2>ZkQGr^y_=KNTJ)*>V94g-P&#XA9xuj7RMJoA<~(Yr~C~KWAta z^f@VdDDlw7U^jm1d394CHX#R1Xn??(6z->SZw{oG}8FFGn&kWM-UQ9y`k8$j1{ zth)!qGECea%n%5%fr6_+quxjnAyld68X8adYe!q&H3l)#^`Fh)gjQo;V=sEKI=B zTf=~>;S!ReLa2$eXt&5tqlvDyZTj<`b!0d;)mI*0Is7dR>F&nfo4J{Ak`8@kGLv2u zLon!3>QhCgbGWRo`8336`sheNM%SqAc6Mv*hFt&U zbG2YP-79N4>GAx;(8k|Zsb!J4AOwDvBh*Y+9-M5-jUPvRrNfL^!Yc;n+yM{He@XnU zDWiE$u_mSkT(NPXhc&X8EBAPPWH~eL>kB;ChZsHk@m5jGXImrm-2J(R6(NYNHfvZX zYmyY>5k9z*puvri*ZPpC{+6(Nud3q)4lBVBAEBOXc(z0N^~F>bt;YkplrE@s_+@Do z`s}j@!8_;Ww0+CV5@g{e`P*72)8;-O>7;wE^S-#O#Bh*+^pe=PLS*QNl-+&4rH)Nw z4#1D7Z+XJMNjbehmAmiu`PDzOsFcJbDzq|ZduUb24u)-uSwr_I9(Sc{$bkSDL-F$V z6M`Yzw)B*H*waJ_4YR(!zsyO31M?2}3hAGY1U55jS0F^qnC{xmkQLstn`?mkLZ^IiKbiCtCyaoQa?DU0iD0GP*iIB9Aq6A z{)_>94uet(>v*1fZ@gn%BL*M8a1V|J0uL2n*z-k~$p`P_4yB#Ig$l7V<`WZOru}7W zFOFqCZs@>4{tQJ_SOP7_BxmCCn2Ey-f!*%}0Arxak8CwcJtE zR+k#33+dE$21FEN@R)F<$Gj|vv3j;lUtYKEavQ_Jwx_so`hXtxurbdC90gB;!#+he zn)Xsx^(7|&V63?uxx5B37B0%eb9OI_ZeN3Rl3|YNF+p-q7h4UZ1j9_awOX_0h16YZ zd(qG`p@(3FqyFDA_)N2>k>FkDd}MOkU9kiUn}t({MAU#eE>lt2?a?#NsdEPf*Fzun zzTZI;>C!RGHyo&l&<2OXFZ6a}!$-igh(WXYn;OsJ?71W`^~zvD21ZwCoEwKLoEVs%#q51J4pJ6E#$Z-urw+Y9MD;hfjgm>xFAagC>2S)qUFdEA&h7kmU+B2B>xm1h7tjTL+syqBpKYR!Gw z7#Bi7-8goSMem{ki_faeGr%Bo^(go70o_v`9?6m$&EJ~*oj}B6HL!~ZNS)ogH$CNj zNLSdF8>s=*LXT0zt?vlKkoZ@zw;WLa`s1Q~PtfQA))yW)uxgICV2F(W_p58M)LW%N z!u<)bGO;nZa!E!x%7vhKVJDzFCq-OfsLpfrgm!1-CV|L@CwB6nVoxJDkqtC0Yr|IG zd!7*z&i)@Z5y8K0cVCcAN9}Z$k9=LZlCkL)<+zM&^p6lr7k5Zm6=jD;>x`DL zev-mw-GmGrXL!a-Vrupj((TZJxf9|_+Ai>pbRJKnme5NOhnLPV$#HxUeIIT1+)~bN zI`R(wqXz*5^SZLk+A^yoCtO=74XIQ=Iz9&T^1sm>{gB+KQG82Xe&T`W%l0_@aA_iZ zpZkXG*?;NE@1YQ&RvX%;R=nq5a@T<-^t0ycKO$5CKAS7fVg3UclGy)8d5y_{yP}Ae za}>`WVUGYBnspXbUB87=buk%0k(D{V^0koXr}Oob8m;LRz+i z=Oo|FDwZhGs+(L*B7I8q>ZwZSEC9%AZ*mIE9gSW{}47XPRUDW(#Hg z{F1b&a24xd8O&ESAFsoAPf3Xx|hX~@c@Z{kvw!`+65=X?SEL@=L2xB zX#Crv`zb*CI|Y`7Bs2J>Rx@PkIpfL;lsx)>XArb$cbyM3&udRxMt}4LzfY8rFa;RU zmHgv=&Ln_^SB30uH4_oituB}>sWYd_y_}MJVR4$^zimWgeuVi7I$97{i1s#})U&Zk163|}0Ew;j0` zbU#PbeN3`}u7K=|iig}BuAKAo@?5>>Z`xN#N#`1#2`n~(y^mKMd$Gb>ycikxFAklE zlPbba=k>mQJJ9uE@HH$y@VrfV@!|y~@M=8+AJYr4L3`$327EAuz4YAtGRlT_oG3<0 z6B6QzoxNr6_f%DiEY9pJdrvf*CAt#+P{x9o9aD7iYmm$S!_ey9jZar?iWvTHCw4Z% zycgly?yRl50(sm{Ab#J|(t$acDMmy}d-Q0yYP5w_Rcrr@(XopW7mYV8xd+K%W z?gfb|@!SXoWkg@SX(?ei(MsK=K*qDSnAem^_rnBf!>h|k?lFbXH{V-&a@_TcD`o$k zfa3YQ?GxR{i}e>+o-eC5P3u3;TDRMzw9|4?!ves;fVb{(qkZw-XRB%eBE52JZZp3M zi$FOv`rs$sQwv;+`%hNUf{`jjHnwCj83ra=hc&$~JXOdBLxf$R0v9NvE4#)#|_+7a^d(4E&98@Wo(1L4QHjST zEoP|T8xvb-h~*Ev*(7~@dpm>d>8#0ytJaIt$L1~0ewg74zPW|(VM7c&gxOc>vi{fQe{09CBn^ElG@nd*mT$Xxs6BH9`^h@@Ehr#}sR@gXt>B?lq zNrxSvo6^Mmrrc))BHM7rW83{H3`zIoao;EqQ3UDN?H=Z@wM^`5Z+{x?PqsGjcVKNG zz_qtXujFJ>%H<^> zcJUsHw-V`;;+)IjM*L)>&whwrw6a z`}%JcYbaKAo!DQShzo=k-MvP`(1FDslKej?ro4H~y-stD{q?&p1fpxKK8`sqzI1Sr z^8F+iYv^z&ty8wEa_44_wQ#+PvQIE*2VZApOS(!~F0fcN@iXG<*cXP+-qN#<`-bX1 z`GAdC4;ELBfU>PP!V6-|G4^@pDmJuPS-++P&4GY7LU41F=r>wz#4ngldX|6bDzfxH z@zH6_m{;mFKR&0fp5&P3_|$fP$@uAHYnz?lI6R%7ne$RZIDNQX!XL`^1mv#(g~jOV zy=TG4C$^zwONhMQG?s=r>W%T9ePLWhk8mTrR#mi2DZ?E&9eC*@I$l^~5ihmLi6Ifo z_8V$NJUP3r=9#Wc>4f!_^Mw%SCNevl=vDeuJ%9SN-o%>_+3hp=G(u@+Q$IO_}siMDs zTd9ll-uM$gL(-0i%HA2R7A0P&bT?pg&lV=5#Q|xzG0e|Cqj%xs^?nF8YQBt)yuX089SXe>Aq5QwWkCDO&Cgr}9Bn$Ks8|bSDJy-j zK=oCjaaPO6M?v4CqCL*KoOokKncu6|$wa*COZ$bo_BwSs^BKGrWN)H@uJkFeh=?W# zGr#@tetA#lnUIxl9@8}!cYwK#XIaR?m0+2~_#`Hv>$&$cK-&qn;A3&Z(#eeAcNobl zxfEo^BC4zntd!Rd5?Gt^(T)Z{ht7zSoTZC;C8uI?hoWCJfYw`ThX>rVj+-ifoaUuYhK}x5Xeh4mKApU~x@88n-JW?{L@$-V z$;8Jm;n&SF8&1D;8#6o86^|}Bk~+uKl$*d`du>;2ObXyf(ii4OYT&zYzAK@2zLA)tMypjx|Wo zjK6OLd2is*G}rPRa{CMdKddu?A{kdTScExdSQ5SWHp6U5ZQKFFa?iSjHPds4QZ_r-u z<1bwlC^twvJ6S7&4Io>+if^SQ9*)`~?4vu=Vfow{TdR)bvWR!Jcqwe^#XkwtuWOZg z@*Ct*FT&fh2j7HE$%jICUJD{@umrCU(w90Fc(CD0tdPa}h$Moq^It0n-G1(g{8=4FYPtZoOP^uGs)nhy>J8}O3?_Vk zSiahFcVy68Zqr#go}|$4On*n%(=BSjT~t>*Un`zyatPRIb(T2H@Dy}7@Lre8Rz9O| zTl(bkP3ejyXXgT$Gdw5x#70rw&4=*SYS+_m>F1|}{yfQXTxS9)j#^7_Je|ar%2|nk z%JfB=sK8O5o2Sz0$(A1?k}M%i%x+_l<_l(=q1_N&l%q*0E0fG=+p+fLdQ$1c6& z3eG_!+Y)!r@^Y(eP8ZZ}_1>TI(PD`ax?NaO5X`rYE&kp8$87n0Kk7<$(ix%GCX7x? zi9-lC64Mx_D-%A078XH{iJ!OlUa+&&y1L&E71+-g7t-cD)@;JIUWrntmZG#9Pz1pL zxeL(vg&<*K2Tztd_d{&7$nzMIZ$j15cNnpTJFthAeVs+517*vk%78$^^B=N{9SWl`s36324Kb$17Q+$dAZ92>{;7ZbAXT8RL zwS&hfrS6<%WCguzqq`%DGGTuIKXMI1euTcPm}&A7HZN- z{X`%v=)(LN6D*#+jZukaA#z=2TowwkXOqFHJ;4;pI;R`N@3IZGrPT~~e@Bp=+ z>|;bImU6tOiC%OQ7ts3``XCjMS(H#{ZQ$p*zk(5OX4plp-~#m1z^7CIC*`RC!#N;} zjpQ?cLHTQuMRq{^N8fi6%5)kc_6&+T3BB2Zb6V@w|8?OHbQ$jB#9&<8w4j^v@^(sY z(Rnm-WrZS5*cLCxyqPg~Sy}|$zsO9eED_=waj(Lu(KJ?oG-hj| z%7s;V!38H;={TS_0N-v{JR=)J7;#ITLn;rcz@~%K(h{Y>aunr_2Nw_C=2{#B>{rIJ zFgA#&H%?Ji{7aq}*YehRg|YL3v(4h%qONwX0LD+zURttghqmU=*qM431FDvR*|~vQ zc|-{w{JQ}P6ycS2)gWa5Srj(IAj2!pS3oogmE5n8L~)vF6DxOW##4)=&9n5|gP#m8 zZsv(L=qYd#VO+iiTIoIXv8m9_V0cJ|!jZFM@Y%~|QIEX>oRwmg_3BO-kGJxe^vUyW zWbHD$*+_2#df2P#@dJK~<(Fr9hR)$U7I|X^A|Eosl7z#IRKuU?l7sDAB}2osusoF0 z>A4rM6yEKvXCzp7=F>b|qY^gN68!V_sTekhDj%pBXp6PQsSb*6l6uldZBWH|MvQV8 zV#+UjcI}WcYf1e9dr&tdHm%d-I{8E8yS>P9$qgqY{s@<64CMzGiy*3^v$9|Jzwb@F zgMKE1p^&}(#K=;^pLu})+U<5VC7)E8LO>fKz%Ji+LbGC##+Lz`t3&+Sf+;V!^5hOl z76Px4vOh^P!h=52{a{U^wsRXx{|;L_T1FpGFxo9mUr7FXL|Ew+)J?kujvoUVzH6Q2 zw4sTrb5kMnJLAVE8N&jf?AyC=(iV#!x|K}RMimkgLSFEOiN8L2E(fX2#CKkz%zf>7 zgb21ToyG%U(pS^)**gajy-Uy~envAJ}*U)6bg>dCf7dpjQjNEam(gX03e z(9gE?NwM3r&K@rk_0)qVRANCVqN5YxAJ?X+oBcseYx0&cY6S_2K{5-P$;@Aom=N$y zqWfzlv&VO#Dti)7MJEUuK{H6MSfom{Qo`+4LmS-_b6>a1=POP|I$pDn=2wk@aU0Z# zoKo053$oeb-3eVjt*KaA+bte3daHvw&3{9W@%9@=MT0%zuZ3xYK@c;i`dYi0oAa{e z8hq?OQHJqY`|~1EolZH3*`ntVFb45DcOi6x+yW{WbHgxtt6S!Im@)44sRw4rtP5v7 zu5JU@X!`c%F{hP}OAR}p;0i#iuW6Csm9_!gAdXxqbj6lCj>%I#VmG71-fq3C!mUnz zSG&yN-*~$};tO_v@+9ecDjUG*pUgsI$Di{lufkkEAF=jCzVwXhD?QPpx2<=&?@P#SmH(j0Sq_N}eb-hfBI+McWnoxg3r}-mV?vJaJ4^K8>U3ZlF2Bq4>QNqp2I#Bc3 zE(0O=Kb+nh&$JvRel(AakjB{5D^cRfr)f`Gekl(rI^L{*59ZMVZX|j2o&Htmpj9qC z)Z%^dyLimw9qR1aEQ>Y7c6KPQn}&>hzwYx}kIKM)LMIfM*dpTx6@~5T^10OqZ+p{^kAI^GjZ03u#tXi9 zdX@5g8;4q9eguT1)w+L$W@p(^1OpZ1OYmz3s6K&krxuL9yviZiN0L9Xw!xQmq6_ zcpeWqeFX*nC}z|bKe)y%8(@Kc3Mm_1Rf$IwAR-Z|HaR@GBr9;&RCC;o9g#nyx1w9r z5BXtEbYs2*0vJ<2cat5y6`cgHgysB{rVFDoIhGFz92Kt7kqrGNTAAVYC&Rgth9vNl zrYE>-Ql)=X=~v4^QZYjRkEv~Hm?Bp7x1iC_I2W|O(PxKxSd|$f6IUil_%*<%X&OJRl zJE%Yw&!%KnH=T&-k!jD%<6wm-zpT*xRj(yaO#Ra|vzw~|tD{%fmaG(ssV#XP;?KEq zwyu+%Ex~swp@CP9{^GDnE*v0;E=VL3&ecYH??Mw9@$}d^%Ar1_SAK{~OOm1VsYjVa zg-)=9pR|wyUcmU>d$;HfR>L)x{x`!CG0)Y!$0rK|_%H~J;h$eoeN9!GU+(_xSknke zNf*S4ibsdq7Abo-EiK14eEXHDPwyI#A^+L(qKAz>erhPf<_?|tE0yiP;s;YW;q%a~ z(N5y|)CoKF-qI`A7T_t43wyWe{5ru&tTmSnivD1(+4{PN6UwKD}eS*F+bY@RvA z0z7rhPk3~nI1?hDZD%=3P`)khE55|~aaw%GdNB&CbvX##RL||-m3aIulmnXch-3~h zeXa4LN(3Fo7dH2yqF0#M#hO(6`4J13GvckBdQa56^SJAzXx5?S!jU;=UaCgZZjtcY zWuyX}?{biplerllZ81RR%pTQ!Ed7?o`^pE(cS)|6Kn)^ita4GzC^OdzlB-_QY!8%G z?x?SyZZf*oWGp97H=K(HV1>M1i|Sml7-F-r3xbL~DK}HbD56FY!0ZG!oGwARPvb&7 zTri@l7P(lY6Z4 zlIhC0ynN`N{`_&^Pz|s|KbB8#=Grz+SS-9$y7?eX(46}i<@U@hb`Tg344}`Hr%RMl z&&XJyW=ET=s4MD=?LXXfcU3G!x5Ap~IuGyWejVEw29NoeeUFKVge{$avWJDVFO@Fa zF)sd9VZ=Ws;)&0g8z+JA=19f`G0SsF7~nZ3=ifQc4!p!o zHUqGG8Z`QSuLB%BH7rkj zqkS%X>LGKI?E7y0#g=vAK67>wlmL{5d7b_F6@H`+rI?Q~;@s@g z(P3UyriDMw)VQP5=XEAont8*(VdJ@JIjV~S%+9@{$$osaX}UB>1nv^hsEVLR$N%G}AE=y5A%Y>uD=%HHXfIxDRNPLGwzh#{GRC0ldnqz7Ya9ws(qf zT1wT(g%YgO8X=4vYt#NjT552-&^J3Fg3uWkzgle1eWe!Fm|lXpD*Td3s+1#q(9B>IhMALCC?%MVJA3}TJ07I~^16!q;AC!HKj5=wd{;>RiC zE(n!>!=PLIEK$RLF^mJd)<^nuCzf_dE~aPBOEodsFY8iPfFLhe08vX94-;d7G}y4?zRJhV z_pJ4aykbg_ZW@+U3yD)e_t(IXKZ;#HFmh5Hh;NX6k1HH@75gkF_y~pDig@hAb$$l&H>T6>aV|n4E9^sbbT3jT>LIiCuA3?nOt)2=CjV!h?9a65 z$z-UV1~oxD#I)7$Dq5!GyN6kWXqy96tt5U_vOLn z4*5p`a-RdrU0v6}1%dRhZ++_z-vL<-+*Qf3_NoVW-vRwt?o?UPX}?9nI1>a*q;tl_ znBS_W{6#au*xs2=H#7)<3S%6Pa@Rl4EIoXdaUAAPhXrUBhpW23Mum%$x}#!u

yF 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%0D<0%%fj(azdY>= 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 literal 0 HcmV?d00001 diff --git a/generated/ErsatzTV.Api.Sdk/.openapi-generator/FILES b/generated/ErsatzTV.Api.Sdk/.openapi-generator/FILES index 410fe939..bbda04b5 100644 --- a/generated/ErsatzTV.Api.Sdk/.openapi-generator/FILES +++ b/generated/ErsatzTV.Api.Sdk/.openapi-generator/FILES @@ -11,13 +11,11 @@ src/ErsatzTV.Api.Sdk.Test/Model/AddProgramScheduleItemTests.cs src/ErsatzTV.Api.Sdk.Test/Model/ChannelViewModelTests.cs src/ErsatzTV.Api.Sdk.Test/Model/CreateChannelTests.cs src/ErsatzTV.Api.Sdk.Test/Model/CreateFFmpegProfileTests.cs -src/ErsatzTV.Api.Sdk.Test/Model/CreateMediaItemTests.cs src/ErsatzTV.Api.Sdk.Test/Model/CreatePlayoutTests.cs src/ErsatzTV.Api.Sdk.Test/Model/CreateProgramScheduleTests.cs src/ErsatzTV.Api.Sdk.Test/Model/CreateSimpleMediaCollectionTests.cs src/ErsatzTV.Api.Sdk.Test/Model/DeleteChannelTests.cs src/ErsatzTV.Api.Sdk.Test/Model/DeleteFFmpegProfileTests.cs -src/ErsatzTV.Api.Sdk.Test/Model/DeleteMediaItemTests.cs src/ErsatzTV.Api.Sdk.Test/Model/DeletePlayoutTests.cs src/ErsatzTV.Api.Sdk.Test/Model/DeleteProgramScheduleTests.cs src/ErsatzTV.Api.Sdk.Test/Model/FFmpegProfileViewModelTests.cs @@ -71,13 +69,11 @@ src/ErsatzTV.Api.Sdk/Model/AddProgramScheduleItem.cs src/ErsatzTV.Api.Sdk/Model/ChannelViewModel.cs src/ErsatzTV.Api.Sdk/Model/CreateChannel.cs src/ErsatzTV.Api.Sdk/Model/CreateFFmpegProfile.cs -src/ErsatzTV.Api.Sdk/Model/CreateMediaItem.cs src/ErsatzTV.Api.Sdk/Model/CreatePlayout.cs src/ErsatzTV.Api.Sdk/Model/CreateProgramSchedule.cs src/ErsatzTV.Api.Sdk/Model/CreateSimpleMediaCollection.cs src/ErsatzTV.Api.Sdk/Model/DeleteChannel.cs src/ErsatzTV.Api.Sdk/Model/DeleteFFmpegProfile.cs -src/ErsatzTV.Api.Sdk/Model/DeleteMediaItem.cs src/ErsatzTV.Api.Sdk/Model/DeletePlayout.cs src/ErsatzTV.Api.Sdk/Model/DeleteProgramSchedule.cs src/ErsatzTV.Api.Sdk/Model/FFmpegProfileViewModel.cs diff --git a/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Api/MediaItemsApi.cs b/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Api/MediaItemsApi.cs index 4f975fe2..183fd463 100644 --- a/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Api/MediaItemsApi.cs +++ b/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Api/MediaItemsApi.cs @@ -30,24 +30,6 @@ namespace ErsatzTV.Api.Sdk.Api /// /// /// Thrown when fails to make API call - /// - /// - void ApiMediaItemsDelete(DeleteMediaItem deleteMediaItem); - - /// - /// - /// - /// - /// - /// - /// Thrown when fails to make API call - /// - /// ApiResponse of Object(void) - ApiResponse ApiMediaItemsDeleteWithHttpInfo(DeleteMediaItem deleteMediaItem); - /// - /// - /// - /// Thrown when fails to make API call /// List<MediaItemViewModel> List ApiMediaItemsGet(); @@ -78,24 +60,6 @@ namespace ErsatzTV.Api.Sdk.Api /// /// ApiResponse of MediaItemViewModel ApiResponse ApiMediaItemsMediaItemIdGetWithHttpInfo(int mediaItemId); - /// - /// - /// - /// Thrown when fails to make API call - /// - /// MediaItemViewModel - MediaItemViewModel ApiMediaItemsPost(CreateMediaItem createMediaItem); - - /// - /// - /// - /// - /// - /// - /// Thrown when fails to make API call - /// - /// ApiResponse of MediaItemViewModel - ApiResponse ApiMediaItemsPostWithHttpInfo(CreateMediaItem createMediaItem); #endregion Synchronous Operations } @@ -112,29 +76,6 @@ namespace ErsatzTV.Api.Sdk.Api /// /// /// Thrown when fails to make API call - /// - /// Cancellation Token to cancel the request. - /// Task of void - System.Threading.Tasks.Task ApiMediaItemsDeleteAsync(DeleteMediaItem deleteMediaItem, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); - - /// - /// - /// - /// - /// - /// - /// Thrown when fails to make API call - /// - /// Cancellation Token to cancel the request. - /// Task of ApiResponse - System.Threading.Tasks.Task> ApiMediaItemsDeleteWithHttpInfoAsync(DeleteMediaItem deleteMediaItem, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); - /// - /// - /// - /// - /// - /// - /// Thrown when fails to make API call /// Cancellation Token to cancel the request. /// Task of List<MediaItemViewModel> System.Threading.Tasks.Task> ApiMediaItemsGetAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); @@ -172,29 +113,6 @@ namespace ErsatzTV.Api.Sdk.Api /// Cancellation Token to cancel the request. /// Task of ApiResponse (MediaItemViewModel) System.Threading.Tasks.Task> ApiMediaItemsMediaItemIdGetWithHttpInfoAsync(int mediaItemId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); - /// - /// - /// - /// - /// - /// - /// Thrown when fails to make API call - /// - /// Cancellation Token to cancel the request. - /// Task of MediaItemViewModel - System.Threading.Tasks.Task ApiMediaItemsPostAsync(CreateMediaItem createMediaItem, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); - - /// - /// - /// - /// - /// - /// - /// Thrown when fails to make API call - /// - /// Cancellation Token to cancel the request. - /// Task of ApiResponse (MediaItemViewModel) - System.Threading.Tasks.Task> ApiMediaItemsPostWithHttpInfoAsync(CreateMediaItem createMediaItem, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); #endregion Asynchronous Operations } @@ -315,127 +233,6 @@ namespace ErsatzTV.Api.Sdk.Api set { _exceptionFactory = value; } } - /// - /// - /// - /// Thrown when fails to make API call - /// - /// - public void ApiMediaItemsDelete(DeleteMediaItem deleteMediaItem) - { - ApiMediaItemsDeleteWithHttpInfo(deleteMediaItem); - } - - /// - /// - /// - /// Thrown when fails to make API call - /// - /// ApiResponse of Object(void) - public ErsatzTV.Api.Sdk.Client.ApiResponse ApiMediaItemsDeleteWithHttpInfo(DeleteMediaItem deleteMediaItem) - { - // verify the required parameter 'deleteMediaItem' is set - if (deleteMediaItem == null) - throw new ErsatzTV.Api.Sdk.Client.ApiException(400, "Missing required parameter 'deleteMediaItem' when calling MediaItemsApi->ApiMediaItemsDelete"); - - ErsatzTV.Api.Sdk.Client.RequestOptions localVarRequestOptions = new ErsatzTV.Api.Sdk.Client.RequestOptions(); - - String[] _contentTypes = new String[] { - "application/json-patch+json", - "application/json", - "text/json", - "application/_*+json" - }; - - // to determine the Accept header - String[] _accepts = new String[] { - "application/json" - }; - - var localVarContentType = ErsatzTV.Api.Sdk.Client.ClientUtils.SelectHeaderContentType(_contentTypes); - if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); - - var localVarAccept = ErsatzTV.Api.Sdk.Client.ClientUtils.SelectHeaderAccept(_accepts); - if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); - - localVarRequestOptions.Data = deleteMediaItem; - - - // make the HTTP request - var localVarResponse = this.Client.Delete("/api/media/items", localVarRequestOptions, this.Configuration); - - if (this.ExceptionFactory != null) - { - Exception _exception = this.ExceptionFactory("ApiMediaItemsDelete", localVarResponse); - if (_exception != null) throw _exception; - } - - return localVarResponse; - } - - /// - /// - /// - /// Thrown when fails to make API call - /// - /// Cancellation Token to cancel the request. - /// Task of void - public async System.Threading.Tasks.Task ApiMediaItemsDeleteAsync(DeleteMediaItem deleteMediaItem, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) - { - await ApiMediaItemsDeleteWithHttpInfoAsync(deleteMediaItem, cancellationToken).ConfigureAwait(false); - } - - /// - /// - /// - /// Thrown when fails to make API call - /// - /// Cancellation Token to cancel the request. - /// Task of ApiResponse - public async System.Threading.Tasks.Task> ApiMediaItemsDeleteWithHttpInfoAsync(DeleteMediaItem deleteMediaItem, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) - { - // verify the required parameter 'deleteMediaItem' is set - if (deleteMediaItem == null) - throw new ErsatzTV.Api.Sdk.Client.ApiException(400, "Missing required parameter 'deleteMediaItem' when calling MediaItemsApi->ApiMediaItemsDelete"); - - - ErsatzTV.Api.Sdk.Client.RequestOptions localVarRequestOptions = new ErsatzTV.Api.Sdk.Client.RequestOptions(); - - String[] _contentTypes = new String[] { - "application/json-patch+json", - "application/json", - "text/json", - "application/_*+json" - }; - - // to determine the Accept header - String[] _accepts = new String[] { - "application/json" - }; - - - var localVarContentType = ErsatzTV.Api.Sdk.Client.ClientUtils.SelectHeaderContentType(_contentTypes); - if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); - - var localVarAccept = ErsatzTV.Api.Sdk.Client.ClientUtils.SelectHeaderAccept(_accepts); - if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); - - localVarRequestOptions.Data = deleteMediaItem; - - - // make the HTTP request - - var localVarResponse = await this.AsynchronousClient.DeleteAsync("/api/media/items", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); - - if (this.ExceptionFactory != null) - { - Exception _exception = this.ExceptionFactory("ApiMediaItemsDelete", localVarResponse); - if (_exception != null) throw _exception; - } - - return localVarResponse; - } - /// /// /// @@ -644,128 +441,5 @@ namespace ErsatzTV.Api.Sdk.Api return localVarResponse; } - /// - /// - /// - /// Thrown when fails to make API call - /// - /// MediaItemViewModel - public MediaItemViewModel ApiMediaItemsPost(CreateMediaItem createMediaItem) - { - ErsatzTV.Api.Sdk.Client.ApiResponse localVarResponse = ApiMediaItemsPostWithHttpInfo(createMediaItem); - return localVarResponse.Data; - } - - /// - /// - /// - /// Thrown when fails to make API call - /// - /// ApiResponse of MediaItemViewModel - public ErsatzTV.Api.Sdk.Client.ApiResponse ApiMediaItemsPostWithHttpInfo(CreateMediaItem createMediaItem) - { - // verify the required parameter 'createMediaItem' is set - if (createMediaItem == null) - throw new ErsatzTV.Api.Sdk.Client.ApiException(400, "Missing required parameter 'createMediaItem' when calling MediaItemsApi->ApiMediaItemsPost"); - - ErsatzTV.Api.Sdk.Client.RequestOptions localVarRequestOptions = new ErsatzTV.Api.Sdk.Client.RequestOptions(); - - String[] _contentTypes = new String[] { - "application/json-patch+json", - "application/json", - "text/json", - "application/_*+json" - }; - - // to determine the Accept header - String[] _accepts = new String[] { - "application/json" - }; - - var localVarContentType = ErsatzTV.Api.Sdk.Client.ClientUtils.SelectHeaderContentType(_contentTypes); - if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); - - var localVarAccept = ErsatzTV.Api.Sdk.Client.ClientUtils.SelectHeaderAccept(_accepts); - if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); - - localVarRequestOptions.Data = createMediaItem; - - - // make the HTTP request - var localVarResponse = this.Client.Post("/api/media/items", localVarRequestOptions, this.Configuration); - - if (this.ExceptionFactory != null) - { - Exception _exception = this.ExceptionFactory("ApiMediaItemsPost", localVarResponse); - if (_exception != null) throw _exception; - } - - return localVarResponse; - } - - /// - /// - /// - /// Thrown when fails to make API call - /// - /// Cancellation Token to cancel the request. - /// Task of MediaItemViewModel - public async System.Threading.Tasks.Task ApiMediaItemsPostAsync(CreateMediaItem createMediaItem, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) - { - ErsatzTV.Api.Sdk.Client.ApiResponse localVarResponse = await ApiMediaItemsPostWithHttpInfoAsync(createMediaItem, cancellationToken).ConfigureAwait(false); - return localVarResponse.Data; - } - - /// - /// - /// - /// Thrown when fails to make API call - /// - /// Cancellation Token to cancel the request. - /// Task of ApiResponse (MediaItemViewModel) - public async System.Threading.Tasks.Task> ApiMediaItemsPostWithHttpInfoAsync(CreateMediaItem createMediaItem, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) - { - // verify the required parameter 'createMediaItem' is set - if (createMediaItem == null) - throw new ErsatzTV.Api.Sdk.Client.ApiException(400, "Missing required parameter 'createMediaItem' when calling MediaItemsApi->ApiMediaItemsPost"); - - - ErsatzTV.Api.Sdk.Client.RequestOptions localVarRequestOptions = new ErsatzTV.Api.Sdk.Client.RequestOptions(); - - String[] _contentTypes = new String[] { - "application/json-patch+json", - "application/json", - "text/json", - "application/_*+json" - }; - - // to determine the Accept header - String[] _accepts = new String[] { - "application/json" - }; - - - var localVarContentType = ErsatzTV.Api.Sdk.Client.ClientUtils.SelectHeaderContentType(_contentTypes); - if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); - - var localVarAccept = ErsatzTV.Api.Sdk.Client.ClientUtils.SelectHeaderAccept(_accepts); - if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); - - localVarRequestOptions.Data = createMediaItem; - - - // make the HTTP request - - var localVarResponse = await this.AsynchronousClient.PostAsync("/api/media/items", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); - - if (this.ExceptionFactory != null) - { - Exception _exception = this.ExceptionFactory("ApiMediaItemsPost", localVarResponse); - if (_exception != null) throw _exception; - } - - return localVarResponse; - } - } } diff --git a/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/CreateMediaItem.cs b/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/CreateMediaItem.cs deleted file mode 100644 index d515d284..00000000 --- a/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/CreateMediaItem.cs +++ /dev/null @@ -1,139 +0,0 @@ -/* - * ErsatzTV API - * - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * The version of the OpenAPI document: v1 - * Generated by: https://github.com/openapitools/openapi-generator.git - */ - - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.IO; -using System.Runtime.Serialization; -using System.Text; -using System.Text.RegularExpressions; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; -using System.ComponentModel.DataAnnotations; -using OpenAPIDateConverter = ErsatzTV.Api.Sdk.Client.OpenAPIDateConverter; - -namespace ErsatzTV.Api.Sdk.Model -{ - /// - /// CreateMediaItem - /// - [DataContract(Name = "CreateMediaItem")] - public partial class CreateMediaItem : IEquatable, IValidatableObject - { - /// - /// Initializes a new instance of the class. - /// - /// mediaSourceId. - /// path. - public CreateMediaItem(int mediaSourceId = default(int), string path = default(string)) - { - this.MediaSourceId = mediaSourceId; - this.Path = path; - } - - /// - /// Gets or Sets MediaSourceId - /// - [DataMember(Name = "mediaSourceId", EmitDefaultValue = false)] - public int MediaSourceId { get; set; } - - /// - /// Gets or Sets Path - /// - [DataMember(Name = "path", EmitDefaultValue = true)] - public string Path { get; set; } - - /// - /// Returns the string presentation of the object - /// - /// String presentation of the object - public override string ToString() - { - var sb = new StringBuilder(); - sb.Append("class CreateMediaItem {\n"); - sb.Append(" MediaSourceId: ").Append(MediaSourceId).Append("\n"); - sb.Append(" Path: ").Append(Path).Append("\n"); - sb.Append("}\n"); - return sb.ToString(); - } - - /// - /// Returns the JSON string presentation of the object - /// - /// JSON string presentation of the object - public virtual string ToJson() - { - return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); - } - - /// - /// Returns true if objects are equal - /// - /// Object to be compared - /// Boolean - public override bool Equals(object input) - { - return this.Equals(input as CreateMediaItem); - } - - /// - /// Returns true if CreateMediaItem instances are equal - /// - /// Instance of CreateMediaItem to be compared - /// Boolean - public bool Equals(CreateMediaItem input) - { - if (input == null) - return false; - - return - ( - this.MediaSourceId == input.MediaSourceId || - this.MediaSourceId.Equals(input.MediaSourceId) - ) && - ( - this.Path == input.Path || - (this.Path != null && - this.Path.Equals(input.Path)) - ); - } - - /// - /// Gets the hash code - /// - /// Hash code - public override int GetHashCode() - { - unchecked // Overflow is fine, just wrap - { - int hashCode = 41; - hashCode = hashCode * 59 + this.MediaSourceId.GetHashCode(); - if (this.Path != null) - hashCode = hashCode * 59 + this.Path.GetHashCode(); - return hashCode; - } - } - - /// - /// To validate all properties of the instance - /// - /// Validation context - /// Validation Result - IEnumerable IValidatableObject.Validate(ValidationContext validationContext) - { - yield break; - } - } - -} diff --git a/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/DeleteMediaItem.cs b/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/DeleteMediaItem.cs deleted file mode 100644 index 5c5f8c71..00000000 --- a/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/DeleteMediaItem.cs +++ /dev/null @@ -1,123 +0,0 @@ -/* - * ErsatzTV API - * - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * The version of the OpenAPI document: v1 - * Generated by: https://github.com/openapitools/openapi-generator.git - */ - - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.IO; -using System.Runtime.Serialization; -using System.Text; -using System.Text.RegularExpressions; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; -using System.ComponentModel.DataAnnotations; -using OpenAPIDateConverter = ErsatzTV.Api.Sdk.Client.OpenAPIDateConverter; - -namespace ErsatzTV.Api.Sdk.Model -{ - /// - /// DeleteMediaItem - /// - [DataContract(Name = "DeleteMediaItem")] - public partial class DeleteMediaItem : IEquatable, IValidatableObject - { - /// - /// Initializes a new instance of the class. - /// - /// mediaItemId. - public DeleteMediaItem(int mediaItemId = default(int)) - { - this.MediaItemId = mediaItemId; - } - - /// - /// Gets or Sets MediaItemId - /// - [DataMember(Name = "mediaItemId", EmitDefaultValue = false)] - public int MediaItemId { get; set; } - - /// - /// Returns the string presentation of the object - /// - /// String presentation of the object - public override string ToString() - { - var sb = new StringBuilder(); - sb.Append("class DeleteMediaItem {\n"); - sb.Append(" MediaItemId: ").Append(MediaItemId).Append("\n"); - sb.Append("}\n"); - return sb.ToString(); - } - - /// - /// Returns the JSON string presentation of the object - /// - /// JSON string presentation of the object - public virtual string ToJson() - { - return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); - } - - /// - /// Returns true if objects are equal - /// - /// Object to be compared - /// Boolean - public override bool Equals(object input) - { - return this.Equals(input as DeleteMediaItem); - } - - /// - /// Returns true if DeleteMediaItem instances are equal - /// - /// Instance of DeleteMediaItem to be compared - /// Boolean - public bool Equals(DeleteMediaItem input) - { - if (input == null) - return false; - - return - ( - this.MediaItemId == input.MediaItemId || - this.MediaItemId.Equals(input.MediaItemId) - ); - } - - /// - /// Gets the hash code - /// - /// Hash code - public override int GetHashCode() - { - unchecked // Overflow is fine, just wrap - { - int hashCode = 41; - hashCode = hashCode * 59 + this.MediaItemId.GetHashCode(); - return hashCode; - } - } - - /// - /// To validate all properties of the instance - /// - /// Validation context - /// Validation Result - IEnumerable IValidatableObject.Validate(ValidationContext validationContext) - { - yield break; - } - } - -} diff --git a/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/PlayoutChannelViewModel.cs b/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/PlayoutChannelViewModel.cs index 2b3c355e..05cadfa8 100644 --- a/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/PlayoutChannelViewModel.cs +++ b/generated/ErsatzTV.Api.Sdk/src/ErsatzTV.Api.Sdk/Model/PlayoutChannelViewModel.cs @@ -35,10 +35,12 @@ namespace ErsatzTV.Api.Sdk.Model /// Initializes a new instance of the class. /// /// id. + /// number. /// name. - public PlayoutChannelViewModel(int id = default(int), string name = default(string)) + public PlayoutChannelViewModel(int id = default(int), int number = default(int), string name = default(string)) { this.Id = id; + this.Number = number; this.Name = name; } @@ -48,6 +50,12 @@ namespace ErsatzTV.Api.Sdk.Model [DataMember(Name = "id", EmitDefaultValue = false)] public int Id { get; set; } + /// + /// Gets or Sets Number + /// + [DataMember(Name = "number", EmitDefaultValue = false)] + public int Number { get; set; } + /// /// Gets or Sets Name /// @@ -63,6 +71,7 @@ namespace ErsatzTV.Api.Sdk.Model var sb = new StringBuilder(); sb.Append("class PlayoutChannelViewModel {\n"); sb.Append(" Id: ").Append(Id).Append("\n"); + sb.Append(" Number: ").Append(Number).Append("\n"); sb.Append(" Name: ").Append(Name).Append("\n"); sb.Append("}\n"); return sb.ToString(); @@ -102,6 +111,10 @@ namespace ErsatzTV.Api.Sdk.Model this.Id == input.Id || this.Id.Equals(input.Id) ) && + ( + this.Number == input.Number || + this.Number.Equals(input.Number) + ) && ( this.Name == input.Name || (this.Name != null && @@ -119,6 +132,7 @@ namespace ErsatzTV.Api.Sdk.Model { int hashCode = 41; hashCode = hashCode * 59 + this.Id.GetHashCode(); + hashCode = hashCode * 59 + this.Number.GetHashCode(); if (this.Name != null) hashCode = hashCode * 59 + this.Name.GetHashCode(); return hashCode;