Browse Source

media source scan interval (#28)

* scan media sources once every six hours

* cleanup

* force scan from ui
pull/29/head
Jason Dove 5 years ago committed by GitHub
parent
commit
38ab6c00ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs
  2. 66
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs
  3. 5
      ErsatzTV.Core/Domain/MediaSource.cs
  4. 10
      ErsatzTV.Core/Errors/MediaSourceRecentlyScanned.cs
  5. 1
      ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs
  6. 4
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  7. 14
      ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs
  8. 1308
      ErsatzTV.Infrastructure/Migrations/20210222120255_MediaSourceLastScan.Designer.cs
  9. 20
      ErsatzTV.Infrastructure/Migrations/20210222120255_MediaSourceLastScan.cs
  10. 3
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  11. 2
      ErsatzTV/Pages/Index.razor
  12. 2
      ErsatzTV/Pages/LocalMediaSourceEditor.razor
  13. 4
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  14. 2
      ErsatzTV/Services/SchedulerService.cs
  15. 2
      ErsatzTV/Services/WorkerService.cs
  16. 6
      ErsatzTV/Shared/LocalMediaSources.razor

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

@ -4,6 +4,19 @@ using MediatR; @@ -4,6 +4,19 @@ using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
{
public record ScanLocalMediaSource(int MediaSourceId) : IRequest<Either<BaseError, string>>,
IBackgroundServiceRequest;
public interface IScanLocalMediaSource : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest
{
int MediaSourceId { get; }
bool ForceScan { get; }
}
public record ScanLocalMediaSourceIfNeeded(int MediaSourceId) : IScanLocalMediaSource
{
public bool ForceScan => false;
}
public record ForceScanLocalMediaSource(int MediaSourceId) : IScanLocalMediaSource
{
public bool ForceScan => true;
}
}

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
@ -8,14 +9,17 @@ using ErsatzTV.Core.Interfaces.Metadata; @@ -8,14 +9,17 @@ using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaSources.Commands
{
public class ScanLocalMediaSourceHandler : IRequestHandler<ScanLocalMediaSource, Either<BaseError, string>>
public class ScanLocalMediaSourceHandler : IRequestHandler<ForceScanLocalMediaSource, Either<BaseError, string>>,
IRequestHandler<ScanLocalMediaSourceIfNeeded, Either<BaseError, string>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEntityLocker _entityLocker;
private readonly ILogger<ScanLocalMediaSourceHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMovieFolderScanner _movieFolderScanner;
private readonly ITelevisionFolderScanner _televisionFolderScanner;
@ -25,44 +29,72 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -25,44 +29,72 @@ namespace ErsatzTV.Application.MediaSources.Commands
IConfigElementRepository configElementRepository,
IMovieFolderScanner movieFolderScanner,
ITelevisionFolderScanner televisionFolderScanner,
IEntityLocker entityLocker)
IEntityLocker entityLocker,
ILogger<ScanLocalMediaSourceHandler> logger)
{
_mediaSourceRepository = mediaSourceRepository;
_configElementRepository = configElementRepository;
_movieFolderScanner = movieFolderScanner;
_televisionFolderScanner = televisionFolderScanner;
_entityLocker = entityLocker;
_logger = logger;
}
public Task<Either<BaseError, string>>
Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) =>
public Task<Either<BaseError, string>> Handle(
ForceScanLocalMediaSource request,
CancellationToken cancellationToken) =>
Handle((IScanLocalMediaSource) request, cancellationToken);
public Task<Either<BaseError, string>> Handle(
ScanLocalMediaSourceIfNeeded request,
CancellationToken cancellationToken) =>
Handle((IScanLocalMediaSource) request, cancellationToken);
private Task<Either<BaseError, string>>
Handle(IScanLocalMediaSource request, CancellationToken cancellationToken) =>
Validate(request)
.MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalMediaSource.Folder))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> PerformScan(RequestParameters parameters)
{
switch (parameters.LocalMediaSource.MediaType)
DateTimeOffset lastScan = parameters.LocalMediaSource.LastScan ?? DateTime.MinValue;
if (parameters.ForceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
{
switch (parameters.LocalMediaSource.MediaType)
{
case MediaType.Movie:
await _movieFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
case MediaType.TvShow:
await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
}
parameters.LocalMediaSource.LastScan = DateTimeOffset.Now;
await _mediaSourceRepository.Update(parameters.LocalMediaSource);
}
else
{
case MediaType.Movie:
await _movieFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
case MediaType.TvShow:
await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
_logger.LogDebug(
"Skipping unforced scan of media source {Folder}",
parameters.LocalMediaSource.Folder);
}
_entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(ScanLocalMediaSource request) =>
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalMediaSource request) =>
(await LocalMediaSourceMustExist(request), await ValidateFFprobePath())
.Apply((localMediaSource, ffprobePath) => new RequestParameters(localMediaSource, ffprobePath));
.Apply(
(localMediaSource, ffprobePath) => new RequestParameters(
localMediaSource,
ffprobePath,
request.ForceScan));
private Task<Validation<BaseError, LocalMediaSource>> LocalMediaSourceMustExist(
ScanLocalMediaSource request) =>
IScanLocalMediaSource request) =>
_mediaSourceRepository.Get(request.MediaSourceId)
.Map(maybeMediaSource => maybeMediaSource.Map(ms => ms as LocalMediaSource))
.Map(v => v.ToValidation<BaseError>($"Local media source {request.MediaSourceId} does not exist."));
@ -74,6 +106,6 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -74,6 +106,6 @@ namespace ErsatzTV.Application.MediaSources.Commands
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters(LocalMediaSource LocalMediaSource, string FFprobePath);
private record RequestParameters(LocalMediaSource LocalMediaSource, string FFprobePath, bool ForceScan);
}
}

