fix duplicate smart collection names (#2720)

* fix duplicate smart collection names

* fix update error
This commit is contained in:
Jason Dove
2025-12-13 14:59:06 -06:00
committed by GitHub
parent 99b8c56a31
commit d8122edad6
14 changed files with 27374 additions and 5 deletions

View File

@@ -56,6 +56,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- NVIDIA: fix stream failure with certain content that should decode in hardware but falls back to software
- Fix stream failure when configured fallback filler collection is empty
- Fix high CPU when errors are displayed; errors will now work ahead before throttling to realtime, similar to primary content
- Fix startup error caused by duplicate smart collection names (and no longer allow duplicate smart collection names)
### Changed
- No longer round framerate to nearest integer when normalizing framerate

View File

@@ -2,5 +2,6 @@
<PropertyGroup>
<InformationalVersion>develop</InformationalVersion>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<AllowMissingPrunePackageData>true</AllowMissingPrunePackageData>
</PropertyGroup>
</Project>
</Project>

View File

@@ -12,7 +12,9 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollection, Either<BaseError, UpdateSmartCollectionResult>>
public class
UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollection,
Either<BaseError, UpdateSmartCollectionResult>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
@@ -71,7 +73,8 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
private static Task<Validation<BaseError, SmartCollection>> Validate(
TvContext dbContext,
UpdateSmartCollection request,
CancellationToken cancellationToken) => SmartCollectionMustExist(dbContext, request, cancellationToken);
CancellationToken cancellationToken) => ValidateName(dbContext, request)
.BindT(_ => SmartCollectionMustExist(dbContext, request, cancellationToken));
private static Task<Validation<BaseError, SmartCollection>> SmartCollectionMustExist(
TvContext dbContext,
@@ -80,4 +83,23 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
dbContext.SmartCollections
.SelectOneAsync(c => c.Id, c => c.Id == updateCollection.Id, cancellationToken)
.Map(o => o.ToValidation<BaseError>("SmartCollection does not exist."));
private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext,
UpdateSmartCollection updateCollection)
{
List<string> allNames = await dbContext.SmartCollections
.Where(c => c.Id != updateCollection.Id)
.Map(c => c.Name)
.ToListAsync();
Validation<BaseError, string> result1 = updateCollection.NotEmpty(c => c.Name)
.Bind(_ => updateCollection.NotLongerThan(50)(c => c.Name));
var result2 = Optional(updateCollection.Name)
.Where(name => !allNames.Contains(name))
.ToValidation<BaseError>("SmartCollection name must be unique");
return (result1, result2).Apply((_, _) => updateCollection.Name);
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Fix_DuplicateSmartCollectionName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"
WITH Numbered AS (
SELECT
Id,
ROW_NUMBER() OVER (PARTITION BY Name ORDER BY Id) as RowNum
FROM SmartCollection
)
UPDATE SmartCollection sc
JOIN Numbered n ON sc.Id = n.Id
SET sc.Name = CONCAT(sc.Name, ' (', n.RowNum - 1, ')')
WHERE n.RowNum > 1;
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_SmartCollectionNameUnique : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "SmartCollection",
type: "varchar(255)",
nullable: true,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_SmartCollection_Name",
table: "SmartCollection",
column: "Name",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_SmartCollection_Name",
table: "SmartCollection");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "SmartCollection",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "varchar(255)",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
}
}

View File

@@ -3235,13 +3235,16 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.HasColumnType("longtext");
.HasColumnType("varchar(255)");
b.Property<string>("Query")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("SmartCollection", (string)null);
});

View File

@@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Fix_DuplicateSmartCollectionName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"
WITH Numbered AS (
SELECT
Id,
ROW_NUMBER() OVER (PARTITION BY Name ORDER BY Id) AS RowNum
FROM SmartCollection
)
UPDATE SmartCollection
SET Name = Name || ' (' || (Numbered.RowNum - 1) || ')'
FROM Numbered
WHERE SmartCollection.Id = Numbered.Id
AND Numbered.RowNum > 1;
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_SmartCollectionNameUnique : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_SmartCollection_Name",
table: "SmartCollection",
column: "Name",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_SmartCollection_Name",
table: "SmartCollection");
}
}
}

View File

@@ -3091,6 +3091,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("SmartCollection", (string)null);
});

View File

@@ -6,5 +6,11 @@ namespace ErsatzTV.Infrastructure.Data.Configurations;
public class SmartCollectionConfiguration : IEntityTypeConfiguration<SmartCollection>
{
public void Configure(EntityTypeBuilder<SmartCollection> builder) => builder.ToTable("SmartCollection");
public void Configure(EntityTypeBuilder<SmartCollection> builder)
{
builder.ToTable("SmartCollection");
builder.HasIndex(sc => sc.Name)
.IsUnique();
}
}