Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fb23f2edb | ||
|
|
1aac2f13c9 | ||
|
|
2c9d4d796a | ||
|
|
9d40caebd6 | ||
|
|
0b5a6f9dcd | ||
|
|
76495c1f7b | ||
|
|
d0d1186b92 | ||
|
|
04ab4ee60f | ||
|
|
e62074cc26 | ||
|
|
db054ece24 | ||
|
|
c2d8a54a47 | ||
|
|
88b645af2d | ||
|
|
941f1a59ee | ||
|
|
a3e20826a5 | ||
|
|
ebff29d6cd | ||
|
|
5a29fc1cbb | ||
|
|
f4487eb422 | ||
|
|
d04f3d574b | ||
|
|
a2e85655d2 | ||
|
|
4d49289dbd |
@@ -21,6 +21,9 @@ jobs:
|
||||
with:
|
||||
dotnet-version: 5.0.x
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
|
||||
@@ -39,23 +39,29 @@ jobs:
|
||||
# Define some variables for things we need
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
release_name="ErsatzTV-$tag-${{ matrix.target }}"
|
||||
release_name_cli="ErsatzTV.CommandLine-$tag-${{ matrix.target }}"
|
||||
|
||||
# Build everything
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name"
|
||||
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
|
||||
7z a -tzip "${release_name}.zip" "./${release_name}/*"
|
||||
7z a -tzip "${release_name_cli}.zip" "./${release_name_cli}/*"
|
||||
else
|
||||
tar czvf "${release_name}.tar.gz" "$release_name"
|
||||
tar czvf "${release_name_cli}.tar.gz" "$release_name_cli"
|
||||
fi
|
||||
|
||||
# Delete output directory
|
||||
rm -r "$release_name"
|
||||
rm -r "$release_name_cli"
|
||||
|
||||
- name: Publish
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
files: |
|
||||
ErsatzTV*.zip
|
||||
ErsatzTV*.tar.gz
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<InformationalVersion>develop</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
+2
-1
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,9 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public record AddItemsToSimpleMediaCollection
|
||||
(int MediaCollectionId, List<int> ItemIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
AddItemsToSimpleMediaCollectionHandler : MediatR.IRequestHandler<AddItemsToSimpleMediaCollection,
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public AddItemsToSimpleMediaCollectionHandler(
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
IMediaItemRepository mediaItemRepository)
|
||||
{
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
AddItemsToSimpleMediaCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(ApplyAddItemsRequest)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> ApplyAddItemsRequest(RequestParameters parameters)
|
||||
{
|
||||
foreach (MediaItem item in parameters.ItemsToAdd.Where(
|
||||
item => parameters.Collection.Items.All(i => i.Id != item.Id)))
|
||||
{
|
||||
parameters.Collection.Items.Add(item);
|
||||
}
|
||||
|
||||
await _mediaCollectionRepository.Update(parameters.Collection);
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>>
|
||||
Validate(AddItemsToSimpleMediaCollection request) =>
|
||||
(await SimpleMediaCollectionMustExist(request), await ValidateItems(request))
|
||||
.Apply(
|
||||
(simpleMediaCollectionToUpdate, itemsToAdd) =>
|
||||
new RequestParameters(simpleMediaCollectionToUpdate, itemsToAdd));
|
||||
|
||||
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
|
||||
AddItemsToSimpleMediaCollection updateSimpleMediaCollection) =>
|
||||
_mediaCollectionRepository.GetSimpleMediaCollection(updateSimpleMediaCollection.MediaCollectionId)
|
||||
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
|
||||
|
||||
private Task<Validation<BaseError, List<MediaItem>>> ValidateItems(
|
||||
AddItemsToSimpleMediaCollection request) =>
|
||||
LoadAllMediaItems(request)
|
||||
.Map(v => v.ToValidation<BaseError>("MediaItem does not exist"));
|
||||
|
||||
private async Task<Option<List<MediaItem>>> LoadAllMediaItems(AddItemsToSimpleMediaCollection request)
|
||||
{
|
||||
var items = (await request.ItemIds.Map(async id => await _mediaItemRepository.Get(id)).Sequence())
|
||||
.ToList();
|
||||
if (items.Any(i => i.IsNone))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
return items.Somes().ToList();
|
||||
}
|
||||
|
||||
private record RequestParameters(SimpleMediaCollection Collection, List<MediaItem> ItemsToAdd);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||
{
|
||||
public record GetSimpleMediaCollectionWithItemsById
|
||||
(int Id) : IRequest<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static LanguageExt.Prelude;
|
||||
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||
{
|
||||
public class GetSimpleMediaCollectionWithItemsByIdHandler : IRequestHandler<GetSimpleMediaCollectionWithItemsById,
|
||||
Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>
|
||||
{
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public GetSimpleMediaCollectionWithItemsByIdHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
|
||||
public async Task<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>> Handle(
|
||||
GetSimpleMediaCollectionWithItemsById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Option<SimpleMediaCollection> maybeCollection =
|
||||
await _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(request.Id);
|
||||
|
||||
return maybeCollection.Match<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>(
|
||||
c => Tuple(ProjectToViewModel(c), c.Items.Map(ProjectToSearchViewModel).ToList()),
|
||||
None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
@@ -49,8 +52,10 @@ namespace ErsatzTV.Application.MediaItems.Commands
|
||||
await _mediaItemRepository.Add(parameters.MediaItem);
|
||||
|
||||
await _localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem);
|
||||
await _localMetadataProvider.RefreshMetadata(parameters.MediaItem);
|
||||
await _smartCollectionBuilder.RefreshSmartCollections(parameters.MediaItem);
|
||||
// TODO: reimplement this
|
||||
// await _localMetadataProvider.RefreshMetadata(parameters.MediaItem);
|
||||
// await _localPosterProvider.RefreshPoster(parameters.MediaItem);
|
||||
// await _smartCollectionBuilder.RefreshSmartCollections(parameters.MediaItem);
|
||||
|
||||
return ProjectToViewModel(parameters.MediaItem);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ namespace ErsatzTV.Application.MediaItems.Commands
|
||||
.Filter(item => File.Exists(item.Path))
|
||||
.ToValidation<BaseError>($"[Path] '{mediaItem.Path}' does not exist on the file system");
|
||||
|
||||
private Task<Unit> RefreshMetadata(MediaItem mediaItem) =>
|
||||
_localMetadataProvider.RefreshMetadata(mediaItem).ToUnit();
|
||||
private Task<Unit> RefreshMetadata(MediaItem mediaItem) => Task.CompletedTask.ToUnit();
|
||||
// TODO: reimplement this
|
||||
// _localMetadataProvider.RefreshMetadata(mediaItem).ToUnit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
@@ -9,5 +10,33 @@ namespace ErsatzTV.Application.MediaItems
|
||||
mediaItem.Id,
|
||||
mediaItem.MediaSourceId,
|
||||
mediaItem.Path);
|
||||
|
||||
internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) =>
|
||||
new(
|
||||
mediaItem.Id,
|
||||
GetSourceName(mediaItem.Source),
|
||||
mediaItem.Metadata.MediaType.ToString(),
|
||||
GetDisplayTitle(mediaItem),
|
||||
GetDisplayDuration(mediaItem));
|
||||
|
||||
|
||||
private static string GetDisplayTitle(this MediaItem mediaItem) =>
|
||||
mediaItem.Metadata.MediaType == MediaType.TvShow &&
|
||||
Optional(mediaItem.Metadata.SeasonNumber).IsSome &&
|
||||
Optional(mediaItem.Metadata.EpisodeNumber).IsSome
|
||||
? $"{mediaItem.Metadata.Title} s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}"
|
||||
: mediaItem.Metadata.Title;
|
||||
|
||||
private static string GetDisplayDuration(MediaItem mediaItem) =>
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
public record MediaItemSearchResultViewModel(
|
||||
int Id,
|
||||
string Source,
|
||||
string MediaType,
|
||||
string Title,
|
||||
string Duration);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -2,42 +2,42 @@
|
||||
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) == 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||
|
||||
@@ -15,9 +16,9 @@ namespace ErsatzTV.Application.MediaItems.Queries
|
||||
public GetAllMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public async Task<List<MediaItemViewModel>> Handle(
|
||||
public Task<List<MediaItemViewModel>> Handle(
|
||||
GetAllMediaItems request,
|
||||
CancellationToken cancellationToken) =>
|
||||
(await _mediaItemRepository.GetAll()).Map(ProjectToViewModel).ToList();
|
||||
_mediaItemRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public record SearchAllMediaItems(string SearchString) : IRequest<List<MediaItemSearchResultViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class SearchAllMediaItemsHandler : IRequestHandler<SearchAllMediaItems, List<MediaItemSearchResultViewModel>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public SearchAllMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public Task<List<MediaItemSearchResultViewModel>>
|
||||
Handle(SearchAllMediaItems request, CancellationToken cancellationToken) =>
|
||||
_mediaItemRepository.Search(request.SearchString).Map(list => list.Map(ProjectToSearchViewModel).ToList());
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace ErsatzTV.Application.Playouts
|
||||
? $"{mediaItem.Metadata.Title} s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}"
|
||||
: mediaItem.Metadata.Title;
|
||||
|
||||
public static string GetDisplayDuration(MediaItem mediaItem) =>
|
||||
private static string GetDisplayDuration(MediaItem mediaItem) =>
|
||||
string.Format(
|
||||
mediaItem.Metadata.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
||||
mediaItem.Metadata.Duration);
|
||||
|
||||
@@ -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}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
{
|
||||
[TestFixture]
|
||||
public class FFmpegPlaybackSettingsCalculatorTests
|
||||
{
|
||||
public class CalculateSettings
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollection(int id) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<Option<TelevisionMediaCollection>> GetTelevisionMediaCollection(int id) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
@@ -42,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();
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Metadata
|
||||
{
|
||||
[TestFixture]
|
||||
public class FallbackMetadataProviderTests
|
||||
{
|
||||
[Test]
|
||||
[TestCase("Awesome Show - s01e02.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S01E02.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - s1e2.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S1E2.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - s01e02 - Episode Title.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S01E02 - Episode Title.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - s1e2 - Episode Title.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S1E2 - Episode Title.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - s01e02 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - S01E02 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - s1e2 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
|
||||
[TestCase("Awesome Show (2021) - S1E2 - Episode Title.mkv", "Awesome Show (2021)", 1, 2)]
|
||||
[TestCase("Awesome Show - s01e02 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S01E02 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - s1e2 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase("Awesome Show - S1E2 - Episode Title-720p.mkv", "Awesome Show", 1, 2)]
|
||||
[TestCase(
|
||||
"Awesome Show (2021) - S01E02 - Description; More Description (1080p QUALITY codec GROUP).mkv",
|
||||
"Awesome Show (2021)",
|
||||
1,
|
||||
2)]
|
||||
[TestCase(
|
||||
"Awesome.Show.S01E02.Description.more.Description.QUAlity.codec.CODEC-GROUP.mkv",
|
||||
"Awesome.Show",
|
||||
1,
|
||||
2)]
|
||||
public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, string title, int season, int episode)
|
||||
{
|
||||
MediaMetadata metadata = FallbackMetadataProvider.GetFallbackMetadata(
|
||||
new MediaItem { Path = path, Source = new LocalMediaSource { MediaType = MediaType.TvShow } });
|
||||
|
||||
metadata.MediaType.Should().Be(MediaType.TvShow);
|
||||
metadata.Title.Should().Be(title);
|
||||
metadata.SeasonNumber.Should().Be(season);
|
||||
metadata.EpisodeNumber.Should().Be(episode);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
[TestFixture]
|
||||
public class ChronologicalContentTests
|
||||
{
|
||||
[Test]
|
||||
|
||||
@@ -14,6 +14,7 @@ using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
[TestFixture]
|
||||
public class PlayoutBuilderTests
|
||||
{
|
||||
private readonly ILogger<PlayoutBuilder> _logger;
|
||||
|
||||
@@ -9,6 +9,7 @@ using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
[TestFixture]
|
||||
public class RandomizedContentTests
|
||||
{
|
||||
private const int KnownSeed = 22295;
|
||||
|
||||
@@ -9,6 +9,7 @@ using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling
|
||||
{
|
||||
[TestFixture]
|
||||
public class ShuffledContentTests
|
||||
{
|
||||
// this seed will produce (shuffle) 1-10 in order
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Core.AggregateModels
|
||||
{
|
||||
public record MediaItemSummary(int MediaItemId, string Title, string SortTitle, string Subtitle, string Poster);
|
||||
}
|
||||
@@ -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,8 @@ 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 DateTime? PosterLastWriteTime { get; set; }
|
||||
public MediaMetadata Metadata { get; set; }
|
||||
public DateTime? LastWriteTime { get; set; }
|
||||
public IList<SimpleMediaCollection> SimpleMediaCollections { get; set; }
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public record MediaMetadata : IDisplaySize
|
||||
{
|
||||
public MetadataSource Source { get; set; }
|
||||
public DateTime? LastWriteTime { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
public string SampleAspectRatio { get; set; }
|
||||
public string DisplayAspectRatio { get; set; }
|
||||
@@ -12,6 +14,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; }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public enum MetadataSource
|
||||
{
|
||||
Fallback = 0,
|
||||
Sidecar = 1
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -19,6 +19,11 @@ namespace ErsatzTV.Core.Hdhr
|
||||
|
||||
public string GuideNumber => _channel.Number.ToString();
|
||||
public string GuideName => _channel.Name;
|
||||
public string URL => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}";
|
||||
|
||||
public string URL => _channel.StreamingMode switch
|
||||
{
|
||||
StreamingMode.HttpLiveStreaming => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.m3u8",
|
||||
_ => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.ts"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalFileSystem
|
||||
{
|
||||
public DateTime GetLastWriteTime(string path);
|
||||
public bool IsMediaSourceAccessible(LocalMediaSource localMediaSource);
|
||||
public Seq<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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalMediaSourcePlanner
|
||||
{
|
||||
public Seq<LocalMediaSourcePlan> DetermineActions(
|
||||
MediaType mediaType,
|
||||
Seq<MediaItem> mediaItems,
|
||||
Seq<string> files);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalMetadataProvider
|
||||
{
|
||||
Task RefreshMetadata(MediaItem mediaItem);
|
||||
Task RefreshSidecarMetadata(MediaItem mediaItem, string path);
|
||||
Task RefreshFallbackMetadata(MediaItem mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
public Task<SimpleMediaCollection> Add(SimpleMediaCollection collection);
|
||||
public Task<Option<MediaCollection>> Get(int id);
|
||||
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollection(int id);
|
||||
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id);
|
||||
public Task<Option<TelevisionMediaCollection>> GetTelevisionMediaCollection(int id);
|
||||
public Task<List<SimpleMediaCollection>> GetSimpleMediaCollections();
|
||||
public Task<List<MediaCollection>> GetAll();
|
||||
@@ -19,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;
|
||||
|
||||
@@ -10,9 +11,11 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
public Task<int> Add(MediaItem mediaItem);
|
||||
public Task<Option<MediaItem>> Get(int id);
|
||||
public Task<List<MediaItem>> GetAll();
|
||||
public Task<List<MediaItem>> GetAll(MediaType mediaType);
|
||||
public Task<List<MediaItem>> Search(string searchString);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
@@ -22,20 +23,32 @@ namespace ErsatzTV.Core.Iptv
|
||||
|
||||
public string ToXml()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<?xml version=\"1.0\"?>");
|
||||
sb.AppendLine("<tv generator-info-name=\"ersatztv\">");
|
||||
using var ms = new MemoryStream();
|
||||
using var xml = XmlWriter.Create(ms);
|
||||
xml.WriteStartDocument();
|
||||
|
||||
xml.WriteStartElement("tv");
|
||||
xml.WriteAttributeString("generator-info-name", "ersatztv");
|
||||
|
||||
foreach (Channel channel in _channels)
|
||||
{
|
||||
sb.AppendLine($"<channel id=\"{channel.Number}\">");
|
||||
sb.AppendLine($"<display-name lang=\"en\">{channel.Name}</display-name>");
|
||||
sb.AppendLine(
|
||||
!string.IsNullOrWhiteSpace(channel.Logo)
|
||||
? $"<icon src=\"{_scheme}://{_host}/iptv/images/{channel.Logo}\"/>"
|
||||
: $"<icon src=\"{_scheme}://{_host}/images/ersatztv-500.png\"/>");
|
||||
xml.WriteStartElement("channel");
|
||||
xml.WriteAttributeString("id", channel.Number.ToString());
|
||||
|
||||
sb.AppendLine("</channel>");
|
||||
xml.WriteStartElement("display-name");
|
||||
xml.WriteAttributeString("lang", "en");
|
||||
xml.WriteString(channel.Name);
|
||||
xml.WriteEndElement(); // display-name
|
||||
|
||||
xml.WriteStartElement("icon");
|
||||
xml.WriteAttributeString(
|
||||
"src",
|
||||
!string.IsNullOrWhiteSpace(channel.Logo)
|
||||
? $"{_scheme}://{_host}/iptv/images/{channel.Logo}"
|
||||
: $"{_scheme}://{_host}/images/ersatztv-500.png");
|
||||
xml.WriteEndElement(); // icon
|
||||
|
||||
xml.WriteEndElement(); // channel
|
||||
}
|
||||
|
||||
foreach (Channel channel in _channels)
|
||||
@@ -50,37 +63,58 @@ namespace ErsatzTV.Core.Iptv
|
||||
Title = Path.GetFileName(playoutItem.MediaItem.Path)
|
||||
});
|
||||
|
||||
sb.AppendLine(
|
||||
$"<programme start=\"{start}\" stop=\"{stop}\" channel=\"{channel.Number}\">");
|
||||
sb.AppendLine($"<title lang=\"en\">{metadata.Title}</title>");
|
||||
sb.AppendLine("<previously-shown/>");
|
||||
sb.AppendLine("<sub-title lang=\"en\"></sub-title>");
|
||||
xml.WriteStartElement("programme");
|
||||
xml.WriteAttributeString("start", start);
|
||||
xml.WriteAttributeString("stop", stop);
|
||||
xml.WriteAttributeString("channel", channel.Number.ToString());
|
||||
|
||||
xml.WriteStartElement("title");
|
||||
xml.WriteAttributeString("lang", "en");
|
||||
xml.WriteString(metadata.Title);
|
||||
xml.WriteEndElement(); // title
|
||||
|
||||
xml.WriteStartElement("previously-shown");
|
||||
xml.WriteEndElement(); // previously-shown
|
||||
|
||||
xml.WriteStartElement("sub-title");
|
||||
xml.WriteAttributeString("lang", "en");
|
||||
xml.WriteEndElement(); // sub-title
|
||||
|
||||
int season = Optional(metadata.SeasonNumber).IfNone(0);
|
||||
int episode = Optional(metadata.EpisodeNumber).IfNone(0);
|
||||
if (season > 0 && episode > 0)
|
||||
{
|
||||
sb.AppendLine($"<episode-num system=\"xmltv_ns\">{season - 1}.{episode - 1}.0/1</episode-num>");
|
||||
xml.WriteStartElement("episode-num");
|
||||
xml.WriteAttributeString("system", "xmltv_ns");
|
||||
xml.WriteString($"{season - 1}.{episode - 1}.0/1");
|
||||
xml.WriteEndElement(); // episode-num
|
||||
}
|
||||
|
||||
// sb.AppendLine("<icon src=\"\"/>");
|
||||
sb.AppendLine($"<desc lang=\"en\">{metadata.Description}</desc>");
|
||||
xml.WriteStartElement("desc");
|
||||
xml.WriteAttributeString("lang", "en");
|
||||
xml.WriteString(metadata.Description);
|
||||
xml.WriteEndElement(); // desc
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.ContentRating))
|
||||
{
|
||||
sb.AppendLine("<rating system=\"MPAA\">");
|
||||
sb.AppendLine($"<value>{metadata.ContentRating}</value>");
|
||||
sb.AppendLine("</rating>");
|
||||
xml.WriteStartElement("rating");
|
||||
xml.WriteAttributeString("system", "MPAA");
|
||||
xml.WriteStartElement("value");
|
||||
xml.WriteString(metadata.ContentRating);
|
||||
xml.WriteEndElement(); // value
|
||||
xml.WriteEndElement(); // rating
|
||||
}
|
||||
|
||||
sb.AppendLine("</programme>");
|
||||
xml.WriteEndElement(); // programme
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("</tv>");
|
||||
xml.WriteEndElement(); // tv
|
||||
xml.WriteEndDocument();
|
||||
|
||||
|
||||
return sb.ToString();
|
||||
xml.Flush();
|
||||
return Encoding.UTF8.GetString(ms.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public record ActionPlan(string TargetPath, ScanningAction TargetAction);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public static class FallbackMetadataProvider
|
||||
{
|
||||
public static MediaMetadata GetFallbackMetadata(MediaItem mediaItem)
|
||||
{
|
||||
string fileName = Path.GetFileName(mediaItem.Path);
|
||||
var metadata = new MediaMetadata { Source = MetadataSource.Fallback, Title = fileName ?? mediaItem.Path };
|
||||
|
||||
if (fileName != null)
|
||||
{
|
||||
if (!(mediaItem.Source is LocalMediaSource localMediaSource))
|
||||
{
|
||||
return metadata;
|
||||
}
|
||||
|
||||
return localMediaSource.MediaType switch
|
||||
{
|
||||
MediaType.TvShow => GetTvShowMetadata(fileName, metadata),
|
||||
MediaType.Movie => GetMovieMetadata(fileName, metadata),
|
||||
_ => metadata
|
||||
};
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private static MediaMetadata GetTvShowMetadata(string fileName, MediaMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
const string PATTERN = @"^(.*?)[.\s-]+[sS](\d+)[eE](\d+).*\.\w+$";
|
||||
Match match = Regex.Match(fileName, PATTERN);
|
||||
if (match.Success)
|
||||
{
|
||||
metadata.MediaType = MediaType.TvShow;
|
||||
metadata.Title = match.Groups[1].Value;
|
||||
metadata.SeasonNumber = int.Parse(match.Groups[2].Value);
|
||||
metadata.EpisodeNumber = int.Parse(match.Groups[3].Value);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private static MediaMetadata GetMovieMetadata(string fileName, MediaMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
const string PATTERN = @"^(.*?)[.\(](\d{4})[.\)].*\.\w+$";
|
||||
Match match = Regex.Match(fileName, PATTERN);
|
||||
if (match.Success)
|
||||
{
|
||||
metadata.MediaType = MediaType.Movie;
|
||||
metadata.Title = match.Groups[1].Value;
|
||||
metadata.Aired = new DateTime(int.Parse(match.Groups[2].Value), 1, 1);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
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 DateTime GetLastWriteTime(string path) =>
|
||||
Try(File.GetLastWriteTimeUtc(path)).IfFail(() => DateTime.MinValue);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,22 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Images;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
using Seq = LanguageExt.Seq;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public class LocalMediaScanner : ILocalMediaScanner
|
||||
{
|
||||
private readonly IImageCache _imageCache;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILocalMediaSourcePlanner _localMediaSourcePlanner;
|
||||
private readonly ILocalMetadataProvider _localMetadataProvider;
|
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||
private readonly ILogger<LocalMediaScanner> _logger;
|
||||
@@ -30,6 +35,9 @@ namespace ErsatzTV.Core.Metadata
|
||||
ILocalMetadataProvider localMetadataProvider,
|
||||
ISmartCollectionBuilder smartCollectionBuilder,
|
||||
IPlayoutBuilder playoutBuilder,
|
||||
ILocalMediaSourcePlanner localMediaSourcePlanner,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IImageCache imageCache,
|
||||
ILogger<LocalMediaScanner> logger)
|
||||
{
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
@@ -38,83 +46,100 @@ namespace ErsatzTV.Core.Metadata
|
||||
_localMetadataProvider = localMetadataProvider;
|
||||
_smartCollectionBuilder = smartCollectionBuilder;
|
||||
_playoutBuilder = playoutBuilder;
|
||||
_localMediaSourcePlanner = localMediaSourcePlanner;
|
||||
_localFileSystem = localFileSystem;
|
||||
_imageCache = imageCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
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>
|
||||
Seq<LocalMediaSourcePlan> actions = _localMediaSourcePlanner.DetermineActions(
|
||||
localMediaSource.MediaType,
|
||||
knownMediaItems.ToSeq(),
|
||||
FindAllFiles(localMediaSource));
|
||||
|
||||
foreach (LocalMediaSourcePlan action in actions)
|
||||
{
|
||||
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4", ".m4p", ".m4v",
|
||||
".avi", ".wmv", ".mov", ".mkv"
|
||||
};
|
||||
Option<ActionPlan> maybeAddPlan =
|
||||
action.ActionPlans.SingleOrDefault(plan => plan.TargetAction == ScanningAction.Add);
|
||||
await maybeAddPlan.IfSomeAsync(
|
||||
async plan =>
|
||||
{
|
||||
Option<MediaItem> maybeMediaItem = await AddMediaItem(localMediaSource, plan.TargetPath);
|
||||
|
||||
var allFiles = Directory.GetFiles(localMediaSource.Folder, "*", SearchOption.AllDirectories)
|
||||
.Filter(file => knownExtensions.Contains(Path.GetExtension(file)))
|
||||
.ToSeq();
|
||||
// any actions other than "add" need to operate on a media item
|
||||
maybeMediaItem.IfSome(mediaItem => action.Source = mediaItem);
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// if exists, check if the file was modified
|
||||
Seq<MediaItem> modifiedMediaItems = existingMediaItems.Filter(
|
||||
mediaItem =>
|
||||
foreach (ActionPlan plan in action.ActionPlans.OrderBy(plan => (int) plan.TargetAction))
|
||||
{
|
||||
DateTime lastWrite = File.GetLastWriteTimeUtc(mediaItem.Path);
|
||||
bool modified = lastWrite > mediaItem.LastWriteTime.IfNone(DateTime.MinValue);
|
||||
return modified || mediaItem.Metadata == null;
|
||||
});
|
||||
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);
|
||||
string sourcePath = action.Source.Match(
|
||||
mediaItem => mediaItem.Path,
|
||||
path => path);
|
||||
|
||||
_logger.LogDebug(
|
||||
"{Source}: {Action} with {File}",
|
||||
Path.GetFileName(sourcePath),
|
||||
plan.TargetAction,
|
||||
Path.GetRelativePath(Path.GetDirectoryName(sourcePath) ?? string.Empty, plan.TargetPath));
|
||||
|
||||
await action.Source.Match(
|
||||
async mediaItem =>
|
||||
{
|
||||
var changed = false;
|
||||
|
||||
switch (plan.TargetAction)
|
||||
{
|
||||
case ScanningAction.Remove:
|
||||
await RemoveMissingItem(mediaItem);
|
||||
break;
|
||||
case ScanningAction.Poster:
|
||||
await SavePosterForItem(mediaItem, plan.TargetPath);
|
||||
break;
|
||||
case ScanningAction.FallbackMetadata:
|
||||
await RefreshFallbackMetadataForItem(mediaItem);
|
||||
break;
|
||||
case ScanningAction.SidecarMetadata:
|
||||
await RefreshSidecarMetadataForItem(mediaItem, plan.TargetPath);
|
||||
break;
|
||||
case ScanningAction.Statistics:
|
||||
changed = await RefreshStatisticsForItem(mediaItem, ffprobePath);
|
||||
break;
|
||||
case ScanningAction.Collections:
|
||||
changed = await RefreshCollectionsForItem(mediaItem);
|
||||
break;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
List<int> ids =
|
||||
await _playoutRepository.GetPlayoutIdsForMediaItems(Seq.create(mediaItem));
|
||||
modifiedPlayoutIds.AddRange(ids);
|
||||
}
|
||||
},
|
||||
path =>
|
||||
{
|
||||
_logger.LogError("This is a bug, something went wrong processing {Path}", path);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if new, add and store mtime, refresh metadata
|
||||
var addedMediaItems = new Seq<MediaItem>();
|
||||
foreach (string path in newFiles)
|
||||
{
|
||||
_logger.LogDebug("Adding new media item {MediaItem}", path);
|
||||
var mediaItem = new MediaItem
|
||||
{
|
||||
MediaSourceId = localMediaSource.Id,
|
||||
Path = path,
|
||||
LastWriteTime = File.GetLastWriteTimeUtc(path)
|
||||
};
|
||||
|
||||
await _mediaItemRepository.Add(mediaItem);
|
||||
await RefreshMetadata(mediaItem, ffprobePath);
|
||||
addedMediaItems.Add(mediaItem);
|
||||
}
|
||||
|
||||
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(addedMediaItems));
|
||||
|
||||
foreach (int playoutId in modifiedPlayoutIds.Distinct())
|
||||
{
|
||||
Option<Playout> maybePlayout = await _playoutRepository.GetFull(playoutId);
|
||||
@@ -127,14 +152,138 @@ namespace ErsatzTV.Core.Metadata
|
||||
Task.CompletedTask);
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
return unit;
|
||||
}
|
||||
|
||||
private async Task RefreshMetadata(MediaItem mediaItem, string ffprobePath)
|
||||
private Seq<string> FindAllFiles(LocalMediaSource localMediaSource)
|
||||
{
|
||||
await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
|
||||
await _localMetadataProvider.RefreshMetadata(mediaItem);
|
||||
await _smartCollectionBuilder.RefreshSmartCollections(mediaItem);
|
||||
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(path => File.Exists(Path.Combine(path, ".etvignore")));
|
||||
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))
|
||||
.OrderBy(identity)
|
||||
.ToSeq();
|
||||
}
|
||||
|
||||
private async Task<Option<MediaItem>> AddMediaItem(MediaSource mediaSource, string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mediaItem = new MediaItem
|
||||
{
|
||||
MediaSourceId = mediaSource.Id,
|
||||
Path = path,
|
||||
LastWriteTime = File.GetLastWriteTimeUtc(path)
|
||||
};
|
||||
|
||||
await _mediaItemRepository.Add(mediaItem);
|
||||
|
||||
return mediaItem;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to add media item for {Path}", path);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveMissingItem(MediaItem mediaItem)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mediaItemRepository.Delete(mediaItem.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to remove missing local media item {MediaItem}", mediaItem.Path);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SavePosterForItem(MediaItem mediaItem, string posterPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] originalBytes = await File.ReadAllBytesAsync(posterPath);
|
||||
Either<BaseError, string> maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, 220, null);
|
||||
await maybeHash.Match(
|
||||
hash =>
|
||||
{
|
||||
mediaItem.Poster = hash;
|
||||
mediaItem.PosterLastWriteTime = File.GetLastWriteTimeUtc(posterPath);
|
||||
return _mediaItemRepository.Update(mediaItem);
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to save poster to disk from {Path}: {Error}",
|
||||
posterPath,
|
||||
error.Value);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh poster for media item {MediaItem}", mediaItem.Path);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> RefreshStatisticsForItem(MediaItem mediaItem, string ffprobePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh statistics for media item {MediaItem}", mediaItem.Path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> RefreshCollectionsForItem(MediaItem mediaItem)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _smartCollectionBuilder.RefreshSmartCollections(mediaItem);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh collections for media item {MediaItem}", mediaItem.Path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshSidecarMetadataForItem(MediaItem mediaItem, string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _localMetadataProvider.RefreshSidecarMetadata(mediaItem, path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh nfo metadata for media item {MediaItem}", mediaItem.Path);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshFallbackMetadataForItem(MediaItem mediaItem)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _localMetadataProvider.RefreshFallbackMetadata(mediaItem);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh fallback metadata for media item {MediaItem}", mediaItem.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public record LocalMediaSourcePlan(Either<string, MediaItem> Source, List<ActionPlan> ActionPlans)
|
||||
{
|
||||
public Either<string, MediaItem> Source { get; set; } = Source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
// TODO: this needs a better name
|
||||
public class LocalMediaSourcePlanner : ILocalMediaSourcePlanner
|
||||
{
|
||||
private static readonly Seq<string> ImageFileExtensions = Seq("jpg", "jpeg", "png", "gif", "tbn");
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
|
||||
public LocalMediaSourcePlanner(ILocalFileSystem localFileSystem) => _localFileSystem = localFileSystem;
|
||||
|
||||
public Seq<LocalMediaSourcePlan> DetermineActions(
|
||||
MediaType mediaType,
|
||||
Seq<MediaItem> mediaItems,
|
||||
Seq<string> files)
|
||||
{
|
||||
var results = new IntermediateResults();
|
||||
Seq<string> videoFiles = files.Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f)))
|
||||
.Filter(f => !IsExtra(f));
|
||||
|
||||
(Seq<string> newFiles, Seq<MediaItem> existingMediaItems) = videoFiles.Map(
|
||||
s => mediaItems.Find(i => i.Path == s).ToEither(s))
|
||||
.Partition();
|
||||
|
||||
// new files
|
||||
foreach (string file in newFiles)
|
||||
{
|
||||
results.Add(file, new ActionPlan(file, ScanningAction.Add));
|
||||
results.Add(file, new ActionPlan(file, ScanningAction.Statistics));
|
||||
|
||||
Option<string> maybeNfoFile = LocateNfoFile(mediaType, files, file);
|
||||
maybeNfoFile.BiIter(
|
||||
nfoFile =>
|
||||
{
|
||||
results.Add(file, new ActionPlan(nfoFile, ScanningAction.SidecarMetadata));
|
||||
results.Add(file, new ActionPlan(nfoFile, ScanningAction.Collections));
|
||||
},
|
||||
() =>
|
||||
{
|
||||
results.Add(file, new ActionPlan(file, ScanningAction.FallbackMetadata));
|
||||
results.Add(file, new ActionPlan(file, ScanningAction.Collections));
|
||||
});
|
||||
|
||||
Option<string> maybePoster = LocatePoster(mediaType, files, file);
|
||||
maybePoster.IfSome(
|
||||
posterFile => results.Add(file, new ActionPlan(posterFile, ScanningAction.Poster)));
|
||||
}
|
||||
|
||||
// existing media items
|
||||
foreach (MediaItem mediaItem in existingMediaItems)
|
||||
{
|
||||
if ((mediaItem.LastWriteTime ?? DateTime.MinValue) < _localFileSystem.GetLastWriteTime(mediaItem.Path))
|
||||
{
|
||||
results.Add(mediaItem, new ActionPlan(mediaItem.Path, ScanningAction.Statistics));
|
||||
}
|
||||
|
||||
Option<string> maybeNfoFile = LocateNfoFile(mediaType, files, mediaItem.Path);
|
||||
maybeNfoFile.IfSome(
|
||||
nfoFile =>
|
||||
{
|
||||
if (mediaItem.Metadata == null || mediaItem.Metadata.Source == MetadataSource.Fallback ||
|
||||
(mediaItem.Metadata.LastWriteTime ?? DateTime.MinValue) <
|
||||
_localFileSystem.GetLastWriteTime(nfoFile))
|
||||
{
|
||||
results.Add(mediaItem, new ActionPlan(nfoFile, ScanningAction.SidecarMetadata));
|
||||
results.Add(mediaItem, new ActionPlan(nfoFile, ScanningAction.Collections));
|
||||
}
|
||||
});
|
||||
|
||||
Option<string> maybePoster = LocatePoster(mediaType, files, mediaItem.Path);
|
||||
maybePoster.IfSome(
|
||||
posterFile =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mediaItem.Poster) ||
|
||||
(mediaItem.PosterLastWriteTime ?? DateTime.MinValue) <
|
||||
_localFileSystem.GetLastWriteTime(posterFile))
|
||||
{
|
||||
results.Add(mediaItem, new ActionPlan(posterFile, ScanningAction.Poster));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// missing media items
|
||||
foreach (MediaItem mediaItem in mediaItems.Where(i => !files.Contains(i.Path)))
|
||||
{
|
||||
results.Add(mediaItem, new ActionPlan(mediaItem.Path, ScanningAction.Remove));
|
||||
}
|
||||
|
||||
return results.Summarize();
|
||||
}
|
||||
|
||||
private static bool IsExtra(string path)
|
||||
{
|
||||
string folder = Path.GetFileName(Path.GetDirectoryName(path) ?? string.Empty);
|
||||
string file = Path.GetFileNameWithoutExtension(path);
|
||||
return ExtraDirectories.Contains(folder, StringComparer.OrdinalIgnoreCase)
|
||||
|| ExtraFiles.Any(f => file.EndsWith(f, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static Option<string> LocateNfoFile(MediaType mediaType, Seq<string> files, string file)
|
||||
{
|
||||
switch (mediaType)
|
||||
{
|
||||
case MediaType.Movie:
|
||||
string movieAsNfo = Path.ChangeExtension(file, "nfo");
|
||||
string movieNfo = Path.Combine(Path.GetDirectoryName(file) ?? string.Empty, "movie.nfo");
|
||||
return Seq(movieAsNfo, movieNfo)
|
||||
.Filter(s => files.Contains(s))
|
||||
.HeadOrNone();
|
||||
case MediaType.TvShow:
|
||||
string episodeAsNfo = Path.ChangeExtension(file, "nfo");
|
||||
return Optional(episodeAsNfo)
|
||||
.Filter(s => files.Contains(s))
|
||||
.HeadOrNone();
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
private static Option<string> LocatePoster(MediaType mediaType, Seq<string> files, string file)
|
||||
{
|
||||
string folder = Path.GetDirectoryName(file) ?? string.Empty;
|
||||
|
||||
switch (mediaType)
|
||||
{
|
||||
case MediaType.Movie:
|
||||
IEnumerable<string> possibleMoviePosters = ImageFileExtensions.Collect(
|
||||
ext => new[] { $"poster.{ext}", Path.GetFileNameWithoutExtension(file) + $"-poster.{ext}" })
|
||||
.Map(f => Path.Combine(folder, f));
|
||||
return possibleMoviePosters.Filter(p => files.Contains(p)).HeadOrNone();
|
||||
case MediaType.TvShow:
|
||||
string parentFolder = Directory.GetParent(folder)?.FullName ?? string.Empty;
|
||||
IEnumerable<string> possibleTvPosters = ImageFileExtensions
|
||||
.Collect(ext => new[] { $"poster.{ext}" })
|
||||
.Map(f => Path.Combine(parentFolder, f));
|
||||
return possibleTvPosters.Filter(p => files.Contains(p)).HeadOrNone();
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
private class IntermediateResults
|
||||
{
|
||||
private readonly List<Tuple<Either<string, MediaItem>, ActionPlan>> _rawResults = new();
|
||||
|
||||
public void Add(Either<string, MediaItem> source, ActionPlan plan) =>
|
||||
_rawResults.Add(Tuple(source, plan));
|
||||
|
||||
public Seq<LocalMediaSourcePlan> Summarize() =>
|
||||
_rawResults
|
||||
.GroupBy(t => t.Item1)
|
||||
.Select(g => new LocalMediaSourcePlan(g.Key, g.Select(g2 => g2.Item2).ToList()))
|
||||
.ToSeq();
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
private static readonly Seq<string> VideoFileExtensions = Seq(
|
||||
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4",
|
||||
".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts");
|
||||
|
||||
private static readonly Seq<string> ExtraDirectories = Seq(
|
||||
"behind the scenes", "deleted scenes", "featurettes",
|
||||
"interviews", "scenes", "shorts", "trailers", "other",
|
||||
"extras", "specials");
|
||||
|
||||
private static readonly Seq<string> ExtraFiles = Seq(
|
||||
"behindthescenes", "deleted", "featurette",
|
||||
"interview", "scene", "short", "trailer", "other");
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,39 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
using ErsatzTV.Core.Domain;
|
||||
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 LocalMetadataProvider : ILocalMetadataProvider
|
||||
{
|
||||
private static readonly XmlSerializer MovieSerializer = new(typeof(MovieNfo));
|
||||
private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowEpisodeNfo));
|
||||
private readonly ILogger<LocalMetadataProvider> _logger;
|
||||
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public LocalMetadataProvider(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public async Task RefreshMetadata(MediaItem mediaItem)
|
||||
public LocalMetadataProvider(IMediaItemRepository mediaItemRepository, ILogger<LocalMetadataProvider> logger)
|
||||
{
|
||||
Option<MediaMetadata> maybeMetadata = await LoadMetadata(mediaItem);
|
||||
MediaMetadata metadata = maybeMetadata.IfNone(() => GetFallbackMetadata(mediaItem));
|
||||
await ApplyMetadataUpdate(mediaItem, metadata);
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RefreshSidecarMetadata(MediaItem mediaItem, string path)
|
||||
{
|
||||
Option<MediaMetadata> maybeMetadata = await LoadMetadata(mediaItem, path);
|
||||
await maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(mediaItem, metadata));
|
||||
}
|
||||
|
||||
public Task RefreshFallbackMetadata(MediaItem mediaItem) =>
|
||||
ApplyMetadataUpdate(mediaItem, FallbackMetadataProvider.GetFallbackMetadata(mediaItem));
|
||||
|
||||
private async Task ApplyMetadataUpdate(MediaItem mediaItem, MediaMetadata metadata)
|
||||
{
|
||||
if (mediaItem.Metadata == null)
|
||||
@@ -32,9 +41,15 @@ namespace ErsatzTV.Core.Metadata
|
||||
mediaItem.Metadata = new MediaMetadata();
|
||||
}
|
||||
|
||||
mediaItem.Metadata.Source = metadata.Source;
|
||||
mediaItem.Metadata.LastWriteTime = metadata.LastWriteTime;
|
||||
mediaItem.Metadata.MediaType = metadata.MediaType;
|
||||
mediaItem.Metadata.Title = metadata.Title;
|
||||
mediaItem.Metadata.Subtitle = metadata.Subtitle;
|
||||
mediaItem.Metadata.SortTitle =
|
||||
(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;
|
||||
@@ -44,80 +59,80 @@ namespace ErsatzTV.Core.Metadata
|
||||
await _mediaItemRepository.Update(mediaItem);
|
||||
}
|
||||
|
||||
private async Task<Option<MediaMetadata>> LoadMetadata(MediaItem mediaItem)
|
||||
private async Task<Option<MediaMetadata>> LoadMetadata(MediaItem mediaItem, string nfoFileName)
|
||||
{
|
||||
string nfoFileName = Path.ChangeExtension(mediaItem.Path, "nfo");
|
||||
if (nfoFileName == null || !File.Exists(nfoFileName))
|
||||
{
|
||||
_logger.LogDebug("NFO file does not exist at {Path}", nfoFileName);
|
||||
return None;
|
||||
}
|
||||
|
||||
var tvShowSerializer = new XmlSerializer(typeof(TvShowEpisodeNfo));
|
||||
var movieSerializer = new XmlSerializer(typeof(MovieNfo));
|
||||
|
||||
TryAsync<object> tvShowAttempt = TryAsync(
|
||||
async () =>
|
||||
{
|
||||
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open);
|
||||
return tvShowSerializer.Deserialize(fileStream);
|
||||
});
|
||||
TryAsync<object> movieAttempt = TryAsync(
|
||||
async () =>
|
||||
{
|
||||
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open);
|
||||
return movieSerializer.Deserialize(fileStream);
|
||||
});
|
||||
return await choice(tvShowAttempt, movieAttempt).Match<object, Option<MediaMetadata>>(
|
||||
result =>
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case TvShowEpisodeNfo nfo:
|
||||
return new MediaMetadata
|
||||
{
|
||||
MediaType = MediaType.TvShow,
|
||||
Title = nfo.ShowTitle,
|
||||
Subtitle = nfo.Title,
|
||||
Description = nfo.Outline,
|
||||
EpisodeNumber = nfo.Episode,
|
||||
SeasonNumber = nfo.Season,
|
||||
Aired = GetAired(nfo.Aired)
|
||||
};
|
||||
case MovieNfo nfo:
|
||||
return new MediaMetadata
|
||||
{
|
||||
MediaType = MediaType.Movie,
|
||||
Title = nfo.Title,
|
||||
Description = nfo.Outline,
|
||||
ContentRating = nfo.ContentRating,
|
||||
Aired = GetAired(nfo.Premiered)
|
||||
};
|
||||
default:
|
||||
return None;
|
||||
}
|
||||
},
|
||||
None);
|
||||
}
|
||||
|
||||
private MediaMetadata GetFallbackMetadata(MediaItem mediaItem)
|
||||
{
|
||||
string fileName = Path.GetFileName(mediaItem.Path);
|
||||
var metadata = new MediaMetadata { Title = fileName ?? mediaItem.Path };
|
||||
|
||||
if (fileName != null)
|
||||
if (!(mediaItem.Source is LocalMediaSource localMediaSource))
|
||||
{
|
||||
const string PATTERN = @"^(.*?)[\s-]+[sS](\d+)[eE](\d+)\.\w+$";
|
||||
Match match = Regex.Match(fileName, PATTERN);
|
||||
if (match.Success)
|
||||
{
|
||||
metadata.MediaType = MediaType.TvShow;
|
||||
metadata.Title = match.Groups[1].Value;
|
||||
metadata.SeasonNumber = int.Parse(match.Groups[2].Value);
|
||||
metadata.EpisodeNumber = int.Parse(match.Groups[3].Value);
|
||||
}
|
||||
_logger.LogDebug("Media source {Name} is not a local media source", mediaItem.Source.Name);
|
||||
return None;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
return localMediaSource.MediaType switch
|
||||
{
|
||||
MediaType.Movie => await LoadMovieMetadata(nfoFileName),
|
||||
MediaType.TvShow => await LoadTvShowMetadata(nfoFileName),
|
||||
_ => None
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Option<MediaMetadata>> LoadTvShowMetadata(string nfoFileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
|
||||
Option<TvShowEpisodeNfo> maybeNfo = TvShowSerializer.Deserialize(fileStream) as TvShowEpisodeNfo;
|
||||
return maybeNfo.Match<Option<MediaMetadata>>(
|
||||
nfo => new MediaMetadata
|
||||
{
|
||||
Source = MetadataSource.Sidecar,
|
||||
LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName),
|
||||
MediaType = MediaType.TvShow,
|
||||
Title = nfo.ShowTitle,
|
||||
Subtitle = nfo.Title,
|
||||
Description = nfo.Outline,
|
||||
EpisodeNumber = nfo.Episode,
|
||||
SeasonNumber = nfo.Season,
|
||||
Aired = GetAired(nfo.Aired)
|
||||
},
|
||||
None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to read TV nfo metadata from {Path}", nfoFileName);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Option<MediaMetadata>> LoadMovieMetadata(string nfoFileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read);
|
||||
Option<MovieNfo> maybeNfo = MovieSerializer.Deserialize(fileStream) as MovieNfo;
|
||||
return maybeNfo.Match<Option<MediaMetadata>>(
|
||||
nfo => new MediaMetadata
|
||||
{
|
||||
Source = MetadataSource.Sidecar,
|
||||
LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName),
|
||||
MediaType = MediaType.Movie,
|
||||
Title = nfo.Title,
|
||||
Description = nfo.Outline,
|
||||
ContentRating = nfo.ContentRating,
|
||||
Aired = GetAired(nfo.Premiered)
|
||||
},
|
||||
None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to read Movie nfo metadata from {Path}", nfoFileName);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private static DateTime? GetAired(string aired)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
public 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
@@ -14,19 +15,33 @@ namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public class LocalStatisticsProvider : ILocalStatisticsProvider
|
||||
{
|
||||
private readonly ILogger<LocalStatisticsProvider> _logger;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public LocalStatisticsProvider(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public async Task RefreshStatistics(string ffprobePath, MediaItem mediaItem)
|
||||
public LocalStatisticsProvider(
|
||||
IMediaItemRepository mediaItemRepository,
|
||||
ILogger<LocalStatisticsProvider> logger)
|
||||
{
|
||||
FFprobe ffprobe = await GetProbeOutput(ffprobePath, mediaItem);
|
||||
MediaMetadata metadata = ProjectToMediaMetadata(ffprobe);
|
||||
await ApplyStatisticsUpdate(mediaItem, metadata);
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task ApplyStatisticsUpdate(
|
||||
public async Task<bool> RefreshStatistics(string ffprobePath, MediaItem mediaItem)
|
||||
{
|
||||
try
|
||||
{
|
||||
FFprobe ffprobe = await GetProbeOutput(ffprobePath, mediaItem);
|
||||
MediaMetadata metadata = ProjectToMediaMetadata(ffprobe);
|
||||
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<bool> ApplyStatisticsUpdate(
|
||||
MediaItem mediaItem,
|
||||
MediaMetadata metadata)
|
||||
{
|
||||
@@ -35,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;
|
||||
@@ -44,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)
|
||||
@@ -82,7 +99,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
|
||||
private MediaMetadata ProjectToMediaMetadata(FFprobe probeOutput) =>
|
||||
Optional(probeOutput)
|
||||
.Filter(json => json != null)
|
||||
.Filter(json => json?.format != null && json.streams != null)
|
||||
.ToValidation<BaseError>("Unable to parse ffprobe output")
|
||||
.ToEither<FFprobe>()
|
||||
.Match(
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public enum ScanningAction
|
||||
{
|
||||
None = 0,
|
||||
Add = 1,
|
||||
Remove = 2,
|
||||
Statistics = 3,
|
||||
SidecarMetadata = 4,
|
||||
FallbackMetadata = 5,
|
||||
Collections = 6,
|
||||
Poster = 7
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -29,6 +30,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollection(int id) =>
|
||||
Get(id).Map(c => c.OfType<SimpleMediaCollection>().HeadOrNone());
|
||||
|
||||
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id) =>
|
||||
_dbContext.SimpleMediaCollections
|
||||
.Include(s => s.Items)
|
||||
.ThenInclude(i => i.Source)
|
||||
.SingleOrDefaultAsync(c => c.Id == id)
|
||||
.Map(Optional);
|
||||
|
||||
public Task<Option<TelevisionMediaCollection>> GetTelevisionMediaCollection(int id) =>
|
||||
Get(id).Map(c => c.OfType<TelevisionMediaCollection>().HeadOrNone());
|
||||
|
||||
@@ -61,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) =>
|
||||
@@ -76,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,5 +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;
|
||||
@@ -22,25 +24,83 @@ 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();
|
||||
|
||||
public Task<List<MediaItem>> GetAll(MediaType mediaType) =>
|
||||
_dbContext.MediaItems
|
||||
.Include(i => i.Source)
|
||||
.Filter(i => i.Metadata.MediaType == mediaType)
|
||||
.ToListAsync();
|
||||
public Task<List<MediaItem>> Search(string searchString)
|
||||
{
|
||||
IQueryable<MediaItem> data = from c in _dbContext.MediaItems.Include(c => c.Source) select c;
|
||||
|
||||
if (!string.IsNullOrEmpty(searchString))
|
||||
{
|
||||
data = data.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%"));
|
||||
}
|
||||
|
||||
return data.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,876 @@
|
||||
// <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("20210212105010_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
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>("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,718 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
"ConfigElements",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Key = table.Column<string>("TEXT", nullable: true),
|
||||
Value = table.Column<string>("TEXT", nullable: true)
|
||||
},
|
||||
constraints: table => { table.PrimaryKey("PK_ConfigElements", x => x.Id); });
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"GenericIntegerIds",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table => { });
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"MediaCollections",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>("TEXT", nullable: true)
|
||||
},
|
||||
constraints: table => { table.PrimaryKey("PK_MediaCollections", x => x.Id); });
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"MediaCollectionSummaries",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false),
|
||||
Name = table.Column<string>("TEXT", nullable: true),
|
||||
ItemCount = table.Column<int>("INTEGER", nullable: false),
|
||||
IsSimple = table.Column<bool>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table => { });
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"MediaSources",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
SourceType = table.Column<int>("INTEGER", nullable: false),
|
||||
Name = table.Column<string>("TEXT", nullable: true)
|
||||
},
|
||||
constraints: table => { table.PrimaryKey("PK_MediaSources", x => x.Id); });
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"ProgramSchedules",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>("TEXT", nullable: true),
|
||||
MediaCollectionPlaybackOrder = table.Column<int>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table => { table.PrimaryKey("PK_ProgramSchedules", x => x.Id); });
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"Resolutions",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>("TEXT", nullable: true),
|
||||
Height = table.Column<int>("INTEGER", nullable: false),
|
||||
Width = table.Column<int>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table => { table.PrimaryKey("PK_Resolutions", x => x.Id); });
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"SimpleMediaCollections",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SimpleMediaCollections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_SimpleMediaCollections_MediaCollections_Id",
|
||||
x => x.Id,
|
||||
"MediaCollections",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"TelevisionMediaCollections",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ShowTitle = table.Column<string>("TEXT", nullable: true),
|
||||
SeasonNumber = table.Column<int>("INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TelevisionMediaCollections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_TelevisionMediaCollections_MediaCollections_Id",
|
||||
x => x.Id,
|
||||
"MediaCollections",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"LocalMediaSources",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
MediaType = table.Column<int>("INTEGER", nullable: false),
|
||||
Folder = table.Column<string>("TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LocalMediaSources", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_LocalMediaSources_MediaSources_Id",
|
||||
x => x.Id,
|
||||
"MediaSources",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"MediaItems",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
MediaSourceId = table.Column<int>("INTEGER", nullable: false),
|
||||
Path = table.Column<string>("TEXT", nullable: true),
|
||||
Metadata_Duration = table.Column<TimeSpan>("TEXT", nullable: true),
|
||||
Metadata_SampleAspectRatio = table.Column<string>("TEXT", nullable: true),
|
||||
Metadata_DisplayAspectRatio = table.Column<string>("TEXT", nullable: true),
|
||||
Metadata_VideoCodec = table.Column<string>("TEXT", nullable: true),
|
||||
Metadata_AudioCodec = table.Column<string>("TEXT", nullable: true),
|
||||
Metadata_MediaType = table.Column<int>("INTEGER", nullable: true),
|
||||
Metadata_Title = table.Column<string>("TEXT", nullable: true),
|
||||
Metadata_Subtitle = table.Column<string>("TEXT", nullable: true),
|
||||
Metadata_Description = table.Column<string>("TEXT", nullable: true),
|
||||
Metadata_SeasonNumber = table.Column<int>("INTEGER", nullable: true),
|
||||
Metadata_EpisodeNumber = table.Column<int>("INTEGER", nullable: true),
|
||||
Metadata_ContentRating = table.Column<string>("TEXT", nullable: true),
|
||||
Metadata_Aired = table.Column<DateTime>("TEXT", nullable: true),
|
||||
Metadata_VideoScanType = table.Column<int>("INTEGER", nullable: true),
|
||||
Metadata_Width = table.Column<int>("INTEGER", nullable: true),
|
||||
Metadata_Height = table.Column<int>("INTEGER", nullable: true),
|
||||
LastWriteTime = table.Column<DateTime>("TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MediaItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_MediaItems_MediaSources_MediaSourceId",
|
||||
x => x.MediaSourceId,
|
||||
"MediaSources",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"PlexMediaSources",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ProductVersion = table.Column<string>("TEXT", nullable: true),
|
||||
ClientIdentifier = table.Column<string>("TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PlexMediaSources", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_PlexMediaSources_MediaSources_Id",
|
||||
x => x.Id,
|
||||
"MediaSources",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"ProgramScheduleItems",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Index = table.Column<int>("INTEGER", nullable: false),
|
||||
StartTime = table.Column<TimeSpan>("TEXT", nullable: true),
|
||||
MediaCollectionId = table.Column<int>("INTEGER", nullable: false),
|
||||
ProgramScheduleId = table.Column<int>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ProgramScheduleItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_ProgramScheduleItems_MediaCollections_MediaCollectionId",
|
||||
x => x.MediaCollectionId,
|
||||
"MediaCollections",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
"FK_ProgramScheduleItems_ProgramSchedules_ProgramScheduleId",
|
||||
x => x.ProgramScheduleId,
|
||||
"ProgramSchedules",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"FFmpegProfiles",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>("TEXT", nullable: true),
|
||||
ThreadCount = table.Column<int>("INTEGER", nullable: false),
|
||||
Transcode = table.Column<bool>("INTEGER", nullable: false),
|
||||
ResolutionId = table.Column<int>("INTEGER", nullable: false),
|
||||
NormalizeResolution = table.Column<bool>("INTEGER", nullable: false),
|
||||
VideoCodec = table.Column<string>("TEXT", nullable: true),
|
||||
NormalizeVideoCodec = table.Column<bool>("INTEGER", nullable: false),
|
||||
VideoBitrate = table.Column<int>("INTEGER", nullable: false),
|
||||
VideoBufferSize = table.Column<int>("INTEGER", nullable: false),
|
||||
AudioCodec = table.Column<string>("TEXT", nullable: true),
|
||||
NormalizeAudioCodec = table.Column<bool>("INTEGER", nullable: false),
|
||||
AudioBitrate = table.Column<int>("INTEGER", nullable: false),
|
||||
AudioBufferSize = table.Column<int>("INTEGER", nullable: false),
|
||||
AudioVolume = table.Column<int>("INTEGER", nullable: false),
|
||||
AudioChannels = table.Column<int>("INTEGER", nullable: false),
|
||||
AudioSampleRate = table.Column<int>("INTEGER", nullable: false),
|
||||
NormalizeAudio = table.Column<bool>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FFmpegProfiles", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_FFmpegProfiles_Resolutions_ResolutionId",
|
||||
x => x.ResolutionId,
|
||||
"Resolutions",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"MediaItemSimpleMediaCollection",
|
||||
table => new
|
||||
{
|
||||
ItemsId = table.Column<int>("INTEGER", nullable: false),
|
||||
SimpleMediaCollectionsId = table.Column<int>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey(
|
||||
"PK_MediaItemSimpleMediaCollection",
|
||||
x => new { x.ItemsId, x.SimpleMediaCollectionsId });
|
||||
table.ForeignKey(
|
||||
"FK_MediaItemSimpleMediaCollection_MediaItems_ItemsId",
|
||||
x => x.ItemsId,
|
||||
"MediaItems",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
"FK_MediaItemSimpleMediaCollection_SimpleMediaCollections_SimpleMediaCollectionsId",
|
||||
x => x.SimpleMediaCollectionsId,
|
||||
"SimpleMediaCollections",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"PlexMediaSourceConnections",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
IsActive = table.Column<bool>("INTEGER", nullable: false),
|
||||
Uri = table.Column<string>("TEXT", nullable: true),
|
||||
PlexMediaSourceId = table.Column<int>("INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PlexMediaSourceConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_PlexMediaSourceConnections_PlexMediaSources_PlexMediaSourceId",
|
||||
x => x.PlexMediaSourceId,
|
||||
"PlexMediaSources",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"PlexMediaSourceLibraries",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Key = table.Column<string>("TEXT", nullable: true),
|
||||
Name = table.Column<string>("TEXT", nullable: true),
|
||||
MediaType = table.Column<int>("INTEGER", nullable: false),
|
||||
PlexMediaSourceId = table.Column<int>("INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PlexMediaSourceLibraries", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_PlexMediaSourceLibraries_PlexMediaSources_PlexMediaSourceId",
|
||||
x => x.PlexMediaSourceId,
|
||||
"PlexMediaSources",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"ProgramScheduleDurationItems",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
PlayoutDuration = table.Column<TimeSpan>("TEXT", nullable: false),
|
||||
OfflineTail = table.Column<bool>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ProgramScheduleDurationItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_ProgramScheduleDurationItems_ProgramScheduleItems_Id",
|
||||
x => x.Id,
|
||||
"ProgramScheduleItems",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"ProgramScheduleFloodItems",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ProgramScheduleFloodItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_ProgramScheduleFloodItems_ProgramScheduleItems_Id",
|
||||
x => x.Id,
|
||||
"ProgramScheduleItems",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"ProgramScheduleMultipleItems",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Count = table.Column<int>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ProgramScheduleMultipleItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_ProgramScheduleMultipleItems_ProgramScheduleItems_Id",
|
||||
x => x.Id,
|
||||
"ProgramScheduleItems",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"ProgramScheduleOneItems",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ProgramScheduleOneItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_ProgramScheduleOneItems_ProgramScheduleItems_Id",
|
||||
x => x.Id,
|
||||
"ProgramScheduleItems",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"Channels",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
UniqueId = table.Column<Guid>("TEXT", nullable: false),
|
||||
Number = table.Column<int>("INTEGER", nullable: false),
|
||||
Name = table.Column<string>("TEXT", nullable: true),
|
||||
Logo = table.Column<string>("TEXT", nullable: true),
|
||||
FFmpegProfileId = table.Column<int>("INTEGER", nullable: false),
|
||||
StreamingMode = table.Column<int>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Channels", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_Channels_FFmpegProfiles_FFmpegProfileId",
|
||||
x => x.FFmpegProfileId,
|
||||
"FFmpegProfiles",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"Playouts",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ChannelId = table.Column<int>("INTEGER", nullable: false),
|
||||
ProgramScheduleId = table.Column<int>("INTEGER", nullable: false),
|
||||
ProgramSchedulePlayoutType = table.Column<int>("INTEGER", nullable: false),
|
||||
Anchor_NextScheduleItemId = table.Column<int>("INTEGER", nullable: true),
|
||||
Anchor_NextStart = table.Column<DateTimeOffset>("TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Playouts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_Playouts_Channels_ChannelId",
|
||||
x => x.ChannelId,
|
||||
"Channels",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
"FK_Playouts_ProgramScheduleItems_Anchor_NextScheduleItemId",
|
||||
x => x.Anchor_NextScheduleItemId,
|
||||
"ProgramScheduleItems",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
"FK_Playouts_ProgramSchedules_ProgramScheduleId",
|
||||
x => x.ProgramScheduleId,
|
||||
"ProgramSchedules",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"PlayoutItems",
|
||||
table => new
|
||||
{
|
||||
Id = table.Column<int>("INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
MediaItemId = table.Column<int>("INTEGER", nullable: false),
|
||||
Start = table.Column<DateTimeOffset>("TEXT", nullable: false),
|
||||
Finish = table.Column<DateTimeOffset>("TEXT", nullable: false),
|
||||
PlayoutId = table.Column<int>("INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PlayoutItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
"FK_PlayoutItems_MediaItems_MediaItemId",
|
||||
x => x.MediaItemId,
|
||||
"MediaItems",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
"FK_PlayoutItems_Playouts_PlayoutId",
|
||||
x => x.PlayoutId,
|
||||
"Playouts",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
"PlayoutProgramScheduleItemAnchors",
|
||||
table => new
|
||||
{
|
||||
PlayoutId = table.Column<int>("INTEGER", nullable: false),
|
||||
ProgramScheduleId = table.Column<int>("INTEGER", nullable: false),
|
||||
MediaCollectionId = table.Column<int>("INTEGER", nullable: false),
|
||||
EnumeratorState_Seed = table.Column<int>("INTEGER", nullable: true),
|
||||
EnumeratorState_Index = table.Column<int>("INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey(
|
||||
"PK_PlayoutProgramScheduleItemAnchors",
|
||||
x => new { x.PlayoutId, x.ProgramScheduleId, x.MediaCollectionId });
|
||||
table.ForeignKey(
|
||||
"FK_PlayoutProgramScheduleItemAnchors_MediaCollections_MediaCollectionId",
|
||||
x => x.MediaCollectionId,
|
||||
"MediaCollections",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
"FK_PlayoutProgramScheduleItemAnchors_Playouts_PlayoutId",
|
||||
x => x.PlayoutId,
|
||||
"Playouts",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
"FK_PlayoutProgramScheduleItemAnchors_ProgramSchedules_ProgramScheduleId",
|
||||
x => x.ProgramScheduleId,
|
||||
"ProgramSchedules",
|
||||
"Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Channels_FFmpegProfileId",
|
||||
"Channels",
|
||||
"FFmpegProfileId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Channels_Number",
|
||||
"Channels",
|
||||
"Number",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_ConfigElements_Key",
|
||||
"ConfigElements",
|
||||
"Key",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_FFmpegProfiles_ResolutionId",
|
||||
"FFmpegProfiles",
|
||||
"ResolutionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_MediaCollections_Name",
|
||||
"MediaCollections",
|
||||
"Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_MediaItems_MediaSourceId",
|
||||
"MediaItems",
|
||||
"MediaSourceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_MediaItemSimpleMediaCollection_SimpleMediaCollectionsId",
|
||||
"MediaItemSimpleMediaCollection",
|
||||
"SimpleMediaCollectionsId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_MediaSources_Name",
|
||||
"MediaSources",
|
||||
"Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_PlayoutItems_MediaItemId",
|
||||
"PlayoutItems",
|
||||
"MediaItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_PlayoutItems_PlayoutId",
|
||||
"PlayoutItems",
|
||||
"PlayoutId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_PlayoutProgramScheduleItemAnchors_MediaCollectionId",
|
||||
"PlayoutProgramScheduleItemAnchors",
|
||||
"MediaCollectionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_PlayoutProgramScheduleItemAnchors_ProgramScheduleId",
|
||||
"PlayoutProgramScheduleItemAnchors",
|
||||
"ProgramScheduleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Playouts_Anchor_NextScheduleItemId",
|
||||
"Playouts",
|
||||
"Anchor_NextScheduleItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Playouts_ChannelId",
|
||||
"Playouts",
|
||||
"ChannelId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_Playouts_ProgramScheduleId",
|
||||
"Playouts",
|
||||
"ProgramScheduleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_PlexMediaSourceConnections_PlexMediaSourceId",
|
||||
"PlexMediaSourceConnections",
|
||||
"PlexMediaSourceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_PlexMediaSourceLibraries_PlexMediaSourceId",
|
||||
"PlexMediaSourceLibraries",
|
||||
"PlexMediaSourceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_ProgramScheduleItems_MediaCollectionId",
|
||||
"ProgramScheduleItems",
|
||||
"MediaCollectionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_ProgramScheduleItems_ProgramScheduleId",
|
||||
"ProgramScheduleItems",
|
||||
"ProgramScheduleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_ProgramSchedules_Name",
|
||||
"ProgramSchedules",
|
||||
"Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
"IX_TelevisionMediaCollections_ShowTitle_SeasonNumber",
|
||||
"TelevisionMediaCollections",
|
||||
new[] { "ShowTitle", "SeasonNumber" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
"ConfigElements");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"GenericIntegerIds");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"LocalMediaSources");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"MediaCollectionSummaries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"MediaItemSimpleMediaCollection");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"PlayoutItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"PlayoutProgramScheduleItemAnchors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"PlexMediaSourceConnections");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"PlexMediaSourceLibraries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"ProgramScheduleDurationItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"ProgramScheduleFloodItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"ProgramScheduleMultipleItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"ProgramScheduleOneItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"TelevisionMediaCollections");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"SimpleMediaCollections");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"MediaItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"Playouts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"PlexMediaSources");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"Channels");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"ProgramScheduleItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"MediaSources");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"FFmpegProfiles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"MediaCollections");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"ProgramSchedules");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
"Resolutions");
|
||||
}
|
||||
}
|
||||
}
|
||||
+879
@@ -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
@@ -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 => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
+905
@@ -0,0 +1,905 @@
|
||||
// <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("20210215153541_MetadataOptimizations")]
|
||||
partial class MetadataOptimizations
|
||||
{
|
||||
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<int>("MediaItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
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.Property<DateTime?>("PosterLastWriteTime")
|
||||
.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<DateTime?>("LastWriteTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
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<int>("Source")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
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,44 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations
|
||||
{
|
||||
public partial class MetadataOptimizations : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
"Metadata_LastWriteTime",
|
||||
"MediaItems",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
"Metadata_Source",
|
||||
"MediaItems",
|
||||
"INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
"PosterLastWriteTime",
|
||||
"MediaItems",
|
||||
"TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
"Metadata_LastWriteTime",
|
||||
"MediaItems");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"Metadata_Source",
|
||||
"MediaItems");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
"PosterLastWriteTime",
|
||||
"MediaItems");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user