diff --git a/DBRepair-Windows.bat b/DBRepair-Windows.bat index 37eb65d..d8d1cfd 100644 --- a/DBRepair-Windows.bat +++ b/DBRepair-Windows.bat @@ -10,6 +10,12 @@ REM This is stable working software but not "Released" software. Development wi 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 set Hour=%time:~0,2% set Min=%time:~3,2% diff --git a/DBRepair-Windows.ps1 b/DBRepair-Windows.ps1 new file mode 100644 index 0000000..bb2acec --- /dev/null +++ b/DBRepair-Windows.ps1 @@ -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)) diff --git a/README-Windows.md b/README-Windows.md new file mode 100644 index 0000000..b8c4641 --- /dev/null +++ b/README-Windows.md @@ -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 ` + * 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] +``` diff --git a/README.md b/README.md index 727bfac..ee16375 100644 --- a/README.md +++ b/README.md @@ -984,3 +984,9 @@ cd /var/packages/PlexMediaServer/shares/PlexMediaServer # Run classic Stop PMS, Automatic optimization/repair, Start PMS, and exit sequence 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. \ No newline at end of file diff --git a/ReleaseNotes-Windows b/ReleaseNotes-Windows new file mode 100644 index 0000000..6276ccc --- /dev/null +++ b/ReleaseNotes-Windows @@ -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