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:
@@ -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
|
||||
Reference in New Issue
Block a user