diff --git a/Directory.Packages.props b/Directory.Packages.props index fe4b3e30..8d35062d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -50,6 +50,7 @@ + @@ -91,4 +92,4 @@ - + \ No newline at end of file diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 0f30217e..0370de72 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -32,6 +32,10 @@ + + + + diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index a56ee97b..c31c466f 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -16,6 +16,7 @@ + diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 99cc2fe9..aeb02501 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -29,6 +29,7 @@ + diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index afd29d52..8d993678 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -16,6 +16,7 @@ + diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 034a295f..99558c05 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -30,6 +30,7 @@ + diff --git a/Jellyfin.Server.Implementations/ScheduledTasks/PostgresPeriodicTuner.cs b/Jellyfin.Server.Implementations/ScheduledTasks/PostgresPeriodicTuner.cs index f48ac429..da3da9a1 100644 --- a/Jellyfin.Server.Implementations/ScheduledTasks/PostgresPeriodicTuner.cs +++ b/Jellyfin.Server.Implementations/ScheduledTasks/PostgresPeriodicTuner.cs @@ -3,12 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Queries; -using Jellyfin.Extensions.Dapper; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Tasks; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.ScheduledTasks @@ -19,17 +19,17 @@ namespace Jellyfin.Server.Implementations.ScheduledTasks public class PostgresPeriodicTuner : IScheduledTask, IConfigurableScheduledTask { private readonly ILogger _logger; - private readonly IDbContext _dbContext; + private readonly IDbContextFactory _dbContextFactory; /// /// Initializes a new instance of the class. /// /// The logger. - /// The database context. - public PostgresPeriodicTuner(ILogger logger, IDbContext dbContext) + /// The database context factory. + public PostgresPeriodicTuner(ILogger logger, IDbContextFactory dbContextFactory) { _logger = logger; - _dbContext = dbContext; + _dbContextFactory = dbContextFactory; } /// @@ -56,15 +56,17 @@ namespace Jellyfin.Server.Implementations.ScheduledTasks /// /// Executes the task. /// - /// The cancellation token. /// The progress. + /// The cancellation token. /// A representing the asynchronous operation. - public async Task ExecuteAsync(CancellationToken cancellationToken, IProgress progress) + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { _logger.LogInformation("PostgreSQL Periodic Tuner task started."); try { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + const string DeadTupleQuery = @" SELECT relname AS TableName, @@ -76,7 +78,7 @@ namespace Jellyfin.Server.Implementations.ScheduledTasks ORDER BY n_dead_tup DESC;"; - var tablesToVacuum = (await _dbContext.QueryAsync<(string TableName, long DeadTuples)>(DeadTupleQuery)).ToList(); + var tablesToVacuum = (await dbContext.Database.SqlQueryRaw<(string TableName, long DeadTuples)>(DeadTupleQuery).ToListAsync(cancellationToken).ConfigureAwait(false)).ToList(); if (tablesToVacuum.Count == 0) { @@ -96,7 +98,7 @@ namespace Jellyfin.Server.Implementations.ScheduledTasks try { - await _dbContext.ExecuteAsync($"VACUUM ANALYZE \"{table.TableName}\""); + await dbContext.Database.ExecuteSqlInterpolatedAsync($"VACUUM ANALYZE \"{table.TableName}\"", cancellationToken).ConfigureAwait(false); _logger.LogInformation("Successfully vacuumed and analyzed table {TableName}.", table.TableName); } catch (Exception ex) @@ -129,7 +131,7 @@ namespace Jellyfin.Server.Implementations.ScheduledTasks { new TaskTriggerInfo { - Type = TaskTriggerInfo.TriggerInterval, + Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index f4320974..4edc979f 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -72,6 +72,7 @@ + @@ -121,16 +122,26 @@ PreserveNewest false + + + PreserveNewest + PreserveNewest + false + - + + + + + diff --git a/Jellyfin.Server/scripts/Linux/Weekly-Performance-Monitor.sh b/Jellyfin.Server/scripts/Linux/Weekly-Performance-Monitor.sh new file mode 100644 index 00000000..6424b83e --- /dev/null +++ b/Jellyfin.Server/scripts/Linux/Weekly-Performance-Monitor.sh @@ -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" < + + + + diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 42315598..17c19d0d 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -24,6 +24,7 @@ + diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 29ed59b1..4a5e01f4 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -37,5 +37,8 @@ + + + diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index a0a14488..2ce33960 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -29,6 +29,7 @@ + diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index bd9cf04b..aa8f96d6 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -43,6 +43,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 7287889b..9eb3112a 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -21,6 +21,7 @@ + diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index 0d705c1b..31060bf3 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -37,5 +37,8 @@ + + + diff --git a/scripts/linux/rebuild-solution.sh b/scripts/linux/rebuild-solution.sh new file mode 100644 index 00000000..d7782599 --- /dev/null +++ b/scripts/linux/rebuild-solution.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Linux script to rebuild the Jellyfin solution properly +# This script cleans and rebuilds in the correct order to resolve CS0006 metadata errors + +echo -e "\e[36mStarting Jellyfin solution rebuild...\e[0m" + +# Step 1: Clean the solution +echo -e "\n\e[33m[1/3] Cleaning solution...\e[0m" +dotnet clean Jellyfin.sln --configuration Debug + +# Remove obj and bin directories to ensure clean state +echo -e "\n\e[33m[2/3] Removing build artifacts...\e[0m" +find . -name bin -type d -exec rm -rf {} + +find . -name obj -type d -exec rm -rf {} + + +# Step 3: Restore NuGet packages +echo -e "\n\e[33m[3/3] Restoring NuGet packages...\e[0m" +dotnet restore Jellyfin.sln + +# Step 4: Build the solution +echo -e "\n\e[33m[4/4] Building solution...\e[0m" +dotnet build Jellyfin.sln --configuration Debug --no-restore + +echo -e "\n\e[32mBuild complete!\e[0m" +echo -e "\e[36mIf you still see errors, try building specific projects in this order:\e[0m" +echo -e "\e[90m 1. Jellyfin.Database.Implementations\e[0m" +echo -e "\e[90m 2. Jellyfin.Database.Providers.Postgres\e[0m" +echo -e "\e[90m 3. Jellyfin.Server.Implementations\e[0m" +echo -e "\e[90m 4. Emby.Server.Implementations\e[0m" +echo -e "\e[90m 5. Jellyfin.Server\e[0m" diff --git a/scripts/Add-All-Indexes.bat b/scripts/windows/Add-All-Indexes.bat similarity index 100% rename from scripts/Add-All-Indexes.bat rename to scripts/windows/Add-All-Indexes.bat diff --git a/scripts/Add-Base-Indexes.bat b/scripts/windows/Add-Base-Indexes.bat similarity index 100% rename from scripts/Add-Base-Indexes.bat rename to scripts/windows/Add-Base-Indexes.bat diff --git a/scripts/Add-Indexes-Quick.bat b/scripts/windows/Add-Indexes-Quick.bat similarity index 100% rename from scripts/Add-Indexes-Quick.bat rename to scripts/windows/Add-Indexes-Quick.bat diff --git a/scripts/Add-Indexes-Quick.ps1 b/scripts/windows/Add-Indexes-Quick.ps1 similarity index 100% rename from scripts/Add-Indexes-Quick.ps1 rename to scripts/windows/Add-Indexes-Quick.ps1 diff --git a/scripts/Apply-SupplementaryIndexes.ps1 b/scripts/windows/Apply-SupplementaryIndexes.ps1 similarity index 100% rename from scripts/Apply-SupplementaryIndexes.ps1 rename to scripts/windows/Apply-SupplementaryIndexes.ps1 diff --git a/scripts/ConvertToFileScopedNamespaces.ps1 b/scripts/windows/ConvertToFileScopedNamespaces.ps1 similarity index 100% rename from scripts/ConvertToFileScopedNamespaces.ps1 rename to scripts/windows/ConvertToFileScopedNamespaces.ps1 diff --git a/scripts/Find-SyncDatabaseOperations.ps1 b/scripts/windows/Find-SyncDatabaseOperations.ps1 similarity index 100% rename from scripts/Find-SyncDatabaseOperations.ps1 rename to scripts/windows/Find-SyncDatabaseOperations.ps1 diff --git a/scripts/Fix-ItemValues-Performance.ps1 b/scripts/windows/Fix-ItemValues-Performance.ps1 similarity index 100% rename from scripts/Fix-ItemValues-Performance.ps1 rename to scripts/windows/Fix-ItemValues-Performance.ps1 diff --git a/scripts/FixJellyfinPaths.ps1 b/scripts/windows/FixJellyfinPaths.ps1 similarity index 100% rename from scripts/FixJellyfinPaths.ps1 rename to scripts/windows/FixJellyfinPaths.ps1 diff --git a/scripts/FixStartupJsonWebDir.ps1 b/scripts/windows/FixStartupJsonWebDir.ps1 similarity index 100% rename from scripts/FixStartupJsonWebDir.ps1 rename to scripts/windows/FixStartupJsonWebDir.ps1 diff --git a/scripts/FixStyleCopWarnings.ps1 b/scripts/windows/FixStyleCopWarnings.ps1 similarity index 100% rename from scripts/FixStyleCopWarnings.ps1 rename to scripts/windows/FixStyleCopWarnings.ps1 diff --git a/scripts/windows/Setup-Weekly-Monitor.ps1 b/scripts/windows/Setup-Weekly-Monitor.ps1 new file mode 100644 index 00000000..bf288669 --- /dev/null +++ b/scripts/windows/Setup-Weekly-Monitor.ps1 @@ -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 diff --git a/scripts/windows/Weekly-Performance-Monitor.ps1 b/scripts/windows/Weekly-Performance-Monitor.ps1 new file mode 100644 index 00000000..336c2acc --- /dev/null +++ b/scripts/windows/Weekly-Performance-Monitor.ps1 @@ -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 diff --git a/scripts/build-installer.ps1 b/scripts/windows/build-installer.ps1 similarity index 100% rename from scripts/build-installer.ps1 rename to scripts/windows/build-installer.ps1 diff --git a/scripts/copy-sql-files.ps1 b/scripts/windows/copy-sql-files.ps1 similarity index 100% rename from scripts/copy-sql-files.ps1 rename to scripts/windows/copy-sql-files.ps1 diff --git a/scripts/windows/db-config.ps1 b/scripts/windows/db-config.ps1 new file mode 100644 index 00000000..f3c036d4 --- /dev/null +++ b/scripts/windows/db-config.ps1 @@ -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 "" diff --git a/scripts/db-quick.ps1 b/scripts/windows/db-quick.ps1 similarity index 100% rename from scripts/db-quick.ps1 rename to scripts/windows/db-quick.ps1 diff --git a/scripts/generate-regression-sql.ps1 b/scripts/windows/generate-regression-sql.ps1 similarity index 100% rename from scripts/generate-regression-sql.ps1 rename to scripts/windows/generate-regression-sql.ps1 diff --git a/scripts/publish-with-sql.ps1 b/scripts/windows/publish-with-sql.ps1 similarity index 100% rename from scripts/publish-with-sql.ps1 rename to scripts/windows/publish-with-sql.ps1 diff --git a/scripts/remove-sqlite.ps1 b/scripts/windows/remove-sqlite.ps1 similarity index 100% rename from scripts/remove-sqlite.ps1 rename to scripts/windows/remove-sqlite.ps1 diff --git a/scripts/rollback-to-net10.ps1 b/scripts/windows/rollback-to-net10.ps1 similarity index 100% rename from scripts/rollback-to-net10.ps1 rename to scripts/windows/rollback-to-net10.ps1 diff --git a/scripts/test_api.py b/scripts/windows/test_api.py similarity index 100% rename from scripts/test_api.py rename to scripts/windows/test_api.py diff --git a/scripts/verify-migration.ps1 b/scripts/windows/verify-migration.ps1 similarity index 100% rename from scripts/verify-migration.ps1 rename to scripts/windows/verify-migration.ps1 diff --git a/src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj b/src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj index 2da59180..e390c2c7 100644 --- a/src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj +++ b/src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs index 4ca6946a..97b9442e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs @@ -350,6 +350,21 @@ public class JellyfinDbContext(DbContextOptions options, ILog }).ConfigureAwait(false); return result; } + catch (DbUpdateConcurrencyException ex) + { + // When marking items as played in quick succession, concurrency conflicts can occur. + // To resolve this, we'll reload the conflicting entities from the database and retry. + logger.LogWarning(ex, "Concurrency exception, retrying operation."); + + foreach (var entry in ex.Entries) + { + // Reload the entity from the database to get the latest version + await entry.ReloadAsync(cancellationToken).ConfigureAwait(false); + } + + // Retry saving changes after resolving conflicts + return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false); + } catch (DbUpdateException ex) { // Check if it's a constraint violation (works across all database providers) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Jellyfin.Database.Providers.Postgres.csproj b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Jellyfin.Database.Providers.Postgres.csproj index a6c95114..85ae0ce0 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Jellyfin.Database.Providers.Postgres.csproj +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Jellyfin.Database.Providers.Postgres.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index f96053ce..38722058 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index b6c566f8..6d0541bc 100644 --- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -25,6 +25,7 @@ + diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index f5a8d45b..12029c2f 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj index f274df54..f0d51be6 100644 --- a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj +++ b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index 2d63d926..05c9c67c 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index d5dc105b..5b32fb82 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Jellyfin.Networking/Jellyfin.Networking.csproj b/src/Jellyfin.Networking/Jellyfin.Networking.csproj index a8631fa0..0f12594b 100644 --- a/src/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/src/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -10,6 +10,10 @@ + + + + diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 5c6ae5d0..8ffc90c4 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 7b2c2b00..099fb963 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index a4974667..57505ca1 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj index 8c007377..fafbfe3d 100644 --- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj +++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj index 15211010..e3dae3b3 100644 --- a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj +++ b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj index 981d9114..79b881a6 100644 --- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj index d4679fe6..f9250750 100644 --- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index 3dd97b83..6b50c2c0 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj index 09cfc9ce..5b67e479 100644 --- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj +++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 991cada5..0230084a 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj index f69c052e..922fe2d9 100644 --- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj index 2913956c..1d8e977f 100644 --- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj +++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj @@ -16,6 +16,7 @@ + diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 94fce55b..6783f416 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -23,6 +23,7 @@ + diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj index dd2e4f34..c4b6f6ac 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj index fa802c37..8aae4a4b 100644 --- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj index 51a614c8..89e6e2c4 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj +++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj @@ -13,6 +13,7 @@ +