Merge pull request #154 from danrahn/master

Interactive Windows Script via PowerShell
This commit is contained in:
Chuck 2024-05-12 10:07:42 -04:00 committed by GitHub
commit f2ec7d2ea9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 890 additions and 0 deletions

View File

@ -10,6 +10,12 @@ REM This is stable working software but not "Released" software. Development wi
setlocal enabledelayedexpansion setlocal enabledelayedexpansion
echo.
echo NOTE: This script is being replaced with the PowerShell script DBRepair-Windows.ps1,
echo which aims to better emulate DBRepair.sh (more options, interative mode, etc).
echo Consider moving over to the new script.
echo.
REM ### Create Timestamp REM ### Create Timestamp
set Hour=%time:~0,2% set Hour=%time:~0,2%
set Min=%time:~3,2% set Min=%time:~3,2%

788
DBRepair-Windows.ps1 Normal file
View File

@ -0,0 +1,788 @@
#########################################################################
# Plex Media Server database check and repair utility script. #
# #
#########################################################################
$PlexDBRepairVersion = 'v1.00.00'
class PlexDBRepair {
[PlexDBRepairOptions] $Options
[string] $PlexDBDir # Path to Plex's Databases directory
[string] $PlexCache # Path to the PhotoTranscoder directory
[string] $PlexSQL # Path to 'Plex SQLite.exe'
[string] $Timestamp # Timestamp used for temporary database files
[string] $LogFile # Path of our log file
[string] $Version # Current script version
PlexDBRepair($Arguments, $Version) {
$this.Options = [PlexDBRepairOptions]::new()
$this.Version = $Version
$Commands = $this.PreprocessArgs($Arguments)
if ($null -eq $Commands) {
return
}
if (!$this.Init()) {
Write-Host "Unable to initialize script, cannot continue."
return
}
$this.PrintHeader($true)
$this.MainLoop($Commands)
}
[void] PrintHeader([boolean] $WriteToLog) {
$OS = [System.Environment]::OSVersion.Version
if ($WriteToLog) {
$this.WriteLog("============================================================")
$this.WriteLog("Session start: Host is Windows $($OS.Major) (Build $($OS.Build))")
}
Write-Host "`n"
Write-Host " Plex Media Server Database Repair Utility (Windows $($OS.Major), Build $($OS.Build))"
Write-Host " Version $($this.Version) "
Write-Host
}
[void] PrintHelp() {
# -Help doesn't write to the log, since our log file path isn't set.
$this.PrintHeader($false)
Write-Host "When run without arguments, starts an interactive session that displays available options"
Write-Host "and lets you select the operations you want to perform. Or, to run tasks automatically,"
Write-Host "provide them directly to the script, e.g. '.\DBRepair-Windows.ps1 Stop Prune Start Exit'"
Write-Host
$this.PrintOptions("Main Options")
Write-Host
Write-Host "Extra Options - These can only be specified once (last one wins)"
Write-Host
Write-Host " -CacheAge [int] - The date cutoff for pruned images. Defaults to pruning images over 30"
Write-Host " days old."
Write-Host
}
[void] PrintMenu() {
$this.PrintOptions("Select")
}
[void] PrintOptions([string]$Header) {
# NOTE: While Windows only supports a subset of DBRepair.sh's features, keep the command
# numbers the same as we attempt to reach feature parity
Write-Host
Write-Host $Header
Write-Host
Write-Host " 1 - 'stop' - Stop PMS."
Write-Host " 2 - 'automatic' - Check, Repair/Optimize, and Reindex Database in one step."
Write-Host
Write-Host " 7 - 'start' - Start PMS"
Write-Host
Write-Host " 21 - 'prune' - Prune (remove) old image files (jpeg,jpg,png) from PhotoTranscoder cache."
Write-Host
Write-Host " 99 - 'quit' - Quit immediately. Keep all temporary files."
Write-Host " 'exit' - Exit with cleanup options."
Write-Host
Write-Host " 'menu x' - Show this menu in interactive mode, where x is on/off/yes/no"
}
# Do initial parsing of arguments that aren't part of the loop, returning
# the list of arguments that _should_ be processed in the loop.
#
# E.g. given "Stop Prune CacheAge 20 Start", this function will set the CacheAge
# to 20, and return "Stop Prune Start"
[System.Collections.ArrayList] PreprocessArgs([string[]] $Arguments) {
$FinalArgs = [System.Collections.ArrayList]::new()
for ($i = 0; $i -lt $Arguments.Count; ++$i) {
switch -Regex ($Arguments[$i]) {
'^-?(H(elp)?|\?)$' {
if ($Arguments.Count -gt 1) {
Write-Warning "Found -Help, ignoring extra arguments"
}
$this.PrintHelp()
return $null
}
'^-?CacheAge$' {
if ($i -eq $Arguments.Count - 1) {
Write-Warning "Found -CacheAge argument, but no value. Using default of 30 days"
Break
}
++$i
$Age = $Arguments[$i]
if (!($Age -match "^\d+$")) {
Write-Warning "Invalid -CacheAge value '$Age'. Using default of 30 days"
Break
}
$this.Options.CacheAge = [int]$Age
}
Default { $FinalArgs.Add($_) }
}
}
return $FinalArgs
}
# Setup variables required for this utility to work.
[bool] Init() {
$this.Timestamp = Get-Date -Format HH-mm-ss
$AppData = $this.GetAppDataDir()
$Success = $this.GetPlexDBDir($AppData) -and $this.GetPlexSQL() -and $this.GetPhotoTranscoderDir($AppData)
if ($Success) {
$this.LogFile = Join-Path $this.PlexDBDir -ChildPath "PlexDBRepair.log"
}
return $Success
}
# Core routine that loops over all provided commands and executes them in order.
[void] MainLoop([System.Collections.ArrayList] $Arguments) {
$this.Options.Scripted = $Arguments.Count -ne 0
$i = 0
$Argc = $Arguments.Count
$NullInput = 0
$EOFExit = $false
while ($true) {
$Choice = $null
if ($this.Options.Scripted) {
if ($i -eq $Argc) {
$Choice = "exit"
} else {
$Choice = $Arguments[$i++]
}
} else {
if ($this.Options.ShowMenu) {
$this.PrintMenu()
}
Write-Host
$Choice = Read-Host "Enter command # -or- command name (4 char min)"
if ($Choice -eq "") {
++$NullInput
if ($NullInput -eq 5) {
$this.Output("Unexpected EOF / End of command line options. Exiting. Keeping temp files. ")
$Choice = "exit"
$EOFExit = $true
} else {
if ($NullInput -eq 4) {
Write-Warning "Next empty command exits as EOF. "
}
continue
}
} else {
$NullInput = 0
}
}
# Update timestamp
$this.Timestamp = Get-Date -Format 'yyyy-MM-dd_HH.mm.ss'
switch -Regex ($Choice) {
"^(1|stop)$" { $this.DoStop() }
"^(2|autom?a?t?i?c?)$" { $this.RunAutomaticDatabaseMaintenance() }
"^(7|start?)$" { $this.StartPMS() }
"^(21|(prune?|remov?e?))$" { $this.PrunePhotoTranscoderCache() }
"^(99|quit)$" {
$this.Output("Retaining all remporary work files.")
$this.WriteLog("Exit - Retain temp files.")
$this.WriteEnd()
return
}
"^exit$" {
if ($EOFExit) {
$this.Output("Unexpected exit command. Keeping all temporary work files.")
$this.WriteLog("EOFExit - Retain temp files.")
return
}
$this.CleanDBTemp(!$this.Options.Scripted)
$this.WriteEnd()
return
}
"^menu\b" {
$Match = $Choice -match "^menu\s+(on|off|yes|no)"
if (!$Match) {
$this.OutputWarn("Invalid 'menu' format. Expected 'menu on/off/yes/no', got '$Choice'")
Break
}
$TurnOn = ($Matches.1 -eq 'on') -or ($Matches.1 -eq 'yes');
$this.Options.ShowMenu = $TurnOn
if (!$TurnOn) {
Write-Host "Menu off: Reenable with 'menu on' command"
}
}
Default {
$this.OutputWarn("Unknown Command: '$Choice'")
$this.WriteLog("Unknown command: '$Choice'")
}
}
}
}
# Attempt to stop Plex Media Server if it's running
[void] DoStop() {
$this.WriteLog("Stop - START")
$PMS = $this.GetPMS()
if ($PMS) {
$this.Output("Stopping PMS.")
} else {
$this.Output("PMS already stopped.")
return
}
# 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
# PMS to close nicely, and bail if that doesn't work.
$ErrorText = $null
Invoke-Expression "taskkill /im ""Plex Media Server.exe""" 2>$null -ErrorVariable ErrorText
if ($ErrorText) {
$this.WriteOutputLogWarn("Failed to send terminate signal to PMS, please stop manually.")
$this.WriteOutputLogWarn($ErrorText -join "`n")
return
}
$PMS.WaitForExit(30000) *>$null # Wait at most 30 seconds for PMS to close. If it still hasn't by then, bail.
if ($PMS.HasExited) {
$this.WriteLog("Stop - PASS")
$this.Output("Stopped PMS.")
} else {
$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
[void] StartPMS() {
$this.WriteLog("Start - START")
if ($this.PMSRunning()) {
$this.Output("Start not needed. PMS is running.")
$this.WriteLog("Start - PASS - PMS is already running")
return
}
$PMS = Join-Path (Split-Path -Parent $this.PlexSQL) -ChildPath "Plex Media Server.exe"
try {
Start-Process $PMS -EA Stop
$this.Output("Started PMS")
$this.WriteLog("Start - PASS")
} catch {
$Err = $Error -join "`n"
$this.OutputWarn("Could not start PMS: $Err")
$Error.Clear()
}
}
# All-in-one database utility - Repair/Check/Reindex
[void] RunAutomaticDatabaseMaintenance() {
$this.Output("Automatic Check,Repair,Index started.")
$this.WriteLog("Auto - START")
if ($this.PMSRunning()) {
$this.WriteLog("Auto - FAIL - PMS running")
$this.OutputWarn("Unable to run automatic sequence. PMS is running. Please stop PlexMediaServer.")
return
}
# Create temporary backup directory
$DBTemp = Join-Path $this.PlexDBDir -ChildPath "dbtmp"
if (!$this.DirExists($DBTemp)) {
$TempDirError = $null
New-Item -Path $DBTemp -ItemType "directory" -ErrorVariable tempDirError *>$null
if ($TempDirError) {
$this.ExitDBMaintenance("Unable to create temporary database directory", $false)
return
}
}
$this.Output("Exporting Main DB")
$MainDBName = "com.plexapp.plugins.library.db"
$MainDB = Join-Path $this.PlexDBDir -ChildPath $MainDBName
$MainDBSQL = Join-Path $DBTemp -ChildPath "library.sql_$($this.TimeStamp)"
if (!$this.FileExists($MainDB)) {
$this.ExitDBMaintenance("Could not find $MainDBName in database directory", $false)
return
}
if (!$this.RunSQLCommand("""$MainDB"" .dump | Set-Content ""$MainDBSQL"" -Encoding utf8", "Failed to export main database")) { return }
$this.Output("Exporting Blobs DB")
$BlobsDBName = "com.plexapp.plugins.library.blobs.db"
$BlobsDB = Join-Path $this.PlexDBDir -ChildPath $BlobsDBName
$BlobsDBSQL = Join-Path $DBTemp -ChildPath "blobs.sql_$($this.Timestamp)"
if (!$this.FileExists($BlobsDB)) {
$this.ExitDBMaintenance("Could not find $BlobsDBName in database directory", $false)
return
}
if (!$this.RunSQLCommand("""$BlobsDB"" .dump | Set-Content ""$BlobsDBSQL"" -Encoding utf8", "Failed to export blobs database")) { return }
$this.Output("Successfully exported the main and blobs databases. Proceeding to import into new database.")
$this.WriteLog("Repair - Export databases - PASS")
$this.Output("Importing Main DB.")
$MainDBImport = Join-Path $this.PlexDBDir -ChildPath "${MainDBName}_$($this.Timestamp)"
if (!$this.ImportPlexDB($MainDBSQL, $MainDBImport)) { return }
$this.Output("Creating Blobs DB")
$BlobsDBImport = Join-Path $this.PlexDBDir -ChildPath "${BlobsDBName}_$($this.Timestamp)"
if (!$this.ImportPlexDB($BlobsDBSQL, $BlobsDBImport)) { return }
$this.Output("Successfully imported databases.")
$this.WriteLog("Repair - Import - PASS")
$this.Output("Verifying databases integrity after importing.")
$VerifyResult = ""
if (!$this.GetSQLCommandResult("""$MainDBImport"" ""PRAGMA integrity_check(1)""", "Failed to verify main DB", [ref]$VerifyResult)) { return }
$this.Output("Main DB verification check is: $VerifyResult")
if ($VerifyResult -ne "ok") {
$this.ExitDBMaintenance("Main DB verification failed: $VerifyResult", $false)
return
}
$this.Output("Verification complete. PMS main database is OK.")
$this.WriteLog("Repair - Verify main database - PASS")
if (!$this.GetSQLCommandResult("""$BlobsDBImport"" ""PRAGMA integrity_check(1)""", "Failed to verify main DB", [ref]$VerifyResult)) { return }
if ($VerifyResult -ne "ok") {
$this.ExitDBMaintenance("Blobs DB verification failed: $VerifyResult", $false)
return
}
$this.Output("Verification complete. PMS blobs database is OK.")
$this.WriteLog("Repair - Verify blobs database - PASS")
# Import complete, now reindex
$this.WriteOutputLog("Reindexing Main DB")
if (!$this.RunSQLCommand("""$MainDBImport"" ""REINDEX;""", "Failed to reindex Main DB")) { return }
$this.WriteOutputLog("Reindexing Blobs DB")
if (!$this.RunSQLCommand("""$BlobsDBImport"" ""REINDEX;""", "Failed to reindex Blobs DB")) { return }
$this.WriteOutputLog("Reindexing complete.")
$this.WriteOutputLog("Moving current DBs to DBTMP and making new databases active")
$MoveError = $null
Move-Item -Path $MainDB -Destination (Join-Path $DBTemp -ChildPath "${MainDBName}_$($this.TimeStamp)") -ErrorVariable moveError *>$null
if ($MoveError) { $this.ExitDBMaintenance("Unable to move Main DB to DBTMP: $MoveError", $false); return }
Move-Item -Path $MainDBImport -Destination $MainDB -ErrorVariable moveError *>$null
if ($MoveError) { $this.ExitDBMaintenance("Unable to replace Main DB with rebuilt DB: $MoveError", $false); return }
Move-Item -Path $BlobsDB -Destination (Join-Path $DBTemp -ChildPath "${BlobsDBName}_$($this.TimeStamp)") -ErrorVariable moveError *>$null
if ($MoveError) { $this.ExitDBMaintenance("Unable to move Blobs DB to DBTMP: $MoveError", $false) }
Move-Item -Path $BlobsDBImport -Destination $BlobsDB -ErrorVariable moveError *>$null
if ($MoveError) { $this.ExitDBMaintenance("Unable to replace Blobs DB with rebuilt DB: $MoveError", $false); return }
$this.ExitDBMaintenance("Database repair/rebuild/reindex completed.", $true)
}
# Attempts to prune PhotoTranscoder images that are older than the specified date cutoff (30 days by default)
[void] PrunePhotoTranscoderCache() {
$this.WriteLog("Prune - START")
if ($this.PMSRunning()) {
$this.OutputWarn("Unable to prune Phototranscoder cache. PMS is running.")
$this.WriteLog("Prune - FAIL - PMS running")
return
}
$Cutoff = $this.Options.CacheAge
$ShouldPrune = $this.Options.Scripted
if (!$ShouldPrune) {
$this.Output("Counting how many files are more than $Cutoff days old")
$CacheResult = $this.CheckPhotoTranscoderCache($true)
$Prunable = $CacheResult.PrunableFiles
$SpaceSaved = $CacheResult.SpaceSavings
if ($Prunable -eq 0) {
$this.Output("No files found to prune.")
$this.WriteLog("Prune - PASS (no files found to prune)")
return
}
$ShouldPrune = $this.GetYesNo("OK to prune $Prunable files ($SpaceSaved)")
}
if ($ShouldPrune) {
$this.Output("Pruning started.")
$PruneResult = $this.CheckPhotoTranscoderCache($false)
$Pruned = $PruneResult.PrunableFiles
$Total = $PruneResult.TotalFiles
$Saved = $PruneResult.SpaceSavings
$this.WriteOutputLog("Prune - Removed $Pruned files over $Cutoff days old ($Saved), out of $Total total files")
$this.Output("Pruning completed.")
} else {
$this.WriteOutputLog("Prune - Prune cancelled by user")
}
$this.WriteLog("Prune - PASS")
}
# Traverses PhotoTranscoder cache to find and delete files older than the specified max age.
# If $DryRun is $true, don't remove items, just gather statistics.
[CleanCacheResult] CheckPhotoTranscoderCache([bool] $DryRun) {
$Cutoff = (Get-Date).AddDays(-$this.Options.CacheAge);
$AllFiles = 0;
$OldFiles = 0;
$FreedBytes = 0;
Get-ChildItem -Path $this.PlexCache -Recurse -File |
Where-Object { $_.extension -in '.jpg','.jpeg','.png','.ppm' } |
ForEach-Object {
$AllFiles++;
if ($_.LastWriteTime -lt $Cutoff) {
$OldFiles++;
$FreedBytes += $_.Length;
if (!$DryRun) {
Remove-Item $_.FullName;
}
}
};
return [CleanCacheResult]::new($AllFiles, $OldFiles, $FreedBytes)
}
### Helpers ###
### Logging Helpers ###
[string] Now() { return Get-Date -Format 'yyyy-MM-dd HH.mm.ss' }
# Write the given text to the console
[void] Output([string] $Text) {
if ($this.Options.Scripted) {
Write-Host "$($this.Now()) $Text"
} else {
Write-Host $Text
}
}
# Write the given text as a warning in the console
[void] OutputWarn([string] $Text) {
if ($this.Options.Scripted) {
Write-Warning "$($this.Now()) $Text"
} else {
Write-Warning $Text
}
}
# Write the given text to the log file
[void] WriteLog([string] $Text) {
Add-Content -Path $this.LogFile -Value "$($this.Now()) -- $($Text)"
}
# Write the given text to the log file and console
[void] WriteOutputLog([string] $Text) {
$this.WriteLog($Text)
$this.Output($Text)
}
# Write the given text to the log file and as warning text in the console
[void] WriteOutputLogWarn([string] $Text) {
$this.WriteLog($Text)
$this.OutputWarn($Text)
}
# Write out the end of the session
[void] WriteEnd() {
$this.WriteLog("Session end. $(Get-Date)")
$this.WriteLog("============================================================")
}
### File Helpers ###
# Check whether the given directory exists (and is a directory)
[bool] DirExists([string] $Dir) {
if ($Dir) {
return Test-Path $Dir -PathType Container
}
return $false
}
# Check whether the given file exists (and is a file)
[bool] FileExists([string] $File) {
if ($File) {
return Test-Path $File -PathType Leaf
}
return $false
}
### Setup Helpers ###
# Retrieve Plex's data directory, exiting the script on falure
[string] GetAppDataDir() {
$PMSRegistry = $this.GetHKCU()
$PlexAppData = $PMSRegistry.LocalAppDataPath
if ($PlexAppData) {
$PlexAppData = Join-Path -Path $PlexAppData -ChildPath "Plex Media Server"
}
if ($this.DirExists($PlexAppData)) {
return $PlexAppData
}
$PlexAppData = "$env:LOCALAPPDATA\Plex Media Server"
if ($this.DirExists($PlexAppData)) {
return $PlexAppData
}
Write-Host "Could not determine Plex data directory, cannot continue"
Write-Host "Normally $env:LOCALAPPDATA\Plex Media Server"
exit
}
# Retrieve PMS settings under HKEY_CURRENT_USER, exiting the script on failure
[PSCustomObject] GetHKCU() {
try {
return (Get-ItemProperty -path 'HKCU:\Software\Plex, Inc.\Plex Media Server' -EA Stop)
} catch {
Write-Warn "Could not find Plex registry settings (HKCU\Software\Plex, Inc.\Plex Media Server). Are you sure Plex is installed on this machine?"
exit
}
}
# Set the Plex database directory, returning whether we found the directory
[bool] GetPlexDBDir([string] $AppData) {
$DBDir = Join-Path -Path $AppData -ChildPath "Plug-in Support\Databases"
if ($this.DirExists($DBDir)) {
$this.PlexDBDir = $DBDir;
return $true;
}
Write-Host "Could not find Databases folder, cannot continue."
Write-Host "Normally $DBDir"
return $false
}
# Set the path to Plex's PhotoTranscoder cache, returning whether we found the directory.
[bool] GetPhotoTranscoderDir([string] $AppData) {
$CacheDir = Join-Path -Path $AppData -ChildPath "Cache\PhotoTranscoder"
if ($this.DirExists($CacheDir)) {
$this.PlexCache = $CacheDir
return $true
}
Write-Host "Could not find PhotoTranscoder path, cannot prune."
Write-Host "Normally $CacheDir"
return $false
}
# Find the path to Plex SQLite.exe, falling back to user input if necessary.
[bool] GetPlexSQL() {
$PMSRegistry = $this.GetHKCU()
$InstallDir = $PMSRegistry.InstallFolder
if (!$InstallDir) {
# Install location might also be in HKLM
$InstallDir = (Get-ItemProperty -path 'HKLM:\SOFTWARE\Plex, Inc.\Plex Media Server' -EA Ignore).InstallFolder
if (!$InstallDir) {
# Final registry attempt - WOW6432Node
$InstallDir = (Get-ItemProperty -path 'HKLM:\SOFTWARE\WOW6432Node\Plex, Inc.\Plex Media Server' -EA Ignore).InstallFolder
}
}
$SQL = if ($InstallDir) { Join-Path -Path $InstallDir -ChildPath "Plex SQLite.exe" } else { $null }
if ($this.FileExists($SQL)) {
$this.PlexSQL = $SQL
return $true
}
# Still couldn't find install directory. Try standard PROGRAMFILES variables
$SQL = "$env:PROGRAMFILES\Plex\Plex Media Server\Plex SQ Lite.exe"
if ($this.FileExists($SQL)) {
$this.PlexSQL = $SQL
return $true
}
if (${env:PROGRAMFILES(X86)}) {
$SQL = "${env:PROGRAMFILES(X86)}\Plex Plex Media Server\Plex SQLite.exe"
if ($this.FileExists($SQL)) {
Write-Host "Note: 32-bit version of PMS detected on a 64-bit version of Windows. Using the 64-bit release of PMS is recommended."
$this.PlexSQL = $SQL
return $true
}
}
Write-Host "Could not determine Plex SQLite location. Please provide it below"
Write-Host "Normally $env:PORGRAMFILES\Plex\Plex Media Server\Plex SQLite.exe"
$First = $true
while (!$this.FileExists($SQL)) {
if (!$First) {
Write-Host "ERROR: '$SQL' could not be found"
}
$First = $false
$SQL = Read-Host -Prompt "Path to Plex SQLite.exe (Ctrl+C to cancel): "
}
$this.PlexSQL = $SQL
return $true
}
### Database Helpers ###
# Writes to output/log when we're done with database maintenance (on success or failure)
[void] ExitDBMaintenance([string] $Message, [boolean] $Success) {
if ($Success) {
$this.Output("Automatic Check,Repair,Index succeeded.")
$this.WriteLog("Auto - PASS")
} else {
$this.OutputWarn("Automatic maintenance failed - $Message")
$this.WriteLog("Auto - $Message, cannot continue.")
$this.WriteLog("Auto - FAIL")
}
}
# Run an SQL command.
# ErrorMessage is the message to output/write to the log on failure
[bool] RunSQLCommand([string] $Command, [string] $ErrorMessage) {
return $this.RunSQLCommandCore($Command, $ErrorMessage, $null)
}
# Run an SQL command and retrieve the output of said command
# ErrorMessage is the message to output/write to the log on failure
[bool] GetSQLCommandResult([string] $Command, [string] $ErrorMessage, [ref]$Output) {
return $this.RunSQLCommandCore($Command, $ErrorMessage, $Output)
}
# Run a 'Plex SQLite' command
[bool] RunSQLCommandCore([string] $Command, [string] $ErrorMessage, [ref] $Output) {
$SqlError = $null
$SqlResult = $null
try {
Invoke-Expression "& ""$($this.PlexSQL)"" $Command" -ev sqlError -OutVariable sqlResult -EA Stop *>$null
} catch {
$Err = $Error -join "`n"
$this.ExitDBMaintenance("Failed to run command '$Command': '$Err'", $false)
$Error.Clear()
return $false
}
if ($SqlError) {
$Err = $SqlError -join "`n"
$Msg = $ErrorMessage
if (!$Msg) {
$Msg = "Plex SQLite operation failed"
}
$this.ExitDBMaintenance("${msg}: $Err", $false)
return $false
}
if ($null -ne $Output.Value) {
$Output.Value = $SqlResult
}
return $true
}
# Import an exported .sql file into a new database
[bool] ImportPlexDB($Source, $Destination) {
$ImportError = $null
try {
# Use Start-Process, since PowerShell doesn't have '<', and alternatives ("Get-Content X | SQLite.exe OutDB") are subpar at best when dealing with large files like these database exports.
Start-Process $this.PlexSQL -ArgumentList @("""$Destination""") -RedirectStandardInput $Source -NoNewWindow -Wait -EA Stop -ErrorVariable importError
} catch {
$Err = $Error -join "`n"
$this.ExitDBMaintenance("Failed to import Plex database (importing '$Source' into '$Destination): $Err", $false)
$Error.Clear()
return $false
}
if ($ImportError) {
$Err = $ImportError -join "`n"
$this.ExitDBMaintenance("Failed to import Plex database (importing '$Source' into '$Destination'): $Err", $false)
return $false
}
return $true
}
# Clear out the temp database directory. If $Confirm is $true, asks the user before doing so.
[void] CleanDBTemp([bool] $Confirm) {
if ($Confirm -and !$this.GetYesNo("Ok to remove temporary databases/workfiles for this session")) {
$this.Output("Retaining all temporary work files.")
$this.WriteLog("Exit - Retain temp files.")
return
}
$DBTemp = Join-Path $this.PlexDBDir -ChildPath "dbtmp"
if ($this.DirExists($DBTemp)) {
try {
Remove-Item $DBTemp -Recurse -Force -EA Stop
$this.Output("Deleted all temporary work files.")
$this.WriteLog("Exit - Deleted temp files.")
} catch {
$Err = $Error -join "`n"
$this.OutputWarn("Failed to remove temporary directory: $Err")
$this.WriteLog("Exit - Failed to remove temporary files: $Err")
$Error.Clear()
}
}
}
### Miscellaneous Helpers ###
# Return whether PMS is running
[bool] PMSRunning() {
return !!$this.GetPMS()
}
# Retrieve the PMS process, if running
[System.Diagnostics.Process] GetPMS() {
return Get-Process -EA Ignore -Name "Plex Media Server"
}
# Ask the user a yes or no question, continuing to prompt them until
# their input starts with either a 'Y' or 'N'
[bool] GetYesNo([string] $Prompt) {
$Response = (Read-Host "$Prompt [Y/N]? ").ToLower()
$Ch = $Response.Substring(0, [Math]::Min($Response.Length, 1))
while (($Ch -ne "y") -and ($Ch -ne "n")) {
Write-Host "Invalid input, please enter [Y]es or [N]o"
$Response = (Read-Host "$Prompt [Y/N]? ").ToLower()
$Ch = $Response.Substring(0, [Math]::Min($Response.Length, 1))
}
return $Ch -eq "y"
}
}
# Contains miscellaneous options/state over the course of a session.
class PlexDBRepairOptions {
[bool] $Scripted # Whether we're running in scripted or interactive mode
[bool] $ShowMenu # Whether to show the menu after each command executes
[int32] $CacheAge # The date cutoff for pruning PhotoTranscoder cached images
PlexDBRepairOptions() {
$this.CacheAge = 30
$this.ShowMenu = $true
$this.Scripted = $false
}
}
# Contains relevant data about a PhotoTranscoder `prune` attempt
class CleanCacheResult {
[int32] $TotalFiles # Total number of PhotoTranscoder files
[int32] $PrunableFiles # Total number of files that are older than the cutoff
[string] $SpaceSavings # Friendly string of (potential) space savings
CleanCacheResult([int32] $TotalFiles, [int32] $PrunableFiles, [int32] $PrunableBytes) {
$this.TotalFiles = $TotalFiles
$this.PrunableFiles = $PrunableFiles
$this.SpaceSavings = "$($PrunableBytes) bytes"
if ($PrunableBytes -gt 1GB) {
$this.SpaceSavings = "$([math]::round($PrunableBytes / 1GB, 2)) GiB";
} elseif ($PrunableBytes -gt 1MB) {
$this.SpaceSavings = "$([math]::round($PrunableBytes / 1MB, 2)) MiB";
} elseif ($PrunableBytes -gt 1KB) {
$this.SpaceSavings = "$([math]::round($PrunableBytes / 1KB, 2)) KiB";
}
}
}
[void]([PlexDBRepair]::new($args, $PlexDBRepairVersion))

76
README-Windows.md Normal file
View File

@ -0,0 +1,76 @@
# PlexDBRepair-Windows
DBRepair-Windows.ps1 (and DBRepair-Windows.bat) are scripts run from the command line, which have
sufficient privilege to read/write the Plex databases in the
[Plex data directory](https://support.plex.tv/articles/202915258-where-is-the-plex-media-server-data-directory-located/).
## DBRepair-Windows.ps1 vs. DBRepair-Windows.bat
Currently, there are two separate Windows scripts, a batch script (.bat) and a PowerShell script
(.ps1). The batch script is a one-shot, zero-input script that attempts automatic database
maintenance (repair/rebuild, check, and reindex). The PowerShell script is intended to align with
PlexDBRepair.sh, offering command-name-based functionality that can either be scripted or
interactive.
In the future, DBRepair-Windows.bat will be removed in favor of DBRepair-Windows.ps1. The batch
file is currently kept as a backup while the PowerShell script continues to be expanded and
tested. If any unexpected issues arise with the PowerShell script, please open an
[issue](https://github.com/ChuckPa/PlexDBRepair/issues) so it can be investigated.
## Functions provided
The Windows utility aims to provide a similar interface to DBRepair.sh as outlined in the main
[README file](README.md), but currently only offers a subset of its functionality. For a full
description of the features below, consult that main README file.
The following commands (or their number) are currently supported on Windows.
```
AUTO(matic)
EXIT
PRUN(e)
STAR(t)
STOP
```
Run `.\DBRepair-Windows.ps1 -Help` for more complete documentation.
# Installation and usage instructions
DBRepair-Windows can be downloaded to any location. However, the PowerShell script might require
some prerequisite work in order to run as expected. By default, PowerShell scripts are blocked on
Windows machines, so in order to run DBRepair-Windows.ps1, you may need to do one of the following:
1. From an administrator PowerShell prompt, run `Set-ExecutionPolicy RemoteSigned`, then run the
script from a normal PowerShell prompt. If PowerShell still will not run the script, you can do
one of the following:
* In Windows Explorer, right-click DBRepair-Windows.ps1, select Properties, and check 'Unblock'
at the bottom of the dialog.
* In PowerShell, run `Unblock-File <path\to\DBRepair-Windows.ps1>`
* Run `Set-ExecutionPolicy Unrestricted` - this may result in an "are you sure" prompt before
running the script. `Set-ExecutionPolicy Bypass` will get around this, but is not recommended,
as it allows _any_ downloaded script to run without notification, not just DBRepair.
2. Explicitly set the `ExecutionPolicy` when running the script, e.g.:
```powershell
powershell -ExecutionPolicy Bypass ".\DBRepair-Windows.ps1 stop auto start"
```
Note that this method may not work if your machine is managed with Group Policy, which
can block manual `ExecutionPolicy` overrides.
3. Similar to 2, but make it a batch script (e.g. `DBRepair.bat`) that lives alongside
the powershell script:
```batch
@echo off
powershell -ExecutionPolicy Bypass -Command ".\DBRepair-Windows.ps1 %*"
```
Then run that script directly:
```batch
.\DBRepair.bat stop auto start
```
Also note that the PowerShell script cannot be run directly from a Command Prompt window.
If you are running this from Command Prompt, you must launch it via PowerShell:
```cmd
powershell .\DBRepair-Windows.ps1 [args]
```

View File

@ -984,3 +984,9 @@ cd /var/packages/PlexMediaServer/shares/PlexMediaServer
# Run classic Stop PMS, Automatic optimization/repair, Start PMS, and exit sequence # Run classic Stop PMS, Automatic optimization/repair, Start PMS, and exit sequence
sudo ./DBRepair.sh stop auto start exit sudo ./DBRepair.sh stop auto start exit
``` ```
# Special Considerations - Windows
Windows support is available via DBRepair-Windows.ps1 and DBRepair-Windows.bat. See [README-Windows](README-Windows.md) for details.

14
ReleaseNotes-Windows Normal file
View File

@ -0,0 +1,14 @@
# PlexDBRepair-Windows
Release notes for the Windows counterpart to DBRepair.sh (DBRepair-Windows.ps1)
# Release Info
v1.00.00
- Initial Windows PowerShell script release, aiming to provide a similar experience as DBRepair.sh, with command-name-based input.
- Initial command support:
- AUTO(matic)
- EXIT
- PRUN(e)
- STAR(t)
- STOP