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" < \"$output_file\""
+ fi
+
+ echo -e "\e[36mConnecting to: $DB_NAME@$DB_HOST as $DB_USER\e[0m"
+ eval "$cmd"
+}
+
+# Display current configuration
+echo -e "\e[36m========================================\e[0m"
+echo -e "\e[36mDatabase Configuration Loaded\e[0m"
+echo -e "\e[36m========================================\e[0m"
+echo -e "\e[33mDatabase: $DB_NAME\e[0m"
+echo -e "\e[33mUser: $DB_USER\e[0m"
+echo -e "\e[33mHost: $DB_HOST\e[0m"
+echo -e "\e[33mPort: $DB_PORT\e[0m"
+echo -e "\e[36m========================================\e[0m"
+echo ""
+echo -e "\e[90mTo change database, edit: db-config.sh\e[0m"
+echo -e "\e[90mThen run: source db-config.sh\e[0m"
+echo ""
diff --git a/scripts/Setup-Weekly-Monitor.ps1 b/Jellyfin.Server/scripts/windows/Setup-Weekly-Monitor.ps1
similarity index 100%
rename from scripts/Setup-Weekly-Monitor.ps1
rename to Jellyfin.Server/scripts/windows/Setup-Weekly-Monitor.ps1
diff --git a/scripts/Weekly-Performance-Monitor.ps1 b/Jellyfin.Server/scripts/windows/Weekly-Performance-Monitor.ps1
similarity index 100%
rename from scripts/Weekly-Performance-Monitor.ps1
rename to Jellyfin.Server/scripts/windows/Weekly-Performance-Monitor.ps1
diff --git a/scripts/db-config.ps1 b/Jellyfin.Server/scripts/windows/db-config.ps1
similarity index 100%
rename from scripts/db-config.ps1
rename to Jellyfin.Server/scripts/windows/db-config.ps1
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 135506b2..3eef0910 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -60,4 +60,8 @@
+
+
+
+
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 @@
+