Browse Source

optimize check for orphaned artwork

pull/2760/head
Jason Dove 7 months ago
parent
commit
5529ba8000
No known key found for this signature in database
  1. 2
      CHANGELOG.md
  2. 12
      ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtworkHandler.cs
  3. 1
      ErsatzTV.Core/Domain/Metadata/Artwork.cs
  4. 8
      ErsatzTV.Core/Interfaces/Repositories/IArtworkRepository.cs
  5. 7021
      ErsatzTV.Infrastructure.MySql/Migrations/20260107222304_Add_ArtworkIsMetadataOrphan.Designer.cs
  6. 39
      ErsatzTV.Infrastructure.MySql/Migrations/20260107222304_Add_ArtworkIsMetadataOrphan.cs
  7. 7
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  8. 6848
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260107222223_Add_ArtworkIsMetadataOrphan.Designer.cs
  9. 39
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260107222223_Add_ArtworkIsMetadataOrphan.cs
  10. 7
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  11. 28
      ErsatzTV.Infrastructure/Data/Configurations/Metadata/ArtworkConfiguration.cs
  12. 32
      ErsatzTV.Infrastructure/Data/Repositories/ArtworkRepository.cs
  13. 1
      ErsatzTV.Infrastructure/Data/TvContext.cs
  14. 1
      ErsatzTV/ErsatzTV.csproj
  15. 2
      ErsatzTV/Program.cs

2
CHANGELOG.md

@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Changed
- Optimize database check for orphaned artwork
## [26.1.0] - 2026-01-06
### Added

12
ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtworkHandler.cs

@ -3,16 +3,12 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -3,16 +3,12 @@ using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Maintenance;
public class DeleteOrphanedArtworkHandler : IRequestHandler<DeleteOrphanedArtwork, Either<BaseError, Unit>>
public class DeleteOrphanedArtworkHandler(IArtworkRepository artworkRepository)
: IRequestHandler<DeleteOrphanedArtwork, Either<BaseError, Unit>>
{
private readonly IArtworkRepository _artworkRepository;
public DeleteOrphanedArtworkHandler(IArtworkRepository artworkRepository) =>
_artworkRepository = artworkRepository;
public Task<Either<BaseError, Unit>>
Handle(DeleteOrphanedArtwork request, CancellationToken cancellationToken) =>
_artworkRepository.GetOrphanedArtwork()
.Bind(_artworkRepository.Delete)
artworkRepository.GetOrphanedArtworkIds()
.Bind(artworkRepository.Delete)
.Map(_ => Right<BaseError, Unit>(Unit.Default));
}

1
ErsatzTV.Core/Domain/Metadata/Artwork.cs

@ -12,6 +12,7 @@ public class Artwork @@ -12,6 +12,7 @@ public class Artwork
public ArtworkKind ArtworkKind { get; set; }
public DateTime DateAdded { get; set; }
public DateTime DateUpdated { get; set; }
public bool? IsMetadataOrphan { get; set; }
public bool IsExternalUrl() => IsExternalUrl(Path);

8
ErsatzTV.Core/Interfaces/Repositories/IArtworkRepository.cs

@ -1,9 +1,7 @@ @@ -1,9 +1,7 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Core.Interfaces.Repositories;
public interface IArtworkRepository
{
Task<List<Artwork>> GetOrphanedArtwork();
Task<Unit> Delete(List<Artwork> artwork);
Task<List<int>> GetOrphanedArtworkIds();
Task<Unit> Delete(List<int> artworkIds);
}

7021
ErsatzTV.Infrastructure.MySql/Migrations/20260107222304_Add_ArtworkIsMetadataOrphan.Designer.cs generated

File diff suppressed because it is too large Load Diff

