Compare commits
8 Commits
v0.0.5-pre
...
v0.0.8-pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aac2f13c9 | ||
|
|
2c9d4d796a | ||
|
|
9d40caebd6 | ||
|
|
0b5a6f9dcd | ||
|
|
76495c1f7b | ||
|
|
d0d1186b92 | ||
|
|
04ab4ee60f | ||
|
|
e62074cc26 |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -42,8 +42,8 @@ jobs:
|
||||
release_name_cli="ErsatzTV.CommandLine-$tag-${{ matrix.target }}"
|
||||
|
||||
# Build everything
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name"
|
||||
dotnet publish ErsatzTV.CommandLine/ErsatzTV.CommandLine.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name_cli"
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}"
|
||||
dotnet publish ErsatzTV.CommandLine/ErsatzTV.CommandLine.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name_cli" /property:InformationalVersion="${tag:1}-${{ matrix.target }}"
|
||||
|
||||
# Pack files
|
||||
if [ "${{ matrix.target }}" == "win-x64" ]; then
|
||||
|
||||
5
Directory.Build.props
Normal file
5
Directory.Build.props
Normal file
@@ -0,0 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<InformationalVersion>develop</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -26,7 +26,8 @@ COPY ErsatzTV.Core/. ./ErsatzTV.Core/
|
||||
COPY ErsatzTV.Core.Tests/. ./ErsatzTV.Core.Tests/
|
||||
COPY ErsatzTV.Infrastructure/. ./ErsatzTV.Infrastructure/
|
||||
WORKDIR /source/ErsatzTV
|
||||
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore
|
||||
ARG INFO_VERSION="unknown"
|
||||
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:InformationalVersion=${INFO_VERSION}
|
||||
|
||||
# final stage/image
|
||||
FROM runtime-base
|
||||
|
||||
9
ErsatzTV.Application/IMediaCard.cs
Normal file
9
ErsatzTV.Application/IMediaCard.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Application
|
||||
{
|
||||
public interface IMediaCard
|
||||
{
|
||||
string Title { get; }
|
||||
string SortTitle { get; }
|
||||
string Subtitle { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
@@ -11,33 +9,12 @@ namespace ErsatzTV.Application.Images.Commands
|
||||
{
|
||||
public class SaveImageToDiskHandler : IRequestHandler<SaveImageToDisk, Either<BaseError, string>>
|
||||
{
|
||||
private static readonly SHA1CryptoServiceProvider Crypto;
|
||||
private readonly IImageCache _imageCache;
|
||||
|
||||
static SaveImageToDiskHandler() => Crypto = new SHA1CryptoServiceProvider();
|
||||
public SaveImageToDiskHandler(IImageCache imageCache) => _imageCache = imageCache;
|
||||
|
||||
public async Task<Either<BaseError, string>> Handle(
|
||||
public Task<Either<BaseError, string>> Handle(
|
||||
SaveImageToDisk request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] hash = Crypto.ComputeHash(request.Buffer);
|
||||
string hex = BitConverter.ToString(hash).Replace("-", string.Empty);
|
||||
|
||||
string fileName = Path.Combine(FileSystemLayout.ImageCacheFolder, hex);
|
||||
|
||||
if (!Directory.Exists(FileSystemLayout.ImageCacheFolder))
|
||||
{
|
||||
Directory.CreateDirectory(FileSystemLayout.ImageCacheFolder);
|
||||
}
|
||||
|
||||
await File.WriteAllBytesAsync(fileName, request.Buffer, cancellationToken);
|
||||
return hex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
CancellationToken cancellationToken) => _imageCache.SaveImage(request.Buffer);
|
||||
}
|
||||
}
|
||||
|
||||
12
ErsatzTV.Application/Logs/LogEntryViewModel.cs
Normal file
12
ErsatzTV.Application/Logs/LogEntryViewModel.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace ErsatzTV.Application.Logs
|
||||
{
|
||||
public record LogEntryViewModel(
|
||||
int Id,
|
||||
DateTime Timestamp,
|
||||
string Level,
|
||||
string Exception,
|
||||
string RenderedMessage,
|
||||
string Properties);
|
||||
}
|
||||
16
ErsatzTV.Application/Logs/Mapper.cs
Normal file
16
ErsatzTV.Application/Logs/Mapper.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Logs
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static LogEntryViewModel ProjectToViewModel(LogEntry logEntry) =>
|
||||
new(
|
||||
logEntry.Id,
|
||||
logEntry.Timestamp,
|
||||
logEntry.Level,
|
||||
logEntry.Exception,
|
||||
logEntry.RenderedMessage,
|
||||
logEntry.Properties);
|
||||
}
|
||||
}
|
||||
7
ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs
Normal file
7
ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Logs.Queries
|
||||
{
|
||||
public record GetRecentLogEntries : IRequest<List<LogEntryViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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.Logs.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Logs.Queries
|
||||
{
|
||||
public class GetRecentLogEntriesHandler : IRequestHandler<GetRecentLogEntries, List<LogEntryViewModel>>
|
||||
{
|
||||
private readonly ILogRepository _logRepository;
|
||||
|
||||
public GetRecentLogEntriesHandler(ILogRepository logRepository) => _logRepository = logRepository;
|
||||
|
||||
public Task<List<LogEntryViewModel>> Handle(GetRecentLogEntries request, CancellationToken cancellationToken) =>
|
||||
_logRepository.GetRecentLogEntries().Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
public record AggregateMediaItemResults(int Count, List<AggregateMediaItemViewModel> DataPage);
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
public record AggregateMediaItemViewModel(string Source, string Title, int Count, string Duration);
|
||||
public record AggregateMediaItemViewModel(
|
||||
int MediaItemId,
|
||||
string Title,
|
||||
string Subtitle,
|
||||
string SortTitle,
|
||||
string Poster);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly ILocalMetadataProvider _localMetadataProvider;
|
||||
private readonly ILocalPosterProvider _localPosterProvider;
|
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
@@ -27,7 +28,8 @@ namespace ErsatzTV.Application.MediaItems.Commands
|
||||
IConfigElementRepository configElementRepository,
|
||||
ISmartCollectionBuilder smartCollectionBuilder,
|
||||
ILocalMetadataProvider localMetadataProvider,
|
||||
ILocalStatisticsProvider localStatisticsProvider)
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalPosterProvider localPosterProvider)
|
||||
{
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
@@ -35,6 +37,7 @@ namespace ErsatzTV.Application.MediaItems.Commands
|
||||
_smartCollectionBuilder = smartCollectionBuilder;
|
||||
_localMetadataProvider = localMetadataProvider;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_localPosterProvider = localPosterProvider;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, MediaItemViewModel>> Handle(
|
||||
@@ -50,6 +53,7 @@ namespace ErsatzTV.Application.MediaItems.Commands
|
||||
|
||||
await _localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem);
|
||||
await _localMetadataProvider.RefreshMetadata(parameters.MediaItem);
|
||||
await _localPosterProvider.RefreshPoster(parameters.MediaItem);
|
||||
await _smartCollectionBuilder.RefreshSmartCollections(parameters.MediaItem);
|
||||
|
||||
return ProjectToViewModel(parameters.MediaItem);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public record RefreshMediaItemPoster : RefreshMediaItem
|
||||
{
|
||||
public RefreshMediaItemPoster(int mediaItemId) : base(mediaItemId)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Commands
|
||||
{
|
||||
public class
|
||||
RefreshMediaItemPosterHandler : MediatR.IRequestHandler<RefreshMediaItemPoster,
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ILocalPosterProvider _localPosterProvider;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public RefreshMediaItemPosterHandler(
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
ILocalPosterProvider localPosterProvider)
|
||||
{
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_localPosterProvider = localPosterProvider;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
RefreshMediaItemPoster request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(RefreshPoster)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<Validation<BaseError, MediaItem>> Validate(RefreshMediaItemPoster request) =>
|
||||
MediaItemMustExist(request);
|
||||
|
||||
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(RefreshMediaItemPoster request) =>
|
||||
_mediaItemRepository.Get(request.MediaItemId)
|
||||
.Map(
|
||||
maybeItem => maybeItem.ToValidation<BaseError>(
|
||||
$"[MediaItem] {request.MediaItemId} does not exist."));
|
||||
|
||||
private Task<Unit> RefreshPoster(MediaItem mediaItem) =>
|
||||
_localPosterProvider.RefreshPoster(mediaItem).ToUnit();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace ErsatzTV.Application.MediaItems
|
||||
internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) =>
|
||||
new(
|
||||
mediaItem.Id,
|
||||
mediaItem.Source.Name,
|
||||
GetSourceName(mediaItem.Source),
|
||||
mediaItem.Metadata.MediaType.ToString(),
|
||||
GetDisplayTitle(mediaItem),
|
||||
GetDisplayDuration(mediaItem));
|
||||
@@ -31,5 +31,12 @@ namespace ErsatzTV.Application.MediaItems
|
||||
string.Format(
|
||||
mediaItem.Metadata.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
||||
mediaItem.Metadata.Duration);
|
||||
|
||||
private static string GetSourceName(MediaSource source) =>
|
||||
source switch
|
||||
{
|
||||
LocalMediaSource lms => lms.Folder,
|
||||
_ => source.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public record GetAggregateMediaItems
|
||||
(MediaType MediaType, string SearchString) : IRequest<List<AggregateMediaItemViewModel>>;
|
||||
(MediaType MediaType, int PageNumber, int PageSize) : IRequest<AggregateMediaItemResults>;
|
||||
}
|
||||
|
||||
@@ -1,46 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.AggregateModels;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class
|
||||
GetAggregateMediaItemsHandler : IRequestHandler<GetAggregateMediaItems, List<AggregateMediaItemViewModel>>
|
||||
GetAggregateMediaItemsHandler : IRequestHandler<GetAggregateMediaItems, AggregateMediaItemResults>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public GetAggregateMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public async Task<List<AggregateMediaItemViewModel>> Handle(
|
||||
public async Task<AggregateMediaItemResults> Handle(
|
||||
GetAggregateMediaItems request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IEnumerable<MediaItem> allItems = await _mediaItemRepository.GetAll(request.MediaType);
|
||||
int count = await _mediaItemRepository.GetCountByType(request.MediaType);
|
||||
|
||||
if (!string.IsNullOrEmpty(request.SearchString))
|
||||
{
|
||||
allItems = allItems.Filter(
|
||||
i => i.Metadata?.Title.Contains(request.SearchString, StringComparison.OrdinalIgnoreCase) ==
|
||||
true);
|
||||
}
|
||||
IEnumerable<MediaItemSummary> allItems = await _mediaItemRepository.GetPageByType(
|
||||
request.MediaType,
|
||||
request.PageNumber,
|
||||
request.PageSize);
|
||||
|
||||
return allItems.GroupBy(c => new { c.Source.Name, c.Metadata.Title }).Map(
|
||||
group => new AggregateMediaItemViewModel(
|
||||
group.Key.Name,
|
||||
group.Key.Title,
|
||||
group.Count(),
|
||||
group.Count() == 1 ? DisplayDuration(group.Head()) : string.Empty))
|
||||
var results = allItems
|
||||
.Map(
|
||||
s => new AggregateMediaItemViewModel(
|
||||
s.MediaItemId,
|
||||
s.Title,
|
||||
s.Subtitle,
|
||||
s.SortTitle,
|
||||
s.Poster))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string DisplayDuration(MediaItem mediaItem) => string.Format(
|
||||
mediaItem.Metadata?.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
||||
mediaItem.Metadata?.Duration);
|
||||
return new AggregateMediaItemResults(count, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,28 +41,28 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
Folder = folder
|
||||
});
|
||||
|
||||
private async Task<Validation<BaseError, string>> ValidateName(CreateLocalMediaSource createCollection)
|
||||
private async Task<Validation<BaseError, string>> ValidateName(CreateLocalMediaSource request)
|
||||
{
|
||||
List<string> allNames = await _mediaSourceRepository.GetAll()
|
||||
.Map(list => list.Map(c => c.Name).ToList());
|
||||
|
||||
Validation<BaseError, string> result1 = createCollection.NotEmpty(c => c.Name)
|
||||
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));
|
||||
Validation<BaseError, string> result1 = request.NotEmpty(c => c.Name)
|
||||
.Bind(_ => request.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
var result2 = Optional(createCollection.Name)
|
||||
var result2 = Optional(request.Name)
|
||||
.Filter(name => !allNames.Contains(name))
|
||||
.ToValidation<BaseError>("Media source name must be unique");
|
||||
|
||||
return (result1, result2).Apply((_, _) => createCollection.Name);
|
||||
return (result1, result2).Apply((_, _) => request.Name);
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalMediaSource createCollection)
|
||||
private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalMediaSource request)
|
||||
{
|
||||
List<string> allFolders = await _mediaSourceRepository.GetAll()
|
||||
.Map(list => list.OfType<LocalMediaSource>().Map(c => c.Folder).ToList());
|
||||
|
||||
|
||||
return Optional(createCollection.Folder)
|
||||
return Optional(request.Folder)
|
||||
.Filter(folder => allFolders.ForAll(f => !AreSubPaths(f, folder)))
|
||||
.ToValidation<BaseError>("Folder must not belong to another media source");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Commands
|
||||
{
|
||||
public record ScanLocalMediaSource(int MediaSourceId) : IRequest<Either<BaseError, string>>,
|
||||
public record ScanLocalMediaSource(int MediaSourceId, ScanningMode ScanningMode) :
|
||||
IRequest<Either<BaseError, string>>,
|
||||
IBackgroundServiceRequest;
|
||||
}
|
||||
|
||||
@@ -3,37 +3,52 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Unit = LanguageExt.Unit;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Commands
|
||||
{
|
||||
public class ScanLocalMediaSourceHandler : IRequestHandler<ScanLocalMediaSource, Either<BaseError, string>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILocalMediaScanner _localMediaScanner;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public ScanLocalMediaSourceHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IConfigElementRepository configElementRepository,
|
||||
ILocalMediaScanner localMediaScanner)
|
||||
ILocalMediaScanner localMediaScanner,
|
||||
IEntityLocker entityLocker)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_configElementRepository = configElementRepository;
|
||||
_localMediaScanner = localMediaScanner;
|
||||
_entityLocker = entityLocker;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, string>>
|
||||
Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(
|
||||
p => _localMediaScanner.ScanLocalMediaSource(p.LocalMediaSource, p.FFprobePath)
|
||||
.Map(_ => p.LocalMediaSource.Name))
|
||||
.MapT(parameters => PerformScan(request, parameters).Map(_ => parameters.LocalMediaSource.Folder))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> PerformScan(ScanLocalMediaSource request, RequestParameters parameters)
|
||||
{
|
||||
await _localMediaScanner.ScanLocalMediaSource(
|
||||
parameters.LocalMediaSource,
|
||||
parameters.FFprobePath,
|
||||
request.ScanningMode);
|
||||
|
||||
_entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id);
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(ScanLocalMediaSource request) =>
|
||||
(await LocalMediaSourceMustExist(request), await ValidateFFprobePath())
|
||||
.Apply((localMediaSource, ffprobePath) => new RequestParameters(localMediaSource, ffprobePath));
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize libraries from Plex server {PlexServer}: {Error}",
|
||||
"Unable to synchronize libraries from plex server {PlexServer}: {Error}",
|
||||
connectionParameters.PlexMediaSource.Name,
|
||||
error.Value);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
@@ -10,7 +11,8 @@ namespace ErsatzTV.Application.MediaSources
|
||||
mediaSource switch
|
||||
{
|
||||
LocalMediaSource lms => new LocalMediaSourceViewModel(lms.Id, lms.Name, lms.Folder),
|
||||
PlexMediaSource pms => ProjectToViewModel(pms)
|
||||
PlexMediaSource pms => ProjectToViewModel(pms),
|
||||
_ => throw new NotSupportedException($"Unsupported media source {mediaSource.GetType().Name}")
|
||||
};
|
||||
|
||||
internal static PlexMediaSourceViewModel ProjectToViewModel(PlexMediaSource plexMediaSource) =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
@@ -62,8 +63,14 @@ namespace ErsatzTV.Application.Playouts.Commands
|
||||
|
||||
private async Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(
|
||||
CreatePlayout createPlayout) =>
|
||||
(await _programScheduleRepository.Get(createPlayout.ProgramScheduleId))
|
||||
.ToValidation<BaseError>("ProgramSchedule does not exist.");
|
||||
(await _programScheduleRepository.GetWithPlayouts(createPlayout.ProgramScheduleId))
|
||||
.ToValidation<BaseError>("ProgramSchedule does not exist.")
|
||||
.Bind(ProgramScheduleMustHaveItems);
|
||||
|
||||
private Validation<BaseError, ProgramSchedule> ProgramScheduleMustHaveItems(ProgramSchedule programSchedule) =>
|
||||
Optional(programSchedule)
|
||||
.Filter(ps => ps.Items.Any())
|
||||
.ToValidation<BaseError>("Program schedule must have items");
|
||||
|
||||
private Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType(CreatePlayout createPlayout) =>
|
||||
Optional(createPlayout.ProgramSchedulePlayoutType)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -88,7 +89,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
MediaCollectionId = item.MediaCollectionId,
|
||||
PlayoutDuration = item.PlayoutDuration.GetValueOrDefault(),
|
||||
OfflineTail = item.OfflineTail.GetValueOrDefault()
|
||||
}
|
||||
},
|
||||
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using System;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules
|
||||
{
|
||||
@@ -40,7 +41,9 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
one.Index,
|
||||
one.StartType,
|
||||
one.StartTime,
|
||||
MediaCollections.Mapper.ProjectToViewModel(one.MediaCollection))
|
||||
MediaCollections.Mapper.ProjectToViewModel(one.MediaCollection)),
|
||||
_ => throw new NotSupportedException(
|
||||
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
|
||||
public Task Update(SimpleMediaCollection collection) => throw new NotSupportedException();
|
||||
|
||||
public Task InsertOrIgnore(TelevisionMediaCollection collection) => throw new NotSupportedException();
|
||||
public Task<bool> InsertOrIgnore(TelevisionMediaCollection collection) => throw new NotSupportedException();
|
||||
|
||||
public Task<Unit> ReplaceItems(int collectionId, List<MediaItem> mediaItems) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
4
ErsatzTV.Core/AggregateModels/MediaItemSummary.cs
Normal file
4
ErsatzTV.Core/AggregateModels/MediaItemSummary.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Core.AggregateModels
|
||||
{
|
||||
public record MediaItemSummary(int MediaItemId, string Title, string SortTitle, string Subtitle, string Poster);
|
||||
}
|
||||
12
ErsatzTV.Core/Domain/LogEntry.cs
Normal file
12
ErsatzTV.Core/Domain/LogEntry.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public record LogEntry(
|
||||
int Id,
|
||||
DateTime Timestamp,
|
||||
string Level,
|
||||
string Exception,
|
||||
string RenderedMessage,
|
||||
string Properties);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Domain
|
||||
public int MediaSourceId { get; set; }
|
||||
public MediaSource Source { get; set; }
|
||||
public string Path { get; set; }
|
||||
public string Poster { get; set; }
|
||||
public MediaMetadata Metadata { get; set; }
|
||||
public DateTime? LastWriteTime { get; set; }
|
||||
public IList<SimpleMediaCollection> SimpleMediaCollections { get; set; }
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace ErsatzTV.Core.Domain
|
||||
public string AudioCodec { get; set; }
|
||||
public MediaType MediaType { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string SortTitle { get; set; }
|
||||
public string Subtitle { get; set; }
|
||||
public string Description { get; set; }
|
||||
public int? SeasonNumber { get; set; }
|
||||
|
||||
@@ -285,7 +285,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
bool hasVideoFilters = _videoFilters.Any();
|
||||
if (hasVideoFilters)
|
||||
{
|
||||
(string filter, string finalLabel) = GenerateFilter(_videoFilters, StreamType.Video);
|
||||
(string filter, string finalLabel) = GenerateVideoFilter(_videoFilters);
|
||||
complexFilter.Append(filter);
|
||||
videoLabel = finalLabel;
|
||||
}
|
||||
@@ -297,7 +297,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
complexFilter.Append(';');
|
||||
}
|
||||
|
||||
(string filter, string finalLabel) = GenerateFilter(_audioFilters, StreamType.Audio);
|
||||
(string filter, string finalLabel) = GenerateAudioFilter(_audioFilters);
|
||||
complexFilter.Append(filter);
|
||||
audioLabel = finalLabel;
|
||||
}
|
||||
@@ -348,20 +348,16 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
};
|
||||
}
|
||||
|
||||
private FilterResult GenerateFilter(Queue<string> filterQueue, StreamType streamType)
|
||||
private FilterResult GenerateVideoFilter(Queue<string> filterQueue) =>
|
||||
GenerateFilter(filterQueue, "null", 'v');
|
||||
|
||||
private FilterResult GenerateAudioFilter(Queue<string> filterQueue) =>
|
||||
GenerateFilter(filterQueue, "anull", 'a');
|
||||
|
||||
private static FilterResult GenerateFilter(Queue<string> filterQueue, string nullFilter, char av)
|
||||
{
|
||||
var filter = new StringBuilder();
|
||||
var index = 0;
|
||||
string nullFilter = streamType switch
|
||||
{
|
||||
StreamType.Audio => "anull",
|
||||
StreamType.Video => "null"
|
||||
};
|
||||
char av = streamType switch
|
||||
{
|
||||
StreamType.Audio => 'a',
|
||||
StreamType.Video => 'v'
|
||||
};
|
||||
filter.Append($"[0:{av}]{nullFilter}[{av}{index}]");
|
||||
while (filterQueue.TryDequeue(out string result))
|
||||
{
|
||||
@@ -372,11 +368,5 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
}
|
||||
|
||||
private record FilterResult(string Filter, string FinalLabel);
|
||||
|
||||
private enum StreamType
|
||||
{
|
||||
Audio,
|
||||
Video
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace ErsatzTV.Core
|
||||
|
||||
public static readonly string DatabasePath = Path.Combine(AppDataFolder, "ersatztv.sqlite3");
|
||||
|
||||
public static readonly string LogDatabasePath = Path.Combine(AppDataFolder, "logs.sqlite3");
|
||||
|
||||
public static readonly string ImageCacheFolder = Path.Combine(AppDataFolder, "cache", "images");
|
||||
|
||||
public static readonly string PlexSecretsPath = Path.Combine(AppDataFolder, "plex-secrets.json");
|
||||
|
||||
11
ErsatzTV.Core/Interfaces/Images/IImageCache.cs
Normal file
11
ErsatzTV.Core/Interfaces/Images/IImageCache.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Threading.Tasks;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Images
|
||||
{
|
||||
public interface IImageCache
|
||||
{
|
||||
Task<Either<BaseError, string>> ResizeAndSaveImage(byte[] imageBuffer, int? height, int? width);
|
||||
Task<Either<BaseError, string>> SaveImage(byte[] imageBuffer);
|
||||
}
|
||||
}
|
||||
12
ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs
Normal file
12
ErsatzTV.Core/Interfaces/Locking/IEntityLocker.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Locking
|
||||
{
|
||||
public interface IEntityLocker
|
||||
{
|
||||
public event EventHandler OnMediaSourceChanged;
|
||||
public bool LockMediaSource(int mediaSourceId);
|
||||
public bool UnlockMediaSource(int mediaSourceId);
|
||||
public bool IsMediaSourceLocked(int mediaSourceId);
|
||||
}
|
||||
}
|
||||
13
ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs
Normal file
13
ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalFileSystem
|
||||
{
|
||||
public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource);
|
||||
public Seq<string> FindRelevantVideos(LocalMediaSource localMediaSource);
|
||||
public bool ShouldRefreshMetadata(LocalMediaSource localMediaSource, MediaItem mediaItem);
|
||||
public bool ShouldRefreshPoster(MediaItem mediaItem);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalMediaScanner
|
||||
{
|
||||
Task<Unit> ScanLocalMediaSource(LocalMediaSource localMediaSource, string ffprobePath);
|
||||
Task<Unit> ScanLocalMediaSource(
|
||||
LocalMediaSource localMediaSource,
|
||||
string ffprobePath,
|
||||
ScanningMode scanningMode);
|
||||
}
|
||||
}
|
||||
|
||||
10
ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs
Normal file
10
ErsatzTV.Core/Interfaces/Metadata/ILocalPosterProvider.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalPosterProvider
|
||||
{
|
||||
Task RefreshPoster(MediaItem mediaItem);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalStatisticsProvider
|
||||
{
|
||||
Task RefreshStatistics(string ffprobePath, MediaItem mediaItem);
|
||||
Task<bool> RefreshStatistics(string ffprobePath, MediaItem mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ISmartCollectionBuilder
|
||||
{
|
||||
Task RefreshSmartCollections(MediaItem mediaItem);
|
||||
Task<bool> RefreshSmartCollections(MediaItem mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
11
ErsatzTV.Core/Interfaces/Repositories/ILogRepository.cs
Normal file
11
ErsatzTV.Core/Interfaces/Repositories/ILogRepository.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
{
|
||||
public interface ILogRepository
|
||||
{
|
||||
public Task<List<LogEntry>> GetRecentLogEntries();
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
public Task<Option<List<MediaItem>>> GetSimpleMediaCollectionItems(int id);
|
||||
public Task<Option<List<MediaItem>>> GetTelevisionMediaCollectionItems(int id);
|
||||
public Task Update(SimpleMediaCollection collection);
|
||||
public Task InsertOrIgnore(TelevisionMediaCollection collection);
|
||||
public Task<bool> InsertOrIgnore(TelevisionMediaCollection collection);
|
||||
public Task<Unit> ReplaceItems(int collectionId, List<MediaItem> mediaItems);
|
||||
public Task Delete(int mediaCollectionId);
|
||||
public Task DeleteEmptyTelevisionCollections();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.AggregateModels;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
@@ -11,9 +12,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
public Task<Option<MediaItem>> Get(int id);
|
||||
public Task<List<MediaItem>> GetAll();
|
||||
public Task<List<MediaItem>> Search(string searchString);
|
||||
public Task<List<MediaItem>> GetAll(MediaType mediaType);
|
||||
public Task<List<MediaItemSummary>> GetPageByType(MediaType mediaType, int pageNumber, int pageSize);
|
||||
public Task<int> GetCountByType(MediaType mediaType);
|
||||
public Task<List<MediaItem>> GetAllByMediaSourceId(int mediaSourceId);
|
||||
public Task Update(MediaItem mediaItem);
|
||||
public Task<bool> Update(MediaItem mediaItem);
|
||||
public Task Delete(int mediaItemId);
|
||||
}
|
||||
}
|
||||
|
||||
64
ErsatzTV.Core/Metadata/LocalFileSystem.cs
Normal file
64
ErsatzTV.Core/Metadata/LocalFileSystem.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public class LocalFileSystem : ILocalFileSystem
|
||||
{
|
||||
public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource) =>
|
||||
Directory.Exists(localMediaSource.Folder);
|
||||
|
||||
public Seq<string> FindRelevantVideos(LocalMediaSource localMediaSource)
|
||||
{
|
||||
Seq<string> allDirectories = Directory
|
||||
.GetDirectories(localMediaSource.Folder, "*", SearchOption.AllDirectories)
|
||||
.ToSeq()
|
||||
.Add(localMediaSource.Folder);
|
||||
|
||||
// remove any directories with an .etvignore file locally, or in any parent directory
|
||||
Seq<string> excluded = allDirectories.Filter(ShouldExcludeDirectory);
|
||||
Seq<string> relevantDirectories = allDirectories
|
||||
.Filter(d => !excluded.Any(d.StartsWith))
|
||||
.Filter(d => localMediaSource.MediaType == MediaType.Other || !IsExtrasFolder(d));
|
||||
|
||||
return relevantDirectories
|
||||
.Collect(d => Directory.GetFiles(d, "*", SearchOption.TopDirectoryOnly))
|
||||
.Filter(file => KnownExtensions.Contains(Path.GetExtension(file)))
|
||||
.OrderBy(identity)
|
||||
.ToSeq();
|
||||
}
|
||||
|
||||
public bool ShouldRefreshMetadata(LocalMediaSource localMediaSource, MediaItem mediaItem)
|
||||
{
|
||||
DateTime lastWrite = File.GetLastWriteTimeUtc(mediaItem.Path);
|
||||
bool modified = lastWrite > mediaItem.LastWriteTime.IfNone(DateTime.MinValue);
|
||||
return modified // media item has been modified
|
||||
|| mediaItem.Metadata == null // media item has no metadata
|
||||
|| mediaItem.Metadata.MediaType != localMediaSource.MediaType; // media item is typed incorrectly
|
||||
}
|
||||
|
||||
public bool ShouldRefreshPoster(MediaItem mediaItem) =>
|
||||
string.IsNullOrWhiteSpace(mediaItem.Poster);
|
||||
|
||||
private static bool ShouldExcludeDirectory(string path) => File.Exists(Path.Combine(path, ".etvignore"));
|
||||
|
||||
// see https://support.emby.media/support/solutions/articles/44001159102-movie-naming
|
||||
private static bool IsExtrasFolder(string path) =>
|
||||
ExtraFolderNames.Contains(Path.GetFileName(path)?.ToLowerInvariant());
|
||||
|
||||
// @formatter:off
|
||||
private static readonly Seq<string> KnownExtensions = Seq(
|
||||
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4",
|
||||
".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts");
|
||||
|
||||
private static readonly Seq<string> ExtraFolderNames = Seq(
|
||||
"extras", "specials", "shorts", "scenes", "featurettes",
|
||||
"behind the scenes", "deleted scenes", "interviews", "trailers");
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -15,7 +14,9 @@ namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public class LocalMediaScanner : ILocalMediaScanner
|
||||
{
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILocalMetadataProvider _localMetadataProvider;
|
||||
private readonly ILocalPosterProvider _localPosterProvider;
|
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||
private readonly ILogger<LocalMediaScanner> _logger;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
@@ -28,77 +29,59 @@ namespace ErsatzTV.Core.Metadata
|
||||
IPlayoutRepository playoutRepository,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalMetadataProvider localMetadataProvider,
|
||||
ILocalPosterProvider localPosterProvider,
|
||||
ISmartCollectionBuilder smartCollectionBuilder,
|
||||
IPlayoutBuilder playoutBuilder,
|
||||
ILogger<LocalMediaScanner> logger)
|
||||
ILogger<LocalMediaScanner> logger,
|
||||
ILocalFileSystem localFileSystem)
|
||||
{
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_playoutRepository = playoutRepository;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_localMetadataProvider = localMetadataProvider;
|
||||
_localPosterProvider = localPosterProvider;
|
||||
_smartCollectionBuilder = smartCollectionBuilder;
|
||||
_playoutBuilder = playoutBuilder;
|
||||
_logger = logger;
|
||||
_localFileSystem = localFileSystem;
|
||||
}
|
||||
|
||||
public async Task<Unit> ScanLocalMediaSource(LocalMediaSource localMediaSource, string ffprobePath)
|
||||
public async Task<Unit> ScanLocalMediaSource(
|
||||
LocalMediaSource localMediaSource,
|
||||
string ffprobePath,
|
||||
ScanningMode scanningMode)
|
||||
{
|
||||
if (!Directory.Exists(localMediaSource.Folder))
|
||||
if (!_localFileSystem.IsMediaSourceAccessible(localMediaSource))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Media source folder {Folder} does not exist; skipping scan",
|
||||
"Media source folder {Folder} does not exist or is inaccessible; skipping scan",
|
||||
localMediaSource.Folder);
|
||||
return Unit.Default;
|
||||
return unit;
|
||||
}
|
||||
|
||||
List<MediaItem> knownMediaItems = await _mediaItemRepository.GetAllByMediaSourceId(localMediaSource.Id);
|
||||
var modifiedPlayoutIds = new List<int>();
|
||||
|
||||
// remove files that no longer exist
|
||||
// add new files
|
||||
// refresh metadata for any files where it is missing
|
||||
var knownExtensions = new List<string>
|
||||
{
|
||||
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4", ".m4p", ".m4v",
|
||||
".avi", ".wmv", ".mov", ".mkv", ".ts"
|
||||
};
|
||||
|
||||
var allFiles = Directory.GetFiles(localMediaSource.Folder, "*", SearchOption.AllDirectories)
|
||||
.Filter(file => knownExtensions.Contains(Path.GetExtension(file)))
|
||||
.ToSeq();
|
||||
Seq<string> allFiles = _localFileSystem.FindRelevantVideos(localMediaSource);
|
||||
|
||||
// check if the media item exists
|
||||
(Seq<string> newFiles, Seq<MediaItem> existingMediaItems) = allFiles.Map(
|
||||
s => Optional(knownMediaItems.Find(i => i.Path == s)).ToEither(s))
|
||||
.Partition();
|
||||
|
||||
// TODO: flag as missing? delete after some period of time?
|
||||
var removedMediaItems = knownMediaItems.Filter(i => !allFiles.Contains(i.Path)).ToSeq();
|
||||
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(removedMediaItems));
|
||||
foreach (MediaItem mediaItem in removedMediaItems)
|
||||
{
|
||||
_logger.LogDebug("Removing missing local media item {MediaItem}", mediaItem.Path);
|
||||
await _mediaItemRepository.Delete(mediaItem.Id);
|
||||
}
|
||||
// remove media items that no longer exist
|
||||
var missingMediaItems = knownMediaItems.Filter(i => !allFiles.Contains(i.Path)).ToSeq();
|
||||
await RemoveMissingItems(missingMediaItems);
|
||||
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(missingMediaItems));
|
||||
|
||||
// if exists, check if the file was modified
|
||||
// also, try to re-categorize "other" by refreshing metadata
|
||||
Seq<MediaItem> modifiedMediaItems = existingMediaItems.Filter(
|
||||
mediaItem =>
|
||||
{
|
||||
DateTime lastWrite = File.GetLastWriteTimeUtc(mediaItem.Path);
|
||||
bool modified = lastWrite > mediaItem.LastWriteTime.IfNone(DateTime.MinValue);
|
||||
return modified || mediaItem.Metadata == null || mediaItem.Metadata.MediaType == MediaType.Other;
|
||||
});
|
||||
Seq<MediaItem> staleMetadataMediaItems = scanningMode == ScanningMode.RescanAll
|
||||
? existingMediaItems
|
||||
: existingMediaItems.Filter(i => _localFileSystem.ShouldRefreshMetadata(localMediaSource, i));
|
||||
Seq<MediaItem> modifiedMediaItems = await RefreshMetadataForItems(ffprobePath, staleMetadataMediaItems);
|
||||
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems));
|
||||
foreach (MediaItem mediaItem in modifiedMediaItems)
|
||||
{
|
||||
_logger.LogDebug("Refreshing metadata for media item {MediaItem}", mediaItem.Path);
|
||||
await RefreshMetadata(mediaItem, ffprobePath);
|
||||
}
|
||||
|
||||
// if new, add and store mtime, refresh metadata
|
||||
var addedMediaItems = new Seq<MediaItem>();
|
||||
var addedMediaItems = new List<MediaItem>();
|
||||
foreach (string path in newFiles)
|
||||
{
|
||||
_logger.LogDebug("Adding new media item {MediaItem}", path);
|
||||
@@ -114,7 +97,12 @@ namespace ErsatzTV.Core.Metadata
|
||||
addedMediaItems.Add(mediaItem);
|
||||
}
|
||||
|
||||
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(addedMediaItems));
|
||||
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(addedMediaItems.ToSeq()));
|
||||
|
||||
Seq<MediaItem> stalePosterMediaItems = existingMediaItems
|
||||
.Filter(_localFileSystem.ShouldRefreshPoster)
|
||||
.Concat(addedMediaItems);
|
||||
await RefreshPosterForItems(stalePosterMediaItems);
|
||||
|
||||
foreach (int playoutId in modifiedPlayoutIds.Distinct())
|
||||
{
|
||||
@@ -128,14 +116,68 @@ namespace ErsatzTV.Core.Metadata
|
||||
Task.CompletedTask);
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
return unit;
|
||||
}
|
||||
|
||||
private async Task RefreshMetadata(MediaItem mediaItem, string ffprobePath)
|
||||
private async Task<Seq<MediaItem>> RefreshMetadataForItems(
|
||||
string ffprobePath,
|
||||
Seq<MediaItem> staleMetadataMediaItems)
|
||||
{
|
||||
await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
|
||||
var modifiedMediaItems = new List<MediaItem>();
|
||||
foreach (MediaItem mediaItem in staleMetadataMediaItems)
|
||||
{
|
||||
_logger.LogDebug("Refreshing metadata for media item {MediaItem}", mediaItem.Path);
|
||||
if (await RefreshMetadata(mediaItem, ffprobePath))
|
||||
{
|
||||
// only queue playout rebuilds for media items
|
||||
// where the duration or collections have changed
|
||||
modifiedMediaItems.Add(mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedMediaItems.ToSeq();
|
||||
}
|
||||
|
||||
private async Task RefreshPosterForItems(Seq<MediaItem> stalePosterMediaItems)
|
||||
{
|
||||
(Seq<MediaItem> movies, Seq<MediaItem> episodes) = stalePosterMediaItems
|
||||
.Map(i => Optional(i).Filter(i2 => i2.Metadata?.MediaType == MediaType.TvShow).ToEither(i))
|
||||
.Partition();
|
||||
|
||||
// there's a 1:1 movie:poster, so refresh all
|
||||
foreach (MediaItem movie in movies)
|
||||
{
|
||||
_logger.LogDebug("Refreshing poster for media item {MediaItem}", movie.Path);
|
||||
await _localPosterProvider.RefreshPoster(movie);
|
||||
}
|
||||
|
||||
// we currently have 1 poster per series, so pick the first from each group
|
||||
IEnumerable<MediaItem> episodesToRefresh = episodes.GroupBy(e => e.Metadata.Title)
|
||||
.SelectMany(g => (Option<MediaItem>) g.FirstOrDefault());
|
||||
|
||||
foreach (MediaItem episode in episodesToRefresh)
|
||||
{
|
||||
_logger.LogDebug("Refreshing poster for media item {MediaItem}", episode.Path);
|
||||
await _localPosterProvider.RefreshPoster(episode);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveMissingItems(Seq<MediaItem> removedMediaItems)
|
||||
{
|
||||
// TODO: flag as missing? delete after some period of time?
|
||||
foreach (MediaItem mediaItem in removedMediaItems)
|
||||
{
|
||||
_logger.LogDebug("Removing missing local media item {MediaItem}", mediaItem.Path);
|
||||
await _mediaItemRepository.Delete(mediaItem.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> RefreshMetadata(MediaItem mediaItem, string ffprobePath)
|
||||
{
|
||||
bool durationChange = await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
|
||||
await _localMetadataProvider.RefreshMetadata(mediaItem);
|
||||
await _smartCollectionBuilder.RefreshSmartCollections(mediaItem);
|
||||
bool collectionChange = await _smartCollectionBuilder.RefreshSmartCollections(mediaItem);
|
||||
return durationChange || collectionChange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ namespace ErsatzTV.Core.Metadata
|
||||
mediaItem.Metadata.MediaType = metadata.MediaType;
|
||||
mediaItem.Metadata.Title = metadata.Title;
|
||||
mediaItem.Metadata.Subtitle = metadata.Subtitle;
|
||||
mediaItem.Metadata.SortTitle =
|
||||
(metadata.Title ?? string.Empty).ToLowerInvariant().StartsWith("the ")
|
||||
? metadata.Title?.Substring(4)
|
||||
: metadata.Title;
|
||||
mediaItem.Metadata.Description = metadata.Description;
|
||||
mediaItem.Metadata.EpisodeNumber = metadata.EpisodeNumber;
|
||||
mediaItem.Metadata.SeasonNumber = metadata.SeasonNumber;
|
||||
|
||||
93
ErsatzTV.Core/Metadata/LocalPosterProvider.cs
Normal file
93
ErsatzTV.Core/Metadata/LocalPosterProvider.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public class LocalPosterProvider : ILocalPosterProvider
|
||||
{
|
||||
private static readonly string[] SupportedExtensions = { "jpg", "jpeg", "png", "gif", "tbn" };
|
||||
|
||||
private readonly IImageCache _imageCache;
|
||||
private readonly ILogger<LocalPosterProvider> _logger;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public LocalPosterProvider(
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
IImageCache imageCache,
|
||||
ILogger<LocalPosterProvider> logger)
|
||||
{
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_imageCache = imageCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RefreshPoster(MediaItem mediaItem)
|
||||
{
|
||||
Option<string> maybePosterPath = mediaItem.Metadata.MediaType switch
|
||||
{
|
||||
MediaType.Movie => RefreshMoviePoster(mediaItem),
|
||||
MediaType.TvShow => RefreshTelevisionPoster(mediaItem),
|
||||
_ => None
|
||||
};
|
||||
|
||||
await maybePosterPath.Match(
|
||||
path => SavePosterToDisk(mediaItem, path),
|
||||
Task.CompletedTask);
|
||||
}
|
||||
|
||||
private static Option<string> RefreshMoviePoster(MediaItem mediaItem)
|
||||
{
|
||||
string folder = Path.GetDirectoryName(mediaItem.Path);
|
||||
if (folder != null)
|
||||
{
|
||||
IEnumerable<string> possiblePaths = SupportedExtensions.Collect(
|
||||
e => new[] { $"poster.{e}", Path.GetFileNameWithoutExtension(mediaItem.Path) + $"-poster.{e}" });
|
||||
Option<string> maybePoster =
|
||||
possiblePaths.Map(p => Path.Combine(folder, p)).FirstOrDefault(File.Exists);
|
||||
return maybePoster;
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
private Option<string> RefreshTelevisionPoster(MediaItem mediaItem)
|
||||
{
|
||||
string folder = Directory.GetParent(Path.GetDirectoryName(mediaItem.Path) ?? string.Empty)?.FullName;
|
||||
if (folder != null)
|
||||
{
|
||||
IEnumerable<string> possiblePaths = SupportedExtensions.Collect(e => new[] { $"poster.{e}" });
|
||||
Option<string> maybePoster =
|
||||
possiblePaths.Map(p => Path.Combine(folder, p)).FirstOrDefault(File.Exists);
|
||||
return maybePoster;
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
private async Task SavePosterToDisk(MediaItem mediaItem, string posterPath)
|
||||
{
|
||||
byte[] originalBytes = await File.ReadAllBytesAsync(posterPath);
|
||||
Either<BaseError, string> maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, 220, null);
|
||||
await maybeHash.Match(
|
||||
hash =>
|
||||
{
|
||||
mediaItem.Poster = hash;
|
||||
return _mediaItemRepository.Update(mediaItem);
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning("Unable to save poster to disk from {Path}: {Error}", posterPath, error.Value);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,21 +26,22 @@ namespace ErsatzTV.Core.Metadata
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RefreshStatistics(string ffprobePath, MediaItem mediaItem)
|
||||
public async Task<bool> RefreshStatistics(string ffprobePath, MediaItem mediaItem)
|
||||
{
|
||||
try
|
||||
{
|
||||
FFprobe ffprobe = await GetProbeOutput(ffprobePath, mediaItem);
|
||||
MediaMetadata metadata = ProjectToMediaMetadata(ffprobe);
|
||||
await ApplyStatisticsUpdate(mediaItem, metadata);
|
||||
return await ApplyStatisticsUpdate(mediaItem, metadata);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to refresh statistics for media item at {Path}", mediaItem.Path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyStatisticsUpdate(
|
||||
private async Task<bool> ApplyStatisticsUpdate(
|
||||
MediaItem mediaItem,
|
||||
MediaMetadata metadata)
|
||||
{
|
||||
@@ -49,6 +50,8 @@ namespace ErsatzTV.Core.Metadata
|
||||
mediaItem.Metadata = new MediaMetadata();
|
||||
}
|
||||
|
||||
bool durationChange = mediaItem.Metadata.Duration != metadata.Duration;
|
||||
|
||||
mediaItem.Metadata.Duration = metadata.Duration;
|
||||
mediaItem.Metadata.AudioCodec = metadata.AudioCodec;
|
||||
mediaItem.Metadata.SampleAspectRatio = metadata.SampleAspectRatio;
|
||||
@@ -58,7 +61,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
mediaItem.Metadata.VideoCodec = metadata.VideoCodec;
|
||||
mediaItem.Metadata.VideoScanType = metadata.VideoScanType;
|
||||
|
||||
await _mediaItemRepository.Update(mediaItem);
|
||||
return await _mediaItemRepository.Update(mediaItem) && durationChange;
|
||||
}
|
||||
|
||||
private Task<FFprobe> GetProbeOutput(string ffprobePath, MediaItem mediaItem)
|
||||
|
||||
8
ErsatzTV.Core/Metadata/ScanningMode.cs
Normal file
8
ErsatzTV.Core/Metadata/ScanningMode.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public enum ScanningMode
|
||||
{
|
||||
Default = 0,
|
||||
RescanAll = 1
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,16 @@ namespace ErsatzTV.Core.Metadata
|
||||
public SmartCollectionBuilder(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
|
||||
public async Task RefreshSmartCollections(MediaItem mediaItem)
|
||||
public async Task<bool> RefreshSmartCollections(MediaItem mediaItem)
|
||||
{
|
||||
var results = new List<bool>();
|
||||
|
||||
foreach (TelevisionMediaCollection collection in GetTelevisionCollections(mediaItem))
|
||||
{
|
||||
await _mediaCollectionRepository.InsertOrIgnore(collection);
|
||||
results.Add(await _mediaCollectionRepository.InsertOrIgnore(collection));
|
||||
}
|
||||
|
||||
return results.Any(identity);
|
||||
}
|
||||
|
||||
private IEnumerable<TelevisionMediaCollection> GetTelevisionCollections(MediaItem mediaItem)
|
||||
|
||||
@@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
public class GenericIntegerIdConfiguration : IEntityTypeConfiguration<GenericIntegerId>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<GenericIntegerId> builder) =>
|
||||
builder.HasNoKey();
|
||||
builder.HasNoKey().ToView(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@ namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
public class MediaCollectionSummaryConfiguration : IEntityTypeConfiguration<MediaCollectionSummary>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<MediaCollectionSummary> builder) =>
|
||||
builder.HasNoKey();
|
||||
builder.HasNoKey().ToView(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using ErsatzTV.Core.AggregateModels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations
|
||||
{
|
||||
public class MediaItemSummaryConfiguration : IEntityTypeConfiguration<MediaItemSummary>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<MediaItemSummary> builder) =>
|
||||
builder.HasNoKey().ToView(null);
|
||||
}
|
||||
}
|
||||
21
ErsatzTV.Infrastructure/Data/LogContext.cs
Normal file
21
ErsatzTV.Infrastructure/Data/LogContext.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data
|
||||
{
|
||||
public class LogContext : DbContext
|
||||
{
|
||||
public LogContext(DbContextOptions<LogContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<LogEntry> LogEntries { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
builder.Entity<LogEntry>().ToTable("Logs");
|
||||
}
|
||||
}
|
||||
}
|
||||
19
ErsatzTV.Infrastructure/Data/Repositories/LogRepository.cs
Normal file
19
ErsatzTV.Infrastructure/Data/Repositories/LogRepository.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
{
|
||||
public class LogRepository : ILogRepository
|
||||
{
|
||||
private readonly LogContext _logContext;
|
||||
|
||||
public LogRepository(LogContext logContext) => _logContext = logContext;
|
||||
|
||||
public Task<List<LogEntry>> GetRecentLogEntries() =>
|
||||
_logContext.LogEntries.OrderByDescending(e => e.Id).Take(100).ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.AggregateModels;
|
||||
@@ -68,7 +69,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
collection => collection switch
|
||||
{
|
||||
SimpleMediaCollection s => SimpleItems(s),
|
||||
TelevisionMediaCollection t => TelevisionItems(t)
|
||||
TelevisionMediaCollection t => TelevisionItems(t),
|
||||
_ => throw new NotSupportedException($"Unsupported collection type {collection.GetType().Name}")
|
||||
}).Bind(x => x.Sequence());
|
||||
|
||||
public Task<Option<List<MediaItem>>> GetSimpleMediaCollectionItems(int id) =>
|
||||
@@ -83,15 +85,18 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
return _dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task InsertOrIgnore(TelevisionMediaCollection collection)
|
||||
public async Task<bool> InsertOrIgnore(TelevisionMediaCollection collection)
|
||||
{
|
||||
if (!_dbContext.TelevisionMediaCollections.Any(
|
||||
existing => existing.ShowTitle == collection.ShowTitle &&
|
||||
existing.SeasonNumber == collection.SeasonNumber))
|
||||
{
|
||||
await _dbContext.TelevisionMediaCollections.AddAsync(collection);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
return await _dbContext.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
// no change
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<Unit> ReplaceItems(int collectionId, List<MediaItem> mediaItems) =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.AggregateModels;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
@@ -23,7 +24,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
public Task<Option<MediaItem>> Get(int id) =>
|
||||
_dbContext.MediaItems.SingleOrDefaultAsync(i => i.Id == id).Map(Optional);
|
||||
_dbContext.MediaItems
|
||||
.Include(i => i.Source)
|
||||
.SingleOrDefaultAsync(i => i.Id == id)
|
||||
.Map(Optional);
|
||||
|
||||
public Task<List<MediaItem>> GetAll() => _dbContext.MediaItems.ToListAsync();
|
||||
|
||||
@@ -40,21 +44,63 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
}
|
||||
|
||||
|
||||
public Task<List<MediaItem>> GetAll(MediaType mediaType) =>
|
||||
_dbContext.MediaItems
|
||||
.Include(i => i.Source)
|
||||
.Filter(i => i.Metadata.MediaType == mediaType)
|
||||
.ToListAsync();
|
||||
public Task<List<MediaItemSummary>> GetPageByType(MediaType mediaType, int pageNumber, int pageSize) =>
|
||||
mediaType switch
|
||||
{
|
||||
MediaType.Movie => _dbContext.MediaItemSummaries.FromSqlRaw(
|
||||
@"SELECT
|
||||
Id AS MediaItemId,
|
||||
Metadata_Title AS Title,
|
||||
Metadata_SortTitle AS SortTitle,
|
||||
substr(Metadata_Aired, 1, 4) AS Subtitle,
|
||||
Poster
|
||||
FROM MediaItems WHERE Metadata_MediaType=2
|
||||
ORDER BY Metadata_SortTitle
|
||||
LIMIT {0} OFFSET {1}",
|
||||
pageSize,
|
||||
(pageNumber - 1) * pageSize)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(),
|
||||
MediaType.TvShow => _dbContext.MediaItemSummaries.FromSqlRaw(
|
||||
@"SELECT
|
||||
min(Id) AS MediaItemId,
|
||||
Metadata_Title AS Title,
|
||||
Metadata_SortTitle AS SortTitle,
|
||||
count(*) || ' Episodes' AS Subtitle,
|
||||
max(Poster) AS Poster
|
||||
FROM MediaItems WHERE Metadata_MediaType=1
|
||||
GROUP BY Metadata_Title, Metadata_SortTitle
|
||||
ORDER BY Metadata_SortTitle
|
||||
LIMIT {0} OFFSET {1}",
|
||||
pageSize,
|
||||
(pageNumber - 1) * pageSize)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(),
|
||||
_ => Task.FromResult(new List<MediaItemSummary>())
|
||||
};
|
||||
|
||||
public Task<int> GetCountByType(MediaType mediaType) =>
|
||||
mediaType switch
|
||||
{
|
||||
MediaType.Movie => _dbContext.MediaItems
|
||||
.Filter(i => i.Metadata.MediaType == mediaType)
|
||||
.CountAsync(),
|
||||
MediaType.TvShow => _dbContext.MediaItems
|
||||
.Filter(i => i.Metadata.MediaType == mediaType)
|
||||
.GroupBy(i => new { i.Metadata.Title, i.Metadata.SortTitle })
|
||||
.CountAsync(),
|
||||
_ => Task.FromResult(0)
|
||||
};
|
||||
|
||||
public Task<List<MediaItem>> GetAllByMediaSourceId(int mediaSourceId) =>
|
||||
_dbContext.MediaItems
|
||||
.Filter(i => i.MediaSourceId == mediaSourceId)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task Update(MediaItem mediaItem)
|
||||
public async Task<bool> Update(MediaItem mediaItem)
|
||||
{
|
||||
_dbContext.MediaItems.Update(mediaItem);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
return await _dbContext.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
public async Task Delete(int mediaItemId)
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace ErsatzTV.Infrastructure.Data
|
||||
// support raw sql queries
|
||||
public DbSet<MediaCollectionSummary> MediaCollectionSummaries { get; set; }
|
||||
public DbSet<GenericIntegerId> GenericIntegerIds { get; set; }
|
||||
public DbSet<MediaItemSummary> MediaItemSummaries { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
|
||||
optionsBuilder.UseLoggerFactory(_loggerFactory);
|
||||
@@ -39,6 +40,11 @@ namespace ErsatzTV.Infrastructure.Data
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
builder.Ignore<MediaCollectionSummary>();
|
||||
builder.Ignore<GenericIntegerId>();
|
||||
builder.Ignore<MediaItemSummary>();
|
||||
|
||||
builder.ApplyConfigurationsFromAssembly(typeof(TvContext).Assembly);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.3" />
|
||||
<PackageReference Include="Refit" Version="6.0.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
64
ErsatzTV.Infrastructure/Images/ImageCache.cs
Normal file
64
ErsatzTV.Infrastructure/Images/ImageCache.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using LanguageExt;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Images
|
||||
{
|
||||
public class ImageCache : IImageCache
|
||||
{
|
||||
private static readonly SHA1CryptoServiceProvider Crypto;
|
||||
|
||||
static ImageCache() => Crypto = new SHA1CryptoServiceProvider();
|
||||
|
||||
public async Task<Either<BaseError, string>> ResizeAndSaveImage(byte[] imageBuffer, int? height, int? width)
|
||||
{
|
||||
await using var inStream = new MemoryStream(imageBuffer);
|
||||
using var image = await Image.LoadAsync(inStream);
|
||||
|
||||
Size size = height.HasValue ? new Size { Height = height.Value } : new Size { Width = width.Value };
|
||||
|
||||
image.Mutate(
|
||||
i => i.Resize(
|
||||
new ResizeOptions
|
||||
{
|
||||
Mode = ResizeMode.Max,
|
||||
Size = size
|
||||
}));
|
||||
|
||||
await using var outStream = new MemoryStream();
|
||||
await image.SaveAsync(outStream, new JpegEncoder { Quality = 90 });
|
||||
|
||||
return await SaveImage(outStream.ToArray());
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, string>> SaveImage(byte[] imageBuffer)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] hash = Crypto.ComputeHash(imageBuffer);
|
||||
string hex = BitConverter.ToString(hash).Replace("-", string.Empty);
|
||||
|
||||
string fileName = Path.Combine(FileSystemLayout.ImageCacheFolder, hex);
|
||||
|
||||
if (!Directory.Exists(FileSystemLayout.ImageCacheFolder))
|
||||
{
|
||||
Directory.CreateDirectory(FileSystemLayout.ImageCacheFolder);
|
||||
}
|
||||
|
||||
await File.WriteAllBytesAsync(fileName, imageBuffer);
|
||||
return hex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
ErsatzTV.Infrastructure/Locking/EntityLocker.cs
Normal file
40
ErsatzTV.Infrastructure/Locking/EntityLocker.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Locking
|
||||
{
|
||||
public class EntityLocker : IEntityLocker
|
||||
{
|
||||
private readonly ConcurrentDictionary<int, byte> _lockedMediaSources;
|
||||
|
||||
public EntityLocker() => _lockedMediaSources = new ConcurrentDictionary<int, byte>();
|
||||
|
||||
public event EventHandler OnMediaSourceChanged;
|
||||
|
||||
public bool LockMediaSource(int mediaSourceId)
|
||||
{
|
||||
if (!_lockedMediaSources.ContainsKey(mediaSourceId) && _lockedMediaSources.TryAdd(mediaSourceId, 0))
|
||||
{
|
||||
OnMediaSourceChanged?.Invoke(this, EventArgs.Empty);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool UnlockMediaSource(int mediaSourceId)
|
||||
{
|
||||
if (_lockedMediaSources.TryRemove(mediaSourceId, out byte _))
|
||||
{
|
||||
OnMediaSourceChanged?.Invoke(this, EventArgs.Empty);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsMediaSourceLocked(int mediaSourceId) =>
|
||||
_lockedMediaSources.ContainsKey(mediaSourceId);
|
||||
}
|
||||
}
|
||||
879
ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.Designer.cs
generated
Normal file
879
ErsatzTV.Infrastructure/Migrations/20210213155419_MetadataSortTitle.Designer.cs
generated
Normal file
@@ -0,0 +1,879 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(TvContext))]
|
||||
[Migration("20210213155419_MetadataSortTitle")]
|
||||
partial class MetadataSortTitle
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.3");
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.GenericIntegerId", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.ToTable("GenericIntegerIds");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaCollectionSummary", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsSimple")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.ToTable("MediaCollectionSummaries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("FFmpegProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Logo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("StreamingMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UniqueId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FFmpegProfileId");
|
||||
|
||||
b.HasIndex("Number")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Channels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ConfigElements");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AudioBitrate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AudioBufferSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AudioChannels")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudioCodec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AudioSampleRate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AudioVolume")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("NormalizeAudio")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("NormalizeAudioCodec")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("NormalizeResolution")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("NormalizeVideoCodec")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ResolutionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ThreadCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Transcode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VideoBitrate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VideoBufferSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("VideoCodec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ResolutionId");
|
||||
|
||||
b.ToTable("FFmpegProfiles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaCollection", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MediaCollections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastWriteTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MediaSourceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MediaSourceId");
|
||||
|
||||
b.ToTable("MediaItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SourceType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MediaSources");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChannelId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProgramScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProgramSchedulePlayoutType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
b.HasIndex("ProgramScheduleId");
|
||||
|
||||
b.ToTable("Playouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Finish")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MediaItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PlayoutId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Start")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MediaItemId");
|
||||
|
||||
b.HasIndex("PlayoutId");
|
||||
|
||||
b.ToTable("PlayoutItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
|
||||
{
|
||||
b.Property<int>("PlayoutId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProgramScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MediaCollectionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("PlayoutId", "ProgramScheduleId", "MediaCollectionId");
|
||||
|
||||
b.HasIndex("MediaCollectionId");
|
||||
|
||||
b.HasIndex("ProgramScheduleId");
|
||||
|
||||
b.ToTable("PlayoutProgramScheduleItemAnchors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("PlexMediaSourceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Uri")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlexMediaSourceId");
|
||||
|
||||
b.ToTable("PlexMediaSourceConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MediaType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("PlexMediaSourceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlexMediaSourceId");
|
||||
|
||||
b.ToTable("PlexMediaSourceLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MediaCollectionPlaybackOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ProgramSchedules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Index")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MediaCollectionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProgramScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeSpan?>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MediaCollectionId");
|
||||
|
||||
b.HasIndex("ProgramScheduleId");
|
||||
|
||||
b.ToTable("ProgramScheduleItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Height")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Width")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Resolutions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MediaItemSimpleMediaCollection", b =>
|
||||
{
|
||||
b.Property<int>("ItemsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SimpleMediaCollectionsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ItemsId", "SimpleMediaCollectionsId");
|
||||
|
||||
b.HasIndex("SimpleMediaCollectionsId");
|
||||
|
||||
b.ToTable("MediaItemSimpleMediaCollection");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection");
|
||||
|
||||
b.ToTable("SimpleMediaCollections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionMediaCollection", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection");
|
||||
|
||||
b.Property<int?>("SeasonNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ShowTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasIndex("ShowTitle", "SeasonNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TelevisionMediaCollections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
|
||||
|
||||
b.Property<string>("Folder")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MediaType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.ToTable("LocalMediaSources");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
|
||||
|
||||
b.Property<string>("ClientIdentifier")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProductVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.ToTable("PlexMediaSources");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
|
||||
|
||||
b.Property<bool>("OfflineTail")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeSpan>("PlayoutDuration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.ToTable("ProgramScheduleDurationItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
|
||||
|
||||
b.ToTable("ProgramScheduleFloodItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
|
||||
|
||||
b.Property<int>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.ToTable("ProgramScheduleMultipleItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
|
||||
|
||||
b.ToTable("ProgramScheduleOneItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile")
|
||||
.WithMany()
|
||||
.HasForeignKey("FFmpegProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("FFmpegProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution")
|
||||
.WithMany()
|
||||
.HasForeignKey("ResolutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Resolution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaSource", "Source")
|
||||
.WithMany()
|
||||
.HasForeignKey("MediaSourceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("ErsatzTV.Core.Domain.MediaMetadata", "Metadata", b1 =>
|
||||
{
|
||||
b1.Property<int>("MediaItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<DateTime?>("Aired")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("AudioCodec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("ContentRating")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("DisplayAspectRatio")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<TimeSpan>("Duration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int?>("EpisodeNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("Height")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("MediaType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("SampleAspectRatio")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int?>("SeasonNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("SortTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("Subtitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("VideoCodec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int>("VideoScanType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("Width")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.HasKey("MediaItemId");
|
||||
|
||||
b1.ToTable("MediaItems");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MediaItemId");
|
||||
});
|
||||
|
||||
b.Navigation("Metadata");
|
||||
|
||||
b.Navigation("Source");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel")
|
||||
.WithMany("Playouts")
|
||||
.HasForeignKey("ChannelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
|
||||
.WithMany("Playouts")
|
||||
.HasForeignKey("ProgramScheduleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 =>
|
||||
{
|
||||
b1.Property<int>("PlayoutId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("NextScheduleItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<DateTimeOffset>("NextStart")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("PlayoutId");
|
||||
|
||||
b1.HasIndex("NextScheduleItemId");
|
||||
|
||||
b1.ToTable("Playouts");
|
||||
|
||||
b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem")
|
||||
.WithMany()
|
||||
.HasForeignKey("NextScheduleItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("PlayoutId");
|
||||
|
||||
b1.Navigation("NextScheduleItem");
|
||||
});
|
||||
|
||||
b.Navigation("Anchor");
|
||||
|
||||
b.Navigation("Channel");
|
||||
|
||||
b.Navigation("ProgramSchedule");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem")
|
||||
.WithMany()
|
||||
.HasForeignKey("MediaItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("PlayoutId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MediaItem");
|
||||
|
||||
b.Navigation("Playout");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection")
|
||||
.WithMany()
|
||||
.HasForeignKey("MediaCollectionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
|
||||
.WithMany("ProgramScheduleAnchors")
|
||||
.HasForeignKey("PlayoutId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProgramScheduleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("ErsatzTV.Core.Domain.MediaCollectionEnumeratorState", "EnumeratorState", b1 =>
|
||||
{
|
||||
b1.Property<int>("PlayoutProgramScheduleAnchorPlayoutId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("PlayoutProgramScheduleAnchorProgramScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("PlayoutProgramScheduleAnchorMediaCollectionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("Index")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("Seed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.HasKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId");
|
||||
|
||||
b1.ToTable("PlayoutProgramScheduleItemAnchors");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId");
|
||||
});
|
||||
|
||||
b.Navigation("EnumeratorState");
|
||||
|
||||
b.Navigation("MediaCollection");
|
||||
|
||||
b.Navigation("Playout");
|
||||
|
||||
b.Navigation("ProgramSchedule");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null)
|
||||
.WithMany("Connections")
|
||||
.HasForeignKey("PlexMediaSourceId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null)
|
||||
.WithMany("Libraries")
|
||||
.HasForeignKey("PlexMediaSourceId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection")
|
||||
.WithMany()
|
||||
.HasForeignKey("MediaCollectionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("ProgramScheduleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MediaCollection");
|
||||
|
||||
b.Navigation("ProgramSchedule");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MediaItemSimpleMediaCollection", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ItemsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SimpleMediaCollectionsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.SimpleMediaCollection", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionMediaCollection", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.TelevisionMediaCollection", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
|
||||
{
|
||||
b.Navigation("Playouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
|
||||
b.Navigation("ProgramScheduleAnchors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
|
||||
b.Navigation("Playouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
|
||||
{
|
||||
b.Navigation("Connections");
|
||||
|
||||
b.Navigation("Libraries");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class MetadataSortTitle : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
"Metadata_SortTitle",
|
||||
"MediaItems",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.Sql(
|
||||
@"UPDATE MediaItems
|
||||
SET Metadata_SortTitle = Metadata_Title");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
@"UPDATE MediaItems
|
||||
SET Metadata_SortTitle = substr(Metadata_Title, 5)
|
||||
WHERE Metadata_Title LIKE 'the %'");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) =>
|
||||
migrationBuilder.DropColumn(
|
||||
"Metadata_SortTitle",
|
||||
"MediaItems");
|
||||
}
|
||||
}
|
||||
893
ErsatzTV.Infrastructure/Migrations/20210213221040_MediaItemPoster.Designer.cs
generated
Normal file
893
ErsatzTV.Infrastructure/Migrations/20210213221040_MediaItemPoster.Designer.cs
generated
Normal file
@@ -0,0 +1,893 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(TvContext))]
|
||||
[Migration("20210213221040_MediaItemPoster")]
|
||||
partial class MediaItemPoster
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "5.0.3");
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.GenericIntegerId", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaCollectionSummary", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsSimple")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaItemSummary", b =>
|
||||
{
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SortTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Subtitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("FFmpegProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Logo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("StreamingMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UniqueId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FFmpegProfileId");
|
||||
|
||||
b.HasIndex("Number")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Channels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ConfigElements");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AudioBitrate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AudioBufferSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AudioChannels")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudioCodec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AudioSampleRate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AudioVolume")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("NormalizeAudio")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("NormalizeAudioCodec")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("NormalizeResolution")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("NormalizeVideoCodec")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ResolutionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ThreadCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Transcode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VideoBitrate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VideoBufferSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("VideoCodec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ResolutionId");
|
||||
|
||||
b.ToTable("FFmpegProfiles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaCollection", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MediaCollections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastWriteTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MediaSourceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MediaSourceId");
|
||||
|
||||
b.ToTable("MediaItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SourceType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MediaSources");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChannelId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProgramScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProgramSchedulePlayoutType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
b.HasIndex("ProgramScheduleId");
|
||||
|
||||
b.ToTable("Playouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Finish")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MediaItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PlayoutId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Start")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MediaItemId");
|
||||
|
||||
b.HasIndex("PlayoutId");
|
||||
|
||||
b.ToTable("PlayoutItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
|
||||
{
|
||||
b.Property<int>("PlayoutId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProgramScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MediaCollectionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("PlayoutId", "ProgramScheduleId", "MediaCollectionId");
|
||||
|
||||
b.HasIndex("MediaCollectionId");
|
||||
|
||||
b.HasIndex("ProgramScheduleId");
|
||||
|
||||
b.ToTable("PlayoutProgramScheduleItemAnchors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("PlexMediaSourceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Uri")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlexMediaSourceId");
|
||||
|
||||
b.ToTable("PlexMediaSourceConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MediaType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("PlexMediaSourceId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlexMediaSourceId");
|
||||
|
||||
b.ToTable("PlexMediaSourceLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MediaCollectionPlaybackOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ProgramSchedules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Index")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MediaCollectionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ProgramScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeSpan?>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MediaCollectionId");
|
||||
|
||||
b.HasIndex("ProgramScheduleId");
|
||||
|
||||
b.ToTable("ProgramScheduleItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Height")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Width")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Resolutions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MediaItemSimpleMediaCollection", b =>
|
||||
{
|
||||
b.Property<int>("ItemsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SimpleMediaCollectionsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ItemsId", "SimpleMediaCollectionsId");
|
||||
|
||||
b.HasIndex("SimpleMediaCollectionsId");
|
||||
|
||||
b.ToTable("MediaItemSimpleMediaCollection");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection");
|
||||
|
||||
b.ToTable("SimpleMediaCollections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionMediaCollection", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaCollection");
|
||||
|
||||
b.Property<int?>("SeasonNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ShowTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasIndex("ShowTitle", "SeasonNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TelevisionMediaCollections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
|
||||
|
||||
b.Property<string>("Folder")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MediaType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.ToTable("LocalMediaSources");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
|
||||
|
||||
b.Property<string>("ClientIdentifier")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProductVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.ToTable("PlexMediaSources");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
|
||||
|
||||
b.Property<bool>("OfflineTail")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeSpan>("PlayoutDuration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.ToTable("ProgramScheduleDurationItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
|
||||
|
||||
b.ToTable("ProgramScheduleFloodItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
|
||||
|
||||
b.Property<int>("Count")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.ToTable("ProgramScheduleMultipleItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b =>
|
||||
{
|
||||
b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem");
|
||||
|
||||
b.ToTable("ProgramScheduleOneItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile")
|
||||
.WithMany()
|
||||
.HasForeignKey("FFmpegProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("FFmpegProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution")
|
||||
.WithMany()
|
||||
.HasForeignKey("ResolutionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Resolution");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaSource", "Source")
|
||||
.WithMany()
|
||||
.HasForeignKey("MediaSourceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("ErsatzTV.Core.Domain.MediaMetadata", "Metadata", b1 =>
|
||||
{
|
||||
b1.Property<int>("MediaItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<DateTime?>("Aired")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("AudioCodec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("ContentRating")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("DisplayAspectRatio")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<TimeSpan>("Duration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int?>("EpisodeNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("Height")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("MediaType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("SampleAspectRatio")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int?>("SeasonNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("SortTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("Subtitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("VideoCodec")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int>("VideoScanType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("Width")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.HasKey("MediaItemId");
|
||||
|
||||
b1.ToTable("MediaItems");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MediaItemId");
|
||||
});
|
||||
|
||||
b.Navigation("Metadata");
|
||||
|
||||
b.Navigation("Source");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel")
|
||||
.WithMany("Playouts")
|
||||
.HasForeignKey("ChannelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
|
||||
.WithMany("Playouts")
|
||||
.HasForeignKey("ProgramScheduleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 =>
|
||||
{
|
||||
b1.Property<int>("PlayoutId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("NextScheduleItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<DateTimeOffset>("NextStart")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("PlayoutId");
|
||||
|
||||
b1.HasIndex("NextScheduleItemId");
|
||||
|
||||
b1.ToTable("Playouts");
|
||||
|
||||
b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem")
|
||||
.WithMany()
|
||||
.HasForeignKey("NextScheduleItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("PlayoutId");
|
||||
|
||||
b1.Navigation("NextScheduleItem");
|
||||
});
|
||||
|
||||
b.Navigation("Anchor");
|
||||
|
||||
b.Navigation("Channel");
|
||||
|
||||
b.Navigation("ProgramSchedule");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem")
|
||||
.WithMany()
|
||||
.HasForeignKey("MediaItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("PlayoutId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MediaItem");
|
||||
|
||||
b.Navigation("Playout");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection")
|
||||
.WithMany()
|
||||
.HasForeignKey("MediaCollectionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
|
||||
.WithMany("ProgramScheduleAnchors")
|
||||
.HasForeignKey("PlayoutId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProgramScheduleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsOne("ErsatzTV.Core.Domain.MediaCollectionEnumeratorState", "EnumeratorState", b1 =>
|
||||
{
|
||||
b1.Property<int>("PlayoutProgramScheduleAnchorPlayoutId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("PlayoutProgramScheduleAnchorProgramScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("PlayoutProgramScheduleAnchorMediaCollectionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("Index")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("Seed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.HasKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId");
|
||||
|
||||
b1.ToTable("PlayoutProgramScheduleItemAnchors");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("PlayoutProgramScheduleAnchorPlayoutId", "PlayoutProgramScheduleAnchorProgramScheduleId", "PlayoutProgramScheduleAnchorMediaCollectionId");
|
||||
});
|
||||
|
||||
b.Navigation("EnumeratorState");
|
||||
|
||||
b.Navigation("MediaCollection");
|
||||
|
||||
b.Navigation("Playout");
|
||||
|
||||
b.Navigation("ProgramSchedule");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceConnection", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null)
|
||||
.WithMany("Connections")
|
||||
.HasForeignKey("PlexMediaSourceId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSourceLibrary", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", null)
|
||||
.WithMany("Libraries")
|
||||
.HasForeignKey("PlexMediaSourceId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", "MediaCollection")
|
||||
.WithMany()
|
||||
.HasForeignKey("MediaCollectionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("ProgramScheduleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MediaCollection");
|
||||
|
||||
b.Navigation("ProgramSchedule");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MediaItemSimpleMediaCollection", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaItem", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ItemsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ErsatzTV.Core.Domain.SimpleMediaCollection", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SimpleMediaCollectionsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.SimpleMediaCollection", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.SimpleMediaCollection", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionMediaCollection", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaCollection", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.TelevisionMediaCollection", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b =>
|
||||
{
|
||||
b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
|
||||
{
|
||||
b.Navigation("Playouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
|
||||
b.Navigation("ProgramScheduleAnchors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
|
||||
b.Navigation("Playouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b =>
|
||||
{
|
||||
b.Navigation("Connections");
|
||||
|
||||
b.Navigation("Libraries");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class MediaItemPoster : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
"GenericIntegerIds");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"MediaCollectionSummaries");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
"Poster",
|
||||
"MediaItems",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
"Poster",
|
||||
"MediaItems");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"GenericIntegerIds",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table => { });
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"MediaCollectionSummaries",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false),
|
||||
IsSimple = table.Column<bool>("INTEGER", nullable: false),
|
||||
ItemCount = table.Column<int>("INTEGER", nullable: false),
|
||||
Name = table.Column<string>("TEXT", nullable: true)
|
||||
},
|
||||
constraints: table => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,6 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.ToTable("GenericIntegerIds");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
@@ -41,8 +39,23 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
});
|
||||
|
||||
b.ToTable("MediaCollectionSummaries");
|
||||
modelBuilder.Entity(
|
||||
"ErsatzTV.Core.AggregateModels.MediaItemSummary",
|
||||
b =>
|
||||
{
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SortTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Subtitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
});
|
||||
|
||||
modelBuilder.Entity(
|
||||
@@ -205,6 +218,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MediaSourceId");
|
||||
@@ -631,6 +647,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b1.Property<int?>("SeasonNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("SortTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("Subtitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace ErsatzTV.Infrastructure.Plex
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
@@ -2,18 +2,26 @@
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DTO/@EntryIndexedValue">DTO</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HDHR/@EntryIndexedValue">HDHR</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SAR/@EntryIndexedValue">SAR</s:String>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=anull/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=apad/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=bufsize/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=cgop/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Deinterlace/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=deinterlaced/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=discardcorrupt/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=drawtext/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ersatztv/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=etvignore/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=faststart/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=featurettes/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ffconcat/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=fflags/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ffprobe/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=fontfile/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fprobe/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=genpts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=igndts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Libavfilter/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=libx/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=maxrate/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
@@ -21,5 +29,10 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=mpegts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=muxdelay/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=muxpreload/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=nostats/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pixfmt/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=playout/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Playouts/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Playouts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=probesize/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=setsar/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=yadif/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
28
ErsatzTV/Controllers/PostersController.cs
Normal file
28
ErsatzTV/Controllers/PostersController.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.Images;
|
||||
using ErsatzTV.Application.Images.Queries;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ErsatzTV.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class PostersController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public PostersController(IMediator mediator) => _mediator = mediator;
|
||||
|
||||
[HttpGet("/posters/{fileName}")]
|
||||
public async Task<IActionResult> GetImage(string fileName)
|
||||
{
|
||||
Either<BaseError, ImageViewModel> imageContents = await _mediator.Send(new GetImageContents(fileName));
|
||||
return imageContents.Match<IActionResult>(
|
||||
Left: _ => new NotFoundResult(),
|
||||
Right: r => new FileContentResult(r.Contents, r.MimeType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,12 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MudBlazor" Version="5.0.0" />
|
||||
<PackageReference Include="MudBlazor" Version="5.0.1" />
|
||||
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.1" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SQLite" Version="5.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.0.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
@page "/media/sources/local/add"
|
||||
@using ErsatzTV.Application
|
||||
@using ErsatzTV.Application.MediaSources
|
||||
@using ErsatzTV.Application.MediaSources.Commands
|
||||
@using ErsatzTV.Application.MediaSources
|
||||
@using ErsatzTV.Core.Metadata
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ILogger<LocalMediaSourceEditor> Logger
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IMediator Mediator
|
||||
@inject IEntityLocker Locker
|
||||
@inject ChannelWriter<IBackgroundServiceRequest> Channel
|
||||
|
||||
<div style="max-width: 400px;">
|
||||
@@ -68,7 +69,13 @@
|
||||
_messageStore.Clear();
|
||||
if (_editContext.Validate())
|
||||
{
|
||||
var command = new CreateLocalMediaSource(_model.Folder, _model.MediaType, _model.Folder);
|
||||
var command = new CreateLocalMediaSource(
|
||||
Convert.ToBase64String(Guid.NewGuid().ToByteArray())
|
||||
.TrimEnd('=')
|
||||
.Replace("/", "_")
|
||||
.Replace("+", "-"),
|
||||
_model.MediaType,
|
||||
_model.Folder);
|
||||
Either<BaseError, MediaSourceViewModel> result = await Mediator.Send(command);
|
||||
await result.Match(
|
||||
Left: error =>
|
||||
@@ -79,10 +86,15 @@
|
||||
},
|
||||
Right: async vm =>
|
||||
{
|
||||
await Channel.WriteAsync(new ScanLocalMediaSource(vm.Id));
|
||||
NavigationManager.NavigateTo("/media/sources");
|
||||
if (Locker.LockMediaSource(vm.Id))
|
||||
{
|
||||
await Channel.WriteAsync(new ScanLocalMediaSource(vm.Id, ScanningMode.Default));
|
||||
NavigationManager.NavigateTo("/media/sources");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
29
ErsatzTV/Pages/Logs.razor
Normal file
29
ErsatzTV/Pages/Logs.razor
Normal file
@@ -0,0 +1,29 @@
|
||||
@page "/system/logs"
|
||||
@using ErsatzTV.Application.Logs
|
||||
@using ErsatzTV.Application.Logs.Queries
|
||||
@inject IMediator Mediator
|
||||
|
||||
<MudTable FixedHeader="true" Dense="true" Items="_logEntries">
|
||||
<HeaderContent>
|
||||
<MudTh>Timestamp</MudTh>
|
||||
<MudTh>Level</MudTh>
|
||||
<MudTh>Message</MudTh>
|
||||
<MudTh>Properties</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Timestamp">@context.Timestamp</MudTd>
|
||||
<MudTd DataLabel="Level">@context.Level</MudTd>
|
||||
<MudTd DataLabel="Message">@context.RenderedMessage</MudTd>
|
||||
<MudTd DataLabel="Message">@context.Properties</MudTd>
|
||||
</RowTemplate>
|
||||
<PagerContent>
|
||||
<MudTablePager/>
|
||||
</PagerContent>
|
||||
</MudTable>
|
||||
|
||||
@code {
|
||||
private List<LogEntryViewModel> _logEntries;
|
||||
|
||||
protected override async Task OnInitializedAsync() => _logEntries = await Mediator.Send(new GetRecentLogEntries());
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
@page "/media/items"
|
||||
|
||||
<MudTabs Elevation="1">
|
||||
<MudTabPanel Text="TV Shows">
|
||||
<MediaItemTable MediaType="@MediaType.TvShow"/>
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="Movies">
|
||||
<MediaItemTable MediaType="@MediaType.Movie"/>
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="Other">
|
||||
<MediaItemTable MediaType="@MediaType.Other"/>
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
8
ErsatzTV/Pages/MediaMovieItems.razor
Normal file
8
ErsatzTV/Pages/MediaMovieItems.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@page "/media/movies/items"
|
||||
@inject IMediator Mediator
|
||||
|
||||
<MediaItemsGrid MediaType="@MediaType.Movie"/>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
8
ErsatzTV/Pages/MediaOtherItems.razor
Normal file
8
ErsatzTV/Pages/MediaOtherItems.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@page "/media/other/items"
|
||||
@inject IMediator Mediator
|
||||
|
||||
<MediaItemsGrid MediaType="@MediaType.Other"/>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
8
ErsatzTV/Pages/MediaTvItems.razor
Normal file
8
ErsatzTV/Pages/MediaTvItems.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@page "/media/tv/items"
|
||||
@inject IMediator Mediator
|
||||
|
||||
<MediaItemsGrid MediaType="@MediaType.TvShow"/>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -65,7 +65,7 @@
|
||||
errorMessage.HeadOrNone().Match(
|
||||
error =>
|
||||
{
|
||||
Snackbar.Add(error.Value);
|
||||
Snackbar.Add(error.Value, Severity.Error);
|
||||
Logger.LogError("Unexpected error saving playout: {Error}", error.Value);
|
||||
},
|
||||
() => NavigationManager.NavigateTo("/playouts"));
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Start Time">
|
||||
<MudText Typo="@(context == _selectedItem ? Typo.subtitle2 : Typo.body2)">
|
||||
@(context.StartType == StartType.Fixed ? context.StartTime : "Dynamic")
|
||||
@(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic")
|
||||
</MudText>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Media Collection">
|
||||
@@ -73,7 +73,7 @@
|
||||
<MudSelectItem Value="@startType">@startType</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTextField Class="mt-3" Label="Start Time" @bind-Value="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)"/>
|
||||
<MudTimePicker Class="mt-3" Label="Start Time" @bind-Time="@_selectedItem.StartTime" For="@(() => _selectedItem.StartTime)" Disabled="@(_selectedItem.StartType == StartType.Dynamic)"/>
|
||||
<MudAutocomplete Class="mt-3" T="MediaCollectionViewModel" Label="Media Collection" @bind-value="_selectedItem.MediaCollection" SearchFunc="@SearchMediaCollections" ToStringFunc="@(c => c?.Name)"/>
|
||||
<MudSelect Class="mt-3" Label="Playout Mode" @bind-Value="@_selectedItem.PlayoutMode" For="@(() => _selectedItem.PlayoutMode)">
|
||||
@foreach (PlayoutMode playoutMode in Enum.GetValues<PlayoutMode>())
|
||||
@@ -82,7 +82,7 @@
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTextField Class="mt-3" Label="Multiple Count" @bind-Value="@_selectedItem.MultipleCount" For="@(() => _selectedItem.MultipleCount)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Multiple)"/>
|
||||
<MudTextField Class="mt-3" Label="Playout Duration" @bind-Value="@_selectedItem.PlayoutDuration" For="@(() => _selectedItem.PlayoutDuration)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
|
||||
<MudTimePicker Class="mt-3" Label="Playout Duration" @bind-Time="@_selectedItem.PlayoutDuration" For="@(() => _selectedItem.PlayoutDuration)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudSwitch Label="Offline Tail" @bind-Checked="@_selectedItem.OfflineTail" For="@(() => _selectedItem.OfflineTail)" Disabled="@(_selectedItem.PlayoutMode != PlayoutMode.Duration)"/>
|
||||
</MudElement>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Start Time">
|
||||
@(context.StartType == StartType.Fixed ? context.StartTime : "Dynamic")
|
||||
@(context.StartType == StartType.Fixed ? context.StartTime?.ToString(@"hh\:mm") ?? string.Empty : "Dynamic")
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Media Collection">@context.MediaCollection.Name</MudTd>
|
||||
<MudTd DataLabel="Media Collection">@context.PlayoutMode</MudTd>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Extensions;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -25,6 +26,7 @@ namespace ErsatzTV
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(Configuration)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.SQLite(FileSystemLayout.LogDatabasePath, retentionPeriod: TimeSpan.FromDays(1))
|
||||
.CreateLogger();
|
||||
|
||||
try
|
||||
|
||||
@@ -63,14 +63,15 @@ namespace ErsatzTV.Services
|
||||
TryCompletePlexPinFlow pinRequest => CompletePinFlow(pinRequest, cancellationToken),
|
||||
SynchronizePlexMediaSources sourcesRequest => SynchronizeSources(
|
||||
sourcesRequest,
|
||||
cancellationToken)
|
||||
cancellationToken),
|
||||
_ => throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}")
|
||||
};
|
||||
|
||||
await requestTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to process poll for Plex auth token request");
|
||||
_logger.LogWarning(ex, "Failed to process plex background service request");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ using System.Threading.Tasks;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.MediaSources.Commands;
|
||||
using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -16,15 +18,18 @@ namespace ErsatzTV.Services
|
||||
public class SchedulerService : IHostedService
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private Timer _timer;
|
||||
|
||||
public SchedulerService(
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
IEntityLocker entityLocker)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_channel = channel;
|
||||
_entityLocker = entityLocker;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
@@ -75,7 +80,12 @@ namespace ErsatzTV.Services
|
||||
|
||||
foreach (int mediaSourceId in localMediaSourceIds)
|
||||
{
|
||||
await _channel.WriteAsync(new ScanLocalMediaSource(mediaSourceId), cancellationToken);
|
||||
if (_entityLocker.LockMediaSource(mediaSourceId))
|
||||
{
|
||||
await _channel.WriteAsync(
|
||||
new ScanLocalMediaSource(mediaSourceId, ScanningMode.Default),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace ErsatzTV.Services
|
||||
RefreshMediaItemMetadata => "metadata",
|
||||
RefreshMediaItemStatistics => "statistics",
|
||||
RefreshMediaItemCollections => "collections",
|
||||
RefreshMediaItemPoster => "poster",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
@using ErsatzTV.Application.MediaSources
|
||||
@using ErsatzTV.Application.MediaSources.Commands
|
||||
@using ErsatzTV.Application.MediaSources.Queries
|
||||
@using ErsatzTV.Core.Metadata
|
||||
@implements IDisposable
|
||||
@inject IDialogService Dialog
|
||||
@inject IMediator Mediator
|
||||
@inject IEntityLocker Locker
|
||||
@inject ChannelWriter<IBackgroundServiceRequest> Channel
|
||||
|
||||
<MudTable Hover="true" Items="_mediaSources">
|
||||
<MudTable Hover="true" Items="_mediaSources" Dense="true">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">Local Media Sources</MudText>
|
||||
</ToolBarContent>
|
||||
<ColGroup>
|
||||
<col/>
|
||||
<col style="width: 60px;"/>
|
||||
<col style="width: 120px;"/>
|
||||
</ColGroup>
|
||||
<HeaderContent>
|
||||
<MudTh>Folder</MudTh>
|
||||
@@ -19,7 +23,29 @@
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Folder">@context.Folder</MudTd>
|
||||
<MudTd>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" OnClick="@(_ => DeleteMediaSource(context))"></MudIconButton>
|
||||
<div style="align-items: center; display: flex;">
|
||||
@if (Locker.IsMediaSourceLocked(context.Id))
|
||||
{
|
||||
<div style="align-items: center; display: flex; height: 48px; justify-content: center; width: 48px;">
|
||||
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="Refresh All Metadata">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
||||
Disabled="@Locker.IsMediaSourceLocked(context.Id)"
|
||||
OnClick="@(_ => RefreshAllMetadata(context))">
|
||||
</MudIconButton>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="Delete Media Source">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Disabled="@Locker.IsMediaSourceLocked(context.Id)"
|
||||
OnClick="@(_ => DeleteMediaSource(context))">
|
||||
</MudIconButton>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
@@ -30,6 +56,9 @@
|
||||
@code {
|
||||
private IList<LocalMediaSourceViewModel> _mediaSources;
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
Locker.OnMediaSourceChanged += LockChanged;
|
||||
|
||||
protected override async Task OnParametersSetAsync() => await LoadMediaSources();
|
||||
|
||||
private async Task LoadMediaSources() =>
|
||||
@@ -42,7 +71,7 @@
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ "EntityType", "media source" },
|
||||
{ "EntityName", mediaSource.Name },
|
||||
{ "EntityName", mediaSource.Folder },
|
||||
{ "DetailText", $"This media source contains {count} media items." },
|
||||
{ "DetailHighlight", count.ToString() }
|
||||
};
|
||||
@@ -57,4 +86,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshAllMetadata(LocalMediaSourceViewModel mediaSource)
|
||||
{
|
||||
if (Locker.LockMediaSource(mediaSource.Id))
|
||||
{
|
||||
await Channel.WriteAsync(new ScanLocalMediaSource(mediaSource.Id, ScanningMode.RescanAll));
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void LockChanged(object sender, EventArgs e) =>
|
||||
InvokeAsync(StateHasChanged);
|
||||
|
||||
void IDisposable.Dispose() => Locker.OnMediaSourceChanged -= LockChanged;
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@inherits LayoutComponentBase
|
||||
@using System.Reflection
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<MudThemeProvider Theme="_ersatzTvTheme"/>
|
||||
<MudDialogProvider DisableBackdropClick="true"/>
|
||||
@@ -27,22 +28,32 @@
|
||||
<MudNavLink Href="/ffmpeg">FFmpeg</MudNavLink>
|
||||
<MudNavGroup Title="Media" Expanded="true">
|
||||
<MudNavLink Href="/media/sources">Media Sources</MudNavLink>
|
||||
<MudNavLink Href="/media/items">Media Items</MudNavLink>
|
||||
<MudNavLink Href="/media/tv/items">TV Shows</MudNavLink>
|
||||
<MudNavLink Href="/media/movies/items">Movies</MudNavLink>
|
||||
<MudNavLink Href="/media/other/items">Other Items</MudNavLink>
|
||||
<MudNavLink Href="/media/collections">Media Collections</MudNavLink>
|
||||
</MudNavGroup>
|
||||
<MudNavLink Href="/schedules">Schedules</MudNavLink>
|
||||
<MudNavLink Href="/playouts">Playouts</MudNavLink>
|
||||
<MudNavLink Href="/system/logs">Logs</MudNavLink>
|
||||
<MudDivider Class="my-6" DividerType="DividerType.Middle"/>
|
||||
<MudContainer Style="text-align: right" Class="mr-6">
|
||||
<MudText Typo="Typo.body2">ErsatzTV Version</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Info">@InfoVersion</MudText>
|
||||
</MudContainer>
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
<MudMainContent>
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="pt-8">
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
@code {
|
||||
bool _drawerOpen = true;
|
||||
private static readonly string InfoVersion = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "unknown";
|
||||
|
||||
private bool _drawerOpen = true;
|
||||
|
||||
private void DrawerToggle() => _drawerOpen = !_drawerOpen;
|
||||
|
||||
|
||||
51
ErsatzTV/Shared/MediaCard.razor
Normal file
51
ErsatzTV/Shared/MediaCard.razor
Normal file
@@ -0,0 +1,51 @@
|
||||
@using ErsatzTV.Application.MediaItems
|
||||
@using ErsatzTV.Application.MediaItems.Commands
|
||||
@using Unit = LanguageExt.Unit
|
||||
@inject IMediator Mediator
|
||||
|
||||
<div class="media-card-container mx-3 pb-3">
|
||||
<MudPaper Class="media-card" Style="@PosterForItem()">
|
||||
@if (string.IsNullOrWhiteSpace(Data.Poster))
|
||||
{
|
||||
<MudText Align="Align.Center" Typo="Typo.h1" Class="media-card-poster-placeholder mud-text-disabled">
|
||||
@Placeholder(Data.SortTitle)
|
||||
</MudText>
|
||||
}
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Color="Color.Primary" OnClick="@(() => RefreshMetadata())" Class="media-card-menu"></MudIconButton>
|
||||
</MudPaper>
|
||||
<MudText Align="Align.Center" Class="media-card-title" UserAttributes="@(new Dictionary<string, object> { { "title", Data.Title } })">
|
||||
@Data.Title
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">
|
||||
@Data.Subtitle
|
||||
</MudText>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public AggregateMediaItemViewModel Data { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Unit> DataRefreshed { get; set; }
|
||||
|
||||
private string Placeholder(string sortTitle)
|
||||
{
|
||||
string first = sortTitle.Substring(0, 1).ToUpperInvariant();
|
||||
return int.TryParse(first, out _) ? "#" : first;
|
||||
}
|
||||
|
||||
private string PosterForItem() => string.IsNullOrWhiteSpace(Data.Poster)
|
||||
? "position: relative"
|
||||
: $"position: relative; background-image: url(/posters/{Data.Poster}); background-size: cover";
|
||||
|
||||
private async Task RefreshMetadata()
|
||||
{
|
||||
// TODO: how should we refresh an entire television show?
|
||||
await Mediator.Send(new RefreshMediaItemMetadata(Data.MediaItemId));
|
||||
await Mediator.Send(new RefreshMediaItemCollections(Data.MediaItemId));
|
||||
await Mediator.Send(new RefreshMediaItemPoster(Data.MediaItemId));
|
||||
await DataRefreshed.InvokeAsync();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
@using ErsatzTV.Application.MediaItems
|
||||
@using ErsatzTV.Application.MediaItems.Queries
|
||||
@inject IMediator Mediator
|
||||
|
||||
<MudTable @ref="_table" Hover="true" ServerData="@(new Func<TableState, Task<TableData<AggregateMediaItemViewModel>>>(ServerReload))">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">Media Items</MudText>
|
||||
<MudToolBarSpacer/>
|
||||
<MudTextField T="string" ValueChanged="@OnSearch" Placeholder="Search" Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0">
|
||||
</MudTextField>
|
||||
</ToolBarContent>
|
||||
<HeaderContent>
|
||||
<MudTh>Source</MudTh>
|
||||
<MudTh>Title</MudTh>
|
||||
<MudTh>Count</MudTh>
|
||||
<MudTh>Duration</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Source">@context.Source</MudTd>
|
||||
<MudTd DataLabel="Title">@context.Title</MudTd>
|
||||
<MudTd DataLabel="Count">@context.Count</MudTd>
|
||||
<MudTd DataLabel="Duration">@context.Duration</MudTd>
|
||||
</RowTemplate>
|
||||
<PagerContent>
|
||||
<MudTablePager/>
|
||||
</PagerContent>
|
||||
</MudTable>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public MediaType MediaType { get; set; }
|
||||
|
||||
private IEnumerable<AggregateMediaItemViewModel> _pagedData;
|
||||
private MudTable<AggregateMediaItemViewModel> _table;
|
||||
|
||||
private int _totalItems;
|
||||
private string _searchString;
|
||||
|
||||
private async Task<TableData<AggregateMediaItemViewModel>> ServerReload(TableState state)
|
||||
{
|
||||
List<AggregateMediaItemViewModel> aggregateData =
|
||||
await Mediator.Send(new GetAggregateMediaItems(MediaType, _searchString));
|
||||
|
||||
_totalItems = aggregateData.Count;
|
||||
|
||||
_pagedData = aggregateData.Skip(state.Page * state.PageSize).Take(state.PageSize);
|
||||
return new TableData<AggregateMediaItemViewModel> { TotalItems = _totalItems, Items = _pagedData };
|
||||
}
|
||||
|
||||
private async Task OnSearch(string text)
|
||||
{
|
||||
_searchString = text;
|
||||
await _table.ReloadServerData();
|
||||
}
|
||||
|
||||
private class MediaItemAggregate
|
||||
{
|
||||
public string Source { get; set; }
|
||||
public string Title { get; set; }
|
||||
public int Count { get; set; }
|
||||
public string Duration { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
55
ErsatzTV/Shared/MediaItemsGrid.razor
Normal file
55
ErsatzTV/Shared/MediaItemsGrid.razor
Normal file
@@ -0,0 +1,55 @@
|
||||
@using ErsatzTV.Application.MediaItems
|
||||
@using ErsatzTV.Application.MediaItems.Queries
|
||||
@inject IMediator Mediator
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Small" Class="mb-6" Style="max-width: 300px">
|
||||
<MudPaper Style="align-items: center; display: flex; justify-content: center;">
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft"
|
||||
OnClick="@(() => PrevPage())"
|
||||
Disabled="@(_pageNumber <= 1)">
|
||||
</MudIconButton>
|
||||
<MudText Style="flex-grow: 1"
|
||||
Align="Align.Center">
|
||||
@Math.Min((_pageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, _pageNumber * PageSize) of @_data.Count
|
||||
</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronRight"
|
||||
OnClick="@(() => NextPage())" Disabled="@(_pageNumber * PageSize >= _data.Count)">
|
||||
</MudIconButton>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid">
|
||||
@foreach (AggregateMediaItemViewModel item in _data.DataPage)
|
||||
{
|
||||
<MediaCard Data="@item" DataRefreshed="@(() => RefreshData())"/>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public MediaType MediaType { get; set; }
|
||||
|
||||
private int PageSize => 100;
|
||||
private int _pageNumber = 1;
|
||||
|
||||
private AggregateMediaItemResults _data;
|
||||
|
||||
protected override Task OnParametersSetAsync() => RefreshData();
|
||||
|
||||
private async Task RefreshData() =>
|
||||
_data = await Mediator.Send(new GetAggregateMediaItems(MediaType, _pageNumber, PageSize));
|
||||
|
||||
private async Task PrevPage()
|
||||
{
|
||||
_pageNumber -= 1;
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
private async Task NextPage()
|
||||
{
|
||||
_pageNumber += 1;
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application;
|
||||
using ErsatzTV.Application.Channels.Queries;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -15,6 +18,8 @@ using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Formatters;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Data.Repositories;
|
||||
using ErsatzTV.Infrastructure.Images;
|
||||
using ErsatzTV.Infrastructure.Locking;
|
||||
using ErsatzTV.Infrastructure.Plex;
|
||||
using ErsatzTV.Serialization;
|
||||
using ErsatzTV.Services;
|
||||
@@ -75,6 +80,11 @@ namespace ErsatzTV
|
||||
|
||||
services.AddMudServices();
|
||||
|
||||
Log.Logger.Information(
|
||||
"ErsatzTV version {Version}",
|
||||
Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
?.InformationalVersion ?? "unknown");
|
||||
|
||||
Log.Logger.Warning("This is pre-alpha software and is likely to be unstable");
|
||||
Log.Logger.Warning(
|
||||
"Give feedback at {GitHub} or {Discord}",
|
||||
@@ -108,6 +118,9 @@ namespace ErsatzTV
|
||||
o.MigrationsAssembly("ErsatzTV.Infrastructure");
|
||||
}));
|
||||
|
||||
services.AddDbContext<LogContext>(
|
||||
options => options.UseSqlite($"Data Source={FileSystemLayout.LogDatabasePath}"));
|
||||
|
||||
services.AddMediatR(typeof(GetAllChannels).Assembly);
|
||||
|
||||
services.AddRefitClient<IPlexTvApi>()
|
||||
@@ -144,6 +157,7 @@ namespace ErsatzTV
|
||||
services.AddSingleton<IPlexSecretStore, PlexSecretStore>();
|
||||
services.AddSingleton<IPlexTvApiClient, PlexTvApiClient>(); // TODO: does this need to be singleton?
|
||||
services.AddSingleton<IPlexServerApiClient, PlexServerApiClient>();
|
||||
services.AddSingleton<IEntityLocker, EntityLocker>();
|
||||
AddChannel<IBackgroundServiceRequest>(services);
|
||||
AddChannel<IPlexBackgroundServiceRequest>(services);
|
||||
|
||||
@@ -156,12 +170,16 @@ namespace ErsatzTV
|
||||
services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
|
||||
services.AddScoped<IProgramScheduleRepository, ProgramScheduleRepository>();
|
||||
services.AddScoped<IPlayoutRepository, PlayoutRepository>();
|
||||
services.AddScoped<ILogRepository, LogRepository>();
|
||||
services.AddScoped<IFFmpegLocator, FFmpegLocator>();
|
||||
services.AddScoped<ISmartCollectionBuilder, SmartCollectionBuilder>();
|
||||
services.AddScoped<ILocalMetadataProvider, LocalMetadataProvider>();
|
||||
services.AddScoped<ILocalStatisticsProvider, LocalStatisticsProvider>();
|
||||
services.AddScoped<ILocalPosterProvider, LocalPosterProvider>();
|
||||
services.AddScoped<ILocalMediaScanner, LocalMediaScanner>();
|
||||
services.AddScoped<IPlayoutBuilder, PlayoutBuilder>();
|
||||
services.AddScoped<IImageCache, ImageCache>();
|
||||
services.AddScoped<ILocalFileSystem, LocalFileSystem>();
|
||||
|
||||
services.AddHostedService<PlexService>();
|
||||
services.AddHostedService<FFmpegLocatorService>();
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
@using MudBlazor
|
||||
@using MudBlazor.Dialog
|
||||
@using ErsatzTV
|
||||
@using ErsatzTV.Application
|
||||
@using ErsatzTV.Core
|
||||
@using ErsatzTV.Core.Domain
|
||||
@using ErsatzTV.Core.Interfaces.Locking
|
||||
@using ErsatzTV.Infrastructure.Data
|
||||
@using ErsatzTV.Shared
|
||||
@using ErsatzTV.ViewModels
|
||||
@@ -1 +1,37 @@
|
||||
|
||||
.media-card-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.media-card-container { width: 152px; }
|
||||
|
||||
.media-card {
|
||||
display: flex;
|
||||
filter: brightness(100%);
|
||||
flex-direction: column;
|
||||
height: 220px;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
width: 152px;
|
||||
}
|
||||
|
||||
.media-card:hover { filter: brightness(80%); }
|
||||
|
||||
.media-card-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media-card-poster-placeholder { font-weight: bold; }
|
||||
|
||||
.media-card-menu {
|
||||
bottom: 0;
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.media-card:hover .media-card-menu { display: block; }
|
||||
@@ -4,6 +4,8 @@ services:
|
||||
ersatztv:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
INFO_VERSION: "docker-compose-develop"
|
||||
ports:
|
||||
- "8409:8409"
|
||||
volumes:
|
||||
|
||||
Reference in New Issue
Block a user