diff --git a/ErsatzTV.Application/Images/Queries/GetImageContentsHandler.cs b/ErsatzTV.Application/Images/Queries/GetImageContentsHandler.cs index 636ee0934..e17c9963e 100644 --- a/ErsatzTV.Application/Images/Queries/GetImageContentsHandler.cs +++ b/ErsatzTV.Application/Images/Queries/GetImageContentsHandler.cs @@ -42,6 +42,7 @@ namespace ErsatzTV.Application.Images.Queries ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder), ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder), ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder), + ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder), _ => FileSystemLayout.LegacyImageCacheFolder }; diff --git a/ErsatzTV.Application/Movies/Mapper.cs b/ErsatzTV.Application/Movies/Mapper.cs index 59e83e29b..6a982d388 100644 --- a/ErsatzTV.Application/Movies/Mapper.cs +++ b/ErsatzTV.Application/Movies/Mapper.cs @@ -13,8 +13,12 @@ namespace ErsatzTV.Application.Movies metadata.Title, metadata.Year?.ToString(), metadata.Plot, - Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster)) - .Match(a => a.Path, string.Empty)); + Artwork(metadata, ArtworkKind.Poster), + Artwork(metadata, ArtworkKind.FanArt)); } + + private static string Artwork(Metadata metadata, ArtworkKind artworkKind) => + Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind)) + .Match(a => a.Path, string.Empty); } } diff --git a/ErsatzTV.Application/Movies/MovieViewModel.cs b/ErsatzTV.Application/Movies/MovieViewModel.cs index a854506ea..46fb009c1 100644 --- a/ErsatzTV.Application/Movies/MovieViewModel.cs +++ b/ErsatzTV.Application/Movies/MovieViewModel.cs @@ -1,4 +1,4 @@ namespace ErsatzTV.Application.Movies { - public record MovieViewModel(string Title, string Year, string Plot, string Poster); + public record MovieViewModel(string Title, string Year, string Plot, string Poster, string FanArt); } diff --git a/ErsatzTV.Core/Domain/Metadata/ArtworkKind.cs b/ErsatzTV.Core/Domain/Metadata/ArtworkKind.cs index 800c4d490..edd4c3e7a 100644 --- a/ErsatzTV.Core/Domain/Metadata/ArtworkKind.cs +++ b/ErsatzTV.Core/Domain/Metadata/ArtworkKind.cs @@ -4,6 +4,7 @@ { Poster = 0, Thumbnail = 1, - Logo = 2 + Logo = 2, + FanArt = 3 } } diff --git a/ErsatzTV.Core/FileSystemLayout.cs b/ErsatzTV.Core/FileSystemLayout.cs index f78453ecf..7c31ae6fb 100644 --- a/ErsatzTV.Core/FileSystemLayout.cs +++ b/ErsatzTV.Core/FileSystemLayout.cs @@ -24,5 +24,6 @@ namespace ErsatzTV.Core public static readonly string PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters"); public static readonly string ThumbnailCacheFolder = Path.Combine(ArtworkCacheFolder, "thumbnails"); public static readonly string LogoCacheFolder = Path.Combine(ArtworkCacheFolder, "logos"); + public static readonly string FanArtCacheFolder = Path.Combine(ArtworkCacheFolder, "fanart"); } } diff --git a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs index c5d6b2b3d..dd29814b7 100644 --- a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs @@ -97,10 +97,10 @@ namespace ErsatzTV.Core.Metadata metadata.Artwork ??= new List(); - Option maybePoster = + Option maybeArtwork = Optional(metadata.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == artworkKind); - bool shouldRefresh = maybePoster.Match( + bool shouldRefresh = maybeArtwork.Match( artwork => artwork.DateUpdated < lastWriteTime, true); @@ -109,7 +109,7 @@ namespace ErsatzTV.Core.Metadata _logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile); string cacheName = _imageCache.CopyArtworkToCache(artworkFile, artworkKind); - maybePoster.Match( + maybeArtwork.Match( artwork => { artwork.Path = cacheName; diff --git a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs index c1754f6c5..92163768e 100644 --- a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs @@ -80,7 +80,8 @@ namespace ErsatzTV.Core.Metadata .GetOrAdd(libraryPath, file) .BindT(movie => UpdateStatistics(movie, ffprobePath).MapT(_ => movie)) .BindT(UpdateMetadata) - .BindT(UpdatePoster); + .BindT(UpdatePoster) + .BindT(UpdateFanArt); maybeMovie.IfLeft( error => _logger.LogWarning("Error processing movie at {Path}: {Error}", file, error.Value)); @@ -139,7 +140,7 @@ namespace ErsatzTV.Core.Metadata { try { - await LocatePoster(movie).IfSomeAsync( + await LocateArtwork(movie, ArtworkKind.Poster).IfSomeAsync( async posterFile => { MovieMetadata metadata = movie.MovieMetadata.Head(); @@ -156,6 +157,28 @@ namespace ErsatzTV.Core.Metadata return BaseError.New(ex.Message); } } + + private async Task> UpdateFanArt(Movie movie) + { + try + { + await LocateArtwork(movie, ArtworkKind.FanArt).IfSomeAsync( + async posterFile => + { + MovieMetadata metadata = movie.MovieMetadata.Head(); + if (RefreshArtwork(posterFile, metadata, ArtworkKind.FanArt)) + { + await _movieRepository.Update(movie); + } + }); + + return movie; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } private Option LocateNfoFile(Movie movie) { @@ -167,12 +190,19 @@ namespace ErsatzTV.Core.Metadata .HeadOrNone(); } - private Option LocatePoster(Movie movie) + private Option LocateArtwork(Movie movie, ArtworkKind artworkKind) { + string segment = artworkKind switch + { + ArtworkKind.Poster => "poster", + ArtworkKind.FanArt => "fanart", + _ => throw new ArgumentOutOfRangeException(nameof(artworkKind)) + }; + string path = movie.MediaVersions.Head().MediaFiles.Head().Path; string folder = Path.GetDirectoryName(path) ?? string.Empty; IEnumerable possibleMoviePosters = ImageFileExtensions.Collect( - ext => new[] { $"poster.{ext}", Path.GetFileNameWithoutExtension(path) + $"-poster.{ext}" }) + ext => new[] { $"{segment}.{ext}", Path.GetFileNameWithoutExtension(path) + $"-{segment}.{ext}" }) .Map(f => Path.Combine(folder, f)); Option result = possibleMoviePosters.Filter(p => _localFileSystem.FileExists(p)).HeadOrNone(); return result; diff --git a/ErsatzTV.Infrastructure/Images/ImageCache.cs b/ErsatzTV.Infrastructure/Images/ImageCache.cs index 1896c92e5..050a88718 100644 --- a/ErsatzTV.Infrastructure/Images/ImageCache.cs +++ b/ErsatzTV.Infrastructure/Images/ImageCache.cs @@ -56,6 +56,7 @@ namespace ErsatzTV.Infrastructure.Images ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder), ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder), ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder), + ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder), _ => FileSystemLayout.LegacyImageCacheFolder }; string target = Path.Combine(baseFolder, hex); @@ -85,6 +86,7 @@ namespace ErsatzTV.Infrastructure.Images ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder), ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder), ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder), + ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder), _ => FileSystemLayout.LegacyImageCacheFolder }; string target = Path.Combine(baseFolder, hex); diff --git a/ErsatzTV.sln.DotSettings b/ErsatzTV.sln.DotSettings index 94ebaf1d1..02108aa4c 100644 --- a/ErsatzTV.sln.DotSettings +++ b/ErsatzTV.sln.DotSettings @@ -14,6 +14,7 @@ True True True + True True True True diff --git a/ErsatzTV/Controllers/ArtworkController.cs b/ErsatzTV/Controllers/ArtworkController.cs index a0a1105cc..423570b22 100644 --- a/ErsatzTV/Controllers/ArtworkController.cs +++ b/ErsatzTV/Controllers/ArtworkController.cs @@ -37,6 +37,16 @@ namespace ErsatzTV.Controllers Left: _ => new NotFoundResult(), Right: r => new FileContentResult(r.Contents, r.MimeType)); } + + [HttpGet("/artwork/fanart/{fileName}")] + public async Task GetFanArt(string fileName) + { + Either imageContents = + await _mediator.Send(new GetImageContents(fileName, ArtworkKind.FanArt)); + return imageContents.Match( + Left: _ => new NotFoundResult(), + Right: r => new FileContentResult(r.Contents, r.MimeType)); + } [HttpGet("/artwork/posters/plex/{plexMediaSourceId}/{*path}")] public async Task GetPlexPoster(int plexMediaSourceId, string path) diff --git a/ErsatzTV/Pages/ChannelEditor.razor b/ErsatzTV/Pages/ChannelEditor.razor index f4e0f5ccb..271438620 100644 --- a/ErsatzTV/Pages/ChannelEditor.razor +++ b/ErsatzTV/Pages/ChannelEditor.razor @@ -10,55 +10,57 @@ @inject ISnackbar Snackbar @inject IMediator Mediator -
- @(IsEdit ? "Edit Channel" : "Add Channel") + +
+ @(IsEdit ? "Edit Channel" : "Add Channel") - - - - - - - - @foreach (StreamingMode streamingMode in Enum.GetValues()) - { - @streamingMode - } - - - @foreach (FFmpegProfileViewModel profile in _ffmpegProfiles) - { - @profile.Name - } - - - - - - - Upload Logo - - - - - - - @(IsEdit ? "Save Changes" : "Add Channel") - - - - -
+ + + @foreach (FFmpegProfileViewModel profile in _ffmpegProfiles) + { + @profile.Name + } + + + + + + + Upload Logo + + + + + + + @(IsEdit ? "Save Changes" : "Add Channel") + + + + +
+ @code { diff --git a/ErsatzTV/Pages/Channels.razor b/ErsatzTV/Pages/Channels.razor index a5508ed45..c98677609 100644 --- a/ErsatzTV/Pages/Channels.razor +++ b/ErsatzTV/Pages/Channels.razor @@ -7,7 +7,7 @@ @inject IDialogService Dialog @inject IMediator Mediator - + Channels @@ -18,7 +18,7 @@ - + @@ -49,14 +49,18 @@ } - - - Edit - - - Delete - - +
+ + + + + + + + +
diff --git a/ErsatzTV/Pages/CollectionEditor.razor b/ErsatzTV/Pages/CollectionEditor.razor index 533d7b5e8..9c9c6784b 100644 --- a/ErsatzTV/Pages/CollectionEditor.razor +++ b/ErsatzTV/Pages/CollectionEditor.razor @@ -8,23 +8,25 @@ @inject ISnackbar Snackbar @inject IMediator Mediator -
- @(IsEdit ? "Edit Collection" : "Add Collection") + +
+ @(IsEdit ? "Edit Collection" : "Add Collection") - - - - - - - - - @(IsEdit ? "Save Changes" : "Add Collection") - - - - -
+ + + + + + + + + @(IsEdit ? "Save Changes" : "Add Collection") + + + + +
+
@code { diff --git a/ErsatzTV/Pages/CollectionItems.razor b/ErsatzTV/Pages/CollectionItems.razor index 1258172bc..e5927e4f0 100644 --- a/ErsatzTV/Pages/CollectionItems.razor +++ b/ErsatzTV/Pages/CollectionItems.razor @@ -9,75 +9,77 @@ @inject IDialogService Dialog @inject ChannelWriter Channel -
- @_data.Name - -
- -@if (_data.MovieCards.Any()) -{ - Movies - - - @foreach (MovieCardViewModel card in _data.MovieCards.OrderBy(m => m.SortTitle)) - { - - } - -} + +
+ @_data.Name + +
+ + @if (_data.MovieCards.Any()) + { + Movies -@if (_data.ShowCards.Any()) -{ - Television Shows + + @foreach (MovieCardViewModel card in _data.MovieCards.OrderBy(m => m.SortTitle)) + { + + } + + } - - @foreach (TelevisionShowCardViewModel card in _data.ShowCards.OrderBy(m => m.SortTitle)) - { - - } - -} + @if (_data.ShowCards.Any()) + { + Television Shows -@if (_data.SeasonCards.Any()) -{ - Television Seasons + + @foreach (TelevisionShowCardViewModel card in _data.ShowCards.OrderBy(m => m.SortTitle)) + { + + } + + } - - @foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards.OrderBy(m => m.SortTitle)) - { - - } - -} + @if (_data.SeasonCards.Any()) + { + Television Seasons -@if (_data.EpisodeCards.Any()) -{ - Television Episodes + + @foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards.OrderBy(m => m.SortTitle)) + { + + } + + } - - @foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired)) - { - - } - -} + @if (_data.EpisodeCards.Any()) + { + Television Episodes + + + @foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired)) + { + + } + + } +
@code { diff --git a/ErsatzTV/Pages/Collections.razor b/ErsatzTV/Pages/Collections.razor index 96dfb3d28..dc329b855 100644 --- a/ErsatzTV/Pages/Collections.razor +++ b/ErsatzTV/Pages/Collections.razor @@ -6,21 +6,23 @@ @inject IDialogService Dialog @inject IMediator Mediator - - @foreach (MediaCollectionViewModel card in _data) - { - - } - + + + @foreach (MediaCollectionViewModel card in _data) + { + + } + - - - Add Collection - + + + Add Collection + + @code { diff --git a/ErsatzTV/Pages/FFmpeg.razor b/ErsatzTV/Pages/FFmpeg.razor index c48605335..9e3370b98 100644 --- a/ErsatzTV/Pages/FFmpeg.razor +++ b/ErsatzTV/Pages/FFmpeg.razor @@ -5,7 +5,7 @@ @inject IDialogService Dialog @inject IMediator Mediator - + @@ -45,7 +45,7 @@ - + Name @@ -76,14 +76,18 @@ - - - Edit - - - Delete - - +
+ + + + + + + + +
diff --git a/ErsatzTV/Pages/FFmpegEditor.razor b/ErsatzTV/Pages/FFmpegEditor.razor index 825044302..a01093eed 100644 --- a/ErsatzTV/Pages/FFmpegEditor.razor +++ b/ErsatzTV/Pages/FFmpegEditor.razor @@ -10,98 +10,99 @@ @inject ISnackbar Snackbar @inject IMediator Mediator - - - - - - @(IsEdit ? "Edit FFmpeg Profile" : "Add FFmpeg Profile") - - - - - - - - General - - - - - - - @foreach (ResolutionViewModel resolution in _resolutions) - { - @resolution.Name - } - - - - - - - - Video - - - - - - - - - - @foreach (HardwareAccelerationKind hwAccel in Enum.GetValues()) - { - @hwAccel - } - - - - - Audio - - - - - - - - - - - - - - - - - - - Normalization - - - - - - - - - - - - - - - - - - @(IsEdit ? "Save Changes" : "Add Profile") - - - - - + + + + + + + @(IsEdit ? "Edit FFmpeg Profile" : "Add FFmpeg Profile") + + + + + + + + General + + + + + + + @foreach (ResolutionViewModel resolution in _resolutions) + { + @resolution.Name + } + + + + + + + + Video + + + + + + + + + + @foreach (HardwareAccelerationKind hwAccel in Enum.GetValues()) + { + @hwAccel + } + + + + + Audio + + + + + + + + + + + + + + + + + + + Normalization + + + + + + + + + + + + + + + + + + @(IsEdit ? "Save Changes" : "Add Profile") + + + + + @code { diff --git a/ErsatzTV/Pages/Index.razor b/ErsatzTV/Pages/Index.razor index ccfde442c..62491598f 100644 --- a/ErsatzTV/Pages/Index.razor +++ b/ErsatzTV/Pages/Index.razor @@ -1,70 +1,72 @@ @page "/" - - - Welcome to ErsatzTV! - - Channels - - Channels are not directly associated with any media. Channels have a number, a name, and a streaming mode that indicates how the channel will play media. - - - In TransportStream mode, the channel will also require an FFmpeg profile to configure transcoding and normalization. - In HttpLiveStreaming mode, the channel will attempt to serve the channel's media without transcoding or normalization beyond the container format. - - - - FFmpeg Profiles - - FFmpeg Profiles are collections of FFmpeg settings that are applied at the channel level. - All content on a given channel will use the same FFmpeg settings. This also means the same content on different channels can use different settings. - - - - Libraries - - Two local libraries are available, one for each media kind: Shows and Movies. Libraries contain paths (folders) to regularly scan for media items. - Support for Plex libraries is under active development; Jellyfin and Emby library support is planned. - - - - Collections - - Collections have a name and contain a logical grouping of media items. - Collections may contain shows, seasons, episodes or movies. - Collections containing shows and seasons are automatically updated as media is added or removed from the linked shows and seasons. - - - - Schedules - - Schedules have a name, a collection playback order and items to continually loop through. - - Three collection playback orders are supported: -
    -
  • Random - to randomly play collection items; repeating is allowed before all collection items have been played.
  • -
  • Shuffle - to randomly play collection items; repeating is not allowed until all collection items have been played.
  • -
  • Chronological - to play collection items sorted by air date and then by season and episode number (for when multiple episodes aired on a single day).
  • -
- - Schedule items have a start type, a start time, a collection and a playout mode. - - - A fixed start type requires a start time, while a dynamic start type means the schedule item will start immediately after the preceding schedule item. - - Four playout modes are supported: -
    -
  • One - to play one media item from the collection before advancing to the next schedule item.
  • -
  • Multiple - to play a specified count of media items from the collection before advancing to the next schedule item.
  • -
  • Duration - to play the maximum number of complete media items that will fit in the specified playout duration, before either going offline for the remainder of the playout duration (an offline tail), or immediately advancing to the next schedule item.
  • -
  • Flood - to play media items from the collection forever, or until the next schedule item's start time if one exists.
  • -
-
- - Playouts - - Playouts assign a schedule to a channel and individually track the ordered playback of collection items. - - -
-
\ No newline at end of file + + + + Welcome to ErsatzTV! + + Channels + + Channels are not directly associated with any media. Channels have a number, a name, and a streaming mode that indicates how the channel will play media. + + + In TransportStream mode, the channel will also require an FFmpeg profile to configure transcoding and normalization. + In HttpLiveStreaming mode, the channel will attempt to serve the channel's media without transcoding or normalization beyond the container format. + + + + FFmpeg Profiles + + FFmpeg Profiles are collections of FFmpeg settings that are applied at the channel level. + All content on a given channel will use the same FFmpeg settings. This also means the same content on different channels can use different settings. + + + + Libraries + + Two local libraries are available, one for each media kind: Shows and Movies. Libraries contain paths (folders) to regularly scan for media items. + Support for Plex libraries is under active development; Jellyfin and Emby library support is planned. + + + + Collections + + Collections have a name and contain a logical grouping of media items. + Collections may contain shows, seasons, episodes or movies. + Collections containing shows and seasons are automatically updated as media is added or removed from the linked shows and seasons. + + + + Schedules + + Schedules have a name, a collection playback order and items to continually loop through. + + Three collection playback orders are supported: +
    +
  • Random - to randomly play collection items; repeating is allowed before all collection items have been played.
  • +
  • Shuffle - to randomly play collection items; repeating is not allowed until all collection items have been played.
  • +
  • Chronological - to play collection items sorted by air date and then by season and episode number (for when multiple episodes aired on a single day).
  • +
+ + Schedule items have a start type, a start time, a collection and a playout mode. + + + A fixed start type requires a start time, while a dynamic start type means the schedule item will start immediately after the preceding schedule item. + + Four playout modes are supported: +
    +
  • One - to play one media item from the collection before advancing to the next schedule item.
  • +
  • Multiple - to play a specified count of media items from the collection before advancing to the next schedule item.
  • +
  • Duration - to play the maximum number of complete media items that will fit in the specified playout duration, before either going offline for the remainder of the playout duration (an offline tail), or immediately advancing to the next schedule item.
  • +
  • Flood - to play media items from the collection forever, or until the next schedule item's start time if one exists.
  • +
+
+ + Playouts + + Playouts assign a schedule to a channel and individually track the ordered playback of collection items. + + +
+
+
\ No newline at end of file diff --git a/ErsatzTV/Pages/Libraries.razor b/ErsatzTV/Pages/Libraries.razor index 6cfd2e1aa..9c7f16d18 100644 --- a/ErsatzTV/Pages/Libraries.razor +++ b/ErsatzTV/Pages/Libraries.razor @@ -8,7 +8,7 @@ @inject IEntityLocker Locker @inject ChannelWriter Channel - + Libraries diff --git a/ErsatzTV/Pages/LocalLibraryEditor.razor b/ErsatzTV/Pages/LocalLibraryEditor.razor index 3c039cf38..7eb701e65 100644 --- a/ErsatzTV/Pages/LocalLibraryEditor.razor +++ b/ErsatzTV/Pages/LocalLibraryEditor.razor @@ -5,7 +5,7 @@ @inject IDialogService Dialog @inject IMediator Mediator - + Library Paths diff --git a/ErsatzTV/Pages/LocalLibraryPathEditor.razor b/ErsatzTV/Pages/LocalLibraryPathEditor.razor index 77941ba25..3c3191d30 100644 --- a/ErsatzTV/Pages/LocalLibraryPathEditor.razor +++ b/ErsatzTV/Pages/LocalLibraryPathEditor.razor @@ -10,7 +10,7 @@ @inject IEntityLocker Locker @inject ChannelWriter Channel - + @_library.Name - Add Local Library Path
diff --git a/ErsatzTV/Pages/Logs.razor b/ErsatzTV/Pages/Logs.razor index d8e3980e4..91c730e3b 100644 --- a/ErsatzTV/Pages/Logs.razor +++ b/ErsatzTV/Pages/Logs.razor @@ -3,7 +3,7 @@ @using ErsatzTV.Application.Logs.Queries @inject IMediator Mediator - + Timestamp diff --git a/ErsatzTV/Pages/Movie.razor b/ErsatzTV/Pages/Movie.razor index fad754cc1..299af2e61 100644 --- a/ErsatzTV/Pages/Movie.razor +++ b/ErsatzTV/Pages/Movie.razor @@ -7,33 +7,42 @@ @inject IDialogService Dialog @inject NavigationManager NavigationManager - - - -
- @if (!string.IsNullOrWhiteSpace(_movie.Poster)) + +
+ @if (!string.IsNullOrWhiteSpace(_movie.FanArt)) + { + fan art + } +
+ +
+ @if (!string.IsNullOrWhiteSpace(_movie.Poster)) + { + movie poster + } +
+ @_movie.Title + @_movie.Year + @if (!string.IsNullOrWhiteSpace(_movie.Plot)) { - - - + + + @_movie.Plot + + } - -
- @_movie.Title - @_movie.Year - @_movie.Plot -
- - Add To Collection - -
-
-
+
+ + Add To Collection + +
- +
@code { diff --git a/ErsatzTV/Pages/MovieList.razor b/ErsatzTV/Pages/MovieList.razor index c3cd5e405..09f81a8d2 100644 --- a/ErsatzTV/Pages/MovieList.razor +++ b/ErsatzTV/Pages/MovieList.razor @@ -12,29 +12,31 @@ @inject NavigationManager NavigationManager @inject ChannelWriter Channel - - - - - - @Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count - - - - - + + + + + + + @Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count + + + + + - - @foreach (MovieCardViewModel card in _data.Cards.Where(d => !string.IsNullOrWhiteSpace(d.Title))) - { - - } + + @foreach (MovieCardViewModel card in _data.Cards.Where(d => !string.IsNullOrWhiteSpace(d.Title))) + { + + } + @code { diff --git a/ErsatzTV/Pages/PlayoutEditor.razor b/ErsatzTV/Pages/PlayoutEditor.razor index 861fc1e8d..9d7207cc7 100644 --- a/ErsatzTV/Pages/PlayoutEditor.razor +++ b/ErsatzTV/Pages/PlayoutEditor.razor @@ -8,24 +8,26 @@ @inject ISnackbar Snackbar @inject IMediator Mediator -
- Add Playout + +
+ Add Playout - - - - - - - - - - Add Playout - - - - -
+ + + + + + + + + + Add Playout + + + + +
+
@code { diff --git a/ErsatzTV/Pages/Playouts.razor b/ErsatzTV/Pages/Playouts.razor index 8f2320177..de069cc9d 100644 --- a/ErsatzTV/Pages/Playouts.razor +++ b/ErsatzTV/Pages/Playouts.razor @@ -5,7 +5,7 @@ @inject IDialogService Dialog @inject IMediator Mediator - + Playouts diff --git a/ErsatzTV/Pages/ScheduleEditor.razor b/ErsatzTV/Pages/ScheduleEditor.razor index 39646dbc5..7bfaf3617 100644 --- a/ErsatzTV/Pages/ScheduleEditor.razor +++ b/ErsatzTV/Pages/ScheduleEditor.razor @@ -7,29 +7,31 @@ @inject ISnackbar Snackbar @inject IMediator Mediator -
- @(IsEdit ? "Edit Schedule" : "Add Schedule") + +
+ @(IsEdit ? "Edit Schedule" : "Add Schedule") - - - - - - - @foreach (PlaybackOrder playbackOrder in Enum.GetValues()) - { - @playbackOrder - } - - - - - @(IsEdit ? "Save Changes" : "Add Schedule") - - - - -
+ + + + + + + @foreach (PlaybackOrder playbackOrder in Enum.GetValues()) + { + @playbackOrder + } + + + + + @(IsEdit ? "Save Changes" : "Add Schedule") + + + + +
+
@code { diff --git a/ErsatzTV/Pages/ScheduleItemsEditor.razor b/ErsatzTV/Pages/ScheduleItemsEditor.razor index 7811878d6..4212d9622 100644 --- a/ErsatzTV/Pages/ScheduleItemsEditor.razor +++ b/ErsatzTV/Pages/ScheduleItemsEditor.razor @@ -11,138 +11,140 @@ @inject ISnackbar Snackbar @inject IMediator Mediator - - - @_schedule.Name Items - - - - - - - - - - - Start Time - Collection - Playout Mode - - - - - - - - @(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic") - - - - - @context.CollectionName - - - - - @context.PlayoutMode - @if (context.PlayoutMode == PlayoutMode.Multiple && context.MultipleCount.HasValue) - { - @($" ({context.MultipleCount})") - } - - - - - - - - - - - - - - - - - - - - - Add Schedule Item - - - Save Changes - + + + + @_schedule.Name Items + + + + + + + + + + + Start Time + Collection + Playout Mode + + + + + + + + @(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic") + + + + + @context.CollectionName + + + + + @context.PlayoutMode + @if (context.PlayoutMode == PlayoutMode.Multiple && context.MultipleCount.HasValue) + { + @($" ({context.MultipleCount})") + } + + + + + + + + + + + + + + + + + + + + + Add Schedule Item + + + Save Changes + -@if (_selectedItem is not null) -{ -
- - - - - - @foreach (StartType startType in Enum.GetValues()) + @if (_selectedItem is not null) + { +
+ + + + + + @foreach (StartType startType in Enum.GetValues()) + { + @startType + } + + + + @foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues()) + { + @collectionType + } + + @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection) { - @startType + } - - - - @foreach (ProgramScheduleItemCollectionType collectionType in Enum.GetValues()) + @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow) { - @collectionType + } - - @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.Collection) - { - - } - @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow) - { - - } - @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason) - { - - } - - @foreach (PlayoutMode playoutMode in Enum.GetValues()) + @if (_selectedItem.CollectionType == ProgramScheduleItemCollectionType.TelevisionSeason) { - @playoutMode + } - - - - - - - - - -
-} + + @foreach (PlayoutMode playoutMode in Enum.GetValues()) + { + @playoutMode + } + + + + + + +
+
+
+
+ } +
@code { diff --git a/ErsatzTV/Pages/Schedules.razor b/ErsatzTV/Pages/Schedules.razor index 3c767ee88..3cb54959a 100644 --- a/ErsatzTV/Pages/Schedules.razor +++ b/ErsatzTV/Pages/Schedules.razor @@ -5,7 +5,7 @@ @inject IDialogService Dialog @inject IMediator Mediator - + Schedules @@ -50,31 +50,32 @@ Add Schedule + + @if (_selectedScheduleItems != null) + { + + + @_selectedSchedule.Name Items + + + Start Time + Collection + Playout Mode + + + + @(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic") + + @context.Name + @context.PlayoutMode + + + + + + } -@if (_selectedScheduleItems != null) -{ - - - @_selectedSchedule.Name Items - - - Start Time - Collection - Playout Mode - - - - @(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic") - - @context.Name - @context.PlayoutMode - - - - - -} @code { private List _schedules; diff --git a/ErsatzTV/Pages/Search.razor b/ErsatzTV/Pages/Search.razor index f17c338d8..cc23414a2 100644 --- a/ErsatzTV/Pages/Search.razor +++ b/ErsatzTV/Pages/Search.razor @@ -13,7 +13,7 @@ @inject IDialogService Dialog @inject ChannelWriter Channel - +
Search Results: "@_query"
diff --git a/ErsatzTV/Pages/TelevisionEpisode.razor b/ErsatzTV/Pages/TelevisionEpisode.razor index 6bf39078e..cd26b7a33 100644 --- a/ErsatzTV/Pages/TelevisionEpisode.razor +++ b/ErsatzTV/Pages/TelevisionEpisode.razor @@ -7,33 +7,35 @@ @inject IDialogService Dialog @inject NavigationManager NavigationManager - - - -
- @if (!string.IsNullOrWhiteSpace(_episode.Poster)) - { - - - - } - -
- @_episode.Title - @_season.Plot - @_episode.Plot -
- - Add To Collection - + + + + +
+ @if (!string.IsNullOrWhiteSpace(_episode.Poster)) + { + + + + } + +
+ @_episode.Title + @_season.Plot + @_episode.Plot +
+ + Add To Collection + +
-
- -
- + +
+ + @code { diff --git a/ErsatzTV/Pages/TelevisionEpisodeList.razor b/ErsatzTV/Pages/TelevisionEpisodeList.razor index f1f93102e..00eec9f02 100644 --- a/ErsatzTV/Pages/TelevisionEpisodeList.razor +++ b/ErsatzTV/Pages/TelevisionEpisodeList.razor @@ -15,53 +15,55 @@ @inject NavigationManager NavigationManager @inject ChannelWriter Channel - - - -
- @if (!string.IsNullOrWhiteSpace(_season.Poster)) - { - - - - } - -
- @_season.Title - @_season.Year - @_season.Plot -
- - Add To Collection - - - Add To Schedule - + + + + +
+ @if (!string.IsNullOrWhiteSpace(_season.Poster)) + { + + + + } + +
+ @_season.Title + @_season.Year + @_season.Plot +
+ + Add To Collection + + + Add To Schedule + +
-
- -
- - + +
+ + - - @foreach (TelevisionEpisodeCardViewModel card in _data.Cards) - { - - } + + @foreach (TelevisionEpisodeCardViewModel card in _data.Cards) + { + + } + @code { diff --git a/ErsatzTV/Pages/TelevisionSeasonList.razor b/ErsatzTV/Pages/TelevisionSeasonList.razor index f6442b66e..21440fcbb 100644 --- a/ErsatzTV/Pages/TelevisionSeasonList.razor +++ b/ErsatzTV/Pages/TelevisionSeasonList.razor @@ -15,49 +15,51 @@ @inject NavigationManager NavigationManager @inject ChannelWriter Channel - - - -
- @if (!string.IsNullOrWhiteSpace(_show.Poster)) - { - - - - } - -
- @_show.Title - @_show.Year - @_show.Plot -
- - Add To Collection - - - Add To Schedule - + + + + +
+ @if (!string.IsNullOrWhiteSpace(_show.Poster)) + { + + + + } + +
+ @_show.Title + @_show.Year + @_show.Plot +
+ + Add To Collection + + + Add To Schedule + +
-
- -
- - + +
+ + - - @foreach (TelevisionSeasonCardViewModel card in _data.Cards) - { - - } + + @foreach (TelevisionSeasonCardViewModel card in _data.Cards) + { + + } + @code { diff --git a/ErsatzTV/Pages/TelevisionShowList.razor b/ErsatzTV/Pages/TelevisionShowList.razor index a6845766a..85d131827 100644 --- a/ErsatzTV/Pages/TelevisionShowList.razor +++ b/ErsatzTV/Pages/TelevisionShowList.razor @@ -10,29 +10,31 @@ @inject IDialogService Dialog @inject ChannelWriter Channel - - - - - - @Math.Min((_pageNumber - 1) * _pageSize + 1, _data.Count)-@Math.Min(_data.Count, _pageNumber * _pageSize) of @_data.Count - - - - - + + + + + + + @Math.Min((_pageNumber - 1) * _pageSize + 1, _data.Count)-@Math.Min(_data.Count, _pageNumber * _pageSize) of @_data.Count + + + + + - - @foreach (TelevisionShowCardViewModel card in _data.Cards) - { - - } + + @foreach (TelevisionShowCardViewModel card in _data.Cards) + { + + } + @code { diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor index 5dbe09036..dd1b793cc 100644 --- a/ErsatzTV/Shared/MainLayout.razor +++ b/ErsatzTV/Shared/MainLayout.razor @@ -57,9 +57,7 @@ - - @Body - + @Body diff --git a/ErsatzTV/wwwroot/css/site.css b/ErsatzTV/wwwroot/css/site.css index 29d0d7136..69a72fa72 100644 --- a/ErsatzTV/wwwroot/css/site.css +++ b/ErsatzTV/wwwroot/css/site.css @@ -4,9 +4,13 @@ flex-wrap: wrap; } -.media-card-container { width: 152px; } +.media-card-container { + width: 152px; +} -.media-card-episode-container { width: 392px; } +.media-card-episode-container { + width: 392px; +} .media-card { display: flex; @@ -18,9 +22,12 @@ width: 152px; } -.media-card-episode { width: 392px; } +.media-card-episode { + width: 392px; +} -.media-card:hover { /*filter: brightness(75%);*/ } +.media-card:hover { /*filter: brightness(75%);*/ +} .media-card-title { overflow: hidden; @@ -29,7 +36,9 @@ width: 100%; } -.media-card-poster-placeholder { font-weight: bold; } +.media-card-poster-placeholder { + font-weight: bold; +} .media-card-menu { bottom: 0; @@ -38,7 +47,9 @@ right: 0; } -.media-card:hover .media-card-menu { display: block; } +.media-card:hover .media-card-menu { + display: block; +} .media-card-overlay { background: rgba(0, 0, 0, 0.4); @@ -52,9 +63,13 @@ transition: opacity 0.2s; } -.media-card-overlay:hover { opacity: 1; } +.media-card-overlay:hover { + opacity: 1; +} -.search-bar .mud-input { height: 42px; } +.search-bar .mud-input { + height: 42px; +} .search-bar { background-color: rgba(255, 255, 255, .15); @@ -63,9 +78,46 @@ margin-bottom: 5px; } -.search-bar div .mud-input-root { color: #fafafa; } +.search-bar div .mud-input-root { + color: #fafafa; +} .search-bar .mud-input.mud-input-outlined .mud-input-outlined-border { border: none; border-radius: 4px; -} \ No newline at end of file +} + +.fanart-container { + position: relative; + z-index: -1; + width: 100%; +} + +.fanart-container img { + width: 100%; + height: 400px; + -o-object-fit: cover; + object-fit: cover; + position: absolute; + transition: opacity 5s ease; +} + +.fanart-container > .fanart-tint { + background: linear-gradient(360deg, black, transparent); + width: 100%; + height: 400px; + opacity: 0.85; + position: absolute; + z-index: 1; +} + +.media-item-title { + color: #fff; + text-shadow: 1px 1px 3px #000; +} + +.media-item-subtitle { + color: #fff; + text-shadow: 1px 1px 3px #000; +} +