5
ErsatzTV.Core/Domain/MediaSource.cs

@ -1,9 +1,12 @@ @@ -1,9 +1,12 @@
namespace ErsatzTV.Core.Domain
using System;
namespace ErsatzTV.Core.Domain
{
public abstract class MediaSource
{
public int Id { get; set; }
public MediaSourceType SourceType { get; set; }
public string Name { get; set; }
public DateTimeOffset? LastScan { get; set; }
}
}

10
ErsatzTV.Core/Errors/MediaSourceRecentlyScanned.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Errors
{
public class MediaSourceRecentlyScanned : BaseError
{
public MediaSourceRecentlyScanned(string folder) :
base($"Media source {folder} was already scanned recently; skipping scan.")
{
}
}
}

1
ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs

@ -14,6 +14,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -14,6 +14,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Option<MediaSource>> Get(int id);
Task<Option<PlexMediaSource>> GetPlex(int id);
Task<int> CountMediaItems(int id);
Task Update(LocalMediaSource localMediaSource);
Task Update(PlexMediaSource plexMediaSource);
Task Delete(int id);
}

4
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -197,7 +197,9 @@ namespace ErsatzTV.Core.Metadata @@ -197,7 +197,9 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Option<TelevisionEpisodeMetadata>> LoadEpisodeMetadata(TelevisionEpisodeMediaItem mediaItem, string nfoFileName)
private async Task<Option<TelevisionEpisodeMetadata>> LoadEpisodeMetadata(
TelevisionEpisodeMediaItem mediaItem,
string nfoFileName)
{
try
{

14
ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs

@ -60,11 +60,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -60,11 +60,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<int> CountMediaItems(int id) =>
_dbContext.MediaItems.CountAsync(i => i.MediaSourceId == id);
public async Task Update(PlexMediaSource plexMediaSource)
{
_dbContext.PlexMediaSources.Update(plexMediaSource);
await _dbContext.SaveChangesAsync();
}
public Task Update(LocalMediaSource localMediaSource) =>
_dbContext.LocalMediaSources.Update(localMediaSource)
.AsTask()
.Bind(_ => _dbContext.SaveChangesAsync());
public Task Update(PlexMediaSource plexMediaSource) =>
_dbContext.PlexMediaSources.Update(plexMediaSource)
.AsTask()
.Bind(_ => _dbContext.SaveChangesAsync());
public async Task Delete(int id)
{

1308
ErsatzTV.Infrastructure/Migrations/20210222120255_MediaSourceLastScan.Designer.cs generated

File diff suppressed because it is too large Load Diff

20
ErsatzTV.Infrastructure/Migrations/20210222120255_MediaSourceLastScan.cs

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class MediaSourceLastScan : Migration
{
protected override void Up(MigrationBuilder migrationBuilder) =>
migrationBuilder.AddColumn<DateTimeOffset>(
"LastScan",
"MediaSources",
"TEXT",
nullable: true);
protected override void Down(MigrationBuilder migrationBuilder) =>
migrationBuilder.DropColumn(
"LastScan",
"MediaSources");
}
}

3
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -248,6 +248,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -248,6 +248,9 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LastScan")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");

2
ErsatzTV/Pages/Index.razor

@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
<MudText>
<MudLink Href="/media/collections">Media collections</MudLink> have a <b>name</b> and contain a logical grouping of media items.
Collections may contain television shows, television seasons, television episodes or movies.
Collections containing television shows and television seasons are automatically updated as media is added or removed from the linked shows and seasons.
Collections containing television shows and television seasons are automatically updated as media is added or removed from the linked shows and seasons.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">

2
ErsatzTV/Pages/LocalMediaSourceEditor.razor

@ -87,7 +87,7 @@ @@ -87,7 +87,7 @@
{
if (Locker.LockMediaSource(vm.Id))
{
await Channel.WriteAsync(new ScanLocalMediaSource(vm.Id));
await Channel.WriteAsync(new ForceScanLocalMediaSource(vm.Id));
NavigationManager.NavigateTo("/media/sources");
}
});

