Browse Source

scan media sources once every six hours

pull/28/head
Jason Dove 6 years ago
parent
commit
8f13989c52
  1. 2
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs
  2. 25
      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. 14
      ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs
  7. 1308
      ErsatzTV.Infrastructure/Migrations/20210222120255_MediaSourceLastScan.Designer.cs
  8. 24
      ErsatzTV.Infrastructure/Migrations/20210222120255_MediaSourceLastScan.cs
  9. 3
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  10. 22
      ErsatzTV/Services/WorkerService.cs

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

@ -4,6 +4,6 @@ using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands namespace ErsatzTV.Application.MediaSources.Commands
{ {
public record ScanLocalMediaSource(int MediaSourceId) : IRequest<Either<BaseError, string>>, public record ScanLocalMediaSource(int MediaSourceId) : IRequest<Either<Seq<BaseError>, string>>,
IBackgroundServiceRequest; IBackgroundServiceRequest;
} }

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

@ -1,18 +1,21 @@
using System.IO; using System;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit; using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaSources.Commands namespace ErsatzTV.Application.MediaSources.Commands
{ {
public class ScanLocalMediaSourceHandler : IRequestHandler<ScanLocalMediaSource, Either<BaseError, string>> public class ScanLocalMediaSourceHandler : IRequestHandler<ScanLocalMediaSource, Either<Seq<BaseError>, string>>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
@ -34,11 +37,11 @@ namespace ErsatzTV.Application.MediaSources.Commands
_entityLocker = entityLocker; _entityLocker = entityLocker;
} }
public Task<Either<BaseError, string>> public Task<Either<Seq<BaseError>, string>>
Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) => Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalMediaSource.Folder)) .MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalMediaSource.Folder))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEither().MapAsync<Seq<BaseError>, Task<string>, string>(identity));
private async Task<Unit> PerformScan(RequestParameters parameters) private async Task<Unit> PerformScan(RequestParameters parameters)
{ {
@ -52,21 +55,33 @@ namespace ErsatzTV.Application.MediaSources.Commands
break; break;
} }
parameters.LocalMediaSource.LastScan = DateTimeOffset.Now;
await _mediaSourceRepository.Update(parameters.LocalMediaSource);
_entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id); _entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id);
return Unit.Default; return Unit.Default;
} }
private async Task<Validation<BaseError, RequestParameters>> Validate(ScanLocalMediaSource request) => private async Task<Validation<BaseError, RequestParameters>> Validate(ScanLocalMediaSource request) =>
(await LocalMediaSourceMustExist(request), await ValidateFFprobePath()) (await ValidateLocalMediaSource(request), await ValidateFFprobePath())
.Apply((localMediaSource, ffprobePath) => new RequestParameters(localMediaSource, ffprobePath)); .Apply((localMediaSource, ffprobePath) => new RequestParameters(localMediaSource, ffprobePath));
private Task<Validation<BaseError, LocalMediaSource>> ValidateLocalMediaSource(ScanLocalMediaSource request) =>
LocalMediaSourceMustExist(request).BindT(ValidateLastScan);
private Task<Validation<BaseError, LocalMediaSource>> LocalMediaSourceMustExist( private Task<Validation<BaseError, LocalMediaSource>> LocalMediaSourceMustExist(
ScanLocalMediaSource request) => ScanLocalMediaSource request) =>
_mediaSourceRepository.Get(request.MediaSourceId) _mediaSourceRepository.Get(request.MediaSourceId)
.Map(maybeMediaSource => maybeMediaSource.Map(ms => ms as LocalMediaSource)) .Map(maybeMediaSource => maybeMediaSource.Map(ms => ms as LocalMediaSource))
.Map(v => v.ToValidation<BaseError>($"Local media source {request.MediaSourceId} does not exist.")); .Map(v => v.ToValidation<BaseError>($"Local media source {request.MediaSourceId} does not exist."));
private Task<Validation<BaseError, LocalMediaSource>> ValidateLastScan(
LocalMediaSource localMediaSource) =>
Optional(localMediaSource)
.Filter(lms => lms.LastScan is null || lms.LastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
.ToValidation<BaseError>(new MediaSourceRecentlyScanned(localMediaSource.Folder))
.AsTask();
private Task<Validation<BaseError, string>> ValidateFFprobePath() => private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath) _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists) .FilterT(File.Exists)

5
ErsatzTV.Core/Domain/MediaSource.cs

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

10
ErsatzTV.Core/Errors/MediaSourceRecentlyScanned.cs

@ -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
Task<Option<MediaSource>> Get(int id); Task<Option<MediaSource>> Get(int id);
Task<Option<PlexMediaSource>> GetPlex(int id); Task<Option<PlexMediaSource>> GetPlex(int id);
Task<int> CountMediaItems(int id); Task<int> CountMediaItems(int id);
Task Update(LocalMediaSource localMediaSource);
Task Update(PlexMediaSource plexMediaSource); Task Update(PlexMediaSource plexMediaSource);
Task Delete(int id); Task Delete(int id);
} }

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

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

24
ErsatzTV.Infrastructure/Migrations/20210222120255_MediaSourceLastScan.cs

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

3
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

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

22
ErsatzTV/Services/WorkerService.cs

@ -6,6 +6,7 @@ using ErsatzTV.Application;
using ErsatzTV.Application.MediaSources.Commands; using ErsatzTV.Application.MediaSources.Commands;
using ErsatzTV.Application.Playouts.Commands; using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Errors;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -56,17 +57,28 @@ namespace ErsatzTV.Services
error.Value)); error.Value));
break; break;
case ScanLocalMediaSource scanLocalMediaSource: case ScanLocalMediaSource scanLocalMediaSource:
Either<BaseError, string> scanResult = await mediator.Send( Either<Seq<BaseError>, string> scanResult = await mediator.Send(
scanLocalMediaSource, scanLocalMediaSource,
cancellationToken); cancellationToken);
scanResult.BiIter( scanResult.BiIter(
name => _logger.LogDebug( name => _logger.LogDebug(
"Done scanning local media source {MediaSource}", "Done scanning local media source {MediaSource}",
name), name),
error => _logger.LogWarning( errors =>
"Unable to scan local media source {MediaSourceId}: {Error}", {
scanLocalMediaSource.MediaSourceId, BaseError error = errors.Head;
error.Value)); if (error is MediaSourceRecentlyScanned recentlyScanned)
{
_logger.LogInformation(recentlyScanned.Value);
}
else
{
_logger.LogWarning(
"Unable to scan local media source {MediaSourceId}: {Error}",
scanLocalMediaSource.MediaSourceId,
error.Value);
}
});
break; break;
} }
} }

Loading…
Cancel
Save