Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
633586ddba | ||
|
|
da3e05b231 | ||
|
|
9e6de7e2eb | ||
|
|
4097288fed | ||
|
|
90f775aab4 | ||
|
|
fc33c5cd05 | ||
|
|
37eee73ab7 |
@@ -40,3 +40,6 @@ msbuild.wrn
|
||||
core
|
||||
|
||||
scripts/generate-api-sdk/swagger.json
|
||||
|
||||
docker-compose.override.yml
|
||||
|
||||
|
||||
@@ -11,17 +11,16 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
bool Transcode,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
int ResolutionId,
|
||||
bool NormalizeResolution,
|
||||
bool NormalizeVideo,
|
||||
string VideoCodec,
|
||||
bool NormalizeVideoCodec,
|
||||
int VideoBitrate,
|
||||
int VideoBufferSize,
|
||||
string AudioCodec,
|
||||
bool NormalizeAudioCodec,
|
||||
int AudioBitrate,
|
||||
int AudioBufferSize,
|
||||
int AudioVolume,
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeAudio) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
bool NormalizeAudio,
|
||||
string FrameRate) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
}
|
||||
|
||||
@@ -43,19 +43,18 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
Transcode = request.Transcode,
|
||||
HardwareAcceleration = request.HardwareAcceleration,
|
||||
ResolutionId = resolutionId,
|
||||
NormalizeResolution = request.NormalizeResolution,
|
||||
NormalizeVideo = request.NormalizeVideo,
|
||||
VideoCodec = request.VideoCodec,
|
||||
NormalizeVideoCodec = request.NormalizeVideoCodec,
|
||||
VideoBitrate = request.VideoBitrate,
|
||||
VideoBufferSize = request.VideoBufferSize,
|
||||
AudioCodec = request.AudioCodec,
|
||||
NormalizeAudioCodec = request.NormalizeAudioCodec,
|
||||
AudioBitrate = request.AudioBitrate,
|
||||
AudioBufferSize = request.AudioBufferSize,
|
||||
AudioVolume = request.AudioVolume,
|
||||
AudioChannels = request.AudioChannels,
|
||||
AudioSampleRate = request.AudioSampleRate,
|
||||
NormalizeAudio = request.NormalizeAudio
|
||||
NormalizeAudio = request.NormalizeAudio,
|
||||
FrameRate = request.FrameRate
|
||||
});
|
||||
|
||||
private Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
@@ -63,7 +62,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
.Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name));
|
||||
|
||||
private Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
createFFmpegProfile.AtLeast(1)(p => p.ThreadCount);
|
||||
createFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
|
||||
|
||||
private async Task<Validation<BaseError, int>> ResolutionMustExist(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
(await _resolutionRepository.Get(createFFmpegProfile.ResolutionId))
|
||||
|
||||
@@ -12,17 +12,16 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
bool Transcode,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
int ResolutionId,
|
||||
bool NormalizeResolution,
|
||||
bool NormalizeVideo,
|
||||
string VideoCodec,
|
||||
bool NormalizeVideoCodec,
|
||||
int VideoBitrate,
|
||||
int VideoBufferSize,
|
||||
string AudioCodec,
|
||||
bool NormalizeAudioCodec,
|
||||
int AudioBitrate,
|
||||
int AudioBufferSize,
|
||||
int AudioVolume,
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeAudio) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
bool NormalizeAudio,
|
||||
string FrameRate) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
}
|
||||
|
||||
@@ -37,19 +37,18 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
p.Transcode = update.Transcode;
|
||||
p.HardwareAcceleration = update.HardwareAcceleration;
|
||||
p.ResolutionId = update.ResolutionId;
|
||||
p.NormalizeResolution = update.NormalizeResolution;
|
||||
p.NormalizeVideo = update.NormalizeVideo;
|
||||
p.VideoCodec = update.VideoCodec;
|
||||
p.NormalizeVideoCodec = update.NormalizeVideoCodec;
|
||||
p.VideoBitrate = update.VideoBitrate;
|
||||
p.VideoBufferSize = update.VideoBufferSize;
|
||||
p.AudioCodec = update.AudioCodec;
|
||||
p.NormalizeAudioCodec = update.NormalizeAudioCodec;
|
||||
p.AudioBitrate = update.AudioBitrate;
|
||||
p.AudioBufferSize = update.AudioBufferSize;
|
||||
p.AudioVolume = update.AudioVolume;
|
||||
p.AudioChannels = update.AudioChannels;
|
||||
p.AudioSampleRate = update.AudioSampleRate;
|
||||
p.NormalizeAudio = update.NormalizeAudio;
|
||||
p.FrameRate = update.FrameRate;
|
||||
await _ffmpegProfileRepository.Update(p);
|
||||
return ProjectToViewModel(p);
|
||||
}
|
||||
@@ -69,7 +68,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
.Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name));
|
||||
|
||||
private Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||
updateFFmpegProfile.AtLeast(1)(p => p.ThreadCount);
|
||||
updateFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
|
||||
|
||||
private async Task<Validation<BaseError, int>> ResolutionMustExist(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||
(await _resolutionRepository.Get(updateFFmpegProfile.ResolutionId))
|
||||
|
||||
@@ -10,17 +10,16 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
||||
bool Transcode,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
ResolutionViewModel Resolution,
|
||||
bool NormalizeResolution,
|
||||
bool NormalizeVideo,
|
||||
string VideoCodec,
|
||||
bool NormalizeVideoCodec,
|
||||
int VideoBitrate,
|
||||
int VideoBufferSize,
|
||||
string AudioCodec,
|
||||
bool NormalizeAudioCodec,
|
||||
int AudioBitrate,
|
||||
int AudioBufferSize,
|
||||
int AudioVolume,
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeAudio);
|
||||
bool NormalizeAudio,
|
||||
string FrameRate);
|
||||
}
|
||||
|
||||
@@ -13,19 +13,18 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
||||
profile.Transcode,
|
||||
profile.HardwareAcceleration,
|
||||
Project(profile.Resolution),
|
||||
profile.NormalizeResolution,
|
||||
profile.NormalizeVideo,
|
||||
profile.VideoCodec,
|
||||
profile.NormalizeVideoCodec,
|
||||
profile.VideoBitrate,
|
||||
profile.VideoBufferSize,
|
||||
profile.AudioCodec,
|
||||
profile.NormalizeAudioCodec,
|
||||
profile.AudioBitrate,
|
||||
profile.AudioBufferSize,
|
||||
profile.AudioVolume,
|
||||
profile.AudioChannels,
|
||||
profile.AudioSampleRate,
|
||||
profile.NormalizeAudio);
|
||||
profile.NormalizeAudio,
|
||||
profile.FrameRate);
|
||||
|
||||
private static ResolutionViewModel Project(Resolution resolution) =>
|
||||
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
|
||||
|
||||
@@ -18,7 +18,11 @@ namespace ErsatzTV.Application.Libraries.Queries
|
||||
|
||||
public Task<List<LibraryViewModel>> Handle(GetAllLibraries request, CancellationToken cancellationToken) =>
|
||||
_libraryRepository.GetAll()
|
||||
.Map(list => list.Filter(ShouldIncludeLibrary).Map(ProjectToViewModel).ToList());
|
||||
.Map(
|
||||
list => list.Filter(ShouldIncludeLibrary)
|
||||
.OrderBy(l => l.MediaSource is LocalMediaSource ? 0 : 1)
|
||||
.ThenBy(l => l.MediaKind)
|
||||
.Map(ProjectToViewModel).ToList());
|
||||
|
||||
private static bool ShouldIncludeLibrary(Library library) =>
|
||||
library switch
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace ErsatzTV.Application.MediaCards
|
||||
List<MovieCardViewModel> MovieCards,
|
||||
List<TelevisionShowCardViewModel> ShowCards,
|
||||
List<TelevisionSeasonCardViewModel> SeasonCards,
|
||||
List<TelevisionEpisodeCardViewModel> EpisodeCards)
|
||||
List<TelevisionEpisodeCardViewModel> EpisodeCards,
|
||||
List<MusicVideoCardViewModel> MusicVideoCards)
|
||||
{
|
||||
public bool UseCustomPlaybackOrder { get; set; }
|
||||
}
|
||||
|
||||
@@ -52,6 +52,14 @@ namespace ErsatzTV.Application.MediaCards
|
||||
movieMetadata.SortTitle,
|
||||
GetPoster(movieMetadata));
|
||||
|
||||
internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) =>
|
||||
new(
|
||||
musicVideoMetadata.MusicVideoId,
|
||||
$"{musicVideoMetadata.Title} ({musicVideoMetadata.Artist})",
|
||||
musicVideoMetadata.Year?.ToString(),
|
||||
musicVideoMetadata.SortTitle,
|
||||
GetThumbnail(musicVideoMetadata));
|
||||
|
||||
internal static CollectionCardResultsViewModel
|
||||
ProjectToViewModel(Collection collection) =>
|
||||
new(
|
||||
@@ -64,6 +72,8 @@ namespace ErsatzTV.Application.MediaCards
|
||||
collection.MediaItems.OfType<Show>().Map(s => ProjectToViewModel(s.ShowMetadata.Head())).ToList(),
|
||||
collection.MediaItems.OfType<Season>().Map(ProjectToViewModel).ToList(),
|
||||
collection.MediaItems.OfType<Episode>().Map(e => ProjectToViewModel(e.EpisodeMetadata.Head()))
|
||||
.ToList(),
|
||||
collection.MediaItems.OfType<MusicVideo>().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head()))
|
||||
.ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
||||
|
||||
private static int GetCustomIndex(Collection collection, int mediaItemId) =>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record MusicVideoCardResultsViewModel(
|
||||
int Count,
|
||||
List<MusicVideoCardViewModel> Cards,
|
||||
Option<SearchPageMap> PageMap);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record MusicVideoCardViewModel
|
||||
(int MusicVideoId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
||||
MusicVideoId,
|
||||
Title,
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster)
|
||||
{
|
||||
public int CustomIndex { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,9 @@ using LanguageExt;
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public record AddItemsToCollection
|
||||
(int CollectionId, List<int> MovieIds, List<int> ShowIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
(
|
||||
int CollectionId,
|
||||
List<int> MovieIds,
|
||||
List<int> ShowIds,
|
||||
List<int> MusicVideoIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItems(
|
||||
request.CollectionId,
|
||||
request.MovieIds.Append(request.ShowIds).ToList()))
|
||||
request.MovieIds.Append(request.ShowIds).Append(request.MusicVideoIds).ToList()))
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public record AddMusicVideoToCollection
|
||||
(int CollectionId, int MusicVideoId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
AddMusicVideoToCollectionHandler : MediatR.IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||
|
||||
public AddMusicVideoToCollectionHandler(
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
IMusicVideoRepository musicVideoRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_musicVideoRepository = musicVideoRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
AddMusicVideoToCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => ApplyAddMusicVideoRequest(request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> ApplyAddMusicVideoRequest(AddMusicVideoToCollection request)
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MusicVideoId))
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(request.CollectionId))
|
||||
{
|
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||
}
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Unit>> Validate(AddMusicVideoToCollection request) =>
|
||||
(await CollectionMustExist(request), await ValidateMusicVideo(request))
|
||||
.Apply((_, _) => Unit.Default);
|
||||
|
||||
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddMusicVideoToCollection request) =>
|
||||
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||
|
||||
private Task<Validation<BaseError, Unit>> ValidateMusicVideo(AddMusicVideoToCollection request) =>
|
||||
LoadMusicVideo(request)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Music video does not exist"));
|
||||
|
||||
private Task<Option<MusicVideo>> LoadMusicVideo(AddMusicVideoToCollection request) =>
|
||||
_musicVideoRepository.GetMusicVideo(request.MusicVideoId);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
@@ -8,59 +7,6 @@ namespace ErsatzTV.Application.MediaItems
|
||||
internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) =>
|
||||
new(mediaItem.Id, mediaItem.LibraryPathId);
|
||||
|
||||
internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) =>
|
||||
mediaItem switch
|
||||
{
|
||||
Episode e => ProjectToSearchViewModel(e),
|
||||
Movie m => ProjectToSearchViewModel(m),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(Episode mediaItem) =>
|
||||
new(
|
||||
mediaItem.Id,
|
||||
GetLibraryName(mediaItem),
|
||||
"TV Show",
|
||||
GetDisplayTitle(mediaItem),
|
||||
GetDisplayDuration(mediaItem));
|
||||
|
||||
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(Movie mediaItem) =>
|
||||
new(
|
||||
mediaItem.Id,
|
||||
GetLibraryName(mediaItem),
|
||||
"Movie",
|
||||
GetDisplayTitle(mediaItem),
|
||||
GetDisplayDuration(mediaItem));
|
||||
|
||||
|
||||
private static string GetDisplayTitle(MediaItem mediaItem) =>
|
||||
mediaItem switch
|
||||
{
|
||||
Episode e => e.EpisodeMetadata.HeadOrNone()
|
||||
.Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}")
|
||||
.IfNone("[unknown episode]"),
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"),
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
private static string GetDisplayDuration(MediaItem mediaItem)
|
||||
{
|
||||
MediaVersion version = mediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
return string.Format(
|
||||
version.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
||||
version.Duration);
|
||||
}
|
||||
|
||||
// TODO: fix this when search is reimplemented
|
||||
private static string GetLibraryName(MediaItem item) =>
|
||||
"Library Name";
|
||||
|
||||
public static NamedMediaItemViewModel ProjectToViewModel(Show show) =>
|
||||
new(show.Id, show.ShowMetadata.HeadOrNone().Map(sm => $"{sm?.Title} ({sm?.Year})").IfNone("???"));
|
||||
|
||||
|
||||
@@ -8,15 +8,24 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
{
|
||||
int LibraryId { get; }
|
||||
bool ForceScan { get; }
|
||||
bool Rescan { get; }
|
||||
}
|
||||
|
||||
public record ScanLocalLibraryIfNeeded(int LibraryId) : IScanLocalLibrary
|
||||
{
|
||||
public bool ForceScan => false;
|
||||
public bool Rescan => false;
|
||||
}
|
||||
|
||||
public record ForceScanLocalLibrary(int LibraryId) : IScanLocalLibrary
|
||||
{
|
||||
public bool ForceScan => true;
|
||||
public bool Rescan => false;
|
||||
}
|
||||
|
||||
public record ForceRescanLocalLibrary(int LibraryId) : IScanLocalLibrary
|
||||
{
|
||||
public bool ForceScan => true;
|
||||
public bool Rescan => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,15 @@ using Unit = LanguageExt.Unit;
|
||||
namespace ErsatzTV.Application.MediaSources.Commands
|
||||
{
|
||||
public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>,
|
||||
IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>
|
||||
IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>,
|
||||
IRequestHandler<ForceRescanLocalLibrary, Either<BaseError, string>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILogger<ScanLocalLibraryHandler> _logger;
|
||||
private readonly IMovieFolderScanner _movieFolderScanner;
|
||||
private readonly IMusicVideoFolderScanner _musicVideoFolderScanner;
|
||||
private readonly ITelevisionFolderScanner _televisionFolderScanner;
|
||||
|
||||
public ScanLocalLibraryHandler(
|
||||
@@ -30,6 +32,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
IConfigElementRepository configElementRepository,
|
||||
IMovieFolderScanner movieFolderScanner,
|
||||
ITelevisionFolderScanner televisionFolderScanner,
|
||||
IMusicVideoFolderScanner musicVideoFolderScanner,
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<ScanLocalLibraryHandler> logger)
|
||||
{
|
||||
@@ -37,10 +40,15 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
_configElementRepository = configElementRepository;
|
||||
_movieFolderScanner = movieFolderScanner;
|
||||
_televisionFolderScanner = televisionFolderScanner;
|
||||
_musicVideoFolderScanner = musicVideoFolderScanner;
|
||||
_entityLocker = entityLocker;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, string>> Handle(
|
||||
ForceRescanLocalLibrary request,
|
||||
CancellationToken cancellationToken) => Handle(request);
|
||||
|
||||
public Task<Either<BaseError, string>> Handle(
|
||||
ForceScanLocalLibrary request,
|
||||
CancellationToken cancellationToken) => Handle(request);
|
||||
@@ -57,7 +65,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
|
||||
private async Task<Unit> PerformScan(RequestParameters parameters)
|
||||
{
|
||||
(LocalLibrary localLibrary, string ffprobePath, bool forceScan) = parameters;
|
||||
(LocalLibrary localLibrary, string ffprobePath, bool forceScan, bool rescan) = parameters;
|
||||
|
||||
var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
|
||||
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
|
||||
@@ -65,15 +73,20 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
DateTimeOffset effectiveLastScan = rescan ? DateTimeOffset.MinValue : lastScan;
|
||||
|
||||
foreach (LibraryPath libraryPath in localLibrary.Paths)
|
||||
{
|
||||
switch (localLibrary.MediaKind)
|
||||
{
|
||||
case LibraryMediaKind.Movies:
|
||||
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath, lastScan);
|
||||
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan);
|
||||
break;
|
||||
case LibraryMediaKind.Shows:
|
||||
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath, lastScan);
|
||||
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan);
|
||||
break;
|
||||
case LibraryMediaKind.MusicVideos:
|
||||
await _musicVideoFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -104,7 +117,8 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
(library, ffprobePath) => new RequestParameters(
|
||||
library,
|
||||
ffprobePath,
|
||||
request.ForceScan));
|
||||
request.ForceScan,
|
||||
request.Rescan));
|
||||
|
||||
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(
|
||||
IScanLocalLibrary request) =>
|
||||
@@ -119,6 +133,6 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
ffprobePath =>
|
||||
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
|
||||
|
||||
private record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan);
|
||||
private record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan, bool Rescan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,15 +24,25 @@ namespace ErsatzTV.Application.Playouts
|
||||
private static PlayoutProgramScheduleViewModel Project(ProgramSchedule programSchedule) =>
|
||||
new(programSchedule.Id, programSchedule.Name);
|
||||
|
||||
private static string GetDisplayTitle(MediaItem mediaItem) =>
|
||||
mediaItem switch
|
||||
private static string GetDisplayTitle(MediaItem mediaItem)
|
||||
{
|
||||
switch (mediaItem)
|
||||
{
|
||||
Episode e => e.EpisodeMetadata.HeadOrNone()
|
||||
.Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}")
|
||||
.IfNone("[unknown episode]"),
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"),
|
||||
_ => string.Empty
|
||||
};
|
||||
case Episode e:
|
||||
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone()
|
||||
.Map(sm => $"{sm.Title} - ").IfNone(string.Empty);
|
||||
return e.EpisodeMetadata.HeadOrNone()
|
||||
.Map(em => $"{showTitle}s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00} - {em.Title}")
|
||||
.IfNone("[unknown episode]");
|
||||
case Movie m:
|
||||
return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]");
|
||||
case MusicVideo mv:
|
||||
return mv.MusicVideoMetadata.HeadOrNone().Map(mvm => $"{mvm.Artist} - {mvm.Title}")
|
||||
.IfNone("[unknown music video]");
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDisplayDuration(MediaItem mediaItem)
|
||||
{
|
||||
@@ -40,6 +50,7 @@ namespace ErsatzTV.Application.Playouts
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public record QuerySearchIndexMusicVideos
|
||||
(string Query, int PageNumber, int PageSize) : IRequest<MusicVideoCardResultsViewModel>;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public class
|
||||
QuerySearchIndexMusicVideosHandler : IRequestHandler<QuerySearchIndexMusicVideos, MusicVideoCardResultsViewModel
|
||||
>
|
||||
{
|
||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public QuerySearchIndexMusicVideosHandler(ISearchIndex searchIndex, IMusicVideoRepository musicVideoRepository)
|
||||
{
|
||||
_searchIndex = searchIndex;
|
||||
_musicVideoRepository = musicVideoRepository;
|
||||
}
|
||||
|
||||
public async Task<MusicVideoCardResultsViewModel> Handle(
|
||||
QuerySearchIndexMusicVideos request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
request.Query,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
List<MusicVideoCardViewModel> items = await _musicVideoRepository
|
||||
.GetMusicVideosForCards(searchResult.Items.Map(i => i.Id).ToList())
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new MusicVideoCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,30 +5,38 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetConcatProcessByChannelNumber>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly FFmpegProcessService _ffmpegProcessService;
|
||||
|
||||
public GetConcatProcessByChannelNumberHandler(
|
||||
IChannelRepository channelRepository,
|
||||
IConfigElementRepository configElementRepository,
|
||||
FFmpegProcessService ffmpegProcessService)
|
||||
: base(channelRepository, configElementRepository) =>
|
||||
: base(channelRepository, configElementRepository)
|
||||
{
|
||||
_configElementRepository = configElementRepository;
|
||||
_ffmpegProcessService = ffmpegProcessService;
|
||||
}
|
||||
|
||||
protected override Task<Either<BaseError, Process>> GetProcess(
|
||||
protected override async Task<Either<BaseError, Process>> GetProcess(
|
||||
GetConcatProcessByChannelNumber request,
|
||||
Channel channel,
|
||||
string ffmpegPath) =>
|
||||
Right<BaseError, Process>(
|
||||
_ffmpegProcessService.ConcatChannel(
|
||||
ffmpegPath,
|
||||
channel,
|
||||
request.Scheme,
|
||||
request.Host)).AsTask();
|
||||
string ffmpegPath)
|
||||
{
|
||||
bool saveReports = await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||
.Map(result => result.IfNone(false));
|
||||
|
||||
return _ffmpegProcessService.ConcatChannel(
|
||||
ffmpegPath,
|
||||
saveReports,
|
||||
channel,
|
||||
request.Scheme,
|
||||
request.Host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath))
|
||||
};
|
||||
|
||||
@@ -153,6 +154,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(playoutItem))
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace ErsatzTV.CommandLine.Commands
|
||||
public string Name { get; set; }
|
||||
|
||||
[CommandOption("thread-count", Description = "The number of threads")]
|
||||
public int ThreadCount { get; set; } = 4;
|
||||
public int ThreadCount { get; set; } = 0;
|
||||
|
||||
[CommandOption("transcode", Description = "Whether to transcode all media")]
|
||||
public bool Transcode { get; set; } = true;
|
||||
|
||||
@@ -163,9 +163,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShouldNot_SetScaledSize_When_NotNormalizingResolution_ForTransportStream()
|
||||
public void ShouldNot_SetScaledSize_When_NotNormalizingVideo_ForTransportStream()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with { NormalizeResolution = false };
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with { NormalizeVideo = false };
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
@@ -184,7 +184,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 }
|
||||
};
|
||||
|
||||
@@ -208,7 +208,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 }
|
||||
};
|
||||
|
||||
@@ -232,7 +232,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 }
|
||||
};
|
||||
|
||||
@@ -257,7 +257,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 }
|
||||
};
|
||||
|
||||
@@ -282,7 +282,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 }
|
||||
};
|
||||
|
||||
@@ -303,11 +303,11 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_NotPadToDesiredResolution_When_NotNormalizingResolution()
|
||||
public void Should_NotPadToDesiredResolution_When_NotNormalizingVideo()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeResolution = false,
|
||||
NormalizeVideo = false,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 }
|
||||
};
|
||||
|
||||
@@ -332,9 +332,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
var ffmpegProfile = new FFmpegProfile
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 },
|
||||
NormalizeVideoCodec = false,
|
||||
VideoCodec = "testCodec"
|
||||
};
|
||||
|
||||
@@ -357,13 +356,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
[Test]
|
||||
public void
|
||||
Should_SetDesiredVideoCodec_When_ContentIsCorrectSize_And_NormalizingWrongCodec_ForTransportStream()
|
||||
Should_SetDesiredVideoCodec_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream()
|
||||
{
|
||||
var ffmpegProfile = new FFmpegProfile
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 },
|
||||
NormalizeVideoCodec = true,
|
||||
VideoCodec = "testCodec"
|
||||
};
|
||||
|
||||
@@ -387,13 +385,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
[Test]
|
||||
public void
|
||||
Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NormalizingWrongCodec_ForHttpLiveStreaming()
|
||||
Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NormalizingVideo_ForHttpLiveStreaming()
|
||||
{
|
||||
var ffmpegProfile = new FFmpegProfile
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 },
|
||||
NormalizeVideoCodec = true,
|
||||
VideoCodec = "testCodec"
|
||||
};
|
||||
|
||||
@@ -420,9 +417,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
var ffmpegProfile = new FFmpegProfile
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 },
|
||||
NormalizeVideoCodec = true,
|
||||
VideoCodec = "libx264"
|
||||
};
|
||||
|
||||
@@ -446,13 +442,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
[Test]
|
||||
public void
|
||||
Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NotNormalizingWrongCodec_ForTransportStream()
|
||||
Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NotNormalizingVideo_ForTransportStream()
|
||||
{
|
||||
var ffmpegProfile = new FFmpegProfile
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = false,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 },
|
||||
NormalizeVideoCodec = false,
|
||||
VideoCodec = "libx264"
|
||||
};
|
||||
|
||||
@@ -479,9 +474,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
var ffmpegProfile = new FFmpegProfile
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 },
|
||||
NormalizeVideoCodec = false,
|
||||
VideoBitrate = 2525
|
||||
};
|
||||
|
||||
@@ -503,13 +497,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_SetVideoBitrate_When_ContentIsCorrectSize_And_NormalizingWrongCodec_ForTransportStream()
|
||||
public void Should_SetVideoBitrate_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream()
|
||||
{
|
||||
var ffmpegProfile = new FFmpegProfile
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 },
|
||||
NormalizeVideoCodec = true,
|
||||
VideoBitrate = 2525
|
||||
};
|
||||
|
||||
@@ -536,9 +529,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
var ffmpegProfile = new FFmpegProfile
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 },
|
||||
NormalizeVideoCodec = false,
|
||||
VideoBufferSize = 2525
|
||||
};
|
||||
|
||||
@@ -561,13 +553,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
[Test]
|
||||
public void
|
||||
Should_SetVideoBufferSize_When_ContentIsCorrectSize_And_NormalizingWrongCodec_ForTransportStream()
|
||||
Should_SetVideoBufferSize_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream()
|
||||
{
|
||||
var ffmpegProfile = new FFmpegProfile
|
||||
{
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideo = true,
|
||||
Resolution = new Resolution { Width = 1920, Height = 1080 },
|
||||
NormalizeVideoCodec = true,
|
||||
VideoBufferSize = 2525
|
||||
};
|
||||
|
||||
@@ -594,7 +585,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeAudioCodec = true,
|
||||
NormalizeAudio = true,
|
||||
AudioCodec = "aac"
|
||||
};
|
||||
|
||||
@@ -613,11 +604,11 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_SetCopyAudioCodec_When_NotNormalizingWrongCodec_ForTransportStream()
|
||||
public void Should_SetCopyAudioCodec_When_NotNormalizingVideo_ForTransportStream()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeAudioCodec = false,
|
||||
NormalizeAudio = false,
|
||||
AudioCodec = "aac"
|
||||
};
|
||||
|
||||
@@ -636,11 +627,11 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_SetDesiredAudioCodec_When_NormalizingWrongCodec_ForTransportStream()
|
||||
public void Should_SetDesiredAudioCodec_When_NormalizingVideo_ForTransportStream()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeAudioCodec = true,
|
||||
NormalizeAudio = true,
|
||||
AudioCodec = "aac"
|
||||
};
|
||||
|
||||
@@ -659,11 +650,11 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_SetCopyAudioCodec_When_NormalizingWrongCodec_ForHttpLiveStreaming()
|
||||
public void Should_SetCopyAudioCodec_When_NormalizingVideo_ForHttpLiveStreaming()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeAudioCodec = true,
|
||||
NormalizeAudio = true,
|
||||
AudioCodec = "aac"
|
||||
};
|
||||
|
||||
@@ -682,11 +673,11 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_SetAudioBitrate_When_NormalizingWrongCodec_ForTransportStream()
|
||||
public void Should_SetAudioBitrate_When_NormalizingVideo_ForTransportStream()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeAudioCodec = true,
|
||||
NormalizeAudio = true,
|
||||
AudioBitrate = 2424
|
||||
};
|
||||
|
||||
@@ -705,11 +696,11 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_SetAudioBufferSize_When_NormalizingWrongCodec_ForTransportStream()
|
||||
public void Should_SetAudioBufferSize_When_NormalizingVideo_ForTransportStream()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeAudioCodec = true,
|
||||
NormalizeAudio = true,
|
||||
AudioBufferSize = 2424
|
||||
};
|
||||
|
||||
@@ -732,7 +723,6 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeAudioCodec = true,
|
||||
NormalizeAudio = true,
|
||||
AudioCodec = "ac3",
|
||||
AudioChannels = 6
|
||||
@@ -757,7 +747,6 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeAudioCodec = true,
|
||||
NormalizeAudio = true,
|
||||
AudioCodec = "ac3",
|
||||
AudioSampleRate = 48
|
||||
@@ -778,11 +767,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_SetAudioChannels_When_NormalizingWrongCodecAndAudio_ForTransportStream()
|
||||
public void Should_SetAudioChannels_When_NormalizingVideoAndAudio_ForTransportStream()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeAudioCodec = true,
|
||||
NormalizeAudio = true,
|
||||
AudioChannels = 6
|
||||
};
|
||||
@@ -802,11 +790,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_SetAudioSampleRate_When_NormalizingWrongCodecAndAudio_ForTransportStream()
|
||||
public void Should_SetAudioSampleRate_When_NormalizingVideoAndAudio_ForTransportStream()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
NormalizeAudioCodec = true,
|
||||
NormalizeAudio = true,
|
||||
AudioSampleRate = 48
|
||||
};
|
||||
|
||||
@@ -9,13 +9,12 @@
|
||||
public HardwareAccelerationKind HardwareAcceleration { get; set; }
|
||||
public int ResolutionId { get; set; }
|
||||
public Resolution Resolution { get; set; }
|
||||
public bool NormalizeResolution { get; set; }
|
||||
public string VideoCodec { get; set; }
|
||||
public bool NormalizeVideoCodec { get; set; }
|
||||
public bool NormalizeVideo { get; set; }
|
||||
public int VideoBitrate { get; set; }
|
||||
public int VideoBufferSize { get; set; }
|
||||
public string FrameRate { get; set; }
|
||||
public string AudioCodec { get; set; }
|
||||
public bool NormalizeAudioCodec { get; set; }
|
||||
public int AudioBitrate { get; set; }
|
||||
public int AudioBufferSize { get; set; }
|
||||
public int AudioVolume { get; set; }
|
||||
@@ -27,7 +26,7 @@
|
||||
new()
|
||||
{
|
||||
Name = name,
|
||||
ThreadCount = 4,
|
||||
ThreadCount = 0,
|
||||
Transcode = true,
|
||||
ResolutionId = resolution.Id,
|
||||
Resolution = resolution,
|
||||
@@ -40,9 +39,8 @@
|
||||
AudioVolume = 100,
|
||||
AudioChannels = 2,
|
||||
AudioSampleRate = 48,
|
||||
NormalizeResolution = true,
|
||||
NormalizeVideoCodec = true,
|
||||
NormalizeAudioCodec = true,
|
||||
NormalizeVideo = true,
|
||||
FrameRate = "24",
|
||||
NormalizeAudio = true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
public enum LibraryMediaKind
|
||||
{
|
||||
Movies = 1,
|
||||
Shows = 2
|
||||
Shows = 2,
|
||||
MusicVideos = 3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,16 +13,6 @@ namespace ErsatzTV.Core.Domain
|
||||
public TimeSpan Duration { get; set; }
|
||||
public string SampleAspectRatio { get; set; }
|
||||
public string DisplayAspectRatio { get; set; }
|
||||
|
||||
[Obsolete("Use MediaSource instead")]
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
[Obsolete("Use MediaSource instead")]
|
||||
public string VideoProfile { get; set; }
|
||||
|
||||
[Obsolete("Use MediaSource instead")]
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
public VideoScanKind VideoScanKind { get; set; }
|
||||
public DateTime DateAdded { get; set; }
|
||||
public DateTime DateUpdated { get; set; }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class MusicVideo : MediaItem
|
||||
{
|
||||
public List<MusicVideoMetadata> MusicVideoMetadata { get; set; }
|
||||
public List<MediaVersion> MediaVersions { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class MusicVideoMetadata : Metadata
|
||||
{
|
||||
public string Album { get; set; }
|
||||
public string Plot { get; set; }
|
||||
public string Artist { get; set; }
|
||||
public int MusicVideoId { get; set; }
|
||||
public MusicVideo MusicVideo { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
private Option<TimeSpan> _audioDuration = None;
|
||||
private bool _deinterlace;
|
||||
private Option<string> _frameRate = None;
|
||||
private Option<HardwareAccelerationKind> _hardwareAccelerationKind = None;
|
||||
private string _inputCodec;
|
||||
private Option<IDisplaySize> _padToSize = None;
|
||||
@@ -54,6 +55,12 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegComplexFilterBuilder WithFrameRate(Option<string> frameRate)
|
||||
{
|
||||
_frameRate = frameRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Option<FFmpegComplexFilter> Build(int videoStreamIndex, int audioStreamIndex)
|
||||
{
|
||||
var complexFilter = new StringBuilder();
|
||||
@@ -104,6 +111,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
}
|
||||
}
|
||||
|
||||
_frameRate.IfSome(frameRate => filterQueue.Add($"fps=fps={frameRate}"));
|
||||
|
||||
_scaleToSize.IfSome(
|
||||
size =>
|
||||
{
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
public Option<TimeSpan> StreamSeek { get; set; }
|
||||
public Option<IDisplaySize> ScaledSize { get; set; }
|
||||
public bool PadToDesiredResolution { get; set; }
|
||||
public string ScalingAlgorithm => "fast_bilinear"; // TODO: from config, add tests
|
||||
public string VideoCodec { get; set; }
|
||||
public Option<int> VideoBitrate { get; set; }
|
||||
public Option<int> VideoBufferSize { get; set; }
|
||||
@@ -27,5 +26,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
public Option<TimeSpan> AudioDuration { get; set; }
|
||||
public string AudioCodec { get; set; }
|
||||
public bool Deinterlace { get; set; }
|
||||
public Option<string> FrameRate { get; set; }
|
||||
public Option<int> VideoTrackTimeScale { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +81,20 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
}
|
||||
|
||||
IDisplaySize sizeAfterScaling = result.ScaledSize.IfNone(version);
|
||||
if (ffmpegProfile.NormalizeResolution && !sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution))
|
||||
if (ffmpegProfile.NormalizeVideo && !sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution))
|
||||
{
|
||||
result.PadToDesiredResolution = true;
|
||||
}
|
||||
|
||||
if (ffmpegProfile.NormalizeVideo)
|
||||
{
|
||||
result.FrameRate = string.IsNullOrWhiteSpace(ffmpegProfile.FrameRate)
|
||||
? None
|
||||
: Some(ffmpegProfile.FrameRate);
|
||||
|
||||
result.VideoTrackTimeScale = 90000;
|
||||
}
|
||||
|
||||
if (result.ScaledSize.IsSome || result.PadToDesiredResolution ||
|
||||
NeedToNormalizeVideoCodec(ffmpegProfile, videoStream))
|
||||
{
|
||||
@@ -141,7 +150,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
};
|
||||
|
||||
private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaVersion version) =>
|
||||
ffmpegProfile.NormalizeResolution &&
|
||||
ffmpegProfile.NormalizeVideo &&
|
||||
IsIncorrectSize(ffmpegProfile.Resolution, version) ||
|
||||
IsTooLarge(ffmpegProfile.Resolution, version) ||
|
||||
IsOddSize(version);
|
||||
@@ -159,10 +168,10 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
version.Height % 2 == 1 || version.Width % 2 == 1;
|
||||
|
||||
private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaStream videoStream) =>
|
||||
ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != videoStream.Codec;
|
||||
ffmpegProfile.NormalizeVideo && ffmpegProfile.VideoCodec != videoStream.Codec;
|
||||
|
||||
private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaStream audioStream) =>
|
||||
ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != audioStream.Codec;
|
||||
ffmpegProfile.NormalizeAudio && ffmpegProfile.AudioCodec != audioStream.Codec;
|
||||
|
||||
private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version)
|
||||
{
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
private readonly string _ffmpegPath;
|
||||
private readonly bool _saveReports;
|
||||
private FFmpegComplexFilterBuilder _complexFilterBuilder = new();
|
||||
private bool _isConcat;
|
||||
|
||||
public FFmpegProcessBuilder(string ffmpegPath, bool saveReports)
|
||||
{
|
||||
@@ -186,6 +187,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
|
||||
public FFmpegProcessBuilder WithConcat(string concatPlaylist)
|
||||
{
|
||||
_isConcat = true;
|
||||
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"-f", "concat",
|
||||
@@ -193,8 +196,6 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
"-protocol_whitelist", "file,http,tcp,https,tcp,tls",
|
||||
"-probesize", "32",
|
||||
"-i", concatPlaylist,
|
||||
"-map", "0:v",
|
||||
"-map", "0:a",
|
||||
"-c", "copy",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0"
|
||||
@@ -228,7 +229,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
const string X = "x=(w-text_w)/2";
|
||||
const string Y = "y=(h-text_h)/3*2";
|
||||
|
||||
string fontSize = text.Length > 60 ? "fontsize=40" : "fontsize=60";
|
||||
string fontSize = text.Length > 80 ? "fontsize=30" : text.Length > 60 ? "fontsize=40" : "fontsize=60";
|
||||
|
||||
return WithFilterComplex(
|
||||
$"[0:0]scale={desiredResolution.Width}:{desiredResolution.Height},drawtext={FONT_FILE}:{fontSize}:{FONT_COLOR}:{X}:{Y}:text='{text}'[v]",
|
||||
@@ -323,6 +324,23 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithFrameRate(Option<string> frameRate)
|
||||
{
|
||||
_complexFilterBuilder = _complexFilterBuilder.WithFrameRate(frameRate);
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithVideoTrackTimeScale(Option<int> videoTrackTimeScale)
|
||||
{
|
||||
videoTrackTimeScale.IfSome(
|
||||
timeScale =>
|
||||
{
|
||||
_arguments.Add("-video_track_timescale");
|
||||
_arguments.Add($"{timeScale}");
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithDeinterlace(bool deinterlace)
|
||||
{
|
||||
_complexFilterBuilder = _complexFilterBuilder.WithDeinterlace(deinterlace);
|
||||
@@ -373,10 +391,13 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
|
||||
if (_saveReports)
|
||||
{
|
||||
string fileName = Path.Combine(FileSystemLayout.FFmpegReportsFolder, "%p-%t.log");
|
||||
string fileName = _isConcat
|
||||
? Path.Combine(FileSystemLayout.FFmpegReportsFolder, "ffmpeg-%t-concat.log")
|
||||
: Path.Combine(FileSystemLayout.FFmpegReportsFolder, "ffmpeg-%t-transcode.log");
|
||||
startInfo.EnvironmentVariables.Add("FFREPORT", $"file={fileName}:level=32");
|
||||
}
|
||||
|
||||
startInfo.ArgumentList.Add("-nostdin");
|
||||
foreach (string argument in _arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
|
||||
@@ -48,7 +48,9 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
||||
.WithSeek(playbackSettings.StreamSeek)
|
||||
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec);
|
||||
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec)
|
||||
.WithFrameRate(playbackSettings.FrameRate)
|
||||
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale);
|
||||
|
||||
playbackSettings.ScaledSize.Match(
|
||||
scaledSize =>
|
||||
@@ -124,11 +126,11 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return builder.WithPipe().Build();
|
||||
}
|
||||
|
||||
public Process ConcatChannel(string ffmpegPath, Channel channel, string scheme, string host)
|
||||
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
|
||||
{
|
||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
|
||||
|
||||
return new FFmpegProcessBuilder(ffmpegPath, false)
|
||||
return new FFmpegProcessBuilder(ffmpegPath, saveReports)
|
||||
.WithThreads(1)
|
||||
.WithQuiet()
|
||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalMetadataProvider
|
||||
{
|
||||
Task<ShowMetadata> GetMetadataForShow(string showFolder);
|
||||
Task<Option<MusicVideoMetadata>> GetMetadataForMusicVideo(string filePath);
|
||||
Task<bool> RefreshSidecarMetadata(MediaItem mediaItem, string path);
|
||||
Task<bool> RefreshSidecarMetadata(Show televisionShow, string showFolder);
|
||||
Task<bool> RefreshFallbackMetadata(MediaItem mediaItem);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface IMusicVideoFolderScanner
|
||||
{
|
||||
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
{
|
||||
public interface IMusicVideoRepository
|
||||
{
|
||||
Task<Option<MusicVideo>> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata);
|
||||
|
||||
Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> Add(
|
||||
LibraryPath libraryPath,
|
||||
string filePath,
|
||||
MusicVideoMetadata metadata);
|
||||
|
||||
Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath);
|
||||
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
|
||||
Task<bool> AddGenre(MusicVideoMetadata metadata, Genre genre);
|
||||
Task<bool> AddTag(MusicVideoMetadata metadata, Tag tag);
|
||||
Task<bool> AddStudio(MusicVideoMetadata metadata, Studio studio);
|
||||
Task<List<MusicVideoMetadata>> GetMusicVideosForCards(List<int> ids);
|
||||
Task<Option<MusicVideo>> GetMusicVideo(int musicVideoId);
|
||||
}
|
||||
}
|
||||
@@ -221,6 +221,8 @@ namespace ErsatzTV.Core.Iptv
|
||||
.IfNone("[unknown movie]"),
|
||||
Episode e => e.Season.Show.ShowMetadata.HeadOrNone().Map(em => em.Title ?? string.Empty)
|
||||
.IfNone("[unknown show]"),
|
||||
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Map(mvm => $"{mvm.Artist} - {mvm.Title}")
|
||||
.IfNone("[unknown music video]"),
|
||||
_ => "[unknown]"
|
||||
};
|
||||
}
|
||||
@@ -253,6 +255,8 @@ namespace ErsatzTV.Core.Iptv
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Plot ?? string.Empty).IfNone(string.Empty),
|
||||
Episode e => e.EpisodeMetadata.HeadOrNone().Map(em => em.Plot ?? string.Empty)
|
||||
.IfNone(string.Empty),
|
||||
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Map(mvm => mvm.Plot ?? string.Empty)
|
||||
.IfNone(string.Empty),
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
|
||||
@@ -18,18 +18,21 @@ namespace ErsatzTV.Core.Metadata
|
||||
private static readonly XmlSerializer MovieSerializer = new(typeof(MovieNfo));
|
||||
private static readonly XmlSerializer EpisodeSerializer = new(typeof(TvShowEpisodeNfo));
|
||||
private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowNfo));
|
||||
private static readonly XmlSerializer MusicVideoSerializer = new(typeof(MusicVideoNfo));
|
||||
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILogger<LocalMetadataProvider> _logger;
|
||||
|
||||
private readonly IMetadataRepository _metadataRepository;
|
||||
private readonly IMovieRepository _movieRepository;
|
||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
|
||||
public LocalMetadataProvider(
|
||||
IMetadataRepository metadataRepository,
|
||||
IMovieRepository movieRepository,
|
||||
ITelevisionRepository televisionRepository,
|
||||
IMusicVideoRepository musicVideoRepository,
|
||||
IFallbackMetadataProvider fallbackMetadataProvider,
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILogger<LocalMetadataProvider> logger)
|
||||
@@ -37,6 +40,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
_metadataRepository = metadataRepository;
|
||||
_movieRepository = movieRepository;
|
||||
_televisionRepository = televisionRepository;
|
||||
_musicVideoRepository = musicVideoRepository;
|
||||
_fallbackMetadataProvider = fallbackMetadataProvider;
|
||||
_localFileSystem = localFileSystem;
|
||||
_logger = logger;
|
||||
@@ -65,6 +69,23 @@ namespace ErsatzTV.Core.Metadata
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<Option<MusicVideoMetadata>> GetMetadataForMusicVideo(string filePath)
|
||||
{
|
||||
string nfoFileName = Path.ChangeExtension(filePath, "nfo");
|
||||
Option<MusicVideoMetadata> maybeMetadata = None;
|
||||
if (_localFileSystem.FileExists(nfoFileName))
|
||||
{
|
||||
maybeMetadata = await LoadMusicVideoMetadata(nfoFileName);
|
||||
}
|
||||
|
||||
return maybeMetadata.Map(
|
||||
metadata =>
|
||||
{
|
||||
metadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(metadata.Title);
|
||||
return metadata;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> RefreshSidecarMetadata(MediaItem mediaItem, string path) =>
|
||||
mediaItem switch
|
||||
{
|
||||
@@ -78,6 +99,11 @@ namespace ErsatzTV.Core.Metadata
|
||||
maybeMetadata => maybeMetadata.Match(
|
||||
metadata => ApplyMetadataUpdate(m, metadata),
|
||||
() => Task.FromResult(false))),
|
||||
MusicVideo mv => LoadMetadata(mv, path)
|
||||
.Bind(
|
||||
maybeMetadata => maybeMetadata.Match(
|
||||
metadata => ApplyMetadataUpdate(mv, metadata),
|
||||
() => Task.FromResult(false))),
|
||||
_ => Task.FromResult(false)
|
||||
};
|
||||
|
||||
@@ -98,6 +124,37 @@ namespace ErsatzTV.Core.Metadata
|
||||
public Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder) =>
|
||||
ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder));
|
||||
|
||||
private async Task<Option<MusicVideoMetadata>> LoadMusicVideoMetadata(string nfoFileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
|
||||
Option<MusicVideoNfo> maybeNfo = MusicVideoSerializer.Deserialize(fileStream) as MusicVideoNfo;
|
||||
return maybeNfo.Match<Option<MusicVideoMetadata>>(
|
||||
nfo => new MusicVideoMetadata
|
||||
{
|
||||
MetadataKind = MetadataKind.Sidecar,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = File.GetLastWriteTimeUtc(nfoFileName),
|
||||
Artist = nfo.Artist,
|
||||
Album = nfo.Album,
|
||||
Title = nfo.Title,
|
||||
Plot = nfo.Plot,
|
||||
Year = GetYear(nfo.Year, nfo.Premiered),
|
||||
ReleaseDate = GetAired(nfo.Year, nfo.Premiered),
|
||||
Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(),
|
||||
Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(),
|
||||
Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList()
|
||||
},
|
||||
None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to read music video nfo metadata from {Path}", nfoFileName);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ApplyMetadataUpdate(Episode episode, Tuple<EpisodeMetadata, int> metadataEpisodeNumber)
|
||||
{
|
||||
(EpisodeMetadata metadata, int episodeNumber) = metadataEpisodeNumber;
|
||||
@@ -344,6 +401,106 @@ namespace ErsatzTV.Core.Metadata
|
||||
return await _metadataRepository.Add(metadata);
|
||||
});
|
||||
|
||||
private Task<bool> ApplyMetadataUpdate(MusicVideo musicVideo, MusicVideoMetadata metadata) =>
|
||||
Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone().Match(
|
||||
async existing =>
|
||||
{
|
||||
var updated = false;
|
||||
|
||||
existing.Artist = metadata.Artist;
|
||||
existing.Title = metadata.Title;
|
||||
existing.Year = metadata.Year;
|
||||
existing.Plot = metadata.Plot;
|
||||
existing.Album = metadata.Album;
|
||||
|
||||
if (existing.DateAdded == DateTime.MinValue)
|
||||
{
|
||||
existing.DateAdded = metadata.DateAdded;
|
||||
}
|
||||
|
||||
existing.DateUpdated = metadata.DateUpdated;
|
||||
existing.MetadataKind = metadata.MetadataKind;
|
||||
existing.OriginalTitle = metadata.OriginalTitle;
|
||||
existing.ReleaseDate = metadata.ReleaseDate;
|
||||
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
|
||||
foreach (Genre genre in existing.Genres.Filter(g => metadata.Genres.All(g2 => g2.Name != g.Name))
|
||||
.ToList())
|
||||
{
|
||||
existing.Genres.Remove(genre);
|
||||
if (await _metadataRepository.RemoveGenre(genre))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Genre genre in metadata.Genres.Filter(g => existing.Genres.All(g2 => g2.Name != g.Name))
|
||||
.ToList())
|
||||
{
|
||||
existing.Genres.Add(genre);
|
||||
if (await _musicVideoRepository.AddGenre(existing, genre))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Tag tag in existing.Tags.Filter(t => metadata.Tags.All(t2 => t2.Name != t.Name))
|
||||
.ToList())
|
||||
{
|
||||
existing.Tags.Remove(tag);
|
||||
if (await _metadataRepository.RemoveTag(tag))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Tag tag in metadata.Tags.Filter(t => existing.Tags.All(t2 => t2.Name != t.Name))
|
||||
.ToList())
|
||||
{
|
||||
existing.Tags.Add(tag);
|
||||
if (await _musicVideoRepository.AddTag(existing, tag))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Studio studio in existing.Studios
|
||||
.Filter(s => metadata.Studios.All(s2 => s2.Name != s.Name))
|
||||
.ToList())
|
||||
{
|
||||
existing.Studios.Remove(studio);
|
||||
if (await _metadataRepository.RemoveStudio(studio))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Studio studio in metadata.Studios
|
||||
.Filter(s => existing.Studios.All(s2 => s2.Name != s.Name))
|
||||
.ToList())
|
||||
{
|
||||
existing.Studios.Add(studio);
|
||||
if (await _musicVideoRepository.AddStudio(existing, studio))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return await _metadataRepository.Update(existing) || updated;
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
|
||||
? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
|
||||
: metadata.SortTitle;
|
||||
metadata.MusicVideoId = musicVideo.Id;
|
||||
musicVideo.MusicVideoMetadata = new List<MusicVideoMetadata> { metadata };
|
||||
|
||||
return await _metadataRepository.Add(metadata);
|
||||
});
|
||||
|
||||
private async Task<Option<MovieMetadata>> LoadMetadata(Movie mediaItem, string nfoFileName)
|
||||
{
|
||||
if (nfoFileName == null || !File.Exists(nfoFileName))
|
||||
@@ -377,6 +534,17 @@ namespace ErsatzTV.Core.Metadata
|
||||
return await LoadTelevisionShowMetadata(nfoFileName);
|
||||
}
|
||||
|
||||
private async Task<Option<MusicVideoMetadata>> LoadMetadata(MusicVideo musicVideo, string nfoFileName)
|
||||
{
|
||||
if (nfoFileName == null || !File.Exists(nfoFileName))
|
||||
{
|
||||
_logger.LogDebug("NFO file does not exist at {Path}", nfoFileName);
|
||||
return None;
|
||||
}
|
||||
|
||||
return await LoadMusicVideoMetadata(nfoFileName);
|
||||
}
|
||||
|
||||
private async Task<Option<ShowMetadata>> LoadTelevisionShowMetadata(string nfoFileName)
|
||||
{
|
||||
try
|
||||
@@ -589,5 +757,36 @@ namespace ErsatzTV.Core.Metadata
|
||||
[XmlElement("plot")]
|
||||
public string Plot { get; set; }
|
||||
}
|
||||
|
||||
[XmlRoot("musicvideo")]
|
||||
public class MusicVideoNfo
|
||||
{
|
||||
[XmlElement("artist")]
|
||||
public string Artist { get; set; }
|
||||
|
||||
[XmlElement("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[XmlElement("album")]
|
||||
public string Album { get; set; }
|
||||
|
||||
[XmlElement("plot")]
|
||||
public string Plot { get; set; }
|
||||
|
||||
[XmlElement("premiered")]
|
||||
public string Premiered { get; set; }
|
||||
|
||||
[XmlElement("year")]
|
||||
public int Year { get; set; }
|
||||
|
||||
[XmlElement("genre")]
|
||||
public List<string> Genres { get; set; }
|
||||
|
||||
[XmlElement("tag")]
|
||||
public List<string> Tags { get; set; }
|
||||
|
||||
[XmlElement("studio")]
|
||||
public List<string> Studios { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
Movie m => m.MediaVersions.Head().MediaFiles.Head().Path,
|
||||
Episode e => e.MediaVersions.Head().MediaFiles.Head().Path,
|
||||
MusicVideo mv => mv.MediaVersions.Head().MediaFiles.Head().Path,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
@@ -63,6 +64,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
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 ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScanner
|
||||
{
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILocalMetadataProvider _localMetadataProvider;
|
||||
private readonly ILogger<MusicVideoFolderScanner> _logger;
|
||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public MusicVideoFolderScanner(
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalMetadataProvider localMetadataProvider,
|
||||
IMetadataRepository metadataRepository,
|
||||
IImageCache imageCache,
|
||||
ISearchIndex searchIndex,
|
||||
IMusicVideoRepository musicVideoRepository,
|
||||
ILogger<MusicVideoFolderScanner> logger) : base(
|
||||
localFileSystem,
|
||||
localStatisticsProvider,
|
||||
metadataRepository,
|
||||
imageCache,
|
||||
logger)
|
||||
{
|
||||
_localFileSystem = localFileSystem;
|
||||
_localMetadataProvider = localMetadataProvider;
|
||||
_searchIndex = searchIndex;
|
||||
_musicVideoRepository = musicVideoRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> ScanFolder(
|
||||
LibraryPath libraryPath,
|
||||
string ffprobePath,
|
||||
DateTimeOffset lastScan)
|
||||
{
|
||||
if (!_localFileSystem.IsLibraryPathAccessible(libraryPath))
|
||||
{
|
||||
return new MediaSourceInaccessible();
|
||||
}
|
||||
|
||||
var folderQueue = new Queue<string>();
|
||||
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).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;
|
||||
}
|
||||
|
||||
if (_localFileSystem.GetLastWriteTime(movieFolder) < lastScan)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (string file in allFiles.OrderBy(identity))
|
||||
{
|
||||
// TODO: figure out how to rebuild playouts
|
||||
Either<BaseError, MediaItemScanResult<MusicVideo>> maybeMusicVideo =
|
||||
await FindOrCreateMusicVideo(libraryPath, file)
|
||||
.BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath))
|
||||
.BindT(UpdateMetadata)
|
||||
.BindT(UpdateThumbnail);
|
||||
|
||||
await maybeMusicVideo.Match(
|
||||
async result =>
|
||||
{
|
||||
if (result.IsAdded)
|
||||
{
|
||||
await _searchIndex.AddItems(new List<MediaItem> { result.Item });
|
||||
}
|
||||
else if (result.IsUpdated)
|
||||
{
|
||||
await _searchIndex.UpdateItems(new List<MediaItem> { result.Item });
|
||||
}
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning("Error processing music video at {Path}: {Error}", file, error.Value);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string path in await _musicVideoRepository.FindMusicVideoPaths(libraryPath))
|
||||
{
|
||||
if (!_localFileSystem.FileExists(path))
|
||||
{
|
||||
_logger.LogInformation("Removing missing music video at {Path}", path);
|
||||
List<int> ids = await _musicVideoRepository.DeleteByPath(libraryPath, path);
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
}
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> FindOrCreateMusicVideo(
|
||||
LibraryPath libraryPath,
|
||||
string filePath)
|
||||
{
|
||||
Option<MusicVideoMetadata> maybeMetadata = await _localMetadataProvider.GetMetadataForMusicVideo(filePath);
|
||||
return await maybeMetadata.Match(
|
||||
async metadata =>
|
||||
{
|
||||
Option<MusicVideo> maybeMusicVideo =
|
||||
await _musicVideoRepository.GetByMetadata(libraryPath, metadata);
|
||||
return await maybeMusicVideo.Match(
|
||||
musicVideo =>
|
||||
Right<BaseError, MediaItemScanResult<MusicVideo>>(
|
||||
new MediaItemScanResult<MusicVideo>(musicVideo))
|
||||
.AsTask(),
|
||||
async () => await _musicVideoRepository.Add(libraryPath, filePath, metadata));
|
||||
},
|
||||
() => Left<BaseError, MediaItemScanResult<MusicVideo>>(
|
||||
BaseError.New("Unable to locate metadata for music video")).AsTask());
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> UpdateMetadata(
|
||||
MediaItemScanResult<MusicVideo> result)
|
||||
{
|
||||
try
|
||||
{
|
||||
MusicVideo musicVideo = result.Item;
|
||||
return await LocateNfoFile(musicVideo).Match<Task<Either<BaseError, MediaItemScanResult<MusicVideo>>>>(
|
||||
async nfoFile =>
|
||||
{
|
||||
bool shouldUpdate = Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone().Match(
|
||||
m => m.MetadataKind == MetadataKind.Fallback ||
|
||||
m.DateUpdated < _localFileSystem.GetLastWriteTime(nfoFile),
|
||||
true);
|
||||
|
||||
if (shouldUpdate)
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile);
|
||||
if (await _localMetadataProvider.RefreshSidecarMetadata(musicVideo, nfoFile))
|
||||
{
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
() => Left<BaseError, MediaItemScanResult<MusicVideo>>(
|
||||
BaseError.New("Unable to locate metadata for music video")).AsTask());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private Option<string> LocateNfoFile(MusicVideo musicVideo)
|
||||
{
|
||||
string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
return Optional(Path.ChangeExtension(path, "nfo"))
|
||||
.Filter(s => _localFileSystem.FileExists(s))
|
||||
.HeadOrNone();
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> UpdateThumbnail(
|
||||
MediaItemScanResult<MusicVideo> result)
|
||||
{
|
||||
try
|
||||
{
|
||||
MusicVideo musicVideo = result.Item;
|
||||
await LocateThumbnail(musicVideo).IfSomeAsync(
|
||||
async thumbnailFile =>
|
||||
{
|
||||
MusicVideoMetadata metadata = musicVideo.MusicVideoMetadata.Head();
|
||||
await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private Option<string> LocateThumbnail(MusicVideo musicVideo)
|
||||
{
|
||||
string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
return ImageFileExtensions
|
||||
.Map(ext => Path.ChangeExtension(path, ext))
|
||||
.Filter(f => _localFileSystem.FileExists(f))
|
||||
.HeadOrNone();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,9 @@ namespace ErsatzTV.Core.Scheduling
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Match(
|
||||
mm => mm.ReleaseDate ?? DateTime.MaxValue,
|
||||
() => DateTime.MaxValue),
|
||||
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match(
|
||||
mvm => mvm.ReleaseDate ?? DateTime.MaxValue,
|
||||
() => DateTime.MaxValue),
|
||||
_ => DateTime.MaxValue
|
||||
};
|
||||
|
||||
@@ -59,6 +62,9 @@ namespace ErsatzTV.Core.Scheduling
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Match(
|
||||
mm => mm.ReleaseDate ?? DateTime.MaxValue,
|
||||
() => DateTime.MaxValue),
|
||||
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match(
|
||||
mvm => mvm.ReleaseDate ?? DateTime.MaxValue,
|
||||
() => DateTime.MaxValue),
|
||||
_ => DateTime.MaxValue
|
||||
};
|
||||
|
||||
|
||||
@@ -99,6 +99,8 @@ namespace ErsatzTV.Core.Scheduling
|
||||
TimeSpan.Zero,
|
||||
Episode e => e.MediaVersions.HeadOrNone().Map(mv => mv.Duration).IfNone(TimeSpan.Zero) ==
|
||||
TimeSpan.Zero,
|
||||
MusicVideo mv => mv.MediaVersions.HeadOrNone().Map(v => v.Duration).IfNone(TimeSpan.Zero) ==
|
||||
TimeSpan.Zero,
|
||||
_ => true
|
||||
})).Map(c => c.Key);
|
||||
if (zeroDurationCollection.IsSome)
|
||||
@@ -180,6 +182,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
@@ -235,11 +238,12 @@ namespace ErsatzTV.Core.Scheduling
|
||||
peekMediaItem =>
|
||||
{
|
||||
customGroup = true;
|
||||
|
||||
|
||||
MediaVersion peekVersion = peekMediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
|
||||
};
|
||||
|
||||
@@ -274,6 +278,7 @@ namespace ErsatzTV.Core.Scheduling
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem))
|
||||
};
|
||||
|
||||
@@ -479,6 +484,9 @@ namespace ErsatzTV.Core.Scheduling
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Match(
|
||||
mm => mm.Title ?? string.Empty,
|
||||
() => "[unknown movie]"),
|
||||
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match(
|
||||
mvm => $"{mvm.Artist} - {mvm.Title}",
|
||||
() => "[unknown music video]"),
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
{
|
||||
public class MusicVideoConfiguration : IEntityTypeConfiguration<MusicVideo>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<MusicVideo> builder)
|
||||
{
|
||||
builder.ToTable("MusicVideo");
|
||||
|
||||
builder.HasMany(m => m.MusicVideoMetadata)
|
||||
.WithOne(m => m.MusicVideo)
|
||||
.HasForeignKey(m => m.MusicVideoId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(m => m.MediaVersions)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
{
|
||||
public class MusicVideoMetadataConfiguration : IEntityTypeConfiguration<MusicVideoMetadata>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<MusicVideoMetadata> builder)
|
||||
{
|
||||
builder.ToTable("MusicVideoMetadata");
|
||||
|
||||
builder.HasMany(mm => mm.Artwork)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(mm => mm.Genres)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(mm => mm.Tags)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(mm => mm.Studios)
|
||||
.WithOne()
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
public Task<List<Library>> GetAll()
|
||||
{
|
||||
using TvContext context = _dbContextFactory.CreateDbContext();
|
||||
return context.Libraries.ToListAsync();
|
||||
return context.Libraries
|
||||
.AsNoTracking()
|
||||
.Include(l => l.MediaSource)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<Unit> UpdateLastScan(Library library) => _dbConnection.ExecuteAsync(
|
||||
|
||||
@@ -104,6 +104,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Movie).MovieMetadata)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as MusicVideo).MusicVideoMetadata)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Show).ShowMetadata)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Season).Show)
|
||||
@@ -128,6 +130,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.ThenInclude(i => (i as Movie).MovieMetadata)
|
||||
.ThenInclude(mm => mm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as MusicVideo).MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Show).ShowMetadata)
|
||||
.ThenInclude(sm => sm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
@@ -196,6 +201,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
result.AddRange(await GetShowItems(collection));
|
||||
result.AddRange(await GetSeasonItems(collection));
|
||||
result.AddRange(await GetEpisodeItems(collection));
|
||||
result.AddRange(await GetMusicVideoItems(collection));
|
||||
|
||||
return result.Distinct().ToList();
|
||||
}
|
||||
@@ -215,6 +221,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<List<MusicVideo>> GetMusicVideoItems(Collection collection)
|
||||
{
|
||||
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
|
||||
@"SELECT m.Id FROM CollectionItem ci
|
||||
INNER JOIN MusicVideo m ON m.Id = ci.MediaItemId
|
||||
WHERE ci.CollectionId = @CollectionId",
|
||||
new { CollectionId = collection.Id });
|
||||
|
||||
return await _dbContext.MusicVideos
|
||||
.Include(m => m.MusicVideoMetadata)
|
||||
.Include(m => m.MediaVersions)
|
||||
.Filter(m => ids.Contains(m.Id))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<List<Episode>> GetShowItems(Collection collection)
|
||||
{
|
||||
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
|
||||
|
||||
@@ -156,6 +156,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
|
||||
parameters)
|
||||
.ToUnit(),
|
||||
MusicVideoMetadata => _dbConnection.ExecuteAsync(
|
||||
@"INSERT INTO Artwork (ArtworkKind, MusicVideoMetadataId, DateAdded, DateUpdated, Path)
|
||||
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
|
||||
parameters)
|
||||
.ToUnit(),
|
||||
_ => Task.FromResult(Unit.Default)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
{
|
||||
public class MusicVideoRepository : IMusicVideoRepository
|
||||
{
|
||||
private readonly IDbConnection _dbConnection;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public MusicVideoRepository(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_dbConnection = dbConnection;
|
||||
}
|
||||
|
||||
public async Task<Option<MusicVideo>> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Option<int> maybeId = await dbContext.MusicVideoMetadata
|
||||
.Where(s => s.Artist == metadata.Artist && s.Title == metadata.Title && s.Year == metadata.Year)
|
||||
.Where(s => s.MusicVideo.LibraryPathId == libraryPath.Id)
|
||||
.SingleOrDefaultAsync()
|
||||
.Map(Optional)
|
||||
.MapT(sm => sm.MusicVideoId);
|
||||
|
||||
return await maybeId.Match(
|
||||
id =>
|
||||
{
|
||||
return dbContext.MusicVideos
|
||||
.AsNoTracking()
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Artwork)
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Genres)
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Tags)
|
||||
.Include(mv => mv.MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Studios)
|
||||
.Include(mv => mv.LibraryPath)
|
||||
.ThenInclude(lp => lp.Library)
|
||||
.Include(mv => mv.MediaVersions)
|
||||
.ThenInclude(mv => mv.MediaFiles)
|
||||
.OrderBy(mv => mv.Id)
|
||||
.SingleOrDefaultAsync(mv => mv.Id == id)
|
||||
.Map(Optional);
|
||||
},
|
||||
() => Option<MusicVideo>.None.AsTask());
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> Add(
|
||||
LibraryPath libraryPath,
|
||||
string filePath,
|
||||
MusicVideoMetadata metadata)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
try
|
||||
{
|
||||
metadata.DateAdded = DateTime.UtcNow;
|
||||
metadata.Genres ??= new List<Genre>();
|
||||
metadata.Tags ??= new List<Tag>();
|
||||
metadata.Studios ??= new List<Studio>();
|
||||
var musicVideo = new MusicVideo
|
||||
{
|
||||
LibraryPathId = libraryPath.Id,
|
||||
MusicVideoMetadata = new List<MusicVideoMetadata> { metadata },
|
||||
MediaVersions = new List<MediaVersion>
|
||||
{
|
||||
new()
|
||||
{
|
||||
MediaFiles = new List<MediaFile>
|
||||
{
|
||||
new() { Path = filePath }
|
||||
},
|
||||
Streams = new List<MediaStream>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await dbContext.MusicVideos.AddAsync(musicVideo);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContext.Entry(musicVideo).Reference(s => s.LibraryPath).LoadAsync();
|
||||
await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
||||
|
||||
return new MediaItemScanResult<MusicVideo>(musicVideo) { IsAdded = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath) =>
|
||||
_dbConnection.QueryAsync<string>(
|
||||
@"SELECT MF.Path
|
||||
FROM MediaFile MF
|
||||
INNER JOIN MediaVersion MV on MF.MediaVersionId = MV.Id
|
||||
INNER JOIN MusicVideo M on MV.MusicVideoId = M.Id
|
||||
INNER JOIN MediaItem MI on M.Id = MI.Id
|
||||
WHERE MI.LibraryPathId = @LibraryPathId",
|
||||
new { LibraryPathId = libraryPath.Id });
|
||||
|
||||
public async Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path)
|
||||
{
|
||||
List<int> ids = await _dbConnection.QueryAsync<int>(
|
||||
@"SELECT M.Id
|
||||
FROM MusicVideo M
|
||||
INNER JOIN MediaItem MI on M.Id = MI.Id
|
||||
INNER JOIN MediaVersion MV on M.Id = MV.EpisodeId
|
||||
INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId
|
||||
WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
|
||||
new { LibraryPathId = libraryPath.Id, Path = path }).Map(result => result.ToList());
|
||||
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
foreach (int musicVideoId in ids)
|
||||
{
|
||||
MusicVideo musicVideo = await dbContext.MusicVideos.FindAsync(musicVideoId);
|
||||
dbContext.MusicVideos.Remove(musicVideo);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
public Task<bool> AddGenre(MusicVideoMetadata metadata, Genre genre) =>
|
||||
_dbConnection.ExecuteAsync(
|
||||
"INSERT INTO Genre (Name, MusicVideoMetadataId) VALUES (@Name, @MetadataId)",
|
||||
new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||
|
||||
public Task<bool> AddTag(MusicVideoMetadata metadata, Tag tag) =>
|
||||
_dbConnection.ExecuteAsync(
|
||||
"INSERT INTO Tag (Name, MusicVideoMetadataId) VALUES (@Name, @MetadataId)",
|
||||
new { tag.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||
|
||||
public Task<bool> AddStudio(MusicVideoMetadata metadata, Studio studio) =>
|
||||
_dbConnection.ExecuteAsync(
|
||||
"INSERT INTO Studio (Name, MusicVideoMetadataId) VALUES (@Name, @MetadataId)",
|
||||
new { studio.Name, MetadataId = metadata.Id }).Map(result => result > 0);
|
||||
|
||||
public async Task<List<MusicVideoMetadata>> GetMusicVideosForCards(List<int> ids)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
return await dbContext.MusicVideoMetadata
|
||||
.AsNoTracking()
|
||||
.Filter(mvm => ids.Contains(mvm.MusicVideoId))
|
||||
.Include(mvm => mvm.Artwork)
|
||||
.OrderBy(mvm => mvm.SortTitle)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Option<MusicVideo>> GetMusicVideo(int musicVideoId)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
return await dbContext.MusicVideos
|
||||
.Include(m => m.MusicVideoMetadata)
|
||||
.ThenInclude(m => m.Artwork)
|
||||
.Include(m => m.MusicVideoMetadata)
|
||||
.ThenInclude(m => m.Genres)
|
||||
.Include(m => m.MusicVideoMetadata)
|
||||
.ThenInclude(m => m.Tags)
|
||||
.Include(m => m.MusicVideoMetadata)
|
||||
.ThenInclude(m => m.Studios)
|
||||
.OrderBy(m => m.Id)
|
||||
.SingleOrDefaultAsync(m => m.Id == musicVideoId)
|
||||
.Map(Optional);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Movie).MediaVersions)
|
||||
.ThenInclude(mv => mv.Streams)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
|
||||
.ThenInclude(mv => mv.MediaFiles)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
|
||||
.ThenInclude(mv => mv.Streams)
|
||||
.AsNoTracking()
|
||||
.SingleOrDefaultAsync()
|
||||
.Map(Optional);
|
||||
@@ -89,6 +95,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Movie).MediaVersions)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata)
|
||||
.ThenInclude(mm => mm.Artwork)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
|
||||
.ThenInclude(em => em.Artwork)
|
||||
.Include(i => i.MediaItem)
|
||||
@@ -96,6 +107,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Episode).Season)
|
||||
.ThenInclude(s => s.SeasonMetadata)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Episode).Season.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
.Filter(i => i.PlayoutId == playoutId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace ErsatzTV.Infrastructure.Data
|
||||
public DbSet<MediaFile> MediaFiles { get; set; }
|
||||
public DbSet<Movie> Movies { get; set; }
|
||||
public DbSet<MovieMetadata> MovieMetadata { get; set; }
|
||||
public DbSet<MusicVideo> MusicVideos { get; set; }
|
||||
public DbSet<MusicVideoMetadata> MusicVideoMetadata { get; set; }
|
||||
public DbSet<Show> Shows { get; set; }
|
||||
public DbSet<ShowMetadata> ShowMetadata { get; set; }
|
||||
public DbSet<Season> Seasons { get; set; }
|
||||
|
||||
Generated
+1829
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Update_FFmpegProfile_ThreadCount : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder) =>
|
||||
migrationBuilder.Sql(@"UPDATE FFmpegProfile SET ThreadCount = 0 WHERE ThreadCount = 4");
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+1829
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_FFmpegProfile_FrameRate : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
"NormalizeResolution",
|
||||
"FFmpegProfile");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
"NormalizeVideoCodec",
|
||||
"FFmpegProfile",
|
||||
"NormalizeVideo");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
"FrameRate",
|
||||
"FFmpegProfile",
|
||||
"TEXT",
|
||||
nullable: true,
|
||||
defaultValue: "24");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
"FrameRate",
|
||||
"FFmpegProfile");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
"NormalizeVideo",
|
||||
"FFmpegProfile",
|
||||
"NormalizeVideoCodec");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
"NormalizeResolution",
|
||||
"FFmpegProfile",
|
||||
"INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1826
File diff suppressed because it is too large
Load Diff
+20
@@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Remove_FFmpegProfile_NormalizeAudioCodec : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder) =>
|
||||
migrationBuilder.DropColumn(
|
||||
"NormalizeAudioCodec",
|
||||
"FFmpegProfile");
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) =>
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
"NormalizeAudioCodec",
|
||||
"FFmpegProfile",
|
||||
"INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
}
|
||||
Generated
+1826
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_LocalLibrary_MusicVideos : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// create local music videos library
|
||||
migrationBuilder.Sql(
|
||||
@"INSERT INTO Library (Name, MediaKind, MediaSourceId)
|
||||
SELECT 'Music Videos', 3, Id FROM
|
||||
(SELECT LMS.Id FROM LocalMediaSource LMS
|
||||
INNER JOIN Library L on L.MediaSourceId = LMS.Id
|
||||
INNER JOIN LocalLibrary LL on L.Id = LL.Id
|
||||
WHERE L.Name = 'Movies')");
|
||||
migrationBuilder.Sql("INSERT INTO LocalLibrary (Id) Values (last_insert_rowid())");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1970
File diff suppressed because it is too large
Load Diff
+228
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Add_MusicVideo_MusicVideoMetadata : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"MusicVideoMetadataId",
|
||||
"Tag",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"MusicVideoMetadataId",
|
||||
"Studio",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"MusicVideoId",
|
||||
"MediaVersion",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"MusicVideoMetadataId",
|
||||
"Genre",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"MusicVideoMetadataId",
|
||||
"Artwork",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"MusicVideo",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MusicVideo", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_MusicVideo_MediaItem_Id",
|
||||
x => x.Id,
|
||||
"MediaItem",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"MusicVideoMetadata",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Album = table.Column<string>("TEXT", nullable: true),
|
||||
Plot = table.Column<string>("TEXT", nullable: true),
|
||||
Artist = table.Column<string>("TEXT", nullable: true),
|
||||
MusicVideoId = table.Column<int>("INTEGER", nullable: false),
|
||||
MetadataKind = table.Column<int>("INTEGER", nullable: false),
|
||||
Title = table.Column<string>("TEXT", nullable: true),
|
||||
OriginalTitle = table.Column<string>("TEXT", nullable: true),
|
||||
SortTitle = table.Column<string>("TEXT", nullable: true),
|
||||
Year = table.Column<int>("INTEGER", nullable: true),
|
||||
ReleaseDate = table.Column<DateTime>("TEXT", nullable: true),
|
||||
DateAdded = table.Column<DateTime>("TEXT", nullable: false),
|
||||
DateUpdated = table.Column<DateTime>("TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MusicVideoMetadata", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_MusicVideoMetadata_MusicVideo_MusicVideoId",
|
||||
x => x.MusicVideoId,
|
||||
"MusicVideo",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Tag_MusicVideoMetadataId",
|
||||
"Tag",
|
||||
"MusicVideoMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Studio_MusicVideoMetadataId",
|
||||
"Studio",
|
||||
"MusicVideoMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_MediaVersion_MusicVideoId",
|
||||
"MediaVersion",
|
||||
"MusicVideoId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Genre_MusicVideoMetadataId",
|
||||
"Genre",
|
||||
"MusicVideoMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Artwork_MusicVideoMetadataId",
|
||||
"Artwork",
|
||||
"MusicVideoMetadataId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_MusicVideoMetadata_MusicVideoId",
|
||||
"MusicVideoMetadata",
|
||||
"MusicVideoId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
"FK_Artwork_MusicVideoMetadata_MusicVideoMetadataId",
|
||||
"Artwork",
|
||||
"MusicVideoMetadataId",
|
||||
"MusicVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
"FK_Genre_MusicVideoMetadata_MusicVideoMetadataId",
|
||||
"Genre",
|
||||
"MusicVideoMetadataId",
|
||||
"MusicVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
"FK_MediaVersion_MusicVideo_MusicVideoId",
|
||||
"MediaVersion",
|
||||
"MusicVideoId",
|
||||
"MusicVideo",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
"FK_Studio_MusicVideoMetadata_MusicVideoMetadataId",
|
||||
"Studio",
|
||||
"MusicVideoMetadataId",
|
||||
"MusicVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
"FK_Tag_MusicVideoMetadata_MusicVideoMetadataId",
|
||||
"Tag",
|
||||
"MusicVideoMetadataId",
|
||||
"MusicVideoMetadata",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
"FK_Artwork_MusicVideoMetadata_MusicVideoMetadataId",
|
||||
"Artwork");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
"FK_Genre_MusicVideoMetadata_MusicVideoMetadataId",
|
||||
"Genre");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
"FK_MediaVersion_MusicVideo_MusicVideoId",
|
||||
"MediaVersion");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
"FK_Studio_MusicVideoMetadata_MusicVideoMetadataId",
|
||||
"Studio");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
"FK_Tag_MusicVideoMetadata_MusicVideoMetadataId",
|
||||
"Tag");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"MusicVideoMetadata");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"MusicVideo");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
"IX_Tag_MusicVideoMetadataId",
|
||||
"Tag");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
"IX_Studio_MusicVideoMetadataId",
|
||||
"Studio");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
"IX_MediaVersion_MusicVideoId",
|
||||
"MediaVersion");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
"IX_Genre_MusicVideoMetadataId",
|
||||
"Genre");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
"IX_Artwork_MusicVideoMetadataId",
|
||||
"Artwork");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"MusicVideoMetadataId",
|
||||
"Tag");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"MusicVideoMetadataId",
|
||||
"Studio");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"MusicVideoId",
|
||||
"MediaVersion");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"MusicVideoMetadataId",
|
||||
"Genre");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"MusicVideoMetadataId",
|
||||
"Artwork");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1961
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Remove_MediaVersion_Codecs : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
"AudioCodec",
|
||||
"MediaVersion");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"VideoCodec",
|
||||
"MediaVersion");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"VideoProfile",
|
||||
"MediaVersion");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
"AudioCodec",
|
||||
"MediaVersion",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
"VideoCodec",
|
||||
"MediaVersion",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
"VideoProfile",
|
||||
"MediaVersion",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int?>("MovieMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MusicVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -59,6 +62,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MovieMetadataId");
|
||||
|
||||
b.HasIndex("MusicVideoMetadataId");
|
||||
|
||||
b.HasIndex("SeasonMetadataId");
|
||||
|
||||
b.HasIndex("ShowMetadataId");
|
||||
@@ -240,6 +245,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int>("AudioVolume")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("FrameRate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("HardwareAcceleration")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -249,13 +257,7 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<bool>("NormalizeAudio")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("NormalizeAudioCodec")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("NormalizeResolution")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("NormalizeVideoCodec")
|
||||
b.Property<bool>("NormalizeVideo")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ResolutionId")
|
||||
@@ -297,6 +299,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int?>("MovieMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MusicVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -312,6 +317,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MovieMetadataId");
|
||||
|
||||
b.HasIndex("MusicVideoMetadataId");
|
||||
|
||||
b.HasIndex("SeasonMetadataId");
|
||||
|
||||
b.HasIndex("ShowMetadataId");
|
||||
@@ -475,9 +482,6 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudioCodec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -499,18 +503,15 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int?>("MovieId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MusicVideoId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SampleAspectRatio")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("VideoCodec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("VideoProfile")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("VideoScanKind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -523,6 +524,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MovieId");
|
||||
|
||||
b.HasIndex("MusicVideoId");
|
||||
|
||||
b.ToTable("MediaVersion");
|
||||
});
|
||||
|
||||
@@ -577,6 +580,57 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.ToTable("MovieMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.MusicVideoMetadata",
|
||||
b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Album")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Artist")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateUpdated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MetadataKind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MusicVideoId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("OriginalTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Plot")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ReleaseDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SortTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Year")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MusicVideoId");
|
||||
|
||||
b.ToTable("MusicVideoMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Playout",
|
||||
b =>
|
||||
@@ -916,6 +970,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int?>("MovieMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MusicVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -931,6 +988,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MovieMetadataId");
|
||||
|
||||
b.HasIndex("MusicVideoMetadataId");
|
||||
|
||||
b.HasIndex("SeasonMetadataId");
|
||||
|
||||
b.HasIndex("ShowMetadataId");
|
||||
@@ -952,6 +1011,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<int?>("MovieMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MusicVideoMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -967,6 +1029,8 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.HasIndex("MovieMetadataId");
|
||||
|
||||
b.HasIndex("MusicVideoMetadataId");
|
||||
|
||||
b.HasIndex("SeasonMetadataId");
|
||||
|
||||
b.HasIndex("ShowMetadataId");
|
||||
@@ -1039,6 +1103,15 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.ToTable("Movie");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.MusicVideo",
|
||||
b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaItem");
|
||||
|
||||
b.ToTable("MusicVideo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Season",
|
||||
b =>
|
||||
@@ -1210,6 +1283,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.HasForeignKey("MovieMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null)
|
||||
.WithMany("Artwork")
|
||||
.HasForeignKey("MusicVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Artwork")
|
||||
.HasForeignKey("SeasonMetadataId")
|
||||
@@ -1294,6 +1372,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.HasForeignKey("MovieMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null)
|
||||
.WithMany("Genres")
|
||||
.HasForeignKey("MusicVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Genres")
|
||||
.HasForeignKey("SeasonMetadataId");
|
||||
@@ -1382,6 +1465,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.WithMany("MediaVersions")
|
||||
.HasForeignKey("MovieId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.MusicVideo", null)
|
||||
.WithMany("MediaVersions")
|
||||
.HasForeignKey("MusicVideoId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
@@ -1397,6 +1485,19 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Navigation("Movie");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.MusicVideoMetadata",
|
||||
b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MusicVideo", "MusicVideo")
|
||||
.WithMany("MusicVideoMetadata")
|
||||
.HasForeignKey("MusicVideoId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MusicVideo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Playout",
|
||||
b =>
|
||||
@@ -1630,6 +1731,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.HasForeignKey("MovieMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null)
|
||||
.WithMany("Studios")
|
||||
.HasForeignKey("MusicVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Studios")
|
||||
.HasForeignKey("SeasonMetadataId");
|
||||
@@ -1653,6 +1759,11 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.HasForeignKey("MovieMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null)
|
||||
.WithMany("Tags")
|
||||
.HasForeignKey("MusicVideoMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null)
|
||||
.WithMany("Tags")
|
||||
.HasForeignKey("SeasonMetadataId");
|
||||
@@ -1726,6 +1837,17 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.MusicVideo",
|
||||
b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.MusicVideo", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Season",
|
||||
b =>
|
||||
@@ -1920,6 +2042,19 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Navigation("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.MusicVideoMetadata",
|
||||
b =>
|
||||
{
|
||||
b.Navigation("Artwork");
|
||||
|
||||
b.Navigation("Genres");
|
||||
|
||||
b.Navigation("Studios");
|
||||
|
||||
b.Navigation("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Playout",
|
||||
b =>
|
||||
@@ -1982,6 +2117,15 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Navigation("MovieMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.MusicVideo",
|
||||
b =>
|
||||
{
|
||||
b.Navigation("MediaVersions");
|
||||
|
||||
b.Navigation("MusicVideoMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.Domain.Season",
|
||||
b =>
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace ErsatzTV.Infrastructure.Plex.Models
|
||||
public string Thumb { get; set; }
|
||||
public string Art { get; set; }
|
||||
public string OriginallyAvailableAt { get; set; }
|
||||
public int AddedAt { get; set; }
|
||||
public int UpdatedAt { get; set; }
|
||||
public long AddedAt { get; set; }
|
||||
public long UpdatedAt { get; set; }
|
||||
public int Index { get; set; }
|
||||
public string Studio { get; set; }
|
||||
public List<PlexMediaResponse> Media { get; set; }
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
|
||||
private const string IdField = "id";
|
||||
private const string TypeField = "type";
|
||||
private const string ArtistField = "artist";
|
||||
private const string TitleField = "title";
|
||||
private const string SortTitleField = "sort_title";
|
||||
private const string GenreField = "genre";
|
||||
@@ -42,6 +43,7 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
|
||||
private const string MovieType = "movie";
|
||||
private const string ShowType = "show";
|
||||
private const string MusicVideoType = "music_video";
|
||||
|
||||
private static bool _isRebuilding;
|
||||
|
||||
@@ -93,6 +95,9 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
case Show show:
|
||||
UpdateShow(show, writer);
|
||||
break;
|
||||
case MusicVideo musicVideo:
|
||||
UpdateMusicVideo(musicVideo, writer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,6 +126,9 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
case Show show:
|
||||
UpdateShow(show, writer);
|
||||
break;
|
||||
case MusicVideo musicVideo:
|
||||
UpdateMusicVideo(musicVideo, writer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,6 +346,66 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMusicVideo(MusicVideo musicVideo, IndexWriter writer)
|
||||
{
|
||||
Option<MusicVideoMetadata> maybeMetadata = musicVideo.MusicVideoMetadata.HeadOrNone();
|
||||
if (maybeMetadata.IsSome)
|
||||
{
|
||||
MusicVideoMetadata metadata = maybeMetadata.ValueUnsafe();
|
||||
|
||||
try
|
||||
{
|
||||
var doc = new Document
|
||||
{
|
||||
new StringField(IdField, musicVideo.Id.ToString(), Field.Store.YES),
|
||||
new StringField(TypeField, MusicVideoType, Field.Store.NO),
|
||||
new TextField(ArtistField, metadata.Artist, Field.Store.NO),
|
||||
new TextField(TitleField, metadata.Title, Field.Store.NO),
|
||||
new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO),
|
||||
new TextField(LibraryNameField, musicVideo.LibraryPath.Library.Name, Field.Store.NO),
|
||||
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
|
||||
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES)
|
||||
};
|
||||
|
||||
if (metadata.ReleaseDate.HasValue)
|
||||
{
|
||||
doc.Add(
|
||||
new StringField(
|
||||
ReleaseDateField,
|
||||
metadata.ReleaseDate.Value.ToString("yyyyMMdd"),
|
||||
Field.Store.NO));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.Plot))
|
||||
{
|
||||
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Genre genre in metadata.Genres)
|
||||
{
|
||||
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Tag tag in metadata.Tags)
|
||||
{
|
||||
doc.Add(new TextField(TagField, tag.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
foreach (Studio studio in metadata.Studios)
|
||||
{
|
||||
doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO));
|
||||
}
|
||||
|
||||
writer.UpdateDocument(new Term(IdField, musicVideo.Id.ToString()), doc);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
metadata.MusicVideo = null;
|
||||
_logger.LogWarning(ex, "Error indexing music video with metadata {@Metadata}", metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SearchItem ProjectToSearchItem(Document doc) => new(Convert.ToInt32(doc.Get(IdField)));
|
||||
|
||||
private Query ParseQuery(string searchQuery, QueryParser parser)
|
||||
|
||||
@@ -44,6 +44,9 @@ namespace ErsatzTV.Controllers
|
||||
process =>
|
||||
{
|
||||
_logger.LogInformation("Starting ts stream for channel {ChannelNumber}", channelNumber);
|
||||
// _logger.LogDebug(
|
||||
// "ffmpeg concat arguments {FFmpegArguments}",
|
||||
// string.Join(" ", process.StartInfo.ArgumentList));
|
||||
process.Start();
|
||||
return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t");
|
||||
},
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#episodes")">@_data.EpisodeCards.Count Episodes</MudLink>
|
||||
}
|
||||
@if (_data.MusicVideoCards.Any())
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#music_videos")">@_data.MusicVideoCards.Count Music Videos</MudLink>
|
||||
}
|
||||
@if (SupportsCustomOrdering())
|
||||
{
|
||||
<div style="margin-left: auto">
|
||||
@@ -165,6 +169,29 @@
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_data.MusicVideoCards.Any())
|
||||
{
|
||||
<MudText GutterBottom="true"
|
||||
Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
|
||||
Music Videos
|
||||
</MudText>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (MusicVideoCardViewModel card in _data.MusicVideoCards.OrderBy(e => e.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link=""
|
||||
DeleteClicked="@RemoveMusicVideoFromCollection"
|
||||
SelectColor="@Color.Error"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
@@ -215,14 +242,12 @@
|
||||
|
||||
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
|
||||
{
|
||||
List<MediaCardViewModel> GetSortedItems()
|
||||
{
|
||||
return _data.MovieCards.OrderBy(m => m.SortTitle)
|
||||
.Append<MediaCardViewModel>(_data.ShowCards.OrderBy(s => s.SortTitle))
|
||||
.Append(_data.SeasonCards.OrderBy(s => s.SortTitle))
|
||||
.Append(_data.EpisodeCards.OrderBy(ep => ep.Aired))
|
||||
.ToList();
|
||||
}
|
||||
List<MediaCardViewModel> GetSortedItems() => _data.MovieCards.OrderBy(m => m.SortTitle)
|
||||
.Append<MediaCardViewModel>(_data.ShowCards.OrderBy(s => s.SortTitle))
|
||||
.Append(_data.SeasonCards.OrderBy(s => s.SortTitle))
|
||||
.Append(_data.EpisodeCards.OrderBy(ep => ep.Aired))
|
||||
.Append(_data.MusicVideoCards.OrderBy(mv => mv.SortTitle))
|
||||
.ToList();
|
||||
|
||||
SelectClicked(GetSortedItems, card, e);
|
||||
}
|
||||
@@ -240,6 +265,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveMusicVideoFromCollection(MediaCardViewModel vm)
|
||||
{
|
||||
if (vm is MusicVideoCardViewModel musicVideo)
|
||||
{
|
||||
var request = new RemoveItemsFromCollection(Id)
|
||||
{
|
||||
MediaItemIds = new List<int> { musicVideo.MusicVideoId }
|
||||
};
|
||||
|
||||
await RemoveItemsWithConfirmation("music video", $"{musicVideo.Title} ({musicVideo.Subtitle})", request);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveShowFromCollection(MediaCardViewModel vm)
|
||||
{
|
||||
if (vm is TelevisionShowCardViewModel show)
|
||||
|
||||
@@ -74,17 +74,17 @@
|
||||
@(context.Transcode ? "Yes" : "No")
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Resolution">
|
||||
<MudText Color="@(context.Transcode && context.NormalizeResolution ? Color.Tertiary : Color.Inherit)">
|
||||
<MudText Color="@(context.Transcode && context.NormalizeVideo ? Color.Tertiary : Color.Inherit)">
|
||||
@context.Resolution.Name
|
||||
</MudText>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Video Codec">
|
||||
<MudText Color="@(context.Transcode && context.NormalizeVideoCodec ? Color.Tertiary : Color.Inherit)">
|
||||
<MudText Color="@(context.Transcode && context.NormalizeVideo ? Color.Tertiary : Color.Inherit)">
|
||||
@context.VideoCodec
|
||||
</MudText>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Audio Codec">
|
||||
<MudText Color="@(context.Transcode && context.NormalizeAudioCodec ? Color.Tertiary : Color.Inherit)">
|
||||
<MudText Color="@(context.Transcode && context.NormalizeAudio ? Color.Tertiary : Color.Inherit)">
|
||||
@context.AudioCodec
|
||||
</MudText>
|
||||
</MudTd>
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
}
|
||||
</MudSelect>
|
||||
</MudElement>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudTextField Disabled="@(!_model.Transcode)" Label="Frame Rate" @bind-Value="_model.FrameRate" For="@(() => _model.FrameRate)" Adornment="Adornment.End" AdornmentText="fps"/>
|
||||
</MudElement>
|
||||
</MudItem>
|
||||
<MudItem>
|
||||
<MudText Typo="Typo.h6">Audio</MudText>
|
||||
@@ -80,13 +83,7 @@
|
||||
</MudItem>
|
||||
<MudItem>
|
||||
<MudText Typo="Typo.h6">Normalization</MudText>
|
||||
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Resolution" @bind-Checked="@_model.NormalizeResolution" For="@(() => _model.NormalizeResolution)"/>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Video Codec" @bind-Checked="@_model.NormalizeVideoCodec" For="@(() => _model.NormalizeVideoCodec)"/>
|
||||
</MudElement>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Audio Codec" @bind-Checked="@_model.NormalizeAudioCodec" For="@(() => _model.NormalizeAudioCodec)"/>
|
||||
</MudElement>
|
||||
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Video" @bind-Checked="@_model.NormalizeVideo" For="@(() => _model.NormalizeVideo)"/>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudCheckBox Disabled="@(!_model.Transcode)" Label="Normalize Audio" @bind-Checked="@_model.NormalizeAudio" For="@(() => _model.NormalizeAudio)"/>
|
||||
</MudElement>
|
||||
|
||||
@@ -70,11 +70,11 @@
|
||||
Logger.LogError("Unexpected error saving local library path: {Error}", error.Value);
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
Right: async vm =>
|
||||
Right: async _ =>
|
||||
{
|
||||
if (Locker.LockLibrary(_library.Id))
|
||||
{
|
||||
await Channel.WriteAsync(new ForceScanLocalLibrary(_library.Id));
|
||||
await Channel.WriteAsync(new ForceRescanLocalLibrary(_library.Id));
|
||||
NavigationManager.NavigateTo("/media/libraries");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -95,6 +95,10 @@
|
||||
{
|
||||
_query = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_query = null;
|
||||
}
|
||||
|
||||
return RefreshData();
|
||||
}
|
||||
|
||||
@@ -107,7 +107,8 @@ namespace ErsatzTV.Pages
|
||||
var request = new AddItemsToCollection(
|
||||
collection.Id,
|
||||
_selectedItems.OfType<MovieCardViewModel>().Map(m => m.MovieId).ToList(),
|
||||
_selectedItems.OfType<TelevisionShowCardViewModel>().Map(s => s.TelevisionShowId).ToList());
|
||||
_selectedItems.OfType<TelevisionShowCardViewModel>().Map(s => s.TelevisionShowId).ToList(),
|
||||
_selectedItems.OfType<MusicVideoCardViewModel>().Map(mv => mv.MusicVideoId).ToList());
|
||||
|
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||
addResult.Match(
|
||||
@@ -144,6 +145,7 @@ namespace ErsatzTV.Pages
|
||||
itemIds.AddRange(_selectedItems.OfType<TelevisionShowCardViewModel>().Map(s => s.TelevisionShowId));
|
||||
itemIds.AddRange(_selectedItems.OfType<TelevisionSeasonCardViewModel>().Map(s => s.TelevisionSeasonId));
|
||||
itemIds.AddRange(_selectedItems.OfType<TelevisionEpisodeCardViewModel>().Map(e => e.EpisodeId));
|
||||
itemIds.AddRange(_selectedItems.OfType<MusicVideoCardViewModel>().Map(mv => mv.MusicVideoId));
|
||||
|
||||
await Mediator.Send(
|
||||
new RemoveItemsFromCollection(collectionId)
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
@page "/media/music/videos"
|
||||
@page "/media/music/videos/page/{PageNumber:int}"
|
||||
@using LanguageExt.UnsafeValueAccess
|
||||
@using Microsoft.AspNetCore.WebUtilities
|
||||
@using Microsoft.Extensions.Primitives
|
||||
@using ErsatzTV.Application.MediaCards
|
||||
@using ErsatzTV.Application.MediaCollections
|
||||
@using ErsatzTV.Application.MediaCollections.Commands
|
||||
@using ErsatzTV.Application.Search.Queries
|
||||
@using Unit = LanguageExt.Unit
|
||||
@inherits MultiSelectBase<MusicVideoList>
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ChannelWriter<IBackgroundServiceRequest> Channel
|
||||
|
||||
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;">
|
||||
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6">
|
||||
@if (IsSelectMode())
|
||||
{
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText>
|
||||
<div style="margin-left: auto">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@(_ => AddSelectionToCollection())">
|
||||
Add To Collection
|
||||
</MudButton>
|
||||
<MudButton Class="ml-3"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
StartIcon="@Icons.Material.Filled.Check"
|
||||
OnClick="@(_ => ClearSelection())">
|
||||
Clear Selection
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Style="margin-bottom: auto; margin-top: auto; width: 33%">@_query</MudText>
|
||||
<div style="max-width: 300px; width: 33%;">
|
||||
<MudPaper Style="align-items: center; display: flex; justify-content: center;">
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft"
|
||||
OnClick="@PrevPage"
|
||||
Disabled="@(PageNumber <= 1)">
|
||||
</MudIconButton>
|
||||
<MudText Style="flex-grow: 1"
|
||||
Align="Align.Center">
|
||||
@Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count
|
||||
</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronRight"
|
||||
OnClick="@NextPage" Disabled="@(PageNumber * PageSize >= _data.Count)">
|
||||
</MudIconButton>
|
||||
</MudPaper>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</MudPaper>
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="margin-top: 64px">
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (MusicVideoCardViewModel card in _data.Cards.Where(m => !string.IsNullOrWhiteSpace(m.Title)).OrderBy(m => m.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link=""
|
||||
ArtworkKind="ArtworkKind.Thumbnail"
|
||||
AddToCollectionClicked="@AddToCollection"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
</MudContainer>
|
||||
@if (_data.PageMap.IsSome)
|
||||
{
|
||||
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()"
|
||||
BaseUri="/media/music/videos"
|
||||
Query="@_query"/>
|
||||
}
|
||||
|
||||
@code {
|
||||
private static int PageSize => 100;
|
||||
|
||||
[Parameter]
|
||||
public int PageNumber { get; set; }
|
||||
|
||||
private MusicVideoCardResultsViewModel _data;
|
||||
private string _query;
|
||||
|
||||
protected override Task OnParametersSetAsync()
|
||||
{
|
||||
if (PageNumber == 0)
|
||||
{
|
||||
PageNumber = 1;
|
||||
}
|
||||
|
||||
string query = new Uri(NavigationManager.Uri).Query;
|
||||
if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value))
|
||||
{
|
||||
_query = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_query = null;
|
||||
}
|
||||
|
||||
return RefreshData();
|
||||
}
|
||||
|
||||
protected override async Task RefreshData()
|
||||
{
|
||||
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:music_video" : $"type:music_video AND ({_query})";
|
||||
_data = await Mediator.Send(new QuerySearchIndexMusicVideos(searchQuery, PageNumber, PageSize));
|
||||
}
|
||||
|
||||
private void PrevPage()
|
||||
{
|
||||
var uri = $"/media/music/videos/page/{PageNumber - 1}";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
uri = QueryHelpers.AddQueryString(uri, "query", _query);
|
||||
}
|
||||
NavigationManager.NavigateTo(uri);
|
||||
}
|
||||
|
||||
private void NextPage()
|
||||
{
|
||||
var uri = $"/media/music/videos/page/{PageNumber + 1}";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
uri = QueryHelpers.AddQueryString(uri, "query", _query);
|
||||
}
|
||||
NavigationManager.NavigateTo(uri);
|
||||
}
|
||||
|
||||
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
|
||||
{
|
||||
List<MediaCardViewModel> GetSortedItems() => _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>();
|
||||
|
||||
SelectClicked(GetSortedItems, card, e);
|
||||
}
|
||||
|
||||
private async Task AddToCollection(MediaCardViewModel card)
|
||||
{
|
||||
if (card is MusicVideoCardViewModel musicVideo)
|
||||
{
|
||||
var parameters = new DialogParameters { { "EntityType", "music video" }, { "EntityName", musicVideo.Title } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = Dialog.Show<AddToCollectionDialog>("Add To Collection", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
|
||||
{
|
||||
var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId);
|
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||
addResult.Match(
|
||||
Left: error =>
|
||||
{
|
||||
Snackbar.Add($"Unexpected error adding music video to collection: {error.Value}");
|
||||
Logger.LogError("Unexpected error adding music video to collection: {Error}", error.Value);
|
||||
},
|
||||
Right: _ => Snackbar.Add($"Added {musicVideo.Title} to collection {collection.Name}", Severity.Success));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,6 +51,15 @@
|
||||
{
|
||||
<MudText Class="ml-4">0 Shows</MudText>
|
||||
}
|
||||
|
||||
if (_musicVideos.Count > 0)
|
||||
{
|
||||
<MudLink Class="ml-4" Href="@(NavigationManager.Uri.Split("#").Head() + "#music_videos")">@_musicVideos.Count Music Videos</MudLink>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Class="ml-4">0 Music Videos</MudText>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</MudPaper>
|
||||
@@ -108,12 +117,40 @@
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@if (_musicVideos.Count > 0)
|
||||
{
|
||||
<div class="mb-4" style="align-items: baseline; display: flex; flex-direction: row;">
|
||||
<MudText Typo="Typo.h4"
|
||||
Style="scroll-margin-top: 160px"
|
||||
UserAttributes="@(new Dictionary<string, object> { { "id", "music_videos" } })">
|
||||
Music Videos
|
||||
</MudText>
|
||||
@if (_shows.Count > 50)
|
||||
{
|
||||
<MudLink Href="@GetMusicVideosLink()" Class="ml-4">See All >></MudLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (MusicVideoCardViewModel card in _musicVideos.Cards.OrderBy(s => s.SortTitle))
|
||||
{
|
||||
<MediaCard Data="@card"
|
||||
Link=""
|
||||
AddToCollectionClicked="@AddToCollection"
|
||||
SelectClicked="@(e => SelectClicked(card, e))"
|
||||
IsSelected="@IsSelected(card)"
|
||||
IsSelectMode="@IsSelectMode()"/>
|
||||
}
|
||||
</MudContainer>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private string _query;
|
||||
private MovieCardResultsViewModel _movies;
|
||||
private TelevisionShowCardResultsViewModel _shows;
|
||||
private MusicVideoCardResultsViewModel _musicVideos;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -125,17 +162,16 @@
|
||||
|
||||
_movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50));
|
||||
_shows = await Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50));
|
||||
_musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50));
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e)
|
||||
{
|
||||
List<MediaCardViewModel> GetSortedItems()
|
||||
{
|
||||
return _movies.Cards.OrderBy(m => m.SortTitle)
|
||||
.Append<MediaCardViewModel>(_shows.Cards.OrderBy(s => s.SortTitle))
|
||||
.ToList();
|
||||
}
|
||||
List<MediaCardViewModel> GetSortedItems() => _movies.Cards.OrderBy(m => m.SortTitle)
|
||||
.Append<MediaCardViewModel>(_shows.Cards.OrderBy(s => s.SortTitle))
|
||||
.Append(_musicVideos.Cards.OrderBy(s => s.SortTitle))
|
||||
.ToList();
|
||||
|
||||
SelectClicked(GetSortedItems, card, e);
|
||||
}
|
||||
@@ -183,11 +219,32 @@
|
||||
Right: _ => Snackbar.Add($"Added {show.Title} to collection {collection.Name}", Severity.Success));
|
||||
}
|
||||
}
|
||||
|
||||
if (card is MusicVideoCardViewModel musicVideo)
|
||||
{
|
||||
var parameters = new DialogParameters { { "EntityType", "music video" }, { "EntityName", musicVideo.Title } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
|
||||
|
||||
IDialogReference dialog = Dialog.Show<AddToCollectionDialog>("Add To Collection", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
|
||||
{
|
||||
var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId);
|
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request);
|
||||
addResult.Match(
|
||||
Left: error =>
|
||||
{
|
||||
Snackbar.Add($"Unexpected error adding music video to collection: {error.Value}");
|
||||
Logger.LogError("Unexpected error adding music video to collection: {Error}", error.Value);
|
||||
},
|
||||
Right: _ => Snackbar.Add($"Added {musicVideo.Title} to collection {collection.Name}", Severity.Success));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetMoviesLink()
|
||||
{
|
||||
var uri = "/media/movies";
|
||||
var uri = "/media/movies/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
uri = QueryHelpers.AddQueryString(uri, "query", _query);
|
||||
@@ -197,7 +254,17 @@
|
||||
|
||||
private string GetShowsLink()
|
||||
{
|
||||
var uri = "/media/tv/shows";
|
||||
var uri = "/media/tv/shows/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
uri = QueryHelpers.AddQueryString(uri, "query", _query);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private string GetMusicVideosLink()
|
||||
{
|
||||
var uri = "/media/music/videos/page/1";
|
||||
if (!string.IsNullOrWhiteSpace(_query))
|
||||
{
|
||||
uri = QueryHelpers.AddQueryString(uri, "query", _query);
|
||||
|
||||
@@ -95,6 +95,10 @@
|
||||
{
|
||||
_query = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_query = null;
|
||||
}
|
||||
|
||||
return RefreshData();
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
<MudNavLink Href="/media/libraries">Libraries</MudNavLink>
|
||||
<MudNavLink Href="/media/tv/shows">TV Shows</MudNavLink>
|
||||
<MudNavLink Href="/media/movies">Movies</MudNavLink>
|
||||
<MudNavLink Href="/media/music/videos">Music Videos</MudNavLink>
|
||||
<MudNavLink Href="/media/collections">Collections</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavLink Href="/schedules">Schedules</MudNavLink>
|
||||
@@ -85,6 +86,7 @@
|
||||
TextSecondary = "rgba(255,255,255, 0.80)",
|
||||
TextDisabled = "rgba(255,255,255, 0.40)",
|
||||
ActionDisabled = "rgba(255,255,255, 0.40)",
|
||||
TableHover = "rgba(255,255,255, 0.10)",
|
||||
Info = "#00c0c0",
|
||||
Tertiary = "#00c000",
|
||||
White = Colors.Shades.White
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@inject IMediator Mediator
|
||||
|
||||
<div class="@((ContainerClass ?? "media-card-container mr-6") + " pb-3")" id="@($"item_{Data.MediaItemId}")">
|
||||
@if (!string.IsNullOrWhiteSpace(Link))
|
||||
@if (SelectClicked.HasDelegate || !string.IsNullOrWhiteSpace(Link))
|
||||
{
|
||||
<div class="@(IsSelected ? DeleteClicked.HasDelegate ? "media-card-selected-delete" : "media-card-selected" : "")"
|
||||
style="border-radius: 4px; position: relative;">
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="media-card-overlay" style="">
|
||||
<MudButton Link="@(IsSelectMode ? null : Link)"
|
||||
Style="height: 100%; width: 100%"
|
||||
OnClick="@(e => IsSelectMode ? SelectClicked.InvokeAsync(e) : Task.CompletedTask)">
|
||||
OnClick="@(e => IsSelectMode || string.IsNullOrWhiteSpace(Link) ? SelectClicked.InvokeAsync(e) : Task.CompletedTask)">
|
||||
</MudButton>
|
||||
@if (SelectClicked.HasDelegate)
|
||||
{
|
||||
|
||||
@@ -198,6 +198,7 @@ namespace ErsatzTV
|
||||
services.AddScoped<ITelevisionRepository, TelevisionRepository>();
|
||||
services.AddScoped<ISearchRepository, SearchRepository>();
|
||||
services.AddScoped<IMovieRepository, MovieRepository>();
|
||||
services.AddScoped<IMusicVideoRepository, MusicVideoRepository>();
|
||||
services.AddScoped<ILibraryRepository, LibraryRepository>();
|
||||
services.AddScoped<IMetadataRepository, MetadataRepository>();
|
||||
services.AddScoped<IFFmpegLocator, FFmpegLocator>();
|
||||
@@ -209,6 +210,7 @@ namespace ErsatzTV
|
||||
services.AddScoped<ILocalFileSystem, LocalFileSystem>();
|
||||
services.AddScoped<IMovieFolderScanner, MovieFolderScanner>();
|
||||
services.AddScoped<ITelevisionFolderScanner, TelevisionFolderScanner>();
|
||||
services.AddScoped<IMusicVideoFolderScanner, MusicVideoFolderScanner>();
|
||||
services.AddScoped<IPlexMovieLibraryScanner, PlexMovieLibraryScanner>();
|
||||
services.AddScoped<IPlexTelevisionLibraryScanner, PlexTelevisionLibraryScanner>();
|
||||
services.AddScoped<IPlexServerApiClient, PlexServerApiClient>();
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace ErsatzTV.Validators
|
||||
public FFmpegProfileEditViewModelValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty();
|
||||
RuleFor(x => x.ThreadCount).GreaterThan(0);
|
||||
RuleFor(x => x.ThreadCount).GreaterThanOrEqualTo(0);
|
||||
|
||||
When(
|
||||
x => x.Transcode,
|
||||
|
||||
@@ -22,9 +22,7 @@ namespace ErsatzTV.ViewModels
|
||||
Id = viewModel.Id;
|
||||
Name = viewModel.Name;
|
||||
NormalizeAudio = viewModel.NormalizeAudio;
|
||||
NormalizeAudioCodec = viewModel.NormalizeAudioCodec;
|
||||
NormalizeResolution = viewModel.NormalizeResolution;
|
||||
NormalizeVideoCodec = viewModel.NormalizeVideoCodec;
|
||||
NormalizeVideo = viewModel.NormalizeVideo;
|
||||
Resolution = viewModel.Resolution;
|
||||
ThreadCount = viewModel.ThreadCount;
|
||||
Transcode = viewModel.Transcode;
|
||||
@@ -32,6 +30,7 @@ namespace ErsatzTV.ViewModels
|
||||
VideoBitrate = viewModel.VideoBitrate;
|
||||
VideoBufferSize = viewModel.VideoBufferSize;
|
||||
VideoCodec = viewModel.VideoCodec;
|
||||
FrameRate = viewModel.FrameRate;
|
||||
}
|
||||
|
||||
public int AudioBitrate { get; set; }
|
||||
@@ -43,9 +42,7 @@ namespace ErsatzTV.ViewModels
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool NormalizeAudio { get; set; }
|
||||
public bool NormalizeAudioCodec { get; set; }
|
||||
public bool NormalizeResolution { get; set; }
|
||||
public bool NormalizeVideoCodec { get; set; }
|
||||
public bool NormalizeVideo { get; set; }
|
||||
public ResolutionViewModel Resolution { get; set; }
|
||||
public int ThreadCount { get; set; }
|
||||
public bool Transcode { get; set; }
|
||||
@@ -53,6 +50,7 @@ namespace ErsatzTV.ViewModels
|
||||
public int VideoBitrate { get; set; }
|
||||
public int VideoBufferSize { get; set; }
|
||||
public string VideoCodec { get; set; }
|
||||
public string FrameRate { get; set; }
|
||||
|
||||
public CreateFFmpegProfile ToCreate() =>
|
||||
new(
|
||||
@@ -61,19 +59,18 @@ namespace ErsatzTV.ViewModels
|
||||
Transcode,
|
||||
HardwareAcceleration,
|
||||
Resolution.Id,
|
||||
NormalizeResolution,
|
||||
NormalizeVideo,
|
||||
VideoCodec,
|
||||
NormalizeVideoCodec,
|
||||
VideoBitrate,
|
||||
VideoBufferSize,
|
||||
AudioCodec,
|
||||
NormalizeAudioCodec,
|
||||
AudioBitrate,
|
||||
AudioBufferSize,
|
||||
AudioVolume,
|
||||
AudioChannels,
|
||||
AudioSampleRate,
|
||||
NormalizeAudio
|
||||
NormalizeAudio,
|
||||
FrameRate
|
||||
);
|
||||
|
||||
public UpdateFFmpegProfile ToUpdate() =>
|
||||
@@ -84,19 +81,18 @@ namespace ErsatzTV.ViewModels
|
||||
Transcode,
|
||||
HardwareAcceleration,
|
||||
Resolution.Id,
|
||||
NormalizeResolution,
|
||||
NormalizeVideo,
|
||||
VideoCodec,
|
||||
NormalizeVideoCodec,
|
||||
VideoBitrate,
|
||||
VideoBufferSize,
|
||||
AudioCodec,
|
||||
NormalizeAudioCodec,
|
||||
AudioBitrate,
|
||||
AudioBufferSize,
|
||||
AudioVolume,
|
||||
AudioChannels,
|
||||
AudioSampleRate,
|
||||
NormalizeAudio
|
||||
NormalizeAudio,
|
||||
FrameRate
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user