fix app startup with mysql (#2205)

* don't run pragma command on mysql

* add not required pathhash

* make media file path hash required

* update changelog
This commit is contained in:
Jason Dove
2025-07-26 17:48:03 +00:00
committed by GitHub
parent 9511e6e6a7
commit ab0f431c85
26 changed files with 18358 additions and 19 deletions
+2
View File
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Fixed
- Fix app startup with MySql/MariaDB
## [25.3.1] - 2025-07-24
### Fixed
@@ -136,7 +136,9 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
{
if (key.StartsWith("ETV_", StringComparison.OrdinalIgnoreCase)
|| key.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase)
|| key.StartsWith("ASPNETCORE_", StringComparison.OrdinalIgnoreCase))
|| key.StartsWith("ASPNETCORE_", StringComparison.OrdinalIgnoreCase)
|| key.Equals("PROVIDER", StringComparison.OrdinalIgnoreCase)
|| key.StartsWith("ELASTICSEARCH", StringComparison.OrdinalIgnoreCase))
{
environment[key] = value;
}
@@ -4,6 +4,7 @@ public class MediaFile
{
public int Id { get; set; }
public string Path { get; set; }
public string PathHash { get; set; }
public int MediaVersionId { get; set; }
public MediaVersion MediaVersion { get; set; }
+19
View File
@@ -0,0 +1,19 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace ErsatzTV.Core;
public static class PathUtils
{
public static string GetPathHash(string path)
{
byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(path));
var builder = new StringBuilder();
foreach (byte b in bytes)
{
builder.Append(b.ToString("x2", CultureInfo.InvariantCulture));
}
return builder.ToString();
}
}
@@ -12,8 +12,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
[DbContext(typeof(TvContext))]
[Migration("20250723030616_Update_MediaFilePath")]
partial class Update_MediaFilePath
[Migration("20250726155754_Add_MediaFilePathHash")]
partial class Add_MediaFilePathHash
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -1116,15 +1116,15 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<string>("Path")
.HasColumnType("longtext");
b.Property<string>("PathHash")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("LibraryFolderId");
b.HasIndex("MediaVersionId");
b.HasIndex("Path")
.IsUnique();
b.ToTable("MediaFile", (string)null);
b.UseTptMappingStrategy();
@@ -5,11 +5,15 @@
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Update_MediaFilePath : Migration
public partial class Add_MediaFilePathHash : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_MediaFile_Path",
table: "MediaFile");
migrationBuilder.AlterColumn<string>(
name: "Path",
table: "MediaFile",
@@ -20,11 +24,22 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "PathHash",
table: "MediaFile",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PathHash",
table: "MediaFile");
migrationBuilder.AlterColumn<string>(
name: "Path",
table: "MediaFile",
@@ -35,6 +50,12 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_MediaFile_Path",
table: "MediaFile",
column: "Path",
unique: true);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Update_MediaFilePathHash : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "MediaFile",
keyColumn: "PathHash",
keyValue: null,
column: "PathHash",
value: "");
migrationBuilder.AlterColumn<string>(
name: "PathHash",
table: "MediaFile",
type: "varchar(255)",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_MediaFile_PathHash",
table: "MediaFile",
column: "PathHash",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_MediaFile_PathHash",
table: "MediaFile");
migrationBuilder.AlterColumn<string>(
name: "PathHash",
table: "MediaFile",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "varchar(255)")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
}
}
@@ -1113,13 +1113,17 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<string>("Path")
.HasColumnType("longtext");
b.Property<string>("PathHash")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("LibraryFolderId");
b.HasIndex("MediaVersionId");
b.HasIndex("Path")
b.HasIndex("PathHash")
.IsUnique();
b.ToTable("MediaFile", (string)null);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_MediaFilePathHash : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_MediaFile_Path",
table: "MediaFile");
migrationBuilder.AddColumn<string>(
name: "PathHash",
table: "MediaFile",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PathHash",
table: "MediaFile");
migrationBuilder.CreateIndex(
name: "IX_MediaFile_Path",
table: "MediaFile",
column: "Path",
unique: true);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Update_MediaFilePathHash : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "PathHash",
table: "MediaFile",
type: "TEXT",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.CreateIndex(
name: "IX_MediaFile_PathHash",
table: "MediaFile",
column: "PathHash",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_MediaFile_PathHash",
table: "MediaFile");
migrationBuilder.AlterColumn<string>(
name: "PathHash",
table: "MediaFile",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
}
}
}
@@ -1056,13 +1056,17 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("PathHash")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("LibraryFolderId");
b.HasIndex("MediaVersionId");
b.HasIndex("Path")
b.HasIndex("PathHash")
.IsUnique();
b.ToTable("MediaFile", (string)null);
@@ -10,9 +10,12 @@ public class MediaFileConfiguration : IEntityTypeConfiguration<MediaFile>
{
builder.ToTable("MediaFile");
builder.HasIndex(f => f.Path)
builder.HasIndex(f => f.PathHash)
.IsUnique();
builder.Property(f => f.PathHash)
.IsRequired();
builder.HasOne(f => f.LibraryFolder)
.WithMany(f => f.MediaFiles)
.HasForeignKey(f => f.LibraryFolderId)
@@ -8,8 +8,11 @@ namespace ErsatzTV.Infrastructure.Data;
public static class DbInitializer
{
public static async Task<Unit> Initialize(TvContext context, CancellationToken cancellationToken)
{
if (TvContext.IsSqlite)
{
await context.Connection.ExecuteAsync("PRAGMA journal_mode=WAL", cancellationToken);
}
if (!context.LanguageCodes.Any())
{
@@ -142,7 +142,7 @@ public class ImageRepository : IImageRepository
[
new MediaVersion
{
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
Streams = []
}
],
@@ -252,7 +252,7 @@ public class MovieRepository : IMovieRepository
[
new MediaVersion
{
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
Streams = []
}
],
@@ -233,7 +233,7 @@ public class MusicVideoRepository : IMusicVideoRepository
[
new MediaVersion
{
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
Streams = []
}
],
@@ -207,7 +207,7 @@ public class OtherVideoRepository : IOtherVideoRepository
[
new MediaVersion
{
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
Streams = []
}
],
@@ -149,7 +149,7 @@ public class RemoteStreamRepository(
[
new MediaVersion
{
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
Streams = []
}
],
@@ -134,7 +134,7 @@ public class SongRepository : ISongRepository
[
new MediaVersion
{
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
Streams = []
}
],
@@ -773,7 +773,7 @@ public class TelevisionRepository : ITelevisionRepository
[
new MediaVersion
{
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
Streams = []
}
],
@@ -17,6 +17,7 @@ public class TvContext : DbContext
public static string LastInsertedRowId { get; set; } = "last_insert_rowid()";
public static string CaseInsensitiveCollation { get; set; } = "NOCASE";
public static bool IsSqlite { get; set; }
public IDbConnection Connection => Database.GetDbConnection();
@@ -1,5 +1,7 @@
using ErsatzTV.Core;
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Infrastructure.Data;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Services.RunOnce;
@@ -28,11 +30,39 @@ public class DatabaseMigratorService : BackgroundService
using IServiceScope scope = _serviceScopeFactory.CreateScope();
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
await dbContext.Database.MigrateAsync("Add_MediaFilePathHash", stoppingToken);
// this can't be part of a migration, so we have to stop here and run some sql
await PopulatePathHashes(dbContext);
// then continue migrating
await dbContext.Database.MigrateAsync(stoppingToken);
await DbInitializer.Initialize(dbContext, stoppingToken);
_systemStartup.DatabaseIsReady();
_logger.LogInformation("Done applying database migrations");
}
private static async Task PopulatePathHashes(TvContext dbContext)
{
if (await dbContext.Connection.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM `MediaFile` WHERE `PathHash` IS NULL OR `PathHash` = ''") == 0)
{
return;
}
if (dbContext.Connection is SqliteConnection sqliteConnection)
{
sqliteConnection.CreateFunction("HASH_SHA256", (string text) => PathUtils.GetPathHash(text));
await dbContext.Connection.ExecuteAsync("UPDATE `MediaFile` SET `PathHash` = HASH_SHA256(`Path`);");
}
else
{
// mysql
await dbContext.Connection.ExecuteAsync("UPDATE `MediaFile` SET `PathHash` = sha2(`Path`, 256);");
}
}
}
+4
View File
@@ -362,6 +362,8 @@ public class Startup
{
if (databaseProvider == Provider.Sqlite.Name)
{
TvContext.IsSqlite = true;
options.UseSqlite(
sqliteConnectionString,
o =>
@@ -373,6 +375,8 @@ public class Startup
if (databaseProvider == Provider.MySql.Name)
{
TvContext.IsSqlite = false;
options.UseMySql(
mySqlConnectionString,
ServerVersion.AutoDetect(mySqlConnectionString),