Postgres perf, path migration, and shutdown race fixes
- Major query performance boost: replaced correlated subqueries with DistinctBy() in BaseItemRepository, added episode deduplication index, and increased default command timeout to 120s. - Fixed LibraryMonitor shutdown race: added disposal checks and exception handling in FileRefresher to prevent ObjectDisposedException. - Added PowerShell and SQL utilities for fixing Linux paths in config files and database; new scripts for WebDir and path migration. - Auto-fix for WebDir in startup.json on load; new helper scripts and docs. - Added automated weekly DB performance monitoring scripts and reporting. - Expanded documentation and README for all fixes, scripts, and migration steps. - Updated SQL scripts for index creation and diagnostics; improved output handling. - Updated db-config defaults and added new diagnostic/report files.
This commit is contained in:
@@ -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 ""
|
||||
@@ -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 ""
|
||||
@@ -0,0 +1,203 @@
|
||||
#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
|
||||
|
||||
.PARAMETER Uninstall
|
||||
Remove the scheduled task
|
||||
|
||||
.PARAMETER TaskTime
|
||||
Time to run the task (default: 6am)
|
||||
|
||||
.PARAMETER DayOfWeek
|
||||
Day of week to run (default: Monday)
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-Weekly-Monitor.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-Weekly-Monitor.ps1 -TaskTime "3am" -DayOfWeek Sunday
|
||||
|
||||
.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'
|
||||
)
|
||||
|
||||
$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
|
||||
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
|
||||
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute "PowerShell.exe" `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$MonitorScript`""
|
||||
|
||||
# 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,396 @@
|
||||
#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. 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
|
||||
# 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
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# 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,
|
||||
tablename,
|
||||
indexname,
|
||||
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, tablename, 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,
|
||||
tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size,
|
||||
pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) AS table_size,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename) - pg_relation_size(schemaname||'.'||tablename)) 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||'.'||tablename) 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 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
|
||||
|
||||
# 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
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
# Database connection settings
|
||||
$PSQL_PATH = "C:\Program Files\PostgreSQL\18\bin\psql.exe"
|
||||
$DB_USER = "postgres"
|
||||
$DB_NAME = "jellyfin_testdata" # ← Change this to switch databases
|
||||
$DB_HOST = "192.168.129.248"
|
||||
$DB_USER = "jellyfin"
|
||||
$DB_NAME = "jellyfin" # ← Change this to switch databases
|
||||
$DB_HOST = "192.168.129.163"
|
||||
$DB_PORT = "5432"
|
||||
|
||||
# Export for use in other scripts
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user