Add scripts for PostgreSQL migration and SQLite removal; update project references
- Introduced `publish-with-sql.ps1` to publish Jellyfin with SQL files. - Added `remove-sqlite.ps1` to facilitate the removal of SQLite dependencies for PostgreSQL-only deployment. - Created `rollback-to-net10.ps1` to revert .NET 11 changes back to .NET 10. - Implemented `test_api.py` for testing API interactions. - Added `verify-migration.ps1` to verify PostgreSQL migration steps. - Updated various `.csproj` files to include `Microsoft.Kiota.Abstractions` package. - Enhanced `JellyfinDbContext` to handle concurrency exceptions during save operations. - Updated tests to include new package references and ensure compatibility with changes.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
@echo off
|
||||
REM Quick script to add ALL performance indexes (base + supplementary)
|
||||
|
||||
echo ========================================
|
||||
echo Add ALL Performance Indexes
|
||||
echo ========================================
|
||||
echo.
|
||||
echo This will create:
|
||||
echo - 18 base performance indexes
|
||||
echo - 5 supplementary indexes
|
||||
echo Total: 23 indexes
|
||||
echo.
|
||||
echo NOTE: This uses the database configured in db-config.ps1
|
||||
echo Current default: jellyfin_testsdata
|
||||
echo.
|
||||
echo This may take 10-30 minutes...
|
||||
echo.
|
||||
|
||||
REM Use PowerShell to load config and run
|
||||
powershell -Command ". .\db-config.ps1; & $PSQL_PATH -U $DB_USER -d $DB_NAME -f sql\all_performance_indexes.sql"
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Success! All 23 indexes added.
|
||||
echo ========================================
|
||||
echo.
|
||||
echo Performance improvement: 70-90 percent faster!
|
||||
echo.
|
||||
echo Next: Restart Jellyfin
|
||||
echo.
|
||||
) else (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Failed to add indexes
|
||||
echo ========================================
|
||||
echo.
|
||||
)
|
||||
|
||||
pause
|
||||
@@ -0,0 +1,37 @@
|
||||
@echo off
|
||||
REM Add missing base performance indexes to Jellyfin database
|
||||
|
||||
echo ========================================
|
||||
echo Add Base Performance Indexes
|
||||
echo ========================================
|
||||
echo.
|
||||
echo These indexes were missing from InitialCreate
|
||||
echo Adding 18 essential performance indexes...
|
||||
echo.
|
||||
|
||||
"C:\Program Files\PostgreSQL\18\bin\psql.exe" -U jellyfin -d jellyfin -f sql\add_base_performance_indexes.sql
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Success! Base indexes added.
|
||||
echo ========================================
|
||||
echo.
|
||||
echo Indexes created:
|
||||
echo - 9 on BaseItems
|
||||
echo - 2 on BaseItemProviders
|
||||
echo - 2 on MediaStreamInfos
|
||||
echo - 2 on PeopleBaseItemMap
|
||||
echo - 3 on UserData
|
||||
echo.
|
||||
echo Next: Restart Jellyfin
|
||||
echo.
|
||||
) else (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Failed to add indexes
|
||||
echo ========================================
|
||||
echo.
|
||||
)
|
||||
|
||||
pause
|
||||
@@ -0,0 +1,38 @@
|
||||
@echo off
|
||||
REM Quick script to add supplementary indexes to jellyfin_testsdata
|
||||
|
||||
echo ========================================
|
||||
echo Add Supplementary Indexes
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
echo Connecting to jellyfin_testsdata database...
|
||||
echo.
|
||||
|
||||
REM Apply the indexes
|
||||
"C:\Program Files\PostgreSQL\18\bin\psql.exe" -U jellyfin -d jellyfin -f sql\schema_init\10_create_supplementary_indexes.sql
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Success! Indexes added.
|
||||
echo ========================================
|
||||
echo.
|
||||
echo Next steps:
|
||||
echo 1. Restart Jellyfin
|
||||
echo 2. Monitor performance
|
||||
echo.
|
||||
) else (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Failed to add indexes
|
||||
echo ========================================
|
||||
echo.
|
||||
echo Troubleshooting:
|
||||
echo - Ensure PostgreSQL is running
|
||||
echo - Check database name is correct
|
||||
echo - Verify credentials
|
||||
echo.
|
||||
)
|
||||
|
||||
pause
|
||||
@@ -0,0 +1,20 @@
|
||||
# Add Supplementary Indexes - Quick Command
|
||||
|
||||
Write-Host "Connecting to jellyfin_testsdata and adding supplementary indexes..." -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Quick and simple - just run the SQL script
|
||||
psql -U jellyfin -d jellyfin_testsdata -f sql\schema_init\10_create_supplementary_indexes.sql
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host ""
|
||||
Write-Host "✓ Success! Supplementary indexes added." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Next: Restart Jellyfin to see performance improvements" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host ""
|
||||
Write-Host "❌ Failed. Check that:" -ForegroundColor Red
|
||||
Write-Host " - PostgreSQL is running" -ForegroundColor Gray
|
||||
Write-Host " - Database 'jellyfin_testsdata' exists" -ForegroundColor Gray
|
||||
Write-Host " - You have the correct password" -ForegroundColor Gray
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
# Apply Supplementary Indexes Migration to PostgreSQL Database
|
||||
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "Apply Supplementary Indexes Migration" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Configuration
|
||||
$connectionString = "Host=localhost;Port=5432;Database=jellyfin_testsdata;Username=jellyfin;Password=YOUR_PASSWORD"
|
||||
$migrationName = "AddSupplementaryIndexes"
|
||||
|
||||
Write-Host "Step 1: Build the solution" -ForegroundColor Yellow
|
||||
Write-Host "This ensures the migration is compiled..." -ForegroundColor Gray
|
||||
dotnet build src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres\Jellyfin.Database.Providers.Postgres.csproj
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ Build failed. Please fix compilation errors." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✓ Build successful" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Step 2: Check current database state" -ForegroundColor Yellow
|
||||
Write-Host "Connecting to jellyfin_testsdata..." -ForegroundColor Gray
|
||||
|
||||
# Test database connection
|
||||
try {
|
||||
$testQuery = @"
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'library'
|
||||
AND table_name = 'BaseItems'
|
||||
) as table_exists;
|
||||
"@
|
||||
|
||||
$result = psql -U jellyfin -d jellyfin_testsdata -t -c $testQuery 2>&1
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✓ Database connection successful" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ Database connection failed" -ForegroundColor Red
|
||||
Write-Host "Error: $result" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host "Please ensure:" -ForegroundColor Yellow
|
||||
Write-Host " 1. PostgreSQL is running" -ForegroundColor Gray
|
||||
Write-Host " 2. Database 'jellyfin_testsdata' exists" -ForegroundColor Gray
|
||||
Write-Host " 3. Credentials are correct" -ForegroundColor Gray
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host "❌ Failed to test database connection: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Step 3: Check for existing indexes" -ForegroundColor Yellow
|
||||
Write-Host "Checking which supplementary indexes already exist..." -ForegroundColor Gray
|
||||
|
||||
$checkIndexQuery = @"
|
||||
SELECT
|
||||
indexname,
|
||||
CASE WHEN indexname IS NOT NULL THEN 'EXISTS' ELSE 'MISSING' END as status
|
||||
FROM (
|
||||
VALUES
|
||||
('idx_baseitems_type_isvirtualitem_topparentid'),
|
||||
('idx_baseitems_topparentid_isfolder'),
|
||||
('idx_baseitems_datecreated_filtered'),
|
||||
('idx_itemvaluesmap_itemvalueid_itemid'),
|
||||
('idx_activitylogs_userid_datecreated')
|
||||
) AS wanted(indexname)
|
||||
LEFT JOIN pg_indexes ON pg_indexes.indexname = wanted.indexname
|
||||
ORDER BY wanted.indexname;
|
||||
"@
|
||||
|
||||
$indexStatus = psql -U jellyfin -d jellyfin_testsdata -c $checkIndexQuery
|
||||
|
||||
Write-Host $indexStatus
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Step 4: Apply migration using EF Core" -ForegroundColor Yellow
|
||||
Write-Host "This will create any missing indexes..." -ForegroundColor Gray
|
||||
|
||||
# Option 1: Use dotnet ef (if you have it installed)
|
||||
Write-Host ""
|
||||
Write-Host "Choose migration method:" -ForegroundColor Cyan
|
||||
Write-Host "1. Use dotnet ef database update (requires dotnet-ef tool)" -ForegroundColor White
|
||||
Write-Host "2. Use SQL script generation" -ForegroundColor White
|
||||
Write-Host "3. Apply SQL directly (recommended)" -ForegroundColor White
|
||||
Write-Host ""
|
||||
|
||||
$choice = Read-Host "Enter your choice (1, 2, or 3)"
|
||||
|
||||
switch ($choice) {
|
||||
"1" {
|
||||
Write-Host "Applying migration using dotnet ef..." -ForegroundColor Yellow
|
||||
$env:ConnectionStrings__DefaultConnection = $connectionString
|
||||
|
||||
dotnet ef database update --project src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres\Jellyfin.Database.Providers.Postgres.csproj
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✓ Migration applied successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ Migration failed" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
"2" {
|
||||
Write-Host "Generating SQL script..." -ForegroundColor Yellow
|
||||
|
||||
dotnet ef migrations script --project src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres\Jellyfin.Database.Providers.Postgres.csproj --output sql\migration_supplementary_indexes.sql
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✓ SQL script generated at: sql\migration_supplementary_indexes.sql" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "To apply, run:" -ForegroundColor Yellow
|
||||
Write-Host "psql -U jellyfin -d jellyfin_testsdata -f sql\migration_supplementary_indexes.sql" -ForegroundColor White
|
||||
} else {
|
||||
Write-Host "❌ Script generation failed" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
"3" {
|
||||
Write-Host "Applying SQL directly..." -ForegroundColor Yellow
|
||||
|
||||
# Apply the SQL from the schema_init script
|
||||
psql -U jellyfin -d jellyfin_testsdata -f sql\schema_init\10_create_supplementary_indexes.sql
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✓ Indexes created successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "❌ Index creation failed" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
default {
|
||||
Write-Host "Invalid choice. Exiting." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Step 5: Verify indexes were created" -ForegroundColor Yellow
|
||||
|
||||
$verifyQuery = @"
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
indexname,
|
||||
pg_size_pretty(pg_relation_size(indexrelid)) as size
|
||||
FROM pg_indexes
|
||||
JOIN pg_stat_user_indexes USING (schemaname, tablename, indexname)
|
||||
WHERE indexname IN (
|
||||
'idx_baseitems_type_isvirtualitem_topparentid',
|
||||
'idx_baseitems_topparentid_isfolder',
|
||||
'idx_baseitems_datecreated_filtered',
|
||||
'idx_itemvaluesmap_itemvalueid_itemid',
|
||||
'idx_activitylogs_userid_datecreated'
|
||||
)
|
||||
ORDER BY schemaname, tablename, indexname;
|
||||
"@
|
||||
|
||||
$verification = psql -U jellyfin -d jellyfin_testsdata -c $verifyQuery
|
||||
|
||||
Write-Host $verification
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "Migration Complete!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:" -ForegroundColor Yellow
|
||||
Write-Host "1. Restart Jellyfin to clear connection pools" -ForegroundColor White
|
||||
Write-Host "2. Monitor index usage with: psql -U jellyfin -d jellyfin_testsdata -f sql\diagnostics.sql" -ForegroundColor White
|
||||
Write-Host "3. Check performance improvements" -ForegroundColor White
|
||||
Write-Host ""
|
||||
@@ -0,0 +1,388 @@
|
||||
#!/usr/bin/env pwsh
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Converts C# files from block-scoped to file-scoped namespaces and fixes System namespace conflicts.
|
||||
|
||||
.DESCRIPTION
|
||||
This script converts all C# files in MediaBrowser.Model from block-scoped namespaces
|
||||
to file-scoped namespaces, and replaces System using directives with global::System
|
||||
to resolve conflicts with MediaBrowser.Model.System namespace.
|
||||
|
||||
.PARAMETER Path
|
||||
The root path to search for C# files. Defaults to "MediaBrowser.Model"
|
||||
|
||||
.PARAMETER DryRun
|
||||
If specified, shows what would be changed without actually modifying files
|
||||
|
||||
.PARAMETER Verbose
|
||||
Shows detailed processing information
|
||||
|
||||
.PARAMETER BackupFiles
|
||||
Creates .bak files before conversion
|
||||
|
||||
.EXAMPLE
|
||||
.\ConvertToFileScopedNamespaces.ps1 -DryRun
|
||||
|
||||
.EXAMPLE
|
||||
.\ConvertToFileScopedNamespaces.ps1 -BackupFiles
|
||||
|
||||
.EXAMPLE
|
||||
.\ConvertToFileScopedNamespaces.ps1 -Path "MediaBrowser.Model\Dlna" -Verbose
|
||||
#>
|
||||
|
||||
param(
|
||||
[string]$Path = "MediaBrowser.Model",
|
||||
[switch]$DryRun,
|
||||
[switch]$BackupFiles
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$VerbosePreference = if ($PSBoundParameters.ContainsKey('Verbose')) { 'Continue' } else { 'SilentlyContinue' }
|
||||
|
||||
function Get-IndentString {
|
||||
param([string]$Line)
|
||||
|
||||
if ($Line -match '^(\s+)') {
|
||||
return $Matches[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function Get-IndentLevel {
|
||||
param([string]$IndentString)
|
||||
|
||||
# Count spaces (4 spaces = 1 level) or tabs (1 tab = 1 level)
|
||||
$spaces = ($IndentString.ToCharArray() | Where-Object { $_ -eq ' ' }).Count
|
||||
$tabs = ($IndentString.ToCharArray() | Where-Object { $_ -eq "`t" }).Count
|
||||
|
||||
return [Math]::Max($tabs, [Math]::Floor($spaces / 4))
|
||||
}
|
||||
|
||||
function Remove-OneIndentLevel {
|
||||
param([string]$Line, [ref]$IndentChar, [ref]$IndentSize)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Line)) {
|
||||
return $Line
|
||||
}
|
||||
|
||||
$indent = Get-IndentString -Line $Line
|
||||
|
||||
if ($indent.Length -eq 0) {
|
||||
return $Line
|
||||
}
|
||||
|
||||
# Detect indent character and size on first indented line
|
||||
if ($IndentChar.Value -eq $null) {
|
||||
if ($indent[0] -eq "`t") {
|
||||
$IndentChar.Value = "`t"
|
||||
$IndentSize.Value = 1
|
||||
} else {
|
||||
$IndentChar.Value = ' '
|
||||
# Detect indent size (usually 4 spaces)
|
||||
$IndentSize.Value = 4
|
||||
if ($indent.Length -ge 2) {
|
||||
# Try to detect actual indent size
|
||||
for ($i = 1; $i -le 8; $i++) {
|
||||
if ($indent.Length % $i -eq 0) {
|
||||
$IndentSize.Value = $i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Write-Verbose "Detected indent: '$($IndentChar.Value)' x $($IndentSize.Value)"
|
||||
}
|
||||
|
||||
# Remove one level of indentation
|
||||
if ($IndentChar.Value -eq "`t") {
|
||||
if ($Line -match '^\t(.*)') {
|
||||
return $Matches[1]
|
||||
}
|
||||
} else {
|
||||
$pattern = "^" + (' ' * $IndentSize.Value) + '(.*)'
|
||||
if ($Line -match $pattern) {
|
||||
return $Matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
return $Line
|
||||
}
|
||||
|
||||
function Test-FileScopedNamespace {
|
||||
param([string]$Content)
|
||||
|
||||
# Check if already using file-scoped namespace (semicolon after namespace declaration)
|
||||
return $Content -match 'namespace\s+[\w\.]+\s*;'
|
||||
}
|
||||
|
||||
function Test-BlockScopedNamespace {
|
||||
param([string]$Content)
|
||||
|
||||
# Check for block-scoped namespace
|
||||
return $Content -match 'namespace\s+[\w\.]+\s*\r?\n?\s*\{'
|
||||
}
|
||||
|
||||
function Add-GlobalPrefix {
|
||||
param([string]$Content)
|
||||
|
||||
# System namespaces to prefix (ordered from most specific to least)
|
||||
$systemNamespaces = @(
|
||||
'System\.Xml\.Serialization',
|
||||
'System\.ComponentModel\.DataAnnotations',
|
||||
'System\.ComponentModel',
|
||||
'System\.Collections\.Generic',
|
||||
'System\.Collections\.Concurrent',
|
||||
'System\.Collections',
|
||||
'System\.Diagnostics\.CodeAnalysis',
|
||||
'System\.Diagnostics',
|
||||
'System\.Globalization',
|
||||
'System\.Linq\.Expressions',
|
||||
'System\.Linq',
|
||||
'System\.Text\.Json\.Serialization',
|
||||
'System\.Text\.Json',
|
||||
'System\.Text\.RegularExpressions',
|
||||
'System\.Text',
|
||||
'System\.Threading\.Tasks',
|
||||
'System\.Threading',
|
||||
'System\.Runtime\.Serialization',
|
||||
'System\.Runtime\.CompilerServices',
|
||||
'System\.Runtime',
|
||||
'System\.Net\.Http',
|
||||
'System\.Net\.Sockets',
|
||||
'System\.Net\.WebSockets',
|
||||
'System\.Net\.Mime',
|
||||
'System\.Net',
|
||||
'System\.IO\.Compression',
|
||||
'System\.IO',
|
||||
'System\.Security',
|
||||
'System' # Must be last to avoid partial matches
|
||||
)
|
||||
|
||||
foreach ($ns in $systemNamespaces) {
|
||||
# Don't add global:: if it's already there
|
||||
# Match: "using System.X" but not "using global::System.X"
|
||||
$pattern = '(\s+using\s+)(?!global::)(' + $ns + ')(\s|;)'
|
||||
$replacement = '${1}global::${2}${3}'
|
||||
$Content = $Content -replace $pattern, $replacement
|
||||
}
|
||||
|
||||
return $Content
|
||||
}
|
||||
|
||||
function Convert-ToFileScopedNamespace {
|
||||
param(
|
||||
[string]$FilePath,
|
||||
[switch]$DryRun,
|
||||
[switch]$CreateBackup
|
||||
)
|
||||
|
||||
$content = Get-Content -Path $FilePath -Raw
|
||||
$originalContent = $content
|
||||
$modified = $false
|
||||
|
||||
# Check current state
|
||||
$hasFileScopedNS = Test-FileScopedNamespace -Content $content
|
||||
$hasBlockScopedNS = Test-BlockScopedNamespace -Content $content
|
||||
|
||||
if ($hasFileScopedNS) {
|
||||
Write-Verbose " Already using file-scoped namespace"
|
||||
|
||||
# But still add global:: prefixes if needed
|
||||
$contentWithGlobal = Add-GlobalPrefix -Content $content
|
||||
if ($contentWithGlobal -ne $content) {
|
||||
$content = $contentWithGlobal
|
||||
$modified = $true
|
||||
Write-Host " [UPDATED] Added global:: prefixes" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host " [SKIP] Already correct" -ForegroundColor Gray
|
||||
return $false
|
||||
}
|
||||
}
|
||||
elseif (-not $hasBlockScopedNS) {
|
||||
Write-Host " [SKIP] No namespace found" -ForegroundColor Gray
|
||||
return $false
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Converting block-scoped to file-scoped namespace"
|
||||
|
||||
# Extract namespace name and line
|
||||
if ($content -match 'namespace\s+([\w\.]+)\s*\r?\n?\s*\{') {
|
||||
$namespaceName = $Matches[1]
|
||||
Write-Verbose " Namespace: $namespaceName"
|
||||
} else {
|
||||
Write-Warning " Could not parse namespace"
|
||||
return $false
|
||||
}
|
||||
|
||||
# Step 1: Convert namespace declaration to file-scoped
|
||||
$content = $content -replace '(namespace\s+[\w\.]+)\s*\r?\n?\s*\{', '$1;'
|
||||
$modified = $true
|
||||
|
||||
# Step 2: Add global:: prefixes to System using directives
|
||||
$content = Add-GlobalPrefix -Content $content
|
||||
|
||||
# Step 3: Parse into lines for indentation removal
|
||||
$lines = $content -split '\r?\n'
|
||||
$hasWindowsLineEndings = $originalContent -match '\r\n'
|
||||
|
||||
# Find namespace declaration line
|
||||
$namespaceLineIndex = -1
|
||||
for ($i = 0; $i -lt $lines.Length; $i++) {
|
||||
if ($lines[$i] -match '^namespace\s+[\w\.]+\s*;') {
|
||||
$namespaceLineIndex = $i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($namespaceLineIndex -lt 0) {
|
||||
Write-Warning " Could not find namespace declaration after conversion"
|
||||
return $false
|
||||
}
|
||||
|
||||
# Step 4: Remove one level of indentation from all lines after namespace
|
||||
$indentChar = $null
|
||||
$indentSize = 4
|
||||
$indentCharRef = [ref]$indentChar
|
||||
$indentSizeRef = [ref]$indentSize
|
||||
|
||||
for ($i = $namespaceLineIndex + 1; $i -lt $lines.Length; $i++) {
|
||||
$lines[$i] = Remove-OneIndentLevel -Line $lines[$i] -IndentChar $indentCharRef -IndentSize $indentSizeRef
|
||||
}
|
||||
|
||||
# Step 5: Remove the closing brace of the namespace
|
||||
# Find the last non-empty, non-whitespace line that's just a closing brace
|
||||
$closingBraceIndex = -1
|
||||
for ($i = $lines.Length - 1; $i -gt $namespaceLineIndex; $i--) {
|
||||
$trimmed = $lines[$i].Trim()
|
||||
if ($trimmed -eq '}') {
|
||||
$closingBraceIndex = $i
|
||||
break
|
||||
}
|
||||
elseif ($trimmed -ne '' -and $trimmed -ne '}') {
|
||||
# Found non-brace content, stop looking
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($closingBraceIndex -ge 0) {
|
||||
Write-Verbose " Removing closing brace at line $closingBraceIndex"
|
||||
$lines = $lines[0..($closingBraceIndex-1)] + $lines[($closingBraceIndex+1)..($lines.Length-1)]
|
||||
}
|
||||
|
||||
# Step 6: Rejoin lines
|
||||
$lineEnding = if ($hasWindowsLineEndings) { "`r`n" } else { "`n" }
|
||||
$content = ($lines | Where-Object { $_ -ne $null }) -join $lineEnding
|
||||
|
||||
# Step 7: Clean up trailing whitespace and ensure single newline at end
|
||||
$content = $content.TrimEnd()
|
||||
$content += $lineEnding
|
||||
|
||||
Write-Host " [CONVERTED] Block-scoped → File-scoped" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Apply changes
|
||||
if ($modified -and $content -ne $originalContent) {
|
||||
if ($DryRun) {
|
||||
Write-Host " [DRY-RUN] Would save changes" -ForegroundColor Cyan
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($CreateBackup) {
|
||||
$backupPath = "$FilePath.bak"
|
||||
Copy-Item -Path $FilePath -Destination $backupPath -Force
|
||||
Write-Verbose " Created backup: $backupPath"
|
||||
}
|
||||
|
||||
# Write with correct encoding (UTF-8 without BOM for .cs files)
|
||||
[System.IO.File]::WriteAllText($FilePath, $content, [System.Text.UTF8Encoding]::new($false))
|
||||
return $true
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
# Main script
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host "C# File-Scoped Namespace Converter" -ForegroundColor Cyan
|
||||
Write-Host "Fixes MediaBrowser.Model.System namespace conflicts" -ForegroundColor Cyan
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "DRY RUN MODE - No files will be modified" -ForegroundColor Yellow
|
||||
Write-Host
|
||||
}
|
||||
|
||||
if ($BackupFiles) {
|
||||
Write-Host "Backup mode enabled - .bak files will be created" -ForegroundColor Yellow
|
||||
Write-Host
|
||||
}
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
Write-Error "❌ Path not found: $Path"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Find all C# files
|
||||
$files = Get-ChildItem -Path $Path -Filter "*.cs" -Recurse | Where-Object {
|
||||
$_.FullName -notmatch "\\obj\\" -and
|
||||
$_.FullName -notmatch "\\bin\\" -and
|
||||
$_.FullName -notmatch "\\Properties\\AssemblyInfo\.cs$"
|
||||
}
|
||||
|
||||
Write-Host "Found $($files.Count) C# files in $Path" -ForegroundColor Cyan
|
||||
Write-Host
|
||||
|
||||
$convertedCount = 0
|
||||
$skippedCount = 0
|
||||
$errorCount = 0
|
||||
$updatedCount = 0
|
||||
|
||||
foreach ($file in $files) {
|
||||
$relativePath = $file.FullName.Replace((Get-Location).Path, "").TrimStart([char]92, [char]47)
|
||||
Write-Host "[Processing] $relativePath" -ForegroundColor White
|
||||
|
||||
try {
|
||||
$result = Convert-ToFileScopedNamespace -FilePath $file.FullName -DryRun:$DryRun -CreateBackup:$BackupFiles
|
||||
|
||||
if ($result) {
|
||||
if ($result -eq "updated") {
|
||||
$updatedCount++
|
||||
} else {
|
||||
$convertedCount++
|
||||
}
|
||||
} else {
|
||||
$skippedCount++
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host " [ERROR] $_" -ForegroundColor Red
|
||||
Write-Verbose $_.ScriptStackTrace
|
||||
$errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host "Summary:" -ForegroundColor Cyan
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " Total files: $($files.Count)" -ForegroundColor White
|
||||
Write-Host " Converted: $convertedCount" -ForegroundColor Green
|
||||
Write-Host " Updated (global::): $updatedCount" -ForegroundColor Yellow
|
||||
Write-Host " Skipped: $skippedCount" -ForegroundColor Gray
|
||||
Write-Host " Errors: $errorCount" -ForegroundColor Red
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host
|
||||
Write-Host "This was a DRY RUN. Run without -DryRun to apply changes." -ForegroundColor Yellow
|
||||
} elseif ($convertedCount -gt 0 -or $updatedCount -gt 0) {
|
||||
Write-Host
|
||||
Write-Host "Conversion complete! Next steps:" -ForegroundColor Green
|
||||
Write-Host " 1. Review changes with: git diff" -ForegroundColor White
|
||||
Write-Host " 2. Build project: dotnet build MediaBrowser.Model\MediaBrowser.Model.csproj" -ForegroundColor White
|
||||
Write-Host " 3. If successful: git add . && git commit -m `"Convert to file-scoped namespaces`"" -ForegroundColor White
|
||||
}
|
||||
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
exit $errorCount
|
||||
@@ -0,0 +1,299 @@
|
||||
# Async Migration Analysis Script
|
||||
# Scans the Jellyfin codebase for synchronous database operations that need conversion
|
||||
|
||||
param(
|
||||
[string]$Path = ".",
|
||||
[switch]$DetailedReport,
|
||||
[switch]$ExportCsv
|
||||
)
|
||||
|
||||
Write-Host "🔍 Scanning for synchronous database operations..." -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Define patterns to search for
|
||||
$patterns = @{
|
||||
"SaveChanges" = @{
|
||||
Pattern = "\.SaveChanges\s*\(\s*\)"
|
||||
Replacement = "await .SaveChangesAsync(cancellationToken)"
|
||||
Severity = "HIGH"
|
||||
Description = "Synchronous database save operation"
|
||||
}
|
||||
"ToList" = @{
|
||||
Pattern = "\.ToList\s*\(\s*\)"
|
||||
Replacement = "await .ToListAsync(cancellationToken)"
|
||||
Severity = "HIGH"
|
||||
Description = "Synchronous list materialization"
|
||||
}
|
||||
"ToArray" = @{
|
||||
Pattern = "\.ToArray\s*\(\s*\)"
|
||||
Replacement = "await .ToArrayAsync(cancellationToken)"
|
||||
Severity = "HIGH"
|
||||
Description = "Synchronous array materialization"
|
||||
}
|
||||
"FirstOrDefault" = @{
|
||||
Pattern = "\.FirstOrDefault\s*\("
|
||||
Replacement = 'await .FirstOrDefaultAsync('
|
||||
Severity = "HIGH"
|
||||
Description = "Synchronous first element query"
|
||||
}
|
||||
"First" = @{
|
||||
Pattern = "\.First\s*\("
|
||||
Replacement = 'await .FirstAsync('
|
||||
Severity = "HIGH"
|
||||
Description = "Synchronous first element query"
|
||||
}
|
||||
"SingleOrDefault" = @{
|
||||
Pattern = "\.SingleOrDefault\s*\("
|
||||
Replacement = 'await .SingleOrDefaultAsync('
|
||||
Severity = "MEDIUM"
|
||||
Description = "Synchronous single element query"
|
||||
}
|
||||
"Single" = @{
|
||||
Pattern = "\.Single\s*\("
|
||||
Replacement = 'await .SingleAsync('
|
||||
Severity = "MEDIUM"
|
||||
Description = "Synchronous single element query"
|
||||
}
|
||||
"Any" = @{
|
||||
Pattern = "\.Any\s*\("
|
||||
Replacement = 'await .AnyAsync('
|
||||
Severity = "MEDIUM"
|
||||
Description = "Synchronous existence check"
|
||||
}
|
||||
"Count" = @{
|
||||
Pattern = "\.Count\s*\(\s*\)"
|
||||
Replacement = "await .CountAsync(cancellationToken)"
|
||||
Severity = "MEDIUM"
|
||||
Description = "Synchronous count operation"
|
||||
}
|
||||
"LongCount" = @{
|
||||
Pattern = "\.LongCount\s*\("
|
||||
Replacement = "await .LongCountAsync("
|
||||
Severity = "MEDIUM"
|
||||
Description = "Synchronous long count operation"
|
||||
}
|
||||
"ExecuteDelete" = @{
|
||||
Pattern = "\.ExecuteDelete\s*\(\s*\)"
|
||||
Replacement = "await .ExecuteDeleteAsync(cancellationToken)"
|
||||
Severity = "HIGH"
|
||||
Description = "Synchronous bulk delete"
|
||||
}
|
||||
"ExecuteUpdate" = @{
|
||||
Pattern = "\.ExecuteUpdate\s*\("
|
||||
Replacement = "await .ExecuteUpdateAsync("
|
||||
Severity = "HIGH"
|
||||
Description = "Synchronous bulk update"
|
||||
}
|
||||
"BeginTransaction" = @{
|
||||
Pattern = "\.BeginTransaction\s*\(\s*\)"
|
||||
Replacement = "await .BeginTransactionAsync(cancellationToken)"
|
||||
Severity = "HIGH"
|
||||
Description = "Synchronous transaction start"
|
||||
}
|
||||
"Commit" = @{
|
||||
Pattern = "transaction\.Commit\s*\(\s*\)"
|
||||
Replacement = "await transaction.CommitAsync(cancellationToken)"
|
||||
Severity = "HIGH"
|
||||
Description = "Synchronous transaction commit"
|
||||
}
|
||||
"Rollback" = @{
|
||||
Pattern = "transaction\.Rollback\s*\(\s*\)"
|
||||
Replacement = "await transaction.RollbackAsync(cancellationToken)"
|
||||
Severity = "MEDIUM"
|
||||
Description = "Synchronous transaction rollback"
|
||||
}
|
||||
"Sum" = @{
|
||||
Pattern = "\.Sum\s*\("
|
||||
Replacement = 'await .SumAsync('
|
||||
Severity = "LOW"
|
||||
Description = "Synchronous sum aggregation"
|
||||
}
|
||||
"Average" = @{
|
||||
Pattern = "\.Average\s*\("
|
||||
Replacement = 'await .AverageAsync('
|
||||
Severity = "LOW"
|
||||
Description = "Synchronous average aggregation"
|
||||
}
|
||||
"Min" = @{
|
||||
Pattern = "\.Min\s*\("
|
||||
Replacement = 'await .MinAsync('
|
||||
Severity = "LOW"
|
||||
Description = "Synchronous min aggregation"
|
||||
}
|
||||
"Max" = @{
|
||||
Pattern = "\.Max\s*\("
|
||||
Replacement = 'await .MaxAsync('
|
||||
Severity = "LOW"
|
||||
Description = "Synchronous max aggregation"
|
||||
}
|
||||
"Load" = @{
|
||||
Pattern = "\.Load\s*\(\s*\)"
|
||||
Replacement = 'await .LoadAsync(cancellationToken)'
|
||||
Severity = "MEDIUM"
|
||||
Description = "Synchronous navigation property load"
|
||||
}
|
||||
}
|
||||
|
||||
# Exclude patterns (already async or intentionally sync)
|
||||
$excludePatterns = @(
|
||||
"Async\s*\(",
|
||||
"// ASYNC",
|
||||
"\.ConfigureAwait",
|
||||
"AsyncEnumerable"
|
||||
)
|
||||
|
||||
# Get all .cs files, excluding test files and migrations initially
|
||||
$files = Get-ChildItem -Path $Path -Recurse -Filter "*.cs" |
|
||||
Where-Object {
|
||||
$_.FullName -notmatch "\\bin\\" -and
|
||||
$_.FullName -notmatch "\\obj\\" -and
|
||||
$_.FullName -notmatch "\\Migrations\\" -and
|
||||
$_.FullName -notmatch "\.Designer\.cs$"
|
||||
}
|
||||
|
||||
$results = @()
|
||||
$summary = @{}
|
||||
|
||||
foreach ($file in $files) {
|
||||
$content = Get-Content -Path $file.FullName -Raw
|
||||
|
||||
foreach ($patternName in $patterns.Keys) {
|
||||
$patternInfo = $patterns[$patternName]
|
||||
$pattern = $patternInfo.Pattern
|
||||
|
||||
# Find matches
|
||||
$matches = [regex]::Matches($content, $pattern)
|
||||
|
||||
foreach ($match in $matches) {
|
||||
# Check if this line is already async or should be excluded
|
||||
$lineStart = $content.Substring(0, $match.Index).LastIndexOf("`n")
|
||||
if ($lineStart -lt 0) { $lineStart = 0 }
|
||||
$lineEnd = $content.IndexOf("`n", $match.Index)
|
||||
if ($lineEnd -lt 0) { $lineEnd = $content.Length }
|
||||
|
||||
$line = $content.Substring($lineStart, $lineEnd - $lineStart).Trim()
|
||||
|
||||
# Skip if already async or excluded
|
||||
$shouldExclude = $false
|
||||
foreach ($excludePattern in $excludePatterns) {
|
||||
if ($line -match $excludePattern) {
|
||||
$shouldExclude = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $shouldExclude) {
|
||||
# Calculate line number
|
||||
$lineNumber = ($content.Substring(0, $match.Index) -split "`n").Count
|
||||
|
||||
$result = [PSCustomObject]@{
|
||||
File = $file.FullName.Replace($Path, "").TrimStart('\')
|
||||
LineNumber = $lineNumber
|
||||
Pattern = $patternName
|
||||
Severity = $patternInfo.Severity
|
||||
Description = $patternInfo.Description
|
||||
MatchedText = $match.Value
|
||||
Line = $line
|
||||
Replacement = $patternInfo.Replacement
|
||||
}
|
||||
|
||||
$results += $result
|
||||
|
||||
# Update summary
|
||||
if (-not $summary.ContainsKey($patternName)) {
|
||||
$summary[$patternName] = 0
|
||||
}
|
||||
$summary[$patternName]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Display results
|
||||
Write-Host "📊 Analysis Complete!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "=" * 80 -ForegroundColor DarkGray
|
||||
Write-Host "SUMMARY" -ForegroundColor Yellow
|
||||
Write-Host "=" * 80 -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
|
||||
$totalIssues = $results.Count
|
||||
$highSeverity = ($results | Where-Object { $_.Severity -eq "HIGH" }).Count
|
||||
$mediumSeverity = ($results | Where-Object { $_.Severity -eq "MEDIUM" }).Count
|
||||
$lowSeverity = ($results | Where-Object { $_.Severity -eq "LOW" }).Count
|
||||
|
||||
Write-Host "Total synchronous operations found: " -NoNewline
|
||||
Write-Host $totalIssues -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host " 🔴 HIGH severity: " -NoNewline
|
||||
Write-Host $highSeverity -ForegroundColor Red
|
||||
Write-Host " 🟡 MEDIUM severity: " -NoNewline
|
||||
Write-Host $mediumSeverity -ForegroundColor Yellow
|
||||
Write-Host " 🟢 LOW severity: " -NoNewline
|
||||
Write-Host $lowSeverity -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Breakdown by operation type:" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
$summary.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object {
|
||||
$severity = $patterns[$_.Key].Severity
|
||||
$color = switch ($severity) {
|
||||
"HIGH" { "Red" }
|
||||
"MEDIUM" { "Yellow" }
|
||||
"LOW" { "Green" }
|
||||
}
|
||||
Write-Host " $($_.Key): " -NoNewline
|
||||
Write-Host $_.Value -ForegroundColor $color -NoNewline
|
||||
Write-Host " ($severity)"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=" * 80 -ForegroundColor DarkGray
|
||||
|
||||
if ($DetailedReport) {
|
||||
Write-Host ""
|
||||
Write-Host "DETAILED FINDINGS" -ForegroundColor Yellow
|
||||
Write-Host "=" * 80 -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
|
||||
# Group by file
|
||||
$resultsByFile = $results | Group-Object -Property File
|
||||
|
||||
foreach ($fileGroup in $resultsByFile | Sort-Object Name) {
|
||||
Write-Host ""
|
||||
Write-Host "📄 $($fileGroup.Name)" -ForegroundColor Cyan
|
||||
Write-Host "-" * 80 -ForegroundColor DarkGray
|
||||
|
||||
foreach ($item in $fileGroup.Group | Sort-Object LineNumber) {
|
||||
$severityColor = switch ($item.Severity) {
|
||||
"HIGH" { "Red" }
|
||||
"MEDIUM" { "Yellow" }
|
||||
"LOW" { "Green" }
|
||||
}
|
||||
|
||||
Write-Host " Line $($item.LineNumber): " -NoNewline
|
||||
Write-Host "$($item.Pattern) " -NoNewline -ForegroundColor $severityColor
|
||||
Write-Host "- $($item.Description)"
|
||||
Write-Host " Current: " -NoNewline
|
||||
Write-Host $item.MatchedText -ForegroundColor Gray
|
||||
Write-Host " Replacement: " -NoNewline
|
||||
Write-Host $item.Replacement -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($ExportCsv) {
|
||||
$csvPath = Join-Path $Path "async_migration_report.csv"
|
||||
$results | Export-Csv -Path $csvPath -NoTypeInformation
|
||||
Write-Host ""
|
||||
Write-Host "📁 Report exported to: " -NoNewline
|
||||
Write-Host $csvPath -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "💡 TIP: Run with -DetailedReport for line-by-line analysis" -ForegroundColor DarkGray
|
||||
Write-Host "💡 TIP: Run with -ExportCsv to export findings to CSV" -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
|
||||
# Return results for further processing
|
||||
return $results
|
||||
@@ -0,0 +1,79 @@
|
||||
# Apply ItemValues Optimized Indexes
|
||||
# These indexes target the critical ItemValues table performance issue
|
||||
# Note: Creates SQL file first, then executes it to avoid CONCURRENTLY transaction issues
|
||||
|
||||
. .\db-config.ps1
|
||||
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "Creating Optimized ItemValues Indexes" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Target: library.ItemValues table" -ForegroundColor Yellow
|
||||
Write-Host "Problem: 1.3 BILLION rows being scanned sequentially" -ForegroundColor Red
|
||||
Write-Host "Solution: 3 targeted indexes" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "This may take 10-30 minutes..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# Create temporary SQL file (CONCURRENTLY requires running outside transaction)
|
||||
$tempSqlFile = "temp_itemvalues_indexes.sql"
|
||||
|
||||
$sqlContent = @"
|
||||
-- Optimized indexes for ItemValues table
|
||||
-- Fixes the 1.3 billion row sequential scan issue
|
||||
|
||||
-- Index 1: CleanValue lookups (for genre/tag searches)
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_itemvalues_cleanvalue
|
||||
ON library."ItemValues" ("CleanValue")
|
||||
WHERE "CleanValue" IS NOT NULL;
|
||||
|
||||
-- Index 2: Value lookups (for direct value searches)
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_itemvalues_value
|
||||
ON library."ItemValues" ("Value")
|
||||
WHERE "Value" IS NOT NULL;
|
||||
|
||||
-- Index 3: ItemValueId + CleanValue for joins (ItemValuesMap to ItemValues)
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_itemvalues_id_cleanvalue
|
||||
ON library."ItemValues" ("ItemValueId", "CleanValue");
|
||||
"@
|
||||
|
||||
# Write SQL to temp file WITHOUT BOM (UTF8 without BOM)
|
||||
[System.IO.File]::WriteAllText((Join-Path $PWD $tempSqlFile), $sqlContent, (New-Object System.Text.UTF8Encoding $false))
|
||||
|
||||
Write-Host "Executing index creation script..." -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Execute the SQL file (allows CONCURRENTLY to work)
|
||||
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f $tempSqlFile
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host ""
|
||||
Write-Host "[OK] All 3 indexes created successfully!" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host ""
|
||||
Write-Host "[FAILED] Some indexes may have failed to create" -ForegroundColor Red
|
||||
Write-Host "Check the output above for details" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Clean up temp file
|
||||
Remove-Item $tempSqlFile -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "Index Creation Complete!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Verify indexes were created
|
||||
Write-Host "Verifying indexes..." -ForegroundColor Yellow
|
||||
$queryVerify = "SELECT indexrelname as indexname, pg_size_pretty(pg_relation_size(indexrelid)) as size FROM pg_stat_user_indexes WHERE schemaname = 'library' AND relname = 'ItemValues' AND indexrelname LIKE 'idx_itemvalues_%' ORDER BY indexrelname;"
|
||||
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c $queryVerify
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:" -ForegroundColor Cyan
|
||||
Write-Host "1. Use Jellyfin normally - browse, filter by genre and tags" -ForegroundColor Gray
|
||||
Write-Host "2. Wait 1 week" -ForegroundColor Gray
|
||||
Write-Host "3. Run diagnostics again using db-quick.ps1" -ForegroundColor Gray
|
||||
Write-Host "4. Check improvement in ItemValues sequential scans" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "Expected improvement: 70-90 percent faster genre and tag queries!" -ForegroundColor Green
|
||||
@@ -0,0 +1,205 @@
|
||||
# Jellyfin Windows Path Fixer
|
||||
# This script checks and fixes Linux paths in Jellyfin configuration files
|
||||
|
||||
param(
|
||||
[string]$ConfigPath = "C:\ProgramData\jellyfin\config",
|
||||
[string]$DataPath = "C:\ProgramData\jellyfin\data",
|
||||
[switch]$DryRun = $false,
|
||||
[switch]$Force = $false
|
||||
)
|
||||
|
||||
Write-Host "=================================================" -ForegroundColor Cyan
|
||||
Write-Host "Jellyfin Windows Path Fixer" -ForegroundColor Cyan
|
||||
Write-Host "=================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Check if config path exists
|
||||
if (-not (Test-Path $ConfigPath)) {
|
||||
Write-Host "ERROR: Config path not found: $ConfigPath" -ForegroundColor Red
|
||||
Write-Host "Please specify the correct path using -ConfigPath parameter" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Config Path: $ConfigPath" -ForegroundColor Green
|
||||
Write-Host "Data Path: $DataPath" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
$issuesFound = @()
|
||||
|
||||
# Function to check XML file for Linux paths
|
||||
function Test-LinuxPathsInXml {
|
||||
param(
|
||||
[string]$FilePath,
|
||||
[string]$FileName
|
||||
)
|
||||
|
||||
if (-not (Test-Path $FilePath)) {
|
||||
Write-Host " [SKIP] $FileName - File not found" -ForegroundColor Gray
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
[xml]$xml = Get-Content $FilePath
|
||||
$linuxPaths = @()
|
||||
|
||||
# Check all text nodes for Linux paths
|
||||
$xml.SelectNodes("//*") | ForEach-Object {
|
||||
if ($_.InnerText -match '^(/var/|/usr/|/etc/)') {
|
||||
$linuxPaths += @{
|
||||
Element = $_.Name
|
||||
CurrentValue = $_.InnerText
|
||||
Line = $_.OuterXml
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($linuxPaths.Count -gt 0) {
|
||||
Write-Host " [ISSUE] $FileName - Found $($linuxPaths.Count) Linux path(s)" -ForegroundColor Yellow
|
||||
return @{
|
||||
FilePath = $FilePath
|
||||
FileName = $FileName
|
||||
LinuxPaths = $linuxPaths
|
||||
}
|
||||
} else {
|
||||
Write-Host " [OK] $FileName - No Linux paths found" -ForegroundColor Green
|
||||
return $null
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host " [ERROR] $FileName - Failed to parse: $_" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# Check configuration files
|
||||
Write-Host "Checking configuration files..." -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
$systemXmlPath = Join-Path $ConfigPath "system.xml"
|
||||
$result = Test-LinuxPathsInXml -FilePath $systemXmlPath -FileName "system.xml"
|
||||
if ($result) {
|
||||
$issuesFound += $result
|
||||
foreach ($path in $result.LinuxPaths) {
|
||||
Write-Host " - $($path.Element): $($path.CurrentValue)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
$encodingXmlPath = Join-Path $ConfigPath "encoding.xml"
|
||||
$result = Test-LinuxPathsInXml -FilePath $encodingXmlPath -FileName "encoding.xml"
|
||||
if ($result) {
|
||||
$issuesFound += $result
|
||||
foreach ($path in $result.LinuxPaths) {
|
||||
Write-Host " - $($path.Element): $($path.CurrentValue)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
$databaseXmlPath = Join-Path $ConfigPath "database.xml"
|
||||
$result = Test-LinuxPathsInXml -FilePath $databaseXmlPath -FileName "database.xml"
|
||||
if ($result) {
|
||||
$issuesFound += $result
|
||||
foreach ($path in $result.LinuxPaths) {
|
||||
Write-Host " - $($path.Element): $($path.CurrentValue)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=================================================" -ForegroundColor Cyan
|
||||
|
||||
# Summary
|
||||
if ($issuesFound.Count -eq 0) {
|
||||
Write-Host "No issues found! All paths are correct." -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Found issues in $($issuesFound.Count) file(s)" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# If DryRun, just show what would be done
|
||||
if ($DryRun) {
|
||||
Write-Host "DRY RUN MODE - No changes will be made" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "The following changes would be made:" -ForegroundColor Yellow
|
||||
|
||||
foreach ($issue in $issuesFound) {
|
||||
Write-Host ""
|
||||
Write-Host "File: $($issue.FileName)" -ForegroundColor Cyan
|
||||
foreach ($path in $issue.LinuxPaths) {
|
||||
Write-Host " $($path.Element):" -ForegroundColor White
|
||||
Write-Host " FROM: $($path.CurrentValue)" -ForegroundColor Red
|
||||
|
||||
if ($path.Element -in @("MetadataPath", "TranscodingTempPath")) {
|
||||
Write-Host " TO: (empty - use default)" -ForegroundColor Green
|
||||
} else {
|
||||
$newPath = $path.CurrentValue -replace '^/var/lib/jellyfin', $DataPath -replace '/', '\'
|
||||
Write-Host " TO: $newPath" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "To apply these changes, run the script without -DryRun" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Ask for confirmation
|
||||
if (-not $Force) {
|
||||
Write-Host ""
|
||||
Write-Host "WARNING: This will modify your configuration files!" -ForegroundColor Yellow
|
||||
Write-Host "Backups will be created automatically." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
$confirm = Read-Host "Do you want to proceed? (yes/no)"
|
||||
if ($confirm -ne "yes") {
|
||||
Write-Host "Operation cancelled." -ForegroundColor Red
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
|
||||
# Apply fixes
|
||||
Write-Host ""
|
||||
Write-Host "Applying fixes..." -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
foreach ($issue in $issuesFound) {
|
||||
Write-Host "Fixing $($issue.FileName)..." -ForegroundColor Yellow
|
||||
|
||||
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
|
||||
$backupPath = "$($issue.FilePath).backup.$timestamp"
|
||||
Copy-Item $issue.FilePath $backupPath
|
||||
Write-Host " Backup created: $backupPath" -ForegroundColor Gray
|
||||
|
||||
[xml]$xml = Get-Content $issue.FilePath
|
||||
|
||||
foreach ($path in $issue.LinuxPaths) {
|
||||
$element = $null
|
||||
$xml.SelectNodes("//*") | ForEach-Object {
|
||||
if ($_.InnerText -eq $path.CurrentValue) {
|
||||
$element = $_
|
||||
}
|
||||
}
|
||||
|
||||
if ($element) {
|
||||
if ($path.Element -in @("MetadataPath", "TranscodingTempPath")) {
|
||||
$element.InnerText = ""
|
||||
Write-Host " Cleared $($path.Element)" -ForegroundColor Green
|
||||
} else {
|
||||
$newPath = $path.CurrentValue -replace '^/var/lib/jellyfin', $DataPath -replace '/', '\'
|
||||
$element.InnerText = $newPath
|
||||
Write-Host " Updated $($path.Element) to: $newPath" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$xml.Save($issue.FilePath)
|
||||
Write-Host " Saved $($issue.FileName)" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Write-Host "=================================================" -ForegroundColor Cyan
|
||||
Write-Host "All fixes applied successfully!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "NEXT STEPS:" -ForegroundColor Yellow
|
||||
Write-Host "1. Restart Jellyfin service/application" -ForegroundColor White
|
||||
Write-Host "2. Check logs to verify paths are now correct" -ForegroundColor White
|
||||
$configInfo = "3. If issues persist, review backups in: $ConfigPath"
|
||||
Write-Host $configInfo -ForegroundColor White
|
||||
Write-Host ""
|
||||
@@ -0,0 +1,83 @@
|
||||
# Fix startup.json WebDir paths
|
||||
# This script finds and fixes incorrect WebDir paths in startup.json files
|
||||
|
||||
Write-Host "=================================================" -ForegroundColor Cyan
|
||||
Write-Host "Jellyfin startup.json WebDir Fixer" -ForegroundColor Cyan
|
||||
Write-Host "=================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
$searchPaths = @(
|
||||
".",
|
||||
".\bin\Debug\net11.0",
|
||||
".\bin\Release\net11.0",
|
||||
".\bin\Debug\net11.0\config",
|
||||
".\bin\Release\net11.0\config"
|
||||
)
|
||||
|
||||
$filesFound = 0
|
||||
$filesFixed = 0
|
||||
|
||||
foreach ($path in $searchPaths) {
|
||||
$jsonPath = Join-Path $path "startup.json"
|
||||
|
||||
if (Test-Path $jsonPath) {
|
||||
$filesFound++
|
||||
Write-Host "Found: $jsonPath" -ForegroundColor Yellow
|
||||
|
||||
try {
|
||||
$content = Get-Content $jsonPath -Raw
|
||||
$originalContent = $content
|
||||
|
||||
# Check for incorrect WebDir
|
||||
if ($content -match '"WebDir":\s*"[^"]*C:/ProgramData/jellyfin/wwwroot[^"]*"') {
|
||||
Write-Host " Status: Has incorrect WebDir path" -ForegroundColor Red
|
||||
|
||||
# Create backup
|
||||
$backupPath = "$jsonPath.backup.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
|
||||
Copy-Item $jsonPath $backupPath
|
||||
Write-Host " Backup: $backupPath" -ForegroundColor Gray
|
||||
|
||||
# Fix the path
|
||||
$content = $content -replace '"WebDir":\s*"C:/ProgramData/jellyfin/wwwroot"', '"WebDir": "wwwroot"'
|
||||
|
||||
# Also fix any data path if it has /data appended
|
||||
$content = $content -replace '"WebDir":\s*"C:/ProgramData/jellyfin/data/wwwroot"', '"WebDir": "wwwroot"'
|
||||
|
||||
Set-Content -Path $jsonPath -Value $content -NoNewline
|
||||
$filesFixed++
|
||||
|
||||
Write-Host " Result: FIXED!" -ForegroundColor Green
|
||||
}
|
||||
elseif ($content -match '"WebDir":\s*"wwwroot"') {
|
||||
Write-Host " Status: Already correct (relative path)" -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host " Status: Has custom path (not changed)" -ForegroundColor Cyan
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host " ERROR: Failed to process - $_" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "=================================================" -ForegroundColor Cyan
|
||||
|
||||
if ($filesFound -eq 0) {
|
||||
Write-Host "No startup.json files found." -ForegroundColor Yellow
|
||||
Write-Host "Run Jellyfin once to create the configuration file." -ForegroundColor White
|
||||
}
|
||||
elseif ($filesFixed -eq 0) {
|
||||
Write-Host "All files are already correct!" -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host "Fixed $filesFixed of $filesFound file(s)" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:" -ForegroundColor Yellow
|
||||
Write-Host "1. Rebuild your solution (if needed)" -ForegroundColor White
|
||||
Write-Host "2. Run Jellyfin to verify WebDir path is correct" -ForegroundColor White
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env pwsh
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Fixes StyleCop warnings in C# files.
|
||||
|
||||
.DESCRIPTION
|
||||
Automatically fixes common StyleCop warnings:
|
||||
- SA1413: Adds trailing commas to multi-line initializers
|
||||
- SA1515: Adds blank lines before single-line comments
|
||||
- SA1518: Ensures files end with single newline
|
||||
|
||||
.PARAMETER Path
|
||||
The root path to search for C# files. Defaults to "MediaBrowser.Model"
|
||||
|
||||
.PARAMETER DryRun
|
||||
If specified, shows what would be changed without actually modifying files
|
||||
|
||||
.EXAMPLE
|
||||
.\FixStyleCopWarnings.ps1 -DryRun
|
||||
#>
|
||||
|
||||
param(
|
||||
[string]$Path = "MediaBrowser.Model",
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Fix-TrailingCommas {
|
||||
param([string]$Content)
|
||||
|
||||
$modified = $false
|
||||
$lines = $content -split '\r?\n'
|
||||
|
||||
for ($i = 0; $i -lt $lines.Length - 1; $i++) {
|
||||
$currentLine = $lines[$i]
|
||||
$nextLine = $lines[$i + 1]
|
||||
|
||||
# Check if current line ends with a property/value without comma
|
||||
# and next line closes the initializer
|
||||
if ($currentLine -match '^\s+\w+\s*=\s*.+[^,]\s*$' -and $nextLine -match '^\s*[}\)]') {
|
||||
$lines[$i] = $currentLine.TrimEnd() + ','
|
||||
$modified = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($modified) {
|
||||
return ($lines -join "`n")
|
||||
}
|
||||
|
||||
return $Content
|
||||
}
|
||||
|
||||
function Fix-BlankLinesBeforeComments {
|
||||
param([string]$Content)
|
||||
|
||||
$modified = $false
|
||||
$lines = $content -split '\r?\n'
|
||||
$newLines = @()
|
||||
|
||||
for ($i = 0; $i -lt $lines.Length; $i++) {
|
||||
$currentLine = $lines[$i]
|
||||
|
||||
# Check if this is a single-line comment
|
||||
if ($currentLine -match '^\s+//\s+\w') {
|
||||
# Check if previous line exists and is not blank
|
||||
if ($i -gt 0) {
|
||||
$prevLine = $lines[$i - 1]
|
||||
$prevLineIsBlank = [string]::IsNullOrWhiteSpace($prevLine)
|
||||
$prevLineIsOpenBrace = $prevLine -match '\{\s*$'
|
||||
$prevLineIsComment = $prevLine -match '^\s*//'
|
||||
|
||||
# Add blank line if previous line is not blank, not open brace, not another comment
|
||||
if (-not $prevLineIsBlank -and -not $prevLineIsOpenBrace -and -not $prevLineIsComment) {
|
||||
$newLines += ""
|
||||
$modified = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$newLines += $currentLine
|
||||
}
|
||||
|
||||
if ($modified) {
|
||||
return ($newLines -join "`n")
|
||||
}
|
||||
|
||||
return $Content
|
||||
}
|
||||
|
||||
function Fix-FileEnding {
|
||||
param([string]$Content)
|
||||
|
||||
# Ensure file ends with exactly one newline
|
||||
$trimmed = $Content.TrimEnd("`r", "`n", " ", "`t")
|
||||
return $trimmed + "`n"
|
||||
}
|
||||
|
||||
function Fix-StyleCopWarnings {
|
||||
param(
|
||||
[string]$FilePath,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$content = Get-Content -Path $FilePath -Raw
|
||||
$originalContent = $content
|
||||
|
||||
# Apply fixes
|
||||
$content = Fix-TrailingCommas -Content $content
|
||||
$content = Fix-BlankLinesBeforeComments -Content $content
|
||||
$content = Fix-FileEnding -Content $content
|
||||
|
||||
# Restore Windows line endings if original had them
|
||||
if ($originalContent -match '\r\n') {
|
||||
$content = $content -replace '(?<!\r)\n', "`r`n"
|
||||
}
|
||||
|
||||
if ($content -ne $originalContent) {
|
||||
if ($DryRun) {
|
||||
Write-Host " [DRY-RUN] Would fix StyleCop warnings" -ForegroundColor Cyan
|
||||
return $true
|
||||
}
|
||||
|
||||
[System.IO.File]::WriteAllText($FilePath, $content, [System.Text.UTF8Encoding]::new($false))
|
||||
Write-Host " [FIXED]" -ForegroundColor Green
|
||||
return $true
|
||||
}
|
||||
|
||||
Write-Host " [SKIP] No warnings to fix" -ForegroundColor Gray
|
||||
return $false
|
||||
}
|
||||
|
||||
# Main script
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host "StyleCop Warning Fixer" -ForegroundColor Cyan
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "DRY RUN MODE - No files will be modified" -ForegroundColor Yellow
|
||||
Write-Host
|
||||
}
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
Write-Error "Path not found: $Path"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$files = Get-ChildItem -Path $Path -Filter "*.cs" -Recurse | Where-Object {
|
||||
$_.FullName -notmatch "\\obj\\" -and
|
||||
$_.FullName -notmatch "\\bin\\"
|
||||
}
|
||||
|
||||
Write-Host "Found $($files.Count) C# files in $Path" -ForegroundColor Cyan
|
||||
Write-Host
|
||||
|
||||
$fixedCount = 0
|
||||
$skippedCount = 0
|
||||
$errorCount = 0
|
||||
|
||||
foreach ($file in $files) {
|
||||
$relativePath = $file.FullName.Replace((Get-Location).Path, "").TrimStart([char]92, [char]47)
|
||||
Write-Host "[Processing] $relativePath" -ForegroundColor White
|
||||
|
||||
try {
|
||||
$fixed = Fix-StyleCopWarnings -FilePath $file.FullName -DryRun:$DryRun
|
||||
if ($fixed) {
|
||||
$fixedCount++
|
||||
} else {
|
||||
$skippedCount++
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host " [ERROR] $_" -ForegroundColor Red
|
||||
$errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host "Summary:" -ForegroundColor Cyan
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " Total files: $($files.Count)" -ForegroundColor White
|
||||
Write-Host " Fixed: $fixedCount" -ForegroundColor Green
|
||||
Write-Host " Skipped: $skippedCount" -ForegroundColor Gray
|
||||
Write-Host " Errors: $errorCount" -ForegroundColor Red
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host
|
||||
Write-Host "This was a DRY RUN. Run without -DryRun to apply changes." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
exit $errorCount
|
||||
@@ -0,0 +1,237 @@
|
||||
#Requires -Version 5.1
|
||||
#Requires -RunAsAdministrator
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sets up automated weekly performance monitoring via Windows Task Scheduler
|
||||
|
||||
.DESCRIPTION
|
||||
Creates a scheduled task to run Weekly-Performance-Monitor.ps1 every Monday at 6 AM.
|
||||
Can optionally enable conditional VACUUM ANALYZE based on dead tuple thresholds.
|
||||
|
||||
.PARAMETER Uninstall
|
||||
Remove the scheduled task
|
||||
|
||||
.PARAMETER TaskTime
|
||||
Time to run the task (default: 6am)
|
||||
|
||||
.PARAMETER DayOfWeek
|
||||
Day of week to run (default: Monday)
|
||||
|
||||
.PARAMETER EnableAutoVacuumAnalyze
|
||||
Enable conditional VACUUM ANALYZE for tables above dead tuple thresholds
|
||||
|
||||
.PARAMETER DeadTupleThreshold
|
||||
Minimum dead tuples before VACUUM ANALYZE runs (default: 10000)
|
||||
|
||||
.PARAMETER DeadTuplePercentThreshold
|
||||
Minimum dead tuple percentage before VACUUM ANALYZE runs (default: 20)
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-Weekly-Monitor.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-Weekly-Monitor.ps1 -TaskTime "3am" -DayOfWeek Sunday
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-Weekly-Monitor.ps1 -EnableAutoVacuumAnalyze -DeadTupleThreshold 10000 -DeadTuplePercentThreshold 20
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-Weekly-Monitor.ps1 -Uninstall
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter()]
|
||||
[switch]$Uninstall,
|
||||
|
||||
[Parameter()]
|
||||
[string]$TaskTime = "6am",
|
||||
|
||||
[Parameter()]
|
||||
[ValidateSet('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday')]
|
||||
[string]$DayOfWeek = 'Monday',
|
||||
|
||||
[Parameter()]
|
||||
[switch]$EnableAutoVacuumAnalyze,
|
||||
|
||||
[Parameter()]
|
||||
[ValidateRange(1, [int]::MaxValue)]
|
||||
[int]$DeadTupleThreshold = 10000,
|
||||
|
||||
[Parameter()]
|
||||
[ValidateRange(0.01, 100.0)]
|
||||
[double]$DeadTuplePercentThreshold = 20.0
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$TaskName = "Jellyfin Database Weekly Monitor"
|
||||
$ScriptRoot = Split-Path -Parent $PSCommandPath
|
||||
$MonitorScript = Join-Path $ScriptRoot "Weekly-Performance-Monitor.ps1"
|
||||
|
||||
# ============================================================================
|
||||
# Functions
|
||||
# ============================================================================
|
||||
|
||||
function Write-ColorOutput {
|
||||
param(
|
||||
[string]$Message,
|
||||
[ConsoleColor]$Color = 'White'
|
||||
)
|
||||
Write-Host $Message -ForegroundColor $Color
|
||||
}
|
||||
|
||||
function Test-IsAdmin {
|
||||
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
|
||||
return $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
Write-ColorOutput "`n========================================" -Color Cyan
|
||||
Write-ColorOutput "Jellyfin DB Monitor Setup" -Color Cyan
|
||||
Write-ColorOutput "========================================`n" -Color Cyan
|
||||
|
||||
# Check if running as administrator
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-ColorOutput "❌ This script must be run as Administrator" -Color Red
|
||||
Write-ColorOutput " Right-click PowerShell and select 'Run as Administrator'" -Color Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Uninstall if requested
|
||||
if ($Uninstall) {
|
||||
Write-ColorOutput "Removing scheduled task..." -Color Yellow
|
||||
|
||||
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
|
||||
if ($task) {
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||
Write-ColorOutput "✅ Scheduled task removed successfully" -Color Green
|
||||
} else {
|
||||
Write-ColorOutput "⚠️ Scheduled task not found" -Color Yellow
|
||||
}
|
||||
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check if monitor script exists
|
||||
if (-not (Test-Path $MonitorScript)) {
|
||||
Write-ColorOutput "❌ Monitor script not found: $MonitorScript" -Color Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-ColorOutput "Configuration:" -Color Cyan
|
||||
Write-ColorOutput " Task Name: $TaskName" -Color White
|
||||
Write-ColorOutput " Script: $MonitorScript" -Color White
|
||||
Write-ColorOutput " Schedule: Every $DayOfWeek at $TaskTime" -Color White
|
||||
if ($EnableAutoVacuumAnalyze) {
|
||||
Write-ColorOutput " Auto VACUUM ANALYZE: Enabled (dead tuples >= $DeadTupleThreshold, dead percent >= $DeadTuplePercentThreshold)" -Color White
|
||||
} else {
|
||||
Write-ColorOutput " Auto VACUUM ANALYZE: Disabled" -Color White
|
||||
}
|
||||
Write-ColorOutput ""
|
||||
|
||||
# Remove existing task if present
|
||||
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
if ($existingTask) {
|
||||
Write-ColorOutput "⚠️ Existing task found. Removing..." -Color Yellow
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||
}
|
||||
|
||||
# Create scheduled task action
|
||||
Write-ColorOutput "Creating scheduled task..." -Color Cyan
|
||||
|
||||
$scriptArguments = "-NoProfile -ExecutionPolicy Bypass -File `"$MonitorScript`""
|
||||
if ($EnableAutoVacuumAnalyze) {
|
||||
$scriptArguments += " -AutoVacuumAnalyze -DeadTupleThreshold $DeadTupleThreshold -DeadTuplePercentThreshold $DeadTuplePercentThreshold"
|
||||
}
|
||||
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute "PowerShell.exe" `
|
||||
-Argument $scriptArguments
|
||||
|
||||
# Create trigger
|
||||
$trigger = New-ScheduledTaskTrigger `
|
||||
-Weekly `
|
||||
-DaysOfWeek $DayOfWeek `
|
||||
-At $TaskTime
|
||||
|
||||
# Task settings
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-RunOnlyIfNetworkAvailable `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
||||
|
||||
# Task principal (run as current user)
|
||||
$principal = New-ScheduledTaskPrincipal `
|
||||
-UserId $env:USERNAME `
|
||||
-LogonType S4U `
|
||||
-RunLevel Highest
|
||||
|
||||
# Register the task
|
||||
try {
|
||||
Register-ScheduledTask `
|
||||
-TaskName $TaskName `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Principal $principal `
|
||||
-Description "Automated weekly performance monitoring for Jellyfin PostgreSQL database" | Out-Null
|
||||
|
||||
Write-ColorOutput "✅ Scheduled task created successfully!" -Color Green
|
||||
Write-ColorOutput ""
|
||||
|
||||
# Verify task
|
||||
$task = Get-ScheduledTask -TaskName $TaskName
|
||||
$nextRun = $task.Triggers[0].StartBoundary
|
||||
|
||||
Write-ColorOutput "Task Details:" -Color Cyan
|
||||
Write-ColorOutput " Status: $($task.State)" -Color White
|
||||
Write-ColorOutput " Next Run: $nextRun" -Color White
|
||||
Write-ColorOutput ""
|
||||
|
||||
# Test run option
|
||||
Write-ColorOutput "Would you like to test the task now? (y/n): " -Color Yellow -NoNewline
|
||||
$response = Read-Host
|
||||
|
||||
if ($response -eq 'y' -or $response -eq 'Y') {
|
||||
Write-ColorOutput "`nRunning test..." -Color Cyan
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
$taskInfo = Get-ScheduledTask -TaskName $TaskName | Get-ScheduledTaskInfo
|
||||
Write-ColorOutput "Last Run Time: $($taskInfo.LastRunTime)" -Color White
|
||||
Write-ColorOutput "Last Result: $($taskInfo.LastTaskResult)" -Color White
|
||||
|
||||
if ($taskInfo.LastTaskResult -eq 0) {
|
||||
Write-ColorOutput "✅ Test run completed successfully!" -Color Green
|
||||
} else {
|
||||
Write-ColorOutput "⚠️ Test run completed with code: $($taskInfo.LastTaskResult)" -Color Yellow
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-ColorOutput "❌ Failed to create scheduled task: $_" -Color Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-ColorOutput "`n========================================" -Color Green
|
||||
Write-ColorOutput "Setup Complete!" -Color Green
|
||||
Write-ColorOutput "========================================" -Color Green
|
||||
Write-ColorOutput ""
|
||||
Write-ColorOutput "Monitoring is now automated!" -Color Cyan
|
||||
Write-ColorOutput ""
|
||||
Write-ColorOutput "To manage the task:" -Color White
|
||||
Write-ColorOutput " View: Get-ScheduledTask -TaskName '$TaskName'" -Color Gray
|
||||
Write-ColorOutput " Run now: Start-ScheduledTask -TaskName '$TaskName'" -Color Gray
|
||||
Write-ColorOutput " Disable: Disable-ScheduledTask -TaskName '$TaskName'" -Color Gray
|
||||
Write-ColorOutput " Remove: .\Setup-Weekly-Monitor.ps1 -Uninstall" -Color Gray
|
||||
Write-ColorOutput ""
|
||||
Write-ColorOutput "Reports will be saved to: reports\weekly" -Color Yellow
|
||||
Write-ColorOutput "========================================`n" -Color Green
|
||||
@@ -0,0 +1,483 @@
|
||||
#Requires -Version 5.1
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Automated weekly performance monitoring for Jellyfin PostgreSQL database
|
||||
|
||||
.DESCRIPTION
|
||||
This script:
|
||||
1. Runs comprehensive diagnostics
|
||||
2. Analyzes slow queries
|
||||
3. Tracks index usage over time
|
||||
4. Generates weekly performance reports
|
||||
5. Optionally runs VACUUM ANALYZE on tables with high dead tuples
|
||||
6. Can be scheduled via Task Scheduler
|
||||
|
||||
.PARAMETER OutputDirectory
|
||||
Directory to save reports (default: reports/weekly)
|
||||
|
||||
.PARAMETER EmailReport
|
||||
If specified, emails the report (requires email configuration)
|
||||
|
||||
.PARAMETER CompareWithPrevious
|
||||
Compare current stats with previous week
|
||||
|
||||
.EXAMPLE
|
||||
.\Weekly-Performance-Monitor.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\Weekly-Performance-Monitor.ps1 -OutputDirectory "C:\Reports\Jellyfin" -CompareWithPrevious
|
||||
|
||||
.EXAMPLE
|
||||
.\Weekly-Performance-Monitor.ps1 -AutoVacuumAnalyze -DeadTupleThreshold 10000 -DeadTuplePercentThreshold 20
|
||||
|
||||
.EXAMPLE
|
||||
# Schedule weekly via Task Scheduler
|
||||
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-File C:\Path\To\Weekly-Performance-Monitor.ps1"
|
||||
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 6am
|
||||
Register-ScheduledTask -TaskName "Jellyfin DB Monitor" -Action $action -Trigger $trigger
|
||||
|
||||
.NOTES
|
||||
Author: Generated for Jellyfin PostgreSQL Project
|
||||
Date: 2026-03-05
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter()]
|
||||
[string]$OutputDirectory = "reports\weekly",
|
||||
|
||||
[Parameter()]
|
||||
[switch]$EmailReport,
|
||||
|
||||
[Parameter()]
|
||||
[switch]$CompareWithPrevious,
|
||||
|
||||
[Parameter()]
|
||||
[switch]$AutoVacuumAnalyze,
|
||||
|
||||
[Parameter()]
|
||||
[ValidateRange(1, [int]::MaxValue)]
|
||||
[int]$DeadTupleThreshold = 100,
|
||||
|
||||
[Parameter()]
|
||||
[ValidateRange(0.01, 100.0)]
|
||||
[double]$DeadTuplePercentThreshold = 0.02
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptRoot = Split-Path -Parent $PSCommandPath
|
||||
$ProjectRoot = Split-Path -Parent $ScriptRoot
|
||||
|
||||
# Load database configuration
|
||||
. "$ProjectRoot\scripts\db-config.ps1"
|
||||
|
||||
# ============================================================================
|
||||
# Functions
|
||||
# ============================================================================
|
||||
|
||||
function Write-ColorOutput {
|
||||
param(
|
||||
[string]$Message,
|
||||
[ConsoleColor]$Color = 'White'
|
||||
)
|
||||
Write-Host $Message -ForegroundColor $Color
|
||||
}
|
||||
|
||||
function Get-ReportTimestamp {
|
||||
return Get-Date -Format "yyyy-MM-dd_HHmmss"
|
||||
}
|
||||
|
||||
function Get-WeekNumber {
|
||||
$date = Get-Date
|
||||
$culture = [System.Globalization.CultureInfo]::CurrentCulture
|
||||
return $culture.Calendar.GetWeekOfYear($date,
|
||||
[System.Globalization.CalendarWeekRule]::FirstDay,
|
||||
[DayOfWeek]::Monday)
|
||||
}
|
||||
|
||||
function Test-DatabaseConnection {
|
||||
Write-ColorOutput "`nTesting database connection..." -Color Cyan
|
||||
|
||||
try {
|
||||
$result = & $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "SELECT version();" 2>&1
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-ColorOutput "[OK] Database connection successful" -Color Green
|
||||
return $true
|
||||
} else {
|
||||
Write-ColorOutput "[ERROR] Database connection failed: $result" -Color Red
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-ColorOutput "[ERROR] Database connection error: $_" -Color Red
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-DiagnosticReport {
|
||||
param([string]$OutputFile)
|
||||
|
||||
Write-ColorOutput "`nRunning comprehensive diagnostics..." -Color Cyan
|
||||
|
||||
try {
|
||||
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$ProjectRoot\sql\diagnostics.sql" > $OutputFile 2>&1
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-ColorOutput "[OK] Diagnostics completed: $OutputFile" -Color Green
|
||||
return $true
|
||||
} else {
|
||||
Write-ColorOutput "[WARN] Diagnostics completed with warnings" -Color Yellow
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
Write-ColorOutput "[ERROR] Diagnostics failed: $_" -Color Red
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-QueryAnalysis {
|
||||
param([string]$OutputFile)
|
||||
|
||||
Write-ColorOutput "`nAnalyzing slow queries..." -Color Cyan
|
||||
|
||||
try {
|
||||
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$ProjectRoot\sql\query-analysis.sql" > $OutputFile 2>&1
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-ColorOutput "[OK] Query analysis completed: $OutputFile" -Color Green
|
||||
return $true
|
||||
} else {
|
||||
Write-ColorOutput "[WARN] Query analysis completed with warnings" -Color Yellow
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
Write-ColorOutput "[ERROR] Query analysis failed: $_" -Color Red
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-IndexUsageStats {
|
||||
param([string]$OutputFile)
|
||||
|
||||
Write-ColorOutput "`nGathering index usage statistics..." -Color Cyan
|
||||
|
||||
$query = @"
|
||||
SELECT
|
||||
schemaname,
|
||||
relname AS tablename,
|
||||
indexrelname,
|
||||
idx_scan,
|
||||
idx_tup_read,
|
||||
idx_tup_fetch,
|
||||
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
|
||||
ORDER BY schemaname, relname, idx_scan DESC;
|
||||
"@
|
||||
|
||||
try {
|
||||
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c $query > $OutputFile 2>&1
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-ColorOutput "[OK] Index stats gathered: $OutputFile" -Color Green
|
||||
return $true
|
||||
} else {
|
||||
Write-ColorOutput "[WARN] Index stats completed with warnings" -Color Yellow
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
Write-ColorOutput "[ERROR] Index stats failed: $_" -Color Red
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-TableSizeStats {
|
||||
param([string]$OutputFile)
|
||||
|
||||
Write-ColorOutput "`nGathering table size statistics..." -Color Cyan
|
||||
|
||||
$query = @"
|
||||
SELECT
|
||||
schemaname,
|
||||
relname,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) AS total_size,
|
||||
pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) AS table_size,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname) - pg_relation_size(schemaname||'.'||relname)) AS index_size,
|
||||
n_live_tup AS row_count,
|
||||
n_dead_tup AS dead_rows,
|
||||
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_pct
|
||||
FROM pg_stat_user_tables
|
||||
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||relname) DESC;
|
||||
"@
|
||||
|
||||
try {
|
||||
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c $query > $OutputFile 2>&1
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-ColorOutput "[OK] Table size stats gathered: $OutputFile" -Color Green
|
||||
return $true
|
||||
} else {
|
||||
Write-ColorOutput "[WARN] Table size stats completed with warnings" -Color Yellow
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
Write-ColorOutput "[ERROR] Table size stats failed: $_" -Color Red
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-ConditionalVacuumAnalyze {
|
||||
param(
|
||||
[int]$MinDeadTuples,
|
||||
[double]$MinDeadPercent
|
||||
)
|
||||
|
||||
Write-ColorOutput "`nChecking for tables that need VACUUM ANALYZE..." -Color Cyan
|
||||
Write-ColorOutput "Thresholds: dead tuples >= $MinDeadTuples and dead percent >= $MinDeadPercent" -Color Gray
|
||||
|
||||
$tableQuery = @"
|
||||
SELECT
|
||||
format('%I.%I', schemaname, relname) AS qualified_table,
|
||||
n_dead_tup,
|
||||
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_pct
|
||||
FROM pg_stat_user_tables
|
||||
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
|
||||
AND n_live_tup > 0
|
||||
AND n_dead_tup >= $MinDeadTuples
|
||||
AND (100.0 * n_dead_tup / NULLIF(n_live_tup, 0)) >= $MinDeadPercent
|
||||
ORDER BY n_dead_tup DESC;
|
||||
"@
|
||||
|
||||
try {
|
||||
$tables = & $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -t -A -F "|" -c $tableQuery 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-ColorOutput "[ERROR] Failed to evaluate dead tuple thresholds: $tables" -Color Red
|
||||
return $false
|
||||
}
|
||||
|
||||
$tableLines = @($tables | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
if ($tableLines.Count -eq 0) {
|
||||
Write-ColorOutput "[OK] No tables exceeded dead tuple thresholds" -Color Green
|
||||
return $true
|
||||
}
|
||||
|
||||
Write-ColorOutput "Found $($tableLines.Count) table(s) requiring VACUUM ANALYZE" -Color Yellow
|
||||
|
||||
foreach ($line in $tableLines) {
|
||||
$parts = $line -split "\|", 3
|
||||
$qualifiedTable = $parts[0].Trim()
|
||||
$deadTuples = if ($parts.Count -ge 2) { $parts[1].Trim() } else { "unknown" }
|
||||
$deadPercent = if ($parts.Count -ge 3) { $parts[2].Trim() } else { "unknown" }
|
||||
|
||||
Write-ColorOutput "Running VACUUM ANALYZE on $qualifiedTable (dead tuples: $deadTuples, dead percent: $deadPercent)" -Color Cyan
|
||||
|
||||
$vacuumResult = & $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "VACUUM (ANALYZE, VERBOSE) $qualifiedTable;" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-ColorOutput "[ERROR] VACUUM ANALYZE failed for $qualifiedTable : $vacuumResult" -Color Red
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-ColorOutput "[OK] VACUUM ANALYZE completed for $qualifiedTable" -Color Green
|
||||
}
|
||||
|
||||
return $true
|
||||
} catch {
|
||||
Write-ColorOutput "[ERROR] Conditional VACUUM ANALYZE failed: $_" -Color Red
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Compare-WithPrevious {
|
||||
param(
|
||||
[string]$CurrentReport,
|
||||
[string]$OutputDirectory
|
||||
)
|
||||
|
||||
Write-ColorOutput "`nComparing with previous week's report..." -Color Cyan
|
||||
|
||||
# Find most recent previous report
|
||||
$previousReports = Get-ChildItem "$OutputDirectory\diagnostics_*.txt" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -ne (Split-Path $CurrentReport -Leaf) } |
|
||||
Sort-Object LastWriteTime -Descending
|
||||
|
||||
if ($previousReports.Count -eq 0) {
|
||||
Write-ColorOutput "[WARN] No previous reports found for comparison" -Color Yellow
|
||||
return
|
||||
}
|
||||
|
||||
$previousReport = $previousReports[0].FullName
|
||||
Write-ColorOutput "Comparing with: $($previousReports[0].Name)" -Color Cyan
|
||||
|
||||
# TODO: Add detailed comparison logic
|
||||
# For now, just note that both reports exist
|
||||
Write-ColorOutput "[OK] Comparison data available" -Color Green
|
||||
Write-ColorOutput " Current: $CurrentReport" -Color Gray
|
||||
Write-ColorOutput " Previous: $previousReport" -Color Gray
|
||||
}
|
||||
|
||||
function New-SummaryReport {
|
||||
param(
|
||||
[string]$OutputDirectory,
|
||||
[string]$Timestamp
|
||||
)
|
||||
|
||||
Write-ColorOutput "`nGenerating summary report..." -Color Cyan
|
||||
|
||||
$summaryFile = Join-Path $OutputDirectory "summary_$Timestamp.txt"
|
||||
|
||||
$summary = @"
|
||||
================================================================================
|
||||
JELLYFIN DATABASE WEEKLY PERFORMANCE SUMMARY
|
||||
================================================================================
|
||||
Report Date: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
||||
Database: $DB_NAME at $DB_HOST
|
||||
Week Number: $(Get-WeekNumber)
|
||||
================================================================================
|
||||
|
||||
REPORTS GENERATED:
|
||||
* diagnostics_$Timestamp.txt : Full diagnostic report
|
||||
* query_analysis_$Timestamp.txt : Slow query analysis
|
||||
* index_usage_$Timestamp.txt : Index usage statistics
|
||||
* table_sizes_$Timestamp.txt : Table size and bloat info
|
||||
* summary_$Timestamp.txt : This summary
|
||||
|
||||
================================================================================
|
||||
QUICK HEALTH CHECK:
|
||||
================================================================================
|
||||
|
||||
To view critical issues check diagnostics file for:
|
||||
* Section 5: TABLES WITH HIGH SEQUENTIAL SCANS
|
||||
* Section 8: UNUSED OR RARELY USED INDEXES
|
||||
* Section 10: SLOWEST QUERIES
|
||||
|
||||
To identify slow queries check query_analysis file for:
|
||||
* Section 2: TOP 20 SLOWEST QUERIES (by max execution time)
|
||||
* Section 5: BASEITEMS TABLE QUERIES ANALYSIS
|
||||
|
||||
================================================================================
|
||||
RECOMMENDED ACTIONS:
|
||||
================================================================================
|
||||
|
||||
* Review slow queries taking longer than 10 seconds
|
||||
* Check if new indexes are being utilized
|
||||
* Look for tables with many sequential scans
|
||||
* Monitor cache hit ratio (should be above 95 percent)
|
||||
* Check for tables with high dead tuples percentage (need VACUUM)
|
||||
|
||||
================================================================================
|
||||
AUTOMATION SETUP:
|
||||
================================================================================
|
||||
|
||||
To schedule this script weekly via Task Scheduler run Setup-Weekly-Monitor.ps1
|
||||
as Administrator. This will create a scheduled task to run every Monday at 6am.
|
||||
|
||||
To disable the scheduled task run Setup-Weekly-Monitor.ps1 with -Uninstall flag.
|
||||
|
||||
================================================================================
|
||||
NEXT REPORT: $(Get-Date).AddDays(7).ToString("yyyy-MM-dd")
|
||||
================================================================================
|
||||
"@
|
||||
|
||||
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
|
||||
Write-ColorOutput "[OK] Summary generated: $summaryFile" -Color Green
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main Execution
|
||||
# ============================================================================
|
||||
|
||||
Write-ColorOutput "`n========================================" -Color Cyan
|
||||
Write-ColorOutput "Jellyfin Database Weekly Monitor" -Color Cyan
|
||||
Write-ColorOutput "========================================" -Color Cyan
|
||||
Write-ColorOutput "Database: $DB_NAME" -Color Yellow
|
||||
Write-ColorOutput "Host: $DB_HOST" -Color Yellow
|
||||
Write-ColorOutput "Week: $(Get-WeekNumber)" -Color Yellow
|
||||
Write-ColorOutput "Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -Color Yellow
|
||||
Write-ColorOutput "========================================`n" -Color Cyan
|
||||
|
||||
# Create output directory
|
||||
$reportDir = Join-Path $ProjectRoot $OutputDirectory
|
||||
if (-not (Test-Path $reportDir)) {
|
||||
New-Item -ItemType Directory -Path $reportDir -Force | Out-Null
|
||||
Write-ColorOutput "Created report directory: $reportDir" -Color Green
|
||||
}
|
||||
|
||||
# Generate timestamp for this run
|
||||
$timestamp = Get-ReportTimestamp
|
||||
|
||||
# Test connection
|
||||
if (-not (Test-DatabaseConnection)) {
|
||||
Write-ColorOutput "`n[ERROR] Cannot connect to database. Exiting." -Color Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Run diagnostics
|
||||
$diagnosticsFile = Join-Path $reportDir "diagnostics_$timestamp.txt"
|
||||
$success = Invoke-DiagnosticReport -OutputFile $diagnosticsFile
|
||||
|
||||
if (-not $success) {
|
||||
Write-ColorOutput "`n[WARN] Some reports failed. Check output directory." -Color Yellow
|
||||
}
|
||||
|
||||
# Run query analysis
|
||||
$queryAnalysisFile = Join-Path $reportDir "query_analysis_$timestamp.txt"
|
||||
Invoke-QueryAnalysis -OutputFile $queryAnalysisFile | Out-Null
|
||||
|
||||
# Gather index usage stats
|
||||
$indexUsageFile = Join-Path $reportDir "index_usage_$timestamp.txt"
|
||||
Get-IndexUsageStats -OutputFile $indexUsageFile | Out-Null
|
||||
|
||||
# Gather table size stats
|
||||
$tableSizesFile = Join-Path $reportDir "table_sizes_$timestamp.txt"
|
||||
Get-TableSizeStats -OutputFile $tableSizesFile | Out-Null
|
||||
|
||||
# Optionally vacuum/analyze tables that exceed dead tuple thresholds
|
||||
if ($AutoVacuumAnalyze) {
|
||||
$vacuumSuccess = Invoke-ConditionalVacuumAnalyze `
|
||||
-MinDeadTuples $DeadTupleThreshold `
|
||||
-MinDeadPercent $DeadTuplePercentThreshold
|
||||
|
||||
if (-not $vacuumSuccess) {
|
||||
Write-ColorOutput "[WARN] Conditional VACUUM ANALYZE encountered errors" -Color Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Compare with previous week if requested
|
||||
if ($CompareWithPrevious) {
|
||||
Compare-WithPrevious -CurrentReport $diagnosticsFile -OutputDirectory $reportDir
|
||||
}
|
||||
|
||||
# Generate summary
|
||||
New-SummaryReport -OutputDirectory $reportDir -Timestamp $timestamp
|
||||
|
||||
# Cleanup old reports (keep last 12 weeks)
|
||||
Write-ColorOutput "`nCleaning up old reports (keeping last 12 weeks)..." -Color Cyan
|
||||
Get-ChildItem $reportDir -File |
|
||||
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-84) } |
|
||||
Remove-Item -Force
|
||||
Write-ColorOutput "[OK] Cleanup complete" -Color Green
|
||||
|
||||
Write-ColorOutput "`n========================================" -Color Green
|
||||
Write-ColorOutput "[OK] WEEKLY MONITORING COMPLETE" -Color Green
|
||||
Write-ColorOutput "========================================" -Color Green
|
||||
Write-ColorOutput "Reports saved to: $reportDir" -Color Yellow
|
||||
Write-ColorOutput "Summary: summary_$timestamp.txt" -Color Yellow
|
||||
Write-ColorOutput "`nNext steps:" -Color Cyan
|
||||
Write-ColorOutput "1. Review summary_$timestamp.txt" -Color White
|
||||
Write-ColorOutput "2. Check for critical issues in diagnostics report" -Color White
|
||||
Write-ColorOutput "3. Analyze slow queries in query_analysis report" -Color White
|
||||
Write-ColorOutput "========================================`n" -Color Green
|
||||
|
||||
# Email report if requested (placeholder)
|
||||
if ($EmailReport) {
|
||||
Write-ColorOutput "[WARN] Email functionality not yet implemented" -Color Yellow
|
||||
Write-ColorOutput " Configure Send-MailMessage with your SMTP settings" -Color Gray
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,162 @@
|
||||
# Build Jellyfin Installer
|
||||
# This script builds the Jellyfin installer using Inno Setup
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Version = "11.0.0-PostgreSQL-PREVIEW",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("Release", "Debug")]
|
||||
[string]$Configuration = "Release",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$SkipBuild
|
||||
)
|
||||
|
||||
Write-Host "`n╔══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
|
||||
Write-Host "║ Jellyfin PostgreSQL Installer Builder ║" -ForegroundColor Cyan
|
||||
Write-Host "╚══════════════════════════════════════════════════════════╝`n" -ForegroundColor Cyan
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDir = $PSScriptRoot
|
||||
$SolutionRoot = "E:\Projects\pgsql-jellyfin"
|
||||
$InnoSetupPath = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
|
||||
|
||||
# Check prerequisites
|
||||
Write-Host "Checking prerequisites..." -ForegroundColor Yellow
|
||||
|
||||
# Check if Inno Setup is installed
|
||||
if (-not (Test-Path $InnoSetupPath)) {
|
||||
Write-Host "❌ Inno Setup 6 not found!" -ForegroundColor Red
|
||||
Write-Host "Please install it from: https://jrsoftware.org/isinfo.php" -ForegroundColor Yellow
|
||||
Write-Host "Or run: winget install JRSoftware.InnoSetup" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Host "✅ Inno Setup found" -ForegroundColor Green
|
||||
|
||||
# Check if lib folder exists
|
||||
if (-not (Test-Path "$SolutionRoot\lib\$Configuration\net11.0")) {
|
||||
if ($SkipBuild) {
|
||||
Write-Host "❌ Build folder not found: $SolutionRoot\lib\$Configuration\net11.0" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "⚠️ Build folder not found, building solution first..." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Build the solution if needed
|
||||
if (-not $SkipBuild) {
|
||||
Write-Host "`nBuilding Jellyfin solution..." -ForegroundColor Cyan
|
||||
Push-Location $SolutionRoot
|
||||
|
||||
try {
|
||||
# Clean previous build
|
||||
Write-Host "Cleaning previous build..." -ForegroundColor Gray
|
||||
dotnet clean --configuration $Configuration | Out-Null
|
||||
|
||||
# Build solution
|
||||
Write-Host "Building $Configuration configuration..." -ForegroundColor Gray
|
||||
$buildOutput = dotnet build Jellyfin.Server --configuration $Configuration 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ Build failed!" -ForegroundColor Red
|
||||
Write-Host $buildOutput
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ Build successful" -ForegroundColor Green
|
||||
|
||||
# Count DLLs
|
||||
$dllCount = (Get-ChildItem "$SolutionRoot\lib\$Configuration\net11.0" -Filter "*.dll" -Recurse).Count
|
||||
Write-Host "📊 Built $dllCount DLL files" -ForegroundColor Gray
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
# Check if installer script exists
|
||||
$InnoScriptPath = "$SolutionRoot\jellyfin-setup.iss"
|
||||
if (-not (Test-Path $InnoScriptPath)) {
|
||||
Write-Host "❌ Installer script not found: $InnoScriptPath" -ForegroundColor Red
|
||||
Write-Host "Please ensure jellyfin-setup.iss is in the solution root" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check for icon file (optional)
|
||||
$IconPath = "$SolutionRoot\jellyfin.ico"
|
||||
if (-not (Test-Path $IconPath)) {
|
||||
Write-Host "⚠️ Icon file not found: $IconPath" -ForegroundColor Yellow
|
||||
Write-Host "The installer will use the default Inno Setup icon" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Update version in script if needed
|
||||
Write-Host "`nPreparing installer script..." -ForegroundColor Cyan
|
||||
$scriptContent = Get-Content $InnoScriptPath -Raw
|
||||
$scriptContent = $scriptContent -replace 'AppVersion=[\d\.]+', "AppVersion=$Version"
|
||||
$scriptContent = $scriptContent -replace 'OutputBaseFilename=JellyfinSetup-PostgreSQL-[\d\.]+', "OutputBaseFilename=JellyfinSetup-PostgreSQL-$Version"
|
||||
Set-Content $InnoScriptPath $scriptContent -NoNewline
|
||||
Write-Host "✅ Version updated to $Version" -ForegroundColor Green
|
||||
|
||||
# Create output directory
|
||||
$OutputDir = "$SolutionRoot\installer-output"
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
# Build the installer
|
||||
Write-Host "`nBuilding installer..." -ForegroundColor Cyan
|
||||
Write-Host "Script: $InnoScriptPath" -ForegroundColor Gray
|
||||
Write-Host "Output: $OutputDir" -ForegroundColor Gray
|
||||
|
||||
Push-Location $SolutionRoot
|
||||
|
||||
try {
|
||||
$compileOutput = & $InnoSetupPath $InnoScriptPath 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ Installer build failed!" -ForegroundColor Red
|
||||
Write-Host $compileOutput
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ Installer built successfully!" -ForegroundColor Green
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
# Find the installer file
|
||||
$installerFile = Get-ChildItem $OutputDir -Filter "JellyfinSetup-PostgreSQL-*.exe" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
|
||||
if ($installerFile) {
|
||||
$fileSize = [math]::Round($installerFile.Length / 1MB, 2)
|
||||
|
||||
Write-Host "`n╔══════════════════════════════════════════════════════════╗" -ForegroundColor Green
|
||||
Write-Host "║ Installer Build Complete! ✅ ║" -ForegroundColor Green
|
||||
Write-Host "╚══════════════════════════════════════════════════════════╝`n" -ForegroundColor Green
|
||||
|
||||
Write-Host "📦 Installer Details:" -ForegroundColor Cyan
|
||||
Write-Host " File: $($installerFile.Name)" -ForegroundColor White
|
||||
Write-Host " Path: $($installerFile.FullName)" -ForegroundColor White
|
||||
Write-Host " Size: $fileSize MB" -ForegroundColor White
|
||||
Write-Host " Date: $($installerFile.LastWriteTime)" -ForegroundColor White
|
||||
|
||||
Write-Host "`n📋 Next Steps:" -ForegroundColor Cyan
|
||||
Write-Host " 1. Test the installer on a clean Windows system" -ForegroundColor Gray
|
||||
Write-Host " 2. Sign the installer (optional): signtool sign /f cert.pfx installer.exe" -ForegroundColor Gray
|
||||
Write-Host " 3. Upload to GitHub releases or your distribution server" -ForegroundColor Gray
|
||||
|
||||
Write-Host "`n🧪 Test Commands:" -ForegroundColor Cyan
|
||||
Write-Host " # Silent install" -ForegroundColor Gray
|
||||
Write-Host " $($installerFile.FullName) /VERYSILENT /LOG=install.log" -ForegroundColor DarkGray
|
||||
Write-Host " # Normal install" -ForegroundColor Gray
|
||||
Write-Host " $($installerFile.FullName)" -ForegroundColor DarkGray
|
||||
|
||||
# Open output folder
|
||||
Write-Host "`nOpening output folder..." -ForegroundColor Cyan
|
||||
Start-Process $OutputDir
|
||||
}
|
||||
else {
|
||||
Write-Host "❌ Installer file not found in output directory!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "`n✅ All done!`n" -ForegroundColor Green
|
||||
@@ -0,0 +1,22 @@
|
||||
# copy-sql-files.ps1
|
||||
# Run this after publish to copy SQL files
|
||||
|
||||
param(
|
||||
[string]$PublishDir = "Jellyfin.Server\bin\Release\net11.0\publish"
|
||||
)
|
||||
|
||||
Write-Host "Copying SQL files to $PublishDir..." -ForegroundColor Cyan
|
||||
|
||||
# Create sql directory
|
||||
New-Item -ItemType Directory -Path "$PublishDir\sql" -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path "$PublishDir\sql\schema_init" -Force | Out-Null
|
||||
|
||||
# Copy SQL files
|
||||
Copy-Item "Jellyfin.Server\sql\*.sql" "$PublishDir\sql\" -Force
|
||||
Copy-Item "Jellyfin.Server\sql\schema_init\*.sql" "$PublishDir\sql\schema_init\" -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "✓ SQL files copied successfully!" -ForegroundColor Green
|
||||
|
||||
# List copied files
|
||||
Write-Host "`nCopied files:" -ForegroundColor Yellow
|
||||
Get-ChildItem "$PublishDir\sql" -Recurse -File | Select-Object FullName
|
||||
@@ -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 ""
|
||||
@@ -0,0 +1,66 @@
|
||||
# Quick Database Command Runner
|
||||
# Uses settings from db-config.ps1
|
||||
|
||||
# Load configuration
|
||||
. .\db-config.ps1
|
||||
|
||||
Write-Host "Quick Database Commands" -ForegroundColor Cyan
|
||||
Write-Host "Using database: $DB_NAME" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# Show menu
|
||||
Write-Host "Choose an option:" -ForegroundColor Green
|
||||
Write-Host "1. Run diagnostics (sql\diagnostics.sql)"
|
||||
Write-Host "2. Check index usage"
|
||||
Write-Host "3. Check if performance indexes exist"
|
||||
Write-Host "4. Run all performance indexes script"
|
||||
Write-Host "5. Custom query"
|
||||
Write-Host "6. Change database"
|
||||
Write-Host "7. Exit"
|
||||
Write-Host ""
|
||||
|
||||
$choice = Read-Host "Enter choice (1-7)"
|
||||
|
||||
switch ($choice) {
|
||||
"1" {
|
||||
Write-Host "Running diagnostics..." -ForegroundColor Yellow
|
||||
Invoke-PSQL -File "sql\diagnostics.sql" -OutputFile "diagnostics_$(Get-Date -Format 'yyyy-MM-dd_HHmmss').txt"
|
||||
Write-Host "✓ Results saved to diagnostics_*.txt" -ForegroundColor Green
|
||||
}
|
||||
"2" {
|
||||
Write-Host "Checking index usage..." -ForegroundColor Yellow
|
||||
$query = "SELECT indexname, idx_scan, idx_tup_read, idx_tup_fetch FROM pg_stat_user_indexes WHERE schemaname = 'library' AND indexname LIKE 'idx_%' ORDER BY idx_scan DESC;"
|
||||
Invoke-PSQL -Query $query
|
||||
}
|
||||
"3" {
|
||||
Write-Host "Checking performance indexes..." -ForegroundColor Yellow
|
||||
$query = "SELECT COUNT(*) as perf_index_count FROM pg_indexes WHERE schemaname = 'library' AND indexname IN ('baseitems_communityrating_idx', 'baseitems_datecreated_idx', 'baseitems_datemodified_idx', 'baseitems_parentid_idx', 'baseitems_premieredate_idx', 'baseitems_productionyear_idx', 'baseitems_seriespresentationuniquekey_idx', 'baseitems_sortname_idx', 'baseitems_topparentid_idx');"
|
||||
Invoke-PSQL -Query $query
|
||||
}
|
||||
"4" {
|
||||
Write-Host "Running all performance indexes script..." -ForegroundColor Yellow
|
||||
Invoke-PSQL -File "sql\all_performance_indexes.sql"
|
||||
Write-Host "✓ Script complete" -ForegroundColor Green
|
||||
}
|
||||
"5" {
|
||||
$query = Read-Host "Enter SQL query"
|
||||
Invoke-PSQL -Query $query
|
||||
}
|
||||
"6" {
|
||||
Write-Host ""
|
||||
Write-Host "Current database: $DB_NAME" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "To change database:" -ForegroundColor Cyan
|
||||
Write-Host "1. Edit db-config.ps1" -ForegroundColor Gray
|
||||
Write-Host "2. Change the line: `$DB_NAME = `"jellyfin_testsdata`"" -ForegroundColor Gray
|
||||
Write-Host "3. Save and run: . .\db-config.ps1" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
}
|
||||
"7" {
|
||||
Write-Host "Goodbye!" -ForegroundColor Cyan
|
||||
exit
|
||||
}
|
||||
default {
|
||||
Write-Host "Invalid choice" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
param(
|
||||
[string]$SourcePath = (Join-Path $PSScriptRoot '..\sql_statements.txt')
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$source = $SourcePath
|
||||
$target = Join-Path $PSScriptRoot '..\sql\regression_from_sql_statements.sql'
|
||||
$targetWithSkipped = Join-Path $PSScriptRoot '..\sql\regression_from_sql_statements_with_skipped.sql'
|
||||
$skipReportCsv = Join-Path $PSScriptRoot '..\sql\regression_from_sql_statements_skip_report.csv'
|
||||
|
||||
function Convert-ParamLiteral {
|
||||
param([string]$Value)
|
||||
|
||||
if ($null -eq $Value) {
|
||||
return 'NULL'
|
||||
}
|
||||
|
||||
$trimmed = $Value.Trim()
|
||||
|
||||
if ($trimmed -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') {
|
||||
return "'$trimmed'::uuid"
|
||||
}
|
||||
|
||||
if ($trimmed -match '^(?i:true|false)$') {
|
||||
return $trimmed.ToLowerInvariant()
|
||||
}
|
||||
|
||||
if ($trimmed -match '^\d+$') {
|
||||
return $trimmed
|
||||
}
|
||||
|
||||
$escaped = $trimmed.Replace("'", "''")
|
||||
return "'$escaped'"
|
||||
}
|
||||
|
||||
function Apply-ParamsToSql {
|
||||
param(
|
||||
[string]$Sql,
|
||||
[string]$ParamsText
|
||||
)
|
||||
|
||||
$result = $Sql
|
||||
$normalizedParams = if ($null -eq $ParamsText) { '' } else { $ParamsText.Trim() }
|
||||
|
||||
# Newer EF logs wrap parameter payload in quotes inside [Parameters=[...]].
|
||||
if ($normalizedParams.StartsWith('"') -and $normalizedParams.EndsWith('"')) {
|
||||
$normalizedParams = $normalizedParams.Trim('"')
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($normalizedParams) -or $normalizedParams -eq '[]') {
|
||||
return $result
|
||||
}
|
||||
|
||||
# Truncated arrays in logs contain ellipsis; these are skipped later.
|
||||
if ($normalizedParams -like '*...*') {
|
||||
return $result
|
||||
}
|
||||
|
||||
$arrayPattern = [regex]"@(?<name>[A-Za-z0-9_]+)=\{(?<vals>.*?)\}\s*\(DbType\s*=\s*Object\)"
|
||||
foreach ($match in $arrayPattern.Matches($normalizedParams)) {
|
||||
$name = $match.Groups['name'].Value
|
||||
$valsRaw = $match.Groups['vals'].Value
|
||||
$valueMatches = [regex]::Matches($valsRaw, "'([^']*)'")
|
||||
|
||||
$renderedVals = @()
|
||||
foreach ($valueMatch in $valueMatches) {
|
||||
$renderedVals += (Convert-ParamLiteral -Value $valueMatch.Groups[1].Value)
|
||||
}
|
||||
|
||||
$arrayExpr = if ($renderedVals.Count -gt 0) {
|
||||
'ARRAY[' + ($renderedVals -join ', ') + ']'
|
||||
}
|
||||
else {
|
||||
'ARRAY[]::text[]'
|
||||
}
|
||||
|
||||
$tokenPattern = '(?<![A-Za-z0-9_])@' + [regex]::Escape($name) + '(?![A-Za-z0-9_])'
|
||||
$result = [regex]::Replace(
|
||||
$result,
|
||||
$tokenPattern,
|
||||
[System.Text.RegularExpressions.MatchEvaluator]{ param($m) $arrayExpr }
|
||||
)
|
||||
}
|
||||
|
||||
$scalarPattern = [regex]"@(?<name>[A-Za-z0-9_]+)='(?<val>(?:''|[^'])*)'"
|
||||
foreach ($match in $scalarPattern.Matches($normalizedParams)) {
|
||||
$name = $match.Groups['name'].Value
|
||||
$rawVal = $match.Groups['val'].Value -replace "''", "'"
|
||||
$literal = Convert-ParamLiteral -Value $rawVal
|
||||
$tokenPattern = '(?<![A-Za-z0-9_])@' + [regex]::Escape($name) + '(?![A-Za-z0-9_])'
|
||||
$result = [regex]::Replace(
|
||||
$result,
|
||||
$tokenPattern,
|
||||
[System.Text.RegularExpressions.MatchEvaluator]{ param($m) $literal }
|
||||
)
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Test-SqlStatementCompleteness {
|
||||
param([string]$Sql)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Sql)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$trimmed = $Sql.Trim()
|
||||
$oneLine = ($trimmed -replace '\s+', ' ')
|
||||
|
||||
if ($oneLine -match '^(?i:SELECT\s+[^;]*\s+WHERE\s+)' -and $oneLine -notmatch '(?i:\sFROM\s)') {
|
||||
return $false
|
||||
}
|
||||
|
||||
if ($oneLine -match '^(?i:SELECT\s+)' -and
|
||||
$oneLine -notmatch '(?i:\sFROM\s|\sEXISTS\s*\(|^SELECT\s+1\b)') {
|
||||
return $false
|
||||
}
|
||||
|
||||
$depth = 0
|
||||
foreach ($ch in $trimmed.ToCharArray()) {
|
||||
if ($ch -eq '(') { $depth++ }
|
||||
elseif ($ch -eq ')') {
|
||||
$depth--
|
||||
if ($depth -lt 0) { return $false }
|
||||
}
|
||||
}
|
||||
if ($depth -ne 0) {
|
||||
return $false
|
||||
}
|
||||
|
||||
if ($oneLine -match '(?i:(SELECT|FROM|WHERE|JOIN|INNER JOIN|LEFT JOIN|RIGHT JOIN|FULL JOIN|ON|ORDER BY|GROUP BY|LIMIT|OFFSET|UNION)\s*$)') {
|
||||
return $false
|
||||
}
|
||||
|
||||
if ($oneLine -match ',\s*$') {
|
||||
return $false
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
function Split-SqlCandidates {
|
||||
param([string]$Sql)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Sql)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$lines = $Sql -split "`n"
|
||||
$startIndexes = New-Object System.Collections.Generic.List[int]
|
||||
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
if ($lines[$i] -match '^(SELECT|UPDATE|INSERT|DELETE|CREATE|ALTER|DROP|WITH)\b') {
|
||||
$startIndexes.Add($i)
|
||||
}
|
||||
}
|
||||
|
||||
if ($startIndexes.Count -le 1) {
|
||||
return @($Sql.Trim())
|
||||
}
|
||||
|
||||
$chunks = New-Object System.Collections.Generic.List[string]
|
||||
for ($s = 0; $s -lt $startIndexes.Count; $s++) {
|
||||
$start = $startIndexes[$s]
|
||||
$end = if ($s -lt $startIndexes.Count - 1) { $startIndexes[$s + 1] - 1 } else { $lines.Count - 1 }
|
||||
if ($end -lt $start) {
|
||||
continue
|
||||
}
|
||||
|
||||
$segment = ($lines[$start..$end] -join "`n").Trim()
|
||||
if (-not [string]::IsNullOrWhiteSpace($segment)) {
|
||||
$chunks.Add($segment)
|
||||
}
|
||||
}
|
||||
|
||||
return $chunks
|
||||
}
|
||||
|
||||
function Normalize-SqlLogLine {
|
||||
param([string]$Line)
|
||||
|
||||
if ($null -eq $Line) {
|
||||
return ''
|
||||
}
|
||||
|
||||
$normalized = $Line.Trim()
|
||||
|
||||
# SQL text in this log file uses C-style escaped identifier quotes.
|
||||
$normalized = $normalized -replace '\\"', '"'
|
||||
|
||||
# Strip only leading wrapper quotes from EF logs (keep trailing identifier quotes).
|
||||
if ($normalized.StartsWith('""')) {
|
||||
$normalized = $normalized.Substring(2)
|
||||
}
|
||||
elseif ($normalized.StartsWith('"')) {
|
||||
$normalized = $normalized.Substring(1)
|
||||
}
|
||||
|
||||
return $normalized
|
||||
}
|
||||
|
||||
$entries = New-Object System.Collections.Generic.List[object]
|
||||
$current = $null
|
||||
|
||||
$commandLinePattern = [regex]'^\[(?<ts>[^\]]+)\].*Executed DbCommand\s*\("?(?<ms>\d+)"?ms\)\s*\[Parameters=\[(?<params>.*?)\],\s*CommandType=''Text'',\s*CommandTimeout=''(?<timeout>\d+)''\](?<tail>.*)$'
|
||||
$logLinePattern = [regex]'^\[(?:\d{2}:\d{2}:\d{2}\.\d{3}|\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}(?:\s+[^\]]+)?)\]'
|
||||
|
||||
Get-Content -Path $source | ForEach-Object {
|
||||
$line = $_
|
||||
$cmdMatch = $commandLinePattern.Match($line)
|
||||
|
||||
if ($cmdMatch.Success) {
|
||||
if ($null -ne $current) {
|
||||
$entries.Add([pscustomobject]@{
|
||||
ms = $current.ms
|
||||
timeout = $current.timeout
|
||||
params = $current.params
|
||||
sql = ($current.sqlLines -join "`n").Trim()
|
||||
})
|
||||
}
|
||||
|
||||
$current = [ordered]@{
|
||||
ms = $cmdMatch.Groups['ms'].Value
|
||||
timeout = $cmdMatch.Groups['timeout'].Value
|
||||
params = $cmdMatch.Groups['params'].Value
|
||||
sqlLines = New-Object System.Collections.Generic.List[string]
|
||||
}
|
||||
|
||||
$tail = Normalize-SqlLogLine -Line $cmdMatch.Groups['tail'].Value
|
||||
if (-not [string]::IsNullOrWhiteSpace($tail)) {
|
||||
$current.sqlLines.Add($tail)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ($null -eq $current) {
|
||||
return
|
||||
}
|
||||
|
||||
if ($logLinePattern.IsMatch($line)) {
|
||||
$entries.Add([pscustomobject]@{
|
||||
ms = $current.ms
|
||||
timeout = $current.timeout
|
||||
params = $current.params
|
||||
sql = ($current.sqlLines -join "`n").Trim()
|
||||
})
|
||||
$current = $null
|
||||
return
|
||||
}
|
||||
|
||||
$normalizedLine = Normalize-SqlLogLine -Line $line
|
||||
if (-not [string]::IsNullOrWhiteSpace($normalizedLine)) {
|
||||
$current.sqlLines.Add($normalizedLine)
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $current) {
|
||||
$entries.Add([pscustomobject]@{
|
||||
ms = $current.ms
|
||||
timeout = $current.timeout
|
||||
params = $current.params
|
||||
sql = ($current.sqlLines -join "`n").Trim()
|
||||
})
|
||||
}
|
||||
|
||||
$output = New-Object System.Collections.Generic.List[string]
|
||||
$output.Add('-- Auto-generated from sql_statements.txt for regression testing')
|
||||
$output.Add("-- Source: $source")
|
||||
$output.Add("-- Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss K')")
|
||||
$output.Add('-- Unresolved parameterized statements are skipped and counted in summary.')
|
||||
$output.Add('\\timing on')
|
||||
$output.Add("SET statement_timeout = '0';")
|
||||
$output.Add('')
|
||||
|
||||
$outputWithSkipped = New-Object System.Collections.Generic.List[string]
|
||||
$outputWithSkipped.Add('-- Auto-generated from sql_statements.txt for regression testing')
|
||||
$outputWithSkipped.Add("-- Source: $source")
|
||||
$outputWithSkipped.Add("-- Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss K')")
|
||||
$outputWithSkipped.Add('-- Includes unresolved statements as commented blocks for manual editing.')
|
||||
$outputWithSkipped.Add('\\timing on')
|
||||
$outputWithSkipped.Add("SET statement_timeout = '0';")
|
||||
$outputWithSkipped.Add('')
|
||||
|
||||
$totalExtracted = 0
|
||||
$written = 0
|
||||
$skipped = 0
|
||||
$writtenWithSkipped = 0
|
||||
$skipReasonCounts = @{}
|
||||
|
||||
function Add-SkipReason {
|
||||
param([string]$Reason)
|
||||
|
||||
if (-not $skipReasonCounts.ContainsKey($Reason)) {
|
||||
$skipReasonCounts[$Reason] = 0
|
||||
}
|
||||
|
||||
$skipReasonCounts[$Reason]++
|
||||
}
|
||||
|
||||
foreach ($entry in $entries) {
|
||||
$totalExtracted++
|
||||
|
||||
$sqlText = $entry.sql
|
||||
if ([string]::IsNullOrWhiteSpace($sqlText)) {
|
||||
$skipped++
|
||||
Add-SkipReason -Reason 'empty_sql_block'
|
||||
continue
|
||||
}
|
||||
|
||||
# Drop only whole-statement wrapper quotes emitted by EF logs.
|
||||
$sqlText = $sqlText.Trim()
|
||||
$sqlText = $sqlText -replace '^"+(?=(SELECT|UPDATE|INSERT|DELETE|CREATE|ALTER|DROP|WITH)\b)', ''
|
||||
$sqlText = $sqlText -replace '"\s*$', ''
|
||||
|
||||
$sqlText = Apply-ParamsToSql -Sql $sqlText -ParamsText $entry.params
|
||||
|
||||
if ($sqlText -notmatch ';\s*$') {
|
||||
$sqlText += ';'
|
||||
}
|
||||
|
||||
$writtenWithSkipped++
|
||||
$outputWithSkipped.Add("-- Query #$writtenWithSkipped | logged_duration_ms=$($entry.ms) | timeout_s=$($entry.timeout)")
|
||||
|
||||
$topLevelStarts = ([regex]::Matches($sqlText, '(?m)^(SELECT|UPDATE|INSERT|DELETE|CREATE|ALTER|DROP|WITH)\b')).Count
|
||||
if ($topLevelStarts -gt 1) {
|
||||
$skipped++
|
||||
Add-SkipReason -Reason 'multiple_top_level_sql_starts'
|
||||
$outputWithSkipped.Add('-- SKIPPED IN RUNNABLE FILE: block contains multiple top-level SQL starts (likely concatenated log fragments)')
|
||||
foreach ($line in ($sqlText -split "`n")) {
|
||||
$outputWithSkipped.Add('-- ' + $line)
|
||||
}
|
||||
$outputWithSkipped.Add('')
|
||||
continue
|
||||
}
|
||||
|
||||
if ($sqlText -match '@[A-Za-z_][A-Za-z0-9_]*') {
|
||||
$skipped++
|
||||
Add-SkipReason -Reason 'unresolved_parameter_placeholders'
|
||||
$outputWithSkipped.Add('-- SKIPPED IN RUNNABLE FILE: unresolved placeholders remain')
|
||||
if (-not [string]::IsNullOrWhiteSpace($entry.params)) {
|
||||
$outputWithSkipped.Add("-- Logged parameters: [$($entry.params)]")
|
||||
}
|
||||
foreach ($line in ($sqlText -split "`n")) {
|
||||
$outputWithSkipped.Add('-- ' + $line)
|
||||
}
|
||||
$outputWithSkipped.Add('')
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not (Test-SqlStatementCompleteness -Sql $sqlText)) {
|
||||
$skipped++
|
||||
Add-SkipReason -Reason 'syntactically_incomplete_or_truncated'
|
||||
$outputWithSkipped.Add('-- SKIPPED IN RUNNABLE FILE: statement appears truncated or syntactically incomplete')
|
||||
foreach ($line in ($sqlText -split "`n")) {
|
||||
$outputWithSkipped.Add('-- ' + $line)
|
||||
}
|
||||
$outputWithSkipped.Add('')
|
||||
continue
|
||||
}
|
||||
|
||||
$written++
|
||||
$output.Add("-- Query #$written | logged_duration_ms=$($entry.ms) | timeout_s=$($entry.timeout)")
|
||||
$output.Add($sqlText)
|
||||
$output.Add('')
|
||||
|
||||
$outputWithSkipped.Add($sqlText)
|
||||
$outputWithSkipped.Add('')
|
||||
}
|
||||
|
||||
$output.Add("-- Summary: total_extracted=$totalExtracted, written=$written, skipped_unresolved=$skipped")
|
||||
$outputWithSkipped.Add("-- Summary: total_extracted=$totalExtracted, written_total=$writtenWithSkipped, unresolved_commented=$skipped")
|
||||
|
||||
$skipReasonRows = @()
|
||||
foreach ($key in ($skipReasonCounts.Keys | Sort-Object)) {
|
||||
$count = [int]$skipReasonCounts[$key]
|
||||
$pct = if ($skipped -gt 0) { [math]::Round(($count * 100.0) / $skipped, 2) } else { 0.0 }
|
||||
$skipReasonRows += [pscustomobject]@{
|
||||
reason = $key
|
||||
count = $count
|
||||
percent_of_skipped = $pct
|
||||
}
|
||||
}
|
||||
|
||||
$skipReasonRows | Export-Csv -Path $skipReportCsv -NoTypeInformation -Encoding UTF8
|
||||
|
||||
Set-Content -Path $target -Value $output -Encoding UTF8
|
||||
Set-Content -Path $targetWithSkipped -Value $outputWithSkipped -Encoding UTF8
|
||||
|
||||
Write-Output "Generated: $target"
|
||||
Write-Output "Generated: $targetWithSkipped"
|
||||
Write-Output "Generated: $skipReportCsv"
|
||||
Write-Output "Total extracted: $totalExtracted"
|
||||
Write-Output "Written runnable: $written"
|
||||
Write-Output "Skipped unresolved: $skipped"
|
||||
@@ -0,0 +1,54 @@
|
||||
# publish-with-sql.ps1
|
||||
# Publishes Jellyfin and ensures SQL files are included
|
||||
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "Publishing Jellyfin with SQL files" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Step 1: Publishing Jellyfin..." -ForegroundColor Yellow
|
||||
dotnet publish Jellyfin.Server\Jellyfin.Server.csproj -c Release -o Jellyfin.Server\bin\Release\net11.0\publish
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ Publish failed" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✓ Publish successful" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Step 2: Copying SQL files..." -ForegroundColor Yellow
|
||||
|
||||
$publishDir = "Jellyfin.Server\bin\Release\net11.0\publish"
|
||||
|
||||
# Create sql directory structure
|
||||
New-Item -ItemType Directory -Path "$publishDir\sql" -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path "$publishDir\sql\schema_init" -Force | Out-Null
|
||||
|
||||
# Copy SQL files
|
||||
try {
|
||||
Copy-Item "Jellyfin.Server\sql\*.sql" "$publishDir\sql\" -Force -ErrorAction Stop
|
||||
$mainFileCount = (Get-ChildItem "$publishDir\sql\*.sql").Count
|
||||
Write-Host " ✓ Copied $mainFileCount main SQL files" -ForegroundColor Green
|
||||
|
||||
Copy-Item "Jellyfin.Server\sql\schema_init\*.sql" "$publishDir\sql\schema_init\" -Force -ErrorAction SilentlyContinue
|
||||
$initFileCount = (Get-ChildItem "$publishDir\sql\schema_init\*.sql" -ErrorAction SilentlyContinue).Count
|
||||
Write-Host " ✓ Copied $initFileCount schema_init SQL files" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "❌ Failed to copy SQL files: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "✓ Publish Complete!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Output directory: $publishDir" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "SQL files:" -ForegroundColor Yellow
|
||||
Get-ChildItem "$publishDir\sql" -Recurse -File | ForEach-Object {
|
||||
Write-Host " - $($_.FullName.Replace($PWD.Path + '\', ''))" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "Next: Run jellyfin.exe from the publish directory" -ForegroundColor Cyan
|
||||
@@ -0,0 +1,227 @@
|
||||
# Remove SQLite from Deployment - Quick Implementation
|
||||
# This script removes SQLite dependencies for PostgreSQL-only deployment
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$DryRun,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$RemoveProject
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$SolutionRoot = "E:\Projects\pgsql-jellyfin"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host " Remove SQLite for PostgreSQL-Only Deployment" -ForegroundColor Cyan
|
||||
Write-Host "================================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "DRY RUN MODE - No changes will be made" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Step 1: Backup current state
|
||||
Write-Host "Step 1: Creating backup..." -ForegroundColor Cyan
|
||||
if (-not $DryRun) {
|
||||
Push-Location $SolutionRoot
|
||||
git stash push -m "Before SQLite removal"
|
||||
Write-Host " Changes stashed" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " Would stash current changes" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Step 2: Find SQLite references
|
||||
Write-Host ""
|
||||
Write-Host "Step 2: Finding SQLite references..." -ForegroundColor Cyan
|
||||
|
||||
$sqliteReferences = @()
|
||||
|
||||
# Check Jellyfin.Server.Implementations
|
||||
$serverImpl = "$SolutionRoot\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj"
|
||||
if (Test-Path $serverImpl) {
|
||||
$content = Get-Content $serverImpl -Raw
|
||||
if ($content -match 'Jellyfin\.Database\.Providers\.Sqlite') {
|
||||
Write-Host " Found: SQLite reference in Jellyfin.Server.Implementations" -ForegroundColor Yellow
|
||||
$sqliteReferences += @{
|
||||
File = $serverImpl
|
||||
Pattern = '<ProjectReference Include="[^"]*Jellyfin\.Database\.Providers\.Sqlite[^"]*" />'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check Emby.Server.Implementations
|
||||
$embyImpl = "$SolutionRoot\Emby.Server.Implementations\Emby.Server.Implementations.csproj"
|
||||
if (Test-Path $embyImpl) {
|
||||
$content = Get-Content $embyImpl -Raw
|
||||
if ($content -match 'Microsoft\.Data\.Sqlite') {
|
||||
Write-Host " Found: SQLite package in Emby.Server.Implementations" -ForegroundColor Yellow
|
||||
$sqliteReferences += @{
|
||||
File = $embyImpl
|
||||
Pattern = '<PackageReference Include="Microsoft\.Data\.Sqlite"[^/]*/>'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($sqliteReferences.Count -eq 0) {
|
||||
Write-Host " No SQLite references found" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Step 3: Remove references
|
||||
Write-Host ""
|
||||
Write-Host "Step 3: Removing SQLite references..." -ForegroundColor Cyan
|
||||
|
||||
foreach ($ref in $sqliteReferences) {
|
||||
$fileName = Split-Path $ref.File -Leaf
|
||||
Write-Host " Processing: $fileName" -ForegroundColor Gray
|
||||
|
||||
if (-not $DryRun) {
|
||||
$content = Get-Content $ref.File -Raw
|
||||
$newContent = $content -replace $ref.Pattern, ''
|
||||
$newContent = $newContent -replace '(\r?\n){3,}', "`r`n`r`n"
|
||||
Set-Content -Path $ref.File -Value $newContent -NoNewline
|
||||
Write-Host " Removed reference" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " Would remove SQLite reference" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
# Step 4: Exclude SQLite project from solution (optional)
|
||||
if ($RemoveProject) {
|
||||
Write-Host ""
|
||||
Write-Host "Step 4: Excluding SQLite project from solution..." -ForegroundColor Cyan
|
||||
|
||||
$solutionFile = "$SolutionRoot\Jellyfin.sln"
|
||||
if (Test-Path $solutionFile) {
|
||||
if (-not $DryRun) {
|
||||
$slnContent = Get-Content $solutionFile -Raw
|
||||
$pattern = '(Project\("[^"]*"\) = "Jellyfin\.Database\.Providers\.Sqlite"[^E]*EndProject)'
|
||||
$replacement = "# REMOVED: SQLite Provider`r`n# " + '$1'
|
||||
$newSlnContent = $slnContent -replace $pattern, $replacement
|
||||
Set-Content -Path $solutionFile -Value $newSlnContent -NoNewline
|
||||
Write-Host " SQLite project commented out in solution" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " Would comment out SQLite project" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Step 5: Clean build directories
|
||||
Write-Host ""
|
||||
Write-Host "Step 5: Cleaning build directories..." -ForegroundColor Cyan
|
||||
|
||||
$buildDirs = @(
|
||||
"$SolutionRoot\lib",
|
||||
"$SolutionRoot\Jellyfin.Server.Implementations\bin",
|
||||
"$SolutionRoot\Jellyfin.Server.Implementations\obj",
|
||||
"$SolutionRoot\Emby.Server.Implementations\bin",
|
||||
"$SolutionRoot\Emby.Server.Implementations\obj"
|
||||
)
|
||||
|
||||
foreach ($dir in $buildDirs) {
|
||||
if (Test-Path $dir) {
|
||||
if (-not $DryRun) {
|
||||
Remove-Item $dir -Recurse -Force
|
||||
$dirName = Split-Path $dir -Leaf
|
||||
Write-Host " Cleaned: $dirName" -ForegroundColor Green
|
||||
} else {
|
||||
$dirName = Split-Path $dir -Leaf
|
||||
Write-Host " Would clean: $dirName" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Step 6: Test build
|
||||
Write-Host ""
|
||||
Write-Host "Step 6: Testing build..." -ForegroundColor Cyan
|
||||
|
||||
if (-not $DryRun) {
|
||||
Push-Location $SolutionRoot
|
||||
|
||||
Write-Host " Building solution..." -ForegroundColor Gray
|
||||
$buildOutput = dotnet build --configuration Release 2>&1
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host " Build successful" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " Build failed" -ForegroundColor Red
|
||||
Write-Host $buildOutput
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Rolling back changes..." -ForegroundColor Yellow
|
||||
git stash pop
|
||||
exit 1
|
||||
}
|
||||
|
||||
Pop-Location
|
||||
} else {
|
||||
Write-Host " Would run: dotnet build --configuration Release" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Step 7: Verify no SQLite DLLs
|
||||
Write-Host ""
|
||||
Write-Host "Step 7: Verifying no SQLite DLLs..." -ForegroundColor Cyan
|
||||
|
||||
$libFolder = "$SolutionRoot\lib\Release\net11.0"
|
||||
if (Test-Path $libFolder) {
|
||||
$sqliteDlls = Get-ChildItem $libFolder -Filter "*Sqlite*" -Recurse
|
||||
|
||||
if ($sqliteDlls.Count -eq 0) {
|
||||
Write-Host " No SQLite DLLs found in output" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " Found SQLite DLLs:" -ForegroundColor Yellow
|
||||
$sqliteDlls | ForEach-Object {
|
||||
Write-Host " - $($_.Name)" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Step 8: Calculate size reduction
|
||||
Write-Host ""
|
||||
Write-Host "Step 8: Calculating deployment size..." -ForegroundColor Cyan
|
||||
|
||||
if (Test-Path $libFolder) {
|
||||
$totalSize = (Get-ChildItem $libFolder -Recurse -File | Measure-Object -Property Length -Sum).Sum
|
||||
$sizeMB = [math]::Round($totalSize / 1MB, 2)
|
||||
Write-Host " Deployment size: $sizeMB MB" -ForegroundColor White
|
||||
}
|
||||
|
||||
# Summary
|
||||
Write-Host ""
|
||||
Write-Host "================================================================" -ForegroundColor Green
|
||||
Write-Host " SQLite Removal Complete" -ForegroundColor Green
|
||||
Write-Host "================================================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
if (-not $DryRun) {
|
||||
Write-Host "Changes made:" -ForegroundColor Cyan
|
||||
Write-Host " - Removed SQLite project references" -ForegroundColor Green
|
||||
Write-Host " - Removed SQLite package references" -ForegroundColor Green
|
||||
if ($RemoveProject) {
|
||||
Write-Host " - Excluded SQLite project from solution" -ForegroundColor Green
|
||||
}
|
||||
Write-Host " - Cleaned build directories" -ForegroundColor Green
|
||||
Write-Host " - Verified build" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:" -ForegroundColor Cyan
|
||||
Write-Host " 1. Review changes: git diff" -ForegroundColor Gray
|
||||
Write-Host " 2. Test application: dotnet run --project Jellyfin.Server" -ForegroundColor Gray
|
||||
Write-Host " 3. Commit changes: git commit -am 'Remove SQLite support'" -ForegroundColor Gray
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Rollback if needed:" -ForegroundColor Yellow
|
||||
Write-Host " git stash pop" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host "Dry run complete - no changes made" -ForegroundColor Yellow
|
||||
Write-Host "Run without -DryRun to apply changes" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
if (-not $DryRun) {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
@@ -0,0 +1,64 @@
|
||||
# Rollback to .NET 10 Script
|
||||
# This script reverts all .NET 11 changes back to .NET 10
|
||||
|
||||
Write-Host "Rolling back to .NET 10..." -ForegroundColor Yellow
|
||||
|
||||
# Revert all .csproj files from net11.0 to net10.0
|
||||
$files = Get-ChildItem -Path . -Filter "*.csproj" -Recurse | Where-Object {
|
||||
$_.FullName -notlike "*\obj\*" -and $_.FullName -notlike "*\bin\*"
|
||||
}
|
||||
|
||||
foreach ($file in $files) {
|
||||
$content = Get-Content $file.FullName -Raw
|
||||
if ($content -match '<TargetFramework>net11\.0</TargetFramework>') {
|
||||
$content = $content -replace '<TargetFramework>net11\.0</TargetFramework>', '<TargetFramework>net10.0</TargetFramework>'
|
||||
Set-Content -Path $file.FullName -Value $content -NoNewline
|
||||
Write-Host "Reverted: $($file.Name)" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`nUpdating Directory.Packages.props..." -ForegroundColor Yellow
|
||||
|
||||
# Read Directory.Packages.props
|
||||
$packagesFile = "Directory.Packages.props"
|
||||
$content = Get-Content $packagesFile -Raw
|
||||
|
||||
# Revert Microsoft packages
|
||||
$content = $content -replace 'Microsoft\.AspNetCore\.Authorization" Version="11\.0\.1"', 'Microsoft.AspNetCore.Authorization" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.AspNetCore\.Mvc\.Testing" Version="11\.0\.1"', 'Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.Data\.Sqlite" Version="11\.0\.1"', 'Microsoft.Data.Sqlite" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.EntityFrameworkCore" Version="11\.0\.1"', 'Microsoft.EntityFrameworkCore" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.EntityFrameworkCore\.Design" Version="11\.0\.1"', 'Microsoft.EntityFrameworkCore.Design" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.EntityFrameworkCore\.Relational" Version="11\.0\.1"', 'Microsoft.EntityFrameworkCore.Relational" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.EntityFrameworkCore\.Sqlite" Version="11\.0\.1"', 'Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.EntityFrameworkCore\.Tools" Version="11\.0\.1"', 'Microsoft.EntityFrameworkCore.Tools" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.Extensions\.Caching\.Abstractions" Version="11\.0\.1"', 'Microsoft.Extensions.Caching.Abstractions" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.Extensions\.Caching\.Memory" Version="11\.0\.1"', 'Microsoft.Extensions.Caching.Memory" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.Extensions\.Configuration\.Abstractions" Version="11\.0\.1"', 'Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.Extensions\.Configuration\.Binder" Version="11\.0\.1"', 'Microsoft.Extensions.Configuration.Binder" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.Extensions\.DependencyInjection" Version="11\.0\.1"', 'Microsoft.Extensions.DependencyInjection" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.Extensions\.Diagnostics\.HealthChecks\.EntityFrameworkCore" Version="11\.0\.1"', 'Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.Extensions\.Hosting\.Abstractions" Version="11\.0\.1"', 'Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.Extensions\.Http" Version="11\.0\.1"', 'Microsoft.Extensions.Http" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.Extensions\.Logging" Version="11\.0\.1"', 'Microsoft.Extensions.Logging" Version="10.0.3"'
|
||||
$content = $content -replace 'Microsoft\.Extensions\.Options" Version="11\.0\.1"', 'Microsoft.Extensions.Options" Version="10.0.3"'
|
||||
|
||||
# Revert Serilog packages
|
||||
$content = $content -replace 'Serilog\.AspNetCore" Version="11\.0\.1"', 'Serilog.AspNetCore" Version="10.0.0"'
|
||||
$content = $content -replace 'Serilog\.Settings\.Configuration" Version="11\.0\.1"', 'Serilog.Settings.Configuration" Version="10.0.0"'
|
||||
|
||||
# Revert System packages
|
||||
$content = $content -replace 'System\.Text\.Json" Version="11\.0\.1"', 'System.Text.Json" Version="10.0.3"'
|
||||
|
||||
# Revert Npgsql with comment
|
||||
$content = $content -replace 'Npgsql\.EntityFrameworkCore\.PostgreSQL" Version="11\.0\.0-preview\.1"',
|
||||
'Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />' + "`n <!-- Note: Using 9.0.2 with EF Core 10 - version constraint warnings expected"
|
||||
|
||||
Set-Content -Path $packagesFile -Value $content -NoNewline
|
||||
|
||||
Write-Host "Updated Directory.Packages.props" -ForegroundColor Green
|
||||
|
||||
Write-Host "`nRollback complete!" -ForegroundColor Cyan
|
||||
Write-Host "Run these commands to restore and build:" -ForegroundColor Cyan
|
||||
Write-Host " dotnet restore Jellyfin.sln" -ForegroundColor White
|
||||
Write-Host " dotnet build Jellyfin.sln /p:TreatWarningsAsErrors=false" -ForegroundColor White
|
||||
@@ -0,0 +1,53 @@
|
||||
import requests
|
||||
|
||||
# Define connection details
|
||||
server_url = 'http://localhost:8096'
|
||||
username = 'admin'
|
||||
password = 'Optimus0329'
|
||||
|
||||
backup_options = {
|
||||
"Database": True, # Include database
|
||||
"Config": True, # Include config files
|
||||
"Data": True, # Include data files
|
||||
"Root": True, # Include root folder
|
||||
"MediaFiles": False # Exclude media files
|
||||
}
|
||||
|
||||
# Build json payload with auth data
|
||||
auth_data = {
|
||||
'username': username,
|
||||
'Pw': password
|
||||
}
|
||||
|
||||
headers = {}
|
||||
|
||||
# Build required connection headers
|
||||
authorization = 'MediaBrowser Client="other", Device="my-script", DeviceId="some-unique-id", Version="0.0.0"'
|
||||
|
||||
headers['Authorization'] = authorization
|
||||
|
||||
# Authenticate to server
|
||||
r = requests.post(server_url + '/Users/AuthenticateByName', headers=headers, json=auth_data)
|
||||
|
||||
# Retrieve auth token and user id from returned data
|
||||
token = r.json().get('AccessToken')
|
||||
user_id = r.json().get('User').get('Id')
|
||||
|
||||
# Update the headers to include the auth token
|
||||
headers['Authorization'] = f'{authorization}, Token="{token}"'
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
# Requests can be made with
|
||||
#requests.get(f'{server_url}/api/endpoint', headers=headers)
|
||||
|
||||
# Create a backup (empty body is OK, will use default options)
|
||||
print("Creating backup...")
|
||||
response = requests.post(f'{server_url}/Backup/Create', headers=headers, json={})
|
||||
print(f"Status: {response.status_code}")
|
||||
print(response.text)
|
||||
|
||||
# List all backups
|
||||
print("\nListing backups...")
|
||||
response = requests.get(f'{server_url}/Backup', headers=headers)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(response.text)
|
||||
@@ -0,0 +1,82 @@
|
||||
# PostgreSQL Migration Verification Script
|
||||
Write-Host "=== Jellyfin PostgreSQL Migration Verification ===" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Check if Docker is available
|
||||
$dockerAvailable = $false
|
||||
try {
|
||||
$null = docker --version 2>&1
|
||||
$dockerAvailable = $true
|
||||
Write-Host "Docker is available" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "Docker is not available" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "--- Step 1: Check Migration Files ---" -ForegroundColor Cyan
|
||||
|
||||
$migrationPath = "src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres\Migrations"
|
||||
if (Test-Path $migrationPath) {
|
||||
$migrations = Get-ChildItem -Path $migrationPath -Filter "*.cs" -Exclude "*Designer.cs", "*Snapshot.cs", "*Factory.cs"
|
||||
Write-Host "Found $($migrations.Count) migration(s):" -ForegroundColor Green
|
||||
foreach ($migration in $migrations) {
|
||||
Write-Host " - $($migration.Name)" -ForegroundColor Gray
|
||||
}
|
||||
} else {
|
||||
Write-Host "Migration path not found" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "--- Step 2: Build PostgreSQL Database Provider ---" -ForegroundColor Cyan
|
||||
|
||||
Push-Location "src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres"
|
||||
try {
|
||||
dotnet build --configuration Release
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "PostgreSQL provider built successfully" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Build failed" -ForegroundColor Red
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "--- Step 3: List Migrations ---" -ForegroundColor Cyan
|
||||
|
||||
Push-Location "src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres"
|
||||
try {
|
||||
Write-Host "Running: dotnet ef migrations list" -ForegroundColor Gray
|
||||
dotnet ef migrations list
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "--- Step 4: Generate SQL Script ---" -ForegroundColor Cyan
|
||||
|
||||
Push-Location "src\Jellyfin.Database\Jellyfin.Database.Providers.Postgres"
|
||||
try {
|
||||
$sqlOutputPath = "..\..\..\migration-script.sql"
|
||||
Write-Host "Generating SQL script..." -ForegroundColor Gray
|
||||
dotnet ef migrations script --output $sqlOutputPath --idempotent
|
||||
|
||||
if (Test-Path $sqlOutputPath) {
|
||||
$sqlContent = Get-Content $sqlOutputPath -Raw
|
||||
$lineCount = ($sqlContent -split "`n").Count
|
||||
Write-Host "SQL script generated successfully ($lineCount lines)" -ForegroundColor Green
|
||||
Write-Host "Location: $((Resolve-Path $sqlOutputPath).Path)" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host "Failed to generate SQL script" -ForegroundColor Red
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Error generating SQL script: $_" -ForegroundColor Red
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Verification Complete ===" -ForegroundColor Cyan
|
||||
Reference in New Issue
Block a user