4
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -103,8 +103,8 @@ @@ -103,8 +103,8 @@
NavigationManager.NavigateTo($"/media/collections/{collection.Id}");
}
}
private async Task AddToSchedule()
{
var parameters = new DialogParameters { { "EntityType", "season" }, { "EntityName", $"{_season.Title} - {_season.Plot}" } };

2
ErsatzTV/Services/SchedulerService.cs

@ -82,7 +82,7 @@ namespace ErsatzTV.Services @@ -82,7 +82,7 @@ namespace ErsatzTV.Services
if (_entityLocker.LockMediaSource(mediaSourceId))
{
await _channel.WriteAsync(
new ScanLocalMediaSource(mediaSourceId),
new ScanLocalMediaSourceIfNeeded(mediaSourceId),
cancellationToken);
}
}

2
ErsatzTV/Services/WorkerService.cs

@ -55,7 +55,7 @@ namespace ErsatzTV.Services @@ -55,7 +55,7 @@ namespace ErsatzTV.Services
buildPlayout.PlayoutId,
error.Value));
break;
case ScanLocalMediaSource scanLocalMediaSource:
case IScanLocalMediaSource scanLocalMediaSource:
Either<BaseError, string> scanResult = await mediator.Send(
scanLocalMediaSource,
cancellationToken);

6
ErsatzTV/Shared/LocalMediaSources.razor

@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
<MudTooltip Text="Scan Media Source">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
Disabled="@Locker.IsMediaSourceLocked(context.Id)"
OnClick="@(_ => RefreshAllMetadata(context))">
OnClick="@(_ => ScanMediaSource(context))">
</MudIconButton>
</MudTooltip>
}
@ -85,11 +85,11 @@ @@ -85,11 +85,11 @@
}
}
private async Task RefreshAllMetadata(LocalMediaSourceViewModel mediaSource)
private async Task ScanMediaSource(LocalMediaSourceViewModel mediaSource)
{
if (Locker.LockMediaSource(mediaSource.Id))
{
await Channel.WriteAsync(new ScanLocalMediaSource(mediaSource.Id));
await Channel.WriteAsync(new ForceScanLocalMediaSource(mediaSource.Id));
StateHasChanged();
}
}

Loading…
Cancel
Save