39
ErsatzTV.Infrastructure.MySql/Migrations/20260107222304_Add_ArtworkIsMetadataOrphan.cs

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_ArtworkIsMetadataOrphan : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsMetadataOrphan",
table: "Artwork",
type: "tinyint(1)",
nullable: true,
computedColumnSql: "CASE WHEN COALESCE(ArtistMetadataId, ChannelId, EpisodeMetadataId, MovieMetadataId, MusicVideoMetadataId, OtherVideoMetadataId, SeasonMetadataId, ShowMetadataId, SongMetadataId, ImageMetadataId, RemoteStreamMetadataId) IS NULL THEN 1 ELSE NULL END",
stored: false);
migrationBuilder.CreateIndex(
name: "IX_Artwork_IsMetadataOrphan",
table: "Artwork",
column: "IsMetadataOrphan");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Artwork_IsMetadataOrphan",
table: "Artwork");
migrationBuilder.DropColumn(
name: "IsMetadataOrphan",
table: "Artwork");
}
}
}

7
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -192,6 +192,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -192,6 +192,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int?>("ImageMetadataId")
.HasColumnType("int");
b.Property<bool?>("IsMetadataOrphan")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("tinyint(1)")
.HasComputedColumnSql("CASE WHEN COALESCE(ArtistMetadataId, ChannelId, EpisodeMetadataId, MovieMetadataId, MusicVideoMetadataId, OtherVideoMetadataId, SeasonMetadataId, ShowMetadataId, SongMetadataId, ImageMetadataId, RemoteStreamMetadataId) IS NULL THEN 1 ELSE NULL END", false);
b.Property<int?>("MovieMetadataId")
.HasColumnType("int");
@ -232,6 +237,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -232,6 +237,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasIndex("ImageMetadataId");
b.HasIndex("IsMetadataOrphan");
b.HasIndex("MovieMetadataId");
b.HasIndex("MusicVideoMetadataId");

6848
ErsatzTV.Infrastructure.Sqlite/Migrations/20260107222223_Add_ArtworkIsMetadataOrphan.Designer.cs generated

File diff suppressed because it is too large Load Diff

39
ErsatzTV.Infrastructure.Sqlite/Migrations/20260107222223_Add_ArtworkIsMetadataOrphan.cs

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_ArtworkIsMetadataOrphan : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsMetadataOrphan",
table: "Artwork",
type: "INTEGER",
nullable: true,
computedColumnSql: "CASE WHEN COALESCE(ArtistMetadataId, ChannelId, EpisodeMetadataId, MovieMetadataId, MusicVideoMetadataId, OtherVideoMetadataId, SeasonMetadataId, ShowMetadataId, SongMetadataId, ImageMetadataId, RemoteStreamMetadataId) IS NULL THEN 1 ELSE NULL END",
stored: false);
migrationBuilder.CreateIndex(
name: "IX_Artwork_IsMetadataOrphan",
table: "Artwork",
column: "IsMetadataOrphan");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Artwork_IsMetadataOrphan",
table: "Artwork");
migrationBuilder.DropColumn(
name: "IsMetadataOrphan",
table: "Artwork");
}
}
}

7
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -181,6 +181,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -181,6 +181,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int?>("ImageMetadataId")
.HasColumnType("INTEGER");
b.Property<bool?>("IsMetadataOrphan")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("INTEGER")
.HasComputedColumnSql("CASE WHEN COALESCE(ArtistMetadataId, ChannelId, EpisodeMetadataId, MovieMetadataId, MusicVideoMetadataId, OtherVideoMetadataId, SeasonMetadataId, ShowMetadataId, SongMetadataId, ImageMetadataId, RemoteStreamMetadataId) IS NULL THEN 1 ELSE NULL END", false);
b.Property<int?>("MovieMetadataId")
.HasColumnType("INTEGER");
@ -221,6 +226,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -221,6 +226,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("ImageMetadataId");
b.HasIndex("IsMetadataOrphan");
b.HasIndex("MovieMetadataId");
b.HasIndex("MusicVideoMetadataId");

28
ErsatzTV.Infrastructure/Data/Configurations/Metadata/ArtworkConfiguration.cs

