Browse Source

resize movie posters and store in cache with channel logos

pull/22/head
Jason Dove 6 years ago
parent
commit
d87ecb6440
  1. 35
      ErsatzTV.Application/Images/Commands/SaveImageToDiskHandler.cs
  2. 3
      ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs
  3. 3
      ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs
  4. 6
      ErsatzTV.Application/MediaItems/Queries/GetPosterContentsHandler.cs
  5. 2
      ErsatzTV.Core/AggregateModels/MediaItemSummary.cs
  6. 2
      ErsatzTV.Core/Domain/MediaItem.cs
  7. 11
      ErsatzTV.Core/Interfaces/Images/IImageCache.cs
  8. 42
      ErsatzTV.Core/Metadata/LocalPosterProvider.cs
  9. 6
      ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs
  10. 1
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  11. 64
      ErsatzTV.Infrastructure/Images/ImageCache.cs
  12. 42
      ErsatzTV.Infrastructure/Migrations/20210213221040_MediaItemPoster.Designer.cs
  13. 6
      ErsatzTV.Infrastructure/Migrations/20210213221040_MediaItemPoster.cs
  14. 44
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  15. 8
      ErsatzTV/Controllers/PostersController.cs
  16. 4
      ErsatzTV/Shared/MediaCard.razor
  17. 3
      ErsatzTV/Startup.cs

35
ErsatzTV.Application/Images/Commands/SaveImageToDiskHandler.cs

