Browse Source

optimize scanning so playouts only rebuild when necessary

pull/24/head
Jason Dove 6 years ago
parent
commit
965877b5c9
  1. 3
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs
  2. 2
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs
  3. 13
      ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs
  4. 2
      ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs
  5. 6
      ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs
  6. 2
      ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs
  7. 2
      ErsatzTV.Core/Interfaces/Metadata/ISmartCollectionBuilder.cs
  8. 2
      ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs
  9. 2
      ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs
  10. 26
      ErsatzTV.Core/Metadata/LocalMediaScanner.cs
  11. 11
      ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs
  12. 8
      ErsatzTV.Core/Metadata/ScanningMode.cs
  13. 8
      ErsatzTV.Core/Metadata/SmartCollectionBuilder.cs
  14. 7
      ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs
  15. 4
      ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs
  16. 2
      ErsatzTV.Infrastructure/Locking/EntityLocker.cs
  17. 3
      ErsatzTV/Pages/LocalMediaSourceEditor.razor
  18. 2
      ErsatzTV/Pages/PlayoutEditor.razor
  19. 5
      ErsatzTV/Services/SchedulerService.cs
  20. 3
      ErsatzTV/Shared/LocalMediaSources.razor

3
ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs

@ -1,10 +1,11 @@ @@ -1,10 +1,11 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Metadata;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
{
public record ScanLocalMediaSource(int MediaSourceId, bool RefreshAllMetadata) :
public record ScanLocalMediaSource(int MediaSourceId, ScanningMode ScanningMode) :
IRequest<Either<BaseError, string>>,
IBackgroundServiceRequest;
}

2
ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs

@ -42,7 +42,7 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -42,7 +42,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
await _localMediaScanner.ScanLocalMediaSource(
parameters.LocalMediaSource,
parameters.FFprobePath,
request.RefreshAllMetadata);
request.ScanningMode);
_entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id);

13
ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs

@ -1,4 +1,5 @@ @@ -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 @@ -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)

2
ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs

@ -45,7 +45,7 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -45,7 +45,7 @@ namespace ErsatzTV.Core.Tests.Fakes
public Task Update(SimpleMediaCollection collection) => throw new NotSupportedException();
public Task InsertOrIgnore(TelevisionMediaCollection collection) => throw new NotSupportedException();
public Task<bool> InsertOrIgnore(TelevisionMediaCollection collection) => throw new NotSupportedException();
public Task<Unit> ReplaceItems(int collectionId, List<MediaItem> mediaItems) =>
throw new NotSupportedException();

6
ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs

@ -1,11 +1,15 @@ @@ -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, bool refreshAllMetadata);
Task<Unit> ScanLocalMediaSource(
LocalMediaSource localMediaSource,
string ffprobePath,
ScanningMode scanningMode);
}
}

2
ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs

@ -5,6 +5,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata @@ -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);
}
}

2
ErsatzTV.Core/Interfaces/Metadata/ISmartCollectionBuilder.cs

@ -5,6 +5,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata @@ -5,6 +5,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata
{
public interface ISmartCollectionBuilder
{
Task RefreshSmartCollections(MediaItem mediaItem);
Task<bool> RefreshSmartCollections(MediaItem mediaItem);
}
}

2
ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs

@ -20,7 +20,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -20,7 +20,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
public Task<Option<List<MediaItem>>> GetSimpleMediaCollectionItems(int id);
public Task<Option<List<MediaItem>>> GetTelevisionMediaCollectionItems(int id);
public Task Update(SimpleMediaCollection collection);
public Task InsertOrIgnore(TelevisionMediaCollection collection);
public Task<bool> InsertOrIgnore(TelevisionMediaCollection collection);
public Task<Unit> ReplaceItems(int collectionId, List<MediaItem> mediaItems);
public Task Delete(int mediaCollectionId);
public Task DeleteEmptyTelevisionCollections();

2
ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -15,7 +15,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
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);
}
}

26
ErsatzTV.Core/Metadata/LocalMediaScanner.cs

