From 78e75c7879f113d96f684b04dcf32b570f3783e0 Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Mon, 22 Jun 2026 17:10:46 -0500 Subject: [PATCH 01/11] feat(migrations): implement EF Core migrations for Artist Search Engine schema - Replaced `EnsureCreatedAsync` with `MigrateAsync` across the project to enable schema versioning and proper migration tracking. - Added baseline migration matching the existing schema and ensured compatibility with production `.ddb` files. - Introduced new design-time DbContext factory and migration tests to validate schema initialization and updates. - Simplified startup logic by removing hand-rolled DDL methods, consolidating schema creation into migrations. - Updated health checks and CLI tools to align with the migration-based approach. --- ...earch-engine-ef-core-migrations-changes.md | 79 +++++++ src/Melodee.Blazor/Program.cs | 4 +- src/Melodee.Blazor/Services/DoctorService.cs | 10 - src/Melodee.Cli/Command/CommandBase.cs | 2 +- src/Melodee.Cli/Command/DoctorCommand.cs | 2 +- ...tistSearchEngineServiceDbContextFactory.cs | 34 +++ ...nitialArtistSearchEngineSchema.Designer.cs | 220 ++++++++++++++++++ ...2211418_InitialArtistSearchEngineSchema.cs | 131 +++++++++++ ...archEngineServiceDbContextModelSnapshot.cs | 217 +++++++++++++++++ .../ArtistSearchEngineServiceDbContext.cs | 13 +- .../ArtistSearchEngineService.cs | 57 +---- ...rchEngineServiceDbContextMigrationTests.cs | 157 +++++++++++++ 12 files changed, 856 insertions(+), 70 deletions(-) create mode 100644 design/docs/20260622-artist-search-engine-ef-core-migrations-changes.md create mode 100644 src/Melodee.Common/Data/ArtistSearchEngineServiceDbContextFactory.cs create mode 100644 src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.Designer.cs create mode 100644 src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.cs create mode 100644 src/Melodee.Common/Migrations/ArtistSearchEngine/ArtistSearchEngineServiceDbContextModelSnapshot.cs create mode 100644 tests/Melodee.Tests.Common/Data/ArtistSearchEngineServiceDbContextMigrationTests.cs diff --git a/design/docs/20260622-artist-search-engine-ef-core-migrations-changes.md b/design/docs/20260622-artist-search-engine-ef-core-migrations-changes.md new file mode 100644 index 000000000..e0fc00a30 --- /dev/null +++ b/design/docs/20260622-artist-search-engine-ef-core-migrations-changes.md @@ -0,0 +1,79 @@ + +# Release Changes: EF Core Migrations for Artist Search Engine + +**Related Plan**: EF Core Migration Implementation for ArtistSearchEngineServiceDbContext +**Implementation Date**: 2026-06-22 + +## Summary + +Migrated the Artist Search Engine DecentDB database from using `EnsureCreatedAsync()` (which only creates schema on first run) to proper EF Core migrations via `MigrateAsync()`. This enables schema versioning, proper migration tracking, and eliminates hand-rolled idempotent DDL blocks that were maintained manually in `ArtistSearchEngineService.cs`. + +## Changes + +### Added + +- `src/Melodee.Common/Data/ArtistSearchEngineServiceDbContextFactory.cs` - Design-time DbContext factory for `dotnet ef` tooling to generate migrations +- `src/Melodee.Common/Migrations.ArtistSearchEngine/ArtistSearchEngine/20260622141948_InitialArtistSearchEngineSchema.cs` - Baseline migration with raw SQL matching the existing production schema exactly +- `src/Melodee.Common/Migrations/ArtistSearchEngine/20260622141948_InitialArtistSearchEngineSchema.Designer.cs` - EF Core migration metadata/snapshot +- `tests/Melodee.Tests.Common/Data/ArtistSearchEngineServiceDbContextMigrationTests.cs` - 3 comprehensive tests validating migration behavior on fresh files, existing schema, and production schema copy + +### Modified + +- `src/Melodee.Blazor/Program.cs` - Added `MigrationsAssembly("Melodee.Common")` to DecentDB options for ArtistSearchEngineServiceDbContext so EF Core finds the dedicated migration folder +- `src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs` - Replaced `EnsureCreatedAsync()` + 3 hand-rolled DDL blocks (`EnsureHousekeepingIndexesAsync`, `EnsureLocalArtistAliasLookupAsync`, `BackfillLocalArtistAliasLookupAsync`) with single `MigrateAsync()` call; kept `BackfillLocalArtistAliasLookupAsync` as a data-seed step +- `src/Melodee.Blazor/Services/DoctorService.cs` - Simplified `ProbeArtistSearchDatabaseAsync` to just verify connectivity without running DDL +- `src/Melodee.Cli/Command/DoctorCommand.cs` - Changed new database creation from `EnsureCreatedAsync()` to `MigrateAsync()` + +### Removed + +- Hand-rolled `EnsureHousekeepingIndexesAsync` method (index creation now in migration) +- Hand-rolled `EnsureLocalArtistAliasLookupAsync` method (table + index creation now in migration) +- Raw `CREATE INDEX IF NOT EXISTS` and `CREATE TABLE IF NOT EXISTS` blocks from startup path + +## Release Summary + +**Total Files Affected**: 8 + +### Files Created (4) + +- `src/Melodee.Common/Data/ArtistSearchEngineServiceDbContextFactory.cs` - Design-time factory for migration generation +- `src/Melodee.Common/Migrations/ArtistSearchEngine/20260622141948_InitialArtistSearchEngineSchema.cs` - Baseline migration with idempotent raw SQL matching production DecentDB schema +- `src/Melodee.Common/Migrations/ArtistSearchEngine/20260622141948_InitialArtistSearchEngineSchema.Designer.cs` - Migration metadata +- `tests/Melodee.Tests.Common/Data/ArtistSearchEngineServiceDbContextMigrationTests.cs` - Migration tests + +### Files Modified (4) + +- `src/Melodee.Blazor/Program.cs` - MigrationsAssembly configuration +- `src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs` - Switched to MigrateAsync, removed hand-rolled DDL +- `src/Melodee.Blazor/Services/DoctorService.cs` - Simplified health probe +- `src/Melodee.Cli/Command/DoctorCommand.cs` - Switched to MigrateAsync for new database creation + +### Dependencies & Infrastructure + +- **New Dependencies**: None (uses existing `DecentDB.EntityFrameworkCore` and `DecentDB.EntityFrameworkCore.NodaTime`) +- **Updated Dependencies**: None +- **Infrastructure Changes**: Created dedicated migration folder `src/Melodee.Common/Migrations.ArtistSearchEngine/` for isolated migration tracking +- **Configuration Updates**: `MigrationsAssembly("Melodee.Common")` on DbContext options to locate migration history table + +### Deployment Notes + +1. **Production file compatibility**: The baseline migration uses raw SQL that exactly matches the existing production `.ddb` schema (verified via DecentDB CLI dump). Running `MigrateAsync()` against the existing file is a no-op — it inserts the `__EFMigrationsHistory` row and leaves all tables/indexes/data intact. + +2. **New environments**: Fresh `.ddb` files will have the full schema created by the migration's `CREATE TABLE IF NOT EXISTS` / `CREATE INDEX IF NOT EXISTS` statements. + +3. **Future schema changes**: New schema modifications should be done by modifying the entity models, then running `dotnet ef migrations add --project src/Melodee.Common --startup-project src/Melodee.Blazor --context ArtistSearchEngineServiceDbContext --output-dir Migrations.ArtistSearchEngine` to generate proper incremental migrations. + +4. **Test fixture**: The production schema test requires setting `MELODEE_TEST_ARTIST_SEARCH_ENGINE_DDB` environment variable to a DecentDB file matching the production schema (not committed due to size). + +## Local Development Baselining Fix + +**Problem**: Local `.ddb` files created with the old `EnsureCreatedAsync()` path lacked the `__EFMigrationsHistory` table. When `MigrateAsync()` ran, it tried to read the history table (failed), then compared the model snapshot to the database and reported "pending changes" — even though the schema matched. + +**Root cause**: The model snapshot (in `Designer.cs`) had `BOOLEAN` for `IsLocked` while the database had `INT64` (from the old `EnsureCreatedAsync()`). EF Core's model validator flagged this as a schema mismatch. + +**Fix applied**: +1. Added explicit value converter for `Artist.IsLocked` in `ArtistSearchEngineServiceDbContext.OnModelCreating()` mapping `bool?` → `int?` (0/1) stored as `INTEGER` +2. Updated the baseline migration's `Designer.cs` model snapshot to match the database: `IsLocked` now uses `INTEGER` with the value converter (instead of `BOOLEAN`) +3. **No additional migration was created** — the baseline migration's model snapshot was manually corrected to match the physical database schema, so EF Core's model validator sees zero differences + +**Result**: Existing local `.ddb` files with historical data work unchanged. `MigrateAsync()` sees the baseline history row, finds zero pending migrations, and proceeds without errors. \ No newline at end of file diff --git a/src/Melodee.Blazor/Program.cs b/src/Melodee.Blazor/Program.cs index b5f94eb3a..662b9dce4 100644 --- a/src/Melodee.Blazor/Program.cs +++ b/src/Melodee.Blazor/Program.cs @@ -159,7 +159,9 @@ builder.Services.AddDbContextFactory(options => { - options.UseDecentDB(builder.Configuration.GetConnectionString("ArtistSearchEngineConnection") ?? throw new Exception("Invalid Connection String"), x => x.UseNodaTime()); + options.UseDecentDB( + builder.Configuration.GetConnectionString("ArtistSearchEngineConnection") ?? throw new Exception("Invalid Connection String"), + x => x.UseNodaTime().MigrationsAssembly("Melodee.Common")); }); builder.Services.AddDbContextFactory(options => diff --git a/src/Melodee.Blazor/Services/DoctorService.cs b/src/Melodee.Blazor/Services/DoctorService.cs index 60bf5635d..a0a4f83ee 100644 --- a/src/Melodee.Blazor/Services/DoctorService.cs +++ b/src/Melodee.Blazor/Services/DoctorService.cs @@ -647,16 +647,6 @@ private static bool HasNonEmptyFileBackedDatabase(string? connectionString) try { await using var db = await _artistSearchEngineDbContextFactory.CreateDbContextAsync(cancellationToken); - await db.Database.EnsureCreatedAsync(cancellationToken); - if (db.Database.IsRelational()) - { - await db.Database.ExecuteSqlRawAsync( - """ - CREATE INDEX IF NOT EXISTS "IX_Artists_IsLocked_LastRefreshed" - ON "Artists" ("IsLocked", "LastRefreshed") - """, - cancellationToken); - } _ = await db.Artists .AsNoTracking() diff --git a/src/Melodee.Cli/Command/CommandBase.cs b/src/Melodee.Cli/Command/CommandBase.cs index 0744b291d..eb65d681a 100644 --- a/src/Melodee.Cli/Command/CommandBase.cs +++ b/src/Melodee.Cli/Command/CommandBase.cs @@ -95,7 +95,7 @@ protected ServiceProvider CreateServiceProvider() services.AddDbContextFactory(options => { - options.UseDecentDB(configuration.GetConnectionString("ArtistSearchEngineConnection") ?? throw new Exception("Invalid Connection String"), x => x.UseNodaTime()); + options.UseDecentDB(configuration.GetConnectionString("ArtistSearchEngineConnection") ?? throw new Exception("Invalid Connection String"), x => x.UseNodaTime().MigrationsAssembly("Melodee.Common")); options.EnableSensitiveDataLogging(true); }); diff --git a/src/Melodee.Cli/Command/DoctorCommand.cs b/src/Melodee.Cli/Command/DoctorCommand.cs index 04c3b1f21..5e9b91039 100644 --- a/src/Melodee.Cli/Command/DoctorCommand.cs +++ b/src/Melodee.Cli/Command/DoctorCommand.cs @@ -317,7 +317,7 @@ await RunCheckAsync(progress, checks, "Database: ArtistSearchEngine (DecentDB)", { _logger.Information("[{JobName}] Creating new empty artist search engine database at [{Path}]", nameof(DoctorCommand), dataSource); await using var db = await _artistSearchEngineDbContextFactory.CreateDbContextAsync(cancellationToken); - await db.Database.EnsureCreatedAsync(cancellationToken); + await db.Database.MigrateAsync(cancellationToken); fileInfo = DescribeFileDatabasePath(cs); } diff --git a/src/Melodee.Common/Data/ArtistSearchEngineServiceDbContextFactory.cs b/src/Melodee.Common/Data/ArtistSearchEngineServiceDbContextFactory.cs new file mode 100644 index 000000000..595b0eab1 --- /dev/null +++ b/src/Melodee.Common/Data/ArtistSearchEngineServiceDbContextFactory.cs @@ -0,0 +1,34 @@ +using DecentDB.EntityFrameworkCore; +using Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace Melodee.Common.Data; + +public class ArtistSearchEngineServiceDbContextFactory : IDesignTimeDbContextFactory +{ + public ArtistSearchEngineServiceDbContext CreateDbContext(string[] args) + { + var connectionString = Environment.GetEnvironmentVariable("ConnectionStrings__ArtistSearchEngineConnection"); + + if (string.IsNullOrEmpty(connectionString)) + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddJsonFile("appsettings.Development.json", optional: true) + .Build(); + connectionString = configuration.GetConnectionString("ArtistSearchEngineConnection"); + } + + if (string.IsNullOrEmpty(connectionString)) + { + connectionString = "Data Source=./_design-time/artistSearchEngine.ddb"; + } + + var builder = new DbContextOptionsBuilder(); + builder.UseDecentDB(connectionString, o => o.UseNodaTime()); + return new ArtistSearchEngineServiceDbContext(builder.Options); + } +} diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.Designer.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.Designer.cs new file mode 100644 index 000000000..8fc06012e --- /dev/null +++ b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.Designer.cs @@ -0,0 +1,220 @@ +// +using Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Melodee.Common.Migrations.ArtistSearchEngine +{ + [DbContext(typeof(ArtistSearchEngineServiceDbContext))] + [Migration("20260622211418_InitialArtistSearchEngineSchema")] + partial class InitialArtistSearchEngineSchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.9"); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Album", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AlbumType") + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("MusicBrainzId") + .HasColumnType("UUID"); + + b.Property("MusicBrainzReleaseGroupId") + .HasColumnType("UUID"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SortName") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SpotifyId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistId", "NameNormalized", "Year"); + + b.ToTable("Albums"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AlternateNames") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("AmgId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("DiscogsId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("ItunesId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("LastFmId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("LastRefreshed") + .HasColumnType("INTEGER"); + + b.Property("MusicBrainzId") + .HasColumnType("UUID"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SortName") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SpotifyId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("WikiDataId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AmgId") + .IsUnique(); + + b.HasIndex("DiscogsId") + .IsUnique(); + + b.HasIndex("ItunesId") + .IsUnique(); + + b.HasIndex("LastFmId") + .IsUnique(); + + b.HasIndex("MusicBrainzId") + .IsUnique(); + + b.HasIndex("Name"); + + b.HasIndex("NameNormalized"); + + b.HasIndex("SortName"); + + b.HasIndex("SpotifyId") + .IsUnique(); + + b.HasIndex("WikiDataId") + .IsUnique(); + + b.HasIndex("IsLocked", "LastRefreshed"); + + b.ToTable("Artists"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.ArtistAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NameNormalized"); + + b.HasIndex("ArtistId", "NameNormalized") + .IsUnique(); + + b.ToTable("ArtistAliases", (string)null); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Album", b => + { + b.HasOne("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", "Artist") + .WithMany("Albums") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.ArtistAlias", b => + { + b.HasOne("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", "Artist") + .WithMany("Aliases") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", b => + { + b.Navigation("Albums"); + + b.Navigation("Aliases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.cs new file mode 100644 index 000000000..f4c0ef65a --- /dev/null +++ b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.cs @@ -0,0 +1,131 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Melodee.Common.Migrations.ArtistSearchEngine +{ + /// + public partial class InitialArtistSearchEngineSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + CREATE TABLE IF NOT EXISTS "Artists" ( + "Id" INTEGER NOT NULL, + "Name" TEXT NOT NULL, + "NameNormalized" TEXT NOT NULL, + "AlternateNames" TEXT NULL, + "SortName" TEXT NOT NULL, + "ItunesId" TEXT NULL, + "AmgId" TEXT NULL, + "DiscogsId" TEXT NULL, + "WikiDataId" TEXT NULL, + "MusicBrainzId" UUID NULL, + "LastFmId" TEXT NULL, + "SpotifyId" TEXT NULL, + "IsLocked" INTEGER NULL, + "LastRefreshed" INTEGER NULL, + CONSTRAINT "PK_Artists" PRIMARY KEY ("Id") + ) + """); + + migrationBuilder.Sql(""" + CREATE TABLE IF NOT EXISTS "Albums" ( + "Id" INTEGER NOT NULL, + "ArtistId" INTEGER NOT NULL, + "SortName" TEXT NOT NULL, + "AlbumType" INTEGER NOT NULL, + "MusicBrainzId" UUID NULL, + "MusicBrainzReleaseGroupId" UUID NULL, + "SpotifyId" TEXT NULL, + "CoverUrl" TEXT NULL, + "Name" TEXT NOT NULL, + "NameNormalized" TEXT NOT NULL, + "Year" INTEGER NOT NULL, + CONSTRAINT "PK_Albums" PRIMARY KEY ("Id"), + CONSTRAINT "FK_Albums_Artists_ArtistId" FOREIGN KEY ("ArtistId") REFERENCES "Artists" ("Id") ON DELETE CASCADE + ) + """); + + migrationBuilder.Sql(""" + CREATE TABLE IF NOT EXISTS "ArtistAliases" ( + "Id" INTEGER NOT NULL, + "ArtistId" INTEGER NOT NULL, + "NameNormalized" TEXT NOT NULL, + CONSTRAINT "PK_ArtistAliases" PRIMARY KEY ("Id"), + CONSTRAINT "FK_ArtistAliases_Artists_ArtistId" FOREIGN KEY ("ArtistId") REFERENCES "Artists" ("Id") ON DELETE CASCADE + ) + """); + + migrationBuilder.Sql(""" + CREATE INDEX IF NOT EXISTS "IX_Albums_ArtistId_NameNormalized_Year" ON "Albums" ("ArtistId", "NameNormalized", "Year") + """); + + migrationBuilder.Sql(""" + CREATE UNIQUE INDEX IF NOT EXISTS "IX_ArtistAliases_ArtistId_NameNormalized" ON "ArtistAliases" ("ArtistId", "NameNormalized") + """); + + migrationBuilder.Sql(""" + CREATE INDEX IF NOT EXISTS "IX_ArtistAliases_NameNormalized" ON "ArtistAliases" ("NameNormalized") + """); + + migrationBuilder.Sql(""" + CREATE UNIQUE INDEX IF NOT EXISTS "IX_Artists_AmgId" ON "Artists" ("AmgId") + """); + + migrationBuilder.Sql(""" + CREATE UNIQUE INDEX IF NOT EXISTS "IX_Artists_DiscogsId" ON "Artists" ("DiscogsId") + """); + + migrationBuilder.Sql(""" + CREATE INDEX IF NOT EXISTS "IX_Artists_IsLocked_LastRefreshed" ON "Artists" ("IsLocked", "LastRefreshed") + """); + + migrationBuilder.Sql(""" + CREATE UNIQUE INDEX IF NOT EXISTS "IX_Artists_ItunesId" ON "Artists" ("ItunesId") + """); + + migrationBuilder.Sql(""" + CREATE UNIQUE INDEX IF NOT EXISTS "IX_Artists_LastFmId" ON "Artists" ("LastFmId") + """); + + migrationBuilder.Sql(""" + CREATE UNIQUE INDEX IF NOT EXISTS "IX_Artists_MusicBrainzId" ON "Artists" ("MusicBrainzId") + """); + + migrationBuilder.Sql(""" + CREATE INDEX IF NOT EXISTS "IX_Artists_Name" ON "Artists" ("Name") + """); + + migrationBuilder.Sql(""" + CREATE INDEX IF NOT EXISTS "IX_Artists_NameNormalized" ON "Artists" ("NameNormalized") + """); + + migrationBuilder.Sql(""" + CREATE INDEX IF NOT EXISTS "IX_Artists_SortName" ON "Artists" ("SortName") + """); + + migrationBuilder.Sql(""" + CREATE UNIQUE INDEX IF NOT EXISTS "IX_Artists_SpotifyId" ON "Artists" ("SpotifyId") + """); + + migrationBuilder.Sql(""" + CREATE UNIQUE INDEX IF NOT EXISTS "IX_Artists_WikiDataId" ON "Artists" ("WikiDataId") + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Albums"); + + migrationBuilder.DropTable( + name: "ArtistAliases"); + + migrationBuilder.DropTable( + name: "Artists"); + } + } +} diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/ArtistSearchEngineServiceDbContextModelSnapshot.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/ArtistSearchEngineServiceDbContextModelSnapshot.cs new file mode 100644 index 000000000..8d73a6ca0 --- /dev/null +++ b/src/Melodee.Common/Migrations/ArtistSearchEngine/ArtistSearchEngineServiceDbContextModelSnapshot.cs @@ -0,0 +1,217 @@ +// +using Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Melodee.Common.Migrations.ArtistSearchEngine +{ + [DbContext(typeof(ArtistSearchEngineServiceDbContext))] + partial class ArtistSearchEngineServiceDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.9"); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Album", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AlbumType") + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("MusicBrainzId") + .HasColumnType("UUID"); + + b.Property("MusicBrainzReleaseGroupId") + .HasColumnType("UUID"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SortName") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SpotifyId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistId", "NameNormalized", "Year"); + + b.ToTable("Albums"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AlternateNames") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("AmgId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("DiscogsId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("ItunesId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("LastFmId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("LastRefreshed") + .HasColumnType("INTEGER"); + + b.Property("MusicBrainzId") + .HasColumnType("UUID"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SortName") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SpotifyId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("WikiDataId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AmgId") + .IsUnique(); + + b.HasIndex("DiscogsId") + .IsUnique(); + + b.HasIndex("ItunesId") + .IsUnique(); + + b.HasIndex("LastFmId") + .IsUnique(); + + b.HasIndex("MusicBrainzId") + .IsUnique(); + + b.HasIndex("Name"); + + b.HasIndex("NameNormalized"); + + b.HasIndex("SortName"); + + b.HasIndex("SpotifyId") + .IsUnique(); + + b.HasIndex("WikiDataId") + .IsUnique(); + + b.HasIndex("IsLocked", "LastRefreshed"); + + b.ToTable("Artists"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.ArtistAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NameNormalized"); + + b.HasIndex("ArtistId", "NameNormalized") + .IsUnique(); + + b.ToTable("ArtistAliases", (string)null); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Album", b => + { + b.HasOne("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", "Artist") + .WithMany("Albums") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.ArtistAlias", b => + { + b.HasOne("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", "Artist") + .WithMany("Aliases") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", b => + { + b.Navigation("Albums"); + + b.Navigation("Aliases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Melodee.Common/Models/SearchEngines/ArtistSearchEngineServiceData/ArtistSearchEngineServiceDbContext.cs b/src/Melodee.Common/Models/SearchEngines/ArtistSearchEngineServiceData/ArtistSearchEngineServiceDbContext.cs index 0c3d952b4..1a53ef8dd 100644 --- a/src/Melodee.Common/Models/SearchEngines/ArtistSearchEngineServiceData/ArtistSearchEngineServiceDbContext.cs +++ b/src/Melodee.Common/Models/SearchEngines/ArtistSearchEngineServiceData/ArtistSearchEngineServiceDbContext.cs @@ -17,8 +17,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset) - || p.PropertyType == - typeof(DateTimeOffset?)); + || p.PropertyType == + typeof(DateTimeOffset?)); foreach (var property in properties) { modelBuilder @@ -36,5 +36,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(x => x.ArtistId) .OnDelete(DeleteBehavior.Cascade); }); + + modelBuilder.Entity(entity => + { + entity.Property(x => x.IsLocked) + .HasConversion( + v => v.HasValue ? (v.Value ? 1 : 0) : null, + v => v.HasValue ? (v.Value != 0) : null) + .HasColumnType("INTEGER"); + }); } } diff --git a/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs b/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs index 1a3b8aa52..8d29c1643 100644 --- a/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs +++ b/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs @@ -87,66 +87,13 @@ public async Task InitializeAsync(IMelodeeConfiguration? configuration = null, await using (var scopedContext = await artistSearchEngineServiceDbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false)) { - await scopedContext.Database.EnsureCreatedAsync(cancellationToken); - await EnsureHousekeepingIndexesAsync(scopedContext, cancellationToken).ConfigureAwait(false); - await EnsureLocalArtistAliasLookupAsync(scopedContext, cancellationToken).ConfigureAwait(false); + await scopedContext.Database.MigrateAsync(cancellationToken); + await BackfillLocalArtistAliasLookupAsync(scopedContext, cancellationToken).ConfigureAwait(false); } _initialized = true; } - private static async Task EnsureHousekeepingIndexesAsync( - ArtistSearchEngineServiceDbContext context, - CancellationToken cancellationToken) - { - if (!context.Database.IsRelational()) - { - return; - } - - await context.Database.ExecuteSqlRawAsync( - """ - CREATE INDEX IF NOT EXISTS "IX_Artists_IsLocked_LastRefreshed" - ON "Artists" ("IsLocked", "LastRefreshed") - """, - cancellationToken); - } - - private static async Task EnsureLocalArtistAliasLookupAsync( - ArtistSearchEngineServiceDbContext context, - CancellationToken cancellationToken) - { - if (!context.Database.IsRelational()) - { - return; - } - - await context.Database.ExecuteSqlRawAsync( - """ - CREATE TABLE IF NOT EXISTS "ArtistAliases" ( - "Id" INTEGER NOT NULL PRIMARY KEY, - "ArtistId" INTEGER NOT NULL, - "NameNormalized" TEXT NOT NULL, - FOREIGN KEY ("ArtistId") REFERENCES "Artists" ("Id") ON DELETE CASCADE - ) - """, - cancellationToken); - await context.Database.ExecuteSqlRawAsync( - """ - CREATE INDEX IF NOT EXISTS "IX_ArtistAliases_NameNormalized" - ON "ArtistAliases" ("NameNormalized") - """, - cancellationToken); - await context.Database.ExecuteSqlRawAsync( - """ - CREATE UNIQUE INDEX IF NOT EXISTS "IX_ArtistAliases_ArtistId_NameNormalized" - ON "ArtistAliases" ("ArtistId", "NameNormalized") - """, - cancellationToken); - - await BackfillLocalArtistAliasLookupAsync(context, cancellationToken).ConfigureAwait(false); - } - private static async Task BackfillLocalArtistAliasLookupAsync( ArtistSearchEngineServiceDbContext context, CancellationToken cancellationToken) diff --git a/tests/Melodee.Tests.Common/Data/ArtistSearchEngineServiceDbContextMigrationTests.cs b/tests/Melodee.Tests.Common/Data/ArtistSearchEngineServiceDbContextMigrationTests.cs new file mode 100644 index 000000000..a6fcc9478 --- /dev/null +++ b/tests/Melodee.Tests.Common/Data/ArtistSearchEngineServiceDbContextMigrationTests.cs @@ -0,0 +1,157 @@ +using DecentDB.EntityFrameworkCore; +using Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData; +using Microsoft.EntityFrameworkCore; + +namespace Melodee.Tests.Common.Data; + +public class ArtistSearchEngineServiceDbContextMigrationTests +{ + [Fact] + public async Task Migrate_OnFreshFile_CreatesArtistsTable() + { + var tempPath = Path.Combine(Path.GetTempPath(), $"artist_search_engine_migrate_{Guid.NewGuid():N}.ddb"); + try + { + var connectionString = $"Data Source={tempPath}"; + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseDecentDB(connectionString, o => o.UseNodaTime()); + + await using var ctx = new ArtistSearchEngineServiceDbContext(optionsBuilder.Options); + await ctx.Database.MigrateAsync(); + + await using var verify = new ArtistSearchEngineServiceDbContext(optionsBuilder.Options); + var canConnect = await verify.Database.CanConnectAsync(); + Assert.True(canConnect); + + var artistCount = await verify.Artists.CountAsync(); + Assert.Equal(0, artistCount); + + var historyTableExists = await verify + .Database.SqlQueryRaw( + "SELECT COUNT(*) AS Value FROM sqlite_master WHERE type='table' AND name='__EFMigrationsHistory'") + .ToListAsync(); + Assert.True(historyTableExists.Count > 0); + + var appliedMigrations = await verify.Database.GetAppliedMigrationsAsync(); + Assert.Contains(appliedMigrations, m => m.Contains("InitialArtistSearchEngineSchema")); + } + finally + { + try { File.Delete(tempPath); } catch { } + try { File.Delete(tempPath + ".wal"); } catch { } + } + } + + [Fact] + public async Task Migrate_OnExistingSchemaFile_IsNoOp() + { + var tempPath = Path.Combine(Path.GetTempPath(), $"artist_search_engine_baseline_{Guid.NewGuid():N}.ddb"); + try + { + var connectionString = $"Data Source={tempPath}"; + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseDecentDB(connectionString, o => o.UseNodaTime()); + + await using (var seedCtx = new ArtistSearchEngineServiceDbContext(optionsBuilder.Options)) + { + await seedCtx.Database.MigrateAsync(); + seedCtx.Artists.Add(new Artist + { + Name = "Test Artist", + NameNormalized = "TESTARTIST", + SortName = "Test Artist" + }); + await seedCtx.SaveChangesAsync(); + } + + var artistsBefore = 0; + await using (var readCtx = new ArtistSearchEngineServiceDbContext(optionsBuilder.Options)) + { + artistsBefore = await readCtx.Artists.CountAsync(); + } + Assert.Equal(1, artistsBefore); + + await using (var migrateCtx = new ArtistSearchEngineServiceDbContext(optionsBuilder.Options)) + { + await migrateCtx.Database.MigrateAsync(); + } + + await using (var verifyCtx = new ArtistSearchEngineServiceDbContext(optionsBuilder.Options)) + { + var artistsAfter = await verifyCtx.Artists.CountAsync(); + Assert.Equal(artistsBefore, artistsAfter); + } + } + finally + { + try { File.Delete(tempPath); } catch { } + try { File.Delete(tempPath + ".wal"); } catch { } + } + } + + [Fact] + public async Task Migrate_OnProductionSchemaCopy_IsNoOp() + { + var fixturePath = Environment.GetEnvironmentVariable("MELODEE_TEST_ARTIST_SEARCH_ENGINE_DDB"); + if (string.IsNullOrEmpty(fixturePath)) + { + return; + } + + if (!File.Exists(fixturePath)) + { + throw new FileNotFoundException( + $"Set MELODEE_TEST_ARTIST_SEARCH_ENGINE_DDB to a DecentDB file matching the production schema. File not found: {fixturePath}"); + } + + var productionCopy = Path.Combine( + Path.GetTempPath(), + $"artist_search_engine_prodcopy_{Guid.NewGuid():N}.ddb"); + File.Copy(fixturePath, productionCopy, overwrite: true); + + try + { + var connectionString = $"Data Source={productionCopy}"; + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseDecentDB(connectionString, o => o.UseNodaTime()); + + int artistCountBefore; + int albumCountBefore; + await using (var readCtx = new ArtistSearchEngineServiceDbContext(optionsBuilder.Options)) + { + artistCountBefore = await readCtx.Artists.CountAsync(); + albumCountBefore = await readCtx.Albums.CountAsync(); + } + + Assert.True(artistCountBefore > 0, "Fixture must contain at least one artist"); + + await using (var migrateCtx = new ArtistSearchEngineServiceDbContext(optionsBuilder.Options)) + { + var pendingBefore = await migrateCtx.Database.GetPendingMigrationsAsync(); + Assert.NotEmpty(pendingBefore); + Assert.Contains(pendingBefore, m => m.Contains("InitialArtistSearchEngineSchema")); + + await migrateCtx.Database.MigrateAsync(); + } + + await using (var verifyCtx = new ArtistSearchEngineServiceDbContext(optionsBuilder.Options)) + { + var artistCountAfter = await verifyCtx.Artists.CountAsync(); + var albumCountAfter = await verifyCtx.Albums.CountAsync(); + Assert.Equal(artistCountBefore, artistCountAfter); + Assert.Equal(albumCountBefore, albumCountAfter); + + var applied = await verifyCtx.Database.GetAppliedMigrationsAsync(); + Assert.Contains(applied, m => m.Contains("InitialArtistSearchEngineSchema")); + + var pendingAfter = await verifyCtx.Database.GetPendingMigrationsAsync(); + Assert.Empty(pendingAfter); + } + } + finally + { + try { File.Delete(productionCopy); } catch { } + try { File.Delete(productionCopy + ".wal"); } catch { } + } + } +} From e4c84c7ff26caa57d04549bed199e62d8d394fe2 Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Mon, 22 Jun 2026 17:22:17 -0500 Subject: [PATCH 02/11] feat(media): add media artist import/export services and UI support - Implemented `MediaArtistExportService` and `MediaArtistImportService` for JSON-based export and import of artist data. - Added import/export buttons and preview logic to the Admin Media Artists UI. - Extended localization files (e.g., DE, CN, NL, PL, IT) with new translations for import/export-related resources. - Updated UI to support overwriting existing artists and provide detailed import/export summaries. --- .../Components/Pages/Media/Artists.razor | 186 +++++++++++ .../Resources/SharedResources.ar-SA.resx | 32 ++ .../Resources/SharedResources.cs-CZ.resx | 32 ++ .../Resources/SharedResources.de-DE.resx | 32 ++ .../Resources/SharedResources.es-ES.resx | 32 ++ .../Resources/SharedResources.fa-IR.resx | 32 ++ .../Resources/SharedResources.fr-FR.resx | 32 ++ .../Resources/SharedResources.id-ID.resx | 32 ++ .../Resources/SharedResources.it-IT.resx | 32 ++ .../Resources/SharedResources.ja-JP.resx | 32 ++ .../Resources/SharedResources.ko-KR.resx | 32 ++ .../Resources/SharedResources.nl-NL.resx | 32 ++ .../Resources/SharedResources.pl-PL.resx | 32 ++ .../Resources/SharedResources.pt-BR.resx | 32 ++ .../Resources/SharedResources.resx | 32 ++ .../Resources/SharedResources.ru-RU.resx | 32 ++ .../Resources/SharedResources.sv-SE.resx | 32 ++ .../Resources/SharedResources.tr-TR.resx | 32 ++ .../Resources/SharedResources.uk-UA.resx | 32 ++ .../Resources/SharedResources.vi-VN.resx | 32 ++ .../Resources/SharedResources.zh-CN.resx | 32 ++ .../Services/MediaArtistExportService.cs | 208 ++++++++++++ .../Services/MediaArtistImportService.cs | 295 ++++++++++++++++++ 23 files changed, 1329 insertions(+) create mode 100644 src/Melodee.Common/Services/MediaArtistExportService.cs create mode 100644 src/Melodee.Common/Services/MediaArtistImportService.cs diff --git a/src/Melodee.Blazor/Components/Pages/Media/Artists.razor b/src/Melodee.Blazor/Components/Pages/Media/Artists.razor index 08dc832c6..b9ce513e3 100644 --- a/src/Melodee.Blazor/Components/Pages/Media/Artists.razor +++ b/src/Melodee.Blazor/Components/Pages/Media/Artists.razor @@ -1,14 +1,19 @@ @page "/admin/mediaartists" @inherits MelodeeComponentBase @using Melodee.Common.Filtering +@using Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData +@using Melodee.Common.Services @using Melodee.Common.Services.SearchEngines +@using Microsoft.EntityFrameworkCore @using Artist = Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist; @using FilterOperator = Melodee.Common.Filtering.FilterOperator @inject MainLayoutProxyService MainLayoutProxyService @inject ILogger Logger +@inject IJSRuntime JSRuntime @inject IMelodeeConfigurationFactory ConfigurationFactory @inject ArtistSearchEngineService ArtistSearchEngineService +@inject IDbContextFactory ArtistSearchEngineDbContextFactory @inject NotificationService NotificationService @inject DialogService DialogService @inject TooltipService TooltipService @@ -34,6 +39,18 @@ + + @@ -44,6 +61,42 @@ + @if (!string.IsNullOrEmpty(_importMessage)) + { + + + + + + + + } + @if (_importPreview != null) + { + + + + @L("MediaArtists.ImportPreview") + + @L("MediaArtists.ArtistsCount", _importPreview.Artists.Count) + @L("MediaArtists.AlbumsCount", _importPreview.Artists.Sum(x => x.Albums.Count)) + @L("MediaArtists.AliasesCount", _importPreview.Artists.Sum(x => x.Aliases.Count)) + + + + + + + + + + } (json, new System.Text.Json.JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + if (_importPreview != null) + { + if (_importPreview.SchemaVersion != "1.0") + { + _importMessage = $"Schema version mismatch. Expected 1.0, got {_importPreview.SchemaVersion}"; + _importSuccess = false; + _importPreview = null; + } + else + { + _importSuccess = true; + _importMessage = $"Ready to import {_importPreview.Artists.Count} artists"; + } + } + else + { + _importMessage = "Invalid import file"; + _importSuccess = false; + } + } + catch + { + _importMessage = "Invalid import file"; + _importSuccess = false; + _importPreview = null; + } + } + + private async Task ApplyImport() + { + if (_importPreview == null) + { + return; + } + + _isImporting = true; + MainLayoutProxyService.ToggleSpinnerVisible(); + try + { + var importService = new MediaArtistImportService(Logger, ArtistSearchEngineDbContextFactory); + var result = await importService.ImportAsync( + System.Text.Json.JsonSerializer.Serialize(_importPreview), + _overwriteExisting); + + if (result.Success) + { + _importMessage = $"Imported {result.ArtistsImported} artists, updated {result.ArtistsUpdated}, skipped {result.ArtistsSkipped}"; + _importSuccess = true; + _importPreview = null; + _importFile = null; + _overwriteExisting = false; + await _grid.RefreshDataAsync(); + } + else + { + _importMessage = result.ErrorMessage; + _importSuccess = false; + } + } + catch (Exception ex) + { + _importMessage = $"Import failed: {ex.Message}"; + _importSuccess = false; + } + finally + { + MainLayoutProxyService.ToggleSpinnerVisible(); + _isImporting = false; + } + } + } diff --git a/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx b/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx index 07df84594..a980bc97e 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx @@ -6860,4 +6860,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx b/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx index 43a429bfd..97aad1276 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx @@ -6511,4 +6511,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx b/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx index 410d29468..928a16d93 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx @@ -6860,4 +6860,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx b/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx index a3993fc8a..1b0693dff 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx @@ -6860,4 +6860,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx b/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx index 5fc234c39..2389ae959 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx @@ -6511,4 +6511,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx b/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx index e66d27cee..39ce460bb 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx @@ -6860,4 +6860,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx b/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx index 12812e47c..ce455f375 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx @@ -6511,4 +6511,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx b/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx index 7bda1afd0..e5c04046f 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx @@ -6860,4 +6860,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx b/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx index 2e3550ea4..6f7ba2007 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx @@ -6860,4 +6860,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx b/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx index db08d69dd..32b155d60 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx @@ -6511,4 +6511,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx b/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx index e42e71a06..66e019e00 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx @@ -6511,4 +6511,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx b/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx index 368823f6c..bd3c854c5 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx @@ -6511,4 +6511,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx b/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx index a90fae212..e47522f3b 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx @@ -6860,4 +6860,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.resx b/src/Melodee.Blazor/Resources/SharedResources.resx index 41a93f8eb..a67429d3d 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.resx @@ -6897,4 +6897,36 @@ Common fixes: Open DecentDB migration guide + + + Export + + + + Import + + + + Import Preview + + + + Artists: {0} + + + + Albums: {0} + + + + Aliases: {0} + + + + Overwrite existing artists + + + + Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx b/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx index fe4a2c2ab..46a880684 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx @@ -6860,4 +6860,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx b/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx index 26f3f61a3..634cbea35 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx @@ -6511,4 +6511,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx b/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx index a2ef27bbd..cd851da6a 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx @@ -6511,4 +6511,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx b/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx index 5a17f0cbd..49469823a 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx @@ -6511,4 +6511,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx b/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx index f5685ec8a..da0cd4d5a 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx @@ -6511,4 +6511,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx b/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx index 67a9ba8e6..e7a3c2c56 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx @@ -6860,4 +6860,36 @@ Common fixes: [NEEDS TRANSLATION] Open DecentDB migration guide + + + [NEEDS TRANSLATION] Export + + + + [NEEDS TRANSLATION] Import + + + + [NEEDS TRANSLATION] Import Preview + + + + [NEEDS TRANSLATION] Artists: {0} + + + + [NEEDS TRANSLATION] Albums: {0} + + + + [NEEDS TRANSLATION] Aliases: {0} + + + + [NEEDS TRANSLATION] Overwrite existing artists + + + + [NEEDS TRANSLATION] Apply Import + diff --git a/src/Melodee.Common/Services/MediaArtistExportService.cs b/src/Melodee.Common/Services/MediaArtistExportService.cs new file mode 100644 index 000000000..50eddd281 --- /dev/null +++ b/src/Melodee.Common/Services/MediaArtistExportService.cs @@ -0,0 +1,208 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace Melodee.Common.Services; + +public sealed class MediaArtistExportService +{ + private const string SchemaVersion = "1.0"; + private readonly ILogger _logger; + private readonly IDbContextFactory _contextFactory; + + public MediaArtistExportService( + ILogger logger, + IDbContextFactory contextFactory) + { + _logger = logger; + _contextFactory = contextFactory; + } + + public async Task ExportAsync(CancellationToken cancellationToken = default) + { + try + { + await using var db = await _contextFactory.CreateDbContextAsync(cancellationToken); + + var artists = await db.Artists + .AsNoTracking() + .Include(x => x.Albums) + .Include(x => x.Aliases) + .OrderBy(x => x.NameNormalized) + .ToListAsync(cancellationToken); + + var exportedArtists = artists.Select(a => new ExportedMediaArtist + { + Name = a.Name, + NameNormalized = a.NameNormalized, + AlternateNames = a.AlternateNames, + SortName = a.SortName, + ItunesId = a.ItunesId, + AmgId = a.AmgId, + DiscogsId = a.DiscogsId, + WikiDataId = a.WikiDataId, + MusicBrainzId = a.MusicBrainzId?.ToString(), + LastFmId = a.LastFmId, + SpotifyId = a.SpotifyId, + IsLocked = a.IsLocked, + LastRefreshed = a.LastRefreshed?.ToString("O"), + Albums = a.Albums.OrderBy(x => x.NameNormalized).Select(x => new ExportedMediaAlbum + { + SortName = x.SortName, + AlbumType = x.AlbumType, + MusicBrainzId = x.MusicBrainzId?.ToString(), + MusicBrainzReleaseGroupId = x.MusicBrainzReleaseGroupId?.ToString(), + SpotifyId = x.SpotifyId, + CoverUrl = x.CoverUrl, + Name = x.Name, + NameNormalized = x.NameNormalized, + Year = x.Year + }).ToList(), + Aliases = a.Aliases.OrderBy(x => x.NameNormalized).Select(x => new ExportedMediaArtistAlias + { + NameNormalized = x.NameNormalized + }).ToList() + }).ToList(); + + var exportData = new MediaArtistExportData + { + SchemaVersion = SchemaVersion, + ExportedAt = DateTimeOffset.UtcNow.ToString("O"), + Artists = exportedArtists + }; + + var jsonOptions = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + return new MediaArtistExportResult + { + Success = true, + Json = JsonSerializer.Serialize(exportData, jsonOptions), + ArtistsCount = exportedArtists.Count, + AlbumsCount = exportedArtists.Sum(x => x.Albums.Count), + AliasesCount = exportedArtists.Sum(x => x.Aliases.Count) + }; + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to export media artists"); + return new MediaArtistExportResult + { + Success = false, + ErrorMessage = $"Export failed: {ex.Message}" + }; + } + } +} + +public sealed class MediaArtistExportData +{ + [JsonPropertyName("schemaVersion")] + public string SchemaVersion { get; init; } = string.Empty; + + [JsonPropertyName("exportedAt")] + public string ExportedAt { get; init; } = string.Empty; + + [JsonPropertyName("artists")] + public List Artists { get; init; } = []; +} + +public sealed class ExportedMediaArtist +{ + [JsonPropertyName("name")] + public string Name { get; init; } = string.Empty; + + [JsonPropertyName("nameNormalized")] + public string NameNormalized { get; init; } = string.Empty; + + [JsonPropertyName("alternateNames")] + public string? AlternateNames { get; init; } + + [JsonPropertyName("sortName")] + public string SortName { get; init; } = string.Empty; + + [JsonPropertyName("itunesId")] + public string? ItunesId { get; init; } + + [JsonPropertyName("amgId")] + public string? AmgId { get; init; } + + [JsonPropertyName("discogsId")] + public string? DiscogsId { get; init; } + + [JsonPropertyName("wikiDataId")] + public string? WikiDataId { get; init; } + + [JsonPropertyName("musicBrainzId")] + public string? MusicBrainzId { get; init; } + + [JsonPropertyName("lastFmId")] + public string? LastFmId { get; init; } + + [JsonPropertyName("spotifyId")] + public string? SpotifyId { get; init; } + + [JsonPropertyName("isLocked")] + public bool? IsLocked { get; init; } + + [JsonPropertyName("lastRefreshed")] + public string? LastRefreshed { get; init; } + + [JsonPropertyName("albums")] + public List Albums { get; init; } = []; + + [JsonPropertyName("aliases")] + public List Aliases { get; init; } = []; +} + +public sealed class ExportedMediaAlbum +{ + [JsonPropertyName("sortName")] + public string SortName { get; init; } = string.Empty; + + [JsonPropertyName("albumType")] + public int AlbumType { get; init; } + + [JsonPropertyName("musicBrainzId")] + public string? MusicBrainzId { get; init; } + + [JsonPropertyName("musicBrainzReleaseGroupId")] + public string? MusicBrainzReleaseGroupId { get; init; } + + [JsonPropertyName("spotifyId")] + public string? SpotifyId { get; init; } + + [JsonPropertyName("coverUrl")] + public string? CoverUrl { get; init; } + + [JsonPropertyName("name")] + public string Name { get; init; } = string.Empty; + + [JsonPropertyName("nameNormalized")] + public string NameNormalized { get; init; } = string.Empty; + + [JsonPropertyName("year")] + public int Year { get; init; } +} + +public sealed class ExportedMediaArtistAlias +{ + [JsonPropertyName("nameNormalized")] + public string NameNormalized { get; init; } = string.Empty; +} + +public sealed class MediaArtistExportResult +{ + public bool Success { get; init; } + public string? ErrorMessage { get; init; } + public string? Json { get; init; } + public int ArtistsCount { get; init; } + public int AlbumsCount { get; init; } + public int AliasesCount { get; init; } +} \ No newline at end of file diff --git a/src/Melodee.Common/Services/MediaArtistImportService.cs b/src/Melodee.Common/Services/MediaArtistImportService.cs new file mode 100644 index 000000000..ea0adab74 --- /dev/null +++ b/src/Melodee.Common/Services/MediaArtistImportService.cs @@ -0,0 +1,295 @@ +using System.Text.Json; +using Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData; +using Microsoft.EntityFrameworkCore; +using Serilog; + +namespace Melodee.Common.Services; + +public sealed class MediaArtistImportService +{ + private const string ExpectedSchemaVersion = "1.0"; + private readonly ILogger _logger; + private readonly IDbContextFactory _contextFactory; + + public MediaArtistImportService( + ILogger logger, + IDbContextFactory contextFactory) + { + _logger = logger; + _contextFactory = contextFactory; + } + + public async Task ImportAsync( + string jsonContent, + bool overwriteExisting = false, + CancellationToken cancellationToken = default) + { + var result = new MediaArtistImportResult(); + + try + { + var jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + var importData = JsonSerializer.Deserialize(jsonContent, jsonOptions); + if (importData == null) + { + return result.WithError("Invalid or empty import file"); + } + + if (importData.SchemaVersion != ExpectedSchemaVersion) + { + return result.WithError($"Schema version mismatch. Expected {ExpectedSchemaVersion}, got {importData.SchemaVersion}"); + } + + await using var db = await _contextFactory.CreateDbContextAsync(cancellationToken); + + foreach (var importedArtist in importData.Artists) + { + cancellationToken.ThrowIfCancellationRequested(); + + await ImportArtistAsync(db, importedArtist, overwriteExisting, result, cancellationToken); + } + + await db.SaveChangesAsync(cancellationToken); + + result.Success = true; + return result; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to import media artists"); + return result.WithError($"Import failed: {ex.Message}"); + } + } + + private static async Task ImportArtistAsync( + ArtistSearchEngineServiceDbContext db, + ExportedMediaArtist importedArtist, + bool overwriteExisting, + MediaArtistImportResult result, + CancellationToken cancellationToken) + { + Artist? existingArtist = null; + + if (!string.IsNullOrEmpty(importedArtist.MusicBrainzId) && + Guid.TryParse(importedArtist.MusicBrainzId, out var mbId)) + { + existingArtist = await db.Artists + .Include(x => x.Albums) + .Include(x => x.Aliases) + .FirstOrDefaultAsync(x => x.MusicBrainzId == mbId, cancellationToken); + } + + if (existingArtist == null) + { + existingArtist = await db.Artists + .Include(x => x.Albums) + .Include(x => x.Aliases) + .FirstOrDefaultAsync(x => x.NameNormalized == importedArtist.NameNormalized, cancellationToken); + } + + if (existingArtist != null) + { + if (!overwriteExisting) + { + result.ArtistsSkipped++; + return; + } + + UpdateExistingArtist(existingArtist, importedArtist); + + ImportAlbumsIntoExisting(existingArtist, importedArtist, overwriteExisting, result); + ImportAliasesIntoExisting(existingArtist, importedArtist, overwriteExisting, result); + + result.ArtistsUpdated++; + } + else + { + var newArtist = CreateNewArtist(importedArtist); + db.Artists.Add(newArtist); + await db.SaveChangesAsync(cancellationToken); + + foreach (var album in importedArtist.Albums) + { + db.Albums.Add(new Album + { + Artist = newArtist, + ArtistId = newArtist.Id, + SortName = album.SortName, + AlbumType = album.AlbumType, + MusicBrainzId = TryParseGuid(album.MusicBrainzId), + MusicBrainzReleaseGroupId = TryParseGuid(album.MusicBrainzReleaseGroupId), + SpotifyId = album.SpotifyId, + CoverUrl = album.CoverUrl, + Name = album.Name, + NameNormalized = album.NameNormalized, + Year = album.Year + }); + } + + foreach (var alias in importedArtist.Aliases) + { + db.ArtistAliases.Add(new ArtistAlias + { + ArtistId = newArtist.Id, + NameNormalized = alias.NameNormalized + }); + } + + result.ArtistsImported++; + result.AlbumsImported += importedArtist.Albums.Count; + result.AliasesImported += importedArtist.Aliases.Count; + } + } + + private static void UpdateExistingArtist(Artist existing, ExportedMediaArtist imported) + { + existing.Name = imported.Name; + existing.NameNormalized = imported.NameNormalized; + existing.AlternateNames = imported.AlternateNames; + existing.SortName = imported.SortName; + existing.ItunesId = imported.ItunesId; + existing.AmgId = imported.AmgId; + existing.DiscogsId = imported.DiscogsId; + existing.WikiDataId = imported.WikiDataId; + existing.MusicBrainzId = TryParseGuid(imported.MusicBrainzId); + existing.LastFmId = imported.LastFmId; + existing.SpotifyId = imported.SpotifyId; + existing.IsLocked = imported.IsLocked; + + if (DateTimeOffset.TryParse(imported.LastRefreshed, out var lastRefreshed)) + { + existing.LastRefreshed = lastRefreshed; + } + } + + private static Artist CreateNewArtist(ExportedMediaArtist imported) + { + var artist = new Artist + { + Name = imported.Name, + NameNormalized = imported.NameNormalized, + AlternateNames = imported.AlternateNames, + SortName = imported.SortName, + ItunesId = imported.ItunesId, + AmgId = imported.AmgId, + DiscogsId = imported.DiscogsId, + WikiDataId = imported.WikiDataId, + MusicBrainzId = TryParseGuid(imported.MusicBrainzId), + LastFmId = imported.LastFmId, + SpotifyId = imported.SpotifyId, + IsLocked = imported.IsLocked + }; + + if (DateTimeOffset.TryParse(imported.LastRefreshed, out var lastRefreshed)) + { + artist.LastRefreshed = lastRefreshed; + } + + return artist; + } + + private static void ImportAlbumsIntoExisting( + Artist existingArtist, + ExportedMediaArtist importedArtist, + bool overwriteExisting, + MediaArtistImportResult result) + { + foreach (var importedAlbum in importedArtist.Albums) + { + var existingAlbum = existingArtist.Albums + .FirstOrDefault(x => x.NameNormalized == importedAlbum.NameNormalized + && x.Year == importedAlbum.Year); + + if (existingAlbum != null) + { + if (!overwriteExisting) + { + continue; + } + + existingAlbum.MusicBrainzId = TryParseGuid(importedAlbum.MusicBrainzId); + existingAlbum.MusicBrainzReleaseGroupId = TryParseGuid(importedAlbum.MusicBrainzReleaseGroupId); + existingAlbum.SpotifyId = importedAlbum.SpotifyId; + result.AlbumsUpdated++; + } + else + { + existingArtist.Albums.Add(new Album + { + Artist = existingArtist, + ArtistId = existingArtist.Id, + SortName = importedAlbum.SortName, + AlbumType = importedAlbum.AlbumType, + MusicBrainzId = TryParseGuid(importedAlbum.MusicBrainzId), + MusicBrainzReleaseGroupId = TryParseGuid(importedAlbum.MusicBrainzReleaseGroupId), + SpotifyId = importedAlbum.SpotifyId, + CoverUrl = importedAlbum.CoverUrl, + Name = importedAlbum.Name, + NameNormalized = importedAlbum.NameNormalized, + Year = importedAlbum.Year + }); + result.AlbumsImported++; + } + } + } + + private static void ImportAliasesIntoExisting( + Artist existingArtist, + ExportedMediaArtist importedArtist, + bool overwriteExisting, + MediaArtistImportResult result) + { + foreach (var importedAlias in importedArtist.Aliases) + { + var existingAlias = existingArtist.Aliases + .FirstOrDefault(x => x.NameNormalized == importedAlias.NameNormalized); + + if (existingAlias == null) + { + existingArtist.Aliases.Add(new ArtistAlias + { + ArtistId = existingArtist.Id, + NameNormalized = importedAlias.NameNormalized + }); + result.AliasesImported++; + } + } + } + + private static Guid? TryParseGuid(string? value) + { + if (string.IsNullOrEmpty(value)) + { + return null; + } + + return Guid.TryParse(value, out var guid) ? guid : null; + } +} + +public sealed class MediaArtistImportResult +{ + public bool Success { get; set; } + public string? ErrorMessage { get; set; } + public int ArtistsImported { get; set; } + public int ArtistsUpdated { get; set; } + public int ArtistsSkipped { get; set; } + public int AlbumsImported { get; set; } + public int AlbumsUpdated { get; set; } + public int AliasesImported { get; set; } + + public MediaArtistImportResult WithError(string error) + { + Success = false; + ErrorMessage = error; + return this; + } +} \ No newline at end of file From d7dde89243069e06b260f67ffda34ba27ceb6d94 Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Mon, 22 Jun 2026 17:44:23 -0500 Subject: [PATCH 03/11] refactor(migrations): correct Artist Search Engine schema and sync UUID column types - Fixed discrepancies in `MusicBrainzId` and `MusicBrainzReleaseGroupId` column types by syncing them to `UUID`. - Refactored initial migrations for consistency and compatibility with schema updates. --- docs/pages/changelog.md | 24 ++ src/Melodee.Blazor/Services/DoctorService.cs | 24 ++ ...itialArtistSearchEngineSchema.Designer.cs} | 2 +- ...223824_InitialArtistSearchEngineSchema.cs} | 13 +- ...847_SyncMusicBrainzUuidColumns.Designer.cs | 220 ++++++++++++++++++ ...260622223847_SyncMusicBrainzUuidColumns.cs | 72 ++++++ 6 files changed, 345 insertions(+), 10 deletions(-) rename src/Melodee.Common/Migrations/ArtistSearchEngine/{20260622211418_InitialArtistSearchEngineSchema.Designer.cs => 20260622223824_InitialArtistSearchEngineSchema.Designer.cs} (99%) rename src/Melodee.Common/Migrations/ArtistSearchEngine/{20260622211418_InitialArtistSearchEngineSchema.cs => 20260622223824_InitialArtistSearchEngineSchema.cs} (95%) create mode 100644 src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.Designer.cs create mode 100644 src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.cs diff --git a/docs/pages/changelog.md b/docs/pages/changelog.md index bf90be8a8..7654bbc9c 100644 --- a/docs/pages/changelog.md +++ b/docs/pages/changelog.md @@ -23,6 +23,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Media Artist export/import: export all search-engine artist data (artists, albums, + aliases) as JSON via the `/admin/mediaartists` page; import JSON exports with optional + overwrite and full preview of artist/album/alias counts. +- ArtistSearch database auto-creation in Blazor Doctor: when the `.ddb` file is missing + but the parent directory exists, the health check now creates it via EF Core migrations + instead of reporting an error. + +### Changed + +- Replaced the hand-rolled raw-SQL ArtistSearch initial migration with a proper + model-driven EF Core migration that uses `CREATE TABLE IF NOT EXISTS` for idempotent + application on both fresh and existing databases. + +### Fixed + +- Resolved EF Core "pending model changes" error for `ArtistSearchEngineServiceDbContext` + by regenerating the model snapshot to match the `IsLocked` `INTEGER` column type + configured in the DbContext. +- ArtistSearch database is now auto-created on first use when the `.ddb` file is absent, + eliminating the "database is empty or not initialized" error that previously required + manual intervention. + ## [2.1.4] - 2026-06-16 ### Added diff --git a/src/Melodee.Blazor/Services/DoctorService.cs b/src/Melodee.Blazor/Services/DoctorService.cs index a0a4f83ee..1821a8984 100644 --- a/src/Melodee.Blazor/Services/DoctorService.cs +++ b/src/Melodee.Blazor/Services/DoctorService.cs @@ -298,6 +298,17 @@ private async Task RunArtistSearchEngineDbCheckAsync(Cancella { var connectionString = configuration.GetConnectionString("ArtistSearchEngineConnection") ?? ""; var fileInfo = DescribeFileDatabasePath(connectionString); + if (!HasNonEmptyFileBackedDatabase(connectionString)) + { + var dataSource = GetDataSourceFromConnectionString(connectionString); + if (!string.IsNullOrWhiteSpace(dataSource) && Directory.Exists(Path.GetDirectoryName(dataSource))) + { + await using var db = await _artistSearchEngineDbContextFactory.CreateDbContextAsync(cancellationToken); + await db.Database.MigrateAsync(cancellationToken); + fileInfo = DescribeFileDatabasePath(connectionString); + } + } + if (!HasNonEmptyFileBackedDatabase(connectionString)) { return new DoctorCheckResult( @@ -622,6 +633,19 @@ private async Task HasArtistSearchConnectionIssuesAsync(CancellationToken return !canQuery; } + private static string? GetDataSourceFromConnectionString(string connectionString) + { + try + { + var builder = new DbConnectionStringBuilder { ConnectionString = connectionString }; + return builder.ContainsKey("Data Source") ? builder["Data Source"]?.ToString() : null; + } + catch + { + return null; + } + } + private static bool HasNonEmptyFileBackedDatabase(string? connectionString) { if (string.IsNullOrWhiteSpace(connectionString)) diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.Designer.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.Designer.cs similarity index 99% rename from src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.Designer.cs rename to src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.Designer.cs index 8fc06012e..22678408b 100644 --- a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.Designer.cs +++ b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.Designer.cs @@ -10,7 +10,7 @@ namespace Melodee.Common.Migrations.ArtistSearchEngine { [DbContext(typeof(ArtistSearchEngineServiceDbContext))] - [Migration("20260622211418_InitialArtistSearchEngineSchema")] + [Migration("20260622223824_InitialArtistSearchEngineSchema")] partial class InitialArtistSearchEngineSchema { /// diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.cs similarity index 95% rename from src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.cs rename to src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.cs index f4c0ef65a..0996e8736 100644 --- a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622211418_InitialArtistSearchEngineSchema.cs +++ b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.cs @@ -118,14 +118,9 @@ protected override void Up(MigrationBuilder migrationBuilder) /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "Albums"); - - migrationBuilder.DropTable( - name: "ArtistAliases"); - - migrationBuilder.DropTable( - name: "Artists"); + migrationBuilder.Sql("DROP TABLE IF EXISTS \"Albums\""); + migrationBuilder.Sql("DROP TABLE IF EXISTS \"ArtistAliases\""); + migrationBuilder.Sql("DROP TABLE IF EXISTS \"Artists\""); } } -} +} \ No newline at end of file diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.Designer.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.Designer.cs new file mode 100644 index 000000000..47e57f930 --- /dev/null +++ b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.Designer.cs @@ -0,0 +1,220 @@ +// +using Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Melodee.Common.Migrations.ArtistSearchEngine +{ + [DbContext(typeof(ArtistSearchEngineServiceDbContext))] + [Migration("20260622223847_SyncMusicBrainzUuidColumns")] + partial class SyncMusicBrainzUuidColumns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.9"); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Album", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AlbumType") + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("MusicBrainzId") + .HasColumnType("UUID"); + + b.Property("MusicBrainzReleaseGroupId") + .HasColumnType("UUID"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SortName") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SpotifyId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistId", "NameNormalized", "Year"); + + b.ToTable("Albums"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AlternateNames") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("AmgId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("DiscogsId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("ItunesId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("LastFmId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("LastRefreshed") + .HasColumnType("INTEGER"); + + b.Property("MusicBrainzId") + .HasColumnType("UUID"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SortName") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SpotifyId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("WikiDataId") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AmgId") + .IsUnique(); + + b.HasIndex("DiscogsId") + .IsUnique(); + + b.HasIndex("ItunesId") + .IsUnique(); + + b.HasIndex("LastFmId") + .IsUnique(); + + b.HasIndex("MusicBrainzId") + .IsUnique(); + + b.HasIndex("Name"); + + b.HasIndex("NameNormalized"); + + b.HasIndex("SortName"); + + b.HasIndex("SpotifyId") + .IsUnique(); + + b.HasIndex("WikiDataId") + .IsUnique(); + + b.HasIndex("IsLocked", "LastRefreshed"); + + b.ToTable("Artists"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.ArtistAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("NameNormalized") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NameNormalized"); + + b.HasIndex("ArtistId", "NameNormalized") + .IsUnique(); + + b.ToTable("ArtistAliases", (string)null); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Album", b => + { + b.HasOne("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", "Artist") + .WithMany("Albums") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.ArtistAlias", b => + { + b.HasOne("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", "Artist") + .WithMany("Aliases") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData.Artist", b => + { + b.Navigation("Albums"); + + b.Navigation("Aliases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.cs new file mode 100644 index 000000000..4967d693e --- /dev/null +++ b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Melodee.Common.Migrations.ArtistSearchEngine +{ + /// + public partial class SyncMusicBrainzUuidColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "MusicBrainzId", + table: "Artists", + type: "UUID", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "BLOB", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MusicBrainzReleaseGroupId", + table: "Albums", + type: "UUID", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "BLOB", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MusicBrainzId", + table: "Albums", + type: "UUID", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "BLOB", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "MusicBrainzId", + table: "Artists", + type: "BLOB", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "UUID", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MusicBrainzReleaseGroupId", + table: "Albums", + type: "BLOB", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "UUID", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MusicBrainzId", + table: "Albums", + type: "BLOB", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "UUID", + oldNullable: true); + } + } +} From b2ae646554b3b46876280b93fee9c3eb03b1634f Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Tue, 23 Jun 2026 07:03:07 -0500 Subject: [PATCH 04/11] refactor(migrations): consolidate Artist Search Engine schema changes and sync column types - Replaced prior initial schema migrations with a single unified version. - Corrected `MusicBrainzId` and `MusicBrainzReleaseGroupId` column type discrepancies by reverting to `BLOB`. - Improved consistency across initial migration scripts for better manageability. --- docs/pages/changelog.md | 7 ++ ...260622223847_SyncMusicBrainzUuidColumns.cs | 72 ------------------- ...itialArtistSearchEngineSchema.Designer.cs} | 2 +- ...224939_InitialArtistSearchEngineSchema.cs} | 6 +- ...02_SyncMusicBrainzUuidColumns.Designer.cs} | 2 +- ...260622225002_SyncMusicBrainzUuidColumns.cs | 27 +++++++ 6 files changed, 39 insertions(+), 77 deletions(-) delete mode 100644 src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.cs rename src/Melodee.Common/Migrations/ArtistSearchEngine/{20260622223824_InitialArtistSearchEngineSchema.Designer.cs => 20260622224939_InitialArtistSearchEngineSchema.Designer.cs} (99%) rename src/Melodee.Common/Migrations/ArtistSearchEngine/{20260622223824_InitialArtistSearchEngineSchema.cs => 20260622224939_InitialArtistSearchEngineSchema.cs} (97%) rename src/Melodee.Common/Migrations/ArtistSearchEngine/{20260622223847_SyncMusicBrainzUuidColumns.Designer.cs => 20260622225002_SyncMusicBrainzUuidColumns.Designer.cs} (99%) create mode 100644 src/Melodee.Common/Migrations/ArtistSearchEngine/20260622225002_SyncMusicBrainzUuidColumns.cs diff --git a/docs/pages/changelog.md b/docs/pages/changelog.md index 7654bbc9c..71cf765dd 100644 --- a/docs/pages/changelog.md +++ b/docs/pages/changelog.md @@ -37,12 +37,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Replaced the hand-rolled raw-SQL ArtistSearch initial migration with a proper model-driven EF Core migration that uses `CREATE TABLE IF NOT EXISTS` for idempotent application on both fresh and existing databases. +- Added a second `SyncMusicBrainzUuidColumns` migration as a no-op to satisfy EF Core's + migration chain. DecentDB reports `Guid`/`byte[]` columns as `UUID` in the EF model but + stores them as `BLOB`, and `ALTER COLUMN TYPE` to `UUID` is unsupported by DecentDB + (only `INT64`, `FLOAT64`, `TEXT`, `BOOL`). The no-op migration records itself as applied + without attempting the unsupported `ALTER`, keeping `MigrateAsync` stable. ### Fixed - Resolved EF Core "pending model changes" error for `ArtistSearchEngineServiceDbContext` by regenerating the model snapshot to match the `IsLocked` `INTEGER` column type configured in the DbContext. +- Resolved DecentDB `ALTER COLUMN TYPE supports only INT64, FLOAT64, TEXT, and BOOL` crash + during ArtistSearch migration by making the `SyncMusicBrainzUuidColumns` migration a no-op. - ArtistSearch database is now auto-created on first use when the `.ddb` file is absent, eliminating the "database is empty or not initialized" error that previously required manual intervention. diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.cs deleted file mode 100644 index 4967d693e..000000000 --- a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Melodee.Common.Migrations.ArtistSearchEngine -{ - /// - public partial class SyncMusicBrainzUuidColumns : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "MusicBrainzId", - table: "Artists", - type: "UUID", - nullable: true, - oldClrType: typeof(byte[]), - oldType: "BLOB", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MusicBrainzReleaseGroupId", - table: "Albums", - type: "UUID", - nullable: true, - oldClrType: typeof(byte[]), - oldType: "BLOB", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MusicBrainzId", - table: "Albums", - type: "UUID", - nullable: true, - oldClrType: typeof(byte[]), - oldType: "BLOB", - oldNullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "MusicBrainzId", - table: "Artists", - type: "BLOB", - nullable: true, - oldClrType: typeof(byte[]), - oldType: "UUID", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MusicBrainzReleaseGroupId", - table: "Albums", - type: "BLOB", - nullable: true, - oldClrType: typeof(byte[]), - oldType: "UUID", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MusicBrainzId", - table: "Albums", - type: "BLOB", - nullable: true, - oldClrType: typeof(byte[]), - oldType: "UUID", - oldNullable: true); - } - } -} diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.Designer.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622224939_InitialArtistSearchEngineSchema.Designer.cs similarity index 99% rename from src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.Designer.cs rename to src/Melodee.Common/Migrations/ArtistSearchEngine/20260622224939_InitialArtistSearchEngineSchema.Designer.cs index 22678408b..5640f9607 100644 --- a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.Designer.cs +++ b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622224939_InitialArtistSearchEngineSchema.Designer.cs @@ -10,7 +10,7 @@ namespace Melodee.Common.Migrations.ArtistSearchEngine { [DbContext(typeof(ArtistSearchEngineServiceDbContext))] - [Migration("20260622223824_InitialArtistSearchEngineSchema")] + [Migration("20260622224939_InitialArtistSearchEngineSchema")] partial class InitialArtistSearchEngineSchema { /// diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622224939_InitialArtistSearchEngineSchema.cs similarity index 97% rename from src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.cs rename to src/Melodee.Common/Migrations/ArtistSearchEngine/20260622224939_InitialArtistSearchEngineSchema.cs index 0996e8736..c5e50fb2a 100644 --- a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223824_InitialArtistSearchEngineSchema.cs +++ b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622224939_InitialArtistSearchEngineSchema.cs @@ -21,7 +21,7 @@ protected override void Up(MigrationBuilder migrationBuilder) "AmgId" TEXT NULL, "DiscogsId" TEXT NULL, "WikiDataId" TEXT NULL, - "MusicBrainzId" UUID NULL, + "MusicBrainzId" BLOB NULL, "LastFmId" TEXT NULL, "SpotifyId" TEXT NULL, "IsLocked" INTEGER NULL, @@ -36,8 +36,8 @@ protected override void Up(MigrationBuilder migrationBuilder) "ArtistId" INTEGER NOT NULL, "SortName" TEXT NOT NULL, "AlbumType" INTEGER NOT NULL, - "MusicBrainzId" UUID NULL, - "MusicBrainzReleaseGroupId" UUID NULL, + "MusicBrainzId" BLOB NULL, + "MusicBrainzReleaseGroupId" BLOB NULL, "SpotifyId" TEXT NULL, "CoverUrl" TEXT NULL, "Name" TEXT NOT NULL, diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.Designer.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622225002_SyncMusicBrainzUuidColumns.Designer.cs similarity index 99% rename from src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.Designer.cs rename to src/Melodee.Common/Migrations/ArtistSearchEngine/20260622225002_SyncMusicBrainzUuidColumns.Designer.cs index 47e57f930..9b7e3b6a4 100644 --- a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622223847_SyncMusicBrainzUuidColumns.Designer.cs +++ b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622225002_SyncMusicBrainzUuidColumns.Designer.cs @@ -10,7 +10,7 @@ namespace Melodee.Common.Migrations.ArtistSearchEngine { [DbContext(typeof(ArtistSearchEngineServiceDbContext))] - [Migration("20260622223847_SyncMusicBrainzUuidColumns")] + [Migration("20260622225002_SyncMusicBrainzUuidColumns")] partial class SyncMusicBrainzUuidColumns { /// diff --git a/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622225002_SyncMusicBrainzUuidColumns.cs b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622225002_SyncMusicBrainzUuidColumns.cs new file mode 100644 index 000000000..b393d6f1a --- /dev/null +++ b/src/Melodee.Common/Migrations/ArtistSearchEngine/20260622225002_SyncMusicBrainzUuidColumns.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Melodee.Common.Migrations.ArtistSearchEngine +{ + /// + /// DecentDB reports Guid/byte[] columns as UUID in the EF model but stores them as BLOB. + /// ALTER COLUMN TYPE to UUID is unsupported by DecentDB (only INT64, FLOAT64, TEXT, BOOL). + /// This migration is a no-op — the columns are already BLOB in the database, which works + /// correctly for storing Guid values. The migration exists to satisfy EF Core's migration + /// chain so MigrateAsync does not detect pending changes. + /// + /// + public partial class SyncMusicBrainzUuidColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} \ No newline at end of file From 2b160ef112b2ca1304c4230cd6d881216d5e1a4e Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Tue, 23 Jun 2026 14:44:09 -0500 Subject: [PATCH 05/11] fix(search): resolve parameter binding issue in paged artist queries - Rearranged the execution order of paged and count queries to prevent unbound parameter errors caused by DecentDB's sequential parameter numbering. --- .../SearchEngines/ArtistSearchEngineService.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs b/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs index 8d29c1643..b8c3d3dbf 100644 --- a/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs +++ b/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs @@ -422,7 +422,12 @@ public async Task> ListAsync( } else { - var pageProbeSize = pagedRequest.TakeValue + 1; + // Execute the paged query before the count query. DecentDB's ADO.NET provider + // numbers positional parameters ($1, $2, ...) per connection and does not reset + // the counter between sequential commands on the same DbContext. Running + // CountAsync first (which may consume $1) leaves the LIMIT/OFFSET params of the + // page query unbound ("missing value for parameter $2"). The count query carries + // no LIMIT/OFFSET parameters, so executing it afterward is safe. var page = await ApplyArtistListOrdering(query, pagedRequest) .Select(x => new Artist { @@ -442,15 +447,13 @@ public async Task> ListAsync( LastRefreshed = x.LastRefreshed }) .Skip(pagedRequest.SkipValue) - .Take(pageProbeSize) + .Take(pagedRequest.TakeValue) .ToArrayAsync(cancellationToken) .ConfigureAwait(false); - var hasMoreRows = page.Length > pagedRequest.TakeValue; - artists = page.Take(pagedRequest.TakeValue).ToArray(); - totalCount = artists.Length == 0 && pagedRequest.SkipValue == 0 - ? 0 - : pagedRequest.SkipValue + artists.Length + (hasMoreRows ? 1 : 0); + artists = page; + + totalCount = await query.CountAsync(cancellationToken).ConfigureAwait(false); var artistIds = artists.Select(x => x.Id).ToArray(); if (artistIds.Length > 0) From 6713bbc38bea73f141c726487d5b3e19c621b16a Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Wed, 24 Jun 2026 18:42:22 -0500 Subject: [PATCH 06/11] refactor(charts): switch ChartEditor to use ApiKey instead of ChartId for edits - Updated routing, parameters, and related services to utilize `ApiKey` in place of `ChartId`. - Adjusted CSV parsing and ChartService methods to support the new identifier. - Improved normalization logic for artist and album matching by removing accents. --- .../Components/Pages/Admin/ChartEditor.razor | 43 ++++++++++--------- .../Components/Pages/Data/Charts.razor | 2 +- .../Pages/Data/ChartsMissingReport.razor | 2 +- src/Melodee.Common/Services/ChartService.cs | 16 ++++--- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/Melodee.Blazor/Components/Pages/Admin/ChartEditor.razor b/src/Melodee.Blazor/Components/Pages/Admin/ChartEditor.razor index 267364979..f3f856ab2 100644 --- a/src/Melodee.Blazor/Components/Pages/Admin/ChartEditor.razor +++ b/src/Melodee.Blazor/Components/Pages/Admin/ChartEditor.razor @@ -1,5 +1,5 @@ @page "/charts/create" -@page "/charts/edit/{ChartId:int}" +@page "/charts/edit/{ApiKey:guid}" @inherits MelodeeComponentBase @using Melodee.Common.Data.Models @@ -115,7 +115,7 @@ - + @* CSV Input - Required for Create, shown inline *@ @if (!_isEditMode) { @@ -131,7 +131,7 @@ - + @if (_csvErrors.Count > 0) { @@ -148,20 +148,20 @@ } - + @if (_previewItems.Count > 0 && _csvErrors.Count == 0) { @L("ChartEditor.ValidItemsReady", _previewItems.Count) } - - - + @* Chart Image - Optional for Create *@ @@ -179,7 +179,7 @@ @if (_createImageBytes != null) { - @L( @L("ChartEditor.ImageSelectedWillUpload") @@ -189,7 +189,7 @@ } - + @@ -210,8 +210,8 @@ @if (_hasImage) { - } else @@ -232,7 +232,7 @@ @if (_selectedImageBytes != null) { - @L( @@ -374,9 +374,9 @@ @code { - [Parameter] public int ChartId { get; set; } + [Parameter] public Guid ApiKey { get; set; } - bool _isEditMode => ChartId > 0; + bool _isEditMode => ApiKey != Guid.Empty; int _chartId; string _title = ""; @@ -413,7 +413,7 @@ await base.OnInitializedAsync(); if (_isEditMode) { - _chartId = ChartId; + _chartApiKey = ApiKey; await LoadChartAsync(); } } @@ -423,7 +423,7 @@ MainLayoutProxyService.ToggleSpinnerVisible(); try { - var result = await ChartService.GetByIdAsync(_chartId); + var result = await ChartService.GetByApiKeyAsync(ApiKey); if (result.IsSuccess && result.Data != null) { var chart = result.Data; @@ -436,6 +436,7 @@ _tagsInput = chart.Tags?.Replace('|', ',') ?? ""; _isVisible = chart.IsVisible; _isGeneratedPlaylistEnabled = chart.IsGeneratedPlaylistEnabled; + _chartId = chart.Id; _chartItems = chart.Items.OrderBy(i => i.Rank).ToList(); _chartApiKey = chart.ApiKey; @@ -500,7 +501,7 @@ }); return; } - + // Validate CSV before creating var parseResult = await ChartService.ParseCsvAsync(_csvInput); if (parseResult.Data is null || parseResult.Data.Errors.Count > 0 || parseResult.Data.Items.Count == 0) @@ -558,7 +559,7 @@ tags, _isVisible, _isGeneratedPlaylistEnabled); - + if (result.IsSuccess && result.Data != null) { // Now save the chart items @@ -582,7 +583,7 @@ Duration = ToastTime }); } - + // Upload image if one was selected if (_createImageBytes != null) { @@ -598,8 +599,8 @@ }); } } - - NavigationManager.NavigateTo($"/charts/edit/{result.Data.Id}"); + + NavigationManager.NavigateTo($"/charts/edit/{result.Data.ApiKey}"); } else { diff --git a/src/Melodee.Blazor/Components/Pages/Data/Charts.razor b/src/Melodee.Blazor/Components/Pages/Data/Charts.razor index 564e49361..4615a1df0 100644 --- a/src/Melodee.Blazor/Components/Pages/Data/Charts.razor +++ b/src/Melodee.Blazor/Components/Pages/Data/Charts.razor @@ -253,7 +253,7 @@ void EditChart(Chart chart) { - NavigationManager.NavigateTo($"/charts/edit/{chart.Id}"); + NavigationManager.NavigateTo($"/charts/edit/{chart.ApiKey}"); } async Task DeleteChart(Chart chart) diff --git a/src/Melodee.Blazor/Components/Pages/Data/ChartsMissingReport.razor b/src/Melodee.Blazor/Components/Pages/Data/ChartsMissingReport.razor index a6af62e50..17a29ce6b 100644 --- a/src/Melodee.Blazor/Components/Pages/Data/ChartsMissingReport.razor +++ b/src/Melodee.Blazor/Components/Pages/Data/ChartsMissingReport.razor @@ -229,7 +229,7 @@ foreach (var item in unlinked) { - var key = $"{item.ArtistName.ToUpperInvariant()}|{item.AlbumTitle.ToUpperInvariant()}"; + var key = $"{item.ArtistName.RemoveAccents()?.ToUpperInvariant() ?? item.ArtistName}|{item.AlbumTitle.RemoveAccents()?.ToUpperInvariant() ?? item.AlbumTitle}"; if (!unlinkedItems.TryGetValue(key, out var missingAlbum)) { diff --git a/src/Melodee.Common/Services/ChartService.cs b/src/Melodee.Common/Services/ChartService.cs index 73a52018f..94be9ec19 100644 --- a/src/Melodee.Common/Services/ChartService.cs +++ b/src/Melodee.Common/Services/ChartService.cs @@ -805,18 +805,22 @@ public async Task> IdentifyMissingAl }; } - var normalizedArtistName = artistName.ToUpperInvariant(); - var normalizedAlbumTitle = albumTitle.ToUpperInvariant(); var linkedStatus = (short)ChartItemLinkStatus.Linked; - var matchingItems = await scopedContext.ChartItems + var allUnlinkedItems = await scopedContext.ChartItems .Include(i => i.Chart) - .Where(i => i.ArtistName.ToUpper() == normalizedArtistName && - i.AlbumTitle.ToUpper() == normalizedAlbumTitle && - i.LinkStatus != linkedStatus) + .Where(i => i.LinkStatus != linkedStatus) .ToListAsync(cancellationToken) .ConfigureAwait(false); + var normalizedInputArtist = artistName.RemoveAccents(); + var normalizedInputAlbum = albumTitle.RemoveAccents(); + + var matchingItems = allUnlinkedItems + .Where(i => string.Equals(i.ArtistName.RemoveAccents(), normalizedInputArtist, StringComparison.OrdinalIgnoreCase) && + string.Equals(i.AlbumTitle.RemoveAccents(), normalizedInputAlbum, StringComparison.OrdinalIgnoreCase)) + .ToList(); + if (matchingItems.Count == 0) { return new OperationResult(["No unlinked chart items found for the specified artist/album"]) From 2a335f492173edcf7f73de661235627c3e745827 Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Wed, 24 Jun 2026 20:58:46 -0500 Subject: [PATCH 07/11] refactor(api-keys): use `Guid` for `ApiKey` parameters and routes across components - Updated `UserDetail`, `PodcastDetail`, and `UserGroupDetail` to use `Guid` for `ApiKey` parameters instead of `string`. - Adjusted route definitions to ensure `ApiKey` is validated as a GUID. - Removed obsolete API key parsing methods. --- .../Components/Pages/Data/PodcastDetail.razor | 8 ++-- .../Components/Pages/UserDetail.razor | 36 ++--------------- .../Components/Pages/UserGroupDetail.razor | 40 ++----------------- 3 files changed, 12 insertions(+), 72 deletions(-) diff --git a/src/Melodee.Blazor/Components/Pages/Data/PodcastDetail.razor b/src/Melodee.Blazor/Components/Pages/Data/PodcastDetail.razor index 542a23e1c..a66589315 100644 --- a/src/Melodee.Blazor/Components/Pages/Data/PodcastDetail.razor +++ b/src/Melodee.Blazor/Components/Pages/Data/PodcastDetail.razor @@ -1,4 +1,4 @@ -@page "/data/podcasts/{ChannelApiKey}" +@page "/data/podcasts/{ChannelApiKey:guid}" @inherits MelodeeComponentBase @using Melodee.Common.Models.Collection @using Melodee.Common.Models @@ -331,7 +331,7 @@ @code { [Parameter] - public string ChannelApiKey { get; set; } = string.Empty; + public Guid ChannelApiKey { get; set; } private RadzenDataGrid? _grid; private PodcastChannelDataInfo? _channel; @@ -349,7 +349,7 @@ private async Task LoadChannel() { - if (CurrentUsersId == 0 || string.IsNullOrEmpty(ChannelApiKey)) + if (CurrentUsersId == 0 || ChannelApiKey == Guid.Empty) { return; } @@ -361,7 +361,7 @@ { // Find channel by ApiKey var channelsResult = await PodcastService.ListChannelsAsync(new PagedRequest { PageSize = 1000 }, CurrentUsersId); - _channel = channelsResult.Data.FirstOrDefault(c => c.ApiKey.ToString() == ChannelApiKey); + _channel = channelsResult.Data.FirstOrDefault(c => c.ApiKey == ChannelApiKey); if (_channel != null) { diff --git a/src/Melodee.Blazor/Components/Pages/UserDetail.razor b/src/Melodee.Blazor/Components/Pages/UserDetail.razor index 1c98bc671..c0960b0c4 100644 --- a/src/Melodee.Blazor/Components/Pages/UserDetail.razor +++ b/src/Melodee.Blazor/Components/Pages/UserDetail.razor @@ -1,4 +1,4 @@ -@page "/user/{UserApiKey}" +@page "/user/{UserApiKey:guid}" @using System.Globalization @using NodaTime @using Melodee.Common.Data @@ -207,7 +207,7 @@ @code { [Parameter] - public string UserApiKey { get; set; } = string.Empty; + public Guid UserApiKey { get; set; } User? _user; string _userName = string.Empty; @@ -225,15 +225,14 @@ try { - var apiKey = ParseUserApiKey(UserApiKey); - if (!apiKey.HasValue) + if (UserApiKey == Guid.Empty) { NotificationService.Notify(new NotificationMessage { Severity = NotificationSeverity.Error, Summary = L("Status.Error"), Detail = L("UserDetail.InvalidApiKeyFormat") }); _isLoading = false; return; } - var userResult = await UserProfileService.GetByApiKeyAsync(apiKey.Value); + var userResult = await UserProfileService.GetByApiKeyAsync(UserApiKey); if (!userResult.IsSuccess || userResult.Data == null) { NotificationService.Notify(new NotificationMessage { Severity = NotificationSeverity.Error, Summary = L("Status.Error"), Detail = L("UserDetail.UserNotFound") }); @@ -387,33 +386,6 @@ .CountAsync(); } - Guid? ParseUserApiKey(string apiKeyInput) - { - if (string.IsNullOrWhiteSpace(apiKeyInput)) - { - return null; - } - - // Try direct GUID parse first (Melodee format: "51b53687-e735-4f63-90f7-ab61f4249b06") - if (Guid.TryParse(apiKeyInput, out var directGuid)) - { - return directGuid; - } - - // Try OpenSubsonic format: "user_{guid}" - const string userPrefix = "user_"; - if (apiKeyInput.StartsWith(userPrefix, StringComparison.OrdinalIgnoreCase)) - { - var guidPart = apiKeyInput.Substring(userPrefix.Length); - if (Guid.TryParse(guidPart, out var parsedGuid)) - { - return parsedGuid; - } - } - - return null; - } - sealed class UserStats { public int StarredArtistsCount { get; set; } diff --git a/src/Melodee.Blazor/Components/Pages/UserGroupDetail.razor b/src/Melodee.Blazor/Components/Pages/UserGroupDetail.razor index 6d76063b6..373786805 100644 --- a/src/Melodee.Blazor/Components/Pages/UserGroupDetail.razor +++ b/src/Melodee.Blazor/Components/Pages/UserGroupDetail.razor @@ -1,4 +1,4 @@ -@page "/usergroup/{ApiKey}" +@page "/usergroup/{ApiKey:guid}" @using System.Globalization @using NodaTime @using Melodee.Common.Data @@ -137,7 +137,7 @@ @code { [Parameter] - public string ApiKey { get; set; } = string.Empty; + public Guid ApiKey { get; set; } UserGroup? _userGroup; List _members = []; @@ -150,22 +150,14 @@ try { - if (string.IsNullOrWhiteSpace(ApiKey)) + if (ApiKey == Guid.Empty) { NotificationService.Notify(new NotificationMessage { Severity = NotificationSeverity.Error, Summary = L("Status.Error"), Detail = L("UserDetail.InvalidApiKeyFormat") }); _isLoading = false; return; } - var apiKeyResult = ParseUserGroupApiKey(ApiKey); - if (!apiKeyResult.HasValue) - { - NotificationService.Notify(new NotificationMessage { Severity = NotificationSeverity.Error, Summary = L("Status.Error"), Detail = L("UserDetail.InvalidApiKeyFormat") }); - _isLoading = false; - return; - } - - var userGroupResult = await UserGroupService.GetByApiKeyAsync(apiKeyResult.Value); + var userGroupResult = await UserGroupService.GetByApiKeyAsync(ApiKey); if (!userGroupResult.IsSuccess || userGroupResult.Data == null) { NotificationService.Notify(new NotificationMessage { Severity = NotificationSeverity.Error, Summary = L("Status.Error"), Detail = L("UserDetail.UserNotFound") }); @@ -192,28 +184,4 @@ } } - Guid? ParseUserGroupApiKey(string apiKeyInput) - { - if (string.IsNullOrWhiteSpace(apiKeyInput)) - { - return null; - } - - if (Guid.TryParse(apiKeyInput, out var directGuid)) - { - return directGuid; - } - - const string userGroupPrefix = "usergroup_"; - if (apiKeyInput.StartsWith(userGroupPrefix, StringComparison.OrdinalIgnoreCase)) - { - var guidPart = apiKeyInput.Substring(userGroupPrefix.Length); - if (Guid.TryParse(guidPart, out var parsedGuid)) - { - return parsedGuid; - } - } - - return null; - } } From 70733af0a28f2c81bfd013f023c3fa3fa6449aad Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Sat, 11 Jul 2026 09:04:52 -0500 Subject: [PATCH 08/11] feat(dialogs): add DecentDB migration UI and logic - Implemented `DecentDbMigrationDialog` to guide users through the migration process for unsupported DecentDB files. - Added migration step-by-step UI, including download links, migration commands, and error details. - Introduced `DecentDbMigrationInstructionsBuilder` to generate migration-related paths and commands dynamically for Windows and POSIX environments. - Updated `Dashboard` and `Doctor` views to surface DecentDB issues and open the migration dialog when needed. - Added unit tests for migration instruction generation across various scenarios. --- Directory.Packages.props | 56 ++--- docs/pages/changelog.md | 4 + docs/pages/decentdb.md | 87 +++++-- .../Dialogs/DecentDbMigrationDialog.razor | 215 +++++++++++++++++ .../Dialogs/DecentDbMigrationDialog.razor.css | 81 +++++++ .../DecentDbMigrationInstructionsBuilder.cs | 220 ++++++++++++++++++ .../Components/Pages/Admin/Dashboard.razor | 47 +++- .../Components/Pages/Admin/Doctor.razor | 49 ++++ .../Components/Pages/Dashboard.razor | 42 +++- .../Pages/DashboardHealthWarningEvaluator.cs | 37 ++- .../Extensions/DialogServiceExtensions.cs | 36 ++- .../Resources/SharedResources.ar-SA.resx | 130 ++++++++++- .../Resources/SharedResources.cs-CZ.resx | 130 ++++++++++- .../Resources/SharedResources.de-DE.resx | 130 ++++++++++- .../Resources/SharedResources.es-ES.resx | 130 ++++++++++- .../Resources/SharedResources.fa-IR.resx | 130 ++++++++++- .../Resources/SharedResources.fr-FR.resx | 130 ++++++++++- .../Resources/SharedResources.id-ID.resx | 130 ++++++++++- .../Resources/SharedResources.it-IT.resx | 130 ++++++++++- .../Resources/SharedResources.ja-JP.resx | 130 ++++++++++- .../Resources/SharedResources.ko-KR.resx | 130 ++++++++++- .../Resources/SharedResources.nl-NL.resx | 130 ++++++++++- .../Resources/SharedResources.pl-PL.resx | 130 ++++++++++- .../Resources/SharedResources.pt-BR.resx | 130 ++++++++++- .../Resources/SharedResources.resx | 130 ++++++++++- .../Resources/SharedResources.ru-RU.resx | 130 ++++++++++- .../Resources/SharedResources.sv-SE.resx | 130 ++++++++++- .../Resources/SharedResources.tr-TR.resx | 130 ++++++++++- .../Resources/SharedResources.uk-UA.resx | 130 ++++++++++- .../Resources/SharedResources.vi-VN.resx | 130 ++++++++++- .../Resources/SharedResources.zh-CN.resx | 130 ++++++++++- src/Melodee.Blazor/Services/DoctorService.cs | 62 ++++- .../DashboardHealthWarningEvaluatorTests.cs | 45 ++++ ...centDbMigrationInstructionsBuilderTests.cs | 145 ++++++++++++ .../Services/DoctorServiceTests.cs | 14 +- 35 files changed, 3636 insertions(+), 104 deletions(-) create mode 100644 src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationDialog.razor create mode 100644 src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationDialog.razor.css create mode 100644 src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationInstructionsBuilder.cs create mode 100644 tests/Melodee.Tests.Blazor/Components/DecentDbMigrationInstructionsBuilderTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 28cdbdc01..f7ec44592 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,9 +17,9 @@ - - - + + + @@ -30,13 +30,13 @@ - - + + - - - + + + @@ -66,28 +66,28 @@ - + - - - - - + + + + + - - - - - - + + + + + + - - + + @@ -96,18 +96,18 @@ - + - + - - + + - + @@ -119,4 +119,4 @@ - + \ No newline at end of file diff --git a/docs/pages/changelog.md b/docs/pages/changelog.md index 71cf765dd..895568419 100644 --- a/docs/pages/changelog.md +++ b/docs/pages/changelog.md @@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Admin health checks now recognize DecentDB error 8 and automatically open a + migration dialog with the matching prebuilt release, configured source and + destination paths, copyable `decentdb-migrate` commands, verification steps, + safe replacement scripts, and links to the official DecentDB guidance. - Media Artist export/import: export all search-engine artist data (artists, albums, aliases) as JSON via the `/admin/mediaartists` page; import JSON exports with optional overwrite and full preview of artist/album/alias counts. diff --git a/docs/pages/decentdb.md b/docs/pages/decentdb.md index 1080d3311..5ed144117 100644 --- a/docs/pages/decentdb.md +++ b/docs/pages/decentdb.md @@ -1,6 +1,8 @@ --- title: DecentDB Usage & Migration -description: How Melodee uses DecentDB, how to diagnose compatibility issues, and how to rebuild generated DecentDB search databases. +description: >- + How Melodee uses DecentDB, how to diagnose compatibility issues, and how to + upgrade or rebuild generated DecentDB search databases. permalink: /decentdb/ tags: - decentdb @@ -57,27 +59,66 @@ MusicBrainz DecentDB database uses a file format that is not supported by the cu Provider error: unsupported DecentDB file format version 11 ``` -This means the current Melodee process cannot safely read that generated search -database. Search and enrichment may continue in degraded mode, but the affected -database should be rebuilt or the Melodee/DecentDB package version should be -updated. +This is DecentDB error 8 (`ERR_UNSUPPORTED_FORMAT_VERSION`). It means the +current Melodee process cannot safely read that generated search database. +Search and enrichment may continue in degraded mode, but the affected database +must be upgraded with `decentdb-migrate` or rebuilt before it can be used. ## Migration Strategy -For Melodee's generated DecentDB files, migration usually means rebuilding the -affected generated database with the current Melodee version. This is safer than -trying to manually edit a DecentDB file created by an incompatible provider. +Use DecentDB's standalone `decentdb-migrate` utility first. The utility reads the +old database and writes an upgraded copy to a new path; it does not overwrite the +source database in place. Use this order: -1. Upgrade Melodee to the intended version. -2. Stop Melodee so no process is writing to the DecentDB files. -3. Back up the affected `.ddb` file and any companion WAL or shared-memory - files. -4. Move the incompatible generated database out of the active path. -5. Start Melodee. -6. Rebuild the affected database from Melodee. -7. Run Doctor again and confirm the DecentDB checks pass. +1. Note the DecentDB version shown in Melodee's migration dialog. +2. Download and extract the matching archive from the + [DecentDB releases](https://github.com/sphildreth/decentdb/releases) page. + Release archives include `decentdb-migrate` and the `decentdb` CLI. +3. Stop every Melodee instance that can access the affected file. +4. Run `decentdb-migrate` with the configured database path as `--source` and a + new, nonexistent path as `--dest`. +5. Verify the new file with the `decentdb` executable from the same release. +6. Preserve the original database and its sidecars, then promote the migrated + database to the configured path. +7. Restart Melodee and confirm the DecentDB checks pass in **Admin > Doctor**. + +See DecentDB's +[official migration guide](https://decentdb.org/user-guide/migration/) for the +tool's supported source formats and build-from-source alternative. + +### Migration Example + +The web migration dialog generates these commands with the active configured +path. A MusicBrainz migration follows this shape: + +```bash +./decentdb-migrate \ + --source /path/to/musicbrainz.ddb \ + --dest /path/to/musicbrainz_migrated.ddb +``` + +Do not continue until the utility reports: + +```text +Migration complete! Your upgraded database is ready at: /path/to/musicbrainz_migrated.ddb +``` + +Verify the upgraded copy with the CLI from the same release: + +```bash +./decentdb info --db /path/to/musicbrainz_migrated.ddb +``` + +Keep Melodee stopped while replacing the active file. Preserve the original +`.ddb` file and any `.wal`, `.coord`, or `.wal-idx` sidecars until normal +operation is verified. If Melodee runs in a container, translate the displayed +container path to the corresponding host volume path when running the utility +on the host. + +If the migration utility reports that the source format has no supported +migration path, use the rebuild procedure below. ## Backup Before Rebuild @@ -99,7 +140,7 @@ backup_decentdb_file() { local db_name db_name="$(basename "$db_path")" - for suffix in "" ".wal" "-wal" ".shm" "-shm"; do + for suffix in "" ".wal" ".coord" ".wal-idx" "-wal" ".shm" "-shm"; do if [ -f "${db_path}${suffix}" ]; then cp -a "${db_path}${suffix}" "$backup_root/${db_name}${suffix}" fi @@ -132,7 +173,7 @@ artist_search_db="/path/to/search-engine-storage/artistSearchEngine.ddb" move_decentdb_file_aside() { local db_path="$1" - for suffix in "" ".wal" "-wal" ".shm" "-shm"; do + for suffix in "" ".wal" ".coord" ".wal-idx" "-wal" ".shm" "-shm"; do if [ -f "${db_path}${suffix}" ]; then mv "${db_path}${suffix}" "${db_path}${suffix}.unsupported-$stamp" fi @@ -188,7 +229,7 @@ aside. ## Verify The Migration -Run Doctor after rebuilding: +Run Doctor after upgrading or rebuilding: ```bash ./mcli doctor --verbose @@ -201,13 +242,12 @@ The following checks should pass: - `MusicBrainzDatabase` - `ArtistSearchEngineDatabase` -If Doctor still reports an unsupported DecentDB file format after rebuilding, -confirm that: +If Doctor still reports an unsupported DecentDB file format, confirm that: - Melodee is running the version you expect. - The active connection strings point to the rebuilt files. -- No old `.wal`, `-wal`, `.shm`, or `-shm` companion files remain beside the - rebuilt database. +- No stale `.wal`, `.coord`, `.wal-idx`, `-wal`, `.shm`, or `-shm` companion + files remain beside the active database. - The app container or service was restarted after the rebuild. ## What Not To Delete @@ -215,4 +255,3 @@ confirm that: Do not delete the primary PostgreSQL database when resolving DecentDB search cache compatibility issues. The unsupported DecentDB file-format warning applies to generated local search databases, not the primary Melodee database. - diff --git a/src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationDialog.razor b/src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationDialog.razor new file mode 100644 index 000000000..ce09063e1 --- /dev/null +++ b/src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationDialog.razor @@ -0,0 +1,215 @@ +@using DecentDB.AdoNet +@using Melodee.Blazor.Components.Pages +@using Melodee.Common.Services.Doctor +@inherits MelodeeComponentBase + +@inject IConfiguration AppConfiguration +@inject DialogService DialogService +@inject IJSRuntime JsRuntime +@inject NotificationService NotificationService + + + + + + @L("DecentDbMigrationDialog.Intro") + + + +
    +
  1. + @L("DecentDbMigrationDialog.DownloadTitle") + + @L("DecentDbMigrationDialog.DownloadStep", DecentDbVersionDisplay) + + + + + + + @L("DecentDbMigrationDialog.BuildAlternative") + + @CommandBlock( + L("DecentDbMigrationDialog.BuildCommand"), + DecentDbMigrationInstructionsBuilder.BuildCommand) +
  2. + +
  3. + @L("DecentDbMigrationDialog.StopTitle") + @L("DecentDbMigrationDialog.StopStep") +
  4. + +
  5. + @L("DecentDbMigrationDialog.MigrateTitle") + @L("DecentDbMigrationDialog.MigrateStep") + + @foreach (var target in _targets) + { + + + + @L("DecentDbMigrationDialog.DetectedError"): @target.ErrorDetails + + + @if (target.DatabasePath is null || + target.MigratedDatabasePath is null || + target.MigrationCommand is null || + target.VerificationCommand is null || + target.ReplacementCommand is null) + { + + @L("DecentDbMigrationDialog.PathUnavailable") + + } + else + { +
    +
    @L("DecentDbMigrationDialog.DatabasePath")
    +
    @target.DatabasePath
    +
    @L("DecentDbMigrationDialog.DestinationPath")
    +
    @target.MigratedDatabasePath
    +
    + + @CommandBlock(L("DecentDbMigrationDialog.MigrationCommand"), target.MigrationCommand) + + + @L("DecentDbMigrationDialog.VerifyTitle") + + @L("DecentDbMigrationDialog.VerifyStep") + @CommandBlock(L("DecentDbMigrationDialog.VerificationCommand"), target.VerificationCommand) + + + @L("DecentDbMigrationDialog.ExpectedOutput") + +
    Migration complete! Your upgraded database is ready at: @target.MigratedDatabasePath
    + + + @L("DecentDbMigrationDialog.ReplaceTitle") + + @L("DecentDbMigrationDialog.ReplaceStep") + @CommandBlock(L("DecentDbMigrationDialog.ReplacementCommand"), target.ReplacementCommand) + } +
    +
    + } +
  6. + +
  7. + @L("DecentDbMigrationDialog.RestartTitle") + @L("DecentDbMigrationDialog.RestartStep") +
  8. +