@ -1,9 +1,7 @@
using System; using System.Threading;
using System.IO;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Images;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
@ -11,33 +9,12 @@ namespace ErsatzTV.Application.Images.Commands
{ {
public class SaveImageToDiskHandler : IRequestHandler<SaveImageToDisk, Either<BaseError, string>> public class SaveImageToDiskHandler : IRequestHandler<SaveImageToDisk, Either<BaseError, string>>
{ {
private static readonly SHA1CryptoServiceProvider Crypto; private readonly IImageCache _imageCache;
static SaveImageToDiskHandler() => Crypto = new SHA1CryptoServiceProvider(); public SaveImageToDiskHandler(IImageCache imageCache) => _imageCache = imageCache;
public async Task<Either<BaseError, string>> Handle( public Task<Either<BaseError, string>> Handle(
SaveImageToDisk request, SaveImageToDisk request,
CancellationToken cancellationToken) CancellationToken cancellationToken) => _imageCache.SaveImage(request.Buffer);
{
try
{
byte[] hash = Crypto.ComputeHash(request.Buffer);
string hex = BitConverter.ToString(hash).Replace("-", string.Empty);
string fileName = Path.Combine(FileSystemLayout.ImageCacheFolder, hex);
if (!Directory.Exists(FileSystemLayout.ImageCacheFolder))
{
Directory.CreateDirectory(FileSystemLayout.ImageCacheFolder);
}
await File.WriteAllBytesAsync(fileName, request.Buffer, cancellationToken);
return hex;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
} }
} }

3
ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs

@ -1,9 +1,8 @@
namespace ErsatzTV.Application.MediaItems namespace ErsatzTV.Application.MediaItems
{ {
public record AggregateMediaItemViewModel( public record AggregateMediaItemViewModel(
int MediaItemId,
string Title, string Title,
string Subtitle, string Subtitle,
string SortTitle, string SortTitle,
bool HasPoster); string Poster);
} }

3
ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs

@ -30,11 +30,10 @@ namespace ErsatzTV.Application.MediaItems.Queries
var results = allItems var results = allItems
.Map( .Map(
s => new AggregateMediaItemViewModel( s => new AggregateMediaItemViewModel(
s.MediaItemId,
s.Title, s.Title,
s.Subtitle, s.Subtitle,
s.SortTitle, s.SortTitle,
!string.IsNullOrWhiteSpace(s.PosterPath))) s.Poster))
.ToList(); .ToList();
return new AggregateMediaItemResults(count, results); return new AggregateMediaItemResults(count, results);

6
ErsatzTV.Application/MediaItems/Queries/GetPosterContentsHandler.cs

@ -39,12 +39,12 @@ namespace ErsatzTV.Application.MediaItems.Queries
try try
{ {
return await _memoryCache.GetOrCreateAsync( return await _memoryCache.GetOrCreateAsync(
mediaItem.PosterPath, mediaItem.Poster,
async entry => async entry =>
{ {
entry.SlidingExpiration = TimeSpan.FromHours(1); entry.SlidingExpiration = TimeSpan.FromHours(1);
byte[] contents = await File.ReadAllBytesAsync(mediaItem.PosterPath); byte[] contents = await File.ReadAllBytesAsync(mediaItem.Poster);
MimeType mimeType = MimeTypes.GetMimeType(contents); MimeType mimeType = MimeTypes.GetMimeType(contents);
return new ImageViewModel(contents, mimeType.Name); return new ImageViewModel(contents, mimeType.Name);
}); });
@ -63,7 +63,7 @@ namespace ErsatzTV.Application.MediaItems.Queries
.ToValidation<BaseError>($"MediaItem {request.MediaItemId} does not exist."); .ToValidation<BaseError>($"MediaItem {request.MediaItemId} does not exist.");
private static Validation<BaseError, MediaItem> PosterPathMustExist(MediaItem mediaItem) => private static Validation<BaseError, MediaItem> PosterPathMustExist(MediaItem mediaItem) =>
Optional(mediaItem.PosterPath) Optional(mediaItem.Poster)
.Filter(p => !string.IsNullOrWhiteSpace(p) && File.Exists(p)) .Filter(p => !string.IsNullOrWhiteSpace(p) && File.Exists(p))
.Map(_ => mediaItem) .Map(_ => mediaItem)
.ToValidation<BaseError>($"MediaItem {mediaItem.Id} does not have a poster"); .ToValidation<BaseError>($"MediaItem {mediaItem.Id} does not have a poster");

2
ErsatzTV.Core/AggregateModels/MediaItemSummary.cs

@ -1,4 +1,4 @@
namespace ErsatzTV.Core.AggregateModels namespace ErsatzTV.Core.AggregateModels
{ {
public record MediaItemSummary(string Title, string SortTitle, string Subtitle, string PosterPath, int MediaItemId); public record MediaItemSummary(string Title, string SortTitle, string Subtitle, string Poster);
} }

2
ErsatzTV.Core/Domain/MediaItem.cs

@ -9,7 +9,7 @@ namespace ErsatzTV.Core.Domain
public int MediaSourceId { get; set; } public int MediaSourceId { get; set; }
public MediaSource Source { get; set; } public MediaSource Source { get; set; }
public string Path { get; set; } public string Path { get; set; }
public string PosterPath { get; set; } public string Poster { get; set; }
public MediaMetadata Metadata { get; set; } public MediaMetadata Metadata { get; set; }
public DateTime? LastWriteTime { get; set; } public DateTime? LastWriteTime { get; set; }
public IList<SimpleMediaCollection> SimpleMediaCollections { get; set; } public IList<SimpleMediaCollection> SimpleMediaCollections { get; set; }

11
ErsatzTV.Core/Interfaces/Images/IImageCache.cs

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using LanguageExt;
namespace ErsatzTV.Core.Interfaces.Images
{
public interface IImageCache
{
Task<Either<BaseError, string>> ResizeAndSaveImage(byte[] imageBuffer, int? height, int? width);
Task<Either<BaseError, string>> SaveImage(byte[] imageBuffer);
}
}

42
ErsatzTV.Core/Metadata/LocalPosterProvider.cs

@ -2,35 +2,42 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Images;
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 Microsoft.Extensions.Logging;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Metadata namespace ErsatzTV.Core.Metadata
{ {
public class LocalPosterProvider : ILocalPosterProvider public class LocalPosterProvider : ILocalPosterProvider
{ {
private readonly IImageCache _imageCache;
private readonly ILogger<LocalPosterProvider> _logger;
private readonly IMediaItemRepository _mediaItemRepository; private readonly IMediaItemRepository _mediaItemRepository;
public LocalPosterProvider(IMediaItemRepository mediaItemRepository) => public LocalPosterProvider(
IMediaItemRepository mediaItemRepository,
IImageCache imageCache,
ILogger<LocalPosterProvider> logger)
{
_mediaItemRepository = mediaItemRepository; _mediaItemRepository = mediaItemRepository;
_imageCache = imageCache;
_logger = logger;
}
public Task RefreshPoster(MediaItem mediaItem) public async Task RefreshPoster(MediaItem mediaItem)
{ {
Option<string> maybePoster = mediaItem.Metadata.MediaType switch Option<string> maybePosterPath = mediaItem.Metadata.MediaType switch
{ {
MediaType.Movie => RefreshMoviePoster(mediaItem), MediaType.Movie => RefreshMoviePoster(mediaItem),
MediaType.TvShow => RefreshTelevisionPoster(mediaItem), MediaType.TvShow => RefreshTelevisionPoster(mediaItem),
_ => None _ => None
}; };
return maybePoster.Match( await maybePosterPath.Match(
path => path => SavePosterToDisk(mediaItem, path),
{
mediaItem.PosterPath = path;
return _mediaItemRepository.Update(mediaItem);
},
Task.CompletedTask); Task.CompletedTask);
} }
@ -50,5 +57,22 @@ namespace ErsatzTV.Core.Metadata
} }
private Option<string> RefreshTelevisionPoster(MediaItem mediaItem) => None; private Option<string> RefreshTelevisionPoster(MediaItem mediaItem) => None;
private async Task SavePosterToDisk(MediaItem mediaItem, string posterPath)
{
byte[] originalBytes = await File.ReadAllBytesAsync(posterPath);
Either<BaseError, string> maybeHash = await _imageCache.ResizeAndSaveImage(originalBytes, 220, null);
await maybeHash.Match(
hash =>
{
mediaItem.Poster = hash;
return _mediaItemRepository.Update(mediaItem);
},
error =>
{
_logger.LogWarning("Unable to save poster to disk from {Path}: {Error}", posterPath, error.Value);
return Task.CompletedTask;
});
}
} }
} }

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

@ -49,8 +49,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
Metadata_Title AS Title, Metadata_Title AS Title,
Metadata_SortTitle AS SortTitle, Metadata_SortTitle AS SortTitle,
substr(Metadata_Aired, 1, 4) AS Subtitle, substr(Metadata_Aired, 1, 4) AS Subtitle,
PosterPath, Poster
Id as MediaItemId
FROM MediaItems WHERE Metadata_MediaType=2 FROM MediaItems WHERE Metadata_MediaType=2
ORDER BY Metadata_SortTitle ORDER BY Metadata_SortTitle
LIMIT {0} OFFSET {1}", LIMIT {0} OFFSET {1}",
@ -63,8 +62,7 @@ LIMIT {0} OFFSET {1}",
Metadata_Title AS Title, Metadata_Title AS Title,
Metadata_SortTitle AS SortTitle, Metadata_SortTitle AS SortTitle,
count(*) || ' Episodes' AS Subtitle, count(*) || ' Episodes' AS Subtitle,
PosterPath, Poster
min(Id) as MediaItemId
FROM MediaItems WHERE Metadata_MediaType=1 FROM MediaItems WHERE Metadata_MediaType=1
GROUP BY Metadata_Title, Metadata_SortTitle GROUP BY Metadata_Title, Metadata_SortTitle
ORDER BY Metadata_SortTitle ORDER BY Metadata_SortTitle

1
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -13,6 +13,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.3" />
<PackageReference Include="Refit" Version="6.0.1" /> <PackageReference Include="Refit" Version="6.0.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

64
ErsatzTV.Infrastructure/Images/ImageCache.cs

@ -0,0 +1,64 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Images;
using LanguageExt;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
namespace ErsatzTV.Infrastructure.Images
{
public class ImageCache : IImageCache
{
private static readonly SHA1CryptoServiceProvider Crypto;
static ImageCache() => Crypto = new SHA1CryptoServiceProvider();
public async Task<Either<BaseError, string>> ResizeAndSaveImage(byte[] imageBuffer, int? height, int? width)
{
await using var inStream = new MemoryStream(imageBuffer);
using var image = await Image.LoadAsync(inStream);
Size size = height.HasValue ? new Size { Height = height.Value } : new Size { Width = width.Value };
image.Mutate(
i => i.Resize(
new ResizeOptions
{
Mode = ResizeMode.Max,
Size = size
}));
await using var outStream = new MemoryStream();
await image.SaveAsync(outStream, new JpegEncoder { Quality = 90 });
return await SaveImage(outStream.ToArray());
}
public async Task<Either<BaseError, string>> SaveImage(byte[] imageBuffer)
{
try
{
byte[] hash = Crypto.ComputeHash(imageBuffer);
string hex = BitConverter.ToString(hash).Replace("-", string.Empty);
string fileName = Path.Combine(FileSystemLayout.ImageCacheFolder, hex);
if (!Directory.Exists(FileSystemLayout.ImageCacheFolder))
{
Directory.CreateDirectory(FileSystemLayout.ImageCacheFolder);
}
await File.WriteAllBytesAsync(fileName, imageBuffer);
return hex;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
}
}

42
ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.Designer.cs → ErsatzTV.Infrastructure/Migrations/20210213221040_MediaItemPoster.Designer.cs generated

@ -9,8 +9,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace ErsatzTV.Infrastructure.Migrations namespace ErsatzTV.Infrastructure.Migrations
{ {
[DbContext(typeof(TvContext))] [DbContext(typeof(TvContext))]
[Migration("20210213191309_MediaItemPosterPath")] [Migration("20210213221040_MediaItemPoster")]
partial class MediaItemPosterPath partial class MediaItemPoster
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
@ -18,6 +18,42 @@ namespace ErsatzTV.Infrastructure.Migrations
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "5.0.3"); .HasAnnotation("ProductVersion", "5.0.3");
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.GenericIntegerId", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
});
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaCollectionSummary", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<bool>("IsSimple")
.HasColumnType("INTEGER");
b.Property<int>("ItemCount")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
});
modelBuilder.Entity("ErsatzTV.Core.AggregateModels.MediaItemSummary", b =>
{
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<string>("SortTitle")
.HasColumnType("TEXT");
b.Property<string>("Subtitle")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -168,7 +204,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("Path") b.Property<string>("Path")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("PosterPath") b.Property<string>("Poster")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.HasKey("Id"); b.HasKey("Id");

6
ErsatzTV.Infrastructure/Migrations/20210213191309_MediaItemPosterPath.cs → ErsatzTV.Infrastructure/Migrations/20210213221040_MediaItemPoster.cs

@ -2,7 +2,7 @@
namespace ErsatzTV.Infrastructure.Migrations namespace ErsatzTV.Infrastructure.Migrations
{ {
public partial class MediaItemPosterPath : Migration public partial class MediaItemPoster : Migration
{ {
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
@ -13,7 +13,7 @@ namespace ErsatzTV.Infrastructure.Migrations
"MediaCollectionSummaries"); "MediaCollectionSummaries");
migrationBuilder.AddColumn<string>( migrationBuilder.AddColumn<string>(
"PosterPath", "Poster",
"MediaItems", "MediaItems",
"TEXT", "TEXT",
nullable: true); nullable: true);
@ -22,7 +22,7 @@ namespace ErsatzTV.Infrastructure.Migrations
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropColumn( migrationBuilder.DropColumn(
"PosterPath", "Poster",
"MediaItems"); "MediaItems");
migrationBuilder.CreateTable( migrationBuilder.CreateTable(

44
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -16,6 +16,48 @@ namespace ErsatzTV.Infrastructure.Migrations
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "5.0.3"); .HasAnnotation("ProductVersion", "5.0.3");
modelBuilder.Entity(
"ErsatzTV.Core.AggregateModels.GenericIntegerId",
b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
});
modelBuilder.Entity(
"ErsatzTV.Core.AggregateModels.MediaCollectionSummary",
b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<bool>("IsSimple")
.HasColumnType("INTEGER");
b.Property<int>("ItemCount")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
});
modelBuilder.Entity(
"ErsatzTV.Core.AggregateModels.MediaItemSummary",
b =>
{
b.Property<string>("Poster")
.HasColumnType("TEXT");
b.Property<string>("SortTitle")
.HasColumnType("TEXT");
b.Property<string>("Subtitle")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
});
modelBuilder.Entity( modelBuilder.Entity(
"ErsatzTV.Core.Domain.Channel", "ErsatzTV.Core.Domain.Channel",
b => b =>
@ -176,7 +218,7 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("Path") b.Property<string>("Path")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("PosterPath") b.Property<string>("Poster")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.HasKey("Id"); b.HasKey("Id");

8
ErsatzTV/Controllers/PostersController.cs

@ -1,6 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.Images; using ErsatzTV.Application.Images;
using ErsatzTV.Application.MediaItems.Queries; using ErsatzTV.Application.Images.Queries;
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
@ -16,10 +16,10 @@ namespace ErsatzTV.Controllers
public PostersController(IMediator mediator) => _mediator = mediator; public PostersController(IMediator mediator) => _mediator = mediator;
[HttpGet("/posters/{mediaItemId}")] [HttpGet("/posters/{fileName}")]
public async Task<IActionResult> ForMediaItem(int mediaItemId) public async Task<IActionResult> GetImage(string fileName)
{ {
Either<BaseError, ImageViewModel> imageContents = await _mediator.Send(new GetPosterContents(mediaItemId)); Either<BaseError, ImageViewModel> imageContents = await _mediator.Send(new GetImageContents(fileName));
return imageContents.Match<IActionResult>( return imageContents.Match<IActionResult>(
Left: _ => new NotFoundResult(), Left: _ => new NotFoundResult(),
Right: r => new FileContentResult(r.Contents, r.MimeType)); Right: r => new FileContentResult(r.Contents, r.MimeType));

4
ErsatzTV/Shared/MediaCard.razor

@ -1,9 +1,9 @@
@using ErsatzTV.Application.MediaItems @using ErsatzTV.Application.MediaItems
<div class="media-card-container mx-3 pb-3"> <div class="media-card-container mx-3 pb-3">
<MudPaper Class="media-card"> <MudPaper Class="media-card">
@if (Data.HasPoster) @if (!string.IsNullOrWhiteSpace(Data.Poster))
{ {
<img src="@($"/posters/{Data.MediaItemId}")" style="max-height: 220px"/> <img src="@($"/posters/{Data.Poster}")" style="max-height: 220px"/>
} }
else else
{ {

3
ErsatzTV/Startup.cs

@ -6,6 +6,7 @@ using ErsatzTV.Application.Channels.Queries;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
@ -15,6 +16,7 @@ using ErsatzTV.Core.Scheduling;
using ErsatzTV.Formatters; using ErsatzTV.Formatters;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Images;
using ErsatzTV.Infrastructure.Plex; using ErsatzTV.Infrastructure.Plex;
using ErsatzTV.Serialization; using ErsatzTV.Serialization;
using ErsatzTV.Services; using ErsatzTV.Services;
@ -167,6 +169,7 @@ namespace ErsatzTV
services.AddScoped<ILocalPosterProvider, LocalPosterProvider>(); services.AddScoped<ILocalPosterProvider, LocalPosterProvider>();
services.AddScoped<ILocalMediaScanner, LocalMediaScanner>(); services.AddScoped<ILocalMediaScanner, LocalMediaScanner>();
services.AddScoped<IPlayoutBuilder, PlayoutBuilder>(); services.AddScoped<IPlayoutBuilder, PlayoutBuilder>();
services.AddScoped<IImageCache, ImageCache>();
services.AddHostedService<PlexService>(); services.AddHostedService<PlexService>();
services.AddHostedService<FFmpegLocatorService>(); services.AddHostedService<FFmpegLocatorService>();

Loading…
Cancel
Save