Compare commits
41 Commits
v0.0.20-pr
...
v0.0.26-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7ebb32a1d | ||
|
|
9ea4459988 | ||
|
|
745b03af73 | ||
|
|
a62c4ecfcf | ||
|
|
c48f0a7d51 | ||
|
|
f2c105174b | ||
|
|
076a88230e | ||
|
|
f06a04ed0e | ||
|
|
07d690a31f | ||
|
|
001453714a | ||
|
|
d303bc0158 | ||
|
|
51b671dec7 | ||
|
|
a5e1cc7c3d | ||
|
|
9ba6686c44 | ||
|
|
104d4a0cbd | ||
|
|
22c4fe2a27 | ||
|
|
7e0bdfdb40 | ||
|
|
6bdaca0222 | ||
|
|
67aa3a5a46 | ||
|
|
a0332e242c | ||
|
|
cd74859d28 | ||
|
|
470fba275b | ||
|
|
e42b000b7f | ||
|
|
489f8d92ff | ||
|
|
527d3c6e4b | ||
|
|
c33c037188 | ||
|
|
4c70d61d48 | ||
|
|
00fdc272e9 | ||
|
|
f04c18c810 | ||
|
|
eca58dbe7f | ||
|
|
cf9479d2a9 | ||
|
|
b6331331b0 | ||
|
|
ed365cfa43 | ||
|
|
b3a1e71570 | ||
|
|
454343d14f | ||
|
|
c0a6677861 | ||
|
|
2efcbca2da | ||
|
|
f96efa9b2f | ||
|
|
f46041305c | ||
|
|
493a496b91 | ||
|
|
739d074bc6 |
@@ -8,5 +8,6 @@ namespace ErsatzTV.Application.Channels
|
||||
string Name,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode);
|
||||
}
|
||||
|
||||
@@ -11,5 +11,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
string Number,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -9,6 +11,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Channels.Mapper;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Channels.Commands
|
||||
{
|
||||
@@ -36,9 +39,10 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
_channelRepository.Add(c).Map(ProjectToViewModel);
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) =>
|
||||
(ValidateName(request), await ValidateNumber(request), await FFmpegProfileMustExist(request))
|
||||
(ValidateName(request), await ValidateNumber(request), await FFmpegProfileMustExist(request),
|
||||
ValidatePreferredLanguage(request))
|
||||
.Apply(
|
||||
(name, number, ffmpegProfileId) =>
|
||||
(name, number, ffmpegProfileId, preferredLanguageCode) =>
|
||||
{
|
||||
var artwork = new List<Artwork>();
|
||||
if (!string.IsNullOrWhiteSpace(request.Logo))
|
||||
@@ -59,7 +63,8 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
Number = number,
|
||||
FFmpegProfileId = ffmpegProfileId,
|
||||
StreamingMode = request.StreamingMode,
|
||||
Artwork = artwork
|
||||
Artwork = artwork,
|
||||
PreferredLanguageCode = preferredLanguageCode
|
||||
};
|
||||
});
|
||||
|
||||
@@ -67,6 +72,13 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
createChannel.NotEmpty(c => c.Name)
|
||||
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
private Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) =>
|
||||
Optional(createChannel.PreferredLanguageCode ?? string.Empty)
|
||||
.Filter(
|
||||
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToValidation<BaseError>("Preferred language code is invalid");
|
||||
|
||||
private async Task<Validation<BaseError, string>> ValidateNumber(CreateChannel createChannel)
|
||||
{
|
||||
Option<Channel> maybeExistingChannel = await _channelRepository.GetByNumber(createChannel.Number);
|
||||
|
||||
@@ -12,5 +12,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
string Number,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
@@ -32,6 +33,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
c.Name = update.Name;
|
||||
c.Number = update.Number;
|
||||
c.FFmpegProfileId = update.FFmpegProfileId;
|
||||
c.PreferredLanguageCode = update.PreferredLanguageCode;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(update.Logo))
|
||||
{
|
||||
@@ -65,8 +67,9 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> Validate(UpdateChannel request) =>
|
||||
(await ChannelMustExist(request), ValidateName(request), await ValidateNumber(request))
|
||||
.Apply((channelToUpdate, _, _) => channelToUpdate);
|
||||
(await ChannelMustExist(request), ValidateName(request), await ValidateNumber(request),
|
||||
ValidatePreferredLanguage(request))
|
||||
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
|
||||
|
||||
private Task<Validation<BaseError, Channel>> ChannelMustExist(UpdateChannel updateChannel) =>
|
||||
_channelRepository.Get(updateChannel.ChannelId)
|
||||
@@ -92,5 +95,12 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
|
||||
return BaseError.New("Channel number must be unique");
|
||||
}
|
||||
|
||||
private Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
|
||||
Optional(updateChannel.PreferredLanguageCode ?? string.Empty)
|
||||
.Filter(
|
||||
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToValidation<BaseError>("Preferred language code is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace ErsatzTV.Application.Channels
|
||||
channel.Name,
|
||||
channel.FFmpegProfileId,
|
||||
GetLogo(channel),
|
||||
channel.PreferredLanguageCode,
|
||||
channel.StreamingMode);
|
||||
|
||||
private static string GetLogo(Channel channel) =>
|
||||
|
||||
@@ -134,6 +134,23 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
|
||||
}
|
||||
|
||||
await _configElementRepository.Get(ConfigElementKey.FFmpegPreferredLanguageCode).Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = request.Settings.PreferredLanguageCode;
|
||||
_configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement
|
||||
{
|
||||
Key = ConfigElementKey.FFmpegPreferredLanguageCode.Key,
|
||||
Value = request.Settings.PreferredLanguageCode
|
||||
};
|
||||
_configElementRepository.Add(ce);
|
||||
});
|
||||
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
public string FFmpegPath { get; set; }
|
||||
public string FFprobePath { get; set; }
|
||||
public int DefaultFFmpegProfileId { get; set; }
|
||||
public string PreferredLanguageCode { get; set; }
|
||||
public bool SaveReports { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,16 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId);
|
||||
Option<bool> saveReports =
|
||||
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
|
||||
Option<string> preferredLanguageCode =
|
||||
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
|
||||
|
||||
return new FFmpegSettingsViewModel
|
||||
{
|
||||
FFmpegPath = ffmpegPath.IfNone(string.Empty),
|
||||
FFprobePath = ffprobePath.IfNone(string.Empty),
|
||||
DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0),
|
||||
SaveReports = saveReports.IfNone(false)
|
||||
SaveReports = saveReports.IfNone(false),
|
||||
PreferredLanguageCode = preferredLanguageCode.IfNone("eng")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Libraries.Commands
|
||||
@@ -11,9 +13,13 @@ namespace ErsatzTV.Application.Libraries.Commands
|
||||
DeleteLocalLibraryPathHandler : MediatR.IRequestHandler<DeleteLocalLibraryPath, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public DeleteLocalLibraryPathHandler(ILibraryRepository libraryRepository) =>
|
||||
public DeleteLocalLibraryPathHandler(ILibraryRepository libraryRepository, ISearchIndex searchIndex)
|
||||
{
|
||||
_libraryRepository = libraryRepository;
|
||||
_searchIndex = searchIndex;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
DeleteLocalLibraryPath request,
|
||||
@@ -22,8 +28,13 @@ namespace ErsatzTV.Application.Libraries.Commands
|
||||
.MapT(DoDeletion)
|
||||
.Bind(t => t.ToEitherAsync());
|
||||
|
||||
private Task<Unit> DoDeletion(LibraryPath libraryPath) =>
|
||||
_libraryRepository.DeleteLocalPath(libraryPath.Id).ToUnit();
|
||||
private async Task<Unit> DoDeletion(LibraryPath libraryPath)
|
||||
{
|
||||
List<int> ids = await _libraryRepository.GetMediaIdsByLocalPath(libraryPath.Id);
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
await _libraryRepository.DeleteLocalPath(libraryPath.Id);
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, LibraryPath>> MediaSourceMustExist(DeleteLocalLibraryPath request) =>
|
||||
(await _libraryRepository.GetPath(request.LocalLibraryPathId))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards);
|
||||
public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards, Option<SearchPageMap> PageMap);
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards.Queries
|
||||
{
|
||||
public record GetMovieCards(int PageNumber, int PageSize) : IRequest<MovieCardResultsViewModel>;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards.Queries
|
||||
{
|
||||
public class
|
||||
GetMovieCardsHandler : IRequestHandler<GetMovieCards, MovieCardResultsViewModel>
|
||||
{
|
||||
private readonly IMovieRepository _movieRepository;
|
||||
|
||||
public GetMovieCardsHandler(IMovieRepository movieRepository) => _movieRepository = movieRepository;
|
||||
|
||||
public async Task<MovieCardResultsViewModel> Handle(GetMovieCards request, CancellationToken cancellationToken)
|
||||
{
|
||||
int count = await _movieRepository.GetMovieCount();
|
||||
|
||||
List<MovieCardViewModel> results = await _movieRepository
|
||||
.GetPagedMovies(request.PageNumber, request.PageSize)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new MovieCardResultsViewModel(count, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards.Queries
|
||||
{
|
||||
public record GetSearchCards(string Query) : IRequest<Either<BaseError, SearchCardResultsViewModel>>;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards.Queries
|
||||
{
|
||||
public class GetSearchCardsHandler : IRequestHandler<GetSearchCards, Either<BaseError, SearchCardResultsViewModel>>
|
||||
{
|
||||
private readonly ISearchRepository _searchRepository;
|
||||
|
||||
public GetSearchCardsHandler(ISearchRepository searchRepository) => _searchRepository = searchRepository;
|
||||
|
||||
public Task<Either<BaseError, SearchCardResultsViewModel>> Handle(
|
||||
GetSearchCards request,
|
||||
CancellationToken cancellationToken) =>
|
||||
request.Query.Split(":").Head() switch
|
||||
{
|
||||
"genre" => GenreSearch(request.Query.Replace("genre:", string.Empty)),
|
||||
"tag" => TagSearch(request.Query.Replace("tag:", string.Empty)),
|
||||
_ => TitleSearch(request.Query)
|
||||
};
|
||||
|
||||
private Task<Either<BaseError, SearchCardResultsViewModel>> TitleSearch(string query) =>
|
||||
Try(_searchRepository.SearchMediaItemsByTitle(query)).Sequence()
|
||||
.Map(ProjectToSearchResults)
|
||||
.Map(t => t.ToEither(ex => BaseError.New($"Failed to search: {ex.Message}")));
|
||||
|
||||
private Task<Either<BaseError, SearchCardResultsViewModel>> GenreSearch(string query) =>
|
||||
Try(_searchRepository.SearchMediaItemsByGenre(query)).Sequence()
|
||||
.Map(ProjectToSearchResults)
|
||||
.Map(t => t.ToEither(ex => BaseError.New($"Failed to search: {ex.Message}")));
|
||||
|
||||
private Task<Either<BaseError, SearchCardResultsViewModel>> TagSearch(string query) =>
|
||||
Try(_searchRepository.SearchMediaItemsByTag(query)).Sequence()
|
||||
.Map(ProjectToSearchResults)
|
||||
.Map(t => t.ToEither(ex => BaseError.New($"Failed to search: {ex.Message}")));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards.Queries
|
||||
{
|
||||
public record GetTelevisionShowCards(int PageNumber, int PageSize) : IRequest<TelevisionShowCardResultsViewModel>;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards.Queries
|
||||
{
|
||||
public class
|
||||
GetTelevisionShowCardsHandler : IRequestHandler<GetTelevisionShowCards, TelevisionShowCardResultsViewModel>
|
||||
{
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
|
||||
public GetTelevisionShowCardsHandler(ITelevisionRepository televisionRepository) =>
|
||||
_televisionRepository = televisionRepository;
|
||||
|
||||
public async Task<TelevisionShowCardResultsViewModel> Handle(
|
||||
GetTelevisionShowCards request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
int count = await _televisionRepository.GetShowCount();
|
||||
|
||||
List<TelevisionShowCardViewModel> results = await _televisionRepository
|
||||
.GetPagedShows(request.PageNumber, request.PageSize)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new TelevisionShowCardResultsViewModel(count, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record TelevisionShowCardResultsViewModel(int Count, List<TelevisionShowCardViewModel> Cards);
|
||||
public record TelevisionShowCardResultsViewModel(
|
||||
int Count,
|
||||
List<TelevisionShowCardViewModel> Cards,
|
||||
Option<SearchPageMap> PageMap);
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public record SearchAllMediaItems(string SearchString) : IRequest<List<MediaItemSearchResultViewModel>>;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class SearchAllMediaItemsHandler : IRequestHandler<SearchAllMediaItems, List<MediaItemSearchResultViewModel>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public SearchAllMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public Task<List<MediaItemSearchResultViewModel>>
|
||||
Handle(SearchAllMediaItems request, CancellationToken cancellationToken) =>
|
||||
_mediaItemRepository.Search(request.SearchString).Map(list => list.Map(ProjectToSearchViewModel).ToList());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -61,21 +62,30 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
|
||||
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
foreach (LibraryPath libraryPath in localLibrary.Paths)
|
||||
{
|
||||
switch (localLibrary.MediaKind)
|
||||
{
|
||||
case LibraryMediaKind.Movies:
|
||||
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath);
|
||||
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath, lastScan);
|
||||
break;
|
||||
case LibraryMediaKind.Shows:
|
||||
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath);
|
||||
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath, lastScan);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
localLibrary.LastScan = DateTime.UtcNow;
|
||||
await _libraryRepository.UpdateLastScan(localLibrary);
|
||||
|
||||
sw.Stop();
|
||||
_logger.LogDebug(
|
||||
"Scan of library {Name} completed in {Duration}",
|
||||
localLibrary.Name,
|
||||
TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -16,7 +16,8 @@ namespace ErsatzTV.Application.Movies
|
||||
Artwork(metadata, ArtworkKind.Poster),
|
||||
Artwork(metadata, ArtworkKind.FanArt),
|
||||
metadata.Genres.Map(g => g.Name).ToList(),
|
||||
metadata.Tags.Map(t => t.Name).ToList());
|
||||
metadata.Tags.Map(t => t.Name).ToList(),
|
||||
metadata.Studios.Map(s => s.Name).ToList());
|
||||
}
|
||||
|
||||
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
|
||||
|
||||
@@ -9,5 +9,6 @@ namespace ErsatzTV.Application.Movies
|
||||
string Poster,
|
||||
string FanArt,
|
||||
List<string> Genres,
|
||||
List<string> Tags);
|
||||
List<string> Tags,
|
||||
List<string> Studios);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands
|
||||
@@ -13,20 +15,24 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IPlexSecretStore _plexSecretStore;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public SignOutOfPlexHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IPlexSecretStore plexSecretStore,
|
||||
IEntityLocker entityLocker)
|
||||
IEntityLocker entityLocker,
|
||||
ISearchIndex searchIndex)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexSecretStore = plexSecretStore;
|
||||
_entityLocker = entityLocker;
|
||||
_searchIndex = searchIndex;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(SignOutOfPlex request, CancellationToken cancellationToken)
|
||||
{
|
||||
await _mediaSourceRepository.DeleteAllPlex();
|
||||
List<int> ids = await _mediaSourceRepository.DeleteAllPlex();
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
await _plexSecretStore.DeleteAll();
|
||||
_entityLocker.UnlockPlex();
|
||||
|
||||
|
||||
@@ -78,10 +78,10 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
var existing = connectionParameters.PlexMediaSource.Libraries.OfType<PlexLibrary>().ToList();
|
||||
var toAdd = libraries.Filter(library => existing.All(l => l.Key != library.Key)).ToList();
|
||||
var toRemove = existing.Filter(library => libraries.All(l => l.Key != library.Key)).ToList();
|
||||
connectionParameters.PlexMediaSource.Libraries.AddRange(toAdd);
|
||||
toRemove.ForEach(c => connectionParameters.PlexMediaSource.Libraries.Remove(c));
|
||||
|
||||
return _mediaSourceRepository.Update(connectionParameters.PlexMediaSource);
|
||||
return _mediaSourceRepository.UpdateLibraries(
|
||||
connectionParameters.PlexMediaSource.Id,
|
||||
toAdd,
|
||||
toRemove);
|
||||
},
|
||||
error =>
|
||||
{
|
||||
|
||||
@@ -64,25 +64,24 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
await maybeExisting.Match(
|
||||
existing =>
|
||||
{
|
||||
existing.Platform = server.Platform;
|
||||
existing.PlatformVersion = server.PlatformVersion;
|
||||
existing.ProductVersion = server.ProductVersion;
|
||||
existing.ServerName = server.ServerName;
|
||||
MergeConnections(existing.Connections, server.Connections);
|
||||
if (existing.Connections.Any() && existing.Connections.All(c => !c.IsActive))
|
||||
{
|
||||
existing.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
return _mediaSourceRepository.Update(existing);
|
||||
var toAdd = server.Connections
|
||||
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
var toRemove = existing.Connections
|
||||
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
return _mediaSourceRepository.Update(existing, toAdd, toRemove);
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
await _mediaSourceRepository.Add(server);
|
||||
if (server.Connections.Any())
|
||||
{
|
||||
server.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
await _mediaSourceRepository.Update(server);
|
||||
await _mediaSourceRepository.Add(server);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands
|
||||
@@ -13,16 +14,23 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public UpdatePlexLibraryPreferencesHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
public UpdatePlexLibraryPreferencesHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
ISearchIndex searchIndex)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_searchIndex = searchIndex;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
UpdatePlexLibraryPreferences request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
|
||||
await _mediaSourceRepository.DisablePlexLibrarySync(toDisable);
|
||||
List<int> ids = await _mediaSourceRepository.DisablePlexLibrarySync(toDisable);
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
|
||||
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
|
||||
await _mediaSourceRepository.EnablePlexLibrarySync(toEnable);
|
||||
|
||||
@@ -6,7 +6,6 @@ using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands
|
||||
{
|
||||
@@ -35,20 +34,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
var toRemove = plexMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
|
||||
var toUpdate = incoming.Except(toAdd).ToList();
|
||||
|
||||
plexMediaSource.PathReplacements.AddRange(toAdd);
|
||||
toRemove.ForEach(pr => plexMediaSource.PathReplacements.Remove(pr));
|
||||
foreach (PlexPathReplacement pathReplacement in toUpdate)
|
||||
{
|
||||
Optional(plexMediaSource.PathReplacements.SingleOrDefault(pr => pr.Id == pathReplacement.Id))
|
||||
.IfSome(
|
||||
pr =>
|
||||
{
|
||||
pr.PlexPath = pathReplacement.PlexPath;
|
||||
pr.LocalPath = pathReplacement.LocalPath;
|
||||
});
|
||||
}
|
||||
|
||||
return _mediaSourceRepository.Update(plexMediaSource).ToUnit();
|
||||
return _mediaSourceRepository.UpdatePathReplacements(plexMediaSource.Id, toAdd, toUpdate, toRemove);
|
||||
}
|
||||
|
||||
private static PlexPathReplacement Project(PlexPathReplacementItem vm) =>
|
||||
|
||||
@@ -16,5 +16,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
int? MediaItemId,
|
||||
int? MultipleCount,
|
||||
TimeSpan? PlayoutDuration,
|
||||
bool? OfflineTail) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
|
||||
bool? OfflineTail,
|
||||
string CustomTitle) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
int? MultipleCount { get; }
|
||||
TimeSpan? PlayoutDuration { get; }
|
||||
bool? OfflineTail { get; }
|
||||
string CustomTitle { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
StartTime = item.StartTime,
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId
|
||||
MediaItemId = item.MediaItemId,
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
PlayoutMode.One => new ProgramScheduleItemOne
|
||||
{
|
||||
@@ -109,7 +110,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
StartTime = item.StartTime,
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId
|
||||
MediaItemId = item.MediaItemId,
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
PlayoutMode.Multiple => new ProgramScheduleItemMultiple
|
||||
{
|
||||
@@ -119,7 +121,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId,
|
||||
Count = item.MultipleCount.GetValueOrDefault()
|
||||
Count = item.MultipleCount.GetValueOrDefault(),
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
PlayoutMode.Duration => new ProgramScheduleItemDuration
|
||||
{
|
||||
@@ -130,7 +133,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId,
|
||||
PlayoutDuration = item.PlayoutDuration.GetValueOrDefault(),
|
||||
OfflineTail = item.OfflineTail.GetValueOrDefault()
|
||||
OfflineTail = item.OfflineTail.GetValueOrDefault(),
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
|
||||
};
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
int? MediaItemId,
|
||||
int? MultipleCount,
|
||||
TimeSpan? PlayoutDuration,
|
||||
bool? OfflineTail) : IProgramScheduleItemRequest;
|
||||
bool? OfflineTail,
|
||||
string CustomTitle) : IProgramScheduleItemRequest;
|
||||
|
||||
public record ReplaceProgramScheduleItems
|
||||
(int ProgramScheduleId, List<ReplaceProgramScheduleItem> Items) : IRequest<
|
||||
|
||||
@@ -28,7 +28,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
_ => null
|
||||
},
|
||||
duration.PlayoutDuration,
|
||||
duration.OfflineTail),
|
||||
duration.OfflineTail,
|
||||
duration.CustomTitle),
|
||||
ProgramScheduleItemFlood flood =>
|
||||
new ProgramScheduleItemFloodViewModel(
|
||||
flood.Id,
|
||||
@@ -44,7 +45,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
Show show => MediaItems.Mapper.ProjectToViewModel(show),
|
||||
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
||||
_ => null
|
||||
}),
|
||||
},
|
||||
flood.CustomTitle),
|
||||
ProgramScheduleItemMultiple multiple =>
|
||||
new ProgramScheduleItemMultipleViewModel(
|
||||
multiple.Id,
|
||||
@@ -61,7 +63,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
||||
_ => null
|
||||
},
|
||||
multiple.Count),
|
||||
multiple.Count,
|
||||
multiple.CustomTitle),
|
||||
ProgramScheduleItemOne one =>
|
||||
new ProgramScheduleItemOneViewModel(
|
||||
one.Id,
|
||||
@@ -77,7 +80,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
Show show => MediaItems.Mapper.ProjectToViewModel(show),
|
||||
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
||||
_ => null
|
||||
}),
|
||||
},
|
||||
one.CustomTitle),
|
||||
_ => throw new NotSupportedException(
|
||||
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
|
||||
};
|
||||
|
||||
@@ -16,7 +16,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
TimeSpan playoutDuration,
|
||||
bool offlineTail) : base(
|
||||
bool offlineTail,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -24,7 +25,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.Duration,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem)
|
||||
mediaItem,
|
||||
customTitle)
|
||||
{
|
||||
PlayoutDuration = playoutDuration;
|
||||
OfflineTail = offlineTail;
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
TimeSpan? startTime,
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem) : base(
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -22,7 +23,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.Flood,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem)
|
||||
mediaItem,
|
||||
customTitle)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
int count) : base(
|
||||
int count,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -23,7 +24,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.Multiple,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem) =>
|
||||
mediaItem,
|
||||
customTitle) =>
|
||||
Count = count;
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
TimeSpan? startTime,
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem) : base(
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -22,7 +23,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.One,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem)
|
||||
mediaItem,
|
||||
customTitle)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode PlayoutMode,
|
||||
ProgramScheduleItemCollectionType CollectionType,
|
||||
MediaCollectionViewModel Collection,
|
||||
NamedMediaItemViewModel MediaItem)
|
||||
NamedMediaItemViewModel MediaItem,
|
||||
string CustomTitle)
|
||||
{
|
||||
public string Name => CollectionType switch
|
||||
{
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Commands
|
||||
{
|
||||
public record RebuildSearchIndex : MediatR.IRequest<Unit>, IBackgroundServiceRequest;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Commands
|
||||
{
|
||||
public class RebuildSearchIndexHandler : MediatR.IRequestHandler<RebuildSearchIndex, Unit>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly ILogger<RebuildSearchIndexHandler> _logger;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
private readonly ISearchRepository _searchRepository;
|
||||
|
||||
public RebuildSearchIndexHandler(
|
||||
ISearchIndex searchIndex,
|
||||
ISearchRepository searchRepository,
|
||||
IConfigElementRepository configElementRepository,
|
||||
ILogger<RebuildSearchIndexHandler> logger)
|
||||
{
|
||||
_searchIndex = searchIndex;
|
||||
_logger = logger;
|
||||
_searchRepository = searchRepository;
|
||||
_configElementRepository = configElementRepository;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(RebuildSearchIndex request, CancellationToken cancellationToken)
|
||||
{
|
||||
bool indexFolderExists = Directory.Exists(FileSystemLayout.SearchIndexFolder);
|
||||
|
||||
if (!indexFolderExists ||
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.SearchIndexVersion) <
|
||||
_searchIndex.Version)
|
||||
{
|
||||
_logger.LogDebug("Migrating search index to version {Version}", _searchIndex.Version);
|
||||
|
||||
List<int> itemIds = await _searchRepository.GetItemIdsToIndex();
|
||||
await _searchIndex.Rebuild(itemIds);
|
||||
|
||||
Option<ConfigElement> maybeVersion =
|
||||
await _configElementRepository.Get(ConfigElementKey.SearchIndexVersion);
|
||||
await maybeVersion.Match(
|
||||
version =>
|
||||
{
|
||||
version.Value = _searchIndex.Version.ToString();
|
||||
return _configElementRepository.Update(version);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var configElement = new ConfigElement
|
||||
{
|
||||
Key = ConfigElementKey.SearchIndexVersion.Key,
|
||||
Value = _searchIndex.Version.ToString()
|
||||
};
|
||||
return _configElementRepository.Add(configElement);
|
||||
});
|
||||
|
||||
_logger.LogDebug("Done migrating search index");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Search index is already version {Version}", _searchIndex.Version);
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
ErsatzTV.Application/Search/Queries/QuerySearchIndex.cs
Normal file
7
ErsatzTV.Application/Search/Queries/QuerySearchIndex.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using ErsatzTV.Core.Search;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public record QuerySearchIndex(string Query) : IRequest<SearchResult>;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public class QuerySearchIndexHandler : IRequestHandler<QuerySearchIndex, SearchResult>
|
||||
{
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public QuerySearchIndexHandler(ISearchIndex searchIndex) => _searchIndex = searchIndex;
|
||||
|
||||
public Task<SearchResult> Handle(QuerySearchIndex request, CancellationToken cancellationToken) =>
|
||||
_searchIndex.Search(request.Query, 0, 100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public record QuerySearchIndexMovies
|
||||
(string Query, int PageNumber, int PageSize) : IRequest<MovieCardResultsViewModel>;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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 QuerySearchIndexMoviesHandler : IRequestHandler<QuerySearchIndexMovies, MovieCardResultsViewModel>
|
||||
{
|
||||
private readonly IMovieRepository _movieRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public QuerySearchIndexMoviesHandler(ISearchIndex searchIndex, IMovieRepository movieRepository)
|
||||
{
|
||||
_searchIndex = searchIndex;
|
||||
_movieRepository = movieRepository;
|
||||
}
|
||||
|
||||
public async Task<MovieCardResultsViewModel> Handle(
|
||||
QuerySearchIndexMovies request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
request.Query,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
List<MovieCardViewModel> items = await _movieRepository
|
||||
.GetMoviesForCards(searchResult.Items.Map(i => i.Id).ToList())
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new MovieCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public record QuerySearchIndexShows
|
||||
(string Query, int PageNumber, int PageSize) : IRequest<TelevisionShowCardResultsViewModel>;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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
|
||||
QuerySearchIndexShowsHandler : IRequestHandler<QuerySearchIndexShows, TelevisionShowCardResultsViewModel>
|
||||
{
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
|
||||
public QuerySearchIndexShowsHandler(ISearchIndex searchIndex, ITelevisionRepository televisionRepository)
|
||||
{
|
||||
_searchIndex = searchIndex;
|
||||
_televisionRepository = televisionRepository;
|
||||
}
|
||||
|
||||
public async Task<TelevisionShowCardResultsViewModel> Handle(
|
||||
QuerySearchIndexShows request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
request.Query,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
List<TelevisionShowCardViewModel> items = await _televisionRepository
|
||||
.GetShowsForCards(searchResult.Items.Map(i => i.Id).ToList())
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new TelevisionShowCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
ErsatzTV.Application/Search/SearchResultViewModel.cs
Normal file
10
ErsatzTV.Application/Search/SearchResultViewModel.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Application.Search
|
||||
{
|
||||
public class SearchResultViewModel<T>
|
||||
{
|
||||
public int TotalCount { get; set; }
|
||||
public List<T> Items { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming.Queries
|
||||
@@ -19,29 +16,26 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
public class
|
||||
GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly FFmpegProcessService _ffmpegProcessService;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IPlayoutRepository _playoutRepository;
|
||||
private readonly IPlexPathReplacementService _plexPathReplacementService;
|
||||
|
||||
public GetPlayoutItemProcessByChannelNumberHandler(
|
||||
IChannelRepository channelRepository,
|
||||
IConfigElementRepository configElementRepository,
|
||||
IPlayoutRepository playoutRepository,
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
FFmpegProcessService ffmpegProcessService,
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger)
|
||||
IPlexPathReplacementService plexPathReplacementService)
|
||||
: base(channelRepository, configElementRepository)
|
||||
{
|
||||
_configElementRepository = configElementRepository;
|
||||
_playoutRepository = playoutRepository;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_ffmpegProcessService = ffmpegProcessService;
|
||||
_localFileSystem = localFileSystem;
|
||||
_logger = logger;
|
||||
_plexPathReplacementService = plexPathReplacementService;
|
||||
}
|
||||
|
||||
protected override async Task<Either<BaseError, Process>> GetProcess(
|
||||
@@ -69,7 +63,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
.Map(result => result.IfNone(false));
|
||||
|
||||
return Right<BaseError, Process>(
|
||||
_ffmpegProcessService.ForPlayoutItem(
|
||||
await _ffmpegProcessService.ForPlayoutItem(
|
||||
ffmpegPath,
|
||||
saveReports,
|
||||
channel,
|
||||
@@ -166,33 +160,16 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
string path = file.Path;
|
||||
return playoutItem.MediaItem switch
|
||||
{
|
||||
PlexMovie plexMovie => await GetReplacementPlexPath(plexMovie.LibraryPathId, path),
|
||||
PlexEpisode plexEpisode => await GetReplacementPlexPath(plexEpisode.LibraryPathId, path),
|
||||
PlexMovie plexMovie => await _plexPathReplacementService.GetReplacementPlexPath(
|
||||
plexMovie.LibraryPathId,
|
||||
path),
|
||||
PlexEpisode plexEpisode => await _plexPathReplacementService.GetReplacementPlexPath(
|
||||
plexEpisode.LibraryPathId,
|
||||
path),
|
||||
_ => path
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string> GetReplacementPlexPath(int libraryPathId, string path)
|
||||
{
|
||||
List<PlexPathReplacement> replacements =
|
||||
await _mediaSourceRepository.GetPlexPathReplacementsByLibraryId(libraryPathId);
|
||||
// TODO: this might barf mixing platforms (i.e. plex on linux, etv on windows)
|
||||
Option<PlexPathReplacement> maybeReplacement = replacements
|
||||
.SingleOrDefault(r => path.StartsWith(r.PlexPath + Path.DirectorySeparatorChar));
|
||||
return maybeReplacement.Match(
|
||||
replacement =>
|
||||
{
|
||||
string finalPath = path.Replace(replacement.PlexPath, replacement.LocalPath);
|
||||
_logger.LogInformation(
|
||||
"Replacing plex path {PlexPath} with {LocalPath} resulting in {FinalPath}",
|
||||
replacement.PlexPath,
|
||||
replacement.LocalPath,
|
||||
finalPath);
|
||||
return finalPath;
|
||||
},
|
||||
() => path);
|
||||
}
|
||||
|
||||
private record PlayoutItemWithPath(PlayoutItem PlayoutItem, string Path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace ErsatzTV.Application.Television
|
||||
show.ShowMetadata.HeadOrNone().Map(GetPoster).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(GetFanArt).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone(new List<string>()),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List<string>()));
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List<string>()),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList())
|
||||
.IfNone(new List<string>()));
|
||||
|
||||
internal static TelevisionSeasonViewModel ProjectToViewModel(Season season) =>
|
||||
new(
|
||||
|
||||
@@ -10,5 +10,6 @@ namespace ErsatzTV.Application.Television
|
||||
string Poster,
|
||||
string FanArt,
|
||||
List<string> Genres,
|
||||
List<string> Tags);
|
||||
List<string> Tags,
|
||||
List<string> Studios);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
var builder = new FFmpegComplexFilterBuilder();
|
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build();
|
||||
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||
|
||||
result.IsNone.Should().BeTrue();
|
||||
}
|
||||
@@ -30,15 +30,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
|
||||
.WithAlignedAudio(duration);
|
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build();
|
||||
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||
|
||||
result.IsSome.Should().BeTrue();
|
||||
result.IfSome(
|
||||
filter =>
|
||||
{
|
||||
filter.ComplexFilter.Should().Be($"[0:a]apad=whole_dur={duration.TotalMilliseconds}ms[a]");
|
||||
filter.ComplexFilter.Should().Be($"[0:1]apad=whole_dur={duration.TotalMilliseconds}ms[a]");
|
||||
filter.AudioLabel.Should().Be("[a]");
|
||||
filter.VideoLabel.Should().Be("0:V");
|
||||
filter.VideoLabel.Should().Be("0:0");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,36 +50,36 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
.WithAlignedAudio(duration)
|
||||
.WithDeinterlace(true);
|
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build();
|
||||
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||
|
||||
result.IsSome.Should().BeTrue();
|
||||
result.IfSome(
|
||||
filter =>
|
||||
{
|
||||
filter.ComplexFilter.Should().Be(
|
||||
$"[0:a]apad=whole_dur={duration.TotalMilliseconds}ms[a];[0:V]yadif=1[v]");
|
||||
$"[0:1]apad=whole_dur={duration.TotalMilliseconds}ms[a];[0:0]yadif=1[v]");
|
||||
filter.AudioLabel.Should().Be("[a]");
|
||||
filter.VideoLabel.Should().Be("[v]");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(true, false, false, "[0:V]yadif=1[v]", "[v]")]
|
||||
[TestCase(true, true, false, "[0:V]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
|
||||
[TestCase(true, false, true, "[0:V]yadif=1,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
|
||||
[TestCase(true, false, false, "[0:0]yadif=1[v]", "[v]")]
|
||||
[TestCase(true, true, false, "[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
|
||||
[TestCase(true, false, true, "[0:0]yadif=1,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
|
||||
[TestCase(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"[0:V]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]",
|
||||
"[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]",
|
||||
"[v]")]
|
||||
[TestCase(false, true, false, "[0:V]scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
|
||||
[TestCase(false, false, true, "[0:V]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
|
||||
[TestCase(false, true, false, "[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
|
||||
[TestCase(false, false, true, "[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
|
||||
[TestCase(
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"[0:V]scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]",
|
||||
"[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]",
|
||||
"[v]")]
|
||||
public void Should_Return_Software_Video_Filter(
|
||||
bool deinterlace,
|
||||
@@ -101,55 +101,55 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
||||
}
|
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build();
|
||||
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||
|
||||
result.IsSome.Should().BeTrue();
|
||||
result.IfSome(
|
||||
filter =>
|
||||
{
|
||||
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
||||
filter.AudioLabel.Should().Be("0:a");
|
||||
filter.AudioLabel.Should().Be("0:1");
|
||||
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(true, false, false, "[0:V]deinterlace_qsv[v]", "[v]")]
|
||||
[TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")]
|
||||
[TestCase(
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
"[0:V]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
|
||||
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
"[0:V]deinterlace_qsv,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||
"[0:0]deinterlace_qsv,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"[0:V]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
"[0:V]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
|
||||
"[0:0]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
"[0:V]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||
"[0:0]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"[0:V]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||
"[0:0]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||
"[v]")]
|
||||
public void Should_Return_QSV_Video_Filter(
|
||||
bool deinterlace,
|
||||
@@ -172,14 +172,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
||||
}
|
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build();
|
||||
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||
|
||||
result.IsSome.Should().BeTrue();
|
||||
result.IfSome(
|
||||
filter =>
|
||||
{
|
||||
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
||||
filter.AudioLabel.Should().Be("0:a");
|
||||
filter.AudioLabel.Should().Be("0:1");
|
||||
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
||||
});
|
||||
}
|
||||
@@ -209,37 +209,37 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
"[0:V]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
||||
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
"[0:V]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"[0:V]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
"[0:V]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
||||
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
"[0:V]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"[0:V]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
public void Should_Return_NVENC_Video_Filter(
|
||||
bool deinterlace,
|
||||
@@ -262,104 +262,104 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
||||
}
|
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build();
|
||||
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||
|
||||
result.IsSome.Should().BeTrue();
|
||||
result.IfSome(
|
||||
filter =>
|
||||
{
|
||||
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
||||
filter.AudioLabel.Should().Be("0:a");
|
||||
filter.AudioLabel.Should().Be("0:1");
|
||||
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("h264", true, false, false, "[0:V]deinterlace_vaapi[v]", "[v]")]
|
||||
[TestCase("h264", true, false, false, "[0:0]deinterlace_vaapi[v]", "[v]")]
|
||||
[TestCase(
|
||||
"h264",
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
"[0:V]deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||
"[0:0]deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
"h264",
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
"[0:V]deinterlace_vaapi,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]deinterlace_vaapi,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
"h264",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"[0:V]deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
"h264",
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
"[0:V]scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||
"[0:0]scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
"h264",
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
"[0:V]hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
"h264",
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"[0:V]scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase("mpeg4", true, false, false, "[0:V]hwupload,deinterlace_vaapi[v]", "[v]")]
|
||||
[TestCase("mpeg4", true, false, false, "[0:0]hwupload,deinterlace_vaapi[v]", "[v]")]
|
||||
[TestCase(
|
||||
"mpeg4",
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
"[0:V]hwupload,deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
"mpeg4",
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
"[0:V]hwupload,deinterlace_vaapi,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]hwupload,deinterlace_vaapi,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
"mpeg4",
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
"[0:V]hwupload,deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
"mpeg4",
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
"[0:V]hwupload,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||
"[0:0]hwupload,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
"mpeg4",
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
"[0:V]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
[TestCase(
|
||||
"mpeg4",
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
"[0:V]hwupload,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[0:0]hwupload,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||
"[v]")]
|
||||
public void Should_Return_VAAPI_Video_Filter(
|
||||
string codec,
|
||||
@@ -384,14 +384,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
||||
}
|
||||
|
||||
Option<FFmpegComplexFilter> result = builder.Build();
|
||||
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||
|
||||
result.IsSome.Should().BeTrue();
|
||||
result.IfSome(
|
||||
filter =>
|
||||
{
|
||||
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
||||
filter.AudioLabel.Should().Be("0:a");
|
||||
filter.AudioLabel.Should().Be("0:1");
|
||||
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
new MediaVersion(),
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -40,6 +42,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.HttpLiveStreaming,
|
||||
ffmpegProfile,
|
||||
new MediaVersion(),
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -55,6 +59,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
new MediaVersion(),
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -72,6 +78,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.HttpLiveStreaming,
|
||||
ffmpegProfile,
|
||||
new MediaVersion(),
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -89,6 +97,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
new MediaVersion(),
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -104,6 +114,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.HttpLiveStreaming,
|
||||
ffmpegProfile,
|
||||
new MediaVersion(),
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -121,6 +133,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
new MediaVersion(),
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
now,
|
||||
now.AddMinutes(5));
|
||||
|
||||
@@ -139,6 +153,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.HttpLiveStreaming,
|
||||
ffmpegProfile,
|
||||
new MediaVersion(),
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
now,
|
||||
now.AddMinutes(5));
|
||||
|
||||
@@ -155,6 +171,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
new MediaVersion(),
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -177,6 +195,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -199,6 +219,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -221,6 +243,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -244,6 +268,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -267,6 +293,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.HttpLiveStreaming,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -290,6 +318,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -315,6 +345,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -337,12 +369,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
// not anamorphic
|
||||
var version = new MediaVersion
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "mpeg2video" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -365,12 +399,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
// not anamorphic
|
||||
var version = new MediaVersion
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.HttpLiveStreaming,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "mpeg2video" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -392,12 +428,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
// not anamorphic
|
||||
var version = new MediaVersion
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "libx264" };
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "libx264" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -420,12 +458,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
// not anamorphic
|
||||
var version = new MediaVersion
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "mpeg2video" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -452,6 +492,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -473,12 +515,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
// not anamorphic
|
||||
var version = new MediaVersion
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "mpeg2video" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -505,6 +549,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -527,12 +573,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
|
||||
// not anamorphic
|
||||
var version = new MediaVersion
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
|
||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "mpeg2video" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -550,12 +598,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
AudioCodec = "aac"
|
||||
};
|
||||
|
||||
var version = new MediaVersion { AudioCodec = "aac" };
|
||||
var version = new MediaVersion();
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "aac" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -571,12 +621,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
AudioCodec = "aac"
|
||||
};
|
||||
|
||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
||||
var version = new MediaVersion();
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -592,12 +644,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
AudioCodec = "aac"
|
||||
};
|
||||
|
||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
||||
var version = new MediaVersion();
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -613,12 +667,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
AudioCodec = "aac"
|
||||
};
|
||||
|
||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
||||
var version = new MediaVersion();
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.HttpLiveStreaming,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -634,12 +690,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
AudioBitrate = 2424
|
||||
};
|
||||
|
||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
||||
var version = new MediaVersion();
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -655,12 +713,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
AudioBufferSize = 2424
|
||||
};
|
||||
|
||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
||||
var version = new MediaVersion();
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -678,12 +738,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
AudioChannels = 6
|
||||
};
|
||||
|
||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
||||
var version = new MediaVersion();
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -701,12 +763,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
AudioSampleRate = 48
|
||||
};
|
||||
|
||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
||||
var version = new MediaVersion();
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -723,12 +787,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
AudioChannels = 6
|
||||
};
|
||||
|
||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
||||
var version = new MediaVersion();
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -745,12 +811,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
AudioSampleRate = 48
|
||||
};
|
||||
|
||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
||||
var version = new MediaVersion();
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
@@ -775,6 +843,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
new MediaVersion(),
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now);
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
_folders = allFolders.Distinct().Map(f => new FakeFolderEntry(f)).ToList();
|
||||
}
|
||||
|
||||
public Unit EnsureFolderExists(string folder) => Unit.Default;
|
||||
|
||||
public DateTime GetLastWriteTime(string path) =>
|
||||
Optional(_files.SingleOrDefault(f => f.Path == path))
|
||||
.Map(f => f.LastWriteTime)
|
||||
@@ -54,8 +56,8 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
|
||||
public Task<byte[]> ReadAllBytes(string path) => TestBytes.AsTask();
|
||||
|
||||
public Unit CopyFile(string source, string destination) =>
|
||||
Unit.Default;
|
||||
public Task<Either<BaseError, Unit>> CopyFile(string source, string destination) =>
|
||||
Task.FromResult(Right<BaseError, Unit>(Unit.Default));
|
||||
|
||||
private static List<DirectoryInfo> Split(DirectoryInfo path)
|
||||
{
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Fakes
|
||||
{
|
||||
public class FakeMovieWithPath : Movie
|
||||
public class FakeMovieWithPath : MediaItemScanResult<Movie>
|
||||
{
|
||||
public FakeMovieWithPath(string path)
|
||||
{
|
||||
Path = path;
|
||||
|
||||
MediaVersions = new List<MediaVersion>
|
||||
{
|
||||
new()
|
||||
: base(
|
||||
new Movie
|
||||
{
|
||||
MediaFiles = new List<MediaFile>
|
||||
MediaVersions = new List<MediaVersion>
|
||||
{
|
||||
new() { Path = path }
|
||||
new()
|
||||
{
|
||||
MediaFiles = new List<MediaFile>
|
||||
{
|
||||
new() { Path = path }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
}) =>
|
||||
IsAdded = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Fakes
|
||||
@@ -11,12 +12,6 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
{
|
||||
public Task<bool> AllShowsExist(List<int> showIds) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> Update(Show show) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> Update(Season season) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> Update(Episode episode) => throw new NotSupportedException();
|
||||
|
||||
public Task<List<Show>> GetAllShows() => throw new NotSupportedException();
|
||||
|
||||
public Task<Option<Show>> GetShow(int showId) => throw new NotSupportedException();
|
||||
@@ -26,6 +21,8 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
public Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<List<ShowMetadata>> GetShowsForCards(List<int> ids) => throw new NotSupportedException();
|
||||
|
||||
public Task<List<Episode>> GetShowItems(int showId) => throw new NotSupportedException();
|
||||
|
||||
public Task<List<Season>> GetAllSeasons() => throw new NotSupportedException();
|
||||
@@ -49,7 +46,7 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
public Task<Option<Show>> GetShowByMetadata(int libraryPathId, ShowMetadata metadata) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<Either<BaseError, Show>>
|
||||
public Task<Either<BaseError, MediaItemScanResult<Show>>>
|
||||
AddShow(int libraryPathId, string showFolder, ShowMetadata metadata) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
@@ -65,9 +62,11 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
|
||||
public Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath) => throw new NotSupportedException();
|
||||
|
||||
public Task<Unit> DeleteEmptyShows(LibraryPath libraryPath) => throw new NotSupportedException();
|
||||
public Task<List<int>> DeleteEmptyShows(LibraryPath libraryPath) => throw new NotSupportedException();
|
||||
|
||||
public Task<Either<BaseError, PlexShow>> GetOrAddPlexShow(PlexLibrary library, PlexShow item) =>
|
||||
public Task<Either<BaseError, MediaItemScanResult<PlexShow>>> GetOrAddPlexShow(
|
||||
PlexLibrary library,
|
||||
PlexShow item) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<Either<BaseError, PlexSeason>> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item) =>
|
||||
@@ -76,9 +75,12 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
public Task<Either<BaseError, PlexEpisode>> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<Unit> AddGenre(ShowMetadata metadata, Genre genre) => throw new NotSupportedException();
|
||||
public Task<bool> AddGenre(ShowMetadata metadata, Genre genre) => throw new NotSupportedException();
|
||||
public Task<bool> AddTag(ShowMetadata metadata, Tag tag) => throw new NotSupportedException();
|
||||
|
||||
public Task<Unit> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys) =>
|
||||
public Task<bool> AddStudio(ShowMetadata metadata, Studio studio) => throw new NotSupportedException();
|
||||
|
||||
public Task<List<int>> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys) =>
|
||||
@@ -86,5 +88,13 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
|
||||
public Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> Update(Show show) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> Update(Season season) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> Update(Episode episode) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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 ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Core.Tests.Fakes;
|
||||
using FluentAssertions;
|
||||
@@ -44,20 +45,24 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
_movieRepository = new Mock<IMovieRepository>();
|
||||
_movieRepository.Setup(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()))
|
||||
.Returns(
|
||||
(LibraryPath _, string path) => Right<BaseError, Movie>(new FakeMovieWithPath(path)).AsTask());
|
||||
(LibraryPath _, string path) =>
|
||||
Right<BaseError, MediaItemScanResult<Movie>>(new FakeMovieWithPath(path)).AsTask());
|
||||
_movieRepository.Setup(x => x.FindMoviePaths(It.IsAny<LibraryPath>()))
|
||||
.Returns(new List<string>().AsEnumerable().AsTask());
|
||||
|
||||
_localStatisticsProvider = new Mock<ILocalStatisticsProvider>();
|
||||
_localMetadataProvider = new Mock<ILocalMetadataProvider>();
|
||||
|
||||
_localStatisticsProvider.Setup(x => x.RefreshStatistics(It.IsAny<string>(), It.IsAny<MediaItem>()))
|
||||
.Returns<string, MediaItem>((_, _) => Right<BaseError, bool>(true).AsTask());
|
||||
|
||||
// fallback metadata adds metadata to a movie, so we need to replicate that here
|
||||
_localMetadataProvider.Setup(x => x.RefreshFallbackMetadata(It.IsAny<MediaItem>()))
|
||||
.Returns(
|
||||
(MediaItem mediaItem) =>
|
||||
{
|
||||
((Movie) mediaItem).MovieMetadata = new List<MovieMetadata> { new() };
|
||||
return Unit.Default.AsTask();
|
||||
return Task.FromResult(true);
|
||||
});
|
||||
|
||||
_imageCache = new Mock<IImageCache>();
|
||||
@@ -76,7 +81,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
);
|
||||
var libraryPath = new LibraryPath { Path = BadFakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsLeft.Should().BeTrue();
|
||||
result.IfLeft(error => error.Should().BeOfType<MediaSourceInaccessible>());
|
||||
@@ -96,7 +104,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -104,11 +115,14 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||
|
||||
_localStatisticsProvider.Verify(
|
||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshStatistics(
|
||||
FFprobePath,
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_localMetadataProvider.Verify(
|
||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshFallbackMetadata(
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -129,7 +143,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -137,11 +154,15 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||
|
||||
_localStatisticsProvider.Verify(
|
||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshStatistics(
|
||||
FFprobePath,
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_localMetadataProvider.Verify(
|
||||
x => x.RefreshSidecarMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath), metadataPath),
|
||||
x => x.RefreshSidecarMetadata(
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath),
|
||||
metadataPath),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -162,7 +183,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -170,11 +194,15 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||
|
||||
_localStatisticsProvider.Verify(
|
||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshStatistics(
|
||||
FFprobePath,
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_localMetadataProvider.Verify(
|
||||
x => x.RefreshSidecarMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath), metadataPath),
|
||||
x => x.RefreshSidecarMetadata(
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath),
|
||||
metadataPath),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -199,7 +227,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -207,11 +238,61 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||
|
||||
_localStatisticsProvider.Verify(
|
||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshStatistics(
|
||||
FFprobePath,
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_localMetadataProvider.Verify(
|
||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshFallbackMetadata(
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_imageCache.Verify(
|
||||
x => x.CopyArtworkToCache(posterPath, ArtworkKind.Poster),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NewMovie_Statistics_And_FallbackMetadata_And_FolderPoster(
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))]
|
||||
string videoExtension,
|
||||
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))]
|
||||
string imageExtension)
|
||||
{
|
||||
string moviePath = Path.Combine(
|
||||
FakeRoot,
|
||||
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
|
||||
|
||||
string posterPath = Path.Combine(
|
||||
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||
$"folder.{imageExtension}");
|
||||
|
||||
MovieFolderScanner service = GetService(
|
||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||
new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now }
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
|
||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||
|
||||
_localStatisticsProvider.Verify(
|
||||
x => x.RefreshStatistics(
|
||||
FFprobePath,
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_localMetadataProvider.Verify(
|
||||
x => x.RefreshFallbackMetadata(
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_imageCache.Verify(
|
||||
@@ -240,7 +321,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -248,11 +332,14 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||
|
||||
_localStatisticsProvider.Verify(
|
||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshStatistics(
|
||||
FFprobePath,
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_localMetadataProvider.Verify(
|
||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshFallbackMetadata(
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_imageCache.Verify(
|
||||
@@ -280,7 +367,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -288,11 +378,14 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||
|
||||
_localStatisticsProvider.Verify(
|
||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshStatistics(
|
||||
FFprobePath,
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_localMetadataProvider.Verify(
|
||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshFallbackMetadata(
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -316,7 +409,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -324,11 +420,14 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||
|
||||
_localStatisticsProvider.Verify(
|
||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshStatistics(
|
||||
FFprobePath,
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_localMetadataProvider.Verify(
|
||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshFallbackMetadata(
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -346,7 +445,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -354,11 +456,14 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||
|
||||
_localStatisticsProvider.Verify(
|
||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshStatistics(
|
||||
FFprobePath,
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
|
||||
_localMetadataProvider.Verify(
|
||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
||||
x => x.RefreshFallbackMetadata(
|
||||
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -378,7 +483,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -402,7 +510,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
);
|
||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -417,7 +528,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
_movieRepository.Object,
|
||||
_localStatisticsProvider.Object,
|
||||
_localMetadataProvider.Object,
|
||||
new Mock<IMetadataRepository>().Object,
|
||||
_imageCache.Object,
|
||||
new Mock<ISearchIndex>().Object,
|
||||
new Mock<ILogger<MovieFolderScanner>>().Object
|
||||
);
|
||||
}
|
||||
|
||||
211
ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs
Normal file
211
ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Runtime;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using FluentAssertions;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Plex
|
||||
{
|
||||
[TestFixture]
|
||||
public class PlexPathReplacementServiceTests
|
||||
{
|
||||
[Test]
|
||||
public async Task PlexWindows_To_EtvWindows()
|
||||
{
|
||||
var replacements = new List<PlexPathReplacement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
PlexPath = @"C:\Something\Some Shared Folder",
|
||||
LocalPath = @"C:\Something Else\Some Shared Folder",
|
||||
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
|
||||
}
|
||||
};
|
||||
|
||||
var repo = new Mock<IMediaSourceRepository>();
|
||||
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||
|
||||
var runtime = new Mock<IRuntimeInfo>();
|
||||
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true);
|
||||
|
||||
var service = new PlexPathReplacementService(
|
||||
repo.Object,
|
||||
runtime.Object,
|
||||
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||
|
||||
string result = await service.GetReplacementPlexPath(
|
||||
0,
|
||||
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PlexWindows_To_EtvLinux()
|
||||
{
|
||||
var replacements = new List<PlexPathReplacement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
PlexPath = @"C:\Something\Some Shared Folder",
|
||||
LocalPath = @"/mnt/something else/Some Shared Folder",
|
||||
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
|
||||
}
|
||||
};
|
||||
|
||||
var repo = new Mock<IMediaSourceRepository>();
|
||||
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||
|
||||
var runtime = new Mock<IRuntimeInfo>();
|
||||
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||
|
||||
var service = new PlexPathReplacementService(
|
||||
repo.Object,
|
||||
runtime.Object,
|
||||
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||
|
||||
string result = await service.GetReplacementPlexPath(
|
||||
0,
|
||||
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PlexWindows_To_EtvLinux_UncPath()
|
||||
{
|
||||
var replacements = new List<PlexPathReplacement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
PlexPath = @"\\192.168.1.100\Something\Some Shared Folder",
|
||||
LocalPath = @"/mnt/something else/Some Shared Folder",
|
||||
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
|
||||
}
|
||||
};
|
||||
|
||||
var repo = new Mock<IMediaSourceRepository>();
|
||||
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||
|
||||
var runtime = new Mock<IRuntimeInfo>();
|
||||
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||
|
||||
var service = new PlexPathReplacementService(
|
||||
repo.Object,
|
||||
runtime.Object,
|
||||
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||
|
||||
string result = await service.GetReplacementPlexPath(
|
||||
0,
|
||||
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PlexWindows_To_EtvLinux_UncPathWithTrailingSlash()
|
||||
{
|
||||
var replacements = new List<PlexPathReplacement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
PlexPath = @"\\192.168.1.100\Something\Some Shared Folder\",
|
||||
LocalPath = @"/mnt/something else/Some Shared Folder/",
|
||||
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
|
||||
}
|
||||
};
|
||||
|
||||
var repo = new Mock<IMediaSourceRepository>();
|
||||
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||
|
||||
var runtime = new Mock<IRuntimeInfo>();
|
||||
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||
|
||||
var service = new PlexPathReplacementService(
|
||||
repo.Object,
|
||||
runtime.Object,
|
||||
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||
|
||||
string result = await service.GetReplacementPlexPath(
|
||||
0,
|
||||
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PlexLinux_To_EtvWindows()
|
||||
{
|
||||
var replacements = new List<PlexPathReplacement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
PlexPath = @"/mnt/something/Some Shared Folder",
|
||||
LocalPath = @"C:\Something Else\Some Shared Folder",
|
||||
PlexMediaSource = new PlexMediaSource { Platform = "Linux" }
|
||||
}
|
||||
};
|
||||
|
||||
var repo = new Mock<IMediaSourceRepository>();
|
||||
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||
|
||||
var runtime = new Mock<IRuntimeInfo>();
|
||||
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true);
|
||||
|
||||
var service = new PlexPathReplacementService(
|
||||
repo.Object,
|
||||
runtime.Object,
|
||||
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||
|
||||
string result = await service.GetReplacementPlexPath(
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PlexLinux_To_EtvLinux()
|
||||
{
|
||||
var replacements = new List<PlexPathReplacement>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
PlexPath = @"/mnt/something/Some Shared Folder",
|
||||
LocalPath = @"/mnt/something else/Some Shared Folder",
|
||||
PlexMediaSource = new PlexMediaSource { Platform = "Linux" }
|
||||
}
|
||||
};
|
||||
|
||||
var repo = new Mock<IMediaSourceRepository>();
|
||||
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||
|
||||
var runtime = new Mock<IRuntimeInfo>();
|
||||
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||
|
||||
var service = new PlexPathReplacementService(
|
||||
repo.Object,
|
||||
runtime.Object,
|
||||
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||
|
||||
string result = await service.GetReplacementPlexPath(
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -610,6 +610,190 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
||||
result.Items[5].MediaItemId.Should().Be(4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Alternating_MultipleContent_Should_Maintain_Counts()
|
||||
{
|
||||
var collectionOne = new Collection
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Multiple Items 1",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var collectionTwo = new Collection
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Multiple Items 2",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var fakeRepository = new FakeMediaCollectionRepository(
|
||||
Map(
|
||||
(collectionOne.Id, collectionOne.MediaItems.ToList()),
|
||||
(collectionTwo.Id, collectionTwo.MediaItems.ToList())));
|
||||
|
||||
var items = new List<ProgramScheduleItem>
|
||||
{
|
||||
new ProgramScheduleItemMultiple
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
Count = 3
|
||||
},
|
||||
new ProgramScheduleItemMultiple
|
||||
{
|
||||
Id = 2,
|
||||
Index = 2,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id,
|
||||
StartTime = null,
|
||||
Count = 3
|
||||
}
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
ProgramSchedule = new ProgramSchedule
|
||||
{
|
||||
Items = items,
|
||||
MediaCollectionPlaybackOrder = PlaybackOrder.Chronological
|
||||
},
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
Anchor = new PlayoutAnchor
|
||||
{
|
||||
NextStart = HoursAfterMidnight(1).UtcDateTime,
|
||||
NextScheduleItem = items[0],
|
||||
NextScheduleItemId = 1,
|
||||
MultipleRemaining = 2
|
||||
}
|
||||
};
|
||||
|
||||
var televisionRepo = new FakeTelevisionRepository();
|
||||
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
|
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(0);
|
||||
DateTimeOffset finish = start + TimeSpan.FromHours(5);
|
||||
|
||||
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
|
||||
|
||||
result.Items.Count.Should().Be(4);
|
||||
|
||||
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1));
|
||||
result.Items[0].MediaItemId.Should().Be(1);
|
||||
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2));
|
||||
result.Items[1].MediaItemId.Should().Be(1);
|
||||
|
||||
result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
|
||||
result.Items[2].MediaItemId.Should().Be(2);
|
||||
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
|
||||
result.Items[3].MediaItemId.Should().Be(2);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[1]);
|
||||
result.Anchor.MultipleRemaining.Should().Be(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Alternating_Duration_Should_Maintain_Duration()
|
||||
{
|
||||
var collectionOne = new Collection
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Duration Items 1",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var collectionTwo = new Collection
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Duration Items 2",
|
||||
MediaItems = new List<MediaItem>
|
||||
{
|
||||
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))
|
||||
}
|
||||
};
|
||||
|
||||
var fakeRepository = new FakeMediaCollectionRepository(
|
||||
Map(
|
||||
(collectionOne.Id, collectionOne.MediaItems.ToList()),
|
||||
(collectionTwo.Id, collectionTwo.MediaItems.ToList())));
|
||||
|
||||
var items = new List<ProgramScheduleItem>
|
||||
{
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 1,
|
||||
Index = 1,
|
||||
Collection = collectionOne,
|
||||
CollectionId = collectionOne.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
OfflineTail = false
|
||||
},
|
||||
new ProgramScheduleItemDuration
|
||||
{
|
||||
Id = 2,
|
||||
Index = 2,
|
||||
Collection = collectionTwo,
|
||||
CollectionId = collectionTwo.Id,
|
||||
StartTime = null,
|
||||
PlayoutDuration = TimeSpan.FromHours(3),
|
||||
OfflineTail = false
|
||||
}
|
||||
};
|
||||
|
||||
var playout = new Playout
|
||||
{
|
||||
ProgramSchedule = new ProgramSchedule
|
||||
{
|
||||
Items = items,
|
||||
MediaCollectionPlaybackOrder = PlaybackOrder.Chronological
|
||||
},
|
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
|
||||
Anchor = new PlayoutAnchor
|
||||
{
|
||||
NextStart = HoursAfterMidnight(1).UtcDateTime,
|
||||
NextScheduleItem = items[0],
|
||||
NextScheduleItemId = 1,
|
||||
DurationFinish = HoursAfterMidnight(3).UtcDateTime
|
||||
}
|
||||
};
|
||||
|
||||
var televisionRepo = new FakeTelevisionRepository();
|
||||
var builder = new PlayoutBuilder(fakeRepository, televisionRepo, _logger);
|
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(0);
|
||||
DateTimeOffset finish = start + TimeSpan.FromHours(5);
|
||||
|
||||
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
|
||||
|
||||
result.Items.Count.Should().Be(4);
|
||||
|
||||
result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1));
|
||||
result.Items[0].MediaItemId.Should().Be(1);
|
||||
result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2));
|
||||
result.Items[1].MediaItemId.Should().Be(1);
|
||||
|
||||
result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3));
|
||||
result.Items[2].MediaItemId.Should().Be(2);
|
||||
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
|
||||
result.Items[3].MediaItemId.Should().Be(2);
|
||||
|
||||
result.Anchor.NextScheduleItem.Should().Be(items[1]);
|
||||
result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime);
|
||||
}
|
||||
|
||||
private static DateTimeOffset HoursAfterMidnight(int hours)
|
||||
{
|
||||
DateTimeOffset now = DateTimeOffset.Now;
|
||||
|
||||
@@ -16,8 +16,7 @@ namespace ErsatzTV.Core.Domain
|
||||
public FFmpegProfile FFmpegProfile { get; set; }
|
||||
public StreamingMode StreamingMode { get; set; }
|
||||
public List<Playout> Playouts { get; set; }
|
||||
|
||||
public List<Artwork> Artwork { get; set; }
|
||||
// public SourceMode Mode { get; set; }
|
||||
public string PreferredLanguageCode { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,7 @@
|
||||
public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id");
|
||||
public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id");
|
||||
public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports");
|
||||
public static ConfigElementKey FFmpegPreferredLanguageCode => new("ffmpeg.preferred_language_code");
|
||||
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
|
||||
}
|
||||
}
|
||||
|
||||
18
ErsatzTV.Core/Domain/MediaItem/MediaStream.cs
Normal file
18
ErsatzTV.Core/Domain/MediaItem/MediaStream.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class MediaStream
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int Index { get; set; }
|
||||
public string Codec { get; set; }
|
||||
public string Profile { get; set; }
|
||||
public MediaStreamKind MediaStreamKind { get; set; }
|
||||
public string Language { get; set; }
|
||||
public int Channels { get; set; }
|
||||
public string Title { get; set; }
|
||||
public bool Default { get; set; }
|
||||
public bool Forced { get; set; }
|
||||
public int MediaVersionId { get; set; }
|
||||
public MediaVersion MediaVersion { get; set; }
|
||||
}
|
||||
}
|
||||
9
ErsatzTV.Core/Domain/MediaItem/MediaStreamKind.cs
Normal file
9
ErsatzTV.Core/Domain/MediaItem/MediaStreamKind.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public enum MediaStreamKind
|
||||
{
|
||||
Video = 1,
|
||||
Audio = 2,
|
||||
Subtitle = 3
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,21 @@ namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public List<MediaFile> MediaFiles { get; set; }
|
||||
|
||||
public List<MediaStream> Streams { get; set; }
|
||||
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; }
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public string ServerName { get; set; }
|
||||
public string ProductVersion { get; set; }
|
||||
public string Platform { get; set; }
|
||||
public string PlatformVersion { get; set; }
|
||||
public string ClientIdentifier { get; set; }
|
||||
|
||||
// public bool IsOwned { get; set; }
|
||||
|
||||
@@ -17,5 +17,6 @@ namespace ErsatzTV.Core.Domain
|
||||
public List<Artwork> Artwork { get; set; }
|
||||
public List<Genre> Genres { get; set; }
|
||||
public List<Tag> Tags { get; set; }
|
||||
public List<Studio> Studios { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
8
ErsatzTV.Core/Domain/Metadata/Studio.cs
Normal file
8
ErsatzTV.Core/Domain/Metadata/Studio.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class Studio
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
@@ -9,7 +11,13 @@ namespace ErsatzTV.Core.Domain
|
||||
public ProgramScheduleItem NextScheduleItem { get; set; }
|
||||
|
||||
public DateTime NextStart { get; set; }
|
||||
public int? MultipleRemaining { get; set; }
|
||||
public DateTime? DurationFinish { get; set; }
|
||||
|
||||
public DateTimeOffset NextStartOffset => new DateTimeOffset(NextStart, TimeSpan.Zero).ToLocalTime();
|
||||
|
||||
public Option<DateTimeOffset> DurationFinishOffset =>
|
||||
Optional(DurationFinish)
|
||||
.Map(durationFinish => new DateTimeOffset(durationFinish, TimeSpan.Zero).ToLocalTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace ErsatzTV.Core.Domain
|
||||
public MediaItem MediaItem { get; set; }
|
||||
public DateTime Start { get; set; }
|
||||
public DateTime Finish { get; set; }
|
||||
public string CustomTitle { get; set; }
|
||||
public bool CustomGroup { get; set; }
|
||||
public int PlayoutId { get; set; }
|
||||
public Playout Playout { get; set; }
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Domain
|
||||
public StartType StartType => StartTime.HasValue ? StartType.Fixed : StartType.Dynamic;
|
||||
public TimeSpan? StartTime { get; set; }
|
||||
public ProgramScheduleItemCollectionType CollectionType { get; set; }
|
||||
public string CustomTitle { get; set; }
|
||||
public int ProgramScheduleId { get; set; }
|
||||
public ProgramSchedule ProgramSchedule { get; set; }
|
||||
public int? CollectionId { get; set; }
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public enum SourceMode
|
||||
{
|
||||
Transcode,
|
||||
DirectPlay,
|
||||
DirectPaths
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
public override string ToString() =>
|
||||
$@"ffconcat version 1.0
|
||||
file {Scheme}://{Host}/ffmpeg/stream/{ChannelNumber}
|
||||
file {Scheme}://{Host}/ffmpeg/stream/{ChannelNumber}";
|
||||
file http://localhost:8409/ffmpeg/stream/{ChannelNumber}
|
||||
file http://localhost:8409/ffmpeg/stream/{ChannelNumber}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,12 +54,12 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public Option<FFmpegComplexFilter> Build()
|
||||
public Option<FFmpegComplexFilter> Build(int videoStreamIndex, int audioStreamIndex)
|
||||
{
|
||||
var complexFilter = new StringBuilder();
|
||||
|
||||
var videoLabel = "0:V";
|
||||
var audioLabel = "0:a";
|
||||
var videoLabel = $"0:{videoStreamIndex}";
|
||||
var audioLabel = $"0:{audioStreamIndex}";
|
||||
|
||||
HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None);
|
||||
bool isHardwareDecode = acceleration switch
|
||||
|
||||
@@ -45,6 +45,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
StreamingMode streamingMode,
|
||||
FFmpegProfile ffmpegProfile,
|
||||
MediaVersion version,
|
||||
MediaStream videoStream,
|
||||
MediaStream audioStream,
|
||||
DateTimeOffset start,
|
||||
DateTimeOffset now)
|
||||
{
|
||||
@@ -85,7 +87,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
}
|
||||
|
||||
if (result.ScaledSize.IsSome || result.PadToDesiredResolution ||
|
||||
NeedToNormalizeVideoCodec(ffmpegProfile, version))
|
||||
NeedToNormalizeVideoCodec(ffmpegProfile, videoStream))
|
||||
{
|
||||
result.VideoCodec = ffmpegProfile.VideoCodec;
|
||||
result.VideoBitrate = ffmpegProfile.VideoBitrate;
|
||||
@@ -96,7 +98,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
result.VideoCodec = "copy";
|
||||
}
|
||||
|
||||
if (NeedToNormalizeAudioCodec(ffmpegProfile, version))
|
||||
if (NeedToNormalizeAudioCodec(ffmpegProfile, audioStream))
|
||||
{
|
||||
result.AudioCodec = ffmpegProfile.AudioCodec;
|
||||
result.AudioBitrate = ffmpegProfile.AudioBitrate;
|
||||
@@ -104,7 +106,11 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
|
||||
if (ffmpegProfile.NormalizeAudio)
|
||||
{
|
||||
result.AudioChannels = ffmpegProfile.AudioChannels;
|
||||
if (audioStream.Channels != ffmpegProfile.AudioChannels)
|
||||
{
|
||||
result.AudioChannels = ffmpegProfile.AudioChannels;
|
||||
}
|
||||
|
||||
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
|
||||
result.AudioDuration = version.Duration;
|
||||
}
|
||||
@@ -152,11 +158,11 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
private static bool IsOddSize(MediaVersion version) =>
|
||||
version.Height % 2 == 1 || version.Width % 2 == 1;
|
||||
|
||||
private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaVersion version) =>
|
||||
ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != version.VideoCodec;
|
||||
private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaStream videoStream) =>
|
||||
ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != videoStream.Codec;
|
||||
|
||||
private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaVersion version) =>
|
||||
ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != version.AudioCodec;
|
||||
private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaStream audioStream) =>
|
||||
ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != audioStream.Codec;
|
||||
|
||||
private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version)
|
||||
{
|
||||
|
||||
@@ -329,12 +329,12 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithFilterComplex()
|
||||
public FFmpegProcessBuilder WithFilterComplex(int videoStreamIndex, int audioStreamIndex)
|
||||
{
|
||||
var videoLabel = "0:V";
|
||||
var audioLabel = "0:a";
|
||||
var videoLabel = $"0:{videoStreamIndex}";
|
||||
var audioLabel = $"0:{audioStreamIndex}";
|
||||
|
||||
Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build();
|
||||
Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build(videoStreamIndex, audioStreamIndex);
|
||||
maybeFilter.IfSome(
|
||||
filter =>
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using LanguageExt;
|
||||
@@ -8,12 +9,18 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
public class FFmpegProcessService
|
||||
{
|
||||
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
|
||||
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
|
||||
|
||||
public FFmpegProcessService(FFmpegPlaybackSettingsCalculator ffmpegPlaybackSettingsService) =>
|
||||
public FFmpegProcessService(
|
||||
FFmpegPlaybackSettingsCalculator ffmpegPlaybackSettingsService,
|
||||
IFFmpegStreamSelector ffmpegStreamSelector)
|
||||
{
|
||||
_playbackSettingsCalculator = ffmpegPlaybackSettingsService;
|
||||
_ffmpegStreamSelector = ffmpegStreamSelector;
|
||||
}
|
||||
|
||||
public Process ForPlayoutItem(
|
||||
public async Task<Process> ForPlayoutItem(
|
||||
string ffmpegPath,
|
||||
bool saveReports,
|
||||
Channel channel,
|
||||
@@ -22,10 +29,15 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
DateTimeOffset start,
|
||||
DateTimeOffset now)
|
||||
{
|
||||
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, version);
|
||||
MediaStream audioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, version);
|
||||
|
||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
|
||||
channel.StreamingMode,
|
||||
channel.FFmpegProfile,
|
||||
version,
|
||||
videoStream,
|
||||
audioStream,
|
||||
start,
|
||||
now);
|
||||
|
||||
@@ -36,7 +48,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
||||
.WithSeek(playbackSettings.StreamSeek)
|
||||
.WithInputCodec(path, playbackSettings.HardwareAcceleration, version.VideoCodec);
|
||||
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec);
|
||||
|
||||
playbackSettings.ScaledSize.Match(
|
||||
scaledSize =>
|
||||
@@ -51,7 +63,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
}
|
||||
|
||||
builder = builder
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration).WithFilterComplex();
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
@@ -61,19 +74,19 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
.WithDeinterlace(playbackSettings.Deinterlace)
|
||||
.WithBlackBars(channel.FFmpegProfile.Resolution)
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
.WithFilterComplex();
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
}
|
||||
else if (playbackSettings.Deinterlace)
|
||||
{
|
||||
builder = builder.WithDeinterlace(playbackSettings.Deinterlace)
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
.WithFilterComplex();
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder = builder
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
.WithFilterComplex();
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -121,7 +134,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
||||
.WithInfiniteLoop()
|
||||
.WithConcat($"{scheme}://{host}/ffmpeg/concat/{channel.Number}")
|
||||
.WithConcat($"http://localhost:8409/ffmpeg/concat/{channel.Number}")
|
||||
.WithMetadata(channel)
|
||||
.WithFormat("mpegts")
|
||||
.WithPipe()
|
||||
|
||||
69
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
Normal file
69
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
public class FFmpegStreamSelector : IFFmpegStreamSelector
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly ILogger<FFmpegStreamSelector> _logger;
|
||||
|
||||
public FFmpegStreamSelector(
|
||||
ILogger<FFmpegStreamSelector> logger,
|
||||
IConfigElementRepository configElementRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_configElementRepository = configElementRepository;
|
||||
}
|
||||
|
||||
public Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version) =>
|
||||
version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Video).AsTask();
|
||||
|
||||
public async Task<MediaStream> SelectAudioStream(Channel channel, MediaVersion version)
|
||||
{
|
||||
var audioStreams = version.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Audio).ToList();
|
||||
|
||||
string language = (channel.PreferredLanguageCode ?? string.Empty).ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(language))
|
||||
{
|
||||
_logger.LogDebug("Channel {Number} has no preferred language code", channel.Number);
|
||||
Option<string> maybeDefaultLanguage = await _configElementRepository.GetValue<string>(
|
||||
ConfigElementKey.FFmpegPreferredLanguageCode);
|
||||
maybeDefaultLanguage.Match(
|
||||
lang => language = lang.ToLowerInvariant(),
|
||||
() =>
|
||||
{
|
||||
_logger.LogDebug("FFmpeg has no preferred language code; falling back to {Code}", "eng");
|
||||
language = "eng";
|
||||
});
|
||||
}
|
||||
|
||||
var correctLanguage = audioStreams.Filter(
|
||||
s => string.Equals(
|
||||
s.Language,
|
||||
language,
|
||||
StringComparison.InvariantCultureIgnoreCase)).ToList();
|
||||
if (correctLanguage.Any())
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Found {Count} audio streams with preferred language code {Code}; selecting stream with most channels",
|
||||
correctLanguage.Count,
|
||||
language);
|
||||
|
||||
return correctLanguage.OrderByDescending(s => s.Channels).Head();
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Unable to find audio stream with preferred language code {Code}; selecting stream with most channels",
|
||||
language);
|
||||
|
||||
return audioStreams.OrderByDescending(s => s.Channels).Head();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ namespace ErsatzTV.Core
|
||||
public static readonly string PlexSecretsPath = Path.Combine(AppDataFolder, "plex-secrets.json");
|
||||
|
||||
public static readonly string FFmpegReportsFolder = Path.Combine(AppDataFolder, "ffmpeg-reports");
|
||||
public static readonly string SearchIndexFolder = Path.Combine(AppDataFolder, "search-index");
|
||||
|
||||
public static readonly string ArtworkCacheFolder = Path.Combine(AppDataFolder, "cache", "artwork");
|
||||
|
||||
|
||||
11
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegStreamSelector.cs
Normal file
11
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegStreamSelector.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.FFmpeg
|
||||
{
|
||||
public interface IFFmpegStreamSelector
|
||||
{
|
||||
Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version);
|
||||
Task<MediaStream> SelectAudioStream(Channel channel, MediaVersion version);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,6 @@ namespace ErsatzTV.Core.Interfaces.Images
|
||||
{
|
||||
Task<Either<BaseError, byte[]>> ResizeImage(byte[] imageBuffer, int height);
|
||||
Task<Either<BaseError, string>> SaveArtworkToCache(byte[] imageBuffer, ArtworkKind artworkKind);
|
||||
string CopyArtworkToCache(string path, ArtworkKind artworkKind);
|
||||
Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalFileSystem
|
||||
{
|
||||
Unit EnsureFolderExists(string folder);
|
||||
DateTime GetLastWriteTime(string path);
|
||||
bool IsLibraryPathAccessible(LibraryPath libraryPath);
|
||||
IEnumerable<string> ListSubdirectories(string folder);
|
||||
IEnumerable<string> ListFiles(string folder);
|
||||
bool FileExists(string path);
|
||||
Task<byte[]> ReadAllBytes(string path);
|
||||
Unit CopyFile(string source, string destination);
|
||||
Task<Either<BaseError, Unit>> CopyFile(string source, string destination);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalMetadataProvider
|
||||
{
|
||||
Task<ShowMetadata> GetMetadataForShow(string showFolder);
|
||||
Task<Unit> RefreshSidecarMetadata(MediaItem mediaItem, string path);
|
||||
Task<Unit> RefreshSidecarMetadata(Show televisionShow, string showFolder);
|
||||
Task<Unit> RefreshFallbackMetadata(MediaItem mediaItem);
|
||||
Task<Unit> RefreshFallbackMetadata(Show televisionShow, string showFolder);
|
||||
Task<bool> RefreshSidecarMetadata(MediaItem mediaItem, string path);
|
||||
Task<bool> RefreshSidecarMetadata(Show televisionShow, string showFolder);
|
||||
Task<bool> RefreshFallbackMetadata(MediaItem mediaItem);
|
||||
Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalStatisticsProvider
|
||||
{
|
||||
Task<Either<BaseError, Unit>> RefreshStatistics(string ffprobePath, MediaItem mediaItem);
|
||||
Task<Either<BaseError, bool>> RefreshStatistics(string ffprobePath, MediaItem mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
@@ -6,6 +7,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface IMovieFolderScanner
|
||||
{
|
||||
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath);
|
||||
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
@@ -6,6 +7,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ITelevisionFolderScanner
|
||||
{
|
||||
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath);
|
||||
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Plex
|
||||
{
|
||||
public interface IPlexPathReplacementService
|
||||
{
|
||||
Task<string> GetReplacementPlexPath(int libraryPathId, string path);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<List<LibraryPath>> GetLocalPaths(int libraryId);
|
||||
Task<Option<LibraryPath>> GetPath(int libraryPathId);
|
||||
Task<int> CountMediaItemsByPath(int libraryPathId);
|
||||
Task<List<int>> GetMediaIdsByLocalPath(int libraryPathId);
|
||||
Task DeleteLocalPath(int libraryPathId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
{
|
||||
Task<Option<MediaItem>> Get(int id);
|
||||
Task<List<MediaItem>> GetAll();
|
||||
Task<List<MediaItem>> Search(string searchString);
|
||||
Task<bool> Update(MediaItem mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,23 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<List<PlexPathReplacement>> GetPlexPathReplacementsByLibraryId(int plexLibraryPathId);
|
||||
Task<int> CountMediaItems(int id);
|
||||
Task Update(LocalMediaSource localMediaSource);
|
||||
Task Update(PlexMediaSource plexMediaSource);
|
||||
Task Update(PlexMediaSource plexMediaSource, List<PlexConnection> toAdd, List<PlexConnection> toDelete);
|
||||
|
||||
Task<Unit> UpdateLibraries(
|
||||
int plexMediaSourceId,
|
||||
List<PlexLibrary> toAdd,
|
||||
List<PlexLibrary> toDelete);
|
||||
|
||||
Task<Unit> UpdatePathReplacements(
|
||||
int plexMediaSourceId,
|
||||
List<PlexPathReplacement> toAdd,
|
||||
List<PlexPathReplacement> toUpdate,
|
||||
List<PlexPathReplacement> toDelete);
|
||||
|
||||
Task Update(PlexLibrary plexMediaSourceLibrary);
|
||||
Task Delete(int mediaSourceId);
|
||||
Task<Unit> DeleteAllPlex();
|
||||
Task DisablePlexLibrarySync(List<int> libraryIds);
|
||||
Task<List<int>> DeleteAllPlex();
|
||||
Task<List<int>> DisablePlexLibrarySync(List<int> libraryIds);
|
||||
Task EnablePlexLibrarySync(IEnumerable<int> libraryIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
@@ -6,10 +7,18 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
{
|
||||
public interface IMetadataRepository
|
||||
{
|
||||
Task<Unit> RemoveGenre(Genre genre);
|
||||
Task<Unit> UpdateStatistics(MediaVersion mediaVersion);
|
||||
Task<bool> RemoveGenre(Genre genre);
|
||||
Task<bool> RemoveTag(Tag tag);
|
||||
Task<bool> RemoveStudio(Studio studio);
|
||||
Task<bool> Update(Domain.Metadata metadata);
|
||||
Task<bool> Add(Domain.Metadata metadata);
|
||||
Task<bool> UpdateLocalStatistics(int mediaVersionId, MediaVersion incoming, bool updateVersion = true);
|
||||
Task<bool> UpdatePlexStatistics(int mediaVersionId, MediaVersion incoming);
|
||||
Task<Unit> UpdateArtworkPath(Artwork artwork);
|
||||
Task<Unit> AddArtwork(Domain.Metadata metadata, Artwork artwork);
|
||||
Task<Unit> RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind);
|
||||
Task<Unit> MarkAsUpdated(ShowMetadata metadata, DateTime dateUpdated);
|
||||
Task<Unit> MarkAsUpdated(SeasonMetadata metadata, DateTime dateUpdated);
|
||||
Task<Unit> MarkAsUpdated(MovieMetadata metadata, DateTime dateUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
@@ -9,14 +10,17 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
{
|
||||
Task<bool> AllMoviesExist(List<int> movieIds);
|
||||
Task<Option<Movie>> GetMovie(int movieId);
|
||||
Task<Either<BaseError, Movie>> GetOrAdd(LibraryPath libraryPath, string path);
|
||||
Task<Either<BaseError, PlexMovie>> GetOrAdd(PlexLibrary library, PlexMovie item);
|
||||
Task<bool> Update(Movie movie);
|
||||
Task<Either<BaseError, MediaItemScanResult<Movie>>> GetOrAdd(LibraryPath libraryPath, string path);
|
||||
Task<Either<BaseError, MediaItemScanResult<PlexMovie>>> GetOrAdd(PlexLibrary library, PlexMovie item);
|
||||
Task<int> GetMovieCount();
|
||||
Task<List<MovieMetadata>> GetPagedMovies(int pageNumber, int pageSize);
|
||||
Task<List<MovieMetadata>> GetMoviesForCards(List<int> ids);
|
||||
Task<IEnumerable<string>> FindMoviePaths(LibraryPath libraryPath);
|
||||
Task<Unit> DeleteByPath(LibraryPath libraryPath, string path);
|
||||
Task<Unit> AddGenre(MovieMetadata metadata, Genre genre);
|
||||
Task<Unit> RemoveMissingPlexMovies(PlexLibrary library, List<string> movieKeys);
|
||||
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
|
||||
Task<bool> AddGenre(MovieMetadata metadata, Genre genre);
|
||||
Task<bool> AddTag(MovieMetadata metadata, Tag tag);
|
||||
Task<bool> AddStudio(MovieMetadata metadata, Studio studio);
|
||||
Task<List<int>> RemoveMissingPlexMovies(PlexLibrary library, List<string> movieKeys);
|
||||
Task<bool> UpdateSortTitle(MovieMetadata movieMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
{
|
||||
public interface ISearchRepository
|
||||
{
|
||||
public Task<List<int>> GetItemIdsToIndex();
|
||||
public Task<Option<MediaItem>> GetItemToIndex(int id);
|
||||
public Task<List<MediaItem>> SearchMediaItemsByTitle(string query);
|
||||
public Task<List<MediaItem>> SearchMediaItemsByGenre(string genre);
|
||||
public Task<List<MediaItem>> SearchMediaItemsByTag(string tag);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
@@ -8,13 +9,11 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
public interface ITelevisionRepository
|
||||
{
|
||||
Task<bool> AllShowsExist(List<int> showIds);
|
||||
Task<bool> Update(Show show);
|
||||
Task<bool> Update(Season season);
|
||||
Task<bool> Update(Episode episode);
|
||||
Task<List<Show>> GetAllShows();
|
||||
Task<Option<Show>> GetShow(int showId);
|
||||
Task<int> GetShowCount();
|
||||
Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize);
|
||||
Task<List<ShowMetadata>> GetShowsForCards(List<int> ids);
|
||||
Task<List<Episode>> GetShowItems(int showId);
|
||||
Task<List<Season>> GetAllSeasons();
|
||||
Task<Option<Season>> GetSeason(int seasonId);
|
||||
@@ -25,19 +24,27 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<int> GetEpisodeCount(int seasonId);
|
||||
Task<List<EpisodeMetadata>> GetPagedEpisodes(int seasonId, int pageNumber, int pageSize);
|
||||
Task<Option<Show>> GetShowByMetadata(int libraryPathId, ShowMetadata metadata);
|
||||
Task<Either<BaseError, Show>> AddShow(int libraryPathId, string showFolder, ShowMetadata metadata);
|
||||
|
||||
Task<Either<BaseError, MediaItemScanResult<Show>>> AddShow(
|
||||
int libraryPathId,
|
||||
string showFolder,
|
||||
ShowMetadata metadata);
|
||||
|
||||
Task<Either<BaseError, Season>> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber);
|
||||
Task<Either<BaseError, Episode>> GetOrAddEpisode(Season season, LibraryPath libraryPath, string path);
|
||||
Task<IEnumerable<string>> FindEpisodePaths(LibraryPath libraryPath);
|
||||
Task<Unit> DeleteByPath(LibraryPath libraryPath, string path);
|
||||
Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath);
|
||||
Task<Unit> DeleteEmptyShows(LibraryPath libraryPath);
|
||||
Task<Either<BaseError, PlexShow>> GetOrAddPlexShow(PlexLibrary library, PlexShow item);
|
||||
Task<List<int>> DeleteEmptyShows(LibraryPath libraryPath);
|
||||
Task<Either<BaseError, MediaItemScanResult<PlexShow>>> GetOrAddPlexShow(PlexLibrary library, PlexShow item);
|
||||
Task<Either<BaseError, PlexSeason>> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item);
|
||||
Task<Either<BaseError, PlexEpisode>> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item);
|
||||
Task<Unit> AddGenre(ShowMetadata metadata, Genre genre);
|
||||
Task<Unit> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys);
|
||||
Task<bool> AddGenre(ShowMetadata metadata, Genre genre);
|
||||
Task<bool> AddTag(ShowMetadata metadata, Tag tag);
|
||||
Task<bool> AddStudio(ShowMetadata metadata, Studio studio);
|
||||
Task<List<int>> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys);
|
||||
Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys);
|
||||
Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys);
|
||||
Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber);
|
||||
}
|
||||
}
|
||||
|
||||
11
ErsatzTV.Core/Interfaces/Runtime/IRuntimeInfo.cs
Normal file
11
ErsatzTV.Core/Interfaces/Runtime/IRuntimeInfo.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Runtime
|
||||
{
|
||||
public interface IRuntimeInfo
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
bool IsOSPlatform(OSPlatform osPlatform);
|
||||
}
|
||||
}
|
||||
19
ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs
Normal file
19
ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Search
|
||||
{
|
||||
public interface ISearchIndex
|
||||
{
|
||||
public int Version { get; }
|
||||
Task<bool> Initialize();
|
||||
Task<Unit> Rebuild(List<int> itemIds);
|
||||
Task<Unit> AddItems(List<MediaItem> items);
|
||||
Task<Unit> UpdateItems(List<MediaItem> items);
|
||||
Task<Unit> RemoveItems(List<int> ids);
|
||||
Task<SearchResult> Search(string query, int skip, int limit, string searchField = "");
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ namespace ErsatzTV.Core.Iptv
|
||||
xml.WriteStartElement("tv");
|
||||
xml.WriteAttributeString("generator-info-name", "ersatztv");
|
||||
|
||||
foreach (Channel channel in _channels.OrderBy(c => c.Number))
|
||||
foreach (Channel channel in _channels.OrderBy(c => decimal.Parse(c.Number)))
|
||||
{
|
||||
xml.WriteStartElement("channel");
|
||||
xml.WriteAttributeString("id", channel.Number);
|
||||
@@ -48,7 +48,7 @@ namespace ErsatzTV.Core.Iptv
|
||||
.HeadOrNone()
|
||||
.Match(
|
||||
artwork => $"{_scheme}://{_host}/iptv/logos/{artwork.Path}",
|
||||
() => $"{_scheme}://{_host}/images/ersatztv-500.png");
|
||||
() => $"{_scheme}://{_host}/iptv/images/ersatztv-500.png");
|
||||
xml.WriteAttributeString("src", logo);
|
||||
xml.WriteEndElement(); // icon
|
||||
|
||||
@@ -57,49 +57,36 @@ namespace ErsatzTV.Core.Iptv
|
||||
|
||||
foreach (Channel channel in _channels.OrderBy(c => c.Number))
|
||||
{
|
||||
foreach (PlayoutItem playoutItem in channel.Playouts.Collect(p => p.Items).OrderBy(i => i.Start))
|
||||
var sorted = channel.Playouts.Collect(p => p.Items).OrderBy(x => x.Start).ToList();
|
||||
var i = 0;
|
||||
while (i < sorted.Count)
|
||||
{
|
||||
string start = playoutItem.StartOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
|
||||
string stop = playoutItem.FinishOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
|
||||
PlayoutItem startItem = sorted[i];
|
||||
bool hasCustomTitle = !string.IsNullOrWhiteSpace(startItem.CustomTitle);
|
||||
|
||||
string title = playoutItem.MediaItem switch
|
||||
int finishIndex = i;
|
||||
while (hasCustomTitle && finishIndex + 1 < sorted.Count && sorted[finishIndex + 1].CustomGroup)
|
||||
{
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title ?? string.Empty)
|
||||
.IfNone("[unknown movie]"),
|
||||
Episode e => e.Season.Show.ShowMetadata.HeadOrNone().Map(em => em.Title ?? string.Empty)
|
||||
.IfNone("[unknown show]"),
|
||||
_ => "[unknown]"
|
||||
};
|
||||
finishIndex++;
|
||||
}
|
||||
|
||||
string subtitle = playoutItem.MediaItem switch
|
||||
{
|
||||
Episode e => e.EpisodeMetadata.HeadOrNone().Match(
|
||||
em => em.Title ?? string.Empty,
|
||||
() => string.Empty),
|
||||
_ => string.Empty
|
||||
};
|
||||
PlayoutItem finishItem = sorted[finishIndex];
|
||||
i = finishIndex;
|
||||
|
||||
string description = playoutItem.MediaItem switch
|
||||
{
|
||||
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),
|
||||
_ => string.Empty
|
||||
};
|
||||
string start = startItem.StartOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
|
||||
string stop = finishItem.FinishOffset.ToString("yyyyMMddHHmmss zzz").Replace(":", string.Empty);
|
||||
|
||||
string contentRating = playoutItem.MediaItem switch
|
||||
{
|
||||
// TODO: re-implement content rating
|
||||
// Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.ContentRating).IfNone(string.Empty),
|
||||
_ => string.Empty
|
||||
};
|
||||
string title = GetTitle(startItem);
|
||||
string subtitle = GetSubtitle(startItem);
|
||||
string description = GetDescription(startItem);
|
||||
string contentRating = string.Empty;
|
||||
|
||||
xml.WriteStartElement("programme");
|
||||
xml.WriteAttributeString("start", start);
|
||||
xml.WriteAttributeString("stop", stop);
|
||||
xml.WriteAttributeString("channel", channel.Number);
|
||||
|
||||
if (playoutItem.MediaItem is Movie movie)
|
||||
if (!hasCustomTitle && startItem.MediaItem is Movie movie)
|
||||
{
|
||||
xml.WriteStartElement("category");
|
||||
xml.WriteAttributeString("lang", "en");
|
||||
@@ -122,7 +109,7 @@ namespace ErsatzTV.Core.Iptv
|
||||
.Filter(a => a.ArtworkKind == ArtworkKind.Poster)
|
||||
.HeadOrNone()
|
||||
.Match(
|
||||
artwork => $"{_scheme}://{_host}/artwork/posters/{artwork.Path}",
|
||||
artwork => $"{_scheme}://{_host}/iptv/artwork/posters/{artwork.Path}",
|
||||
() => string.Empty);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(poster))
|
||||
@@ -150,7 +137,7 @@ namespace ErsatzTV.Core.Iptv
|
||||
xml.WriteStartElement("previously-shown");
|
||||
xml.WriteEndElement(); // previously-shown
|
||||
|
||||
if (playoutItem.MediaItem is Episode episode)
|
||||
if (!hasCustomTitle && startItem.MediaItem is Episode episode)
|
||||
{
|
||||
Option<ShowMetadata> maybeMetadata =
|
||||
Optional(episode.Season?.Show?.ShowMetadata.HeadOrNone()).Flatten();
|
||||
@@ -161,7 +148,7 @@ namespace ErsatzTV.Core.Iptv
|
||||
.Filter(a => a.ArtworkKind == ArtworkKind.Poster)
|
||||
.HeadOrNone()
|
||||
.Match(
|
||||
artwork => $"{_scheme}://{_host}/artwork/posters/{artwork.Path}",
|
||||
artwork => $"{_scheme}://{_host}/iptv/artwork/posters/{artwork.Path}",
|
||||
() => string.Empty);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(poster))
|
||||
@@ -209,6 +196,8 @@ namespace ErsatzTV.Core.Iptv
|
||||
}
|
||||
|
||||
xml.WriteEndElement(); // programme
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,5 +207,54 @@ namespace ErsatzTV.Core.Iptv
|
||||
xml.Flush();
|
||||
return Encoding.UTF8.GetString(ms.ToArray());
|
||||
}
|
||||
|
||||
private static string GetTitle(PlayoutItem playoutItem)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))
|
||||
{
|
||||
return playoutItem.CustomTitle;
|
||||
}
|
||||
|
||||
return playoutItem.MediaItem switch
|
||||
{
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title ?? string.Empty)
|
||||
.IfNone("[unknown movie]"),
|
||||
Episode e => e.Season.Show.ShowMetadata.HeadOrNone().Map(em => em.Title ?? string.Empty)
|
||||
.IfNone("[unknown show]"),
|
||||
_ => "[unknown]"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetSubtitle(PlayoutItem playoutItem)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return playoutItem.MediaItem switch
|
||||
{
|
||||
Episode e => e.EpisodeMetadata.HeadOrNone().Match(
|
||||
em => em.Title ?? string.Empty,
|
||||
() => string.Empty),
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetDescription(PlayoutItem playoutItem)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return playoutItem.MediaItem switch
|
||||
{
|
||||
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),
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace ErsatzTV.Core.Iptv
|
||||
.HeadOrNone()
|
||||
.Match(
|
||||
artwork => $"{_scheme}://{_host}/iptv/logos/{artwork.Path}",
|
||||
() => $"{_scheme}://{_host}/images/ersatztv-500.png");
|
||||
() => $"{_scheme}://{_host}/iptv/images/ersatztv-500.png");
|
||||
|
||||
string shortUniqueId = Convert.ToBase64String(channel.UniqueId.ToByteArray())
|
||||
.TrimEnd('=')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using ErsatzTV.Core.Domain;
|
||||
@@ -22,7 +23,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
string path = episode.MediaVersions.Head().MediaFiles.Head().Path;
|
||||
string fileName = Path.GetFileName(path);
|
||||
var metadata = new EpisodeMetadata
|
||||
{ MetadataKind = MetadataKind.Fallback, Title = fileName ?? path };
|
||||
{ MetadataKind = MetadataKind.Fallback, Title = fileName ?? path, DateAdded = DateTime.UtcNow };
|
||||
return fileName != null ? GetEpisodeMetadata(fileName, metadata) : Tuple(metadata, 0);
|
||||
}
|
||||
|
||||
@@ -47,6 +48,16 @@ namespace ErsatzTV.Core.Metadata
|
||||
return title.Substring(4);
|
||||
}
|
||||
|
||||
if (title.StartsWith("a ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return title.Substring(2);
|
||||
}
|
||||
|
||||
if (title.StartsWith("an ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return title.Substring(3);
|
||||
}
|
||||
|
||||
if (title.StartsWith("Æ"))
|
||||
{
|
||||
return title.Replace("Æ", "E");
|
||||
@@ -64,6 +75,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
if (match.Success)
|
||||
{
|
||||
metadata.Title = match.Groups[1].Value;
|
||||
metadata.DateUpdated = DateTime.UtcNow;
|
||||
return Tuple(metadata, int.Parse(match.Groups[3].Value));
|
||||
}
|
||||
}
|
||||
@@ -86,6 +98,10 @@ namespace ErsatzTV.Core.Metadata
|
||||
metadata.Title = match.Groups[1].Value;
|
||||
metadata.Year = int.Parse(match.Groups[2].Value);
|
||||
metadata.ReleaseDate = new DateTime(int.Parse(match.Groups[2].Value), 1, 1);
|
||||
metadata.Genres = new List<Genre>();
|
||||
metadata.Tags = new List<Tag>();
|
||||
metadata.Studios = new List<Studio>();
|
||||
metadata.DateUpdated = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -107,6 +123,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
metadata.Title = match.Groups[1].Value;
|
||||
metadata.Year = int.Parse(match.Groups[2].Value);
|
||||
metadata.ReleaseDate = new DateTime(int.Parse(match.Groups[2].Value), 1, 1);
|
||||
metadata.DateUpdated = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
|
||||
@@ -11,6 +11,16 @@ namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public class LocalFileSystem : ILocalFileSystem
|
||||
{
|
||||
public Unit EnsureFolderExists(string folder)
|
||||
{
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
public DateTime GetLastWriteTime(string path) =>
|
||||
Try(File.GetLastWriteTimeUtc(path)).IfFail(() => DateTime.MinValue);
|
||||
|
||||
@@ -26,17 +36,26 @@ namespace ErsatzTV.Core.Metadata
|
||||
public bool FileExists(string path) => File.Exists(path);
|
||||
public Task<byte[]> ReadAllBytes(string path) => File.ReadAllBytesAsync(path);
|
||||
|
||||
public Unit CopyFile(string source, string destination)
|
||||
public async Task<Either<BaseError, Unit>> CopyFile(string source, string destination)
|
||||
{
|
||||
string directory = Path.GetDirectoryName(destination) ?? string.Empty;
|
||||
if (!Directory.Exists(directory))
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
string directory = Path.GetDirectoryName(destination) ?? string.Empty;
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
await using FileStream sourceStream = File.OpenRead(source);
|
||||
await using FileStream destinationStream = File.Create(destination);
|
||||
await sourceStream.CopyToAsync(destinationStream);
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
|
||||
File.Copy(source, destination, true);
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public abstract class LocalFolderScanner
|
||||
{
|
||||
private static readonly SHA1CryptoServiceProvider Crypto;
|
||||
|
||||
public static readonly List<string> VideoFileExtensions = new()
|
||||
{
|
||||
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4",
|
||||
@@ -48,27 +45,30 @@ namespace ErsatzTV.Core.Metadata
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
static LocalFolderScanner() => Crypto = new SHA1CryptoServiceProvider();
|
||||
private readonly IMetadataRepository _metadataRepository;
|
||||
|
||||
protected LocalFolderScanner(
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
IMetadataRepository metadataRepository,
|
||||
IImageCache imageCache,
|
||||
ILogger logger)
|
||||
{
|
||||
_localFileSystem = localFileSystem;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_metadataRepository = metadataRepository;
|
||||
_imageCache = imageCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected async Task<Either<BaseError, T>> UpdateStatistics<T>(T mediaItem, string ffprobePath)
|
||||
protected async Task<Either<BaseError, MediaItemScanResult<T>>> UpdateStatistics<T>(
|
||||
MediaItemScanResult<T> mediaItem,
|
||||
string ffprobePath)
|
||||
where T : MediaItem
|
||||
{
|
||||
try
|
||||
{
|
||||
MediaVersion version = mediaItem switch
|
||||
MediaVersion version = mediaItem.Item switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
@@ -77,12 +77,19 @@ namespace ErsatzTV.Core.Metadata
|
||||
|
||||
string path = version.MediaFiles.Head().Path;
|
||||
|
||||
if (version.DateUpdated < _localFileSystem.GetLastWriteTime(path))
|
||||
if (version.DateUpdated < _localFileSystem.GetLastWriteTime(path) || !version.Streams.Any())
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", path);
|
||||
Either<BaseError, Unit> refreshResult =
|
||||
await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
|
||||
refreshResult.IfLeft(
|
||||
Either<BaseError, bool> refreshResult =
|
||||
await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem.Item);
|
||||
refreshResult.Match(
|
||||
result =>
|
||||
{
|
||||
if (result)
|
||||
{
|
||||
mediaItem.IsUpdated = true;
|
||||
}
|
||||
},
|
||||
error =>
|
||||
_logger.LogWarning(
|
||||
"Unable to refresh {Attribute} for media item {Path}. Error: {Error}",
|
||||
@@ -99,44 +106,61 @@ namespace ErsatzTV.Core.Metadata
|
||||
}
|
||||
}
|
||||
|
||||
protected bool RefreshArtwork(string artworkFile, Domain.Metadata metadata, ArtworkKind artworkKind)
|
||||
protected async Task<bool> RefreshArtwork(string artworkFile, Domain.Metadata metadata, ArtworkKind artworkKind)
|
||||
{
|
||||
DateTime lastWriteTime = _localFileSystem.GetLastWriteTime(artworkFile);
|
||||
|
||||
metadata.Artwork ??= new List<Artwork>();
|
||||
|
||||
Option<Artwork> maybeArtwork =
|
||||
Optional(metadata.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == artworkKind);
|
||||
Option<Artwork> maybeArtwork = metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind);
|
||||
|
||||
bool shouldRefresh = maybeArtwork.Match(
|
||||
artwork => artwork.DateUpdated < lastWriteTime,
|
||||
artwork => lastWriteTime.Subtract(artwork.DateUpdated) > TimeSpan.FromSeconds(1),
|
||||
true);
|
||||
|
||||
if (shouldRefresh)
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile);
|
||||
string cacheName = _imageCache.CopyArtworkToCache(artworkFile, artworkKind);
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile);
|
||||
Either<BaseError, string> maybeCacheName =
|
||||
await _imageCache.CopyArtworkToCache(artworkFile, artworkKind);
|
||||
|
||||
maybeArtwork.Match(
|
||||
artwork =>
|
||||
{
|
||||
artwork.Path = cacheName;
|
||||
artwork.DateUpdated = lastWriteTime;
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var artwork = new Artwork
|
||||
return await maybeCacheName.Match(
|
||||
async cacheName =>
|
||||
{
|
||||
Path = cacheName,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = lastWriteTime,
|
||||
ArtworkKind = artworkKind
|
||||
};
|
||||
await maybeArtwork.Match(
|
||||
async artwork =>
|
||||
{
|
||||
artwork.Path = cacheName;
|
||||
artwork.DateUpdated = lastWriteTime;
|
||||
await _metadataRepository.UpdateArtworkPath(artwork);
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
var artwork = new Artwork
|
||||
{
|
||||
Path = cacheName,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = lastWriteTime,
|
||||
ArtworkKind = artworkKind
|
||||
};
|
||||
metadata.Artwork.Add(artwork);
|
||||
await _metadataRepository.AddArtwork(metadata, artwork);
|
||||
});
|
||||
|
||||
metadata.Artwork.Add(artwork);
|
||||
});
|
||||
|
||||
return true;
|
||||
return true;
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogDebug("Failed to cache artwork from {Path}: {Error}", artworkFile, error.Value);
|
||||
return Task.FromResult(false);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error refreshing artwork");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user