allow shuffling block items (#1571)
* allow shuffling block items * fix drop down search results
This commit is contained in:
@@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Re-uploading images should fix them
|
||||
- Recreate XMLTV channel list (including logos) when channels are edited in ErsatzTV
|
||||
- This bug caused the ErsatzTV logo to be used instead of channel logos in some cases
|
||||
- Update drop down search results in main search bar when items are created/edited/removed
|
||||
|
||||
### Changed
|
||||
- Upgrade from .NET 7 to .NET 8
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading.Channels;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -13,7 +14,8 @@ namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public class CreateChannelHandler(
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
ISearchTargets searchTargets)
|
||||
: IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
|
||||
{
|
||||
public async Task<Either<BaseError, CreateChannelResult>> Handle(
|
||||
@@ -29,6 +31,7 @@ public class CreateChannelHandler(
|
||||
{
|
||||
await dbContext.Channels.AddAsync(channel);
|
||||
await dbContext.SaveChangesAsync();
|
||||
searchTargets.SearchTargetsChanged();
|
||||
await workerChannel.WriteAsync(new RefreshChannelList());
|
||||
return new CreateChannelResult(channel.Id);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -12,24 +13,26 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||
|
||||
public DeleteChannelHandler(
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
ILocalFileSystem localFileSystem)
|
||||
ILocalFileSystem localFileSystem,
|
||||
ISearchTargets searchTargets)
|
||||
{
|
||||
_workerChannel = workerChannel;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_localFileSystem = localFileSystem;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(DeleteChannel request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Channel> validation = await ChannelMustExist(dbContext, request);
|
||||
|
||||
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c, cancellationToken));
|
||||
return await validation.Apply(c => DoDeletion(dbContext, c, cancellationToken));
|
||||
}
|
||||
|
||||
private async Task<Unit> DoDeletion(TvContext dbContext, Channel channel, CancellationToken cancellationToken)
|
||||
@@ -37,6 +40,8 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
|
||||
dbContext.Channels.Remove(channel);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
|
||||
// delete channel data from channel guide cache
|
||||
string cacheFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channel.Number}.xml");
|
||||
if (_localFileSystem.FileExists(cacheFile))
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Subtitles;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -14,7 +15,8 @@ namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public class UpdateChannelHandler(
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
ISearchTargets searchTargets)
|
||||
: IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
|
||||
{
|
||||
public async Task<Either<BaseError, ChannelViewModel>> Handle(
|
||||
@@ -70,6 +72,8 @@ public class UpdateChannelHandler(
|
||||
c.FallbackFillerId = update.FallbackFillerId;
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
searchTargets.SearchTargetsChanged();
|
||||
|
||||
if (c.SubtitleMode != ChannelSubtitleMode.None)
|
||||
{
|
||||
Option<Playout> maybePlayout = await dbContext.Playouts
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
@@ -8,9 +10,13 @@ public class
|
||||
CopyFFmpegProfileHandler : IRequestHandler<CopyFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
|
||||
{
|
||||
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
|
||||
public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository, ISearchTargets searchTargets)
|
||||
{
|
||||
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
|
||||
CopyFFmpegProfile request,
|
||||
@@ -19,9 +25,12 @@ public class
|
||||
.MapT(PerformCopy)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<FFmpegProfileViewModel> PerformCopy(CopyFFmpegProfile request) =>
|
||||
_ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name)
|
||||
.Map(ProjectToViewModel);
|
||||
private async Task<FFmpegProfileViewModel> PerformCopy(CopyFFmpegProfile request)
|
||||
{
|
||||
FFmpegProfile copy = await _ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name);
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
return ProjectToViewModel(copy);
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, CopyFFmpegProfile>> Validate(CopyFFmpegProfile request) =>
|
||||
ValidateName(request).AsTask().MapT(_ => request);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -10,9 +11,13 @@ public class CreateFFmpegProfileHandler :
|
||||
IRequestHandler<CreateFFmpegProfile, Either<BaseError, CreateFFmpegProfileResult>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, CreateFFmpegProfileResult>> Handle(
|
||||
CreateFFmpegProfile request,
|
||||
@@ -23,12 +28,13 @@ public class CreateFFmpegProfileHandler :
|
||||
return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
|
||||
}
|
||||
|
||||
private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
|
||||
private async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
|
||||
TvContext dbContext,
|
||||
FFmpegProfile ffmpegProfile)
|
||||
{
|
||||
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -9,23 +10,28 @@ namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
DeleteFFmpegProfile request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, FFmpegProfile> validation = await FFmpegProfileMustExist(dbContext, request);
|
||||
return await validation.Apply(p => DoDeletion(dbContext, p));
|
||||
}
|
||||
|
||||
private static async Task<Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
|
||||
private async Task<Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
|
||||
{
|
||||
dbContext.FFmpegProfiles.Remove(ffmpegProfile);
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -10,9 +11,13 @@ public class
|
||||
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, UpdateFFmpegProfileResult>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, UpdateFFmpegProfileResult>> Handle(
|
||||
UpdateFFmpegProfile request,
|
||||
@@ -23,7 +28,7 @@ public class
|
||||
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
|
||||
}
|
||||
|
||||
private static async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
|
||||
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
|
||||
TvContext dbContext,
|
||||
FFmpegProfile p,
|
||||
UpdateFFmpegProfile update)
|
||||
@@ -54,6 +59,9 @@ public class
|
||||
p.NormalizeFramerate = update.NormalizeFramerate;
|
||||
p.DeinterlaceVideo = update.DeinterlaceVideo;
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
|
||||
return new UpdateFFmpegProfileResult(p.Id);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||
@@ -10,25 +11,28 @@ public class CreateCollectionHandler :
|
||||
IRequestHandler<CreateCollection, Either<BaseError, MediaCollectionViewModel>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public CreateCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public CreateCollectionHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, MediaCollectionViewModel>> Handle(
|
||||
CreateCollection request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => PersistCollection(dbContext, c));
|
||||
}
|
||||
|
||||
private static async Task<MediaCollectionViewModel> PersistCollection(
|
||||
TvContext dbContext,
|
||||
Collection collection)
|
||||
private async Task<MediaCollectionViewModel> PersistCollection(TvContext dbContext, Collection collection)
|
||||
{
|
||||
await dbContext.Collections.AddAsync(collection);
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
return ProjectToViewModel(collection);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||
@@ -10,25 +11,30 @@ public class CreateMultiCollectionHandler :
|
||||
IRequestHandler<CreateMultiCollection, Either<BaseError, MultiCollectionViewModel>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public CreateMultiCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public CreateMultiCollectionHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, MultiCollectionViewModel>> Handle(
|
||||
CreateMultiCollection request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, MultiCollection> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => PersistCollection(dbContext, c));
|
||||
}
|
||||
|
||||
private static async Task<MultiCollectionViewModel> PersistCollection(
|
||||
private async Task<MultiCollectionViewModel> PersistCollection(
|
||||
TvContext dbContext,
|
||||
MultiCollection multiCollection)
|
||||
{
|
||||
await dbContext.MultiCollections.AddAsync(multiCollection);
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
await dbContext.Entry(multiCollection)
|
||||
.Collection(c => c.MultiCollectionItems)
|
||||
.Query()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||
@@ -10,25 +11,30 @@ public class CreateSmartCollectionHandler :
|
||||
IRequestHandler<CreateSmartCollection, Either<BaseError, SmartCollectionViewModel>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public CreateSmartCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public CreateSmartCollectionHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, SmartCollectionViewModel>> Handle(
|
||||
CreateSmartCollection request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, SmartCollection> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => PersistCollection(dbContext, c));
|
||||
}
|
||||
|
||||
private static async Task<SmartCollectionViewModel> PersistCollection(
|
||||
private async Task<SmartCollectionViewModel> PersistCollection(
|
||||
TvContext dbContext,
|
||||
SmartCollection smartCollection)
|
||||
{
|
||||
await dbContext.SmartCollections.AddAsync(smartCollection);
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
return ProjectToViewModel(smartCollection);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -9,24 +10,29 @@ namespace ErsatzTV.Application.MediaCollections;
|
||||
public class DeleteCollectionHandler : IRequestHandler<DeleteCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public DeleteCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public DeleteCollectionHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
DeleteCollection request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Collection> validation = await CollectionMustExist(dbContext, request);
|
||||
return await validation.Apply(c => DoDeletion(dbContext, c));
|
||||
}
|
||||
|
||||
private static Task<Unit> DoDeletion(TvContext dbContext, Collection collection)
|
||||
private async Task<Unit> DoDeletion(TvContext dbContext, Collection collection)
|
||||
{
|
||||
dbContext.Collections.Remove(collection);
|
||||
return dbContext.SaveChangesAsync().ToUnit();
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -9,24 +10,30 @@ namespace ErsatzTV.Application.MediaCollections;
|
||||
public class DeleteMultiCollectionHandler : IRequestHandler<DeleteMultiCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public DeleteMultiCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public DeleteMultiCollectionHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
DeleteMultiCollection request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
Validation<BaseError, MultiCollection> validation = await MultiCollectionMustExist(dbContext, request);
|
||||
return await validation.Apply(c => DoDeletion(dbContext, c));
|
||||
}
|
||||
|
||||
private static Task<Unit> DoDeletion(TvContext dbContext, MultiCollection multiCollection)
|
||||
private async Task<Unit> DoDeletion(TvContext dbContext, MultiCollection multiCollection)
|
||||
{
|
||||
dbContext.MultiCollections.Remove(multiCollection);
|
||||
return dbContext.SaveChangesAsync().ToUnit();
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, MultiCollection>> MultiCollectionMustExist(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -9,24 +10,29 @@ namespace ErsatzTV.Application.MediaCollections;
|
||||
public class DeleteSmartCollectionHandler : IRequestHandler<DeleteSmartCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public DeleteSmartCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public DeleteSmartCollectionHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
DeleteSmartCollection request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, SmartCollection> validation = await SmartCollectionMustExist(dbContext, request);
|
||||
return await validation.Apply(c => DoDeletion(dbContext, c));
|
||||
}
|
||||
|
||||
private static Task<Unit> DoDeletion(TvContext dbContext, SmartCollection smartCollection)
|
||||
private async Task<Unit> DoDeletion(TvContext dbContext, SmartCollection smartCollection)
|
||||
{
|
||||
dbContext.SmartCollections.Remove(smartCollection);
|
||||
return dbContext.SaveChangesAsync().ToUnit();
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, SmartCollection>> SmartCollectionMustExist(
|
||||
|
||||
@@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
@@ -13,17 +14,20 @@ namespace ErsatzTV.Application.MediaCollections;
|
||||
public class UpdateCollectionHandler : IRequestHandler<UpdateCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public UpdateCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -32,7 +36,7 @@ public class UpdateCollectionHandler : IRequestHandler<UpdateCollection, Either<
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
|
||||
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
|
||||
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyUpdateRequest(TvContext dbContext, Collection c, UpdateCollection request)
|
||||
@@ -53,6 +57,8 @@ public class UpdateCollectionHandler : IRequestHandler<UpdateCollection, Either<
|
||||
}
|
||||
}
|
||||
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
@@ -13,17 +14,20 @@ namespace ErsatzTV.Application.MediaCollections;
|
||||
public class UpdateMultiCollectionHandler : IRequestHandler<UpdateMultiCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public UpdateMultiCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -115,6 +119,8 @@ public class UpdateMultiCollectionHandler : IRequestHandler<UpdateMultiCollectio
|
||||
// rebuild playouts
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingMultiCollection(
|
||||
request.MultiCollectionId))
|
||||
|
||||
@@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
@@ -13,17 +14,20 @@ namespace ErsatzTV.Application.MediaCollections;
|
||||
public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public UpdateSmartCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -32,7 +36,7 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, SmartCollection> validation = await Validate(dbContext, request);
|
||||
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
|
||||
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyUpdateRequest(TvContext dbContext, SmartCollection c, UpdateSmartCollection request)
|
||||
@@ -42,6 +46,8 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
|
||||
// rebuild playouts
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
|
||||
// refresh all playouts that use this smart collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingSmartCollection(request.Id))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
@@ -11,9 +12,13 @@ public class CopyWatermarkHandler :
|
||||
IRequestHandler<CopyWatermark, Either<BaseError, WatermarkViewModel>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public CopyWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public CopyWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, WatermarkViewModel>> Handle(
|
||||
CopyWatermark request,
|
||||
@@ -24,7 +29,7 @@ public class CopyWatermarkHandler :
|
||||
|
||||
private async Task<WatermarkViewModel> PerformCopy(CopyWatermark request)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
ChannelWatermark channelWatermark = await dbContext.ChannelWatermarks.FindAsync(request.WatermarkId);
|
||||
|
||||
PropertyValues values = dbContext.Entry(channelWatermark).CurrentValues.Clone();
|
||||
@@ -37,6 +42,8 @@ public class CopyWatermarkHandler :
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
|
||||
return ProjectToViewModel(clone);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -8,25 +9,30 @@ namespace ErsatzTV.Application.Watermarks;
|
||||
public class CreateWatermarkHandler : IRequestHandler<CreateWatermark, Either<BaseError, CreateWatermarkResult>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public CreateWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public CreateWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, CreateWatermarkResult>> Handle(
|
||||
CreateWatermark request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, ChannelWatermark> validation = Validate(request);
|
||||
return await validation.Apply(profile => PersistChannelWatermark(dbContext, profile));
|
||||
}
|
||||
|
||||
private static async Task<CreateWatermarkResult> PersistChannelWatermark(
|
||||
private async Task<CreateWatermarkResult> PersistChannelWatermark(
|
||||
TvContext dbContext,
|
||||
ChannelWatermark watermark)
|
||||
{
|
||||
await dbContext.ChannelWatermarks.AddAsync(watermark);
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
return new CreateWatermarkResult(watermark.Id);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -9,25 +10,30 @@ namespace ErsatzTV.Application.Watermarks;
|
||||
public class DeleteWatermarkHandler : IRequestHandler<DeleteWatermark, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public DeleteWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public DeleteWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
DeleteWatermark request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, ChannelWatermark> validation = await WatermarkMustExist(dbContext, request);
|
||||
return await validation.Apply(p => DoDeletion(dbContext, p));
|
||||
}
|
||||
|
||||
private static async Task<Unit> DoDeletion(TvContext dbContext, ChannelWatermark watermark)
|
||||
private async Task<Unit> DoDeletion(TvContext dbContext, ChannelWatermark watermark)
|
||||
{
|
||||
await dbContext.Database.ExecuteSqlAsync(
|
||||
$"UPDATE Channel SET WatermarkId = NULL WHERE WatermarkId = {watermark.Id}");
|
||||
dbContext.ChannelWatermarks.Remove(watermark);
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -9,9 +10,13 @@ namespace ErsatzTV.Application.Watermarks;
|
||||
public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<BaseError, UpdateWatermarkResult>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
|
||||
public UpdateWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public UpdateWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, UpdateWatermarkResult>> Handle(
|
||||
UpdateWatermark request,
|
||||
@@ -22,7 +27,7 @@ public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<Ba
|
||||
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
|
||||
}
|
||||
|
||||
private static async Task<UpdateWatermarkResult> ApplyUpdateRequest(
|
||||
private async Task<UpdateWatermarkResult> ApplyUpdateRequest(
|
||||
TvContext dbContext,
|
||||
ChannelWatermark p,
|
||||
UpdateWatermark update)
|
||||
@@ -41,6 +46,7 @@ public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<Ba
|
||||
p.Opacity = update.Opacity;
|
||||
p.PlaceWithinSourceContent = update.PlaceWithinSourceContent;
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
return new UpdateWatermarkResult(p.Id);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ public class PlayoutHistory
|
||||
|
||||
public int BlockId { get; set; }
|
||||
public Block Block { get; set; }
|
||||
public PlaybackOrder PlaybackOrder { get; set; }
|
||||
public int Seed { get; set; }
|
||||
|
||||
// something that uniquely identifies the collection within the block
|
||||
public string Key { get; set; }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Interfaces.Search;
|
||||
|
||||
public interface ISearchTargets
|
||||
{
|
||||
event EventHandler OnSearchTargetsChanged;
|
||||
|
||||
void SearchTargetsChanged();
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public class BlockPlayoutBuilder(
|
||||
[
|
||||
PlaybackOrder.Chronological,
|
||||
PlaybackOrder.SeasonEpisode,
|
||||
PlaybackOrder.Shuffle,
|
||||
PlaybackOrder.Random
|
||||
];
|
||||
|
||||
@@ -177,6 +178,8 @@ public class BlockPlayoutBuilder(
|
||||
{
|
||||
PlayoutId = playout.Id,
|
||||
BlockId = blockItem.BlockId,
|
||||
PlaybackOrder = blockItem.PlaybackOrder,
|
||||
Seed = enumerator.State.Seed,
|
||||
When = currentTime.UtcDateTime,
|
||||
Key = historyKey,
|
||||
Details = HistoryDetails.ForMediaItem(mediaItem)
|
||||
@@ -212,41 +215,37 @@ public class BlockPlayoutBuilder(
|
||||
string historyKey,
|
||||
Map<CollectionKey, List<MediaItem>> collectionMediaItems)
|
||||
{
|
||||
DateTime historyTime = currentTime.UtcDateTime;
|
||||
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
|
||||
.Filter(h => h.BlockId == blockItem.BlockId)
|
||||
.Filter(h => h.Key == historyKey)
|
||||
.Filter(h => h.When < historyTime)
|
||||
.OrderByDescending(h => h.When)
|
||||
.HeadOrNone();
|
||||
|
||||
var state = new CollectionEnumeratorState { Seed = 0, Index = 0 };
|
||||
|
||||
var collectionKey = CollectionKey.ForBlockItem(blockItem);
|
||||
List<MediaItem> collectionItems = collectionMediaItems[collectionKey];
|
||||
|
||||
// get enumerator
|
||||
IMediaCollectionEnumerator enumerator = blockItem.PlaybackOrder switch
|
||||
{
|
||||
PlaybackOrder.Chronological => new ChronologicalMediaCollectionEnumerator(collectionItems, state),
|
||||
PlaybackOrder.SeasonEpisode => new SeasonEpisodeMediaCollectionEnumerator(collectionItems, state),
|
||||
PlaybackOrder.Chronological => BlockPlayoutEnumerator.Chronological(
|
||||
collectionItems,
|
||||
currentTime,
|
||||
playout,
|
||||
blockItem,
|
||||
historyKey,
|
||||
Logger),
|
||||
PlaybackOrder.SeasonEpisode => BlockPlayoutEnumerator.SeasonEpisode(
|
||||
collectionItems,
|
||||
currentTime,
|
||||
playout,
|
||||
blockItem,
|
||||
historyKey,
|
||||
Logger),
|
||||
PlaybackOrder.Shuffle => BlockPlayoutEnumerator.Shuffle(
|
||||
collectionItems,
|
||||
currentTime,
|
||||
playout,
|
||||
blockItem,
|
||||
historyKey),
|
||||
_ => new RandomizedMediaCollectionEnumerator(
|
||||
collectionItems,
|
||||
new CollectionEnumeratorState { Seed = new Random().Next(), Index = 0 })
|
||||
};
|
||||
|
||||
// seek to the appropriate place in the collection enumerator
|
||||
foreach (PlayoutHistory history in maybeHistory)
|
||||
{
|
||||
Logger.LogDebug("History is applicable: {When}: {History}", history.When, history.Details);
|
||||
|
||||
HistoryDetails.MoveToNextItem(
|
||||
collectionItems,
|
||||
history.Details,
|
||||
enumerator,
|
||||
blockItem.PlaybackOrder);
|
||||
}
|
||||
|
||||
return enumerator;
|
||||
}
|
||||
|
||||
@@ -288,8 +287,20 @@ public class BlockPlayoutBuilder(
|
||||
|
||||
IEnumerable<PlayoutHistory> toDelete = group
|
||||
.Filter(h => h.When < start.UtcDateTime)
|
||||
.OrderByDescending(h => h.When)
|
||||
.Tail();
|
||||
.OrderByDescending(h => h.When);
|
||||
|
||||
// chronological and season, episode only need to keep most recent entry
|
||||
if (group.Count > 0 && group[0].PlaybackOrder is PlaybackOrder.Chronological or PlaybackOrder.SeasonEpisode)
|
||||
{
|
||||
toDelete = toDelete.Tail();
|
||||
}
|
||||
|
||||
// shuffle needs to keep all entries with current seed
|
||||
if (group.Count > 0 && group[0].PlaybackOrder is PlaybackOrder.Shuffle)
|
||||
{
|
||||
int currentSeed = group[0].Seed;
|
||||
toDelete = toDelete.Filter(h => h.Seed != currentSeed);
|
||||
}
|
||||
|
||||
foreach (PlayoutHistory delete in toDelete)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
using System.Collections.Immutable;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling.BlockScheduling;
|
||||
|
||||
public static class BlockPlayoutEnumerator
|
||||
{
|
||||
public static IMediaCollectionEnumerator Chronological(
|
||||
List<MediaItem> collectionItems,
|
||||
DateTimeOffset currentTime,
|
||||
Playout playout,
|
||||
BlockItem blockItem,
|
||||
string historyKey,
|
||||
ILogger logger)
|
||||
{
|
||||
DateTime historyTime = currentTime.UtcDateTime;
|
||||
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
|
||||
.Filter(h => h.BlockId == blockItem.BlockId)
|
||||
.Filter(h => h.Key == historyKey)
|
||||
.Filter(h => h.When < historyTime)
|
||||
.OrderByDescending(h => h.When)
|
||||
.HeadOrNone();
|
||||
|
||||
var state = new CollectionEnumeratorState { Seed = 0, Index = 0 };
|
||||
|
||||
var enumerator = new ChronologicalMediaCollectionEnumerator(collectionItems, state);
|
||||
|
||||
// seek to the appropriate place in the collection enumerator
|
||||
foreach (PlayoutHistory h in maybeHistory)
|
||||
{
|
||||
logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details);
|
||||
|
||||
HistoryDetails.MoveToNextItem(
|
||||
collectionItems,
|
||||
h.Details,
|
||||
enumerator,
|
||||
blockItem.PlaybackOrder);
|
||||
}
|
||||
|
||||
return enumerator;
|
||||
}
|
||||
|
||||
public static IMediaCollectionEnumerator SeasonEpisode(
|
||||
List<MediaItem> collectionItems,
|
||||
DateTimeOffset currentTime,
|
||||
Playout playout,
|
||||
BlockItem blockItem,
|
||||
string historyKey,
|
||||
ILogger logger)
|
||||
{
|
||||
DateTime historyTime = currentTime.UtcDateTime;
|
||||
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
|
||||
.Filter(h => h.BlockId == blockItem.BlockId)
|
||||
.Filter(h => h.Key == historyKey)
|
||||
.Filter(h => h.When < historyTime)
|
||||
.OrderByDescending(h => h.When)
|
||||
.HeadOrNone();
|
||||
|
||||
var state = new CollectionEnumeratorState { Seed = 0, Index = 0 };
|
||||
|
||||
var enumerator = new SeasonEpisodeMediaCollectionEnumerator(collectionItems, state);
|
||||
|
||||
// seek to the appropriate place in the collection enumerator
|
||||
foreach (PlayoutHistory h in maybeHistory)
|
||||
{
|
||||
logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details);
|
||||
|
||||
HistoryDetails.MoveToNextItem(
|
||||
collectionItems,
|
||||
h.Details,
|
||||
enumerator,
|
||||
blockItem.PlaybackOrder);
|
||||
}
|
||||
|
||||
return enumerator;
|
||||
}
|
||||
|
||||
public static IMediaCollectionEnumerator Shuffle(
|
||||
List<MediaItem> collectionItems,
|
||||
DateTimeOffset currentTime,
|
||||
Playout playout,
|
||||
BlockItem blockItem,
|
||||
string historyKey)
|
||||
{
|
||||
// need a new shuffled media collection enumerator that can "hide" items for one iteration, then include all items again
|
||||
// maybe take a "masked items" hash set, then clear it after shuffling
|
||||
|
||||
DateTime historyTime = currentTime.UtcDateTime;
|
||||
var maskedMediaItemIds = new System.Collections.Generic.HashSet<int>();
|
||||
List<PlayoutHistory> history = playout.PlayoutHistory
|
||||
.Filter(h => h.BlockId == blockItem.BlockId)
|
||||
.Filter(h => h.Key == historyKey)
|
||||
.Filter(h => h.When < historyTime)
|
||||
.OrderByDescending(h => h.When)
|
||||
.ToList();
|
||||
|
||||
if (history.Count > 0)
|
||||
{
|
||||
int currentSeed = history[0].Seed;
|
||||
history = history.Filter(h => h.Seed == currentSeed).ToList();
|
||||
}
|
||||
|
||||
var knownMediaIds = collectionItems.Map(ci => ci.Id).ToImmutableHashSet();
|
||||
foreach (PlayoutHistory h in history)
|
||||
{
|
||||
HistoryDetails.Details details = JsonConvert.DeserializeObject<HistoryDetails.Details>(h.Details);
|
||||
foreach (int mediaItemId in Optional(details.MediaItemId))
|
||||
{
|
||||
if (knownMediaIds.Contains(mediaItemId))
|
||||
{
|
||||
maskedMediaItemIds.Add(mediaItemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var state = new CollectionEnumeratorState { Seed = new Random().Next(), Index = 0 };
|
||||
|
||||
// keep the current seed if one exists
|
||||
if (maskedMediaItemIds.Count > 0 && maskedMediaItemIds.Count < collectionItems.Count && history.Count > 0)
|
||||
{
|
||||
state.Seed = history[0].Seed;
|
||||
}
|
||||
|
||||
// if everything is masked, nothing is masked
|
||||
if (maskedMediaItemIds.Count == collectionItems.Count)
|
||||
{
|
||||
maskedMediaItemIds.Clear();
|
||||
}
|
||||
|
||||
// TODO: fix multi-collection groups, keep multi-part episodes together
|
||||
var mediaItems = collectionItems
|
||||
.Map(mi => new GroupedMediaItem(mi, null))
|
||||
.ToList();
|
||||
|
||||
Serilog.Log.Logger.Debug(
|
||||
"scheduling {X} media items with {Y} masked",
|
||||
mediaItems.Count,
|
||||
maskedMediaItemIds.Count);
|
||||
|
||||
// it shouldn't matter which order the remaining items are shuffled in,
|
||||
// as long as already-played items are not included
|
||||
return new MaskedShuffledMediaCollectionEnumerator(
|
||||
mediaItems,
|
||||
maskedMediaItemIds,
|
||||
state,
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Extensions;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling;
|
||||
|
||||
public class MaskedShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator
|
||||
{
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
private readonly Lazy<Option<TimeSpan>> _lazyMinimumDuration;
|
||||
private readonly int _mediaItemCount;
|
||||
private readonly IList<GroupedMediaItem> _mediaItems;
|
||||
private CloneableRandom _random;
|
||||
private IList<MediaItem> _shuffled;
|
||||
|
||||
public MaskedShuffledMediaCollectionEnumerator(
|
||||
IList<GroupedMediaItem> mediaItems,
|
||||
IReadOnlySet<int> maskedMediaItemIds,
|
||||
CollectionEnumeratorState state,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_mediaItemCount = mediaItems.Sum(i => 1 + Optional(i.Additional).Flatten().Count());
|
||||
_mediaItems = mediaItems;
|
||||
_cancellationToken = cancellationToken;
|
||||
|
||||
if (state.Index >= _mediaItems.Count)
|
||||
{
|
||||
state.Index = 0;
|
||||
state.Seed = new Random(state.Seed).Next();
|
||||
}
|
||||
|
||||
_random = new CloneableRandom(state.Seed);
|
||||
|
||||
// remove masked items from initial shuffle
|
||||
var filtered = _mediaItems.Filter(mi => !maskedMediaItemIds.Contains(mi.First.Id)).ToList();
|
||||
foreach (GroupedMediaItem group in filtered)
|
||||
{
|
||||
group.Additional.RemoveAll(mi => maskedMediaItemIds.Contains(mi.Id));
|
||||
}
|
||||
|
||||
_shuffled = Shuffle(filtered, _random);
|
||||
_lazyMinimumDuration =
|
||||
new Lazy<Option<TimeSpan>>(
|
||||
() => _shuffled.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone());
|
||||
|
||||
State = state;
|
||||
}
|
||||
|
||||
public void ResetState(CollectionEnumeratorState state)
|
||||
{
|
||||
// only re-shuffle if needed
|
||||
if (State.Seed != state.Seed)
|
||||
{
|
||||
_random = new CloneableRandom(state.Seed);
|
||||
_shuffled = Shuffle(_mediaItems, _random);
|
||||
}
|
||||
|
||||
State.Index = state.Index;
|
||||
}
|
||||
|
||||
public CollectionEnumeratorState State { get; }
|
||||
|
||||
public Option<MediaItem> Current => _shuffled.Any() ? _shuffled[State.Index % _mediaItemCount] : None;
|
||||
|
||||
public void MoveNext()
|
||||
{
|
||||
if ((State.Index + 1) % _mediaItemCount == 0)
|
||||
{
|
||||
Option<MediaItem> tail = Current;
|
||||
|
||||
State.Index = 0;
|
||||
do
|
||||
{
|
||||
State.Seed = _random.Next();
|
||||
_random = new CloneableRandom(State.Seed);
|
||||
_shuffled = Shuffle(_mediaItems, _random);
|
||||
} while (!_cancellationToken.IsCancellationRequested && _mediaItems.Count > 1 &&
|
||||
Current.Map(x => x.Id) == tail.Map(x => x.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
State.Index++;
|
||||
}
|
||||
|
||||
State.Index %= _mediaItemCount;
|
||||
}
|
||||
|
||||
public Option<TimeSpan> MinimumDuration => _lazyMinimumDuration.Value;
|
||||
|
||||
public int Count => _shuffled.Count;
|
||||
|
||||
private IList<MediaItem> Shuffle(IEnumerable<GroupedMediaItem> list, CloneableRandom random)
|
||||
{
|
||||
GroupedMediaItem[] copy = list.ToArray();
|
||||
|
||||
int n = copy.Length;
|
||||
while (n > 1)
|
||||
{
|
||||
n--;
|
||||
int k = random.Next(n + 1);
|
||||
(copy[k], copy[n]) = (copy[n], copy[k]);
|
||||
}
|
||||
|
||||
return GroupedMediaItem.FlattenGroups(copy, _mediaItemCount);
|
||||
}
|
||||
}
|
||||
Generated
+4922
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_PlayoutHistorySeed : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "PlaybackOrder",
|
||||
table: "PlayoutHistory",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Seed",
|
||||
table: "PlayoutHistory",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PlaybackOrder",
|
||||
table: "PlayoutHistory");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Seed",
|
||||
table: "PlayoutHistory");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1937,9 +1937,15 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("PlaybackOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PlayoutId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Seed")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("When")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
|
||||
ErsatzTV.Infrastructure.Sqlite/Migrations/20240124020453_Add_PlayoutHistoryPlaybackOrder.Designer.cs
Generated
+4917
File diff suppressed because it is too large
Load Diff
+29
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_PlayoutHistoryPlaybackOrder : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "PlaybackOrder",
|
||||
table: "PlayoutHistory",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PlaybackOrder",
|
||||
table: "PlayoutHistory");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+4920
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_PlayoutHistorySeed : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Seed",
|
||||
table: "PlayoutHistory",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Seed",
|
||||
table: "PlayoutHistory");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1935,9 +1935,15 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("PlaybackOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PlayoutId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Seed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("When")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Search;
|
||||
|
||||
public class SearchTargets : ISearchTargets
|
||||
{
|
||||
public event EventHandler OnSearchTargetsChanged;
|
||||
|
||||
public void SearchTargetsChanged()
|
||||
{
|
||||
OnSearchTargetsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@@ -208,21 +208,21 @@
|
||||
@switch (_selectedItem.CollectionType)
|
||||
{
|
||||
case ProgramScheduleItemCollectionType.MultiCollection:
|
||||
@* <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> *@
|
||||
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
|
||||
@* <MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem> *@
|
||||
break;
|
||||
case ProgramScheduleItemCollectionType.Collection:
|
||||
case ProgramScheduleItemCollectionType.SmartCollection:
|
||||
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
|
||||
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
|
||||
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
|
||||
@* <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> *@
|
||||
@* <MudSelectItem Value="PlaybackOrder.ShuffleInOrder">Shuffle In Order</MudSelectItem> *@
|
||||
break;
|
||||
case ProgramScheduleItemCollectionType.TelevisionShow:
|
||||
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
|
||||
<MudSelectItem Value="PlaybackOrder.SeasonEpisode">Season, Episode</MudSelectItem>
|
||||
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
|
||||
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
|
||||
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
|
||||
@* <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> *@
|
||||
@* <MudSelectItem Value="PlaybackOrder.MultiEpisodeShuffle">Multi-Episode Shuffle</MudSelectItem> *@
|
||||
break;
|
||||
case ProgramScheduleItemCollectionType.TelevisionSeason:
|
||||
@@ -230,8 +230,8 @@
|
||||
case ProgramScheduleItemCollectionType.FakeCollection:
|
||||
default:
|
||||
<MudSelectItem Value="PlaybackOrder.Chronological">Chronological</MudSelectItem>
|
||||
<MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem>
|
||||
<MudSelectItem Value="PlaybackOrder.Random">Random</MudSelectItem>
|
||||
@* <MudSelectItem Value="PlaybackOrder.Shuffle">Shuffle</MudSelectItem> *@
|
||||
break;
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
@using System.Reflection
|
||||
@using ErsatzTV.Extensions
|
||||
@using ErsatzTV.Application.Search
|
||||
@using ErsatzTV.Core.Interfaces.Search
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IMediator Mediator
|
||||
@inject SystemStartup SystemStartup
|
||||
@inject ISearchTargets SearchTargets;
|
||||
|
||||
<MudThemeProvider Theme="_ersatzTvTheme"/>
|
||||
<MudDialogProvider DisableBackdropClick="true"/>
|
||||
@@ -158,6 +160,8 @@
|
||||
{
|
||||
SystemStartup.OnDatabaseReady += OnStartupProgress;
|
||||
SystemStartup.OnSearchIndexReady += OnStartupProgress;
|
||||
|
||||
SearchTargets.OnSearchTargetsChanged += OnSearchTargetsChanged;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -165,6 +169,8 @@
|
||||
SystemStartup.OnDatabaseReady -= OnStartupProgress;
|
||||
SystemStartup.OnSearchIndexReady -= OnStartupProgress;
|
||||
|
||||
SearchTargets.OnSearchTargetsChanged -= OnSearchTargetsChanged;
|
||||
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
}
|
||||
@@ -231,6 +237,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
protected async void OnSearchTargetsChanged(object sender, EventArgs e)
|
||||
{
|
||||
_searchTargets = await Mediator.Send(new QuerySearchTargets(), _cts.Token);
|
||||
}
|
||||
|
||||
private void PerformSearch()
|
||||
{
|
||||
NavigationManager.NavigateTo(_query.GetRelativeSearchQuery(), true);
|
||||
|
||||
@@ -544,6 +544,7 @@ public class Startup
|
||||
services.AddSingleton<IPlexTvApiClient, PlexTvApiClient>(); // TODO: does this need to be singleton?
|
||||
services.AddSingleton<ITraktApiClient, TraktApiClient>();
|
||||
services.AddSingleton<IEntityLocker, EntityLocker>();
|
||||
services.AddSingleton<ISearchTargets, SearchTargets>();
|
||||
|
||||
if (SearchHelper.IsElasticSearchEnabled)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user