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
+12 -1
View File
@@ -72,6 +72,7 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" />
<PackageReference Include="Microsoft.Kiota.Abstractions" />
<PackageReference Include="Morestachio" />
<PackageReference Include="prometheus-net" />
<PackageReference Include="prometheus-net.AspNetCore" />
@@ -121,16 +122,26 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>false</Pack>
</None>
<!-- Utility scripts -->
<None Include="scripts\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>false</Pack>
</None>
</ItemGroup>
<!-- Post-build event to ensure SQL files are copied -->
<!-- Post-build event to ensure SQL and scripts files are copied -->
<Target Name="CopySQLFiles" AfterTargets="AfterBuild">
<ItemGroup>
<SQLFiles Include="$(MSBuildProjectDirectory)\sql\**\*.sql" />
<ScriptFiles Include="$(MSBuildProjectDirectory)\scripts\**\*" Exclude="$(MSBuildProjectDirectory)\scripts\**\*.git*" />
</ItemGroup>
<Message Importance="high" Text="Copying SQL files to $(OutDir)sql\" />
<Copy SourceFiles="@(SQLFiles)" DestinationFiles="@(SQLFiles->'$(OutDir)sql\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
<Message Importance="high" Text="✅ SQL files copied to output directory: $(OutDir)sql\" />
<Message Importance="high" Text="Copying script files to $(OutDir)scripts\" />
<Copy SourceFiles="@(ScriptFiles)" DestinationFiles="@(ScriptFiles->'$(OutDir)scripts\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
<Message Importance="high" Text="✅ Script files copied to output directory: $(OutDir)scripts\" />
</Target>
</Project>
@@ -0,0 +1,393 @@
#!/bin/bash
#
# 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 cron
#
# USAGE
# ./Weekly-Performance-Monitor.sh [OPTIONS]
#
# OPTIONS
# -o, --output-directory DIR Directory to save reports (default: reports/weekly)
# -e, --email-report If specified, emails the report (requires email configuration)
# -c, --compare-with-previous Compare current stats with previous week
# -a, --auto-vacuum-analyze Enable auto vacuum analyze
# --dead-tuple-threshold NUM Minimum dead tuples to trigger vacuum (default: 100)
# --dead-tuple-percent-threshold NUM Minimum dead tuple percentage to trigger vacuum (default: 0.02)
#
# EXAMPLE
# ./Weekly-Performance-Monitor.sh
# ./Weekly-Performance-Monitor.sh -o "/reports/jellyfin" -c
# ./Weekly-Performance-Monitor.sh -a --dead-tuple-threshold 10000 --dead-tuple-percent-threshold 20
#
# CRON JOB EXAMPLE
# # Run every Monday at 6am
# 0 6 * * 1 /path/to/Weekly-Performance-Monitor.sh -a
#
# ============================================================================
# Default Configuration
# ============================================================================
OUTPUT_DIRECTORY="reports/weekly"
EMAIL_REPORT=false
COMPARE_WITH_PREVIOUS=false
AUTO_VACUUM_ANALYZE=false
DEAD_TUPLE_THRESHOLD=100
DEAD_TUPLE_PERCENT_THRESHOLD=0.02
# ============================================================================
# Helper Functions
# ============================================================================
# Color definitions
CYAN='\033[0;36m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
GRAY='\033[0;90m'
NC='\033[0m' # No Color
write_color_output() {
local message="$1"
local color="$2"
echo -e "${color}${message}${NC}"
}
get_report_timestamp() {
date +"%Y-%m-%d_%H%M%S"
}
get_week_number() {
date +%V
}
# ============================================================================
# Script Argument Parsing
# ============================================================================
while [[ "$#" -gt 0 ]]; do
case $1 in
-o|--output-directory) OUTPUT_DIRECTORY="$2"; shift ;;
-e|--email-report) EMAIL_REPORT=true ;;
-c|--compare-with-previous) COMPARE_WITH_PREVIOUS=true ;;
-a|--auto-vacuum-analyze) AUTO_VACUUM_ANALYZE=true ;;
--dead-tuple-threshold) DEAD_TUPLE_THRESHOLD="$2"; shift ;;
--dead-tuple-percent-threshold) DEAD_TUPLE_PERCENT_THRESHOLD="$2"; shift ;;
*) write_color_output "Unknown parameter passed: $1" "$RED"; exit 1 ;;
esac
shift
done
# ============================================================================
# Configuration
# ============================================================================
set -e # Exit on error
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
# Load database configuration
source "./db-config.sh"
# ============================================================================
# Functions
# ============================================================================
test_database_connection() {
write_color_output "\nTesting database connection..." "$CYAN"
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "SELECT version();" > /dev/null 2>&1; then
write_color_output "[OK] Database connection successful" "$GREEN"
return 0
else
write_color_output "[ERROR] Database connection failed" "$RED"
return 1
fi
}
invoke_diagnostic_report() {
local output_file="$1"
write_color_output "\nRunning comprehensive diagnostics..." "$CYAN"
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$PROJECT_ROOT/sql/diagnostics.sql" > "$output_file" 2>&1; then
write_color_output "[OK] Diagnostics completed: $output_file" "$GREEN"
else
write_color_output "[WARN] Diagnostics completed with warnings" "$YELLOW"
fi
}
invoke_query_analysis() {
local output_file="$1"
write_color_output "\nAnalyzing slow queries..." "$CYAN"
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$PROJECT_ROOT/sql/query-analysis.sql" > "$output_file" 2>&1; then
write_color_output "[OK] Query analysis completed: $output_file" "$GREEN"
else
write_color_output "[WARN] Query analysis completed with warnings" "$YELLOW"
fi
}
get_index_usage_stats() {
local output_file="$1"
write_color_output "\nGathering index usage statistics..." "$CYAN"
local 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;
"
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "$query" > "$output_file" 2>&1; then
write_color_output "[OK] Index stats gathered: $output_file" "$GREEN"
else
write_color_output "[WARN] Index stats completed with warnings" "$YELLOW"
fi
}
get_table_size_stats() {
local output_file="$1"
write_color_output "\nGathering table size statistics..." "$CYAN"
local 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;
"
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "$query" > "$output_file" 2>&1; then
write_color_output "[OK] Table size stats gathered: $output_file" "$GREEN"
else
write_color_output "[WARN] Table size stats completed with warnings" "$YELLOW"
fi
}
invoke_conditional_vacuum_analyze() {
local min_dead_tuples="$1"
local min_dead_percent="$2"
write_color_output "\nChecking for tables that need VACUUM ANALYZE..." "$CYAN"
write_color_output "Thresholds: dead tuples >= $min_dead_tuples and dead percent >= $min_dead_percent" "$GRAY"
local table_query="
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 >= $min_dead_tuples
AND (100.0 * n_dead_tup / NULLIF(n_live_tup, 0)) >= $min_dead_percent
ORDER BY n_dead_tup DESC;
"
tables=$(PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -t -A -F "|" -c "$table_query")
if [ -z "$tables" ]; then
write_color_output "[OK] No tables exceeded dead tuple thresholds" "$GREEN"
return
fi
write_color_output "Found $(echo "$tables" | wc -l) table(s) requiring VACUUM ANALYZE" "$YELLOW"
while IFS= read -r line; do
qualified_table=$(echo "$line" | cut -d'|' -f1)
dead_tuples=$(echo "$line" | cut -d'|' -f2)
dead_percent=$(echo "$line" | cut -d'|' -f3)
write_color_output "Running VACUUM ANALYZE on $qualified_table (dead tuples: $dead_tuples, dead percent: $dead_percent)" "$CYAN"
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "VACUUM (ANALYZE, VERBOSE) $qualified_table;"; then
write_color_output "[OK] VACUUM ANALYZE completed for $qualified_table" "$GREEN"
else
write_color_output "[ERROR] VACUUM ANALYZE failed for $qualified_table" "$RED"
fi
done <<< "$tables"
}
compare_with_previous() {
local current_report="$1"
local output_dir="$2"
write_color_output "\nComparing with previous week's report..." "$CYAN"
previous_report=$(find "$output_dir" -name "diagnostics_*.txt" ! -name "$(basename "$current_report")" -printf '%T@ %p\n' | sort -n | tail -1 | cut -d' ' -f2-)
if [ -z "$previous_report" ]; then
write_color_output "[WARN] No previous reports found for comparison" "$YELLOW"
return
fi
write_color_output "Comparing with: $(basename "$previous_report")" "$CYAN"
# TODO: Add detailed comparison logic
write_color_output "[OK] Comparison data available" "$GREEN"
write_color_output " Current: $current_report" "$GRAY"
write_color_output " Previous: $previous_report" "$GRAY"
}
new_summary_report() {
local output_dir="$1"
local timestamp="$2"
write_color_output "\nGenerating summary report..." "$CYAN"
local summary_file="$output_dir/summary_$timestamp.txt"
cat > "$summary_file" <<EOF
================================================================================
JELLYFIN DATABASE WEEKLY PERFORMANCE SUMMARY
================================================================================
Report Date: $(date +"%Y-%m-%d %H:%M:%S")
Database: $DB_NAME at $DB_HOST
Week Number: $(get_week_number)
================================================================================
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, add the following to your crontab:
0 6 * * 1 /path/to/$(basename "$0")
================================================================================
NEXT REPORT: $(date -d "+7 days" +"%Y-%m-%d")
================================================================================
EOF
write_color_output "[OK] Summary generated: $summary_file" "$GREEN"
}
# ============================================================================
# Main Execution
# ============================================================================
write_color_output "\n${CYAN}========================================${NC}"
write_color_output "Jellyfin Database Weekly Monitor" "$CYAN"
write_color_output "========================================${NC}"
write_color_output "Database: $DB_NAME" "$YELLOW"
write_color_output "Host: $DB_HOST" "$YELLOW"
write_color_output "Week: $(get_week_number)" "$YELLOW"
write_color_output "Date: $(date +'%Y-%m-%d %H:%M:%S')" "$YELLOW"
write_color_output "========================================\n${NC}"
# Create output directory
report_dir="./$OUTPUT_DIRECTORY"
mkdir -p "$report_dir"
write_color_output "Created report directory: $report_dir" "$GREEN"
# Generate timestamp for this run
timestamp=$(get_report_timestamp)
# Test connection
if ! test_database_connection; then
write_color_output "\n[ERROR] Cannot connect to database. Exiting." "$RED"
exit 1
fi
# Run diagnostics
diagnostics_file="$report_dir/diagnostics_$timestamp.txt"
invoke_diagnostic_report "$diagnostics_file"
# Run query analysis
query_analysis_file="$report_dir/query_analysis_$timestamp.txt"
invoke_query_analysis "$query_analysis_file"
# Gather index usage stats
index_usage_file="$report_dir/index_usage_$timestamp.txt"
get_index_usage_stats "$index_usage_file"
# Gather table size stats
table_sizes_file="$report_dir/table_sizes_$timestamp.txt"
get_table_size_stats "$table_sizes_file"
# Optionally vacuum/analyze tables
if [ "$AUTO_VACUUM_ANALYZE" = true ]; then
invoke_conditional_vacuum_analyze "$DEAD_TUPLE_THRESHOLD" "$DEAD_TUPLE_PERCENT_THRESHOLD"
fi
# Compare with previous week
if [ "$COMPARE_WITH_PREVIOUS" = true ]; then
compare_with_previous "$diagnostics_file" "$report_dir"
fi
# Generate summary
new_summary_report "$report_dir" "$timestamp"
# Cleanup old reports (keep last 12 weeks)
write_color_output "\nCleaning up old reports (keeping last 12 weeks)..." "$CYAN"
find "$report_dir" -type f -mtime +84 -delete
write_color_output "[OK] Cleanup complete" "$GREEN"
write_color_output "\n${GREEN}========================================${NC}"
write_color_output "[OK] WEEKLY MONITORING COMPLETE" "$GREEN"
write_color_output "========================================${NC}"
write_color_output "Reports saved to: $report_dir" "$YELLOW"
write_color_output "Summary: summary_$timestamp.txt" "$YELLOW"
write_color_output "\nNext steps:" "$CYAN"
write_color_output "1. Review summary_$timestamp.txt" "$NC"
write_color_output "2. Check for critical issues in diagnostics report" "$NC"
write_color_output "3. Analyze slow queries in query_analysis report" "$NC"
write_color_output "========================================\n${NC}"
# Email report if requested
if [ "$EMAIL_REPORT" = true ]; then
write_color_output "[WARN] Email functionality not yet implemented" "$YELLOW"
write_color_output " Configure a mail client like 'mail' or 'sendmail'" "$GRAY"
fi
exit 0
@@ -0,0 +1,78 @@
#!/bin/bash
# Database Configuration
# Edit this file to change which database commands run against
# Database connection settings
PSQL_PATH="/usr/bin/psql"
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
export PSQL_PATH
export DB_USER
export DB_NAME
export DB_HOST
export DB_PORT
# Helper function to run psql commands
invoke_psql() {
local query=""
local file=""
local output_file=""
while [[ $# -gt 0 ]]; do
case "$1" in
-q|--query)
query="$2"
shift 2
;;
-f|--file)
file="$2"
shift 2
;;
-o|--output-file)
output_file="$2"
shift 2
;;
*)
echo "Unknown option: $1"
return 1
;;
esac
done
local base_cmd="$PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME"
local cmd=""
if [ -n "$file" ]; then
cmd="$base_cmd -f \"$file\""
elif [ -n "$query" ]; then
cmd="$base_cmd -c \"$query\""
else
echo "Must provide either --query or --file parameter"
return 1
fi
if [ -n "$output_file" ]; then
cmd+=" > \"$output_file\""
fi
echo -e "\e[36mConnecting to: $DB_NAME@$DB_HOST as $DB_USER\e[0m"
eval "$cmd"
}
# Display current configuration
echo -e "\e[36m========================================\e[0m"
echo -e "\e[36mDatabase Configuration Loaded\e[0m"
echo -e "\e[36m========================================\e[0m"
echo -e "\e[33mDatabase: $DB_NAME\e[0m"
echo -e "\e[33mUser: $DB_USER\e[0m"
echo -e "\e[33mHost: $DB_HOST\e[0m"
echo -e "\e[33mPort: $DB_PORT\e[0m"
echo -e "\e[36m========================================\e[0m"
echo ""
echo -e "\e[90mTo change database, edit: db-config.sh\e[0m"
echo -e "\e[90mThen run: source db-config.sh\e[0m"
echo ""
@@ -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
@@ -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 ""