Add scripts for PostgreSQL migration and SQLite removal; update project references

- Introduced `publish-with-sql.ps1` to publish Jellyfin with SQL files.
- Added `remove-sqlite.ps1` to facilitate the removal of SQLite dependencies for PostgreSQL-only deployment.
- Created `rollback-to-net10.ps1` to revert .NET 11 changes back to .NET 10.
- Implemented `test_api.py` for testing API interactions.
- Added `verify-migration.ps1` to verify PostgreSQL migration steps.
- Updated various `.csproj` files to include `Microsoft.Kiota.Abstractions` package.
- Enhanced `JellyfinDbContext` to handle concurrency exceptions during save operations.
- Updated tests to include new package references and ensure compatibility with changes.
This commit is contained in:
2026-07-07 10:59:40 -04:00
parent bf51bff748
commit 9bcb8501ab
70 changed files with 1370 additions and 13 deletions
+40
View File
@@ -0,0 +1,40 @@
@echo off
REM Quick script to add ALL performance indexes (base + supplementary)
echo ========================================
echo Add ALL Performance Indexes
echo ========================================
echo.
echo This will create:
echo - 18 base performance indexes
echo - 5 supplementary indexes
echo Total: 23 indexes
echo.
echo NOTE: This uses the database configured in db-config.ps1
echo Current default: jellyfin_testsdata
echo.
echo This may take 10-30 minutes...
echo.
REM Use PowerShell to load config and run
powershell -Command ". .\db-config.ps1; & $PSQL_PATH -U $DB_USER -d $DB_NAME -f sql\all_performance_indexes.sql"
if %ERRORLEVEL% EQU 0 (
echo.
echo ========================================
echo Success! All 23 indexes added.
echo ========================================
echo.
echo Performance improvement: 70-90 percent faster!
echo.
echo Next: Restart Jellyfin
echo.
) else (
echo.
echo ========================================
echo Failed to add indexes
echo ========================================
echo.
)
pause
+37
View File
@@ -0,0 +1,37 @@
@echo off
REM Add missing base performance indexes to Jellyfin database
echo ========================================
echo Add Base Performance Indexes
echo ========================================
echo.
echo These indexes were missing from InitialCreate
echo Adding 18 essential performance indexes...
echo.
"C:\Program Files\PostgreSQL\18\bin\psql.exe" -U jellyfin -d jellyfin -f sql\add_base_performance_indexes.sql
if %ERRORLEVEL% EQU 0 (
echo.
echo ========================================
echo Success! Base indexes added.
echo ========================================
echo.
echo Indexes created:
echo - 9 on BaseItems
echo - 2 on BaseItemProviders
echo - 2 on MediaStreamInfos
echo - 2 on PeopleBaseItemMap
echo - 3 on UserData
echo.
echo Next: Restart Jellyfin
echo.
) else (
echo.
echo ========================================
echo Failed to add indexes
echo ========================================
echo.
)
pause
+38
View File
@@ -0,0 +1,38 @@
@echo off
REM Quick script to add supplementary indexes to jellyfin_testsdata
echo ========================================
echo Add Supplementary Indexes
echo ========================================
echo.
echo Connecting to jellyfin_testsdata database...
echo.
REM Apply the indexes
"C:\Program Files\PostgreSQL\18\bin\psql.exe" -U jellyfin -d jellyfin -f sql\schema_init\10_create_supplementary_indexes.sql
if %ERRORLEVEL% EQU 0 (
echo.
echo ========================================
echo Success! Indexes added.
echo ========================================
echo.
echo Next steps:
echo 1. Restart Jellyfin
echo 2. Monitor performance
echo.
) else (
echo.
echo ========================================
echo Failed to add indexes
echo ========================================
echo.
echo Troubleshooting:
echo - Ensure PostgreSQL is running
echo - Check database name is correct
echo - Verify credentials
echo.
)
pause
+20
View File
@@ -0,0 +1,20 @@
# Add Supplementary Indexes - Quick Command
Write-Host "Connecting to jellyfin_testsdata and adding supplementary indexes..." -ForegroundColor Cyan
Write-Host ""
# Quick and simple - just run the SQL script
psql -U jellyfin -d jellyfin_testsdata -f sql\schema_init\10_create_supplementary_indexes.sql
if ($LASTEXITCODE -eq 0) {
Write-Host ""
Write-Host "✓ Success! Supplementary indexes added." -ForegroundColor Green
Write-Host ""
Write-Host "Next: Restart Jellyfin to see performance improvements" -ForegroundColor Yellow
} else {
Write-Host ""
Write-Host "❌ Failed. Check that:" -ForegroundColor Red
Write-Host " - PostgreSQL is running" -ForegroundColor Gray
Write-Host " - Database 'jellyfin_testsdata' exists" -ForegroundColor Gray
Write-Host " - You have the correct password" -ForegroundColor Gray
}
@@ -0,0 +1,175 @@
# Apply Supplementary Indexes Migration to PostgreSQL Database
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Apply Supplementary Indexes Migration" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Configuration
$connectionString = "Host=localhost;Port=5432;Database=jellyfin_testsdata;Username=jellyfin;Password=YOUR_PASSWORD"
$migrationName = "AddSupplementaryIndexes"
Write-Host "Step 1: Build the solution" -ForegroundColor Yellow
Write-Host "This ensures the migration is compiled..." -ForegroundColor Gray
dotnet build src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres\Jellyfin.Database.Providers.Postgres.csproj
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ Build failed. Please fix compilation errors." -ForegroundColor Red
exit 1
}
Write-Host "✓ Build successful" -ForegroundColor Green
Write-Host ""
Write-Host "Step 2: Check current database state" -ForegroundColor Yellow
Write-Host "Connecting to jellyfin_testsdata..." -ForegroundColor Gray
# Test database connection
try {
$testQuery = @"
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'library'
AND table_name = 'BaseItems'
) as table_exists;
"@
$result = psql -U jellyfin -d jellyfin_testsdata -t -c $testQuery 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "✓ Database connection successful" -ForegroundColor Green
} else {
Write-Host "❌ Database connection failed" -ForegroundColor Red
Write-Host "Error: $result" -ForegroundColor Red
Write-Host ""
Write-Host "Please ensure:" -ForegroundColor Yellow
Write-Host " 1. PostgreSQL is running" -ForegroundColor Gray
Write-Host " 2. Database 'jellyfin_testsdata' exists" -ForegroundColor Gray
Write-Host " 3. Credentials are correct" -ForegroundColor Gray
exit 1
}
} catch {
Write-Host "❌ Failed to test database connection: $_" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "Step 3: Check for existing indexes" -ForegroundColor Yellow
Write-Host "Checking which supplementary indexes already exist..." -ForegroundColor Gray
$checkIndexQuery = @"
SELECT
indexname,
CASE WHEN indexname IS NOT NULL THEN 'EXISTS' ELSE 'MISSING' END as status
FROM (
VALUES
('idx_baseitems_type_isvirtualitem_topparentid'),
('idx_baseitems_topparentid_isfolder'),
('idx_baseitems_datecreated_filtered'),
('idx_itemvaluesmap_itemvalueid_itemid'),
('idx_activitylogs_userid_datecreated')
) AS wanted(indexname)
LEFT JOIN pg_indexes ON pg_indexes.indexname = wanted.indexname
ORDER BY wanted.indexname;
"@
$indexStatus = psql -U jellyfin -d jellyfin_testsdata -c $checkIndexQuery
Write-Host $indexStatus
Write-Host ""
Write-Host "Step 4: Apply migration using EF Core" -ForegroundColor Yellow
Write-Host "This will create any missing indexes..." -ForegroundColor Gray
# Option 1: Use dotnet ef (if you have it installed)
Write-Host ""
Write-Host "Choose migration method:" -ForegroundColor Cyan
Write-Host "1. Use dotnet ef database update (requires dotnet-ef tool)" -ForegroundColor White
Write-Host "2. Use SQL script generation" -ForegroundColor White
Write-Host "3. Apply SQL directly (recommended)" -ForegroundColor White
Write-Host ""
$choice = Read-Host "Enter your choice (1, 2, or 3)"
switch ($choice) {
"1" {
Write-Host "Applying migration using dotnet ef..." -ForegroundColor Yellow
$env:ConnectionStrings__DefaultConnection = $connectionString
dotnet ef database update --project src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres\Jellyfin.Database.Providers.Postgres.csproj
if ($LASTEXITCODE -eq 0) {
Write-Host "✓ Migration applied successfully" -ForegroundColor Green
} else {
Write-Host "❌ Migration failed" -ForegroundColor Red
}
}
"2" {
Write-Host "Generating SQL script..." -ForegroundColor Yellow
dotnet ef migrations script --project src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres\Jellyfin.Database.Providers.Postgres.csproj --output sql\migration_supplementary_indexes.sql
if ($LASTEXITCODE -eq 0) {
Write-Host "✓ SQL script generated at: sql\migration_supplementary_indexes.sql" -ForegroundColor Green
Write-Host ""
Write-Host "To apply, run:" -ForegroundColor Yellow
Write-Host "psql -U jellyfin -d jellyfin_testsdata -f sql\migration_supplementary_indexes.sql" -ForegroundColor White
} else {
Write-Host "❌ Script generation failed" -ForegroundColor Red
}
}
"3" {
Write-Host "Applying SQL directly..." -ForegroundColor Yellow
# Apply the SQL from the schema_init script
psql -U jellyfin -d jellyfin_testsdata -f sql\schema_init\10_create_supplementary_indexes.sql
if ($LASTEXITCODE -eq 0) {
Write-Host "✓ Indexes created successfully" -ForegroundColor Green
} else {
Write-Host "❌ Index creation failed" -ForegroundColor Red
}
}
default {
Write-Host "Invalid choice. Exiting." -ForegroundColor Red
exit 1
}
}
Write-Host ""
Write-Host "Step 5: Verify indexes were created" -ForegroundColor Yellow
$verifyQuery = @"
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_indexes
JOIN pg_stat_user_indexes USING (schemaname, tablename, indexname)
WHERE indexname IN (
'idx_baseitems_type_isvirtualitem_topparentid',
'idx_baseitems_topparentid_isfolder',
'idx_baseitems_datecreated_filtered',
'idx_itemvaluesmap_itemvalueid_itemid',
'idx_activitylogs_userid_datecreated'
)
ORDER BY schemaname, tablename, indexname;
"@
$verification = psql -U jellyfin -d jellyfin_testsdata -c $verifyQuery
Write-Host $verification
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Migration Complete!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Yellow
Write-Host "1. Restart Jellyfin to clear connection pools" -ForegroundColor White
Write-Host "2. Monitor index usage with: psql -U jellyfin -d jellyfin_testsdata -f sql\diagnostics.sql" -ForegroundColor White
Write-Host "3. Check performance improvements" -ForegroundColor White
Write-Host ""
@@ -0,0 +1,388 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Converts C# files from block-scoped to file-scoped namespaces and fixes System namespace conflicts.
.DESCRIPTION
This script converts all C# files in MediaBrowser.Model from block-scoped namespaces
to file-scoped namespaces, and replaces System using directives with global::System
to resolve conflicts with MediaBrowser.Model.System namespace.
.PARAMETER Path
The root path to search for C# files. Defaults to "MediaBrowser.Model"
.PARAMETER DryRun
If specified, shows what would be changed without actually modifying files
.PARAMETER Verbose
Shows detailed processing information
.PARAMETER BackupFiles
Creates .bak files before conversion
.EXAMPLE
.\ConvertToFileScopedNamespaces.ps1 -DryRun
.EXAMPLE
.\ConvertToFileScopedNamespaces.ps1 -BackupFiles
.EXAMPLE
.\ConvertToFileScopedNamespaces.ps1 -Path "MediaBrowser.Model\Dlna" -Verbose
#>
param(
[string]$Path = "MediaBrowser.Model",
[switch]$DryRun,
[switch]$BackupFiles
)
$ErrorActionPreference = "Stop"
$VerbosePreference = if ($PSBoundParameters.ContainsKey('Verbose')) { 'Continue' } else { 'SilentlyContinue' }
function Get-IndentString {
param([string]$Line)
if ($Line -match '^(\s+)') {
return $Matches[1]
}
return ""
}
function Get-IndentLevel {
param([string]$IndentString)
# Count spaces (4 spaces = 1 level) or tabs (1 tab = 1 level)
$spaces = ($IndentString.ToCharArray() | Where-Object { $_ -eq ' ' }).Count
$tabs = ($IndentString.ToCharArray() | Where-Object { $_ -eq "`t" }).Count
return [Math]::Max($tabs, [Math]::Floor($spaces / 4))
}
function Remove-OneIndentLevel {
param([string]$Line, [ref]$IndentChar, [ref]$IndentSize)
if ([string]::IsNullOrWhiteSpace($Line)) {
return $Line
}
$indent = Get-IndentString -Line $Line
if ($indent.Length -eq 0) {
return $Line
}
# Detect indent character and size on first indented line
if ($IndentChar.Value -eq $null) {
if ($indent[0] -eq "`t") {
$IndentChar.Value = "`t"
$IndentSize.Value = 1
} else {
$IndentChar.Value = ' '
# Detect indent size (usually 4 spaces)
$IndentSize.Value = 4
if ($indent.Length -ge 2) {
# Try to detect actual indent size
for ($i = 1; $i -le 8; $i++) {
if ($indent.Length % $i -eq 0) {
$IndentSize.Value = $i
break
}
}
}
}
Write-Verbose "Detected indent: '$($IndentChar.Value)' x $($IndentSize.Value)"
}
# Remove one level of indentation
if ($IndentChar.Value -eq "`t") {
if ($Line -match '^\t(.*)') {
return $Matches[1]
}
} else {
$pattern = "^" + (' ' * $IndentSize.Value) + '(.*)'
if ($Line -match $pattern) {
return $Matches[1]
}
}
return $Line
}
function Test-FileScopedNamespace {
param([string]$Content)
# Check if already using file-scoped namespace (semicolon after namespace declaration)
return $Content -match 'namespace\s+[\w\.]+\s*;'
}
function Test-BlockScopedNamespace {
param([string]$Content)
# Check for block-scoped namespace
return $Content -match 'namespace\s+[\w\.]+\s*\r?\n?\s*\{'
}
function Add-GlobalPrefix {
param([string]$Content)
# System namespaces to prefix (ordered from most specific to least)
$systemNamespaces = @(
'System\.Xml\.Serialization',
'System\.ComponentModel\.DataAnnotations',
'System\.ComponentModel',
'System\.Collections\.Generic',
'System\.Collections\.Concurrent',
'System\.Collections',
'System\.Diagnostics\.CodeAnalysis',
'System\.Diagnostics',
'System\.Globalization',
'System\.Linq\.Expressions',
'System\.Linq',
'System\.Text\.Json\.Serialization',
'System\.Text\.Json',
'System\.Text\.RegularExpressions',
'System\.Text',
'System\.Threading\.Tasks',
'System\.Threading',
'System\.Runtime\.Serialization',
'System\.Runtime\.CompilerServices',
'System\.Runtime',
'System\.Net\.Http',
'System\.Net\.Sockets',
'System\.Net\.WebSockets',
'System\.Net\.Mime',
'System\.Net',
'System\.IO\.Compression',
'System\.IO',
'System\.Security',
'System' # Must be last to avoid partial matches
)
foreach ($ns in $systemNamespaces) {
# Don't add global:: if it's already there
# Match: "using System.X" but not "using global::System.X"
$pattern = '(\s+using\s+)(?!global::)(' + $ns + ')(\s|;)'
$replacement = '${1}global::${2}${3}'
$Content = $Content -replace $pattern, $replacement
}
return $Content
}
function Convert-ToFileScopedNamespace {
param(
[string]$FilePath,
[switch]$DryRun,
[switch]$CreateBackup
)
$content = Get-Content -Path $FilePath -Raw
$originalContent = $content
$modified = $false
# Check current state
$hasFileScopedNS = Test-FileScopedNamespace -Content $content
$hasBlockScopedNS = Test-BlockScopedNamespace -Content $content
if ($hasFileScopedNS) {
Write-Verbose " Already using file-scoped namespace"
# But still add global:: prefixes if needed
$contentWithGlobal = Add-GlobalPrefix -Content $content
if ($contentWithGlobal -ne $content) {
$content = $contentWithGlobal
$modified = $true
Write-Host " [UPDATED] Added global:: prefixes" -ForegroundColor Yellow
} else {
Write-Host " [SKIP] Already correct" -ForegroundColor Gray
return $false
}
}
elseif (-not $hasBlockScopedNS) {
Write-Host " [SKIP] No namespace found" -ForegroundColor Gray
return $false
}
else {
Write-Verbose " Converting block-scoped to file-scoped namespace"
# Extract namespace name and line
if ($content -match 'namespace\s+([\w\.]+)\s*\r?\n?\s*\{') {
$namespaceName = $Matches[1]
Write-Verbose " Namespace: $namespaceName"
} else {
Write-Warning " Could not parse namespace"
return $false
}
# Step 1: Convert namespace declaration to file-scoped
$content = $content -replace '(namespace\s+[\w\.]+)\s*\r?\n?\s*\{', '$1;'
$modified = $true
# Step 2: Add global:: prefixes to System using directives
$content = Add-GlobalPrefix -Content $content
# Step 3: Parse into lines for indentation removal
$lines = $content -split '\r?\n'
$hasWindowsLineEndings = $originalContent -match '\r\n'
# Find namespace declaration line
$namespaceLineIndex = -1
for ($i = 0; $i -lt $lines.Length; $i++) {
if ($lines[$i] -match '^namespace\s+[\w\.]+\s*;') {
$namespaceLineIndex = $i
break
}
}
if ($namespaceLineIndex -lt 0) {
Write-Warning " Could not find namespace declaration after conversion"
return $false
}
# Step 4: Remove one level of indentation from all lines after namespace
$indentChar = $null
$indentSize = 4
$indentCharRef = [ref]$indentChar
$indentSizeRef = [ref]$indentSize
for ($i = $namespaceLineIndex + 1; $i -lt $lines.Length; $i++) {
$lines[$i] = Remove-OneIndentLevel -Line $lines[$i] -IndentChar $indentCharRef -IndentSize $indentSizeRef
}
# Step 5: Remove the closing brace of the namespace
# Find the last non-empty, non-whitespace line that's just a closing brace
$closingBraceIndex = -1
for ($i = $lines.Length - 1; $i -gt $namespaceLineIndex; $i--) {
$trimmed = $lines[$i].Trim()
if ($trimmed -eq '}') {
$closingBraceIndex = $i
break
}
elseif ($trimmed -ne '' -and $trimmed -ne '}') {
# Found non-brace content, stop looking
break
}
}
if ($closingBraceIndex -ge 0) {
Write-Verbose " Removing closing brace at line $closingBraceIndex"
$lines = $lines[0..($closingBraceIndex-1)] + $lines[($closingBraceIndex+1)..($lines.Length-1)]
}
# Step 6: Rejoin lines
$lineEnding = if ($hasWindowsLineEndings) { "`r`n" } else { "`n" }
$content = ($lines | Where-Object { $_ -ne $null }) -join $lineEnding
# Step 7: Clean up trailing whitespace and ensure single newline at end
$content = $content.TrimEnd()
$content += $lineEnding
Write-Host " [CONVERTED] Block-scoped → File-scoped" -ForegroundColor Green
}
# Apply changes
if ($modified -and $content -ne $originalContent) {
if ($DryRun) {
Write-Host " [DRY-RUN] Would save changes" -ForegroundColor Cyan
return $true
}
if ($CreateBackup) {
$backupPath = "$FilePath.bak"
Copy-Item -Path $FilePath -Destination $backupPath -Force
Write-Verbose " Created backup: $backupPath"
}
# Write with correct encoding (UTF-8 without BOM for .cs files)
[System.IO.File]::WriteAllText($FilePath, $content, [System.Text.UTF8Encoding]::new($false))
return $true
}
return $false
}
# Main script
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "C# File-Scoped Namespace Converter" -ForegroundColor Cyan
Write-Host "Fixes MediaBrowser.Model.System namespace conflicts" -ForegroundColor Cyan
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host
if ($DryRun) {
Write-Host "DRY RUN MODE - No files will be modified" -ForegroundColor Yellow
Write-Host
}
if ($BackupFiles) {
Write-Host "Backup mode enabled - .bak files will be created" -ForegroundColor Yellow
Write-Host
}
if (-not (Test-Path $Path)) {
Write-Error "❌ Path not found: $Path"
exit 1
}
# Find all C# files
$files = Get-ChildItem -Path $Path -Filter "*.cs" -Recurse | Where-Object {
$_.FullName -notmatch "\\obj\\" -and
$_.FullName -notmatch "\\bin\\" -and
$_.FullName -notmatch "\\Properties\\AssemblyInfo\.cs$"
}
Write-Host "Found $($files.Count) C# files in $Path" -ForegroundColor Cyan
Write-Host
$convertedCount = 0
$skippedCount = 0
$errorCount = 0
$updatedCount = 0
foreach ($file in $files) {
$relativePath = $file.FullName.Replace((Get-Location).Path, "").TrimStart([char]92, [char]47)
Write-Host "[Processing] $relativePath" -ForegroundColor White
try {
$result = Convert-ToFileScopedNamespace -FilePath $file.FullName -DryRun:$DryRun -CreateBackup:$BackupFiles
if ($result) {
if ($result -eq "updated") {
$updatedCount++
} else {
$convertedCount++
}
} else {
$skippedCount++
}
}
catch {
Write-Host " [ERROR] $_" -ForegroundColor Red
Write-Verbose $_.ScriptStackTrace
$errorCount++
}
}
Write-Host
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "Summary:" -ForegroundColor Cyan
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host " Total files: $($files.Count)" -ForegroundColor White
Write-Host " Converted: $convertedCount" -ForegroundColor Green
Write-Host " Updated (global::): $updatedCount" -ForegroundColor Yellow
Write-Host " Skipped: $skippedCount" -ForegroundColor Gray
Write-Host " Errors: $errorCount" -ForegroundColor Red
if ($DryRun) {
Write-Host
Write-Host "This was a DRY RUN. Run without -DryRun to apply changes." -ForegroundColor Yellow
} elseif ($convertedCount -gt 0 -or $updatedCount -gt 0) {
Write-Host
Write-Host "Conversion complete! Next steps:" -ForegroundColor Green
Write-Host " 1. Review changes with: git diff" -ForegroundColor White
Write-Host " 2. Build project: dotnet build MediaBrowser.Model\MediaBrowser.Model.csproj" -ForegroundColor White
Write-Host " 3. If successful: git add . && git commit -m `"Convert to file-scoped namespaces`"" -ForegroundColor White
}
Write-Host ("=" * 80) -ForegroundColor Cyan
exit $errorCount
@@ -0,0 +1,299 @@
# Async Migration Analysis Script
# Scans the Jellyfin codebase for synchronous database operations that need conversion
param(
[string]$Path = ".",
[switch]$DetailedReport,
[switch]$ExportCsv
)
Write-Host "🔍 Scanning for synchronous database operations..." -ForegroundColor Cyan
Write-Host ""
# Define patterns to search for
$patterns = @{
"SaveChanges" = @{
Pattern = "\.SaveChanges\s*\(\s*\)"
Replacement = "await .SaveChangesAsync(cancellationToken)"
Severity = "HIGH"
Description = "Synchronous database save operation"
}
"ToList" = @{
Pattern = "\.ToList\s*\(\s*\)"
Replacement = "await .ToListAsync(cancellationToken)"
Severity = "HIGH"
Description = "Synchronous list materialization"
}
"ToArray" = @{
Pattern = "\.ToArray\s*\(\s*\)"
Replacement = "await .ToArrayAsync(cancellationToken)"
Severity = "HIGH"
Description = "Synchronous array materialization"
}
"FirstOrDefault" = @{
Pattern = "\.FirstOrDefault\s*\("
Replacement = 'await .FirstOrDefaultAsync('
Severity = "HIGH"
Description = "Synchronous first element query"
}
"First" = @{
Pattern = "\.First\s*\("
Replacement = 'await .FirstAsync('
Severity = "HIGH"
Description = "Synchronous first element query"
}
"SingleOrDefault" = @{
Pattern = "\.SingleOrDefault\s*\("
Replacement = 'await .SingleOrDefaultAsync('
Severity = "MEDIUM"
Description = "Synchronous single element query"
}
"Single" = @{
Pattern = "\.Single\s*\("
Replacement = 'await .SingleAsync('
Severity = "MEDIUM"
Description = "Synchronous single element query"
}
"Any" = @{
Pattern = "\.Any\s*\("
Replacement = 'await .AnyAsync('
Severity = "MEDIUM"
Description = "Synchronous existence check"
}
"Count" = @{
Pattern = "\.Count\s*\(\s*\)"
Replacement = "await .CountAsync(cancellationToken)"
Severity = "MEDIUM"
Description = "Synchronous count operation"
}
"LongCount" = @{
Pattern = "\.LongCount\s*\("
Replacement = "await .LongCountAsync("
Severity = "MEDIUM"
Description = "Synchronous long count operation"
}
"ExecuteDelete" = @{
Pattern = "\.ExecuteDelete\s*\(\s*\)"
Replacement = "await .ExecuteDeleteAsync(cancellationToken)"
Severity = "HIGH"
Description = "Synchronous bulk delete"
}
"ExecuteUpdate" = @{
Pattern = "\.ExecuteUpdate\s*\("
Replacement = "await .ExecuteUpdateAsync("
Severity = "HIGH"
Description = "Synchronous bulk update"
}
"BeginTransaction" = @{
Pattern = "\.BeginTransaction\s*\(\s*\)"
Replacement = "await .BeginTransactionAsync(cancellationToken)"
Severity = "HIGH"
Description = "Synchronous transaction start"
}
"Commit" = @{
Pattern = "transaction\.Commit\s*\(\s*\)"
Replacement = "await transaction.CommitAsync(cancellationToken)"
Severity = "HIGH"
Description = "Synchronous transaction commit"
}
"Rollback" = @{
Pattern = "transaction\.Rollback\s*\(\s*\)"
Replacement = "await transaction.RollbackAsync(cancellationToken)"
Severity = "MEDIUM"
Description = "Synchronous transaction rollback"
}
"Sum" = @{
Pattern = "\.Sum\s*\("
Replacement = 'await .SumAsync('
Severity = "LOW"
Description = "Synchronous sum aggregation"
}
"Average" = @{
Pattern = "\.Average\s*\("
Replacement = 'await .AverageAsync('
Severity = "LOW"
Description = "Synchronous average aggregation"
}
"Min" = @{
Pattern = "\.Min\s*\("
Replacement = 'await .MinAsync('
Severity = "LOW"
Description = "Synchronous min aggregation"
}
"Max" = @{
Pattern = "\.Max\s*\("
Replacement = 'await .MaxAsync('
Severity = "LOW"
Description = "Synchronous max aggregation"
}
"Load" = @{
Pattern = "\.Load\s*\(\s*\)"
Replacement = 'await .LoadAsync(cancellationToken)'
Severity = "MEDIUM"
Description = "Synchronous navigation property load"
}
}
# Exclude patterns (already async or intentionally sync)
$excludePatterns = @(
"Async\s*\(",
"// ASYNC",
"\.ConfigureAwait",
"AsyncEnumerable"
)
# Get all .cs files, excluding test files and migrations initially
$files = Get-ChildItem -Path $Path -Recurse -Filter "*.cs" |
Where-Object {
$_.FullName -notmatch "\\bin\\" -and
$_.FullName -notmatch "\\obj\\" -and
$_.FullName -notmatch "\\Migrations\\" -and
$_.FullName -notmatch "\.Designer\.cs$"
}
$results = @()
$summary = @{}
foreach ($file in $files) {
$content = Get-Content -Path $file.FullName -Raw
foreach ($patternName in $patterns.Keys) {
$patternInfo = $patterns[$patternName]
$pattern = $patternInfo.Pattern
# Find matches
$matches = [regex]::Matches($content, $pattern)
foreach ($match in $matches) {
# Check if this line is already async or should be excluded
$lineStart = $content.Substring(0, $match.Index).LastIndexOf("`n")
if ($lineStart -lt 0) { $lineStart = 0 }
$lineEnd = $content.IndexOf("`n", $match.Index)
if ($lineEnd -lt 0) { $lineEnd = $content.Length }
$line = $content.Substring($lineStart, $lineEnd - $lineStart).Trim()
# Skip if already async or excluded
$shouldExclude = $false
foreach ($excludePattern in $excludePatterns) {
if ($line -match $excludePattern) {
$shouldExclude = $true
break
}
}
if (-not $shouldExclude) {
# Calculate line number
$lineNumber = ($content.Substring(0, $match.Index) -split "`n").Count
$result = [PSCustomObject]@{
File = $file.FullName.Replace($Path, "").TrimStart('\')
LineNumber = $lineNumber
Pattern = $patternName
Severity = $patternInfo.Severity
Description = $patternInfo.Description
MatchedText = $match.Value
Line = $line
Replacement = $patternInfo.Replacement
}
$results += $result
# Update summary
if (-not $summary.ContainsKey($patternName)) {
$summary[$patternName] = 0
}
$summary[$patternName]++
}
}
}
}
# Display results
Write-Host "📊 Analysis Complete!" -ForegroundColor Green
Write-Host ""
Write-Host "=" * 80 -ForegroundColor DarkGray
Write-Host "SUMMARY" -ForegroundColor Yellow
Write-Host "=" * 80 -ForegroundColor DarkGray
Write-Host ""
$totalIssues = $results.Count
$highSeverity = ($results | Where-Object { $_.Severity -eq "HIGH" }).Count
$mediumSeverity = ($results | Where-Object { $_.Severity -eq "MEDIUM" }).Count
$lowSeverity = ($results | Where-Object { $_.Severity -eq "LOW" }).Count
Write-Host "Total synchronous operations found: " -NoNewline
Write-Host $totalIssues -ForegroundColor Red
Write-Host ""
Write-Host " 🔴 HIGH severity: " -NoNewline
Write-Host $highSeverity -ForegroundColor Red
Write-Host " 🟡 MEDIUM severity: " -NoNewline
Write-Host $mediumSeverity -ForegroundColor Yellow
Write-Host " 🟢 LOW severity: " -NoNewline
Write-Host $lowSeverity -ForegroundColor Green
Write-Host ""
Write-Host "Breakdown by operation type:" -ForegroundColor Cyan
Write-Host ""
$summary.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object {
$severity = $patterns[$_.Key].Severity
$color = switch ($severity) {
"HIGH" { "Red" }
"MEDIUM" { "Yellow" }
"LOW" { "Green" }
}
Write-Host " $($_.Key): " -NoNewline
Write-Host $_.Value -ForegroundColor $color -NoNewline
Write-Host " ($severity)"
}
Write-Host ""
Write-Host "=" * 80 -ForegroundColor DarkGray
if ($DetailedReport) {
Write-Host ""
Write-Host "DETAILED FINDINGS" -ForegroundColor Yellow
Write-Host "=" * 80 -ForegroundColor DarkGray
Write-Host ""
# Group by file
$resultsByFile = $results | Group-Object -Property File
foreach ($fileGroup in $resultsByFile | Sort-Object Name) {
Write-Host ""
Write-Host "📄 $($fileGroup.Name)" -ForegroundColor Cyan
Write-Host "-" * 80 -ForegroundColor DarkGray
foreach ($item in $fileGroup.Group | Sort-Object LineNumber) {
$severityColor = switch ($item.Severity) {
"HIGH" { "Red" }
"MEDIUM" { "Yellow" }
"LOW" { "Green" }
}
Write-Host " Line $($item.LineNumber): " -NoNewline
Write-Host "$($item.Pattern) " -NoNewline -ForegroundColor $severityColor
Write-Host "- $($item.Description)"
Write-Host " Current: " -NoNewline
Write-Host $item.MatchedText -ForegroundColor Gray
Write-Host " Replacement: " -NoNewline
Write-Host $item.Replacement -ForegroundColor Green
}
}
}
if ($ExportCsv) {
$csvPath = Join-Path $Path "async_migration_report.csv"
$results | Export-Csv -Path $csvPath -NoTypeInformation
Write-Host ""
Write-Host "📁 Report exported to: " -NoNewline
Write-Host $csvPath -ForegroundColor Cyan
}
Write-Host ""
Write-Host "💡 TIP: Run with -DetailedReport for line-by-line analysis" -ForegroundColor DarkGray
Write-Host "💡 TIP: Run with -ExportCsv to export findings to CSV" -ForegroundColor DarkGray
Write-Host ""
# Return results for further processing
return $results
@@ -0,0 +1,79 @@
# Apply ItemValues Optimized Indexes
# These indexes target the critical ItemValues table performance issue
# Note: Creates SQL file first, then executes it to avoid CONCURRENTLY transaction issues
. .\db-config.ps1
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Creating Optimized ItemValues Indexes" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Target: library.ItemValues table" -ForegroundColor Yellow
Write-Host "Problem: 1.3 BILLION rows being scanned sequentially" -ForegroundColor Red
Write-Host "Solution: 3 targeted indexes" -ForegroundColor Green
Write-Host ""
Write-Host "This may take 10-30 minutes..." -ForegroundColor Yellow
Write-Host ""
# Create temporary SQL file (CONCURRENTLY requires running outside transaction)
$tempSqlFile = "temp_itemvalues_indexes.sql"
$sqlContent = @"
-- Optimized indexes for ItemValues table
-- Fixes the 1.3 billion row sequential scan issue
-- Index 1: CleanValue lookups (for genre/tag searches)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_itemvalues_cleanvalue
ON library."ItemValues" ("CleanValue")
WHERE "CleanValue" IS NOT NULL;
-- Index 2: Value lookups (for direct value searches)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_itemvalues_value
ON library."ItemValues" ("Value")
WHERE "Value" IS NOT NULL;
-- Index 3: ItemValueId + CleanValue for joins (ItemValuesMap to ItemValues)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_itemvalues_id_cleanvalue
ON library."ItemValues" ("ItemValueId", "CleanValue");
"@
# Write SQL to temp file WITHOUT BOM (UTF8 without BOM)
[System.IO.File]::WriteAllText((Join-Path $PWD $tempSqlFile), $sqlContent, (New-Object System.Text.UTF8Encoding $false))
Write-Host "Executing index creation script..." -ForegroundColor Cyan
Write-Host ""
# Execute the SQL file (allows CONCURRENTLY to work)
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f $tempSqlFile
if ($LASTEXITCODE -eq 0) {
Write-Host ""
Write-Host "[OK] All 3 indexes created successfully!" -ForegroundColor Green
} else {
Write-Host ""
Write-Host "[FAILED] Some indexes may have failed to create" -ForegroundColor Red
Write-Host "Check the output above for details" -ForegroundColor Yellow
}
# Clean up temp file
Remove-Item $tempSqlFile -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Index Creation Complete!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Verify indexes were created
Write-Host "Verifying indexes..." -ForegroundColor Yellow
$queryVerify = "SELECT indexrelname as indexname, pg_size_pretty(pg_relation_size(indexrelid)) as size FROM pg_stat_user_indexes WHERE schemaname = 'library' AND relname = 'ItemValues' AND indexrelname LIKE 'idx_itemvalues_%' ORDER BY indexrelname;"
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c $queryVerify
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Cyan
Write-Host "1. Use Jellyfin normally - browse, filter by genre and tags" -ForegroundColor Gray
Write-Host "2. Wait 1 week" -ForegroundColor Gray
Write-Host "3. Run diagnostics again using db-quick.ps1" -ForegroundColor Gray
Write-Host "4. Check improvement in ItemValues sequential scans" -ForegroundColor Gray
Write-Host ""
Write-Host "Expected improvement: 70-90 percent faster genre and tag queries!" -ForegroundColor Green
+205
View File
@@ -0,0 +1,205 @@
# Jellyfin Windows Path Fixer
# This script checks and fixes Linux paths in Jellyfin configuration files
param(
[string]$ConfigPath = "C:\ProgramData\jellyfin\config",
[string]$DataPath = "C:\ProgramData\jellyfin\data",
[switch]$DryRun = $false,
[switch]$Force = $false
)
Write-Host "=================================================" -ForegroundColor Cyan
Write-Host "Jellyfin Windows Path Fixer" -ForegroundColor Cyan
Write-Host "=================================================" -ForegroundColor Cyan
Write-Host ""
# Check if config path exists
if (-not (Test-Path $ConfigPath)) {
Write-Host "ERROR: Config path not found: $ConfigPath" -ForegroundColor Red
Write-Host "Please specify the correct path using -ConfigPath parameter" -ForegroundColor Yellow
exit 1
}
Write-Host "Config Path: $ConfigPath" -ForegroundColor Green
Write-Host "Data Path: $DataPath" -ForegroundColor Green
Write-Host ""
$issuesFound = @()
# Function to check XML file for Linux paths
function Test-LinuxPathsInXml {
param(
[string]$FilePath,
[string]$FileName
)
if (-not (Test-Path $FilePath)) {
Write-Host " [SKIP] $FileName - File not found" -ForegroundColor Gray
return $null
}
try {
[xml]$xml = Get-Content $FilePath
$linuxPaths = @()
# Check all text nodes for Linux paths
$xml.SelectNodes("//*") | ForEach-Object {
if ($_.InnerText -match '^(/var/|/usr/|/etc/)') {
$linuxPaths += @{
Element = $_.Name
CurrentValue = $_.InnerText
Line = $_.OuterXml
}
}
}
if ($linuxPaths.Count -gt 0) {
Write-Host " [ISSUE] $FileName - Found $($linuxPaths.Count) Linux path(s)" -ForegroundColor Yellow
return @{
FilePath = $FilePath
FileName = $FileName
LinuxPaths = $linuxPaths
}
} else {
Write-Host " [OK] $FileName - No Linux paths found" -ForegroundColor Green
return $null
}
}
catch {
Write-Host " [ERROR] $FileName - Failed to parse: $_" -ForegroundColor Red
return $null
}
}
# Check configuration files
Write-Host "Checking configuration files..." -ForegroundColor Cyan
Write-Host ""
$systemXmlPath = Join-Path $ConfigPath "system.xml"
$result = Test-LinuxPathsInXml -FilePath $systemXmlPath -FileName "system.xml"
if ($result) {
$issuesFound += $result
foreach ($path in $result.LinuxPaths) {
Write-Host " - $($path.Element): $($path.CurrentValue)" -ForegroundColor Red
}
}
$encodingXmlPath = Join-Path $ConfigPath "encoding.xml"
$result = Test-LinuxPathsInXml -FilePath $encodingXmlPath -FileName "encoding.xml"
if ($result) {
$issuesFound += $result
foreach ($path in $result.LinuxPaths) {
Write-Host " - $($path.Element): $($path.CurrentValue)" -ForegroundColor Red
}
}
$databaseXmlPath = Join-Path $ConfigPath "database.xml"
$result = Test-LinuxPathsInXml -FilePath $databaseXmlPath -FileName "database.xml"
if ($result) {
$issuesFound += $result
foreach ($path in $result.LinuxPaths) {
Write-Host " - $($path.Element): $($path.CurrentValue)" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "=================================================" -ForegroundColor Cyan
# Summary
if ($issuesFound.Count -eq 0) {
Write-Host "No issues found! All paths are correct." -ForegroundColor Green
exit 0
}
Write-Host "Found issues in $($issuesFound.Count) file(s)" -ForegroundColor Yellow
Write-Host ""
# If DryRun, just show what would be done
if ($DryRun) {
Write-Host "DRY RUN MODE - No changes will be made" -ForegroundColor Cyan
Write-Host ""
Write-Host "The following changes would be made:" -ForegroundColor Yellow
foreach ($issue in $issuesFound) {
Write-Host ""
Write-Host "File: $($issue.FileName)" -ForegroundColor Cyan
foreach ($path in $issue.LinuxPaths) {
Write-Host " $($path.Element):" -ForegroundColor White
Write-Host " FROM: $($path.CurrentValue)" -ForegroundColor Red
if ($path.Element -in @("MetadataPath", "TranscodingTempPath")) {
Write-Host " TO: (empty - use default)" -ForegroundColor Green
} else {
$newPath = $path.CurrentValue -replace '^/var/lib/jellyfin', $DataPath -replace '/', '\'
Write-Host " TO: $newPath" -ForegroundColor Green
}
}
}
Write-Host ""
Write-Host "To apply these changes, run the script without -DryRun" -ForegroundColor Yellow
exit 0
}
# Ask for confirmation
if (-not $Force) {
Write-Host ""
Write-Host "WARNING: This will modify your configuration files!" -ForegroundColor Yellow
Write-Host "Backups will be created automatically." -ForegroundColor Yellow
Write-Host ""
$confirm = Read-Host "Do you want to proceed? (yes/no)"
if ($confirm -ne "yes") {
Write-Host "Operation cancelled." -ForegroundColor Red
exit 0
}
}
# Apply fixes
Write-Host ""
Write-Host "Applying fixes..." -ForegroundColor Cyan
Write-Host ""
foreach ($issue in $issuesFound) {
Write-Host "Fixing $($issue.FileName)..." -ForegroundColor Yellow
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$backupPath = "$($issue.FilePath).backup.$timestamp"
Copy-Item $issue.FilePath $backupPath
Write-Host " Backup created: $backupPath" -ForegroundColor Gray
[xml]$xml = Get-Content $issue.FilePath
foreach ($path in $issue.LinuxPaths) {
$element = $null
$xml.SelectNodes("//*") | ForEach-Object {
if ($_.InnerText -eq $path.CurrentValue) {
$element = $_
}
}
if ($element) {
if ($path.Element -in @("MetadataPath", "TranscodingTempPath")) {
$element.InnerText = ""
Write-Host " Cleared $($path.Element)" -ForegroundColor Green
} else {
$newPath = $path.CurrentValue -replace '^/var/lib/jellyfin', $DataPath -replace '/', '\'
$element.InnerText = $newPath
Write-Host " Updated $($path.Element) to: $newPath" -ForegroundColor Green
}
}
}
$xml.Save($issue.FilePath)
Write-Host " Saved $($issue.FileName)" -ForegroundColor Green
Write-Host ""
}
Write-Host "=================================================" -ForegroundColor Cyan
Write-Host "All fixes applied successfully!" -ForegroundColor Green
Write-Host ""
Write-Host "NEXT STEPS:" -ForegroundColor Yellow
Write-Host "1. Restart Jellyfin service/application" -ForegroundColor White
Write-Host "2. Check logs to verify paths are now correct" -ForegroundColor White
$configInfo = "3. If issues persist, review backups in: $ConfigPath"
Write-Host $configInfo -ForegroundColor White
Write-Host ""
+83
View File
@@ -0,0 +1,83 @@
# Fix startup.json WebDir paths
# This script finds and fixes incorrect WebDir paths in startup.json files
Write-Host "=================================================" -ForegroundColor Cyan
Write-Host "Jellyfin startup.json WebDir Fixer" -ForegroundColor Cyan
Write-Host "=================================================" -ForegroundColor Cyan
Write-Host ""
$searchPaths = @(
".",
".\bin\Debug\net11.0",
".\bin\Release\net11.0",
".\bin\Debug\net11.0\config",
".\bin\Release\net11.0\config"
)
$filesFound = 0
$filesFixed = 0
foreach ($path in $searchPaths) {
$jsonPath = Join-Path $path "startup.json"
if (Test-Path $jsonPath) {
$filesFound++
Write-Host "Found: $jsonPath" -ForegroundColor Yellow
try {
$content = Get-Content $jsonPath -Raw
$originalContent = $content
# Check for incorrect WebDir
if ($content -match '"WebDir":\s*"[^"]*C:/ProgramData/jellyfin/wwwroot[^"]*"') {
Write-Host " Status: Has incorrect WebDir path" -ForegroundColor Red
# Create backup
$backupPath = "$jsonPath.backup.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
Copy-Item $jsonPath $backupPath
Write-Host " Backup: $backupPath" -ForegroundColor Gray
# Fix the path
$content = $content -replace '"WebDir":\s*"C:/ProgramData/jellyfin/wwwroot"', '"WebDir": "wwwroot"'
# Also fix any data path if it has /data appended
$content = $content -replace '"WebDir":\s*"C:/ProgramData/jellyfin/data/wwwroot"', '"WebDir": "wwwroot"'
Set-Content -Path $jsonPath -Value $content -NoNewline
$filesFixed++
Write-Host " Result: FIXED!" -ForegroundColor Green
}
elseif ($content -match '"WebDir":\s*"wwwroot"') {
Write-Host " Status: Already correct (relative path)" -ForegroundColor Green
}
else {
Write-Host " Status: Has custom path (not changed)" -ForegroundColor Cyan
}
}
catch {
Write-Host " ERROR: Failed to process - $_" -ForegroundColor Red
}
Write-Host ""
}
}
Write-Host "=================================================" -ForegroundColor Cyan
if ($filesFound -eq 0) {
Write-Host "No startup.json files found." -ForegroundColor Yellow
Write-Host "Run Jellyfin once to create the configuration file." -ForegroundColor White
}
elseif ($filesFixed -eq 0) {
Write-Host "All files are already correct!" -ForegroundColor Green
}
else {
Write-Host "Fixed $filesFixed of $filesFound file(s)" -ForegroundColor Green
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Yellow
Write-Host "1. Rebuild your solution (if needed)" -ForegroundColor White
Write-Host "2. Run Jellyfin to verify WebDir path is correct" -ForegroundColor White
}
Write-Host ""
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Fixes StyleCop warnings in C# files.
.DESCRIPTION
Automatically fixes common StyleCop warnings:
- SA1413: Adds trailing commas to multi-line initializers
- SA1515: Adds blank lines before single-line comments
- SA1518: Ensures files end with single newline
.PARAMETER Path
The root path to search for C# files. Defaults to "MediaBrowser.Model"
.PARAMETER DryRun
If specified, shows what would be changed without actually modifying files
.EXAMPLE
.\FixStyleCopWarnings.ps1 -DryRun
#>
param(
[string]$Path = "MediaBrowser.Model",
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
function Fix-TrailingCommas {
param([string]$Content)
$modified = $false
$lines = $content -split '\r?\n'
for ($i = 0; $i -lt $lines.Length - 1; $i++) {
$currentLine = $lines[$i]
$nextLine = $lines[$i + 1]
# Check if current line ends with a property/value without comma
# and next line closes the initializer
if ($currentLine -match '^\s+\w+\s*=\s*.+[^,]\s*$' -and $nextLine -match '^\s*[}\)]') {
$lines[$i] = $currentLine.TrimEnd() + ','
$modified = $true
}
}
if ($modified) {
return ($lines -join "`n")
}
return $Content
}
function Fix-BlankLinesBeforeComments {
param([string]$Content)
$modified = $false
$lines = $content -split '\r?\n'
$newLines = @()
for ($i = 0; $i -lt $lines.Length; $i++) {
$currentLine = $lines[$i]
# Check if this is a single-line comment
if ($currentLine -match '^\s+//\s+\w') {
# Check if previous line exists and is not blank
if ($i -gt 0) {
$prevLine = $lines[$i - 1]
$prevLineIsBlank = [string]::IsNullOrWhiteSpace($prevLine)
$prevLineIsOpenBrace = $prevLine -match '\{\s*$'
$prevLineIsComment = $prevLine -match '^\s*//'
# Add blank line if previous line is not blank, not open brace, not another comment
if (-not $prevLineIsBlank -and -not $prevLineIsOpenBrace -and -not $prevLineIsComment) {
$newLines += ""
$modified = $true
}
}
}
$newLines += $currentLine
}
if ($modified) {
return ($newLines -join "`n")
}
return $Content
}
function Fix-FileEnding {
param([string]$Content)
# Ensure file ends with exactly one newline
$trimmed = $Content.TrimEnd("`r", "`n", " ", "`t")
return $trimmed + "`n"
}
function Fix-StyleCopWarnings {
param(
[string]$FilePath,
[switch]$DryRun
)
$content = Get-Content -Path $FilePath -Raw
$originalContent = $content
# Apply fixes
$content = Fix-TrailingCommas -Content $content
$content = Fix-BlankLinesBeforeComments -Content $content
$content = Fix-FileEnding -Content $content
# Restore Windows line endings if original had them
if ($originalContent -match '\r\n') {
$content = $content -replace '(?<!\r)\n', "`r`n"
}
if ($content -ne $originalContent) {
if ($DryRun) {
Write-Host " [DRY-RUN] Would fix StyleCop warnings" -ForegroundColor Cyan
return $true
}
[System.IO.File]::WriteAllText($FilePath, $content, [System.Text.UTF8Encoding]::new($false))
Write-Host " [FIXED]" -ForegroundColor Green
return $true
}
Write-Host " [SKIP] No warnings to fix" -ForegroundColor Gray
return $false
}
# Main script
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "StyleCop Warning Fixer" -ForegroundColor Cyan
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host
if ($DryRun) {
Write-Host "DRY RUN MODE - No files will be modified" -ForegroundColor Yellow
Write-Host
}
if (-not (Test-Path $Path)) {
Write-Error "Path not found: $Path"
exit 1
}
$files = Get-ChildItem -Path $Path -Filter "*.cs" -Recurse | Where-Object {
$_.FullName -notmatch "\\obj\\" -and
$_.FullName -notmatch "\\bin\\"
}
Write-Host "Found $($files.Count) C# files in $Path" -ForegroundColor Cyan
Write-Host
$fixedCount = 0
$skippedCount = 0
$errorCount = 0
foreach ($file in $files) {
$relativePath = $file.FullName.Replace((Get-Location).Path, "").TrimStart([char]92, [char]47)
Write-Host "[Processing] $relativePath" -ForegroundColor White
try {
$fixed = Fix-StyleCopWarnings -FilePath $file.FullName -DryRun:$DryRun
if ($fixed) {
$fixedCount++
} else {
$skippedCount++
}
}
catch {
Write-Host " [ERROR] $_" -ForegroundColor Red
$errorCount++
}
}
Write-Host
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "Summary:" -ForegroundColor Cyan
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host " Total files: $($files.Count)" -ForegroundColor White
Write-Host " Fixed: $fixedCount" -ForegroundColor Green
Write-Host " Skipped: $skippedCount" -ForegroundColor Gray
Write-Host " Errors: $errorCount" -ForegroundColor Red
if ($DryRun) {
Write-Host
Write-Host "This was a DRY RUN. Run without -DryRun to apply changes." -ForegroundColor Yellow
}
Write-Host ("=" * 80) -ForegroundColor Cyan
exit $errorCount
+237
View File
@@ -0,0 +1,237 @@
#Requires -Version 5.1
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Sets up automated weekly performance monitoring via Windows Task Scheduler
.DESCRIPTION
Creates a scheduled task to run Weekly-Performance-Monitor.ps1 every Monday at 6 AM.
Can optionally enable conditional VACUUM ANALYZE based on dead tuple thresholds.
.PARAMETER Uninstall
Remove the scheduled task
.PARAMETER TaskTime
Time to run the task (default: 6am)
.PARAMETER DayOfWeek
Day of week to run (default: Monday)
.PARAMETER EnableAutoVacuumAnalyze
Enable conditional VACUUM ANALYZE for tables above dead tuple thresholds
.PARAMETER DeadTupleThreshold
Minimum dead tuples before VACUUM ANALYZE runs (default: 10000)
.PARAMETER DeadTuplePercentThreshold
Minimum dead tuple percentage before VACUUM ANALYZE runs (default: 20)
.EXAMPLE
.\Setup-Weekly-Monitor.ps1
.EXAMPLE
.\Setup-Weekly-Monitor.ps1 -TaskTime "3am" -DayOfWeek Sunday
.EXAMPLE
.\Setup-Weekly-Monitor.ps1 -EnableAutoVacuumAnalyze -DeadTupleThreshold 10000 -DeadTuplePercentThreshold 20
.EXAMPLE
.\Setup-Weekly-Monitor.ps1 -Uninstall
#>
[CmdletBinding()]
param (
[Parameter()]
[switch]$Uninstall,
[Parameter()]
[string]$TaskTime = "6am",
[Parameter()]
[ValidateSet('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday')]
[string]$DayOfWeek = 'Monday',
[Parameter()]
[switch]$EnableAutoVacuumAnalyze,
[Parameter()]
[ValidateRange(1, [int]::MaxValue)]
[int]$DeadTupleThreshold = 10000,
[Parameter()]
[ValidateRange(0.01, 100.0)]
[double]$DeadTuplePercentThreshold = 20.0
)
$ErrorActionPreference = "Stop"
$TaskName = "Jellyfin Database Weekly Monitor"
$ScriptRoot = Split-Path -Parent $PSCommandPath
$MonitorScript = Join-Path $ScriptRoot "Weekly-Performance-Monitor.ps1"
# ============================================================================
# Functions
# ============================================================================
function Write-ColorOutput {
param(
[string]$Message,
[ConsoleColor]$Color = 'White'
)
Write-Host $Message -ForegroundColor $Color
}
function Test-IsAdmin {
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
return $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
# ============================================================================
# Main
# ============================================================================
Write-ColorOutput "`n========================================" -Color Cyan
Write-ColorOutput "Jellyfin DB Monitor Setup" -Color Cyan
Write-ColorOutput "========================================`n" -Color Cyan
# Check if running as administrator
if (-not (Test-IsAdmin)) {
Write-ColorOutput "❌ This script must be run as Administrator" -Color Red
Write-ColorOutput " Right-click PowerShell and select 'Run as Administrator'" -Color Yellow
exit 1
}
# Uninstall if requested
if ($Uninstall) {
Write-ColorOutput "Removing scheduled task..." -Color Yellow
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($task) {
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
Write-ColorOutput "✅ Scheduled task removed successfully" -Color Green
} else {
Write-ColorOutput "⚠️ Scheduled task not found" -Color Yellow
}
exit 0
}
# Check if monitor script exists
if (-not (Test-Path $MonitorScript)) {
Write-ColorOutput "❌ Monitor script not found: $MonitorScript" -Color Red
exit 1
}
Write-ColorOutput "Configuration:" -Color Cyan
Write-ColorOutput " Task Name: $TaskName" -Color White
Write-ColorOutput " Script: $MonitorScript" -Color White
Write-ColorOutput " Schedule: Every $DayOfWeek at $TaskTime" -Color White
if ($EnableAutoVacuumAnalyze) {
Write-ColorOutput " Auto VACUUM ANALYZE: Enabled (dead tuples >= $DeadTupleThreshold, dead percent >= $DeadTuplePercentThreshold)" -Color White
} else {
Write-ColorOutput " Auto VACUUM ANALYZE: Disabled" -Color White
}
Write-ColorOutput ""
# Remove existing task if present
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($existingTask) {
Write-ColorOutput "⚠️ Existing task found. Removing..." -Color Yellow
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
}
# Create scheduled task action
Write-ColorOutput "Creating scheduled task..." -Color Cyan
$scriptArguments = "-NoProfile -ExecutionPolicy Bypass -File `"$MonitorScript`""
if ($EnableAutoVacuumAnalyze) {
$scriptArguments += " -AutoVacuumAnalyze -DeadTupleThreshold $DeadTupleThreshold -DeadTuplePercentThreshold $DeadTuplePercentThreshold"
}
$action = New-ScheduledTaskAction `
-Execute "PowerShell.exe" `
-Argument $scriptArguments
# Create trigger
$trigger = New-ScheduledTaskTrigger `
-Weekly `
-DaysOfWeek $DayOfWeek `
-At $TaskTime
# Task settings
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-RunOnlyIfNetworkAvailable `
-ExecutionTimeLimit (New-TimeSpan -Hours 2)
# Task principal (run as current user)
$principal = New-ScheduledTaskPrincipal `
-UserId $env:USERNAME `
-LogonType S4U `
-RunLevel Highest
# Register the task
try {
Register-ScheduledTask `
-TaskName $TaskName `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Principal $principal `
-Description "Automated weekly performance monitoring for Jellyfin PostgreSQL database" | Out-Null
Write-ColorOutput "✅ Scheduled task created successfully!" -Color Green
Write-ColorOutput ""
# Verify task
$task = Get-ScheduledTask -TaskName $TaskName
$nextRun = $task.Triggers[0].StartBoundary
Write-ColorOutput "Task Details:" -Color Cyan
Write-ColorOutput " Status: $($task.State)" -Color White
Write-ColorOutput " Next Run: $nextRun" -Color White
Write-ColorOutput ""
# Test run option
Write-ColorOutput "Would you like to test the task now? (y/n): " -Color Yellow -NoNewline
$response = Read-Host
if ($response -eq 'y' -or $response -eq 'Y') {
Write-ColorOutput "`nRunning test..." -Color Cyan
Start-ScheduledTask -TaskName $TaskName
Start-Sleep -Seconds 2
$taskInfo = Get-ScheduledTask -TaskName $TaskName | Get-ScheduledTaskInfo
Write-ColorOutput "Last Run Time: $($taskInfo.LastRunTime)" -Color White
Write-ColorOutput "Last Result: $($taskInfo.LastTaskResult)" -Color White
if ($taskInfo.LastTaskResult -eq 0) {
Write-ColorOutput "✅ Test run completed successfully!" -Color Green
} else {
Write-ColorOutput "⚠️ Test run completed with code: $($taskInfo.LastTaskResult)" -Color Yellow
}
}
} catch {
Write-ColorOutput "❌ Failed to create scheduled task: $_" -Color Red
exit 1
}
Write-ColorOutput "`n========================================" -Color Green
Write-ColorOutput "Setup Complete!" -Color Green
Write-ColorOutput "========================================" -Color Green
Write-ColorOutput ""
Write-ColorOutput "Monitoring is now automated!" -Color Cyan
Write-ColorOutput ""
Write-ColorOutput "To manage the task:" -Color White
Write-ColorOutput " View: Get-ScheduledTask -TaskName '$TaskName'" -Color Gray
Write-ColorOutput " Run now: Start-ScheduledTask -TaskName '$TaskName'" -Color Gray
Write-ColorOutput " Disable: Disable-ScheduledTask -TaskName '$TaskName'" -Color Gray
Write-ColorOutput " Remove: .\Setup-Weekly-Monitor.ps1 -Uninstall" -Color Gray
Write-ColorOutput ""
Write-ColorOutput "Reports will be saved to: reports\weekly" -Color Yellow
Write-ColorOutput "========================================`n" -Color Green
@@ -0,0 +1,483 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Automated weekly performance monitoring for Jellyfin PostgreSQL database
.DESCRIPTION
This script:
1. Runs comprehensive diagnostics
2. Analyzes slow queries
3. Tracks index usage over time
4. Generates weekly performance reports
5. Optionally runs VACUUM ANALYZE on tables with high dead tuples
6. Can be scheduled via Task Scheduler
.PARAMETER OutputDirectory
Directory to save reports (default: reports/weekly)
.PARAMETER EmailReport
If specified, emails the report (requires email configuration)
.PARAMETER CompareWithPrevious
Compare current stats with previous week
.EXAMPLE
.\Weekly-Performance-Monitor.ps1
.EXAMPLE
.\Weekly-Performance-Monitor.ps1 -OutputDirectory "C:\Reports\Jellyfin" -CompareWithPrevious
.EXAMPLE
.\Weekly-Performance-Monitor.ps1 -AutoVacuumAnalyze -DeadTupleThreshold 10000 -DeadTuplePercentThreshold 20
.EXAMPLE
# Schedule weekly via Task Scheduler
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-File C:\Path\To\Weekly-Performance-Monitor.ps1"
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 6am
Register-ScheduledTask -TaskName "Jellyfin DB Monitor" -Action $action -Trigger $trigger
.NOTES
Author: Generated for Jellyfin PostgreSQL Project
Date: 2026-03-05
#>
[CmdletBinding()]
param (
[Parameter()]
[string]$OutputDirectory = "reports\weekly",
[Parameter()]
[switch]$EmailReport,
[Parameter()]
[switch]$CompareWithPrevious,
[Parameter()]
[switch]$AutoVacuumAnalyze,
[Parameter()]
[ValidateRange(1, [int]::MaxValue)]
[int]$DeadTupleThreshold = 100,
[Parameter()]
[ValidateRange(0.01, 100.0)]
[double]$DeadTuplePercentThreshold = 0.02
)
# ============================================================================
# Configuration
# ============================================================================
$ErrorActionPreference = "Stop"
$ScriptRoot = Split-Path -Parent $PSCommandPath
$ProjectRoot = Split-Path -Parent $ScriptRoot
# Load database configuration
. "$ProjectRoot\scripts\db-config.ps1"
# ============================================================================
# Functions
# ============================================================================
function Write-ColorOutput {
param(
[string]$Message,
[ConsoleColor]$Color = 'White'
)
Write-Host $Message -ForegroundColor $Color
}
function Get-ReportTimestamp {
return Get-Date -Format "yyyy-MM-dd_HHmmss"
}
function Get-WeekNumber {
$date = Get-Date
$culture = [System.Globalization.CultureInfo]::CurrentCulture
return $culture.Calendar.GetWeekOfYear($date,
[System.Globalization.CalendarWeekRule]::FirstDay,
[DayOfWeek]::Monday)
}
function Test-DatabaseConnection {
Write-ColorOutput "`nTesting database connection..." -Color Cyan
try {
$result = & $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "SELECT version();" 2>&1
if ($LASTEXITCODE -eq 0) {
Write-ColorOutput "[OK] Database connection successful" -Color Green
return $true
} else {
Write-ColorOutput "[ERROR] Database connection failed: $result" -Color Red
return $false
}
} catch {
Write-ColorOutput "[ERROR] Database connection error: $_" -Color Red
return $false
}
}
function Invoke-DiagnosticReport {
param([string]$OutputFile)
Write-ColorOutput "`nRunning comprehensive diagnostics..." -Color Cyan
try {
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$ProjectRoot\sql\diagnostics.sql" > $OutputFile 2>&1
if ($LASTEXITCODE -eq 0) {
Write-ColorOutput "[OK] Diagnostics completed: $OutputFile" -Color Green
return $true
} else {
Write-ColorOutput "[WARN] Diagnostics completed with warnings" -Color Yellow
return $true
}
} catch {
Write-ColorOutput "[ERROR] Diagnostics failed: $_" -Color Red
return $false
}
}
function Invoke-QueryAnalysis {
param([string]$OutputFile)
Write-ColorOutput "`nAnalyzing slow queries..." -Color Cyan
try {
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$ProjectRoot\sql\query-analysis.sql" > $OutputFile 2>&1
if ($LASTEXITCODE -eq 0) {
Write-ColorOutput "[OK] Query analysis completed: $OutputFile" -Color Green
return $true
} else {
Write-ColorOutput "[WARN] Query analysis completed with warnings" -Color Yellow
return $true
}
} catch {
Write-ColorOutput "[ERROR] Query analysis failed: $_" -Color Red
return $false
}
}
function Get-IndexUsageStats {
param([string]$OutputFile)
Write-ColorOutput "`nGathering index usage statistics..." -Color Cyan
$query = @"
SELECT
schemaname,
relname AS tablename,
indexrelname,
idx_scan,
idx_tup_read,
idx_tup_fetch,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
ORDER BY schemaname, relname, idx_scan DESC;
"@
try {
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c $query > $OutputFile 2>&1
if ($LASTEXITCODE -eq 0) {
Write-ColorOutput "[OK] Index stats gathered: $OutputFile" -Color Green
return $true
} else {
Write-ColorOutput "[WARN] Index stats completed with warnings" -Color Yellow
return $true
}
} catch {
Write-ColorOutput "[ERROR] Index stats failed: $_" -Color Red
return $false
}
}
function Get-TableSizeStats {
param([string]$OutputFile)
Write-ColorOutput "`nGathering table size statistics..." -Color Cyan
$query = @"
SELECT
schemaname,
relname,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) AS total_size,
pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) AS table_size,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname) - pg_relation_size(schemaname||'.'||relname)) AS index_size,
n_live_tup AS row_count,
n_dead_tup AS dead_rows,
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
ORDER BY pg_total_relation_size(schemaname||'.'||relname) DESC;
"@
try {
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c $query > $OutputFile 2>&1
if ($LASTEXITCODE -eq 0) {
Write-ColorOutput "[OK] Table size stats gathered: $OutputFile" -Color Green
return $true
} else {
Write-ColorOutput "[WARN] Table size stats completed with warnings" -Color Yellow
return $true
}
} catch {
Write-ColorOutput "[ERROR] Table size stats failed: $_" -Color Red
return $false
}
}
function Invoke-ConditionalVacuumAnalyze {
param(
[int]$MinDeadTuples,
[double]$MinDeadPercent
)
Write-ColorOutput "`nChecking for tables that need VACUUM ANALYZE..." -Color Cyan
Write-ColorOutput "Thresholds: dead tuples >= $MinDeadTuples and dead percent >= $MinDeadPercent" -Color Gray
$tableQuery = @"
SELECT
format('%I.%I', schemaname, relname) AS qualified_table,
n_dead_tup,
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
AND n_live_tup > 0
AND n_dead_tup >= $MinDeadTuples
AND (100.0 * n_dead_tup / NULLIF(n_live_tup, 0)) >= $MinDeadPercent
ORDER BY n_dead_tup DESC;
"@
try {
$tables = & $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -t -A -F "|" -c $tableQuery 2>&1
if ($LASTEXITCODE -ne 0) {
Write-ColorOutput "[ERROR] Failed to evaluate dead tuple thresholds: $tables" -Color Red
return $false
}
$tableLines = @($tables | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
if ($tableLines.Count -eq 0) {
Write-ColorOutput "[OK] No tables exceeded dead tuple thresholds" -Color Green
return $true
}
Write-ColorOutput "Found $($tableLines.Count) table(s) requiring VACUUM ANALYZE" -Color Yellow
foreach ($line in $tableLines) {
$parts = $line -split "\|", 3
$qualifiedTable = $parts[0].Trim()
$deadTuples = if ($parts.Count -ge 2) { $parts[1].Trim() } else { "unknown" }
$deadPercent = if ($parts.Count -ge 3) { $parts[2].Trim() } else { "unknown" }
Write-ColorOutput "Running VACUUM ANALYZE on $qualifiedTable (dead tuples: $deadTuples, dead percent: $deadPercent)" -Color Cyan
$vacuumResult = & $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "VACUUM (ANALYZE, VERBOSE) $qualifiedTable;" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-ColorOutput "[ERROR] VACUUM ANALYZE failed for $qualifiedTable : $vacuumResult" -Color Red
return $false
}
Write-ColorOutput "[OK] VACUUM ANALYZE completed for $qualifiedTable" -Color Green
}
return $true
} catch {
Write-ColorOutput "[ERROR] Conditional VACUUM ANALYZE failed: $_" -Color Red
return $false
}
}
function Compare-WithPrevious {
param(
[string]$CurrentReport,
[string]$OutputDirectory
)
Write-ColorOutput "`nComparing with previous week's report..." -Color Cyan
# Find most recent previous report
$previousReports = Get-ChildItem "$OutputDirectory\diagnostics_*.txt" -ErrorAction SilentlyContinue |
Where-Object { $_.Name -ne (Split-Path $CurrentReport -Leaf) } |
Sort-Object LastWriteTime -Descending
if ($previousReports.Count -eq 0) {
Write-ColorOutput "[WARN] No previous reports found for comparison" -Color Yellow
return
}
$previousReport = $previousReports[0].FullName
Write-ColorOutput "Comparing with: $($previousReports[0].Name)" -Color Cyan
# TODO: Add detailed comparison logic
# For now, just note that both reports exist
Write-ColorOutput "[OK] Comparison data available" -Color Green
Write-ColorOutput " Current: $CurrentReport" -Color Gray
Write-ColorOutput " Previous: $previousReport" -Color Gray
}
function New-SummaryReport {
param(
[string]$OutputDirectory,
[string]$Timestamp
)
Write-ColorOutput "`nGenerating summary report..." -Color Cyan
$summaryFile = Join-Path $OutputDirectory "summary_$Timestamp.txt"
$summary = @"
================================================================================
JELLYFIN DATABASE WEEKLY PERFORMANCE SUMMARY
================================================================================
Report Date: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
Database: $DB_NAME at $DB_HOST
Week Number: $(Get-WeekNumber)
================================================================================
REPORTS GENERATED:
* diagnostics_$Timestamp.txt : Full diagnostic report
* query_analysis_$Timestamp.txt : Slow query analysis
* index_usage_$Timestamp.txt : Index usage statistics
* table_sizes_$Timestamp.txt : Table size and bloat info
* summary_$Timestamp.txt : This summary
================================================================================
QUICK HEALTH CHECK:
================================================================================
To view critical issues check diagnostics file for:
* Section 5: TABLES WITH HIGH SEQUENTIAL SCANS
* Section 8: UNUSED OR RARELY USED INDEXES
* Section 10: SLOWEST QUERIES
To identify slow queries check query_analysis file for:
* Section 2: TOP 20 SLOWEST QUERIES (by max execution time)
* Section 5: BASEITEMS TABLE QUERIES ANALYSIS
================================================================================
RECOMMENDED ACTIONS:
================================================================================
* Review slow queries taking longer than 10 seconds
* Check if new indexes are being utilized
* Look for tables with many sequential scans
* Monitor cache hit ratio (should be above 95 percent)
* Check for tables with high dead tuples percentage (need VACUUM)
================================================================================
AUTOMATION SETUP:
================================================================================
To schedule this script weekly via Task Scheduler run Setup-Weekly-Monitor.ps1
as Administrator. This will create a scheduled task to run every Monday at 6am.
To disable the scheduled task run Setup-Weekly-Monitor.ps1 with -Uninstall flag.
================================================================================
NEXT REPORT: $(Get-Date).AddDays(7).ToString("yyyy-MM-dd")
================================================================================
"@
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
Write-ColorOutput "[OK] Summary generated: $summaryFile" -Color Green
}
# ============================================================================
# Main Execution
# ============================================================================
Write-ColorOutput "`n========================================" -Color Cyan
Write-ColorOutput "Jellyfin Database Weekly Monitor" -Color Cyan
Write-ColorOutput "========================================" -Color Cyan
Write-ColorOutput "Database: $DB_NAME" -Color Yellow
Write-ColorOutput "Host: $DB_HOST" -Color Yellow
Write-ColorOutput "Week: $(Get-WeekNumber)" -Color Yellow
Write-ColorOutput "Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -Color Yellow
Write-ColorOutput "========================================`n" -Color Cyan
# Create output directory
$reportDir = Join-Path $ProjectRoot $OutputDirectory
if (-not (Test-Path $reportDir)) {
New-Item -ItemType Directory -Path $reportDir -Force | Out-Null
Write-ColorOutput "Created report directory: $reportDir" -Color Green
}
# Generate timestamp for this run
$timestamp = Get-ReportTimestamp
# Test connection
if (-not (Test-DatabaseConnection)) {
Write-ColorOutput "`n[ERROR] Cannot connect to database. Exiting." -Color Red
exit 1
}
# Run diagnostics
$diagnosticsFile = Join-Path $reportDir "diagnostics_$timestamp.txt"
$success = Invoke-DiagnosticReport -OutputFile $diagnosticsFile
if (-not $success) {
Write-ColorOutput "`n[WARN] Some reports failed. Check output directory." -Color Yellow
}
# Run query analysis
$queryAnalysisFile = Join-Path $reportDir "query_analysis_$timestamp.txt"
Invoke-QueryAnalysis -OutputFile $queryAnalysisFile | Out-Null
# Gather index usage stats
$indexUsageFile = Join-Path $reportDir "index_usage_$timestamp.txt"
Get-IndexUsageStats -OutputFile $indexUsageFile | Out-Null
# Gather table size stats
$tableSizesFile = Join-Path $reportDir "table_sizes_$timestamp.txt"
Get-TableSizeStats -OutputFile $tableSizesFile | Out-Null
# Optionally vacuum/analyze tables that exceed dead tuple thresholds
if ($AutoVacuumAnalyze) {
$vacuumSuccess = Invoke-ConditionalVacuumAnalyze `
-MinDeadTuples $DeadTupleThreshold `
-MinDeadPercent $DeadTuplePercentThreshold
if (-not $vacuumSuccess) {
Write-ColorOutput "[WARN] Conditional VACUUM ANALYZE encountered errors" -Color Yellow
}
}
# Compare with previous week if requested
if ($CompareWithPrevious) {
Compare-WithPrevious -CurrentReport $diagnosticsFile -OutputDirectory $reportDir
}
# Generate summary
New-SummaryReport -OutputDirectory $reportDir -Timestamp $timestamp
# Cleanup old reports (keep last 12 weeks)
Write-ColorOutput "`nCleaning up old reports (keeping last 12 weeks)..." -Color Cyan
Get-ChildItem $reportDir -File |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-84) } |
Remove-Item -Force
Write-ColorOutput "[OK] Cleanup complete" -Color Green
Write-ColorOutput "`n========================================" -Color Green
Write-ColorOutput "[OK] WEEKLY MONITORING COMPLETE" -Color Green
Write-ColorOutput "========================================" -Color Green
Write-ColorOutput "Reports saved to: $reportDir" -Color Yellow
Write-ColorOutput "Summary: summary_$timestamp.txt" -Color Yellow
Write-ColorOutput "`nNext steps:" -Color Cyan
Write-ColorOutput "1. Review summary_$timestamp.txt" -Color White
Write-ColorOutput "2. Check for critical issues in diagnostics report" -Color White
Write-ColorOutput "3. Analyze slow queries in query_analysis report" -Color White
Write-ColorOutput "========================================`n" -Color Green
# Email report if requested (placeholder)
if ($EmailReport) {
Write-ColorOutput "[WARN] Email functionality not yet implemented" -Color Yellow
Write-ColorOutput " Configure Send-MailMessage with your SMTP settings" -Color Gray
}
exit 0
+162
View File
@@ -0,0 +1,162 @@
# Build Jellyfin Installer
# This script builds the Jellyfin installer using Inno Setup
param(
[Parameter(Mandatory=$false)]
[string]$Version = "11.0.0-PostgreSQL-PREVIEW",
[Parameter(Mandatory=$false)]
[ValidateSet("Release", "Debug")]
[string]$Configuration = "Release",
[Parameter(Mandatory=$false)]
[switch]$SkipBuild
)
Write-Host "`n╔══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ Jellyfin PostgreSQL Installer Builder ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════╝`n" -ForegroundColor Cyan
$ErrorActionPreference = "Stop"
$ScriptDir = $PSScriptRoot
$SolutionRoot = "E:\Projects\pgsql-jellyfin"
$InnoSetupPath = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
# Check prerequisites
Write-Host "Checking prerequisites..." -ForegroundColor Yellow
# Check if Inno Setup is installed
if (-not (Test-Path $InnoSetupPath)) {
Write-Host "❌ Inno Setup 6 not found!" -ForegroundColor Red
Write-Host "Please install it from: https://jrsoftware.org/isinfo.php" -ForegroundColor Yellow
Write-Host "Or run: winget install JRSoftware.InnoSetup" -ForegroundColor Yellow
exit 1
}
Write-Host "✅ Inno Setup found" -ForegroundColor Green
# Check if lib folder exists
if (-not (Test-Path "$SolutionRoot\lib\$Configuration\net11.0")) {
if ($SkipBuild) {
Write-Host "❌ Build folder not found: $SolutionRoot\lib\$Configuration\net11.0" -ForegroundColor Red
exit 1
}
Write-Host "⚠️ Build folder not found, building solution first..." -ForegroundColor Yellow
}
# Build the solution if needed
if (-not $SkipBuild) {
Write-Host "`nBuilding Jellyfin solution..." -ForegroundColor Cyan
Push-Location $SolutionRoot
try {
# Clean previous build
Write-Host "Cleaning previous build..." -ForegroundColor Gray
dotnet clean --configuration $Configuration | Out-Null
# Build solution
Write-Host "Building $Configuration configuration..." -ForegroundColor Gray
$buildOutput = dotnet build Jellyfin.Server --configuration $Configuration 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ Build failed!" -ForegroundColor Red
Write-Host $buildOutput
exit 1
}
Write-Host "✅ Build successful" -ForegroundColor Green
# Count DLLs
$dllCount = (Get-ChildItem "$SolutionRoot\lib\$Configuration\net11.0" -Filter "*.dll" -Recurse).Count
Write-Host "📊 Built $dllCount DLL files" -ForegroundColor Gray
}
finally {
Pop-Location
}
}
# Check if installer script exists
$InnoScriptPath = "$SolutionRoot\jellyfin-setup.iss"
if (-not (Test-Path $InnoScriptPath)) {
Write-Host "❌ Installer script not found: $InnoScriptPath" -ForegroundColor Red
Write-Host "Please ensure jellyfin-setup.iss is in the solution root" -ForegroundColor Yellow
exit 1
}
# Check for icon file (optional)
$IconPath = "$SolutionRoot\jellyfin.ico"
if (-not (Test-Path $IconPath)) {
Write-Host "⚠️ Icon file not found: $IconPath" -ForegroundColor Yellow
Write-Host "The installer will use the default Inno Setup icon" -ForegroundColor Gray
}
# Update version in script if needed
Write-Host "`nPreparing installer script..." -ForegroundColor Cyan
$scriptContent = Get-Content $InnoScriptPath -Raw
$scriptContent = $scriptContent -replace 'AppVersion=[\d\.]+', "AppVersion=$Version"
$scriptContent = $scriptContent -replace 'OutputBaseFilename=JellyfinSetup-PostgreSQL-[\d\.]+', "OutputBaseFilename=JellyfinSetup-PostgreSQL-$Version"
Set-Content $InnoScriptPath $scriptContent -NoNewline
Write-Host "✅ Version updated to $Version" -ForegroundColor Green
# Create output directory
$OutputDir = "$SolutionRoot\installer-output"
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
# Build the installer
Write-Host "`nBuilding installer..." -ForegroundColor Cyan
Write-Host "Script: $InnoScriptPath" -ForegroundColor Gray
Write-Host "Output: $OutputDir" -ForegroundColor Gray
Push-Location $SolutionRoot
try {
$compileOutput = & $InnoSetupPath $InnoScriptPath 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ Installer build failed!" -ForegroundColor Red
Write-Host $compileOutput
exit 1
}
Write-Host "✅ Installer built successfully!" -ForegroundColor Green
}
finally {
Pop-Location
}
# Find the installer file
$installerFile = Get-ChildItem $OutputDir -Filter "JellyfinSetup-PostgreSQL-*.exe" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($installerFile) {
$fileSize = [math]::Round($installerFile.Length / 1MB, 2)
Write-Host "`n╔══════════════════════════════════════════════════════════╗" -ForegroundColor Green
Write-Host "║ Installer Build Complete! ✅ ║" -ForegroundColor Green
Write-Host "╚══════════════════════════════════════════════════════════╝`n" -ForegroundColor Green
Write-Host "📦 Installer Details:" -ForegroundColor Cyan
Write-Host " File: $($installerFile.Name)" -ForegroundColor White
Write-Host " Path: $($installerFile.FullName)" -ForegroundColor White
Write-Host " Size: $fileSize MB" -ForegroundColor White
Write-Host " Date: $($installerFile.LastWriteTime)" -ForegroundColor White
Write-Host "`n📋 Next Steps:" -ForegroundColor Cyan
Write-Host " 1. Test the installer on a clean Windows system" -ForegroundColor Gray
Write-Host " 2. Sign the installer (optional): signtool sign /f cert.pfx installer.exe" -ForegroundColor Gray
Write-Host " 3. Upload to GitHub releases or your distribution server" -ForegroundColor Gray
Write-Host "`n🧪 Test Commands:" -ForegroundColor Cyan
Write-Host " # Silent install" -ForegroundColor Gray
Write-Host " $($installerFile.FullName) /VERYSILENT /LOG=install.log" -ForegroundColor DarkGray
Write-Host " # Normal install" -ForegroundColor Gray
Write-Host " $($installerFile.FullName)" -ForegroundColor DarkGray
# Open output folder
Write-Host "`nOpening output folder..." -ForegroundColor Cyan
Start-Process $OutputDir
}
else {
Write-Host "❌ Installer file not found in output directory!" -ForegroundColor Red
exit 1
}
Write-Host "`n✅ All done!`n" -ForegroundColor Green
+22
View File
@@ -0,0 +1,22 @@
# copy-sql-files.ps1
# Run this after publish to copy SQL files
param(
[string]$PublishDir = "Jellyfin.Server\bin\Release\net11.0\publish"
)
Write-Host "Copying SQL files to $PublishDir..." -ForegroundColor Cyan
# Create sql directory
New-Item -ItemType Directory -Path "$PublishDir\sql" -Force | Out-Null
New-Item -ItemType Directory -Path "$PublishDir\sql\schema_init" -Force | Out-Null
# Copy SQL files
Copy-Item "Jellyfin.Server\sql\*.sql" "$PublishDir\sql\" -Force
Copy-Item "Jellyfin.Server\sql\schema_init\*.sql" "$PublishDir\sql\schema_init\" -Force -ErrorAction SilentlyContinue
Write-Host "✓ SQL files copied successfully!" -ForegroundColor Green
# List copied files
Write-Host "`nCopied files:" -ForegroundColor Yellow
Get-ChildItem "$PublishDir\sql" -Recurse -File | Select-Object FullName
+57
View File
@@ -0,0 +1,57 @@
# Database Configuration
# Edit this file to change which database commands run against
# Database connection settings
$PSQL_PATH = "C:\Program Files\PostgreSQL\18\bin\psql.exe"
$DB_USER = "jellyfin"
$DB_NAME = "jellyfin_test2" # ← Change this to switch databases
$DB_HOST = "192.168.129.253"
$DB_PORT = "6432"
# Export for use in other scripts
$global:PSQL_PATH = $PSQL_PATH
$global:DB_USER = $DB_USER
$global:DB_NAME = $DB_NAME
$global:DB_HOST = $DB_HOST
$global:DB_PORT = $DB_PORT
# Helper function to run psql commands
function Invoke-PSQL {
param(
[string]$Query,
[string]$File,
[string]$OutputFile
)
$baseCmd = "& `"$PSQL_PATH`" -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME"
if ($File) {
$cmd = "$baseCmd -f `"$File`""
} elseif ($Query) {
$cmd = "$baseCmd -c `"$Query`""
} else {
Write-Error "Must provide either -Query or -File parameter"
return
}
if ($OutputFile) {
$cmd += " > `"$OutputFile`""
}
Write-Host "Connecting to: $DB_NAME@$DB_HOST as $DB_USER" -ForegroundColor Cyan
Invoke-Expression $cmd
}
# Display current configuration
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Database Configuration Loaded" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Database: $DB_NAME" -ForegroundColor Yellow
Write-Host "User: $DB_USER" -ForegroundColor Yellow
Write-Host "Host: $DB_HOST" -ForegroundColor Yellow
Write-Host "Port: $DB_PORT" -ForegroundColor Yellow
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "To change database, edit: db-config.ps1" -ForegroundColor Gray
Write-Host "Then run: . .\db-config.ps1" -ForegroundColor Gray
Write-Host ""
+66
View File
@@ -0,0 +1,66 @@
# Quick Database Command Runner
# Uses settings from db-config.ps1
# Load configuration
. .\db-config.ps1
Write-Host "Quick Database Commands" -ForegroundColor Cyan
Write-Host "Using database: $DB_NAME" -ForegroundColor Yellow
Write-Host ""
# Show menu
Write-Host "Choose an option:" -ForegroundColor Green
Write-Host "1. Run diagnostics (sql\diagnostics.sql)"
Write-Host "2. Check index usage"
Write-Host "3. Check if performance indexes exist"
Write-Host "4. Run all performance indexes script"
Write-Host "5. Custom query"
Write-Host "6. Change database"
Write-Host "7. Exit"
Write-Host ""
$choice = Read-Host "Enter choice (1-7)"
switch ($choice) {
"1" {
Write-Host "Running diagnostics..." -ForegroundColor Yellow
Invoke-PSQL -File "sql\diagnostics.sql" -OutputFile "diagnostics_$(Get-Date -Format 'yyyy-MM-dd_HHmmss').txt"
Write-Host "✓ Results saved to diagnostics_*.txt" -ForegroundColor Green
}
"2" {
Write-Host "Checking index usage..." -ForegroundColor Yellow
$query = "SELECT indexname, idx_scan, idx_tup_read, idx_tup_fetch FROM pg_stat_user_indexes WHERE schemaname = 'library' AND indexname LIKE 'idx_%' ORDER BY idx_scan DESC;"
Invoke-PSQL -Query $query
}
"3" {
Write-Host "Checking performance indexes..." -ForegroundColor Yellow
$query = "SELECT COUNT(*) as perf_index_count FROM pg_indexes WHERE schemaname = 'library' AND indexname IN ('baseitems_communityrating_idx', 'baseitems_datecreated_idx', 'baseitems_datemodified_idx', 'baseitems_parentid_idx', 'baseitems_premieredate_idx', 'baseitems_productionyear_idx', 'baseitems_seriespresentationuniquekey_idx', 'baseitems_sortname_idx', 'baseitems_topparentid_idx');"
Invoke-PSQL -Query $query
}
"4" {
Write-Host "Running all performance indexes script..." -ForegroundColor Yellow
Invoke-PSQL -File "sql\all_performance_indexes.sql"
Write-Host "✓ Script complete" -ForegroundColor Green
}
"5" {
$query = Read-Host "Enter SQL query"
Invoke-PSQL -Query $query
}
"6" {
Write-Host ""
Write-Host "Current database: $DB_NAME" -ForegroundColor Yellow
Write-Host ""
Write-Host "To change database:" -ForegroundColor Cyan
Write-Host "1. Edit db-config.ps1" -ForegroundColor Gray
Write-Host "2. Change the line: `$DB_NAME = `"jellyfin_testsdata`"" -ForegroundColor Gray
Write-Host "3. Save and run: . .\db-config.ps1" -ForegroundColor Gray
Write-Host ""
}
"7" {
Write-Host "Goodbye!" -ForegroundColor Cyan
exit
}
default {
Write-Host "Invalid choice" -ForegroundColor Red
}
}
+397
View File
@@ -0,0 +1,397 @@
param(
[string]$SourcePath = (Join-Path $PSScriptRoot '..\sql_statements.txt')
)
$ErrorActionPreference = 'Stop'
$source = $SourcePath
$target = Join-Path $PSScriptRoot '..\sql\regression_from_sql_statements.sql'
$targetWithSkipped = Join-Path $PSScriptRoot '..\sql\regression_from_sql_statements_with_skipped.sql'
$skipReportCsv = Join-Path $PSScriptRoot '..\sql\regression_from_sql_statements_skip_report.csv'
function Convert-ParamLiteral {
param([string]$Value)
if ($null -eq $Value) {
return 'NULL'
}
$trimmed = $Value.Trim()
if ($trimmed -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') {
return "'$trimmed'::uuid"
}
if ($trimmed -match '^(?i:true|false)$') {
return $trimmed.ToLowerInvariant()
}
if ($trimmed -match '^\d+$') {
return $trimmed
}
$escaped = $trimmed.Replace("'", "''")
return "'$escaped'"
}
function Apply-ParamsToSql {
param(
[string]$Sql,
[string]$ParamsText
)
$result = $Sql
$normalizedParams = if ($null -eq $ParamsText) { '' } else { $ParamsText.Trim() }
# Newer EF logs wrap parameter payload in quotes inside [Parameters=[...]].
if ($normalizedParams.StartsWith('"') -and $normalizedParams.EndsWith('"')) {
$normalizedParams = $normalizedParams.Trim('"')
}
if ([string]::IsNullOrWhiteSpace($normalizedParams) -or $normalizedParams -eq '[]') {
return $result
}
# Truncated arrays in logs contain ellipsis; these are skipped later.
if ($normalizedParams -like '*...*') {
return $result
}
$arrayPattern = [regex]"@(?<name>[A-Za-z0-9_]+)=\{(?<vals>.*?)\}\s*\(DbType\s*=\s*Object\)"
foreach ($match in $arrayPattern.Matches($normalizedParams)) {
$name = $match.Groups['name'].Value
$valsRaw = $match.Groups['vals'].Value
$valueMatches = [regex]::Matches($valsRaw, "'([^']*)'")
$renderedVals = @()
foreach ($valueMatch in $valueMatches) {
$renderedVals += (Convert-ParamLiteral -Value $valueMatch.Groups[1].Value)
}
$arrayExpr = if ($renderedVals.Count -gt 0) {
'ARRAY[' + ($renderedVals -join ', ') + ']'
}
else {
'ARRAY[]::text[]'
}
$tokenPattern = '(?<![A-Za-z0-9_])@' + [regex]::Escape($name) + '(?![A-Za-z0-9_])'
$result = [regex]::Replace(
$result,
$tokenPattern,
[System.Text.RegularExpressions.MatchEvaluator]{ param($m) $arrayExpr }
)
}
$scalarPattern = [regex]"@(?<name>[A-Za-z0-9_]+)='(?<val>(?:''|[^'])*)'"
foreach ($match in $scalarPattern.Matches($normalizedParams)) {
$name = $match.Groups['name'].Value
$rawVal = $match.Groups['val'].Value -replace "''", "'"
$literal = Convert-ParamLiteral -Value $rawVal
$tokenPattern = '(?<![A-Za-z0-9_])@' + [regex]::Escape($name) + '(?![A-Za-z0-9_])'
$result = [regex]::Replace(
$result,
$tokenPattern,
[System.Text.RegularExpressions.MatchEvaluator]{ param($m) $literal }
)
}
return $result
}
function Test-SqlStatementCompleteness {
param([string]$Sql)
if ([string]::IsNullOrWhiteSpace($Sql)) {
return $false
}
$trimmed = $Sql.Trim()
$oneLine = ($trimmed -replace '\s+', ' ')
if ($oneLine -match '^(?i:SELECT\s+[^;]*\s+WHERE\s+)' -and $oneLine -notmatch '(?i:\sFROM\s)') {
return $false
}
if ($oneLine -match '^(?i:SELECT\s+)' -and
$oneLine -notmatch '(?i:\sFROM\s|\sEXISTS\s*\(|^SELECT\s+1\b)') {
return $false
}
$depth = 0
foreach ($ch in $trimmed.ToCharArray()) {
if ($ch -eq '(') { $depth++ }
elseif ($ch -eq ')') {
$depth--
if ($depth -lt 0) { return $false }
}
}
if ($depth -ne 0) {
return $false
}
if ($oneLine -match '(?i:(SELECT|FROM|WHERE|JOIN|INNER JOIN|LEFT JOIN|RIGHT JOIN|FULL JOIN|ON|ORDER BY|GROUP BY|LIMIT|OFFSET|UNION)\s*$)') {
return $false
}
if ($oneLine -match ',\s*$') {
return $false
}
return $true
}
function Split-SqlCandidates {
param([string]$Sql)
if ([string]::IsNullOrWhiteSpace($Sql)) {
return @()
}
$lines = $Sql -split "`n"
$startIndexes = New-Object System.Collections.Generic.List[int]
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match '^(SELECT|UPDATE|INSERT|DELETE|CREATE|ALTER|DROP|WITH)\b') {
$startIndexes.Add($i)
}
}
if ($startIndexes.Count -le 1) {
return @($Sql.Trim())
}
$chunks = New-Object System.Collections.Generic.List[string]
for ($s = 0; $s -lt $startIndexes.Count; $s++) {
$start = $startIndexes[$s]
$end = if ($s -lt $startIndexes.Count - 1) { $startIndexes[$s + 1] - 1 } else { $lines.Count - 1 }
if ($end -lt $start) {
continue
}
$segment = ($lines[$start..$end] -join "`n").Trim()
if (-not [string]::IsNullOrWhiteSpace($segment)) {
$chunks.Add($segment)
}
}
return $chunks
}
function Normalize-SqlLogLine {
param([string]$Line)
if ($null -eq $Line) {
return ''
}
$normalized = $Line.Trim()
# SQL text in this log file uses C-style escaped identifier quotes.
$normalized = $normalized -replace '\\"', '"'
# Strip only leading wrapper quotes from EF logs (keep trailing identifier quotes).
if ($normalized.StartsWith('""')) {
$normalized = $normalized.Substring(2)
}
elseif ($normalized.StartsWith('"')) {
$normalized = $normalized.Substring(1)
}
return $normalized
}
$entries = New-Object System.Collections.Generic.List[object]
$current = $null
$commandLinePattern = [regex]'^\[(?<ts>[^\]]+)\].*Executed DbCommand\s*\("?(?<ms>\d+)"?ms\)\s*\[Parameters=\[(?<params>.*?)\],\s*CommandType=''Text'',\s*CommandTimeout=''(?<timeout>\d+)''\](?<tail>.*)$'
$logLinePattern = [regex]'^\[(?:\d{2}:\d{2}:\d{2}\.\d{3}|\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}(?:\s+[^\]]+)?)\]'
Get-Content -Path $source | ForEach-Object {
$line = $_
$cmdMatch = $commandLinePattern.Match($line)
if ($cmdMatch.Success) {
if ($null -ne $current) {
$entries.Add([pscustomobject]@{
ms = $current.ms
timeout = $current.timeout
params = $current.params
sql = ($current.sqlLines -join "`n").Trim()
})
}
$current = [ordered]@{
ms = $cmdMatch.Groups['ms'].Value
timeout = $cmdMatch.Groups['timeout'].Value
params = $cmdMatch.Groups['params'].Value
sqlLines = New-Object System.Collections.Generic.List[string]
}
$tail = Normalize-SqlLogLine -Line $cmdMatch.Groups['tail'].Value
if (-not [string]::IsNullOrWhiteSpace($tail)) {
$current.sqlLines.Add($tail)
}
return
}
if ($null -eq $current) {
return
}
if ($logLinePattern.IsMatch($line)) {
$entries.Add([pscustomobject]@{
ms = $current.ms
timeout = $current.timeout
params = $current.params
sql = ($current.sqlLines -join "`n").Trim()
})
$current = $null
return
}
$normalizedLine = Normalize-SqlLogLine -Line $line
if (-not [string]::IsNullOrWhiteSpace($normalizedLine)) {
$current.sqlLines.Add($normalizedLine)
}
}
if ($null -ne $current) {
$entries.Add([pscustomobject]@{
ms = $current.ms
timeout = $current.timeout
params = $current.params
sql = ($current.sqlLines -join "`n").Trim()
})
}
$output = New-Object System.Collections.Generic.List[string]
$output.Add('-- Auto-generated from sql_statements.txt for regression testing')
$output.Add("-- Source: $source")
$output.Add("-- Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss K')")
$output.Add('-- Unresolved parameterized statements are skipped and counted in summary.')
$output.Add('\\timing on')
$output.Add("SET statement_timeout = '0';")
$output.Add('')
$outputWithSkipped = New-Object System.Collections.Generic.List[string]
$outputWithSkipped.Add('-- Auto-generated from sql_statements.txt for regression testing')
$outputWithSkipped.Add("-- Source: $source")
$outputWithSkipped.Add("-- Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss K')")
$outputWithSkipped.Add('-- Includes unresolved statements as commented blocks for manual editing.')
$outputWithSkipped.Add('\\timing on')
$outputWithSkipped.Add("SET statement_timeout = '0';")
$outputWithSkipped.Add('')
$totalExtracted = 0
$written = 0
$skipped = 0
$writtenWithSkipped = 0
$skipReasonCounts = @{}
function Add-SkipReason {
param([string]$Reason)
if (-not $skipReasonCounts.ContainsKey($Reason)) {
$skipReasonCounts[$Reason] = 0
}
$skipReasonCounts[$Reason]++
}
foreach ($entry in $entries) {
$totalExtracted++
$sqlText = $entry.sql
if ([string]::IsNullOrWhiteSpace($sqlText)) {
$skipped++
Add-SkipReason -Reason 'empty_sql_block'
continue
}
# Drop only whole-statement wrapper quotes emitted by EF logs.
$sqlText = $sqlText.Trim()
$sqlText = $sqlText -replace '^"+(?=(SELECT|UPDATE|INSERT|DELETE|CREATE|ALTER|DROP|WITH)\b)', ''
$sqlText = $sqlText -replace '"\s*$', ''
$sqlText = Apply-ParamsToSql -Sql $sqlText -ParamsText $entry.params
if ($sqlText -notmatch ';\s*$') {
$sqlText += ';'
}
$writtenWithSkipped++
$outputWithSkipped.Add("-- Query #$writtenWithSkipped | logged_duration_ms=$($entry.ms) | timeout_s=$($entry.timeout)")
$topLevelStarts = ([regex]::Matches($sqlText, '(?m)^(SELECT|UPDATE|INSERT|DELETE|CREATE|ALTER|DROP|WITH)\b')).Count
if ($topLevelStarts -gt 1) {
$skipped++
Add-SkipReason -Reason 'multiple_top_level_sql_starts'
$outputWithSkipped.Add('-- SKIPPED IN RUNNABLE FILE: block contains multiple top-level SQL starts (likely concatenated log fragments)')
foreach ($line in ($sqlText -split "`n")) {
$outputWithSkipped.Add('-- ' + $line)
}
$outputWithSkipped.Add('')
continue
}
if ($sqlText -match '@[A-Za-z_][A-Za-z0-9_]*') {
$skipped++
Add-SkipReason -Reason 'unresolved_parameter_placeholders'
$outputWithSkipped.Add('-- SKIPPED IN RUNNABLE FILE: unresolved placeholders remain')
if (-not [string]::IsNullOrWhiteSpace($entry.params)) {
$outputWithSkipped.Add("-- Logged parameters: [$($entry.params)]")
}
foreach ($line in ($sqlText -split "`n")) {
$outputWithSkipped.Add('-- ' + $line)
}
$outputWithSkipped.Add('')
continue
}
if (-not (Test-SqlStatementCompleteness -Sql $sqlText)) {
$skipped++
Add-SkipReason -Reason 'syntactically_incomplete_or_truncated'
$outputWithSkipped.Add('-- SKIPPED IN RUNNABLE FILE: statement appears truncated or syntactically incomplete')
foreach ($line in ($sqlText -split "`n")) {
$outputWithSkipped.Add('-- ' + $line)
}
$outputWithSkipped.Add('')
continue
}
$written++
$output.Add("-- Query #$written | logged_duration_ms=$($entry.ms) | timeout_s=$($entry.timeout)")
$output.Add($sqlText)
$output.Add('')
$outputWithSkipped.Add($sqlText)
$outputWithSkipped.Add('')
}
$output.Add("-- Summary: total_extracted=$totalExtracted, written=$written, skipped_unresolved=$skipped")
$outputWithSkipped.Add("-- Summary: total_extracted=$totalExtracted, written_total=$writtenWithSkipped, unresolved_commented=$skipped")
$skipReasonRows = @()
foreach ($key in ($skipReasonCounts.Keys | Sort-Object)) {
$count = [int]$skipReasonCounts[$key]
$pct = if ($skipped -gt 0) { [math]::Round(($count * 100.0) / $skipped, 2) } else { 0.0 }
$skipReasonRows += [pscustomobject]@{
reason = $key
count = $count
percent_of_skipped = $pct
}
}
$skipReasonRows | Export-Csv -Path $skipReportCsv -NoTypeInformation -Encoding UTF8
Set-Content -Path $target -Value $output -Encoding UTF8
Set-Content -Path $targetWithSkipped -Value $outputWithSkipped -Encoding UTF8
Write-Output "Generated: $target"
Write-Output "Generated: $targetWithSkipped"
Write-Output "Generated: $skipReportCsv"
Write-Output "Total extracted: $totalExtracted"
Write-Output "Written runnable: $written"
Write-Output "Skipped unresolved: $skipped"
+54
View File
@@ -0,0 +1,54 @@
# publish-with-sql.ps1
# Publishes Jellyfin and ensures SQL files are included
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Publishing Jellyfin with SQL files" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Step 1: Publishing Jellyfin..." -ForegroundColor Yellow
dotnet publish Jellyfin.Server\Jellyfin.Server.csproj -c Release -o Jellyfin.Server\bin\Release\net11.0\publish
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ Publish failed" -ForegroundColor Red
exit 1
}
Write-Host "✓ Publish successful" -ForegroundColor Green
Write-Host ""
Write-Host "Step 2: Copying SQL files..." -ForegroundColor Yellow
$publishDir = "Jellyfin.Server\bin\Release\net11.0\publish"
# Create sql directory structure
New-Item -ItemType Directory -Path "$publishDir\sql" -Force | Out-Null
New-Item -ItemType Directory -Path "$publishDir\sql\schema_init" -Force | Out-Null
# Copy SQL files
try {
Copy-Item "Jellyfin.Server\sql\*.sql" "$publishDir\sql\" -Force -ErrorAction Stop
$mainFileCount = (Get-ChildItem "$publishDir\sql\*.sql").Count
Write-Host " ✓ Copied $mainFileCount main SQL files" -ForegroundColor Green
Copy-Item "Jellyfin.Server\sql\schema_init\*.sql" "$publishDir\sql\schema_init\" -Force -ErrorAction SilentlyContinue
$initFileCount = (Get-ChildItem "$publishDir\sql\schema_init\*.sql" -ErrorAction SilentlyContinue).Count
Write-Host " ✓ Copied $initFileCount schema_init SQL files" -ForegroundColor Green
} catch {
Write-Host "❌ Failed to copy SQL files: $_" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "✓ Publish Complete!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Output directory: $publishDir" -ForegroundColor Yellow
Write-Host ""
Write-Host "SQL files:" -ForegroundColor Yellow
Get-ChildItem "$publishDir\sql" -Recurse -File | ForEach-Object {
Write-Host " - $($_.FullName.Replace($PWD.Path + '\', ''))" -ForegroundColor Gray
}
Write-Host ""
Write-Host "Next: Run jellyfin.exe from the publish directory" -ForegroundColor Cyan
+227
View File
@@ -0,0 +1,227 @@
# Remove SQLite from Deployment - Quick Implementation
# This script removes SQLite dependencies for PostgreSQL-only deployment
param(
[Parameter(Mandatory=$false)]
[switch]$DryRun,
[Parameter(Mandatory=$false)]
[switch]$RemoveProject
)
$ErrorActionPreference = "Stop"
$SolutionRoot = "E:\Projects\pgsql-jellyfin"
Write-Host ""
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host " Remove SQLite for PostgreSQL-Only Deployment" -ForegroundColor Cyan
Write-Host "================================================================" -ForegroundColor Cyan
Write-Host ""
if ($DryRun) {
Write-Host "DRY RUN MODE - No changes will be made" -ForegroundColor Yellow
Write-Host ""
}
# Step 1: Backup current state
Write-Host "Step 1: Creating backup..." -ForegroundColor Cyan
if (-not $DryRun) {
Push-Location $SolutionRoot
git stash push -m "Before SQLite removal"
Write-Host " Changes stashed" -ForegroundColor Green
} else {
Write-Host " Would stash current changes" -ForegroundColor Gray
}
# Step 2: Find SQLite references
Write-Host ""
Write-Host "Step 2: Finding SQLite references..." -ForegroundColor Cyan
$sqliteReferences = @()
# Check Jellyfin.Server.Implementations
$serverImpl = "$SolutionRoot\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj"
if (Test-Path $serverImpl) {
$content = Get-Content $serverImpl -Raw
if ($content -match 'Jellyfin\.Database\.Providers\.Sqlite') {
Write-Host " Found: SQLite reference in Jellyfin.Server.Implementations" -ForegroundColor Yellow
$sqliteReferences += @{
File = $serverImpl
Pattern = '<ProjectReference Include="[^"]*Jellyfin\.Database\.Providers\.Sqlite[^"]*" />'
}
}
}
# Check Emby.Server.Implementations
$embyImpl = "$SolutionRoot\Emby.Server.Implementations\Emby.Server.Implementations.csproj"
if (Test-Path $embyImpl) {
$content = Get-Content $embyImpl -Raw
if ($content -match 'Microsoft\.Data\.Sqlite') {
Write-Host " Found: SQLite package in Emby.Server.Implementations" -ForegroundColor Yellow
$sqliteReferences += @{
File = $embyImpl
Pattern = '<PackageReference Include="Microsoft\.Data\.Sqlite"[^/]*/>'
}
}
}
if ($sqliteReferences.Count -eq 0) {
Write-Host " No SQLite references found" -ForegroundColor Green
exit 0
}
# Step 3: Remove references
Write-Host ""
Write-Host "Step 3: Removing SQLite references..." -ForegroundColor Cyan
foreach ($ref in $sqliteReferences) {
$fileName = Split-Path $ref.File -Leaf
Write-Host " Processing: $fileName" -ForegroundColor Gray
if (-not $DryRun) {
$content = Get-Content $ref.File -Raw
$newContent = $content -replace $ref.Pattern, ''
$newContent = $newContent -replace '(\r?\n){3,}', "`r`n`r`n"
Set-Content -Path $ref.File -Value $newContent -NoNewline
Write-Host " Removed reference" -ForegroundColor Green
} else {
Write-Host " Would remove SQLite reference" -ForegroundColor Gray
}
}
# Step 4: Exclude SQLite project from solution (optional)
if ($RemoveProject) {
Write-Host ""
Write-Host "Step 4: Excluding SQLite project from solution..." -ForegroundColor Cyan
$solutionFile = "$SolutionRoot\Jellyfin.sln"
if (Test-Path $solutionFile) {
if (-not $DryRun) {
$slnContent = Get-Content $solutionFile -Raw
$pattern = '(Project\("[^"]*"\) = "Jellyfin\.Database\.Providers\.Sqlite"[^E]*EndProject)'
$replacement = "# REMOVED: SQLite Provider`r`n# " + '$1'
$newSlnContent = $slnContent -replace $pattern, $replacement
Set-Content -Path $solutionFile -Value $newSlnContent -NoNewline
Write-Host " SQLite project commented out in solution" -ForegroundColor Green
} else {
Write-Host " Would comment out SQLite project" -ForegroundColor Gray
}
}
}
# Step 5: Clean build directories
Write-Host ""
Write-Host "Step 5: Cleaning build directories..." -ForegroundColor Cyan
$buildDirs = @(
"$SolutionRoot\lib",
"$SolutionRoot\Jellyfin.Server.Implementations\bin",
"$SolutionRoot\Jellyfin.Server.Implementations\obj",
"$SolutionRoot\Emby.Server.Implementations\bin",
"$SolutionRoot\Emby.Server.Implementations\obj"
)
foreach ($dir in $buildDirs) {
if (Test-Path $dir) {
if (-not $DryRun) {
Remove-Item $dir -Recurse -Force
$dirName = Split-Path $dir -Leaf
Write-Host " Cleaned: $dirName" -ForegroundColor Green
} else {
$dirName = Split-Path $dir -Leaf
Write-Host " Would clean: $dirName" -ForegroundColor Gray
}
}
}
# Step 6: Test build
Write-Host ""
Write-Host "Step 6: Testing build..." -ForegroundColor Cyan
if (-not $DryRun) {
Push-Location $SolutionRoot
Write-Host " Building solution..." -ForegroundColor Gray
$buildOutput = dotnet build --configuration Release 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " Build successful" -ForegroundColor Green
} else {
Write-Host " Build failed" -ForegroundColor Red
Write-Host $buildOutput
Write-Host ""
Write-Host "Rolling back changes..." -ForegroundColor Yellow
git stash pop
exit 1
}
Pop-Location
} else {
Write-Host " Would run: dotnet build --configuration Release" -ForegroundColor Gray
}
# Step 7: Verify no SQLite DLLs
Write-Host ""
Write-Host "Step 7: Verifying no SQLite DLLs..." -ForegroundColor Cyan
$libFolder = "$SolutionRoot\lib\Release\net11.0"
if (Test-Path $libFolder) {
$sqliteDlls = Get-ChildItem $libFolder -Filter "*Sqlite*" -Recurse
if ($sqliteDlls.Count -eq 0) {
Write-Host " No SQLite DLLs found in output" -ForegroundColor Green
} else {
Write-Host " Found SQLite DLLs:" -ForegroundColor Yellow
$sqliteDlls | ForEach-Object {
Write-Host " - $($_.Name)" -ForegroundColor Gray
}
}
}
# Step 8: Calculate size reduction
Write-Host ""
Write-Host "Step 8: Calculating deployment size..." -ForegroundColor Cyan
if (Test-Path $libFolder) {
$totalSize = (Get-ChildItem $libFolder -Recurse -File | Measure-Object -Property Length -Sum).Sum
$sizeMB = [math]::Round($totalSize / 1MB, 2)
Write-Host " Deployment size: $sizeMB MB" -ForegroundColor White
}
# Summary
Write-Host ""
Write-Host "================================================================" -ForegroundColor Green
Write-Host " SQLite Removal Complete" -ForegroundColor Green
Write-Host "================================================================" -ForegroundColor Green
Write-Host ""
if (-not $DryRun) {
Write-Host "Changes made:" -ForegroundColor Cyan
Write-Host " - Removed SQLite project references" -ForegroundColor Green
Write-Host " - Removed SQLite package references" -ForegroundColor Green
if ($RemoveProject) {
Write-Host " - Excluded SQLite project from solution" -ForegroundColor Green
}
Write-Host " - Cleaned build directories" -ForegroundColor Green
Write-Host " - Verified build" -ForegroundColor Green
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Cyan
Write-Host " 1. Review changes: git diff" -ForegroundColor Gray
Write-Host " 2. Test application: dotnet run --project Jellyfin.Server" -ForegroundColor Gray
Write-Host " 3. Commit changes: git commit -am 'Remove SQLite support'" -ForegroundColor Gray
Write-Host ""
Write-Host "Rollback if needed:" -ForegroundColor Yellow
Write-Host " git stash pop" -ForegroundColor Gray
} else {
Write-Host "Dry run complete - no changes made" -ForegroundColor Yellow
Write-Host "Run without -DryRun to apply changes" -ForegroundColor Gray
}
if (-not $DryRun) {
Pop-Location
}
Write-Host ""
+64
View File
@@ -0,0 +1,64 @@
# Rollback to .NET 10 Script
# This script reverts all .NET 11 changes back to .NET 10
Write-Host "Rolling back to .NET 10..." -ForegroundColor Yellow
# Revert all .csproj files from net11.0 to net10.0
$files = Get-ChildItem -Path . -Filter "*.csproj" -Recurse | Where-Object {
$_.FullName -notlike "*\obj\*" -and $_.FullName -notlike "*\bin\*"
}
foreach ($file in $files) {
$content = Get-Content $file.FullName -Raw
if ($content -match '<TargetFramework>net11\.0</TargetFramework>') {
$content = $content -replace '<TargetFramework>net11\.0</TargetFramework>', '<TargetFramework>net10.0</TargetFramework>'
Set-Content -Path $file.FullName -Value $content -NoNewline
Write-Host "Reverted: $($file.Name)" -ForegroundColor Green
}
}
Write-Host "`nUpdating Directory.Packages.props..." -ForegroundColor Yellow
# Read Directory.Packages.props
$packagesFile = "Directory.Packages.props"
$content = Get-Content $packagesFile -Raw
# Revert Microsoft packages
$content = $content -replace 'Microsoft\.AspNetCore\.Authorization" Version="11\.0\.1"', 'Microsoft.AspNetCore.Authorization" Version="10.0.3"'
$content = $content -replace 'Microsoft\.AspNetCore\.Mvc\.Testing" Version="11\.0\.1"', 'Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3"'
$content = $content -replace 'Microsoft\.Data\.Sqlite" Version="11\.0\.1"', 'Microsoft.Data.Sqlite" Version="10.0.3"'
$content = $content -replace 'Microsoft\.EntityFrameworkCore" Version="11\.0\.1"', 'Microsoft.EntityFrameworkCore" Version="10.0.3"'
$content = $content -replace 'Microsoft\.EntityFrameworkCore\.Design" Version="11\.0\.1"', 'Microsoft.EntityFrameworkCore.Design" Version="10.0.3"'
$content = $content -replace 'Microsoft\.EntityFrameworkCore\.Relational" Version="11\.0\.1"', 'Microsoft.EntityFrameworkCore.Relational" Version="10.0.3"'
$content = $content -replace 'Microsoft\.EntityFrameworkCore\.Sqlite" Version="11\.0\.1"', 'Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3"'
$content = $content -replace 'Microsoft\.EntityFrameworkCore\.Tools" Version="11\.0\.1"', 'Microsoft.EntityFrameworkCore.Tools" Version="10.0.3"'
$content = $content -replace 'Microsoft\.Extensions\.Caching\.Abstractions" Version="11\.0\.1"', 'Microsoft.Extensions.Caching.Abstractions" Version="10.0.3"'
$content = $content -replace 'Microsoft\.Extensions\.Caching\.Memory" Version="11\.0\.1"', 'Microsoft.Extensions.Caching.Memory" Version="10.0.3"'
$content = $content -replace 'Microsoft\.Extensions\.Configuration\.Abstractions" Version="11\.0\.1"', 'Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3"'
$content = $content -replace 'Microsoft\.Extensions\.Configuration\.Binder" Version="11\.0\.1"', 'Microsoft.Extensions.Configuration.Binder" Version="10.0.3"'
$content = $content -replace 'Microsoft\.Extensions\.DependencyInjection" Version="11\.0\.1"', 'Microsoft.Extensions.DependencyInjection" Version="10.0.3"'
$content = $content -replace 'Microsoft\.Extensions\.Diagnostics\.HealthChecks\.EntityFrameworkCore" Version="11\.0\.1"', 'Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3"'
$content = $content -replace 'Microsoft\.Extensions\.Hosting\.Abstractions" Version="11\.0\.1"', 'Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3"'
$content = $content -replace 'Microsoft\.Extensions\.Http" Version="11\.0\.1"', 'Microsoft.Extensions.Http" Version="10.0.3"'
$content = $content -replace 'Microsoft\.Extensions\.Logging" Version="11\.0\.1"', 'Microsoft.Extensions.Logging" Version="10.0.3"'
$content = $content -replace 'Microsoft\.Extensions\.Options" Version="11\.0\.1"', 'Microsoft.Extensions.Options" Version="10.0.3"'
# Revert Serilog packages
$content = $content -replace 'Serilog\.AspNetCore" Version="11\.0\.1"', 'Serilog.AspNetCore" Version="10.0.0"'
$content = $content -replace 'Serilog\.Settings\.Configuration" Version="11\.0\.1"', 'Serilog.Settings.Configuration" Version="10.0.0"'
# Revert System packages
$content = $content -replace 'System\.Text\.Json" Version="11\.0\.1"', 'System.Text.Json" Version="10.0.3"'
# Revert Npgsql with comment
$content = $content -replace 'Npgsql\.EntityFrameworkCore\.PostgreSQL" Version="11\.0\.0-preview\.1"',
'Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />' + "`n <!-- Note: Using 9.0.2 with EF Core 10 - version constraint warnings expected"
Set-Content -Path $packagesFile -Value $content -NoNewline
Write-Host "Updated Directory.Packages.props" -ForegroundColor Green
Write-Host "`nRollback complete!" -ForegroundColor Cyan
Write-Host "Run these commands to restore and build:" -ForegroundColor Cyan
Write-Host " dotnet restore Jellyfin.sln" -ForegroundColor White
Write-Host " dotnet build Jellyfin.sln /p:TreatWarningsAsErrors=false" -ForegroundColor White
+53
View File
@@ -0,0 +1,53 @@
import requests
# Define connection details
server_url = 'http://localhost:8096'
username = 'admin'
password = 'Optimus0329'
backup_options = {
"Database": True, # Include database
"Config": True, # Include config files
"Data": True, # Include data files
"Root": True, # Include root folder
"MediaFiles": False # Exclude media files
}
# Build json payload with auth data
auth_data = {
'username': username,
'Pw': password
}
headers = {}
# Build required connection headers
authorization = 'MediaBrowser Client="other", Device="my-script", DeviceId="some-unique-id", Version="0.0.0"'
headers['Authorization'] = authorization
# Authenticate to server
r = requests.post(server_url + '/Users/AuthenticateByName', headers=headers, json=auth_data)
# Retrieve auth token and user id from returned data
token = r.json().get('AccessToken')
user_id = r.json().get('User').get('Id')
# Update the headers to include the auth token
headers['Authorization'] = f'{authorization}, Token="{token}"'
headers['Content-Type'] = 'application/json'
# Requests can be made with
#requests.get(f'{server_url}/api/endpoint', headers=headers)
# Create a backup (empty body is OK, will use default options)
print("Creating backup...")
response = requests.post(f'{server_url}/Backup/Create', headers=headers, json={})
print(f"Status: {response.status_code}")
print(response.text)
# List all backups
print("\nListing backups...")
response = requests.get(f'{server_url}/Backup', headers=headers)
print(f"Status: {response.status_code}")
print(response.text)
+82
View File
@@ -0,0 +1,82 @@
# PostgreSQL Migration Verification Script
Write-Host "=== Jellyfin PostgreSQL Migration Verification ===" -ForegroundColor Cyan
Write-Host ""
# Check if Docker is available
$dockerAvailable = $false
try {
$null = docker --version 2>&1
$dockerAvailable = $true
Write-Host "Docker is available" -ForegroundColor Green
} catch {
Write-Host "Docker is not available" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "--- Step 1: Check Migration Files ---" -ForegroundColor Cyan
$migrationPath = "src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres\Migrations"
if (Test-Path $migrationPath) {
$migrations = Get-ChildItem -Path $migrationPath -Filter "*.cs" -Exclude "*Designer.cs", "*Snapshot.cs", "*Factory.cs"
Write-Host "Found $($migrations.Count) migration(s):" -ForegroundColor Green
foreach ($migration in $migrations) {
Write-Host " - $($migration.Name)" -ForegroundColor Gray
}
} else {
Write-Host "Migration path not found" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "--- Step 2: Build PostgreSQL Database Provider ---" -ForegroundColor Cyan
Push-Location "src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres"
try {
dotnet build --configuration Release
if ($LASTEXITCODE -eq 0) {
Write-Host "PostgreSQL provider built successfully" -ForegroundColor Green
} else {
Write-Host "Build failed" -ForegroundColor Red
Pop-Location
exit 1
}
} finally {
Pop-Location
}
Write-Host ""
Write-Host "--- Step 3: List Migrations ---" -ForegroundColor Cyan
Push-Location "src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres"
try {
Write-Host "Running: dotnet ef migrations list" -ForegroundColor Gray
dotnet ef migrations list
} finally {
Pop-Location
}
Write-Host ""
Write-Host "--- Step 4: Generate SQL Script ---" -ForegroundColor Cyan
Push-Location "src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres"
try {
$sqlOutputPath = "..\..\..\migration-script.sql"
Write-Host "Generating SQL script..." -ForegroundColor Gray
dotnet ef migrations script --output $sqlOutputPath --idempotent
if (Test-Path $sqlOutputPath) {
$sqlContent = Get-Content $sqlOutputPath -Raw
$lineCount = ($sqlContent -split "`n").Count
Write-Host "SQL script generated successfully ($lineCount lines)" -ForegroundColor Green
Write-Host "Location: $((Resolve-Path $sqlOutputPath).Path)" -ForegroundColor Gray
} else {
Write-Host "Failed to generate SQL script" -ForegroundColor Red
}
} catch {
Write-Host "Error generating SQL script: $_" -ForegroundColor Red
} finally {
Pop-Location
}
Write-Host ""
Write-Host "=== Verification Complete ===" -ForegroundColor Cyan