Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fb23f2edb |
@@ -52,9 +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 _localPosterProvider.RefreshPoster(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();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ namespace ErsatzTV.Core.Domain
|
||||
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; }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public enum MetadataSource
|
||||
{
|
||||
Fallback = 0,
|
||||
Sidecar = 1
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
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);
|
||||
|
||||
@@ -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,4 @@
|
||||
namespace ErsatzTV.Core.Metadata
|
||||
{
|
||||
public record ActionPlan(string TargetPath, ScanningAction TargetAction);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
public static MediaMetadata GetFallbackMetadata(MediaItem mediaItem)
|
||||
{
|
||||
string fileName = Path.GetFileName(mediaItem.Path);
|
||||
var metadata = new MediaMetadata { Title = fileName ?? mediaItem.Path };
|
||||
var metadata = new MediaMetadata { Source = MetadataSource.Fallback, Title = fileName ?? mediaItem.Path };
|
||||
|
||||
if (fileName != null)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,9 @@ 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);
|
||||
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
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 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 ILocalPosterProvider _localPosterProvider;
|
||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||
private readonly ILogger<LocalMediaScanner> _logger;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
@@ -29,21 +33,23 @@ namespace ErsatzTV.Core.Metadata
|
||||
IPlayoutRepository playoutRepository,
|
||||
ILocalStatisticsProvider localStatisticsProvider,
|
||||
ILocalMetadataProvider localMetadataProvider,
|
||||
ILocalPosterProvider localPosterProvider,
|
||||
ISmartCollectionBuilder smartCollectionBuilder,
|
||||
IPlayoutBuilder playoutBuilder,
|
||||
ILogger<LocalMediaScanner> logger,
|
||||
ILocalFileSystem localFileSystem)
|
||||
ILocalMediaSourcePlanner localMediaSourcePlanner,
|
||||
ILocalFileSystem localFileSystem,
|
||||
IImageCache imageCache,
|
||||
ILogger<LocalMediaScanner> logger)
|
||||
{
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
_playoutRepository = playoutRepository;
|
||||
_localStatisticsProvider = localStatisticsProvider;
|
||||
_localMetadataProvider = localMetadataProvider;
|
||||
_localPosterProvider = localPosterProvider;
|
||||
_smartCollectionBuilder = smartCollectionBuilder;
|
||||
_playoutBuilder = playoutBuilder;
|
||||
_logger = logger;
|
||||
_localMediaSourcePlanner = localMediaSourcePlanner;
|
||||
_localFileSystem = localFileSystem;
|
||||
_imageCache = imageCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Unit> ScanLocalMediaSource(
|
||||
@@ -62,48 +68,78 @@ namespace ErsatzTV.Core.Metadata
|
||||
List<MediaItem> knownMediaItems = await _mediaItemRepository.GetAllByMediaSourceId(localMediaSource.Id);
|
||||
var modifiedPlayoutIds = new List<int>();
|
||||
|
||||
Seq<string> allFiles = _localFileSystem.FindRelevantVideos(localMediaSource);
|
||||
Seq<LocalMediaSourcePlan> actions = _localMediaSourcePlanner.DetermineActions(
|
||||
localMediaSource.MediaType,
|
||||
knownMediaItems.ToSeq(),
|
||||
FindAllFiles(localMediaSource));
|
||||
|
||||
// check if the media item exists
|
||||
(Seq<string> newFiles, Seq<MediaItem> existingMediaItems) = allFiles.Map(
|
||||
s => Optional(knownMediaItems.Find(i => i.Path == s)).ToEither(s))
|
||||
.Partition();
|
||||
|
||||
// remove media items that no longer exist
|
||||
var missingMediaItems = knownMediaItems.Filter(i => !allFiles.Contains(i.Path)).ToSeq();
|
||||
await RemoveMissingItems(missingMediaItems);
|
||||
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(missingMediaItems));
|
||||
|
||||
Seq<MediaItem> staleMetadataMediaItems = scanningMode == ScanningMode.RescanAll
|
||||
? existingMediaItems
|
||||
: existingMediaItems.Filter(i => _localFileSystem.ShouldRefreshMetadata(localMediaSource, i));
|
||||
Seq<MediaItem> modifiedMediaItems = await RefreshMetadataForItems(ffprobePath, staleMetadataMediaItems);
|
||||
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems));
|
||||
|
||||
// if new, add and store mtime, refresh metadata
|
||||
var addedMediaItems = new List<MediaItem>();
|
||||
foreach (string path in newFiles)
|
||||
foreach (LocalMediaSourcePlan action in actions)
|
||||
{
|
||||
_logger.LogDebug("Adding new media item {MediaItem}", path);
|
||||
var mediaItem = new MediaItem
|
||||
Option<ActionPlan> maybeAddPlan =
|
||||
action.ActionPlans.SingleOrDefault(plan => plan.TargetAction == ScanningAction.Add);
|
||||
await maybeAddPlan.IfSomeAsync(
|
||||
async plan =>
|
||||
{
|
||||
Option<MediaItem> maybeMediaItem = await AddMediaItem(localMediaSource, plan.TargetPath);
|
||||
|
||||
// any actions other than "add" need to operate on a media item
|
||||
maybeMediaItem.IfSome(mediaItem => action.Source = mediaItem);
|
||||
});
|
||||
|
||||
foreach (ActionPlan plan in action.ActionPlans.OrderBy(plan => (int) plan.TargetAction))
|
||||
{
|
||||
MediaSourceId = localMediaSource.Id,
|
||||
Path = path,
|
||||
LastWriteTime = File.GetLastWriteTimeUtc(path)
|
||||
};
|
||||
string sourcePath = action.Source.Match(
|
||||
mediaItem => mediaItem.Path,
|
||||
path => path);
|
||||
|
||||
await _mediaItemRepository.Add(mediaItem);
|
||||
await RefreshMetadata(mediaItem, ffprobePath);
|
||||
addedMediaItems.Add(mediaItem);
|
||||
_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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(addedMediaItems.ToSeq()));
|
||||
|
||||
Seq<MediaItem> stalePosterMediaItems = existingMediaItems
|
||||
.Filter(_localFileSystem.ShouldRefreshPoster)
|
||||
.Concat(addedMediaItems);
|
||||
await RefreshPosterForItems(stalePosterMediaItems);
|
||||
|
||||
foreach (int playoutId in modifiedPlayoutIds.Distinct())
|
||||
{
|
||||
Option<Playout> maybePlayout = await _playoutRepository.GetFull(playoutId);
|
||||
@@ -119,65 +155,135 @@ namespace ErsatzTV.Core.Metadata
|
||||
return unit;
|
||||
}
|
||||
|
||||
private async Task<Seq<MediaItem>> RefreshMetadataForItems(
|
||||
string ffprobePath,
|
||||
Seq<MediaItem> staleMetadataMediaItems)
|
||||
private Seq<string> FindAllFiles(LocalMediaSource localMediaSource)
|
||||
{
|
||||
var modifiedMediaItems = new List<MediaItem>();
|
||||
foreach (MediaItem mediaItem in staleMetadataMediaItems)
|
||||
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
|
||||
{
|
||||
_logger.LogDebug("Refreshing metadata for media item {MediaItem}", mediaItem.Path);
|
||||
if (await RefreshMetadata(mediaItem, ffprobePath))
|
||||
var mediaItem = new MediaItem
|
||||
{
|
||||
// only queue playout rebuilds for media items
|
||||
// where the duration or collections have changed
|
||||
modifiedMediaItems.Add(mediaItem);
|
||||
}
|
||||
MediaSourceId = mediaSource.Id,
|
||||
Path = path,
|
||||
LastWriteTime = File.GetLastWriteTimeUtc(path)
|
||||
};
|
||||
|
||||
await _mediaItemRepository.Add(mediaItem);
|
||||
|
||||
return mediaItem;
|
||||
}
|
||||
|
||||
return modifiedMediaItems.ToSeq();
|
||||
}
|
||||
|
||||
private async Task RefreshPosterForItems(Seq<MediaItem> stalePosterMediaItems)
|
||||
{
|
||||
(Seq<MediaItem> movies, Seq<MediaItem> episodes) = stalePosterMediaItems
|
||||
.Map(i => Optional(i).Filter(i2 => i2.Metadata?.MediaType == MediaType.TvShow).ToEither(i))
|
||||
.Partition();
|
||||
|
||||
// there's a 1:1 movie:poster, so refresh all
|
||||
foreach (MediaItem movie in movies)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug("Refreshing poster for media item {MediaItem}", movie.Path);
|
||||
await _localPosterProvider.RefreshPoster(movie);
|
||||
}
|
||||
|
||||
// we currently have 1 poster per series, so pick the first from each group
|
||||
IEnumerable<MediaItem> episodesToRefresh = episodes.GroupBy(e => e.Metadata.Title)
|
||||
.SelectMany(g => (Option<MediaItem>) g.FirstOrDefault());
|
||||
|
||||
foreach (MediaItem episode in episodesToRefresh)
|
||||
{
|
||||
_logger.LogDebug("Refreshing poster for media item {MediaItem}", episode.Path);
|
||||
await _localPosterProvider.RefreshPoster(episode);
|
||||
_logger.LogError(ex, "Failed to add media item for {Path}", path);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveMissingItems(Seq<MediaItem> removedMediaItems)
|
||||
private async Task RemoveMissingItem(MediaItem mediaItem)
|
||||
{
|
||||
// TODO: flag as missing? delete after some period of time?
|
||||
foreach (MediaItem mediaItem in removedMediaItems)
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Removing missing local media item {MediaItem}", mediaItem.Path);
|
||||
await _mediaItemRepository.Delete(mediaItem.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to remove missing local media item {MediaItem}", mediaItem.Path);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> RefreshMetadata(MediaItem mediaItem, string ffprobePath)
|
||||
private async Task SavePosterForItem(MediaItem mediaItem, string posterPath)
|
||||
{
|
||||
bool durationChange = await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
|
||||
await _localMetadataProvider.RefreshMetadata(mediaItem);
|
||||
bool collectionChange = await _smartCollectionBuilder.RefreshSmartCollections(mediaItem);
|
||||
return durationChange || collectionChange;
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,15 @@ namespace ErsatzTV.Core.Metadata
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RefreshMetadata(MediaItem mediaItem)
|
||||
public async Task RefreshSidecarMetadata(MediaItem mediaItem, string path)
|
||||
{
|
||||
Option<MediaMetadata> maybeMetadata = await LoadMetadata(mediaItem);
|
||||
MediaMetadata metadata =
|
||||
maybeMetadata.IfNone(() => FallbackMetadataProvider.GetFallbackMetadata(mediaItem));
|
||||
await ApplyMetadataUpdate(mediaItem, metadata);
|
||||
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)
|
||||
@@ -40,6 +41,8 @@ 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;
|
||||
@@ -56,9 +59,8 @@ 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);
|
||||
@@ -88,6 +90,8 @@ namespace ErsatzTV.Core.Metadata
|
||||
return maybeNfo.Match<Option<MediaMetadata>>(
|
||||
nfo => new MediaMetadata
|
||||
{
|
||||
Source = MetadataSource.Sidecar,
|
||||
LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName),
|
||||
MediaType = MediaType.TvShow,
|
||||
Title = nfo.ShowTitle,
|
||||
Subtitle = nfo.Title,
|
||||
@@ -114,6 +118,8 @@ namespace ErsatzTV.Core.Metadata
|
||||
return maybeNfo.Match<Option<MediaMetadata>>(
|
||||
nfo => new MediaMetadata
|
||||
{
|
||||
Source = MetadataSource.Sidecar,
|
||||
LastWriteTime = File.GetLastWriteTimeUtc(nfoFileName),
|
||||
MediaType = MediaType.Movie,
|
||||
Title = nfo.Title,
|
||||
Description = nfo.Outline,
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace ErsatzTV.Core.Metadata
|
||||
return None;
|
||||
}
|
||||
|
||||
private async Task SavePosterToDisk(MediaItem mediaItem, string posterPath)
|
||||
public async Task SavePosterToDisk(MediaItem mediaItem, string posterPath)
|
||||
{
|
||||
byte[] originalBytes = await File.ReadAllBytesAsync(posterPath);
|
||||
Either<BaseError, string> maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, 220, null);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
"ErsatzTV.Core.AggregateModels.MediaItemSummary",
|
||||
b =>
|
||||
{
|
||||
b.Property<int>("MediaItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -221,6 +224,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b.Property<string>("Poster")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("PosterLastWriteTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MediaSourceId");
|
||||
@@ -638,6 +644,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b1.Property<int>("Height")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<DateTime?>("LastWriteTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int>("MediaType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -650,6 +659,9 @@ namespace ErsatzTV.Infrastructure.Migrations
|
||||
b1.Property<string>("SortTitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int>("Source")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Subtitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace ErsatzTV.Services
|
||||
case RefreshMediaItem refreshMediaItem:
|
||||
string type = refreshMediaItem switch
|
||||
{
|
||||
RefreshMediaItemMetadata => "metadata",
|
||||
// RefreshMediaItemMetadata => "metadata",
|
||||
RefreshMediaItemStatistics => "statistics",
|
||||
RefreshMediaItemCollections => "collections",
|
||||
RefreshMediaItemPoster => "poster",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="Refresh All Metadata">
|
||||
<MudTooltip Text="Scan Media Source">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
||||
Disabled="@Locker.IsMediaSourceLocked(context.Id)"
|
||||
OnClick="@(_ => RefreshAllMetadata(context))">
|
||||
|
||||
@@ -180,6 +180,7 @@ namespace ErsatzTV
|
||||
services.AddScoped<IPlayoutBuilder, PlayoutBuilder>();
|
||||
services.AddScoped<IImageCache, ImageCache>();
|
||||
services.AddScoped<ILocalFileSystem, LocalFileSystem>();
|
||||
services.AddScoped<ILocalMediaSourcePlanner, LocalMediaSourcePlanner>();
|
||||
|
||||
services.AddHostedService<PlexService>();
|
||||
services.AddHostedService<FFmpegLocatorService>();
|
||||
|
||||
Reference in New Issue
Block a user