allow shuffling block items (#1571)

* allow shuffling block items

* fix drop down search results
This commit is contained in:
Jason Dove
2024-01-23 22:42:28 -06:00
committed by GitHub
parent d37ce2d38a
commit 12234c3e21
39 changed files with 15380 additions and 87 deletions
+1
View File
@@ -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);
}
}
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)");
@@ -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");
}
}
}
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);
}
}
+5 -5
View File
@@ -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>
+11
View File
@@ -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);
+1
View File
@@ -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)
{