[Windows] Check PMS state in more places

While it probably won't address #184 since the batch script works without issue,
but I was unable to replicate this on my system, so this is at least an
intermediate step. Also do some refactoring around the export process, and don't
remove temp files in scripted mode if the last operation failed.
This commit is contained in:
danrahn 2025-04-26 10:53:17 -07:00
parent 19b9b5bf6f
commit 82f1575293
2 changed files with 108 additions and 77 deletions

View File

@ -3,7 +3,7 @@
# # # #
######################################################################### #########################################################################
$PlexDBRepairVersion = 'v1.00.01' $PlexDBRepairVersion = 'v1.00.02'
class PlexDBRepair { class PlexDBRepair {
[PlexDBRepairOptions] $Options [PlexDBRepairOptions] $Options
@ -14,10 +14,12 @@ class PlexDBRepair {
[string] $Timestamp # Timestamp used for temporary database files [string] $Timestamp # Timestamp used for temporary database files
[string] $LogFile # Path of our log file [string] $LogFile # Path of our log file
[string] $Version # Current script version [string] $Version # Current script version
[bool] $IsError # Whether we're currently in an error state
PlexDBRepair($Arguments, $Version) { PlexDBRepair($Arguments, $Version) {
$this.Options = [PlexDBRepairOptions]::new() $this.Options = [PlexDBRepairOptions]::new()
$this.Version = $Version $this.Version = $Version
$this.IsError = $false
$Commands = $this.PreprocessArgs($Arguments) $Commands = $this.PreprocessArgs($Arguments)
if ($null -eq $Commands) { if ($null -eq $Commands) {
return return
@ -187,7 +189,9 @@ class PlexDBRepair {
switch -Regex ($Choice) { switch -Regex ($Choice) {
"^(1|stop)$" { $this.DoStop() } "^(1|stop)$" { $this.DoStop() }
"^(2|autom?a?t?i?c?)$" { $this.RunAutomaticDatabaseMaintenance() } "^(2|autom?a?t?i?c?)$" {
$this.IsError = !$this.RunAutomaticDatabaseMaintenance()
}
"^(7|start?)$" { $this.StartPMS() } "^(7|start?)$" { $this.StartPMS() }
"^(21|(prune?|remov?e?))$" { $this.PrunePhotoTranscoderCache() } "^(21|(prune?|remov?e?))$" { $this.PrunePhotoTranscoderCache() }
"^(42|ignor?e?|honor?)$" { "^(42|ignor?e?|honor?)$" {
@ -213,6 +217,14 @@ class PlexDBRepair {
return return
} }
# If our last DB operation failed, we don't want to automatically delete
# temporary files when in scripted mode.
if ($this.IsError -and $this.Options.Scripted) {
$this.Output("Exiting with errors. Keeping all temporary work files.")
$this.WriteLog("Exit - Retain temp files.")
return
}
$this.CleanDBTemp(!$this.Options.Scripted) $this.CleanDBTemp(!$this.Options.Scripted)
$this.WriteEnd() $this.WriteEnd()
return return
@ -242,13 +254,12 @@ class PlexDBRepair {
[void] DoStop() { [void] DoStop() {
$this.WriteLog("Stop - START") $this.WriteLog("Stop - START")
$PMS = $this.GetPMS() $PMS = $this.GetPMS()
if ($PMS) { if ($null -eq $PMS) {
$this.Output("Stopping PMS.")
} else {
$this.Output("PMS already stopped.") $this.Output("PMS already stopped.")
return return
} }
$this.Output("Stopping PMS.")
# Plex doesn't respond to CloseMainWindow because it doesn't have a window, # Plex doesn't respond to CloseMainWindow because it doesn't have a window,
# and Stop-Process does a forced exit of the process, so use taskkill to ask # and Stop-Process does a forced exit of the process, so use taskkill to ask
@ -265,10 +276,11 @@ class PlexDBRepair {
if ($PMS.HasExited) { if ($PMS.HasExited) {
$this.WriteLog("Stop - PASS") $this.WriteLog("Stop - PASS")
$this.Output("Stopped PMS.") $this.Output("Stopped PMS.")
} else { return
$this.OutputWarn("Could not stop PMS. PMS did not shutdown within 30 second limit.")
$this.WriteLog("Stop - FAIL (Timeout)")
} }
$this.OutputWarn("Could not stop PMS. PMS did not shutdown within 30 second limit.")
$this.WriteLog("Stop - FAIL (Timeout)")
} }
# Start Plex Media Server if it isn't already running # Start Plex Media Server if it isn't already running
@ -293,14 +305,14 @@ class PlexDBRepair {
} }
# All-in-one database utility - Repair/Check/Reindex # All-in-one database utility - Repair/Check/Reindex
[void] RunAutomaticDatabaseMaintenance() { [bool] RunAutomaticDatabaseMaintenance() {
$this.Output("Automatic Check,Repair,Index started.") $this.Output("Automatic Check,Repair,Index started.")
$this.WriteLog("Auto - START") $this.WriteLog("Auto - START")
if ($this.PMSRunning()) { if ($this.PMSRunning()) {
$this.WriteLog("Auto - FAIL - PMS running") $this.WriteLog("Auto - FAIL - PMS running")
$this.OutputWarn("Unable to run automatic sequence. PMS is running. Please stop PlexMediaServer.") $this.OutputWarn("Unable to run automatic sequence. PMS is running. Please stop PlexMediaServer.")
return return $false
} }
# Create temporary backup directory # Create temporary backup directory
@ -310,7 +322,7 @@ class PlexDBRepair {
New-Item -Path $DBTemp -ItemType "directory" -ErrorVariable tempDirError *>$null New-Item -Path $DBTemp -ItemType "directory" -ErrorVariable tempDirError *>$null
if ($TempDirError) { if ($TempDirError) {
$this.ExitDBMaintenance("Unable to create temporary database directory", $false) $this.ExitDBMaintenance("Unable to create temporary database directory", $false)
return return $false
} }
} }
@ -320,10 +332,10 @@ class PlexDBRepair {
$MainDBSQL = Join-Path $DBTemp -ChildPath "library.sql_$($this.TimeStamp)" $MainDBSQL = Join-Path $DBTemp -ChildPath "library.sql_$($this.TimeStamp)"
if (!$this.FileExists($MainDB)) { if (!$this.FileExists($MainDB)) {
$this.ExitDBMaintenance("Could not find $MainDBName in database directory", $false) $this.ExitDBMaintenance("Could not find $MainDBName in database directory", $false)
return return $false
} }
if (!$this.ExportPlexDB($MainDB, $MainDBSQL)) { return } if (!$this.ExportPlexDB($MainDB, $MainDBSQL)) { return $false }
$this.Output("Exporting Blobs DB") $this.Output("Exporting Blobs DB")
$BlobsDBName = "com.plexapp.plugins.library.blobs.db" $BlobsDBName = "com.plexapp.plugins.library.blobs.db"
@ -331,56 +343,86 @@ class PlexDBRepair {
$BlobsDBSQL = Join-Path $DBTemp -ChildPath "blobs.sql_$($this.Timestamp)" $BlobsDBSQL = Join-Path $DBTemp -ChildPath "blobs.sql_$($this.Timestamp)"
if (!$this.FileExists($BlobsDB)) { if (!$this.FileExists($BlobsDB)) {
$this.ExitDBMaintenance("Could not find $BlobsDBName in database directory", $false) $this.ExitDBMaintenance("Could not find $BlobsDBName in database directory", $false)
return return $false
} }
if (!$this.ExportPlexDB($BlobsDB, $BlobsDBSQL)) { return } if (!$this.ExportPlexDB($BlobsDB, $BlobsDBSQL)) { return $false }
$this.Output("Successfully exported the main and blobs databases. Proceeding to import into new database.") $this.Output("Successfully exported the main and blobs databases. Proceeding to import into new database.")
$this.WriteLog("Repair - Export databases - PASS") $this.WriteLog("Repair - Export databases - PASS")
# Make sure Plex hasn't been started while we were exporting
if (!$this.CheckPMS("Auto ", "export")) { return $false }
$this.Output("Importing Main DB.") $this.Output("Importing Main DB.")
$MainDBImport = Join-Path $this.PlexDBDir -ChildPath "${MainDBName}_$($this.Timestamp)" $MainDBImport = Join-Path $this.PlexDBDir -ChildPath "${MainDBName}_$($this.Timestamp)"
if (!$this.ImportPlexDB($MainDBSQL, $MainDBImport)) { return } if (!$this.ImportPlexDB($MainDBSQL, $MainDBImport)) { return $false }
$this.Output("Creating Blobs DB") $this.Output("Importing Blobs DB.")
$BlobsDBImport = Join-Path $this.PlexDBDir -ChildPath "${BlobsDBName}_$($this.Timestamp)" $BlobsDBImport = Join-Path $this.PlexDBDir -ChildPath "${BlobsDBName}_$($this.Timestamp)"
if (!$this.ImportPlexDB($BlobsDBSQL, $BlobsDBImport)) { return } if (!$this.ImportPlexDB($BlobsDBSQL, $BlobsDBImport)) { return $false }
$this.Output("Successfully imported databases.") $this.Output("Successfully imported databases.")
$this.WriteLog("Repair - Import - PASS") $this.WriteLog("Repair - Import - PASS")
$this.Output("Verifying databases integrity after importing.") $this.Output("Verifying databases integrity after importing.")
if (!$this.IntegrityCheck($MainDBImport, "Main")) { return } if (!$this.IntegrityCheck($MainDBImport, "Main")) { return $false }
$this.Output("Verification complete. PMS main database is OK.") $this.Output("Verification complete. PMS main database is OK.")
$this.WriteLog("Repair - Verify main database - PASS") $this.WriteLog("Repair - Verify main database - PASS")
if (!$this.IntegrityCheck($BlobsDBImport, "Blobs")) { return } if (!$this.IntegrityCheck($BlobsDBImport, "Blobs")) { return $false }
$this.Output("Verification complete. PMS blobs database is OK.") $this.Output("Verification complete. PMS blobs database is OK.")
$this.WriteLog("Repair - Verify blobs database - PASS") $this.WriteLog("Repair - Verify blobs database - PASS")
if (!$this.CheckPMS("Auto ", "import")) { return $false }
# Import complete, now reindex # Import complete, now reindex
$this.WriteOutputLog("Reindexing Main DB") $this.WriteOutputLog("Reindexing Main DB")
if (!$this.RunSQLCommand("""$MainDBImport"" ""REINDEX;""", "Failed to reindex Main DB")) { return } if (!$this.RunSQLCommand("""$MainDBImport"" ""REINDEX;""", "Failed to reindex Main DB")) { return $false }
$this.WriteOutputLog("Reindexing Blobs DB") $this.WriteOutputLog("Reindexing Blobs DB")
if (!$this.RunSQLCommand("""$BlobsDBImport"" ""REINDEX;""", "Failed to reindex Blobs DB")) { return } if (!$this.RunSQLCommand("""$BlobsDBImport"" ""REINDEX;""", "Failed to reindex Blobs DB")) { return $false }
$this.WriteOutputLog("Reindexing complete.") $this.WriteOutputLog("Reindexing complete.")
$this.WriteOutputLog("Moving current DBs to DBTMP and making new databases active") $this.WriteOutputLog("Moving current DBs to DBTMP and making new databases active")
if (!$this.CheckPMS("Auto ", "new database copy")) { return $false }
$MoveError = $null try {
Move-Item -Path $MainDB -Destination (Join-Path $DBTemp -ChildPath "${MainDBName}_$($this.TimeStamp)") -ErrorVariable moveError *>$null $this.MoveDatabase($MainDB, (Join-Path $DBTemp -ChildPath "${MainDBName}_$($this.Timestamp)"), "move Main DB to DBTMP")
if ($MoveError) { $this.ExitDBMaintenance("Unable to move Main DB to DBTMP: $MoveError", $false); return } $this.MoveDatabase($MainDBImport, $MainDB, "replace Main DB with rebuilt DB")
Move-Item -Path $MainDBImport -Destination $MainDB -ErrorVariable moveError *>$null
if ($MoveError) { $this.ExitDBMaintenance("Unable to replace Main DB with rebuilt DB: $MoveError", $false); return } $this.MoveDatabase($BlobsDB, (Join-Path $DBTemp -ChildPath "${BlobsDBName}_$($this.Timestamp)"), "move Blobs DB to DBTMP")
$this.MoveDatabase($BlobsDBImport, $BlobsDB, "replace Blobs DB with rebuilt DB")
Move-Item -Path $BlobsDB -Destination (Join-Path $DBTemp -ChildPath "${BlobsDBName}_$($this.TimeStamp)") -ErrorVariable moveError *>$null } catch {
if ($MoveError) { $this.ExitDBMaintenance("Unable to move Blobs DB to DBTMP: $MoveError", $false) } $Error.Clear()
Move-Item -Path $BlobsDBImport -Destination $BlobsDB -ErrorVariable moveError *>$null return $false
if ($MoveError) { $this.ExitDBMaintenance("Unable to replace Blobs DB with rebuilt DB: $MoveError", $false); return } }
$this.ExitDBMaintenance("Database repair/rebuild/reindex completed.", $true) $this.ExitDBMaintenance("Database repair/rebuild/reindex completed.", $true)
return $true
}
# Return whether we can continue DB repair (i.e. whether PMS is running) at the given stage in the process.
[bool] CheckPMS([string] $Stage, [string] $SubStage) {
if ($this.PMSRunning()) {
$SubMessage = if ($SubStage) { "during $SubStage" } else { "" }
$this.WriteLog("$Stage - FAIL - PMS running $SubMessage")
$this.OutputWarn("Unable to run $Stage. PMS is running. Please stop PlexMediaServer.")
return $false
}
return $true
}
# Try to move the source file to the destination. If it fails, attempt to find
# open file handles (requires handle.exe on PATH) and throw.
[void] MoveDatabase([string] $Source, [string] $Destination, [string] $FriendlyString) {
$MoveError = $null
Move-Item -Path $Source -Destination $Destination -ErrorVariable MoveError *>$null
if ($MoveError) {
$this.ExitDBMaintenance("Unable to $($FriendlyString): $MoveError", $false)
throw "Unable to move database"
}
} }
# Attempts to prune PhotoTranscoder images that are older than the specified date cutoff (30 days by default) # Attempts to prune PhotoTranscoder images that are older than the specified date cutoff (30 days by default)
@ -639,6 +681,10 @@ class PlexDBRepair {
} }
} }
[bool] ExportPlexDB([string] $Source, [string] $Destination) {
return $this.RunSQLCommand("""$Source"" "".output '$Destination'"" .dump", "Failed to export '$Source' to '$Destination'")
}
# Run an SQL command. # Run an SQL command.
# ErrorMessage is the message to output/write to the log on failure # ErrorMessage is the message to output/write to the log on failure
[bool] RunSQLCommand([string] $Command, [string] $ErrorMessage) { [bool] RunSQLCommand([string] $Command, [string] $ErrorMessage) {
@ -689,35 +735,15 @@ class PlexDBRepair {
return $true return $true
} }
[bool] ExportPlexDB([string] $Source, [string] $Destination) {
$SqlError = $null
$Command = """$Source"" .dump | Set-Content ""$Destination"" -Encoding utf8"
try {
Invoke-Expression "& ""$($this.PlexSQL)"" $Command" -ev SqlError -EA Stop *>$null
} catch {
$Err = $Error -join "`n"
# Even if we ignore errors, we can't continue if the export failed.
$this.ExitDBMaintenance("Failed to export '$Source' to '$Destination': '$Err'", $false)
$Error.Clear()
return $false
}
if ($SqlError) {
$Err = $SqlError -join "`n"
if ($this.Options.IgnoreErrors) {
$this.OutputWarn("Ignoring database errors during export: $Err")
} else {
$this.ExitDBMaintenance("Failed to export '$Source' to '$Destination': '$Err'", $false)
return $false
}
}
return $true
}
# Import an exported .sql file into a new database # Import an exported .sql file into a new database
[bool] ImportPlexDB($Source, $Destination) { [bool] ImportPlexDB($Source, $Destination) {
# SQLite's .read can't handle files larger than 2GB on versions <3.45.0 (https://sqlite.org/forum/forumpost/9af57ba66fbb5349),
# and Plex SQLite is currently on 3.39.4 (as of PMS 1.41.6).
# If the source is smaller than 2GB we can .read it directly, otherwise do things in a more roundabout way.
if ($this.FileExists($Source) -and (Get-Item $Source).Length -lt 2GB) {
return $this.RunSQLCommand("""$Destination"" "".read '$Source'""", "Failed to import Plex database (importing '$Source' into '$Destination')")
}
$ImportError = $null $ImportError = $null
$ExitCode = 0 $ExitCode = 0
$Err = $null $Err = $null
@ -791,7 +817,7 @@ class PlexDBRepair {
# Return whether PMS is running # Return whether PMS is running
[bool] PMSRunning() { [bool] PMSRunning() {
return !!$this.GetPMS() return $null -ne $this.GetPMS()
} }
# Retrieve the PMS process, if running # Retrieve the PMS process, if running

View File

@ -1,17 +1,22 @@
# PlexDBRepair-Windows # PlexDBRepair-Windows
Release notes for the Windows counterpart to DBRepair.sh (DBRepair-Windows.ps1) Release notes for the Windows counterpart to DBRepair.sh (DBRepair-Windows.ps1)
# Release Info # Release Info
v1.00.01 v1.00.02
- Bug Fix: Ensure UTF-8 characters get exported/imported properly. - Check whether PMS is running at more points in the process.
- Don't remove temp files in scripted mode if the last operation failed.
v1.00.00 - Better export process that improves upon the UTF-8 fix in v1.00.01.
- Initial Windows PowerShell script release, aiming to provide a similar experience as DBRepair.sh, with command-name-based input.
- Initial command support: v1.00.01
- AUTO(matic) - Bug Fix: Ensure UTF-8 characters get exported/imported properly.
- EXIT
- PRUN(e) v1.00.00
- STAR(t) - Initial Windows PowerShell script release, aiming to provide a similar experience as DBRepair.sh, with command-name-based input.
- STOP - Initial command support:
- AUTO(matic)
- EXIT
- PRUN(e)
- STAR(t)
- STOP