@ -6,5 +6,31 @@ namespace ErsatzTV.Infrastructure.Data.Configurations; @@ -6,5 +6,31 @@ namespace ErsatzTV.Infrastructure.Data.Configurations;
public class ArtworkConfiguration : IEntityTypeConfiguration<Artwork>
{
public void Configure(EntityTypeBuilder<Artwork> builder) => builder.ToTable("Artwork");
public void Configure(EntityTypeBuilder<Artwork> builder)
{
builder.ToTable("Artwork");
string[] fkColumns =
[
"ArtistMetadataId",
"ChannelId",
"EpisodeMetadataId",
"MovieMetadataId",
"MusicVideoMetadataId",
"OtherVideoMetadataId",
"SeasonMetadataId",
"ShowMetadataId",
"SongMetadataId",
"ImageMetadataId",
"RemoteStreamMetadataId"
];
var coalesceList = string.Join(", ", fkColumns);
var computedSql = $"CASE WHEN COALESCE({coalesceList}) IS NULL THEN 1 ELSE NULL END";
builder.Property(a => a.IsMetadataOrphan)
.HasComputedColumnSql(computedSql, stored: false);
builder.HasIndex(a => a.IsMetadataOrphan);
}
}

32
ErsatzTV.Infrastructure/Data/Repositories/ArtworkRepository.cs

@ -1,34 +1,26 @@ @@ -1,34 +1,26 @@
using Dapper;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class ArtworkRepository : IArtworkRepository
public class ArtworkRepository(IDbContextFactory<TvContext> dbContextFactory) : IArtworkRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public ArtworkRepository(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<List<Artwork>> GetOrphanedArtwork()
public async Task<List<int>> GetOrphanedArtworkIds()
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<Artwork>(
@"SELECT A.Id, A.Path FROM Artwork A
WHERE A.ArtistMetadataId IS NULL AND A.EpisodeMetadataId IS NULL
AND A.MovieMetadataId IS NULL AND A.MusicVideoMetadataId IS NULL
AND A.SeasonMetadataId IS NULL AND A.ShowMetadataId IS NULL
AND A.SongMetadataId IS NULL AND A.ChannelId IS NULL
AND A.OtherVideoMetadataId IS NULL AND A.RemoteStreamMetadataId IS NULL
AND NOT EXISTS (SELECT * FROM Actor WHERE Actor.ArtworkId = A.Id)")
.Map(result => result.ToList());
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Artwork
.TagWithCallSite()
.Where(a => a.IsMetadataOrphan == true)
.Where(a => !dbContext.Actors.Any(actor => actor.ArtworkId == a.Id))
.Select(a => a.Id)
.ToListAsync();
}
public async Task<Unit> Delete(List<Artwork> artwork)
public async Task<Unit> Delete(List<int> artworkIds)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
IEnumerable<List<int>> chunks = Chunk(artwork.Map(a => a.Id), 100);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
IEnumerable<List<int>> chunks = Chunk(artworkIds, 100);
foreach (List<int> chunk in chunks)
{
await dbContext.Connection.ExecuteAsync(

1
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -55,6 +55,7 @@ public class TvContext : DbContext @@ -55,6 +55,7 @@ public class TvContext : DbContext
public DbSet<MovieMetadata> MovieMetadata { get; set; }
public DbSet<Artwork> Artwork { get; set; }
public DbSet<Artist> Artists { get; set; }
public DbSet<Actor> Actors { get; set; }
public DbSet<ArtistMetadata> ArtistMetadata { get; set; }
public DbSet<MusicVideo> MusicVideos { get; set; }
public DbSet<MusicVideoMetadata> MusicVideoMetadata { get; set; }

1
ErsatzTV/ErsatzTV.csproj

@ -28,6 +28,7 @@ @@ -28,6 +28,7 @@
</Target>
<ItemGroup>
<!-- <PackageReference Include="EntityFrameworkProfiler.Appender" Version="6.0.6049" /> -->
<PackageReference Include="Blazored.FluentValidation" Version="2.2.0" />
<PackageReference Include="BlazorSortable" Version="5.2.1" />
<PackageReference Include="Bugsnag.AspNet.Core" Version="4.1.0" />

2
ErsatzTV/Program.cs

@ -130,6 +130,8 @@ public class Program @@ -130,6 +130,8 @@ public class Program
IHost host = CreateHostBuilder(args).Build();
//HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
// run environment validation and exit on failure
var validator = host.Services.GetRequiredService<IEnvironmentValidator>();
if (!await validator.Validate())

Loading…
Cancel
Save