Compare commits

..
Author SHA1 Message Date
Jason Dove 40ed4b8b0e update changelog for release v0.6.4-beta [no ci] 2022-07-28 12:23:33 -05:00
Jason DoveandGitHub b43d08ca67 fix repeating schedules (#901) 2022-07-26 13:04:05 -05:00
Jason DoveandGitHub 5e7e386108 fix search result filtering for episodes and other videos (#900) 2022-07-25 20:02:07 -05:00
Jason DoveandGitHub 4176df9940 fix nvidia capabilities for second-gen maxwell (#899) 2022-07-24 12:23:30 -05:00
Jason DoveandGitHub de2ef959fe add 640x480 resolution (#898)
* update dependencies

* add 640x480 resolution
2022-07-24 08:17:27 -05:00
Jason DoveandGitHub b53cfebac1 fix bug with unsupported aac channel layouts (#893)
* fix bug with unsupported aac channel layouts

* update dependencies
2022-07-14 10:52:25 -05:00
Jason DoveandGitHub 6895b9cc6b fix search repo caching bug (#886)
* add failing test

* fix search repo bug

* update dependencies
2022-07-10 15:32:06 -05:00
Jason Dove c60d6e46f1 fix changelog [no ci] 2022-07-04 15:23:26 -05:00
48 changed files with 4966 additions and 119 deletions
+15 -1
View File
@@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [0.6.4-beta] - 2022-07-28
### Fixed
- Fix subtitle stream selection when subtitle language is different than audio language
- Fix bug with unsupported AAC channel layouts
- Fix NVIDIA second-gen maxwell capabilities detection
- Return distinct search results for episodes and other videos that have the same title
- For example, two other videos both named `Trailer` would previously have displayed as one item in search results
- Fix schedules that would begin to repeat the same content in the same order after a couple of days
### Added
- Add `640x480` resolution
## [0.6.3-beta] - 2022-07-04
### Fixed
- Maintain stream continuity when playout is rebuilt for a channel that is actively being streamed
@@ -1264,7 +1276,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.2-beta...HEAD
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.4-beta...HEAD
[0.6.4-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.3-beta...v0.6.4-beta
[0.6.3-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.2-beta...v0.6.3-beta
[0.6.2-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.1-beta...v0.6.2-beta
[0.6.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.0-beta...v0.6.1-beta
[0.6.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.8-beta...v0.6.0-beta
@@ -2,7 +2,7 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@@ -17,11 +17,11 @@ public class MoveLocalLibraryPathHandler : IRequestHandler<MoveLocalLibraryPath,
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILogger<MoveLocalLibraryPathHandler> _logger;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
public MoveLocalLibraryPathHandler(
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IDbContextFactory<TvContext> dbContextFactory,
ILogger<MoveLocalLibraryPathHandler> logger)
@@ -2,7 +2,7 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Interfaces.Trakt;
using ErsatzTV.Infrastructure.Data;
@@ -18,7 +18,7 @@ public class AddTraktListHandler : TraktCommandBase, IRequestHandler<AddTraktLis
public AddTraktListHandler(
ITraktApiClient traktApiClient,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
IDbContextFactory<TvContext> dbContextFactory,
@@ -2,7 +2,7 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Interfaces.Trakt;
using ErsatzTV.Infrastructure.Data;
@@ -17,11 +17,11 @@ public class DeleteTraktListHandler : TraktCommandBase, IRequestHandler<DeleteTr
private readonly IEntityLocker _entityLocker;
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
public DeleteTraktListHandler(
ITraktApiClient traktApiClient,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
IDbContextFactory<TvContext> dbContextFactory,
@@ -2,7 +2,7 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Interfaces.Trakt;
using ErsatzTV.Infrastructure.Data;
@@ -19,7 +19,7 @@ public class MatchTraktListItemsHandler : TraktCommandBase,
public MatchTraktListItemsHandler(
ITraktApiClient traktApiClient,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
IDbContextFactory<TvContext> dbContextFactory,
@@ -1,7 +1,7 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Interfaces.Trakt;
using ErsatzTV.Core.Trakt;
@@ -17,11 +17,11 @@ public abstract class TraktCommandBase
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILogger _logger;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
protected TraktCommandBase(
ITraktApiClient traktApiClient,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
ILogger logger)
@@ -3,6 +3,7 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using Humanizer;
using Microsoft.Extensions.Logging;
@@ -16,11 +17,11 @@ public class RebuildSearchIndexHandler : IRequestHandler<RebuildSearchIndex, Uni
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<RebuildSearchIndexHandler> _logger;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
public RebuildSearchIndexHandler(
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem,
IFallbackMetadataProvider fallbackMetadataProvider,
@@ -31,6 +31,7 @@
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
@@ -585,7 +585,7 @@ public class TranscodingTests
public Task<IHardwareCapabilities> GetHardwareCapabilities(
string ffmpegPath,
HardwareAccelerationMode hardwareAccelerationMode) =>
Task.FromResult<IHardwareCapabilities>(new NvidiaHardwareCapabilities(61));
Task.FromResult<IHardwareCapabilities>(new NvidiaHardwareCapabilities(61, string.Empty));
}
private static string ExecutableName(string baseName) =>
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Tests.Fakes;
@@ -622,7 +623,7 @@ public class MovieFolderScannerTests
new Mock<IMetadataRepository>().Object,
_imageCache.Object,
new Mock<ISearchIndex>().Object,
new Mock<ISearchRepository>().Object,
new Mock<ICachingSearchRepository>().Object,
new Mock<IFallbackMetadataProvider>().Object,
new Mock<ILibraryRepository>().Object,
_mediaItemRepository.Object,
@@ -643,7 +644,7 @@ public class MovieFolderScannerTests
new Mock<IMetadataRepository>().Object,
_imageCache.Object,
new Mock<ISearchIndex>().Object,
new Mock<ISearchRepository>().Object,
new Mock<ICachingSearchRepository>().Object,
new Mock<IFallbackMetadataProvider>().Object,
new Mock<ILibraryRepository>().Object,
_mediaItemRepository.Object,
@@ -2235,7 +2235,7 @@ public class PlayoutBuilderTests
DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2);
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed;
@@ -2244,6 +2244,57 @@ public class PlayoutBuilderTests
result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0);
}
[Test]
public async Task ShuffleFlood_Should_MaintainRandomSeed_MultipleDays()
{
var mediaItems = new List<MediaItem>();
for (int i = 1; i <= 25; i++)
{
mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i)));
}
(PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle);
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5);
DateTimeOffset finish = start + TimeSpan.FromDays(2);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
result.Items.Count.Should().Be(53);
result.ProgramScheduleAnchors.Count.Should().Be(2);
result.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).Should().BeTrue();
PlayoutProgramScheduleAnchor lastCheckpoint = result.ProgramScheduleAnchors
.OrderByDescending(a => a.AnchorDate ?? DateTime.MinValue)
.First();
lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0);
lastCheckpoint.EnumeratorState.Index.Should().Be(3);
// we need to mess up the ordering to trigger the problematic behavior
// this simulates the way the rows are loaded with EF
PlayoutProgramScheduleAnchor oldest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate).Last();
PlayoutProgramScheduleAnchor newest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate).First();
result.ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>
{
oldest,
newest
};
int firstSeedValue = lastCheckpoint.EnumeratorState.Seed;
DateTimeOffset start2 = start.AddHours(1);
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2);
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
PlayoutProgramScheduleAnchor continueAnchor =
result2.ProgramScheduleAnchors.First(x => x.AnchorDate is null);
int secondSeedValue = continueAnchor.EnumeratorState.Seed;
// the continue anchor should have the same seed as the most recent (last) checkpoint from the first run
firstSeedValue.Should().Be(secondSeedValue);
}
[Test]
public async Task FloodContent_Should_FloodWithFixedStartTime_FromAnchor()
{
@@ -0,0 +1,244 @@
using Dapper;
using Destructurama;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt.UnsafeValueAccess;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Serilog;
using Serilog.Events;
using Serilog.Extensions.Logging;
namespace ErsatzTV.Core.Tests.Scheduling;
[TestFixture]
[Explicit]
public class ScheduleIntegrationTests
{
public ScheduleIntegrationTests()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning)
.WriteTo.Console()
.Destructure.UsingAttributes()
.CreateLogger();
}
[Test]
public async Task Test()
{
string dbFileName = Path.GetTempFileName() + ".sqlite3";
IServiceCollection services = new ServiceCollection()
.AddLogging();
var connectionString = $"Data Source={dbFileName};foreign keys=true;";
services.AddDbContext<TvContext>(
options => options.UseSqlite(
connectionString,
o =>
{
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure");
}),
ServiceLifetime.Scoped,
ServiceLifetime.Singleton);
services.AddDbContextFactory<TvContext>(
options => options.UseSqlite(
connectionString,
o =>
{
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure");
}));
SqlMapper.AddTypeHandler(new DateTimeOffsetHandler());
SqlMapper.AddTypeHandler(new GuidHandler());
SqlMapper.AddTypeHandler(new TimeSpanHandler());
services.AddSingleton((Func<IServiceProvider, ILoggerFactory>)(_ => new SerilogLoggerFactory()));
ServiceProvider provider = services.BuildServiceProvider();
IDbContextFactory<TvContext> factory = provider.GetRequiredService<IDbContextFactory<TvContext>>();
ILogger<ScheduleIntegrationTests> logger = provider.GetRequiredService<ILogger<ScheduleIntegrationTests>>();
logger.LogInformation("Database is at {File}", dbFileName);
await using TvContext dbContext = await factory.CreateDbContextAsync(CancellationToken.None);
await dbContext.Database.MigrateAsync(CancellationToken.None);
await DbInitializer.Initialize(dbContext, CancellationToken.None);
var path = new LibraryPath
{
Path = "Test LibraryPath"
};
var library = new LocalLibrary
{
MediaKind = LibraryMediaKind.Movies,
Paths = new List<LibraryPath> { path },
MediaSource = new LocalMediaSource()
};
await dbContext.Libraries.AddAsync(library);
await dbContext.SaveChangesAsync();
var movies = new List<Movie>();
for (var i = 1; i < 25; i++)
{
var movie = new Movie
{
MediaVersions = new List<MediaVersion>
{
new() { Duration = TimeSpan.FromMinutes(55) }
},
MovieMetadata = new List<MovieMetadata>
{
new()
{
Title = $"Movie {i}",
ReleaseDate = new DateTime(2000, 1, 1).AddDays(i)
}
},
LibraryPath = path,
LibraryPathId = path.Id
};
movies.Add(movie);
}
await dbContext.Movies.AddRangeAsync(movies);
await dbContext.SaveChangesAsync();
var collection = new Collection
{
Name = "Test Collection",
MediaItems = movies.Cast<MediaItem>().ToList()
};
await dbContext.Collections.AddAsync(collection);
await dbContext.SaveChangesAsync();
var scheduleItems = new List<ProgramScheduleItem>
{
new ProgramScheduleItemDuration
{
Collection = collection,
CollectionId = collection.Id,
PlayoutDuration = TimeSpan.FromHours(1),
TailMode = TailMode.None, // immediately continue
PlaybackOrder = PlaybackOrder.Shuffle
}
};
int playoutId = await AddTestData(dbContext, scheduleItems);
DateTimeOffset start = new DateTimeOffset(2022, 7, 26, 8, 0, 5, TimeSpan.FromHours(-5));
DateTimeOffset finish = start.AddDays(2);
var builder = new PlayoutBuilder(
new ConfigElementRepository(factory),
new MediaCollectionRepository(new Mock<ISearchIndex>().Object, factory),
new TelevisionRepository(factory),
new ArtistRepository(factory),
provider.GetRequiredService<ILogger<PlayoutBuilder>>());
for (var i = 0; i <= (24 * 4); i++)
{
await using TvContext context = await factory.CreateDbContextAsync();
Option<Playout> maybePlayout = await GetPlayout(context, playoutId);
Playout playout = maybePlayout.ValueUnsafe();
await builder.Build(playout, PlayoutBuildMode.Continue, start.AddHours(i), finish.AddHours(i));
await context.SaveChangesAsync();
}
}
private static async Task<int> AddTestData(TvContext dbContext, List<ProgramScheduleItem> scheduleItems)
{
var ffmpegProfile = new FFmpegProfile
{
Name = "Test FFmpeg Profile"
};
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
await dbContext.SaveChangesAsync();
var channel = new Channel(Guid.Parse("00000000-0000-0000-0000-000000000001"))
{
Name = "Test Channel",
FFmpegProfile = ffmpegProfile,
FFmpegProfileId = ffmpegProfile.Id
};
await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
var schedule = new ProgramSchedule
{
Name = "Test Schedule",
Items = scheduleItems
};
await dbContext.ProgramSchedules.AddAsync(schedule);
await dbContext.SaveChangesAsync();
var playout = new Playout
{
Channel = channel,
ChannelId = channel.Id,
ProgramSchedule = schedule,
ProgramScheduleId = schedule.Id
};
await dbContext.Playouts.AddAsync(playout);
await dbContext.SaveChangesAsync();
return playout.Id;
}
private static async Task<Option<Playout>> GetPlayout(TvContext dbContext, int playoutId)
{
return await dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.MediaItem)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Collection)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MediaItem)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PreRollFiller)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MidRollFiller)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PostRollFiller)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.TailFiller)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.SelectOneAsync(p => p.Id, p => p.Id == playoutId);
}
}
+3 -2
View File
@@ -2,6 +2,7 @@
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using Microsoft.Extensions.Logging;
@@ -14,12 +15,12 @@ public class EmbyCollectionScanner : IEmbyCollectionScanner
private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
private readonly ILogger<EmbyCollectionScanner> _logger;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
public EmbyCollectionScanner(
IEmbyCollectionRepository embyCollectionRepository,
IEmbyApiClient embyApiClient,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
ILogger<EmbyCollectionScanner> logger)
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata;
using MediatR;
@@ -25,7 +26,7 @@ public class EmbyMovieLibraryScanner :
IMediator mediator,
IMediaSourceRepository mediaSourceRepository,
IEmbyMovieRepository embyMovieRepository,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IEmbyPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem,
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata;
using MediatR;
@@ -24,7 +25,7 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
IMediaSourceRepository mediaSourceRepository,
IEmbyTelevisionRepository televisionRepository,
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IEmbyPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem,
@@ -148,14 +148,11 @@ public class FFmpegPlaybackSettingsCalculator
result.AudioBitrate = ffmpegProfile.AudioBitrate;
result.AudioBufferSize = ffmpegProfile.AudioBufferSize;
audioStream.IfSome(
stream =>
{
if (stream.Channels != ffmpegProfile.AudioChannels)
{
result.AudioChannels = ffmpegProfile.AudioChannels;
}
});
foreach (MediaStream _ in audioStream)
{
// this can be optimized out later, depending on the audio codec
result.AudioChannels = ffmpegProfile.AudioChannels;
}
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
result.AudioDuration = outPoint - inPoint;
@@ -0,0 +1,5 @@
namespace ErsatzTV.Core.Interfaces.Repositories.Caching;
public interface ICachingSearchRepository : ISearchRepository
{
}
@@ -1,6 +1,7 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Search;
namespace ErsatzTV.Core.Interfaces.Search;
@@ -9,15 +10,15 @@ public interface ISearchIndex : IDisposable
{
public int Version { get; }
Task<bool> Initialize(ILocalFileSystem localFileSystem, IConfigElementRepository configElementRepository);
Task<Unit> Rebuild(ISearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider);
Task<Unit> Rebuild(ICachingSearchRepository searchRepository, IFallbackMetadataProvider fallbackMetadataProvider);
Task<Unit> RebuildItems(
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
List<int> itemIds);
Task<Unit> UpdateItems(
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
List<MediaItem> items);
@@ -2,6 +2,7 @@
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using Microsoft.Extensions.Logging;
@@ -14,12 +15,12 @@ public class JellyfinCollectionScanner : IJellyfinCollectionScanner
private readonly IJellyfinCollectionRepository _jellyfinCollectionRepository;
private readonly ILogger<JellyfinCollectionScanner> _logger;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
public JellyfinCollectionScanner(
IJellyfinCollectionRepository jellyfinCollectionRepository,
IJellyfinApiClient jellyfinApiClient,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
ILogger<JellyfinCollectionScanner> logger)
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata;
using MediatR;
@@ -24,7 +25,7 @@ public class JellyfinMovieLibraryScanner :
ISearchIndex searchIndex,
IMediator mediator,
IJellyfinMovieRepository jellyfinMovieRepository,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IJellyfinPathReplacementService pathReplacementService,
IMediaSourceRepository mediaSourceRepository,
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata;
using MediatR;
@@ -25,7 +26,7 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
IMediaSourceRepository mediaSourceRepository,
IJellyfinTelevisionRepository televisionRepository,
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IJellyfinPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem,
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain.MediaServer;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using MediatR;
using Microsoft.Extensions.Logging;
@@ -22,7 +23,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
private readonly ILogger _logger;
private readonly IMediator _mediator;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
protected MediaServerMovieLibraryScanner(
ILocalStatisticsProvider localStatisticsProvider,
@@ -30,7 +31,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
ILocalFileSystem localFileSystem,
IMediator mediator,
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
ILogger logger)
{
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Domain.MediaServer;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using MediatR;
using Microsoft.Extensions.Logging;
@@ -25,13 +26,13 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
private readonly ILogger _logger;
private readonly IMediator _mediator;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
protected MediaServerTelevisionLibraryScanner(
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILocalFileSystem localFileSystem,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
ISearchIndex searchIndex,
IFallbackMetadataProvider fallbackMetadataProvider,
IMediator mediator,
+3 -2
View File
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using MediatR;
using Microsoft.Extensions.Logging;
@@ -24,7 +25,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
private readonly IMediator _mediator;
private readonly IMovieRepository _movieRepository;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
public MovieFolderScanner(
ILocalFileSystem localFileSystem,
@@ -35,7 +36,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
IMetadataRepository metadataRepository,
IImageCache imageCache,
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
ILibraryRepository libraryRepository,
IMediaItemRepository mediaItemRepository,
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using MediatR;
using Microsoft.Extensions.Logging;
@@ -24,7 +25,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
private readonly IMediator _mediator;
private readonly IMusicVideoRepository _musicVideoRepository;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
public MusicVideoFolderScanner(
ILocalFileSystem localFileSystem,
@@ -34,7 +35,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
IMetadataRepository metadataRepository,
IImageCache imageCache,
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IArtistRepository artistRepository,
IMusicVideoRepository musicVideoRepository,
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using MediatR;
using Microsoft.Extensions.Logging;
@@ -23,7 +24,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
private readonly IMediator _mediator;
private readonly IOtherVideoRepository _otherVideoRepository;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
public OtherVideoFolderScanner(
ILocalFileSystem localFileSystem,
@@ -34,7 +35,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
IImageCache imageCache,
IMediator mediator,
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IOtherVideoRepository otherVideoRepository,
ILibraryRepository libraryRepository,
+3 -2
View File
@@ -6,6 +6,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using MediatR;
using Microsoft.Extensions.Logging;
@@ -22,7 +23,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
private readonly ILogger<SongFolderScanner> _logger;
private readonly IMediator _mediator;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
private readonly ISongRepository _songRepository;
public SongFolderScanner(
@@ -33,7 +34,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
IImageCache imageCache,
IMediator mediator,
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
ISongRepository songRepository,
ILibraryRepository libraryRepository,
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using MediatR;
using Microsoft.Extensions.Logging;
@@ -23,7 +24,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
private readonly IMediator _mediator;
private readonly IMetadataRepository _metadataRepository;
private readonly ISearchIndex _searchIndex;
private readonly ISearchRepository _searchRepository;
private readonly ICachingSearchRepository _searchRepository;
private readonly ITelevisionRepository _televisionRepository;
public TelevisionFolderScanner(
@@ -36,7 +37,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
IMetadataRepository metadataRepository,
IImageCache imageCache,
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
ILibraryRepository libraryRepository,
IMediaItemRepository mediaItemRepository,
IMediator mediator,
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata;
using MediatR;
@@ -27,7 +28,7 @@ public class PlexMovieLibraryScanner :
IMovieRepository movieRepository,
IMetadataRepository metadataRepository,
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IMediator mediator,
IMediaSourceRepository mediaSourceRepository,
@@ -3,6 +3,7 @@ using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata;
using MediatR;
@@ -27,7 +28,7 @@ public class PlexTelevisionLibraryScanner :
ITelevisionRepository televisionRepository,
IMetadataRepository metadataRepository,
ISearchIndex searchIndex,
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
IMediator mediator,
IMediaSourceRepository mediaSourceRepository,
+4 -6
View File
@@ -626,9 +626,7 @@ public class PlayoutBuilder : IPlayoutBuilder
&& a.MediaItemId == collectionKey.MediaItemId
&& a.AnchorDate is null);
var maybeEnumeratorState = collectionEnumerators.GroupBy(e => e.Key, e => e.Value.State).ToDictionary(
mcs => mcs.Key,
mcs => mcs.Head());
var maybeEnumeratorState = collectionEnumerators.ToDictionary(e => e.Key, e => e.Value.State);
PlayoutProgramScheduleAnchor scheduleAnchor = maybeExisting.Match(
existing =>
@@ -658,10 +656,10 @@ public class PlayoutBuilder : IPlayoutBuilder
result.Add(scheduleAnchor);
}
foreach (PlayoutProgramScheduleAnchor continueAnchor in playout.ProgramScheduleAnchors.Where(
foreach (PlayoutProgramScheduleAnchor checkpointAnchor in playout.ProgramScheduleAnchors.Where(
a => a.AnchorDate is not null))
{
result.Add(continueAnchor);
result.Add(checkpointAnchor);
}
return result;
@@ -675,7 +673,7 @@ public class PlayoutBuilder : IPlayoutBuilder
bool randomStartPoint)
{
Option<PlayoutProgramScheduleAnchor> maybeAnchor = playout.ProgramScheduleAnchors
.OrderByDescending(a => a.AnchorDate is null)
.OrderByDescending(a => a.AnchorDate ?? DateTime.MaxValue)
.FirstOrDefault(
a => a.ProgramScheduleId == playout.ProgramScheduleId
&& a.CollectionType == collectionKey.CollectionType
+71 -1
View File
@@ -86,7 +86,77 @@ public class PipelineGeneratorTests
string command = PrintCommand(videoInputFile, audioInputFile, None, None, result);
command.Should().Be(
"-threads 1 -nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -re -i /tmp/whatever.mkv -map 0:1 -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:a aac -ac 2 -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
"-threads 1 -nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -re -i /tmp/whatever.mkv -map 0:1 -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:a aac -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
}
[Test]
public void Aac_6_Channel_Should_Specify_Audio_Channels()
{
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
new List<VideoStream>
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "24", false) });
var audioInputFile = new AudioInputFile(
"/tmp/whatever.mkv",
new List<AudioStream> { new(1, AudioFormat.Aac, 6) },
new AudioState(
AudioFormat.Aac,
6,
320,
640,
48,
Option<TimeSpan>.None,
false));
var desiredState = new FrameState(
true,
false,
VideoFormat.Hevc,
new PixelFormatYuv420P(),
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
Option<int>.None,
2000,
4000,
90_000,
false);
var ffmpegState = new FFmpegState(
false,
HardwareAccelerationMode.None,
HardwareAccelerationMode.None,
Option<string>.None,
Option<string>.None,
TimeSpan.FromSeconds(1),
Option<TimeSpan>.None,
false,
Option<string>.None,
Option<string>.None,
Option<string>.None,
OutputFormatKind.MpegTs,
Option<string>.None,
Option<string>.None,
0,
Option<int>.None);
var builder = new PipelineBuilder(
new DefaultHardwareCapabilities(),
videoInputFile,
audioInputFile,
None,
None,
"",
"",
_logger);
FFmpegPipeline result = builder.Build(ffmpegState, desiredState);
result.PipelineSteps.Should().HaveCountGreaterThan(0);
result.PipelineSteps.Should().Contain(ps => ps is EncoderLibx265);
string command = PrintCommand(videoInputFile, audioInputFile, None, None, result);
command.Should().Be(
"-threads 1 -nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -re -i /tmp/whatever.mkv -map 0:1 -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:a aac -ac 6 -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
}
[Test]
@@ -9,7 +9,8 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
{
private const string CacheKey = "ffmpeg.hardware.nvidia.architecture";
private const string ArchitectureCacheKey = "ffmpeg.hardware.nvidia.architecture";
private const string ModelCacheKey = "ffmpeg.hardware.nvidia.model";
private readonly ILogger<HardwareCapabilitiesFactory> _logger;
private readonly IMemoryCache _memoryCache;
@@ -31,9 +32,10 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
private async Task<IHardwareCapabilities> GetNvidiaCapabilities(string ffmpegPath)
{
if (_memoryCache.TryGetValue(CacheKey, out int cachedArchitecture))
if (_memoryCache.TryGetValue(ArchitectureCacheKey, out int cachedArchitecture)
&& _memoryCache.TryGetValue(ModelCacheKey, out string cachedModel))
{
return new NvidiaHardwareCapabilities(cachedArchitecture);
return new NvidiaHardwareCapabilities(cachedArchitecture, cachedModel);
}
string[] arguments =
@@ -57,13 +59,20 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
Option<string> maybeLine = Optional(output.Split("\n").FirstOrDefault(x => x.Contains("GPU")));
foreach (string line in maybeLine)
{
const string PATTERN = @"SM\s+(\d\.\d)";
Match match = Regex.Match(line, PATTERN);
const string ARCHITECTURE_PATTERN = @"SM\s+(\d\.\d)";
Match match = Regex.Match(line, ARCHITECTURE_PATTERN);
if (match.Success && int.TryParse(match.Groups[1].Value.Replace(".", string.Empty), out int architecture))
{
_logger.LogInformation("Detected NVIDIA GPU architecture SM {Architecture}", architecture);
_memoryCache.Set(CacheKey, architecture);
return new NvidiaHardwareCapabilities(architecture);
const string MODEL_PATTERN = @"(GTX\s+[0-9a-zA-Z]+[\sTtIi]+)";
Match modelMatch = Regex.Match(line, MODEL_PATTERN);
string model = modelMatch.Success ? modelMatch.Groups[1].Value.Trim() : "unknown";
_logger.LogInformation(
"Detected NVIDIA GPU model {Model} architecture SM {Architecture}",
model,
architecture);
_memoryCache.Set(ArchitectureCacheKey, architecture);
_memoryCache.Set(ModelCacheKey, model);
return new NvidiaHardwareCapabilities(architecture, model);
}
}
@@ -5,8 +5,14 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public class NvidiaHardwareCapabilities : IHardwareCapabilities
{
private readonly int _architecture;
private readonly List<string> _maxwellGm206 = new() { "GTX 750", "GTX 950", "GTX 960", "GTX 965M" };
private readonly string _model;
public NvidiaHardwareCapabilities(int architecture) => _architecture = architecture;
public NvidiaHardwareCapabilities(int architecture, string model)
{
_architecture = architecture;
_model = model;
}
public bool CanDecode(string videoFormat, Option<IPixelFormat> maybePixelFormat)
{
@@ -14,14 +20,14 @@ public class NvidiaHardwareCapabilities : IHardwareCapabilities
return videoFormat switch
{
// second gen maxwell is required to decode hevc
VideoFormat.Hevc => _architecture >= 52,
// some second gen maxwell can decode hevc, otherwise pascal is required
VideoFormat.Hevc => _architecture == 52 && _maxwellGm206.Contains(_model) || _architecture >= 60,
// pascal is required to decode vp9 10-bit
VideoFormat.Vp9 when bitDepth == 10 => _architecture >= 60,
// second gen maxwell is required to decode vp9
VideoFormat.Vp9 => _architecture >= 52,
// some second gen maxwell can decode vp9, otherwise pascal is required
VideoFormat.Vp9 => _architecture == 52 && _maxwellGm206.Contains(_model) || _architecture >= 60,
_ => true
};
@@ -1,10 +1,33 @@
namespace ErsatzTV.FFmpeg.Option;
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg.Option;
public class AudioChannelsOutputOption : OutputOption
{
private readonly int _channels;
private readonly Option<string> _audioFormat;
private readonly int _desiredChannels;
private readonly int _sourceChannels;
public AudioChannelsOutputOption(int channels) => _channels = channels;
public AudioChannelsOutputOption(Option<string> audioFormat, int sourceChannels, int desiredChannels)
{
_audioFormat = audioFormat;
_sourceChannels = sourceChannels;
_desiredChannels = desiredChannels;
}
public override IList<string> OutputOptions => new List<string> { "-ac", _channels.ToString() };
public override IList<string> OutputOptions
{
get
{
if (_sourceChannels != _desiredChannels || _audioFormat == Some(AudioFormat.Aac) && _desiredChannels > 2)
{
return new List<string>
{
"-ac", _desiredChannels.ToString()
};
}
return Array.Empty<string>();
}
}
}
+9 -2
View File
@@ -538,9 +538,16 @@ public class PipelineBuilder
_pipelineSteps.Add(step);
}
foreach (int desiredAudioChannels in audioInputFile.DesiredState.AudioChannels)
foreach (AudioStream audioStream in audioInputFile.AudioStreams.HeadOrNone())
{
_pipelineSteps.Add(new AudioChannelsOutputOption(desiredAudioChannels));
foreach (int desiredAudioChannels in audioInputFile.DesiredState.AudioChannels)
{
_pipelineSteps.Add(
new AudioChannelsOutputOption(
audioInputFile.DesiredState.AudioFormat,
audioStream.Channels,
desiredAudioChannels));
}
}
foreach (int desiredBitrate in audioInputFile.DesiredState.AudioBitrate)
@@ -0,0 +1,33 @@
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data.Repositories.Caching;
using FluentAssertions;
using LanguageExt;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Infrastructure.Tests.Data.Repositories.Caching;
[TestFixture]
public class CachingSearchRepositoryTests
{
[Test]
public async Task GetAllLanguageCodes_Should_Cache_Languages_Separately()
{
var englishMediaCodes = new List<string> { "eng" };
var frenchMediaCodes = new List<string> { "fre" };
var englishResult = new List<string> { "english_result" };
var frenchResult = new List<string> { "french_result" };
var searchRepo = new Mock<ISearchRepository>();
searchRepo.Setup(x => x.GetAllLanguageCodes(englishMediaCodes)).Returns(englishResult.AsTask());
searchRepo.Setup(x => x.GetAllLanguageCodes(frenchMediaCodes)).Returns(frenchResult.AsTask());
var repo = new CachingSearchRepository(searchRepo.Object);
List<string> result1 = await repo.GetAllLanguageCodes(englishMediaCodes);
result1.Should().BeEquivalentTo(englishResult);
List<string> result2 = await repo.GetAllLanguageCodes(frenchMediaCodes);
result2.Should().BeEquivalentTo(frenchResult);
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,43 @@
using System.Collections.Concurrent;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
namespace ErsatzTV.Infrastructure.Data.Repositories.Caching;
public class CachingSearchRepository : ICachingSearchRepository
{
private readonly ConcurrentDictionary<List<string>, List<string>> _cache = new();
private readonly ISearchRepository _searchRepository;
private readonly SemaphoreSlim _slim = new(1, 1);
public CachingSearchRepository(ISearchRepository searchRepository) => _searchRepository = searchRepository;
public Task<Option<MediaItem>> GetItemToIndex(int id) => _searchRepository.GetItemToIndex(id);
public Task<List<string>> GetLanguagesForShow(Show show) => _searchRepository.GetLanguagesForShow(show);
public Task<List<string>> GetLanguagesForSeason(Season season) => _searchRepository.GetLanguagesForSeason(season);
public Task<List<string>> GetLanguagesForArtist(Artist artist) => _searchRepository.GetLanguagesForArtist(artist);
public async Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes)
{
if (!_cache.ContainsKey(mediaCodes))
{
await _slim.WaitAsync();
try
{
_cache.TryAdd(mediaCodes, await _searchRepository.GetAllLanguageCodes(mediaCodes));
}
finally
{
_slim.Release();
}
}
return _cache[mediaCodes];
}
public IAsyncEnumerable<MediaItem> GetAllMediaItems() => _searchRepository.GetAllMediaItems();
}
@@ -9,8 +9,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories;
public class SearchRepository : ISearchRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly SemaphoreSlim _slim = new(1, 1);
private List<string> _allLanguageCodes;
public SearchRepository(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
@@ -164,26 +162,10 @@ public class SearchRepository : ISearchRepository
new { ArtistId = artist.Id }).Map(result => result.ToList());
}
public async Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes)
public virtual async Task<List<string>> GetAllLanguageCodes(List<string> mediaCodes)
{
if (_allLanguageCodes == null)
{
await _slim.WaitAsync();
try
{
if (_allLanguageCodes == null)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
_allLanguageCodes = await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCodes);
}
}
finally
{
_slim.Release();
}
}
return _allLanguageCodes;
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCodes);
}
public IAsyncEnumerable<MediaItem> GetAllMediaItems()
@@ -14,12 +14,12 @@
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.2.32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations;
public partial class Add_Resolution640480 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("INSERT INTO Resolution (Id, Height, Width, Name) VALUES (0, 480, 640, '640x480')");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
@@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.5");
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{
@@ -3,6 +3,7 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Search;
using LanguageExt.UnsafeValueAccess;
@@ -88,7 +89,7 @@ public sealed class SearchIndex : ISearchIndex
_initialized = false;
}
public int Version => 28;
public int Version => 30;
public async Task<bool> Initialize(
ILocalFileSystem localFileSystem,
@@ -118,7 +119,7 @@ public sealed class SearchIndex : ISearchIndex
}
public async Task<Unit> UpdateItems(
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
List<MediaItem> items)
{
@@ -219,7 +220,7 @@ public sealed class SearchIndex : ISearchIndex
}
public async Task<Unit> Rebuild(
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider)
{
_writer.DeleteAll();
@@ -235,7 +236,7 @@ public sealed class SearchIndex : ISearchIndex
}
public async Task<Unit> RebuildItems(
ISearchRepository searchRepository,
ICachingSearchRepository searchRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
List<int> itemIds)
{
@@ -1141,9 +1142,9 @@ public sealed class SearchIndex : ISearchIndex
metadata switch
{
EpisodeMetadata em =>
$"{em.Title}_{em.Year}_{em.Episode.Season.SeasonNumber}_{em.EpisodeNumber}_{em.Episode.State}"
$"{em.Title}_{em.Episode.Season.Show.ShowMetadata.Head().Title}_{em.Year}_{em.Episode.Season.SeasonNumber}_{em.EpisodeNumber}_{em.Episode.State}"
.ToLowerInvariant(),
OtherVideoMetadata ovm => $"{ovm.Title}_{ovm.Year}_{ovm.OtherVideo.State}".ToLowerInvariant(),
OtherVideoMetadata ovm => $"{ovm.OriginalTitle}_{ovm.Year}_{ovm.OtherVideo.State}".ToLowerInvariant(),
SongMetadata sm => $"{sm.Title}_{sm.Year}_{sm.Song.State}".ToLowerInvariant(),
MovieMetadata mm => $"{mm.Title}_{mm.Year}_{mm.Movie.State}".ToLowerInvariant(),
ArtistMetadata am => $"{am.Title}_{am.Year}_{am.Artist.State}".ToLowerInvariant(),
+6
View File
@@ -14,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErsatzTV.FFmpeg", "ErsatzTV
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErsatzTV.FFmpeg.Tests", "ErsatzTV.FFmpeg.Tests\ErsatzTV.FFmpeg.Tests.csproj", "{1C892530-CF92-4F43-8C64-BCEEF958D726}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErsatzTV.Infrastructure.Tests", "ErsatzTV.Infrastructure.Tests\ErsatzTV.Infrastructure.Tests.csproj", "{591FB3F4-4DD8-441B-B7C8-F2A42BF69992}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -48,6 +50,10 @@ Global
{1C892530-CF92-4F43-8C64-BCEEF958D726}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C892530-CF92-4F43-8C64-BCEEF958D726}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C892530-CF92-4F43-8C64-BCEEF958D726}.Release|Any CPU.Build.0 = Release|Any CPU
{591FB3F4-4DD8-441B-B7C8-F2A42BF69992}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{591FB3F4-4DD8-441B-B7C8-F2A42BF69992}.Debug|Any CPU.Build.0 = Debug|Any CPU
{591FB3F4-4DD8-441B-B7C8-F2A42BF69992}.Release|Any CPU.ActiveCfg = Release|Any CPU
{591FB3F4-4DD8-441B-B7C8-F2A42BF69992}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
+7 -7
View File
@@ -57,14 +57,14 @@
<PackageReference Include="Bugsnag.AspNet.Core" Version="3.1.0" />
<PackageReference Include="FluentValidation" Version="11.1.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.1.2" />
<PackageReference Include="HtmlSanitizer" Version="7.1.512" />
<PackageReference Include="HtmlSanitizer" Version="7.1.542" />
<PackageReference Include="LanguageExt.Core" Version="4.2.9" />
<PackageReference Include="Markdig" Version="0.30.2" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6">
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.7" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -72,12 +72,12 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MudBlazor" Version="6.0.11" />
<PackageReference Include="MudBlazor" Version="6.0.12" />
<PackageReference Include="NaturalSort.Extension" Version="3.2.0" />
<PackageReference Include="PPioli.FluentValidation.Blazor" Version="5.0.0" />
<PackageReference Include="PPioli.FluentValidation.Blazor" Version="11.1.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.3.2" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
<PackageReference Include="Serilog.Sinks.SQLite" Version="6.0.1" />
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
+3
View File
@@ -22,6 +22,7 @@ using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Metadata.Nfo;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Interfaces.Search;
@@ -36,6 +37,7 @@ using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.Formatters;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Data.Repositories.Caching;
using ErsatzTV.Infrastructure.Emby;
using ErsatzTV.Infrastructure.GitHub;
using ErsatzTV.Infrastructure.Health;
@@ -352,6 +354,7 @@ public class Startup
services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
services.AddScoped<ITelevisionRepository, TelevisionRepository>();
services.AddScoped<ISearchRepository, SearchRepository>();
services.AddScoped<ICachingSearchRepository, CachingSearchRepository>();
services.AddScoped<IMovieRepository, MovieRepository>();
services.AddScoped<IArtistRepository, ArtistRepository>();
services.AddScoped<IMusicVideoRepository, MusicVideoRepository>();