+ + + @L("DecentDbMigrationDialog.UnsupportedSource") + + + + + + +
+ +@code { + [Parameter] + public IReadOnlyList Issues { get; set; } = []; + + private IReadOnlyList _targets = []; + private string? _decentDbVersion; + + private string DecentDbVersionDisplay => string.IsNullOrWhiteSpace(_decentDbVersion) + ? L("DecentDbMigrationDialog.BundledVersion") + : L("DecentDbMigrationDialog.EngineVersion", _decentDbVersion.TrimStart('v', 'V')); + + private string DecentDbReleaseUrl => string.IsNullOrWhiteSpace(_decentDbVersion) + ? DecentDbMigrationInstructionsBuilder.ReleasesUrl + : $"{DecentDbMigrationInstructionsBuilder.ReleasesUrl}/tag/v{Uri.EscapeDataString(_decentDbVersion.TrimStart('v', 'V'))}"; + + protected override void OnParametersSet() + { + _targets = DecentDbMigrationInstructionsBuilder.Build(Issues, AppConfiguration, OperatingSystem.IsWindows()); + _decentDbVersion = GetDecentDbVersion(); + base.OnParametersSet(); + } + + private static string? GetDecentDbVersion() + { + try + { + return DecentDBConnection.EngineVersion(); + } + catch + { + return null; + } + } + + private RenderFragment CommandBlock(string label, string command) => @
+ + @label + + +
@command
+
; + + private async Task CopyCommandAsync(string command) + { + await JsRuntime.InvokeVoidAsync("navigator.clipboard.writeText", command); + NotificationService.Notify(new NotificationMessage + { + Severity = NotificationSeverity.Info, + Summary = L("DecentDbMigrationDialog.CommandCopied"), + Duration = ToastTime + }); + } + + private async Task OpenReleases() + { + await JsRuntime.InvokeVoidAsync( + "open", + DecentDbReleaseUrl, + "_blank", + "noopener,noreferrer"); + } + + private async Task OpenOfficialGuide() + { + await JsRuntime.InvokeVoidAsync( + "open", + DecentDbMigrationInstructionsBuilder.MigrationGuideUrl, + "_blank", + "noopener,noreferrer"); + } + + private async Task OpenSourceRepository() + { + await JsRuntime.InvokeVoidAsync( + "open", + DecentDbMigrationInstructionsBuilder.SourceRepositoryUrl, + "_blank", + "noopener,noreferrer"); + } + + private void Close() + { + DialogService.Close(); + } +} diff --git a/src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationDialog.razor.css b/src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationDialog.razor.css new file mode 100644 index 000000000..3f28d055c --- /dev/null +++ b/src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationDialog.razor.css @@ -0,0 +1,81 @@ +.decentdb-migration-dialog { + max-height: 76vh; + overflow-y: auto; + padding-right: 0.25rem; +} + +.migration-steps { + display: grid; + gap: 1.25rem; + margin: 0; + padding-inline-start: 1.5rem; +} + +.migration-steps > li { + padding-inline-start: 0.35rem; +} + +.migration-target { + min-width: 0; +} + +.error-details { + overflow-wrap: anywhere; +} + +.migration-paths { + display: grid; + gap: 0.35rem 0.75rem; + grid-template-columns: max-content minmax(0, 1fr); + margin: 0; +} + +.migration-paths dt { + font-weight: 600; +} + +.migration-paths dd { + margin: 0; + min-width: 0; + overflow-wrap: anywhere; +} + +.migration-paths code, +.migration-output, +.decentdb-command-block pre { + direction: ltr; + text-align: left; +} + +.migration-output { + background: var(--rz-base-100); + border: 1px solid var(--rz-base-300); + border-radius: var(--rz-border-radius); + margin: 0; + overflow-x: auto; + padding: 0.75rem; + white-space: pre; +} + +.decentdb-command-block { + display: grid; + gap: 0.35rem; + min-width: 0; +} + +.decentdb-command-block pre { + background: var(--rz-base-100); + border: 1px solid var(--rz-base-300); + border-radius: var(--rz-border-radius); + margin: 0; + max-height: 260px; + overflow: auto; + padding: 0.75rem; + white-space: pre; +} + +@media (max-width: 600px) { + .migration-paths { + grid-template-columns: 1fr; + } +} diff --git a/src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationInstructionsBuilder.cs b/src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationInstructionsBuilder.cs new file mode 100644 index 000000000..406d9d78d --- /dev/null +++ b/src/Melodee.Blazor/Components/Dialogs/DecentDbMigrationInstructionsBuilder.cs @@ -0,0 +1,220 @@ +using System.Data.Common; +using Melodee.Blazor.Components.Pages; +using Melodee.Common.Services.Doctor; + +namespace Melodee.Blazor.Components.Dialogs; + +internal sealed record DecentDbMigrationTarget( + string DisplayNameKey, + string ErrorDetails, + string? DatabasePath, + string? MigratedDatabasePath, + string? MigrationCommand, + string? VerificationCommand, + string? ReplacementCommand); + +internal static class DecentDbMigrationInstructionsBuilder +{ + internal const string MigrationGuideUrl = "https://decentdb.org/user-guide/migration/"; + internal const string ReleasesUrl = "https://github.com/sphildreth/decentdb/releases"; + internal const string SourceRepositoryUrl = "https://github.com/sphildreth/decentdb"; + internal const string BuildCommand = "cargo build --release -p decentdb-migrate"; + + internal static IReadOnlyList Build( + IEnumerable issues, + IConfiguration configuration, + bool isWindows) + { + ArgumentNullException.ThrowIfNull(issues); + ArgumentNullException.ThrowIfNull(configuration); + + var targets = new List(); + foreach (var issue in DashboardHealthWarningEvaluator.GetUnsupportedDecentDbIssues(issues) + .DistinctBy(x => x.Name)) + { + var database = GetDatabaseConfiguration(issue.Name); + if (database is null) + { + continue; + } + + var databasePath = GetDatabasePath(configuration.GetConnectionString(database.Value.ConnectionStringName)); + if (databasePath is null) + { + targets.Add(new DecentDbMigrationTarget( + database.Value.DisplayNameKey, + issue.Details, + null, + null, + null, + null, + null)); + continue; + } + + var migratedDatabasePath = CreateMigratedDatabasePath(databasePath); + targets.Add(new DecentDbMigrationTarget( + database.Value.DisplayNameKey, + issue.Details, + databasePath, + migratedDatabasePath, + CreateMigrationCommand(databasePath, migratedDatabasePath, isWindows), + CreateVerificationCommand(migratedDatabasePath, isWindows), + CreateReplacementCommand(databasePath, migratedDatabasePath, isWindows))); + } + + return targets; + } + + internal static string QuotePosixArgument(string value) + { + ArgumentNullException.ThrowIfNull(value); + + return $"'{value.Replace("'", "'\"'\"'", StringComparison.Ordinal)}'"; + } + + internal static string QuotePowerShellArgument(string value) + { + ArgumentNullException.ThrowIfNull(value); + + return $"'{value.Replace("'", "''", StringComparison.Ordinal)}'"; + } + + private static (string ConnectionStringName, string DisplayNameKey)? GetDatabaseConfiguration(string checkName) + { + return checkName switch + { + "MusicBrainzDatabase" => ("MusicBrainzConnection", "AdminDoctor.Check.MusicBrainzDatabase"), + "ArtistSearchEngineDatabase" => ("ArtistSearchEngineConnection", "AdminDoctor.Check.ArtistSearchEngineDatabase"), + _ => null + }; + } + + private static string? GetDatabasePath(string? connectionString) + { + if (string.IsNullOrWhiteSpace(connectionString)) + { + return null; + } + + try + { + var builder = new DbConnectionStringBuilder { ConnectionString = connectionString }; + var configuredPath = builder.ContainsKey("Data Source") + ? builder["Data Source"]?.ToString() + : null; + if (string.IsNullOrWhiteSpace(configuredPath)) + { + return null; + } + + return Path.GetFullPath(configuredPath); + } + catch (ArgumentException) + { + return null; + } + catch (NotSupportedException) + { + return null; + } + catch (IOException) + { + return null; + } + } + + private static string CreateMigratedDatabasePath(string databasePath) + { + var directory = Path.GetDirectoryName(databasePath) ?? string.Empty; + var extension = Path.GetExtension(databasePath); + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(databasePath); + var migratedFileName = $"{fileNameWithoutExtension}_migrated{extension}"; + + return Path.Combine(directory, migratedFileName); + } + + private static string CreateMigrationCommand(string databasePath, string migratedDatabasePath, bool isWindows) + { + var executable = isWindows ? @".\decentdb-migrate.exe" : "./decentdb-migrate"; + Func quote = isWindows + ? QuotePowerShellArgument + : QuotePosixArgument; + + return $"{executable} --source {quote(databasePath)} --dest {quote(migratedDatabasePath)}"; + } + + private static string CreateVerificationCommand(string migratedDatabasePath, bool isWindows) + { + var executable = isWindows ? @".\decentdb.exe" : "./decentdb"; + Func quote = isWindows + ? QuotePowerShellArgument + : QuotePosixArgument; + + return $"{executable} info --db {quote(migratedDatabasePath)}"; + } + + private static string CreateReplacementCommand(string databasePath, string migratedDatabasePath, bool isWindows) + { + return isWindows + ? CreatePowerShellReplacementCommand(databasePath, migratedDatabasePath) + : CreatePosixReplacementCommand(databasePath, migratedDatabasePath); + } + + private static string CreatePosixReplacementCommand(string databasePath, string migratedDatabasePath) + { + return string.Join('\n', + [ + "set -euo pipefail", + $"source_db={QuotePosixArgument(databasePath)}", + $"migrated_db={QuotePosixArgument(migratedDatabasePath)}", + "backup_dir=\"${source_db}.pre-migration-$(date +%Y%m%d-%H%M%S)\"", + "mkdir \"$backup_dir\"", + "mv \"$source_db\" \"$backup_dir/\"", + "for suffix in .wal .coord .wal-idx; do", + " if [ -e \"${source_db}${suffix}\" ]; then", + " mv \"${source_db}${suffix}\" \"$backup_dir/\"", + " fi", + "done", + "mv \"$migrated_db\" \"$source_db\"", + "if [ -e \"${migrated_db}.wal\" ]; then", + " mv \"${migrated_db}.wal\" \"${source_db}.wal\"", + "fi", + "for suffix in .coord .wal-idx; do", + " if [ -e \"${migrated_db}${suffix}\" ]; then", + " mv \"${migrated_db}${suffix}\" \"$backup_dir/\"", + " fi", + "done" + ]); + } + + private static string CreatePowerShellReplacementCommand(string databasePath, string migratedDatabasePath) + { + return string.Join('\n', + [ + "$ErrorActionPreference = 'Stop'", + "Set-StrictMode -Version Latest", + $"$sourceDb = {QuotePowerShellArgument(databasePath)}", + $"$migratedDb = {QuotePowerShellArgument(migratedDatabasePath)}", + "$backupDir = \"${sourceDb}.pre-migration-$(Get-Date -Format 'yyyyMMdd-HHmmss')\"", + "New-Item -ItemType Directory -Path $backupDir | Out-Null", + "Move-Item -LiteralPath $sourceDb -Destination $backupDir", + "foreach ($suffix in '.wal', '.coord', '.wal-idx') {", + " $sidecar = \"${sourceDb}${suffix}\"", + " if (Test-Path -LiteralPath $sidecar) {", + " Move-Item -LiteralPath $sidecar -Destination $backupDir", + " }", + "}", + "Move-Item -LiteralPath $migratedDb -Destination $sourceDb", + "if (Test-Path -LiteralPath \"${migratedDb}.wal\") {", + " Move-Item -LiteralPath \"${migratedDb}.wal\" -Destination \"${sourceDb}.wal\"", + "}", + "foreach ($suffix in '.coord', '.wal-idx') {", + " $sidecar = \"${migratedDb}${suffix}\"", + " if (Test-Path -LiteralPath $sidecar) {", + " Move-Item -LiteralPath $sidecar -Destination $backupDir", + " }", + "}" + ]); + } +} diff --git a/src/Melodee.Blazor/Components/Pages/Admin/Dashboard.razor b/src/Melodee.Blazor/Components/Pages/Admin/Dashboard.razor index 61e34aaab..1f7ece525 100644 --- a/src/Melodee.Blazor/Components/Pages/Admin/Dashboard.razor +++ b/src/Melodee.Blazor/Components/Pages/Admin/Dashboard.razor @@ -29,6 +29,7 @@ @inject ISchedulerFactory SchedulerFactory @inject NavigationManager NavigationManager @inject IDoctorService DoctorService +@inject DialogService DialogService @inject IJSRuntime JSRuntime @inject Serilog.ILogger Logger @@ -65,14 +66,14 @@ @FormatHealthIssue(issue) } - @if (DashboardHealthWarningEvaluator.HasUnsupportedDecentDbIssue(_systemHealthIssues)) + @if (_decentDbMigrationIssues.Length > 0) { - - @L("Dashboard.DecentDbMigrationGuide") - + } !c.Success).Take(5).ToArray(); + _decentDbMigrationIssues = DashboardHealthWarningEvaluator + .GetUnsupportedDecentDbIssues(results.Checks) + .ToArray(); _systemHealthy = _systemHealthIssues.Length == 0; + _openDecentDbMigrationDialogAfterRender = + !_hasOpenedDecentDbMigrationDialog && + _decentDbMigrationIssues.Length > 0; } catch { + _decentDbMigrationIssues = []; + _openDecentDbMigrationDialogAfterRender = false; _systemHealthIssues = [ new Melodee.Common.Services.Doctor.DoctorCheckResult( @@ -700,6 +724,15 @@ } } + private Task OpenDecentDbMigrationDialogAsync() + { + return _decentDbMigrationIssues.Length == 0 + ? Task.CompletedTask + : DialogService.OpenDecentDbMigrationDialogAsync( + L("DecentDbMigrationDialog.Title"), + _decentDbMigrationIssues); + } + private string FormatHealthIssue(Melodee.Common.Services.Doctor.DoctorCheckResult issue) { var name = LocalizeHealthIssueName(issue.Name); diff --git a/src/Melodee.Blazor/Components/Pages/Admin/Doctor.razor b/src/Melodee.Blazor/Components/Pages/Admin/Doctor.razor index 5101d0a9f..18a251b4f 100644 --- a/src/Melodee.Blazor/Components/Pages/Admin/Doctor.razor +++ b/src/Melodee.Blazor/Components/Pages/Admin/Doctor.razor @@ -21,6 +21,7 @@ @inject IDbContextFactory MusicBrainzDbContextFactory @inject IConfiguration AppConfiguration @inject NotificationService NotificationService +@inject DialogService DialogService @inject IWebHostEnvironment WebHostEnvironment @inject ISchedulerFactory SchedulerFactory @inject DoctorServiceInterface DoctorService @@ -85,6 +86,23 @@ + @if (_decentDbMigrationIssues.Length > 0) + { + + + @L("DecentDbMigrationDialog.Intro") + + + + } + @* MusicBrainz Database Action Card - shown when DB is empty *@ @if (_isMusicBrainzEmpty) { @@ -367,6 +385,9 @@ @code { private bool _isLoading = true; private bool _allChecksPassed; + private DoctorCheckResult[] _decentDbMigrationIssues = []; + private bool _openDecentDbMigrationDialogAfterRender; + private bool _hasOpenedDecentDbMigrationDialog; private TimeSpan _totalDuration; private bool _isMusicBrainzEmpty; private bool _isMusicBrainzJobRunning; @@ -389,6 +410,18 @@ await RunDiagnosticsAsync(); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (_openDecentDbMigrationDialogAfterRender && !_hasOpenedDecentDbMigrationDialog) + { + _openDecentDbMigrationDialogAfterRender = false; + _hasOpenedDecentDbMigrationDialog = true; + await OpenDecentDbMigrationDialogAsync(); + } + } + private async Task TriggerMusicBrainzJobAsync() { try @@ -429,6 +462,7 @@ { _isLoading = true; _isMusicBrainzEmpty = false; + _openDecentDbMigrationDialogAfterRender = false; StateHasChanged(); var stopwatch = Stopwatch.StartNew(); @@ -447,6 +481,12 @@ // Run all checks via DoctorService var results = await DoctorService.RunAllChecksAsync(); + _decentDbMigrationIssues = DashboardHealthWarningEvaluator + .GetUnsupportedDecentDbIssues(results.Checks) + .ToArray(); + _openDecentDbMigrationDialogAfterRender = + !_hasOpenedDecentDbMigrationDialog && + _decentDbMigrationIssues.Length > 0; // Map results to local types with localized names foreach (var check in results.Checks) @@ -588,6 +628,15 @@ _isLoading = false; StateHasChanged(); } + + private Task OpenDecentDbMigrationDialogAsync() + { + return _decentDbMigrationIssues.Length == 0 + ? Task.CompletedTask + : DialogService.OpenDecentDbMigrationDialogAsync( + L("DecentDbMigrationDialog.Title"), + _decentDbMigrationIssues); + } private string GetLocalizedCheckName(string checkName) => checkName switch { diff --git a/src/Melodee.Blazor/Components/Pages/Dashboard.razor b/src/Melodee.Blazor/Components/Pages/Dashboard.razor index adcefc0ce..4e446629b 100644 --- a/src/Melodee.Blazor/Components/Pages/Dashboard.razor +++ b/src/Melodee.Blazor/Components/Pages/Dashboard.razor @@ -23,6 +23,7 @@ @inject IMelodeeConfigurationFactory ConfigurationFactory @inject NavigationManager NavigationManager @inject IDoctorService DoctorService +@inject DialogService DialogService @inject AuthenticationStateProvider DashboardAuthenticationStateProvider @inject MainLayoutProxyService MainLayoutProxyService @@ -56,13 +57,14 @@ } @if (DashboardHealthWarningEvaluator.HasUnsupportedDecentDbIssue(_healthIssues)) { - - @L("Dashboard.DecentDbMigrationGuide") - +
+ +
} @@ -369,6 +371,8 @@ private Melodee.Common.Services.Doctor.DoctorCheckResult[] _healthIssues = []; private bool _showHealthWarning; private bool _isAdmin; + private bool _openDecentDbMigrationDialogAfterRender; + private bool _hasOpenedDecentDbMigrationDialog; private bool _healthLoading = true; private bool _pinsLoading = true; @@ -427,6 +431,13 @@ { await base.OnAfterRenderAsync(firstRender); + if (_openDecentDbMigrationDialogAfterRender && !_hasOpenedDecentDbMigrationDialog) + { + _openDecentDbMigrationDialogAfterRender = false; + _hasOpenedDecentDbMigrationDialog = true; + await OpenDecentDbMigrationDialogAsync(); + } + if (!firstRender) { return; @@ -490,11 +501,15 @@ { _healthIssues = (await DashboardHealthWarningEvaluator.GetIssuesAsync(CurrentUser, DoctorService)).ToArray(); _showHealthWarning = _healthIssues.Length > 0; + _openDecentDbMigrationDialogAfterRender = + !_hasOpenedDecentDbMigrationDialog && + DashboardHealthWarningEvaluator.HasUnsupportedDecentDbIssue(_healthIssues); } catch { _healthIssues = []; _showHealthWarning = false; + _openDecentDbMigrationDialogAfterRender = false; } finally { @@ -723,10 +738,23 @@ _isAdmin = authenticationState.User.IsAdmin(); _healthIssues = (await DashboardHealthWarningEvaluator.GetIssuesAsync(authenticationState.User, DoctorService)).ToArray(); _showHealthWarning = _healthIssues.Length > 0; + _openDecentDbMigrationDialogAfterRender = + !_hasOpenedDecentDbMigrationDialog && + DashboardHealthWarningEvaluator.HasUnsupportedDecentDbIssue(_healthIssues); StateHasChanged(); }); } + private Task OpenDecentDbMigrationDialogAsync() + { + var issues = DashboardHealthWarningEvaluator.GetUnsupportedDecentDbIssues(_healthIssues); + return issues.Count == 0 + ? Task.CompletedTask + : DialogService.OpenDecentDbMigrationDialogAsync( + L("DecentDbMigrationDialog.Title"), + issues); + } + private string FormatHealthIssue(Melodee.Common.Services.Doctor.DoctorCheckResult issue) { var name = LocalizeHealthIssueName(issue.Name); diff --git a/src/Melodee.Blazor/Components/Pages/DashboardHealthWarningEvaluator.cs b/src/Melodee.Blazor/Components/Pages/DashboardHealthWarningEvaluator.cs index c44f6632f..d276729d2 100644 --- a/src/Melodee.Blazor/Components/Pages/DashboardHealthWarningEvaluator.cs +++ b/src/Melodee.Blazor/Components/Pages/DashboardHealthWarningEvaluator.cs @@ -7,8 +7,6 @@ namespace Melodee.Blazor.Components.Pages; internal static class DashboardHealthWarningEvaluator { - public const string DecentDbMigrationGuideUrl = "https://melodee.org/decentdb/"; - public static Task ShouldShowAsync(ClaimsPrincipal? user, BlazorDoctorService doctorService, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(doctorService); @@ -34,11 +32,36 @@ public static bool HasUnsupportedDecentDbIssue(IEnumerable is { ArgumentNullException.ThrowIfNull(issues); - return issues.Any(issue => - IsDecentDbCheck(issue.Name) && - issue.Details.Contains("DecentDB", StringComparison.OrdinalIgnoreCase) && - issue.Details.Contains("file format", StringComparison.OrdinalIgnoreCase) && - issue.Details.Contains("not supported", StringComparison.OrdinalIgnoreCase)); + return issues.Any(IsUnsupportedDecentDbIssue); + } + + public static IReadOnlyList GetUnsupportedDecentDbIssues(IEnumerable issues) + { + ArgumentNullException.ThrowIfNull(issues); + + return issues.Where(IsUnsupportedDecentDbIssue).ToArray(); + } + + private static bool IsUnsupportedDecentDbIssue(DoctorCheckResult issue) + { + if (issue.Success || !IsDecentDbCheck(issue.Name)) + { + return false; + } + + var details = issue.Details; + if (details.Contains("ERR_UNSUPPORTED_FORMAT_VERSION", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + var describesUnsupportedFormat = + details.Contains("unsupported", StringComparison.OrdinalIgnoreCase) || + details.Contains("not supported", StringComparison.OrdinalIgnoreCase); + + return details.Contains("DecentDB", StringComparison.OrdinalIgnoreCase) && + describesUnsupportedFormat && + details.Contains("format", StringComparison.OrdinalIgnoreCase); } private static bool IsDecentDbCheck(string checkName) diff --git a/src/Melodee.Blazor/Extensions/DialogServiceExtensions.cs b/src/Melodee.Blazor/Extensions/DialogServiceExtensions.cs index a213c909b..22acddec7 100644 --- a/src/Melodee.Blazor/Extensions/DialogServiceExtensions.cs +++ b/src/Melodee.Blazor/Extensions/DialogServiceExtensions.cs @@ -1,3 +1,5 @@ +using Melodee.Blazor.Components.Dialogs; +using Melodee.Common.Services.Doctor; using Radzen; namespace Melodee.Blazor.Extensions; @@ -27,5 +29,37 @@ public static class DialogServiceExtensions builder.CloseElement(); }, title, options ?? new ConfirmOptions()); } -} + /// + /// Opens the DecentDB unsupported-format remediation dialog. + /// + /// The dialog service. + /// Localized dialog title. + /// Unsupported-format Doctor results for affected DecentDB databases. + public static async Task OpenDecentDbMigrationDialogAsync( + this DialogService dialogService, + string title, + IReadOnlyList issues) + { + ArgumentNullException.ThrowIfNull(dialogService); + ArgumentException.ThrowIfNullOrWhiteSpace(title); + ArgumentNullException.ThrowIfNull(issues); + + await dialogService.OpenAsync( + title, + new Dictionary + { + { nameof(DecentDbMigrationDialog.Issues), issues.ToArray() } + }, + new DialogOptions + { + Width = "900px", + Height = "auto", + Resizable = true, + Draggable = true, + ShowClose = true, + CloseDialogOnOverlayClick = false, + AriaLabel = title + }); + } +} diff --git a/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx b/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx index a980bc97e..560482945 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ar-SA.resx @@ -6858,7 +6858,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6892,4 +6892,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx b/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx index 97aad1276..002cbd296 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.cs-CZ.resx @@ -6509,7 +6509,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6543,4 +6543,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx b/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx index 928a16d93..7016ea080 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.de-DE.resx @@ -6858,7 +6858,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6892,4 +6892,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx b/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx index 1b0693dff..ebd57aa83 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.es-ES.resx @@ -6858,7 +6858,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6892,4 +6892,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx b/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx index 2389ae959..db51d29d4 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.fa-IR.resx @@ -6509,7 +6509,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6543,4 +6543,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx b/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx index 39ce460bb..a4ee71b7d 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.fr-FR.resx @@ -6858,7 +6858,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6892,4 +6892,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx b/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx index ce455f375..719546cfc 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.id-ID.resx @@ -6509,7 +6509,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6543,4 +6543,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx b/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx index e5c04046f..16e425810 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.it-IT.resx @@ -6858,7 +6858,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6892,4 +6892,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx b/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx index 6f7ba2007..467454dd5 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ja-JP.resx @@ -6858,7 +6858,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6892,4 +6892,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx b/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx index 32b155d60..e66536234 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ko-KR.resx @@ -6509,7 +6509,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6543,4 +6543,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx b/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx index 66e019e00..ecd9256cb 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.nl-NL.resx @@ -6509,7 +6509,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6543,4 +6543,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx b/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx index bd3c854c5..5526dcdde 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.pl-PL.resx @@ -6509,7 +6509,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6543,4 +6543,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx b/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx index e47522f3b..43cf45e2c 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.pt-BR.resx @@ -6858,7 +6858,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6892,4 +6892,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.resx b/src/Melodee.Blazor/Resources/SharedResources.resx index a67429d3d..57f4a6dbd 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.resx @@ -6895,7 +6895,7 @@ Common fixes: - Open DecentDB migration guide + Show DecentDB upgrade steps @@ -6929,4 +6929,132 @@ Common fixes: Apply Import + + + Upgrade DecentDB database + + + + DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + Download decentdb-migrate + + + + Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + Download a DecentDB release + + + + Open the official migration guide + + + + A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + Optional source-build command + + + + Stop Melodee + + + + Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + Create an upgraded copy + + + + Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + Detected error + + + + Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + Configured database path + + + + Migration destination + + + + Migration command + + + + Verify the upgraded copy + + + + Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + Verification command + + + + Do not replace the active database until the migration output ends with this confirmation: + + + + Back up and replace the active file + + + + Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + Backup and replacement script + + + + Restart and confirm + + + + Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + Open the DecentDB source repository + + + + the DecentDB version bundled with Melodee + + + + Copy command + + + + Command copied to the clipboard + + + + If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx b/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx index 46a880684..0a5d8d2ed 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.ru-RU.resx @@ -6858,7 +6858,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6892,4 +6892,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx b/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx index 634cbea35..d0e4f2dc0 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.sv-SE.resx @@ -6509,7 +6509,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6543,4 +6543,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx b/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx index cd851da6a..d0f18fa65 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.tr-TR.resx @@ -6509,7 +6509,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6543,4 +6543,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx b/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx index 49469823a..ef4dc0361 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.uk-UA.resx @@ -6509,7 +6509,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6543,4 +6543,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx b/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx index da0cd4d5a..d1a5de516 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.vi-VN.resx @@ -6509,7 +6509,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6543,4 +6543,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx b/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx index e7a3c2c56..342edab59 100644 --- a/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx +++ b/src/Melodee.Blazor/Resources/SharedResources.zh-CN.resx @@ -6858,7 +6858,7 @@ Common fixes: - [NEEDS TRANSLATION] Open DecentDB migration guide + [NEEDS TRANSLATION] Show DecentDB upgrade steps @@ -6892,4 +6892,132 @@ Common fixes: [NEEDS TRANSLATION] Apply Import + + + [NEEDS TRANSLATION] Upgrade DecentDB database + + + + [NEEDS TRANSLATION] DecentDB error 8 (Unsupported database format) means Melodee cannot open one or more local DecentDB files. Follow these steps to create an upgraded copy and safely replace the incompatible file. + + + + [NEEDS TRANSLATION] Download decentdb-migrate + + + + [NEEDS TRANSLATION] Download and extract the release archive for {0} and this server's operating system and architecture. The release archive includes decentdb-migrate. + + + + [NEEDS TRANSLATION] Download a DecentDB release + + + + [NEEDS TRANSLATION] Open the official migration guide + + + + [NEEDS TRANSLATION] A prebuilt release is recommended. If you prefer to build from source, run this command from the DecentDB repository root; the executable will be written to target/release/decentdb-migrate: + + + + [NEEDS TRANSLATION] Optional source-build command + + + + [NEEDS TRANSLATION] Stop Melodee + + + + [NEEDS TRANSLATION] Stop every Melodee instance or process that can access the affected database before continuing. For a container deployment, run the tool where the displayed path resolves to the same mounted file, or substitute the host volume path. + + + + [NEEDS TRANSLATION] Create an upgraded copy + + + + [NEEDS TRANSLATION] Open a terminal in the extracted release directory and run each migration command below. decentdb-migrate reads the source and writes a new destination; the destination file and its .wal sidecar must not already exist. + + + + [NEEDS TRANSLATION] Detected error + + + + [NEEDS TRANSLATION] Melodee could not read a Data Source path from this database's connection string. Open Admin > Doctor, find the connection string path, and substitute that path in the official migration command. + + + + [NEEDS TRANSLATION] Configured database path + + + + [NEEDS TRANSLATION] Migration destination + + + + [NEEDS TRANSLATION] Migration command + + + + [NEEDS TRANSLATION] Verify the upgraded copy + + + + [NEEDS TRANSLATION] Confirm that the migration reports success, then open the new file with the decentdb executable from the same release. + + + + [NEEDS TRANSLATION] Verification command + + + + [NEEDS TRANSLATION] Do not replace the active database until the migration output ends with this confirmation: + + + + [NEEDS TRANSLATION] Back up and replace the active file + + + + [NEEDS TRANSLATION] Keep Melodee stopped. This script moves the original database and its DecentDB sidecars into a timestamped backup directory, promotes the migrated database to the configured path, and carries forward the migrated WAL when present. + + + + [NEEDS TRANSLATION] Backup and replacement script + + + + [NEEDS TRANSLATION] Restart and confirm + + + + [NEEDS TRANSLATION] Restart Melodee, open Admin > Doctor, and confirm the MusicBrainzDatabase and ArtistSearchEngineDatabase checks pass. Keep the timestamped backup until normal operation is verified. + + + + [NEEDS TRANSLATION] Open the DecentDB source repository + + + + [NEEDS TRANSLATION] the DecentDB version bundled with Melodee + + + + [NEEDS TRANSLATION] Copy command + + + + [NEEDS TRANSLATION] Command copied to the clipboard + + + + [NEEDS TRANSLATION] If decentdb-migrate reports that the source format is unsupported or newer than its target format, do not replace the active file. Upgrade Melodee and DecentDB to a compatible version or rebuild the affected generated database instead. + + + + [NEEDS TRANSLATION] DecentDB v{0} + diff --git a/src/Melodee.Blazor/Services/DoctorService.cs b/src/Melodee.Blazor/Services/DoctorService.cs index 1821a8984..2685a31e5 100644 --- a/src/Melodee.Blazor/Services/DoctorService.cs +++ b/src/Melodee.Blazor/Services/DoctorService.cs @@ -1,5 +1,6 @@ using System.Data.Common; using System.Diagnostics; +using DecentDB.Native; using Melodee.Common.Configuration; using Melodee.Common.Constants; using Melodee.Common.Data; @@ -58,6 +59,7 @@ public sealed class DoctorService( private const long MemoryPressureWarningBytes = 500L * 1024 * 1024; // 500 MB private const long MemoryPressureCriticalBytes = 100L * 1024 * 1024; // 100 MB available private const int JobStalenessHours = 48; // Jobs should run within this period + private const int DecentDbUnsupportedFormatErrorCode = 8; public async Task NeedsAttentionAsync(CancellationToken cancellationToken = default) { @@ -274,7 +276,9 @@ private async Task HasLibraryPathIssuesAsync(CancellationToken cancellatio { var details = string.IsNullOrWhiteSpace(error) ? $"Unable to query; {fileInfo}" - : $"{error}; {fileInfo}"; + : IsUnsupportedDecentDbFormat(error) + ? FormatDecentDbOpenFailure("MusicBrainz", error, fileInfo) + : $"{error}; {fileInfo}"; return (new DoctorCheckResult("MusicBrainzDatabase", false, details, sw.Elapsed), true); } @@ -323,7 +327,9 @@ private async Task RunArtistSearchEngineDbCheckAsync(Cancella ? $"OK; {fileInfo}" : string.IsNullOrWhiteSpace(error) ? $"Unable to query; {fileInfo}" - : $"{error}; {fileInfo}"; + : IsUnsupportedDecentDbFormat(error) + ? FormatDecentDbOpenFailure("ArtistSearch", error, fileInfo) + : $"{error}; {fileInfo}"; return new DoctorCheckResult("ArtistSearchEngineDatabase", canQuery, details, sw.Elapsed); } @@ -589,18 +595,53 @@ private async Task RunArtistSearchAttentionCheckAsync(Cancell private static string FormatDecentDbOpenFailure(string databaseName, Exception exception, string fileInfo) { var message = FirstMessageLine(exception); - if (IsUnsupportedDecentDbFormat(message)) + if (IsUnsupportedDecentDbFormat(exception)) { - return $"{databaseName} DecentDB database uses a file format that is not supported by the current DecentDB provider. Rebuild the database or upgrade Melodee/DecentDB. Provider error: {message}; {fileInfo}"; + return FormatDecentDbOpenFailure(databaseName, message, fileInfo); } return $"Unable to open {databaseName} DecentDB database: {message}; {fileInfo}"; } - private static bool IsUnsupportedDecentDbFormat(string message) + private static string FormatDecentDbOpenFailure(string databaseName, string message, string fileInfo) { - return message.Contains("unsupported", StringComparison.OrdinalIgnoreCase) && - message.Contains("format", StringComparison.OrdinalIgnoreCase); + return $"{databaseName} DecentDB database uses a file format that is not supported by the current DecentDB provider. Run decentdb-migrate to upgrade the database. Provider error: {FirstMessageLine(message)}; {fileInfo}"; + } + + private static bool IsUnsupportedDecentDbFormat(Exception exception) + { + for (Exception? current = exception; current is not null; current = current.InnerException) + { + if (current is DecentDBException { ErrorCode: DecentDbUnsupportedFormatErrorCode }) + { + return true; + } + + if (current is DecentDBException decentDbException && + string.Equals( + decentDbException.Diagnostic?.Subcode, + "format.unsupported_version", + StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (IsUnsupportedDecentDbFormat(current.Message)) + { + return true; + } + } + + return false; + } + + private static bool IsUnsupportedDecentDbFormat(string? message) + { + return !string.IsNullOrWhiteSpace(message) && + (message.Contains("ERR_UNSUPPORTED_FORMAT_VERSION", StringComparison.OrdinalIgnoreCase) || + ((message.Contains("unsupported", StringComparison.OrdinalIgnoreCase) || + message.Contains("not supported", StringComparison.OrdinalIgnoreCase)) && + message.Contains("format", StringComparison.OrdinalIgnoreCase))); } private static string FirstMessageLine(Exception exception) @@ -608,7 +649,12 @@ private static string FirstMessageLine(Exception exception) var message = exception.GetBaseException().Message; return string.IsNullOrWhiteSpace(message) ? exception.GetType().Name - : message.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? message; + : FirstMessageLine(message); + } + + private static string FirstMessageLine(string message) + { + return message.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? message; } private async Task HasMusicBrainzConnectionIssuesAsync(CancellationToken cancellationToken) diff --git a/tests/Melodee.Tests.Blazor/Components/DashboardHealthWarningEvaluatorTests.cs b/tests/Melodee.Tests.Blazor/Components/DashboardHealthWarningEvaluatorTests.cs index badff8496..1124d3500 100644 --- a/tests/Melodee.Tests.Blazor/Components/DashboardHealthWarningEvaluatorTests.cs +++ b/tests/Melodee.Tests.Blazor/Components/DashboardHealthWarningEvaluatorTests.cs @@ -92,6 +92,51 @@ public void HasUnsupportedDecentDbIssue_WhenMusicBrainzUnsupportedFormat_Returns result.Should().BeTrue(); } + [Fact] + public void HasUnsupportedDecentDbIssue_WhenProviderReturnsError8_ReturnsTrue() + { + var issues = new[] + { + new DoctorCheckResult( + "MusicBrainzDatabase", + false, + "DecentDB error 8: Unsupported database format version: 13", + TimeSpan.Zero) + }; + + var result = DashboardHealthWarningEvaluator.HasUnsupportedDecentDbIssue(issues); + + result.Should().BeTrue(); + } + + [Fact] + public void GetUnsupportedDecentDbIssues_WithMixedResults_ReturnsOnlyAffectedDatabases() + { + var unsupportedMusicBrainz = new DoctorCheckResult( + "MusicBrainzDatabase", + false, + "DecentDB error 8: Unsupported database format version: 13", + TimeSpan.Zero); + var issues = new[] + { + unsupportedMusicBrainz, + new DoctorCheckResult( + "ArtistSearchEngineDatabase", + true, + "DecentDB error 8 from an earlier probe", + TimeSpan.Zero), + new DoctorCheckResult( + "PostgresDatabase", + false, + "ERR_UNSUPPORTED_FORMAT_VERSION", + TimeSpan.Zero) + }; + + var result = DashboardHealthWarningEvaluator.GetUnsupportedDecentDbIssues(issues); + + result.Should().ContainSingle().Which.Should().Be(unsupportedMusicBrainz); + } + [Fact] public void HasUnsupportedDecentDbIssue_WhenDifferentDoctorIssue_ReturnsFalse() { diff --git a/tests/Melodee.Tests.Blazor/Components/DecentDbMigrationInstructionsBuilderTests.cs b/tests/Melodee.Tests.Blazor/Components/DecentDbMigrationInstructionsBuilderTests.cs new file mode 100644 index 000000000..1addcb3dc --- /dev/null +++ b/tests/Melodee.Tests.Blazor/Components/DecentDbMigrationInstructionsBuilderTests.cs @@ -0,0 +1,145 @@ +using FluentAssertions; +using Melodee.Blazor.Components.Dialogs; +using Melodee.Common.Services.Doctor; +using Microsoft.Extensions.Configuration; + +namespace Melodee.Tests.Blazor.Components; + +public class DecentDbMigrationInstructionsBuilderTests +{ + [Fact] + public void Build_WithMusicBrainzError8_CreatesCommandsForConfiguredDatabase() + { + var databasePath = Path.GetFullPath(Path.Combine("search engine", "musicbrainz.ddb")); + var configuration = CreateConfiguration(new Dictionary + { + ["ConnectionStrings:MusicBrainzConnection"] = $"Data Source={databasePath}" + }); + var issues = new[] + { + new DoctorCheckResult( + "MusicBrainzDatabase", + false, + "DecentDB error 8: Unsupported database format version: 13", + TimeSpan.Zero) + }; + + var targets = DecentDbMigrationInstructionsBuilder.Build(issues, configuration, isWindows: false); + + var target = targets.Should().ContainSingle().Which; + var migratedPath = Path.Combine( + Path.GetDirectoryName(databasePath)!, + "musicbrainz_migrated.ddb"); + target.DatabasePath.Should().Be(databasePath); + target.MigratedDatabasePath.Should().Be(migratedPath); + target.MigrationCommand.Should().Be( + $"./decentdb-migrate --source '{databasePath}' --dest '{migratedPath}'"); + target.VerificationCommand.Should().Be($"./decentdb info --db '{migratedPath}'"); + target.ReplacementCommand.Should().StartWith("set -euo pipefail"); + target.ReplacementCommand.Should().Contain(".wal .coord .wal-idx"); + target.ReplacementCommand.Should().Contain("pre-migration-$(date +%Y%m%d-%H%M%S)"); + } + + [Fact] + public void Build_WithApostropheInPath_QuotesPosixCommandSafely() + { + var databasePath = Path.GetFullPath(Path.Combine("administrator's music", "artist search.ddb")); + var configuration = CreateConfiguration(new Dictionary + { + ["ConnectionStrings:ArtistSearchEngineConnection"] = $"Data Source={databasePath}" + }); + var issues = new[] + { + new DoctorCheckResult( + "ArtistSearchEngineDatabase", + false, + "ERR_UNSUPPORTED_FORMAT_VERSION", + TimeSpan.Zero) + }; + + var targets = DecentDbMigrationInstructionsBuilder.Build(issues, configuration, isWindows: false); + + targets.Should().ContainSingle() + .Which.MigrationCommand.Should().Contain("administrator'\"'\"'s music"); + } + + [Fact] + public void Build_ForWindows_CreatesPowerShellCommands() + { + var databasePath = Path.GetFullPath(Path.Combine("search", "musicbrainz.ddb")); + var configuration = CreateConfiguration(new Dictionary + { + ["ConnectionStrings:MusicBrainzConnection"] = $"Data Source={databasePath}" + }); + var issues = new[] + { + new DoctorCheckResult( + "MusicBrainzDatabase", + false, + "DecentDB error 8: Unsupported database format", + TimeSpan.Zero) + }; + + var targets = DecentDbMigrationInstructionsBuilder.Build(issues, configuration, isWindows: true); + + var target = targets.Should().ContainSingle().Which; + target.MigrationCommand.Should().StartWith(@".\decentdb-migrate.exe --source '"); + target.VerificationCommand.Should().StartWith(@".\decentdb.exe info --db '"); + target.ReplacementCommand.Should().StartWith("$ErrorActionPreference = 'Stop'"); + target.ReplacementCommand.Should().Contain("Move-Item -LiteralPath $migratedDb -Destination $sourceDb"); + } + + [Fact] + public void QuotePowerShellArgument_WithApostrophe_EscapesLiteralPath() + { + var result = DecentDbMigrationInstructionsBuilder.QuotePowerShellArgument(@"C:\Admin's Music\musicbrainz.ddb"); + + result.Should().Be(@"'C:\Admin''s Music\musicbrainz.ddb'"); + } + + [Fact] + public void Build_WithoutDataSource_KeepsErrorVisibleWithoutInventingAPath() + { + var configuration = CreateConfiguration(new Dictionary()); + var issues = new[] + { + new DoctorCheckResult( + "MusicBrainzDatabase", + false, + "DecentDB error 8: Unsupported database format", + TimeSpan.Zero) + }; + + var targets = DecentDbMigrationInstructionsBuilder.Build(issues, configuration, isWindows: false); + + var target = targets.Should().ContainSingle().Which; + target.DatabasePath.Should().BeNull(); + target.MigrationCommand.Should().BeNull(); + target.ErrorDetails.Should().Contain("error 8"); + } + + [Fact] + public void Build_WithUnrelatedDoctorIssue_ReturnsNoTargets() + { + var configuration = CreateConfiguration(new Dictionary()); + var issues = new[] + { + new DoctorCheckResult( + "PostgresDatabase", + false, + "Unsupported database format", + TimeSpan.Zero) + }; + + var targets = DecentDbMigrationInstructionsBuilder.Build(issues, configuration, isWindows: false); + + targets.Should().BeEmpty(); + } + + private static IConfiguration CreateConfiguration(Dictionary values) + { + return new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + } +} diff --git a/tests/Melodee.Tests.Blazor/Services/DoctorServiceTests.cs b/tests/Melodee.Tests.Blazor/Services/DoctorServiceTests.cs index 9ce653221..99dc7ad9a 100644 --- a/tests/Melodee.Tests.Blazor/Services/DoctorServiceTests.cs +++ b/tests/Melodee.Tests.Blazor/Services/DoctorServiceTests.cs @@ -1,3 +1,4 @@ +using DecentDB.Native; using Melodee.Common.Data; using Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData; using Melodee.Common.Plugins.SearchEngine.MusicBrainz.Data; @@ -156,7 +157,12 @@ public async Task GetAttentionChecksAsync_WhenMusicBrainzDecentDbFormatUnsupport _musicBrainzDbContextFactory .Setup(x => x.CreateDbContextAsync(It.IsAny())) - .ThrowsAsync(new InvalidOperationException("unsupported DecentDB file format version 11")); + .ThrowsAsync(new InvalidOperationException( + "Failed to open database", + new DecentDBException( + 8, + "Unsupported database format version: 13", + "Open(ReadOnly)"))); var configuration = CreateConfiguration(new Dictionary { @@ -171,7 +177,8 @@ public async Task GetAttentionChecksAsync_WhenMusicBrainzDecentDbFormatUnsupport var musicBrainzCheck = Assert.Single(results, x => x.Name == "MusicBrainzDatabase"); Assert.False(musicBrainzCheck.Success); Assert.Contains("not supported by the current DecentDB provider", musicBrainzCheck.Details); - Assert.Contains("unsupported DecentDB file format version 11", musicBrainzCheck.Details); + Assert.Contains("Run decentdb-migrate to upgrade the database", musicBrainzCheck.Details); + Assert.Contains("DecentDB error 8: Unsupported database format version: 13", musicBrainzCheck.Details); } finally { @@ -505,6 +512,7 @@ public async Task RunAllChecksAsync_WhenArtistSearchDatabaseCannotBeQueried_Syst var artistSearchCheck = results.Checks.Single(x => x.Name == "ArtistSearchEngineDatabase"); Assert.False(artistSearchCheck.Success); + Assert.Contains("Run decentdb-migrate to upgrade the database", artistSearchCheck.Details); Assert.Contains("unsupported database format version: 3", artistSearchCheck.Details); } finally @@ -677,6 +685,8 @@ private static void DeleteDatabaseArtifacts(string path) { DeleteFileIfExists(path); DeleteFileIfExists($"{path}.wal"); + DeleteFileIfExists($"{path}.coord"); + DeleteFileIfExists($"{path}.wal-idx"); DeleteFileIfExists($"{path}-wal"); DeleteFileIfExists($"{path}.shm"); DeleteFileIfExists($"{path}-shm"); From e43b75001f6821906e0f41fb630b2c599e0cfd63 Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Sat, 11 Jul 2026 09:09:06 -0500 Subject: [PATCH 09/11] chore(deps): bump `SkiaSharp.NativeAssets.Linux` to v4.150.0 --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f7ec44592..7bd95baba 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -97,7 +97,7 @@ - + @@ -119,4 +119,4 @@ - \ No newline at end of file + From 27019028191811c0253ad1cf39615882ee8d5c9e Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Sat, 11 Jul 2026 09:26:32 -0500 Subject: [PATCH 10/11] refactor(codebase): enhance param documentation, adjust deps, and add OpenAPI support - Improved parameter XML documentation across multiple models and services (e.g., `Child`, `ScrobbleInfo`, `FilterOperatorInfo`, `ArtistReadModel`, `Directory`). - Adjusted `ImageProcessor` to use a default sampling option for bitmap rendering. - Updated `.editorconfig` to handle specific migration files and enforce newline consistency. - Added `Microsoft.OpenApi` dependencies to support OpenAPI features in `Melodee.Blazor`. - Enabled XML documentation file generation and suppressed `CS1591` warnings. - Corrected minor typos in comments and refined string formatting for clarity. --- .editorconfig | 9 +++++++++ Directory.Build.props | 2 ++ Directory.Packages.props | 1 + src/Melodee.Blazor/Melodee.Blazor.csproj | 1 + src/Melodee.Common/Extensions/StringExtensions.cs | 2 +- src/Melodee.Common/Filtering/FilterOperatorInfo.cs | 1 + src/Melodee.Common/Imaging/ImageHasher.cs | 4 ++-- src/Melodee.Common/Imaging/ImageProcessor.cs | 2 +- src/Melodee.Common/Models/OpenSubsonic/Artist.cs | 1 + src/Melodee.Common/Models/OpenSubsonic/Child.cs | 6 ++++++ src/Melodee.Common/Models/OpenSubsonic/Directory.cs | 2 +- .../Models/OpenSubsonic/Requests/ApiRequest.cs | 3 +-- src/Melodee.Common/Models/Scrobbling/ScrobbleInfo.cs | 8 ++++++++ src/Melodee.Common/Services/MediaArtistExportService.cs | 2 +- src/Melodee.Common/Services/MediaArtistImportService.cs | 2 +- .../Services/Models/ArtistDuplicate/ArtistReadModel.cs | 2 +- src/Melodee.Common/Services/PlaylistService.cs | 7 +++++++ src/Melodee.Common/Services/StreamingLimiter.cs | 3 +-- 18 files changed, 46 insertions(+), 12 deletions(-) diff --git a/.editorconfig b/.editorconfig index 5991eed83..50f409892 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,6 +23,15 @@ indent_size = 2 indent_size = 2 generated_code = true +# Preserve immutable generated migrations with their original byte encoding. +[src/Melodee.Common/Migrations/ArtistSearchEngine/20260622224939_InitialArtistSearchEngineSchema.cs] +charset = utf-8-bom +insert_final_newline = false + +[src/Melodee.Common/Migrations/ArtistSearchEngine/20260622225002_SyncMusicBrainzUuidColumns.cs] +charset = utf-8-bom +insert_final_newline = false + # JSON files [*.{json,json5}] indent_size = 2 diff --git a/Directory.Build.props b/Directory.Build.props index 9949ef02c..865ffc7fc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,7 @@ true + true + $(NoWarn);CS1591 diff --git a/Directory.Packages.props b/Directory.Packages.props index 7bd95baba..f45c36ba2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -67,6 +67,7 @@ + diff --git a/src/Melodee.Blazor/Melodee.Blazor.csproj b/src/Melodee.Blazor/Melodee.Blazor.csproj index ba78a34b2..cc0403cf4 100644 --- a/src/Melodee.Blazor/Melodee.Blazor.csproj +++ b/src/Melodee.Blazor/Melodee.Blazor.csproj @@ -61,6 +61,7 @@ + diff --git a/src/Melodee.Common/Extensions/StringExtensions.cs b/src/Melodee.Common/Extensions/StringExtensions.cs index c0f6ad368..d3c0fe928 100644 --- a/src/Melodee.Common/Extensions/StringExtensions.cs +++ b/src/Melodee.Common/Extensions/StringExtensions.cs @@ -217,7 +217,7 @@ private static string UpdateIrish(string nameString) } /// - /// Updates irish Mac & Mc. + /// Updates Irish Mac and Mc name prefixes. /// /// /// diff --git a/src/Melodee.Common/Filtering/FilterOperatorInfo.cs b/src/Melodee.Common/Filtering/FilterOperatorInfo.cs index 611fe4ec6..09e4ba57d 100644 --- a/src/Melodee.Common/Filtering/FilterOperatorInfo.cs +++ b/src/Melodee.Common/Filtering/FilterOperatorInfo.cs @@ -10,6 +10,7 @@ namespace Melodee.Common.Filtering; /// Value to filter on. /// The Join condition when more than one filter, e.g. 'OR' or 'AND' /// Optional column name to use when building SQL +/// Optional operator text to use instead of the mapped filter operator. public record FilterOperatorInfo( string PropertyName, FilterOperator Operator, diff --git a/src/Melodee.Common/Imaging/ImageHasher.cs b/src/Melodee.Common/Imaging/ImageHasher.cs index 29777ca9f..051734bf9 100644 --- a/src/Melodee.Common/Imaging/ImageHasher.cs +++ b/src/Melodee.Common/Imaging/ImageHasher.cs @@ -5,8 +5,8 @@ namespace Melodee.Common.Imaging; /// and recognition. /// Credit for the AverageHash implementation to David Oftedal of the University of Oslo. /// -/// NOTE: This class is now a thin wrapper around . -/// Prefer injecting directly for new code. +/// NOTE: This class is now a thin wrapper around . +/// Prefer injecting directly for new code. /// public static class ImageHasher { diff --git a/src/Melodee.Common/Imaging/ImageProcessor.cs b/src/Melodee.Common/Imaging/ImageProcessor.cs index 29bf46380..e5a3f02a3 100644 --- a/src/Melodee.Common/Imaging/ImageProcessor.cs +++ b/src/Melodee.Common/Imaging/ImageProcessor.cs @@ -184,7 +184,7 @@ public byte[] ResizeAndPadToSquare(ReadOnlyMemory imageBytes, int targetSi var destRect = new SKRectI(x, y, x + newWidth, y + newHeight); var srcRect = new SKRectI(0, 0, sourceBitmap.Width, sourceBitmap.Height); - canvas.DrawBitmap(sourceBitmap, srcRect, destRect); + canvas.DrawBitmap(sourceBitmap, srcRect, destRect, SKSamplingOptions.Default); canvas.Flush(); using var image = surface.Snapshot(); diff --git a/src/Melodee.Common/Models/OpenSubsonic/Artist.cs b/src/Melodee.Common/Models/OpenSubsonic/Artist.cs index bb98325ce..23b6b89a9 100644 --- a/src/Melodee.Common/Models/OpenSubsonic/Artist.cs +++ b/src/Melodee.Common/Models/OpenSubsonic/Artist.cs @@ -14,6 +14,7 @@ namespace Melodee.Common.Models.OpenSubsonic; /// Artist album count /// Timestamp when user starred artist /// Artist image url +/// Albums associated with the artist. public record Artist( string Id, string Name, diff --git a/src/Melodee.Common/Models/OpenSubsonic/Child.cs b/src/Melodee.Common/Models/OpenSubsonic/Child.cs index be08ef519..fb8b437e6 100644 --- a/src/Melodee.Common/Models/OpenSubsonic/Child.cs +++ b/src/Melodee.Common/Models/OpenSubsonic/Child.cs @@ -46,6 +46,12 @@ namespace Melodee.Common.Models.OpenSubsonic; /// /// /// +/// +/// +/// +/// +/// +/// public record Child( string Id, string? Parent, diff --git a/src/Melodee.Common/Models/OpenSubsonic/Directory.cs b/src/Melodee.Common/Models/OpenSubsonic/Directory.cs index 2b55e7bfb..e68f61bf1 100644 --- a/src/Melodee.Common/Models/OpenSubsonic/Directory.cs +++ b/src/Melodee.Common/Models/OpenSubsonic/Directory.cs @@ -16,7 +16,7 @@ namespace Melodee.Common.Models.OpenSubsonic; /// The user rating [1-5] /// The average rating [1.0-5.0] /// The play count -/// Last played date [ISO 8601] +/// Last played date [ISO 8601] /// The directory content (can be an album, can be a song) public record Directory( string Id, diff --git a/src/Melodee.Common/Models/OpenSubsonic/Requests/ApiRequest.cs b/src/Melodee.Common/Models/OpenSubsonic/Requests/ApiRequest.cs index 3ab739382..f3b092c22 100644 --- a/src/Melodee.Common/Models/OpenSubsonic/Requests/ApiRequest.cs +++ b/src/Melodee.Common/Models/OpenSubsonic/Requests/ApiRequest.cs @@ -5,9 +5,8 @@ namespace Melodee.Common.Models.OpenSubsonic.Requests; /// /// Setup from Query/Post parameters for the Subsonic request. /// -/// S /// All request headers for request. -/// If false then an internal request from the Blazor UI else an API call. +/// If false then an internal request from the Blazor UI else an API call. /// (u) The username. /// /// (v) The protocol version implemented by the client, i.e., the version of the diff --git a/src/Melodee.Common/Models/Scrobbling/ScrobbleInfo.cs b/src/Melodee.Common/Models/Scrobbling/ScrobbleInfo.cs index 01fd0b512..05623fce8 100644 --- a/src/Melodee.Common/Models/Scrobbling/ScrobbleInfo.cs +++ b/src/Melodee.Common/Models/Scrobbling/ScrobbleInfo.cs @@ -6,6 +6,9 @@ namespace Melodee.Common.Models.Scrobbling; /// Scrobble Info /// /// The ApiKey of the Song to scrobble. +/// The internal artist identifier. +/// The internal album identifier. +/// The internal song identifier. /// The song name. /// The album artist name /// Flag if the user selected the song or played via some random operation. @@ -14,6 +17,11 @@ namespace Melodee.Common.Models.Scrobbling; /// The MusicBrainz Track ID /// The song number of the song on the album. /// The album artist - if this differs from the song artist. +/// When the scrobble was created. +/// The player that submitted the scrobble. +/// The client user-agent value. +/// The client IP address. +/// The number of seconds played. public record ScrobbleInfo( Guid SongApiKey, int ArtistId, diff --git a/src/Melodee.Common/Services/MediaArtistExportService.cs b/src/Melodee.Common/Services/MediaArtistExportService.cs index 50eddd281..62f43f9ce 100644 --- a/src/Melodee.Common/Services/MediaArtistExportService.cs +++ b/src/Melodee.Common/Services/MediaArtistExportService.cs @@ -205,4 +205,4 @@ public sealed class MediaArtistExportResult public int ArtistsCount { get; init; } public int AlbumsCount { get; init; } public int AliasesCount { get; init; } -} \ No newline at end of file +} diff --git a/src/Melodee.Common/Services/MediaArtistImportService.cs b/src/Melodee.Common/Services/MediaArtistImportService.cs index ea0adab74..f14b8b41e 100644 --- a/src/Melodee.Common/Services/MediaArtistImportService.cs +++ b/src/Melodee.Common/Services/MediaArtistImportService.cs @@ -292,4 +292,4 @@ public MediaArtistImportResult WithError(string error) ErrorMessage = error; return this; } -} \ No newline at end of file +} diff --git a/src/Melodee.Common/Services/Models/ArtistDuplicate/ArtistReadModel.cs b/src/Melodee.Common/Services/Models/ArtistDuplicate/ArtistReadModel.cs index 4fb146318..1623dc85a 100644 --- a/src/Melodee.Common/Services/Models/ArtistDuplicate/ArtistReadModel.cs +++ b/src/Melodee.Common/Services/Models/ArtistDuplicate/ArtistReadModel.cs @@ -20,7 +20,7 @@ namespace Melodee.Common.Services.Models.ArtistDuplicate; /// External AMG ID. /// External WikiData ID. /// Lightweight album stubs for album overlap calculation. -/// When the artist was created in the database. +/// The database creation timestamp represented as ticks. public sealed record ArtistReadModel( int ArtistId, Guid ApiKey, diff --git a/src/Melodee.Common/Services/PlaylistService.cs b/src/Melodee.Common/Services/PlaylistService.cs index c61c5fc05..38ca2c034 100644 --- a/src/Melodee.Common/Services/PlaylistService.cs +++ b/src/Melodee.Common/Services/PlaylistService.cs @@ -1241,7 +1241,14 @@ public async Task> DeleteByApiKeyAsync( /// /// Create a new playlist with songs /// + /// The playlist name. + /// The identifier of the user who owns the playlist. + /// An optional playlist comment. + /// Whether the playlist is publicly visible. + /// Optional API keys of songs to add to the playlist. /// When true, returns "playlist|{guid}" format for OpenSubsonic API; when false, returns raw GUID string for Melodee API. + /// A token used to cancel the operation. + /// The created playlist API key when successful. public async Task> CreatePlaylistAsync( string name, int userId, diff --git a/src/Melodee.Common/Services/StreamingLimiter.cs b/src/Melodee.Common/Services/StreamingLimiter.cs index 3ad731e99..0e29220c5 100644 --- a/src/Melodee.Common/Services/StreamingLimiter.cs +++ b/src/Melodee.Common/Services/StreamingLimiter.cs @@ -6,7 +6,7 @@ namespace Melodee.Common.Services; /// /// Simple in-memory limiter for concurrent streaming to prevent resource exhaustion. -/// Limits are configurable via SettingRegistry and default to unlimited when unset or <= 0. +/// Limits are configurable via SettingRegistry and default to unlimited when unset or <= 0. /// public class StreamingLimiter { @@ -86,4 +86,3 @@ public void Exit(string userKey) } } } - From 17dacedd9544a6950f64cfc6c351476321e59109 Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Sat, 11 Jul 2026 09:47:08 -0500 Subject: [PATCH 11/11] feat(versioning): bump to v2.2.0, enhance changelog automation, and improve ArtistSearch initialization - Updated to version 2.2.0 across all projects and documentation. - Adjusted changelog logic to prevent relocation of the `[Unreleased]` section above Jekyll front matter. - Enhanced ArtistSearch initialization to support relational migrations and schema creation for non-relational providers. --- docs/VERSION | 2 +- docs/pages/changelog.md | 32 +++++++++++++++++++ scripts/bump_version.sh | 10 ++---- src/Melodee.Blazor/Melodee.Blazor.csproj | 2 +- src/Melodee.Cli/Melodee.Cli.csproj | 2 +- src/Melodee.Common/Melodee.Common.csproj | 2 +- .../ArtistSearchEngineService.cs | 10 +++++- src/Melodee.Mql/Melodee.Mql.csproj | 2 +- .../ArtistSearchEngineServiceTests.cs | 28 ++++++++++++++-- 9 files changed, 75 insertions(+), 15 deletions(-) diff --git a/docs/VERSION b/docs/VERSION index 7d2ed7c70..ccbccc3dc 100644 --- a/docs/VERSION +++ b/docs/VERSION @@ -1 +1 @@ -2.1.4 +2.2.0 diff --git a/docs/pages/changelog.md b/docs/pages/changelog.md index 895568419..370938a1a 100644 --- a/docs/pages/changelog.md +++ b/docs/pages/changelog.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.2.0] - 2026-07-11 + ### Added - Admin health checks now recognize DecentDB error 8 and automatically open a @@ -46,6 +48,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 stores them as `BLOB`, and `ALTER COLUMN TYPE` to `UUID` is unsupported by DecentDB (only `INT64`, `FLOAT64`, `TEXT`, `BOOL`). The no-op migration records itself as applied without attempting the unsupported `ALTER`, keeping `MigrateAsync` stable. +- Updated the DecentDB providers to `2.16.1`, SkiaSharp managed and Linux native + assets to `4.150.0`, Radzen to `11.1.3`, and related runtime, UI, test, and + tooling dependencies. +- Chart editing, user, user-group, and podcast detail routes now bind public + GUID API keys directly instead of internal numeric IDs or custom string + parsing. +- Enabled solution-wide XML documentation output so build-time unused-using + analysis runs consistently, and corrected malformed API documentation and + formatter configuration. ### Fixed @@ -57,6 +68,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ArtistSearch database is now auto-created on first use when the `.ddb` file is absent, eliminating the "database is empty or not initialized" error that previously required manual intervention. +- Paged ArtistSearch queries now retrieve the requested page before counting + results, preventing DecentDB positional parameters from carrying across + commands and causing missing-parameter failures. +- Chart edit navigation now uses the chart API key, and missing chart album + matching is accent-insensitive. +- Aligned the SkiaSharp managed and Linux native packages and moved image + drawing to the supported sampling API, preventing native `119.0` and managed + `150.0` incompatibility failures during image processing. +- Solution Debug and CI analyzer builds now complete without dependency, + obsolete API, or XML documentation warnings. +- Version bump automation now promotes release notes without moving the fresh + Unreleased heading above the changelog's Jekyll front matter. +- ArtistSearch initialization now uses migrations for relational databases and + schema creation for non-relational providers, preventing OpenSubsonic + `getTopSongs` failures in in-memory hosts. + +### Security + +- Pinned `Microsoft.OpenApi` to patched version `2.7.5`, preventing crafted + circular schema references from terminating the process during OpenAPI + document parsing. ## [2.1.4] - 2026-06-16 diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index 482a71425..00d23c424 100755 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -149,13 +149,9 @@ if [[ -f "$CHANGELOG" ]]; then # Check if [Unreleased] section exists if grep -q "^## \[Unreleased\]" "$CHANGELOG"; then - # Replace [Unreleased] header with versioned header + date - sed -i "s|^## \[Unreleased\]|## [${VERSION}] - ${today}|" "$CHANGELOG" - - # Insert a fresh [Unreleased] section before the new version header - sed -i "1i\\ -## [Unreleased]\\ -" "$CHANGELOG" + # Keep the fresh Unreleased section at the existing content location so + # Jekyll front matter remains the first block in the document. + sed -i "s|^## \[Unreleased\]|## [Unreleased]\\n\\n## [${VERSION}] - ${today}|" "$CHANGELOG" log_update "$CHANGELOG" "promoted [Unreleased] → [$VERSION] - $today" else diff --git a/src/Melodee.Blazor/Melodee.Blazor.csproj b/src/Melodee.Blazor/Melodee.Blazor.csproj index cc0403cf4..d7476f67d 100644 --- a/src/Melodee.Blazor/Melodee.Blazor.csproj +++ b/src/Melodee.Blazor/Melodee.Blazor.csproj @@ -16,7 +16,7 @@ - 2.1.4 + 2.2.0 build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) $(VersionPrefix).0 diff --git a/src/Melodee.Cli/Melodee.Cli.csproj b/src/Melodee.Cli/Melodee.Cli.csproj index 2123d6235..2d517d7b0 100644 --- a/src/Melodee.Cli/Melodee.Cli.csproj +++ b/src/Melodee.Cli/Melodee.Cli.csproj @@ -9,7 +9,7 @@ false $(NoWarn);NU1507 - 2.1.4 + 2.2.0 build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) $(VersionPrefix).0 diff --git a/src/Melodee.Common/Melodee.Common.csproj b/src/Melodee.Common/Melodee.Common.csproj index c3e66b2fc..60cbc13c1 100644 --- a/src/Melodee.Common/Melodee.Common.csproj +++ b/src/Melodee.Common/Melodee.Common.csproj @@ -9,7 +9,7 @@ $(NoWarn);NU1507 - 2.1.4 + 2.2.0 build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) $(VersionPrefix).0 $(VersionPrefix).0 diff --git a/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs b/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs index b8c3d3dbf..8686c857b 100644 --- a/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs +++ b/src/Melodee.Common/Services/SearchEngines/ArtistSearchEngineService.cs @@ -87,7 +87,15 @@ public async Task InitializeAsync(IMelodeeConfiguration? configuration = null, await using (var scopedContext = await artistSearchEngineServiceDbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false)) { - await scopedContext.Database.MigrateAsync(cancellationToken); + if (scopedContext.Database.IsRelational()) + { + await scopedContext.Database.MigrateAsync(cancellationToken).ConfigureAwait(false); + } + else + { + await scopedContext.Database.EnsureCreatedAsync(cancellationToken).ConfigureAwait(false); + } + await BackfillLocalArtistAliasLookupAsync(scopedContext, cancellationToken).ConfigureAwait(false); } diff --git a/src/Melodee.Mql/Melodee.Mql.csproj b/src/Melodee.Mql/Melodee.Mql.csproj index 5513eaf39..3169597e2 100644 --- a/src/Melodee.Mql/Melodee.Mql.csproj +++ b/src/Melodee.Mql/Melodee.Mql.csproj @@ -7,7 +7,7 @@ true $(NoWarn);NU1507 - 2.1.4 + 2.2.0 build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) $(VersionPrefix).0 $(VersionPrefix).0 diff --git a/tests/Melodee.Tests.Common/Services/SearchEngines/ArtistSearchEngineServiceTests.cs b/tests/Melodee.Tests.Common/Services/SearchEngines/ArtistSearchEngineServiceTests.cs index 1915013df..f0d7016ac 100644 --- a/tests/Melodee.Tests.Common/Services/SearchEngines/ArtistSearchEngineServiceTests.cs +++ b/tests/Melodee.Tests.Common/Services/SearchEngines/ArtistSearchEngineServiceTests.cs @@ -7,6 +7,7 @@ using Melodee.Common.Filtering; using Melodee.Common.Models; using Melodee.Common.Models.SearchEngines; +using Melodee.Common.Models.SearchEngines.ArtistSearchEngineServiceData; using Melodee.Common.Services.SearchEngines; using Microsoft.EntityFrameworkCore; using Moq; @@ -32,6 +33,28 @@ public async Task InitializeAsync_WhenCalled_InitializesService() Assert.True(true); } + [Fact] + public async Task InitializeAsync_WithNonRelationalProvider_CreatesDatabaseWithoutMigrations() + { + var databaseName = $"artist-search-{Guid.NewGuid():N}"; + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName) + .Options; + var contextFactory = new Mock>(); + contextFactory + .Setup(x => x.CreateDbContextAsync(It.IsAny())) + .ReturnsAsync(() => new ArtistSearchEngineServiceDbContext(options)); + var service = CreateArtistSearchEngineService( + MockConfigurationFactory(), + MockHttpClientFactory(), + contextFactory.Object); + + await service.InitializeAsync(); + + await using var context = new ArtistSearchEngineServiceDbContext(options); + Assert.True(await context.Database.CanConnectAsync()); + } + [Fact] public async Task InitializeAsync_WhenCalledMultipleTimes_OnlyInitializesOnce() { @@ -603,7 +626,8 @@ private static Album NewAlbum(Artist artist, string name, int year) private ArtistSearchEngineService CreateArtistSearchEngineService( IMelodeeConfigurationFactory configFactory, - IHttpClientFactory httpClientFactory) + IHttpClientFactory httpClientFactory, + IDbContextFactory? artistSearchEngineContextFactory = null) { return new ArtistSearchEngineService( Logger, @@ -612,7 +636,7 @@ private ArtistSearchEngineService CreateArtistSearchEngineService( MockSpotifyClientBuilder(), configFactory, MockFactory(), - MockArtistSearchEngineFactory(), + artistSearchEngineContextFactory ?? MockArtistSearchEngineFactory(), GetMusicBrainzRepository(), Serializer, httpClientFactory);