diff --git a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs index b53ff14ee..38c0be919 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs @@ -88,6 +88,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Option maybeExisting = await dbContext.JellyfinShows + .TagWithCallSite() .Include(m => m.LibraryPath) .ThenInclude(lp => lp.Library) .Include(m => m.ShowMetadata) @@ -128,6 +129,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Option maybeExisting = await dbContext.JellyfinSeasons + .TagWithCallSite() .Include(m => m.LibraryPath) .Include(m => m.SeasonMetadata) .ThenInclude(mm => mm.Artwork) @@ -158,6 +160,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Option maybeExisting = await dbContext.JellyfinEpisodes + .TagWithCallSite() .Include(m => m.LibraryPath) .ThenInclude(lp => lp.Library) .Include(m => m.MediaVersions) @@ -511,6 +514,8 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Option maybeShow = await dbContext.JellyfinShows + .AsNoTracking() + .TagWithCallSite() .Where(s => s.Id == showId) .Where(s => s.LibraryPath.LibraryId == libraryId) .Include(s => s.ShowMetadata) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs index 21129a4fa..26cb1d44a 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs @@ -15,6 +15,7 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I var baseItem = await dbContext.MediaItems .AsNoTracking() + .TagWithCallSite() .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); if (baseItem is null) @@ -27,60 +28,70 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I case Movie: return await dbContext.Movies .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); case Episode: return await dbContext.Episodes .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); case Season: return await dbContext.Seasons .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); case Show: return await dbContext.Shows .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); case MusicVideo: return await dbContext.MusicVideos .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); case Artist: return await dbContext.Artists .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); case OtherVideo: return await dbContext.OtherVideos .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); case Song: return await dbContext.Songs .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); case Image: return await dbContext.Images .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); case RemoteStream: return await dbContext.RemoteStreams .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); @@ -225,6 +236,7 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I ConfiguredCancelableAsyncEnumerable movies = dbContext.Movies .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .AsAsyncEnumerable() @@ -242,6 +254,7 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I ConfiguredCancelableAsyncEnumerable shows = dbContext.Shows .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .AsAsyncEnumerable() @@ -259,6 +272,7 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I ConfiguredCancelableAsyncEnumerable seasons = dbContext.Seasons .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .AsAsyncEnumerable() @@ -276,6 +290,7 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I ConfiguredCancelableAsyncEnumerable episodes = dbContext.Episodes .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .AsAsyncEnumerable() @@ -294,6 +309,7 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I ConfiguredCancelableAsyncEnumerable musicVideos = dbContext.MusicVideos .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .AsAsyncEnumerable() @@ -311,6 +327,7 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I ConfiguredCancelableAsyncEnumerable artists = dbContext.Artists .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .AsAsyncEnumerable() @@ -329,6 +346,7 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I ConfiguredCancelableAsyncEnumerable otherVideos = dbContext.OtherVideos .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .AsAsyncEnumerable() @@ -346,6 +364,7 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I ConfiguredCancelableAsyncEnumerable songs = dbContext.Songs .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .AsAsyncEnumerable() @@ -363,6 +382,7 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I ConfiguredCancelableAsyncEnumerable images = dbContext.Images .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .AsAsyncEnumerable() @@ -381,6 +401,7 @@ public class SearchRepository(IDbContextFactory dbContextFactory) : I ConfiguredCancelableAsyncEnumerable remoteStreams = dbContext.RemoteStreams .AsNoTracking() + .TagWithCallSite() .IncludeForSearch() .AsSplitQuery() .AsAsyncEnumerable() diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index 912ff1810..f1da5ed20 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -10,10 +10,17 @@ namespace ErsatzTV.Infrastructure.Data; public class TvContext : DbContext { private readonly ILoggerFactory _loggerFactory; + private readonly SlowQueryInterceptor _slowQueryInterceptor; - public TvContext(DbContextOptions options, ILoggerFactory loggerFactory) - : base(options) => + public TvContext( + DbContextOptions options, + ILoggerFactory loggerFactory, + SlowQueryInterceptor slowQueryInterceptor) + : base(options) + { _loggerFactory = loggerFactory; + _slowQueryInterceptor = slowQueryInterceptor; + } public static string LastInsertedRowId { get; set; } = "last_insert_rowid()"; public static string CaseInsensitiveCollation { get; set; } = "NOCASE"; @@ -119,8 +126,11 @@ public class TvContext : DbContext public DbSet Subtitles { get; set; } public DbSet GraphicsElements { get; set; } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { optionsBuilder.UseLoggerFactory(_loggerFactory); + optionsBuilder.AddInterceptors(_slowQueryInterceptor); + } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/ErsatzTV/SlowApiHandler.cs b/ErsatzTV.Infrastructure/SlowApiHandler.cs similarity index 93% rename from ErsatzTV/SlowApiHandler.cs rename to ErsatzTV.Infrastructure/SlowApiHandler.cs index 997250b58..7b5a3bc87 100644 --- a/ErsatzTV/SlowApiHandler.cs +++ b/ErsatzTV.Infrastructure/SlowApiHandler.cs @@ -1,6 +1,7 @@ using ErsatzTV.Core; +using Microsoft.Extensions.Logging; -namespace ErsatzTV; +namespace ErsatzTV.Infrastructure; using System.Diagnostics; diff --git a/ErsatzTV/SlowQueryInterceptor.cs b/ErsatzTV.Infrastructure/SlowQueryInterceptor.cs similarity index 63% rename from ErsatzTV/SlowQueryInterceptor.cs rename to ErsatzTV.Infrastructure/SlowQueryInterceptor.cs index 52ce0fb19..54c8b72e3 100644 --- a/ErsatzTV/SlowQueryInterceptor.cs +++ b/ErsatzTV.Infrastructure/SlowQueryInterceptor.cs @@ -1,9 +1,11 @@ using System.Data.Common; +using ErsatzTV.Core; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Logging; -namespace ErsatzTV; +namespace ErsatzTV.Infrastructure; -public class SlowQueryInterceptor(int threshold) : DbCommandInterceptor +public class SlowQueryInterceptor(ILogger logger) : DbCommandInterceptor { public override ValueTask ReaderExecutedAsync( DbCommand command, @@ -11,9 +13,9 @@ public class SlowQueryInterceptor(int threshold) : DbCommandInterceptor DbDataReader result, CancellationToken cancellationToken = default) { - if (eventData.Duration.TotalMilliseconds > threshold) + if (SystemEnvironment.SlowDbMs > 0 && eventData.Duration.TotalMilliseconds > SystemEnvironment.SlowDbMs) { - Serilog.Log.Logger.Debug( + logger.LogDebug( "[SLOW QUERY] ({Milliseconds}ms): {Command}", eventData.Duration.TotalMilliseconds, command.CommandText); diff --git a/ErsatzTV.Scanner/Program.cs b/ErsatzTV.Scanner/Program.cs index 478b83173..d5e09b62e 100644 --- a/ErsatzTV.Scanner/Program.cs +++ b/ErsatzTV.Scanner/Program.cs @@ -19,6 +19,7 @@ using ErsatzTV.Core.Plex; using ErsatzTV.Core.Search; using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Runtime; +using ErsatzTV.Infrastructure; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Emby; @@ -171,6 +172,8 @@ public class Program services.AddHttpClient(); + services.AddHttpClient("RefitCustomClient").AddHttpMessageHandler(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -256,6 +259,9 @@ public class Program services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining()); services.AddMemoryCache(); diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index b14b7f506..1127336a3 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -51,6 +51,7 @@ using ErsatzTV.FFmpeg.Pipeline; using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.Filters; using ErsatzTV.Formatters; +using ErsatzTV.Infrastructure; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Database; @@ -408,12 +409,6 @@ public class Startup var sqliteConnectionString = $"Data Source={FileSystemLayout.DatabasePath};foreign keys=true;"; string mySqlConnectionString = Configuration.GetValue("MySql:ConnectionString"); - SlowQueryInterceptor interceptor = null; - if (SystemEnvironment.SlowDbMs.HasValue) - { - interceptor = new SlowQueryInterceptor(SystemEnvironment.SlowDbMs.Value); - } - services.AddDbContext( options => { @@ -444,11 +439,6 @@ public class Startup } ); } - - if (interceptor != null) - { - options.AddInterceptors(interceptor); - } }, ServiceLifetime.Scoped, ServiceLifetime.Singleton); @@ -478,11 +468,6 @@ public class Startup } ); } - - if (interceptor != null) - { - options.AddInterceptors(interceptor); - } }); if (databaseProvider == Provider.Sqlite.Name) @@ -870,6 +855,7 @@ public class Startup // services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>)); services.AddTransient(); + services.AddTransient(); // run-once/blocking startup services services.AddHostedService();