@ -47,7 +47,7 @@ namespace ErsatzTV.Core.Metadata @@ -47,7 +47,7 @@ namespace ErsatzTV.Core.Metadata
public async Task<Unit> ScanLocalMediaSource(
LocalMediaSource localMediaSource,
string ffprobePath,
bool refreshAllMetadata)
ScanningMode scanningMode)
{
if (!Directory.Exists(localMediaSource.Folder))
{
@ -100,7 +100,7 @@ namespace ErsatzTV.Core.Metadata @@ -100,7 +100,7 @@ namespace ErsatzTV.Core.Metadata
// if exists, check if the file was modified
// also, try to re-categorize incorrect media types by refreshing metadata
Seq<MediaItem> modifiedMediaItems = refreshAllMetadata
Seq<MediaItem> mediaItemsToScan = scanningMode == ScanningMode.RescanAll
? existingMediaItems
: existingMediaItems.Filter(
mediaItem =>
@ -110,13 +110,22 @@ namespace ErsatzTV.Core.Metadata @@ -110,13 +110,22 @@ namespace ErsatzTV.Core.Metadata
return modified || mediaItem.Metadata == null ||
mediaItem.Metadata.MediaType != localMediaSource.MediaType;
});
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems));
foreach (MediaItem mediaItem in modifiedMediaItems)
var modifiedMediaItems = new Seq<MediaItem>();
foreach (MediaItem mediaItem in mediaItemsToScan)
{
_logger.LogDebug("Refreshing metadata for media item {MediaItem}", mediaItem.Path);
await RefreshMetadata(mediaItem, ffprobePath);
if (await RefreshMetadata(mediaItem, ffprobePath))
{
// only queue playout rebuilds for media items
// where the duration or collections have changed
modifiedMediaItems.Add(mediaItem);
}
}
modifiedPlayoutIds.AddRange(await _playoutRepository.GetPlayoutIdsForMediaItems(modifiedMediaItems));
// if new, add and store mtime, refresh metadata
var addedMediaItems = new Seq<MediaItem>();
foreach (string path in newFiles)
@ -151,12 +160,13 @@ namespace ErsatzTV.Core.Metadata @@ -151,12 +160,13 @@ namespace ErsatzTV.Core.Metadata
return Unit.Default;
}
private async Task RefreshMetadata(MediaItem mediaItem, string ffprobePath)
private async Task<bool> RefreshMetadata(MediaItem mediaItem, string ffprobePath)
{
await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
bool durationChange = await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem);
await _localMetadataProvider.RefreshMetadata(mediaItem);
await _localPosterProvider.RefreshPoster(mediaItem);
await _smartCollectionBuilder.RefreshSmartCollections(mediaItem);
bool collectionChange = await _smartCollectionBuilder.RefreshSmartCollections(mediaItem);
return durationChange || collectionChange;
}
private static bool ShouldExcludeDirectory(string path) => File.Exists(Path.Combine(path, ".etvignore"));

11
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs

@ -26,21 +26,22 @@ namespace ErsatzTV.Core.Metadata @@ -26,21 +26,22 @@ namespace ErsatzTV.Core.Metadata
_logger = logger;
}
public async Task RefreshStatistics(string ffprobePath, MediaItem mediaItem)
public async Task<bool> RefreshStatistics(string ffprobePath, MediaItem mediaItem)
{
try
{
FFprobe ffprobe = await GetProbeOutput(ffprobePath, mediaItem);
MediaMetadata metadata = ProjectToMediaMetadata(ffprobe);
await ApplyStatisticsUpdate(mediaItem, metadata);
return await ApplyStatisticsUpdate(mediaItem, metadata);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to refresh statistics for media item at {Path}", mediaItem.Path);
return false;
}
}
private async Task ApplyStatisticsUpdate(
private async Task<bool> ApplyStatisticsUpdate(
MediaItem mediaItem,
MediaMetadata metadata)
{
@ -49,6 +50,8 @@ namespace ErsatzTV.Core.Metadata @@ -49,6 +50,8 @@ namespace ErsatzTV.Core.Metadata
mediaItem.Metadata = new MediaMetadata();
}
bool durationChange = mediaItem.Metadata.Duration != metadata.Duration;
mediaItem.Metadata.Duration = metadata.Duration;
mediaItem.Metadata.AudioCodec = metadata.AudioCodec;
mediaItem.Metadata.SampleAspectRatio = metadata.SampleAspectRatio;
@ -58,7 +61,7 @@ namespace ErsatzTV.Core.Metadata @@ -58,7 +61,7 @@ namespace ErsatzTV.Core.Metadata
mediaItem.Metadata.VideoCodec = metadata.VideoCodec;
mediaItem.Metadata.VideoScanType = metadata.VideoScanType;
await _mediaItemRepository.Update(mediaItem);
return await _mediaItemRepository.Update(mediaItem) && durationChange;
}
private Task<FFprobe> GetProbeOutput(string ffprobePath, MediaItem mediaItem)

8
ErsatzTV.Core/Metadata/ScanningMode.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Metadata
{
public enum ScanningMode
{
Default = 0,
RescanAll = 1
}
}

8
ErsatzTV.Core/Metadata/SmartCollectionBuilder.cs

@ -16,12 +16,16 @@ namespace ErsatzTV.Core.Metadata @@ -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
ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs

@ -85,15 +85,18 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -85,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) =>

4
ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs

@ -97,10 +97,10 @@ LIMIT {0} OFFSET {1}", @@ -97,10 +97,10 @@ LIMIT {0} OFFSET {1}",
.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)

2
ErsatzTV.Infrastructure/Locking/EntityLocker.cs

@ -14,7 +14,7 @@ namespace ErsatzTV.Infrastructure.Locking @@ -14,7 +14,7 @@ namespace ErsatzTV.Infrastructure.Locking
public bool LockMediaSource(int mediaSourceId)
{
if (_lockedMediaSources.TryAdd(mediaSourceId, 0))
if (!_lockedMediaSources.ContainsKey(mediaSourceId) && _lockedMediaSources.TryAdd(mediaSourceId, 0))
{
OnMediaSourceChanged?.Invoke(this, EventArgs.Empty);
return true;

3
ErsatzTV/Pages/LocalMediaSourceEditor.razor

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
@page "/media/sources/local/add"
@using ErsatzTV.Application.MediaSources.Commands
@using ErsatzTV.Application.MediaSources
@using ErsatzTV.Core.Metadata
@inject NavigationManager NavigationManager
@inject ILogger<LocalMediaSourceEditor> Logger
@inject ISnackbar Snackbar
@ -87,7 +88,7 @@ @@ -87,7 +88,7 @@
{
if (Locker.LockMediaSource(vm.Id))
{
await Channel.WriteAsync(new ScanLocalMediaSource(vm.Id, false));
await Channel.WriteAsync(new ScanLocalMediaSource(vm.Id, ScanningMode.Default));
NavigationManager.NavigateTo("/media/sources");
}
});

2
ErsatzTV/Pages/PlayoutEditor.razor

@ -65,7 +65,7 @@ @@ -65,7 +65,7 @@
errorMessage.HeadOrNone().Match(
error =>
{
Snackbar.Add(error.Value);
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error saving playout: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("/playouts"));

5
ErsatzTV/Services/SchedulerService.cs

@ -7,6 +7,7 @@ using ErsatzTV.Application; @@ -7,6 +7,7 @@ using ErsatzTV.Application;
using ErsatzTV.Application.MediaSources.Commands;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@ -81,7 +82,9 @@ namespace ErsatzTV.Services @@ -81,7 +82,9 @@ namespace ErsatzTV.Services
{
if (_entityLocker.LockMediaSource(mediaSourceId))
{
await _channel.WriteAsync(new ScanLocalMediaSource(mediaSourceId, false), cancellationToken);
await _channel.WriteAsync(
new ScanLocalMediaSource(mediaSourceId, ScanningMode.Default),
cancellationToken);
}
}
}

3
ErsatzTV/Shared/LocalMediaSources.razor

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
@using ErsatzTV.Application.MediaSources
@using ErsatzTV.Application.MediaSources.Commands
@using ErsatzTV.Application.MediaSources.Queries
@using ErsatzTV.Core.Metadata
@implements IDisposable
@inject IDialogService Dialog
@inject IMediator Mediator
@ -89,7 +90,7 @@ @@ -89,7 +90,7 @@
{
if (Locker.LockMediaSource(mediaSource.Id))
{
await Channel.WriteAsync(new ScanLocalMediaSource(mediaSource.Id, true));
await Channel.WriteAsync(new ScanLocalMediaSource(mediaSource.Id, ScanningMode.RescanAll));
StateHasChanged();
}
}

Loading…
Cancel
Save