From 7cac5fd95766ab36f6f2eb6fc69be58fee34a6dd Mon Sep 17 00:00:00 2001 From: deathbybandaid Date: Wed, 21 Feb 2024 10:30:59 -0500 Subject: [PATCH] test --- DBRepair.sh | 770 +++++++++++++++++++++++++++-------- remove_lonely_collections.py | 29 +- 2 files changed, 625 insertions(+), 174 deletions(-) diff --git a/DBRepair.sh b/DBRepair.sh index 1c9aff3..627ad08 100644 --- a/DBRepair.sh +++ b/DBRepair.sh @@ -2,12 +2,12 @@ ######################################################################### # Plex Media Server database check and repair utility script. # # Maintainer: ChuckPa # -# Version: v1.0.0 # -# Date: 13-Mar-2023 # +# Version: v1.05.00 # +# Date: 09-Feb-2024 # ######################################################################### # Version for display purposes -Version="v1.0.0" +Version="v1.05.00" # Flag when temp files are to be retained Retain=0 @@ -17,10 +17,18 @@ CheckedDB=0 # By default, we cannot start/stop PMS HaveStartStop=0 -StartStopUser=0 StartCommand="" StopCommand="" +# By default, require root privilege +RootRequired=1 + +# By default, Errors are fatal. +IgnoreErrors=0 + +# By default, Duplicate view states not Removed +RemoveDuplicates=0 + # Keep track of how many times the user's hit enter with no command (implied EOF) NullCommands=0 @@ -33,11 +41,13 @@ TimeStamp="$(date "+%Y-%m-%d_%H.%M.%S")" # Initialize global runtime variables CheckedDB=0 Damaged=0 +DbPageSize=0 Fail=0 HaveStartStop=0 HostType="" LOG_TOOL="echo" ShowMenu=1 +Exit=0 # Universal output function Output() { @@ -193,7 +203,7 @@ FreeSpaceAvailable() { [ "$1" != "" ] && Multiplier=$1 # Available space where DB resides - SpaceAvailable=$(df -m "$AppSuppDir" | tail -1 | awk '{print $4}') + SpaceAvailable=$(df $DFFLAGS "$AppSuppDir" | tail -1 | awk '{print $4}') # Get size of DB and blobs, Minimally needing sum of both LibSize="$(stat $STATFMT $STATBYTES "$CPPL.db")" @@ -249,18 +259,27 @@ MakeBackups() { ConfirmYesNo() { Answer="" - while [ "$Answer" = "" ] + while [ "$Answer" != "Y" ] && [ "$Answer" != "N" ] do - printf "$1 (Y/N) ? " + printf "%s (Y/N) ? " "$1" read Input # EOF = No - [ "$Input" = "" ] && Answer=N ; [ "$Input" = "n" ] && Answer=N ; [ "$Input" = "N" ] && Answer=N - [ "$Input" = "y" ] && Answer=Y ; [ "$Input" = "Y" ] && Answer=Y + case "$Input" in + YES|YE|Y|yes|ye|y) + Answer=Y + ;; + NO|N|no|n) + Answer=N + ;; + *) + Answer="" + ;; + esac # Unrecognized if [ "$Answer" != "Y" ] && [ "$Answer" != "N" ]; then - printf "$Input" was not a valid reply. Please try again. + echo \"$Input\" was not a valid reply. Please try again. continue fi done @@ -285,6 +304,13 @@ RestoreSaved() { done } +# Return only the digits in the given version string +VersionDigits() { + local ver + ver=$(echo "$1" | tr -d [v\.] ) + echo $ver +} + # Get the size of the given DB in MB GetSize() { @@ -294,6 +320,37 @@ GetSize() { echo $Size } +# Extract specified value from override file if it exists (Null if not) +GetOverride() { + + Retval="" + + # Don't know if we have pushd so do it long hand + CurrDir="$(pwd)" + + # Find the metadata dir if customized + if [ -e /etc/systemd/system/plexmediaserver.service.d ]; then + + # Get there + cd /etc/systemd/system/plexmediaserver.service.d + + # Glob up all 'conf files' found + ConfFile="$(find override.conf local.conf *.conf 2>/dev/null | head -1)" + + # If there is one, search it + if [ "$ConfFile" != "" ]; then + Retval="$(grep "$1" $ConfFile | head -1 | sed -e "s/.*${1}=//" | tr -d \" | tr -d \')" + fi + + fi + + # Go back to where we were + cd "$CurrDir" + + # What did we find + echo "$Retval" +} + # Determine which host we are running on and set variables HostConfig() { @@ -301,6 +358,10 @@ HostConfig() { PIDOF="pidof" STATFMT="-c" STATBYTES="%s" + STATPERMS="%a" + + # On all hosts except QNAP + DFFLAGS="-m" # Synology (DSM 7) if [ -d /var/packages/PlexMediaServer ] && \ @@ -377,9 +438,30 @@ HostConfig() { StopCommand="/etc/init.d/plex.sh stop" fi + # Use custom DFFLAGS (force POSIX mode) + DFFLAGS="-Pm" + HostType="QNAP" return 0 + # SNAP host (check before standard) + elif [ -d "/var/snap/plexmediaserver/common/Library/Application Support/Plex Media Server" ]; then + + # Where things are + PLEX_SQLITE="/snap/plexmediaserver/current/Plex SQLite" + AppSuppDir="/var/snap/plexmediaserver/common/Library/Application Support" + PID_FILE="$AppSuppDir/plexmediaserver.pid" + DBDIR="$AppSuppDir/Plex Media Server/Plug-in Support/Databases" + LOGFILE="$DBDIR/DBRepair.log" + LOG_TOOL="logger" + + HaveStartStop=1 + StartCommand="snap start plexmediaserver" + StopCommand="snap stop plexmediaserver" + + HostType="SNAP" + return 0 + # Standard configuration Linux host elif [ -f /etc/os-release ] && \ [ -d /usr/lib/plexmediaserver ] && \ @@ -398,18 +480,13 @@ HostConfig() { # Find the metadata dir if customized if [ -e /etc/systemd/system/plexmediaserver.service.d ]; then - # Glob up all 'conf files' found - NewSuppDir="$(cd /etc/systemd/system/plexmediaserver.service.d ; \ - cat override.conf local.conf *.conf 2>/dev/null | grep "APPLICATION_SUPPORT_DIR" | head -1)" + # Get custom AppSuppDir if specified + NewSuppDir="$(GetOverride PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR)" - if [ "$NewSuppDir" != "" ]; then - NewSuppDir="$(echo $NewSuppDir | sed -e 's/.*_DIR=//' | tr -d '"' | tr -d "'")" - - if [ -d "$NewSuppDir" ]; then + if [ -d "$NewSuppDir" ]; then AppSuppDir="$NewSuppDir" - else + else Output "Given application support directory override specified does not exist: '$NewSuppDir'. Ignoring." - fi fi fi @@ -440,6 +517,10 @@ HostConfig() { LOGFILE="$DBDIR/DBRepair.log" LOG_TOOL="logger" + HaveStartStop=1 + StartCommand="systemctl start fvapp-plexmediaserver" + StopCommand="systemctl stop fvapp-plexmediaserver" + HostType="Netgear ReadyNAS" return 0 fi @@ -459,93 +540,6 @@ HostConfig() { HostType="ASUSTOR" return 0 - # Containers: - # - Docker cgroup v1 & v2 - # - Podman (libpod) - elif [ "$(grep docker /proc/1/cgroup | wc -l)" -gt 0 ] || [ "$(grep 0::/ /proc/1/cgroup)" = "0::/" ] || - [ "$(grep libpod /proc/1/cgroup | wc -l)" -gt 0 ]; then - - # HOTIO Plex image structure is non-standard (contains symlink which breaks detection) - if [ -d "/app/usr/lib/plexmediaserver" ] && [ -d "/config/Plug-in Support" ]; then - PLEX_SQLITE="/app/usr/lib/plexmediaserver/Plex SQLite" - AppSuppDir="/config" - PID_FILE="$AppSuppDir/plexmediaserver.pid" - DBDIR="$AppSuppDir/Plug-in Support/Databases" - LOGFILE="$DBDIR/DBRepair.log" - LOG_TOOL="logger" - - HostType="HOTIO" - return 0 - - # Docker (All main image variants except binhex and hotio) - elif [ -d "/config/Library/Application Support" ]; then - - PLEX_SQLITE="/usr/lib/plexmediaserver/Plex SQLite" - AppSuppDir="/config/Library/Application Support" - PID_FILE="$AppSuppDir/Plex Media Server/plexmediaserver.pid" - DBDIR="$AppSuppDir/Plex Media Server/Plug-in Support/Databases" - LOGFILE="$DBDIR/DBRepair.log" - LOG_TOOL="logger" - - if [ -d "/var/run/service/svc-plex" ]; then - HaveStartStop=1 - StartCommand="s6-svc -u /var/run/service/svc-plex" - StopCommand="s6-svc -d /var/run/service/svc-plex" - fi - - if [ -d "/var/run/s6/services/plex" ]; then - HaveStartStop=1 - StartCommand="s6-svc -u /var/run/s6/services/plex" - StopCommand="s6-svc -d /var/run/s6/services/plex" - fi - - # lsio stop - if [ -d "/var/run/service/svc-plex" ]; then - HaveStartStop=1 - StartCommand="s6-svc -u /var/run/service/svc-plex" - StopCommand="s6-svc -d /var/run/service/svc-plex" - fi - - # HOTIO - if [ -d /run/service/plex ]; then - HaveStartStop=1 - StartCommand="s6-svc -u /run/service/plex" - StopCommand="s6-svc -d /run/service/plex" - fi - - HostType="Docker" - return 0 - - # BINHEX Plex image - elif [-f /usr/lib/python3.10/binhex.py ] && [ -d "/config/Plex Media Server" ]; then - - PLEX_SQLITE="/usr/lib/plexmediaserver/Plex SQLite" - AppSuppDir="/config" - PID_FILE="$AppSuppDir/Plex Media Server/plexmediaserver.pid" - DBDIR="$AppSuppDir/Plex Media Server/Plug-in Support/Databases" - LOGFILE="$DBDIR/DBRepair.log" - LOG_TOOL="logger" - - HostType="BINHEX" - return 0 - - fi - - - # Western Digital (OS5) - elif [ -f /etc/system.conf ] && [ -d /mnt/HD/HD_a2/Nas_Prog/plexmediaserver ] && \ - grep "Western Digital Corp" /etc/system.conf >/dev/null; then - - # Where things are - PLEX_SQLITE="/mnt/HD/HD_a2/Nas_Prog/plexmediaserver/binaries/Plex SQLite" - AppSuppDir="$(echo /mnt/HD/HD*/Nas_Prog/plex_conf)" - PID_FILE="$AppSuppDir/Plex Media Server/plexmediaserver.pid" - DBDIR="$AppSuppDir/Plex Media Server/Plug-in Support/Databases" - LOGFILE="$DBDIR/DBRepair.log" - LOG_TOOL="logger" - - HostType="Western Digital" - return 0 # Apple Mac elif [ -d "/Applications/Plex Media Server.app" ] && \ @@ -563,6 +557,10 @@ HostConfig() { PIDOF="pgrep" STATFMT="-f" STATBYTES="%z" + STATPERMS="%A" + + # Root not required on MacOS. PMS runs as username. + RootRequired=0 # make the TMP directory in advance to store plexmediaserver.pid mkdir -p "$DBDIR/dbtmp" @@ -576,8 +574,133 @@ HostConfig() { HostType="Mac" return 0 + + # Western Digital (OS5) + elif [ -f /etc/system.conf ] && [ -d /mnt/HD/HD_a2/Nas_Prog/plexmediaserver ] && \ + grep "Western Digital Corp" /etc/system.conf >/dev/null; then + + # Where things are + PLEX_SQLITE="/mnt/HD/HD_a2/Nas_Prog/plexmediaserver/binaries/Plex SQLite" + AppSuppDir="$(echo /mnt/HD/HD*/Nas_Prog/plex_conf)" + PID_FILE="$AppSuppDir/Plex Media Server/plexmediaserver.pid" + DBDIR="$AppSuppDir/Plex Media Server/Plug-in Support/Databases" + LOGFILE="$DBDIR/DBRepair.log" + LOG_TOOL="logger" + + HostType="Western Digital" + return 0 + + + # - Docker cgroup v1 & v2 + # - Podman (libpod) + # - Kubernetes (and TrueNAS platforms) + elif [ "$(grep docker /proc/1/cgroup | wc -l)" -gt 0 ] || [ "$(grep 0::/ /proc/1/cgroup)" = "0::/" ] || + [ "$(grep libpod /proc/1/cgroup | wc -l)" -gt 0 ] || [ "$(grep kube /proc/1/cgroup | wc -l)" -gt 0 ]; then + + # HOTIO Plex image structure is non-standard (contains symlink which breaks detection) + if [ -n "$(grep -irslm 1 hotio /etc/s6-overlay/s6-rc.d)" ]; then + PLEX_SQLITE=$(find /app/usr/lib/plexmediaserver /usr/lib/plexmediaserver -maxdepth 0 -type d -print -quit 2>/dev/null); PLEX_SQLITE="$PLEX_SQLITE/Plex SQLite" + AppSuppDir="/config" + PID_FILE="$AppSuppDir/plexmediaserver.pid" + DBDIR="$AppSuppDir/Plug-in Support/Databases" + LOGFILE="$DBDIR/DBRepair.log" + LOG_TOOL="logger" + if [ -d "/run/service/plex" ] || [ -d "/run/service/service-plex" ]; then + SERVICE_PATH=$([ -d "/run/service/plex" ] && echo "/run/service/plex" || [ -d "/run/service/service-plex" ] && echo "/run/service/service-plex") + HaveStartStop=1 + StartCommand="s6-svc -u $SERVICE_PATH" + StopCommand="s6-svc -d $SERVICE_PATH" + fi + + HostType="HOTIO" + return 0 + + # Docker (All main image variants except binhex and hotio) + elif [ -d "/config/Library/Application Support" ]; then + + PLEX_SQLITE="/usr/lib/plexmediaserver/Plex SQLite" + AppSuppDir="/config/Library/Application Support" + PID_FILE="$AppSuppDir/Plex Media Server/plexmediaserver.pid" + DBDIR="$AppSuppDir/Plex Media Server/Plug-in Support/Databases" + LOGFILE="$DBDIR/DBRepair.log" + LOG_TOOL="logger" + + # Miscellaneous start/stop methods + if [ -d "/var/run/service/svc-plex" ]; then + HaveStartStop=1 + StartCommand="s6-svc -u /var/run/service/svc-plex" + StopCommand="s6-svc -d /var/run/service/svc-plex" + fi + + if [ -d "/var/run/s6/services/plex" ]; then + HaveStartStop=1 + StartCommand="s6-svc -u /var/run/s6/services/plex" + StopCommand="s6-svc -d /var/run/s6/services/plex" + fi + + HostType="Docker" + return 0 + + # BINHEX Plex image + elif [ -e /etc/os-release ] && grep "IMAGE_ID=archlinux" /etc/os-release 1>/dev/null && \ + [ -e /home/nobody/start.sh ] && grep PLEX_MEDIA /home/nobody/start.sh 1> /dev/null ; then + + PLEX_SQLITE="/usr/lib/plexmediaserver/Plex SQLite" + AppSuppDir="/config" + PID_FILE="$AppSuppDir/Plex Media Server/plexmediaserver.pid" + DBDIR="$AppSuppDir/Plex Media Server/Plug-in Support/Databases" + LOGFILE="$DBDIR/DBRepair.log" + LOG_TOOL="logger" + + HostType="BINHEX" + return 0 + + fi + + # Last chance to identify this host + elif [ -e /etc/os-release ]; then + + # Arch Linux (must check for native Arch after binhex) + if [ "$(grep -E '=arch|="arch"' /etc/os-release)" != "" ] && \ + [ -d /usr/lib/plexmediaserver ] && \ + [ -d /var/lib/plex ]; then + + # Where is the software + PKGDIR="/usr/lib/plexmediaserver" + PLEX_SQLITE="$PKGDIR/Plex SQLite" + LOG_TOOL="logger" + + # Where is the data + AppSuppDir="/var/lib/plex" + + # Find the metadata dir if customized + if [ -e /etc/systemd/system/plexmediaserver.service.d ]; then + + # Get custom AppSuppDir if specified + NewSuppDir="$(GetOverride PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR)" + + if [ "$NewSuppDir" != "" ]; then + if [ -d "$NewSuppDir" ]; then + AppSuppDir="$NewSuppDir" + else + Output "Given application support directory override specified does not exist: '$NewSuppDir'. Ignoring." + fi + fi + fi + + DBDIR="$AppSuppDir/Plex Media Server/Plug-in Support/Databases" + LOGFILE="$DBDIR/DBRepair.log" + LOG_TOOL="logger" + HostType="$(grep PRETTY_NAME /etc/os-release | sed -e 's/^.*="//' | tr -d \" )" + + HaveStartStop=1 + StartCommand="systemctl start plexmediaserver" + StopCommand="systemctl stop plexmediaserver" + return 0 + fi fi + # Unknown / currently unsupported host return 1 } @@ -600,6 +723,7 @@ DoIndex() { Damaged=1 CheckedDB=1 Fail=1 + [ $IgnoreErrors -eq 1 ] && Fail=0 fi @@ -613,6 +737,8 @@ DoIndex() { Output "Backing up of databases" MakeBackups "Reindex" Result=$? + [ $IgnoreErrors -eq 1 ] && Result=0 + if [ $Result -eq 0 ]; then WriteLog "Reindex - MakeBackup - PASS" else @@ -626,6 +752,8 @@ DoIndex() { Output "Reindexing main database" "$PLEX_SQLITE" $CPPL.db 'REINDEX;' Result=$? + [ $IgnoreErrors -eq 1 ] && Result=0 + if SQLiteOK $Result; then Output "Reindexing main database successful." WriteLog "Reindex - Reindex: $CPPL.db - PASS" @@ -638,6 +766,8 @@ DoIndex() { Output "Reindexing blobs database" "$PLEX_SQLITE" $CPPL.blobs.db 'REINDEX;' Result=$? + [ $IgnoreErrors -eq 1 ] && Result=0 + if SQLiteOK $Result; then Output "Reindexing blobs database successful." WriteLog "Reindex - Reindex: $CPPL.blobs.db - PASS" @@ -694,6 +824,69 @@ DoUndo(){ } +##### DoSetPageSize +DoSetPageSize() { + + # If DBREPAIR_PAGESIZE variable exists, validate it. + [ "$DBREPAIR_PAGESIZE" = "" ] && return + + # Is it a valid positive integer ? + if [ "$DBREPAIR_PAGESIZE" != "$(echo "$DBREPAIR_PAGESIZE" | sed 's/[^0-9]*//g')" ]; then + WriteLog "SetPageSize - ERROR: DBREPAIR_PAGESIZE is not a valid integer. Ignoring '$DBREPAIR_PAGESIZE'" + Output "ERROR: DBREPAIR_PAGESIZE is not a valid integer. Ignoring '$DBREPAIR_PAGESIZE'" + return + fi + + # Make certain it's a multiple of 1024 and gt 0 + DbPageSize=$DBREPAIR_PAGESIZE + [ $DbPageSize -le 0 ] && return + + if [ $(expr $DbPageSize % 1024) -ne 0 ]; then + + DbPageSize=$(expr $DBREPAIR_PAGESIZE + 1023) + DbPageSize=$(expr $DbPageSize / 1024) + DbPageSize=$(expr $DbPageSize \* 1024) + + WriteLog "DoSetPageSize - ERROR: DBREPAIR_PAGESIZE ($DBREPAIR_PAGESIZE) not a multiple of 1024. New value = $DbPageSize." + Output "WARNING: DBREPAIR_PAGESIZE ($DBREPAIR_PAGESIZE) not a multiple of 1024. New value = $DbPageSize." + fi + + # Must be compliant + if [ $DbPageSize -gt 65536 ]; then + Output "WARNING: DBREPAIR_PAGESIZE ($DbPageSize) too large. Reducing to 65536." + WriteLog "SetPageSize - DBREPAIR_PAGESIZE ($DbPageSize) too large. Reducing." + DbPageSize=65536 + fi + + # Confirm a valid power of two. + IsPowTwo=0 + for i in 1024 2048 4096 8192 16384 32768 65536 + do + [ $i -eq $DbPageSize ] && IsPowTwo=1 && break + done + + if [ $IsPowTwo -eq 0 ] && [ $DbPageSize -lt 65536 ]; then + for i in 1024 2048 4096 8192 16384 32768 65536 + do + if [ $i -gt $DbPageSize ]; then + Output "ERROR: DBREPAIR_SIZE ($DbPageSize) not a power of 2 between 1024 and 65536. Value selected = $i." + WriteLog "SetPageSize - DBREPAIR_PAGESIZE ($DbPageSize) not a power of 2. New value selected = $i" + DbPageSize=$i + IsPowTwo=1 + fi + [ $IsPowTwo -eq 1 ] && break + done + fi + + Output "Setting Plex SQLite page size ($DbPageSize)" + WriteLog "SetPageSize - Setting Plex SQLite page_size: $DbPageSize" + + +# Create DB with desired page size +"$PLEX_SQLITE" "$1" "PRAGMA page_size=${DbPageSize}; VACUUM;" + +} + ##### DoRepair DoRepair() { @@ -724,13 +917,12 @@ DoRepair() { Output "Exporting current databases using timestamp: $TimeStamp" Fail=0 - # Get the owning UID/GID before we proceed so we can restore - Owner="$(stat $STATFMT '%u:%g' $CPPL.db)" - # Attempt to export main db to SQL file (Step 1) Output "Exporting Main DB" "$PLEX_SQLITE" $CPPL.db ".output '$TMPDIR/library.plexapp.sql-$TimeStamp'" .dump Result=$? + [ $IgnoreErrors -eq 1 ] && Result=0 + if ! SQLiteOK $Result; then # Cannot dump file @@ -745,6 +937,8 @@ DoRepair() { Output "Exporting Blobs DB" "$PLEX_SQLITE" $CPPL.blobs.db ".output '$TMPDIR/blobs.plexapp.sql-$TimeStamp'" .dump Result=$? + [ $IgnoreErrors -eq 1 ] && Result=0 + if ! SQLiteOK $Result; then # Cannot dump file @@ -769,9 +963,12 @@ DoRepair() { # Library and blobs successfully exported, create new Output "Importing Main DB." + DoSetPageSize "$TMPDIR/$CPPL.db-REPAIR-$TimeStamp" "$PLEX_SQLITE" "$TMPDIR/$CPPL.db-REPAIR-$TimeStamp" < "$TMPDIR/library.plexapp.sql-$TimeStamp" Result=$? - if ! SQLiteOK $Result; then + [ $IgnoreErrors -eq 1 ] && Result=0 + + if ! SQLiteOK $Result ; then Output "Error $Result from Plex SQLite while importing from '$TMPDIR/library.plexapp.sql-$TimeStamp'" WriteLog "Repair - Cannot import main database from '$TMPDIR/library.plexapp.sql-$TimeStamp' - FAIL ($Result)" Output "Cannot continue." @@ -780,8 +977,11 @@ DoRepair() { fi Output "Importing Blobs DB." + DoSetPageSize "$TMPDIR/$CPPL.blobs.db-REPAIR-$TimeStamp" "$PLEX_SQLITE" "$TMPDIR/$CPPL.blobs.db-REPAIR-$TimeStamp" < "$TMPDIR/blobs.plexapp.sql-$TimeStamp" Result=$? + [ $IgnoreErrors -eq 1 ] && Result=0 + if ! SQLiteOK $Result ; then Output "Error $Result from Plex SQLite while importing from '$TMPDIR/blobs.plexapp.sql-$TimeStamp'" WriteLog "Repair - Cannot import blobs database from '$TMPDIR/blobs.plexapp.sql-$TimeStamp' - FAIL ($Result)" @@ -791,7 +991,7 @@ DoRepair() { fi # Made it to here, now verify - Output "Successfully imported SQL data." + Output "Successfully imported databases." WriteLog "Repair - Import - PASS" # Verify databases are intact and pass testing @@ -829,6 +1029,7 @@ DoRepair() { [ -e $CPPL.blobs.db ] && mv $CPPL.blobs.db "$TMPDIR/$CPPL.blobs.db-BACKUP-$TimeStamp" Output "Making repaired databases active" + WriteLog "Making repaired databases active" mv "$TMPDIR/$CPPL.db-REPAIR-$TimeStamp" $CPPL.db mv "$TMPDIR/$CPPL.blobs.db-REPAIR-$TimeStamp" $CPPL.blobs.db @@ -842,7 +1043,27 @@ DoRepair() { [ -e $CPPL.db-shm ] && rm -f $CPPL.db-shm # Set ownership on new files + chmod $Perms $CPPL.db $CPPL.blobs.db + Result=$? + if [ $Result -ne 0 ]; then + Output "ERROR: Cannot set permissions on new databases. Error $Result" + Output " Please exit tool, keeping temp files, seek assistance." + Output " Use files: $TMPDIR/*-BACKUP-$TimeStamp" + WriteLog "Repair - Move files - FAIL" + Fail=1 + return 1 + fi + chown $Owner $CPPL.db $CPPL.blobs.db + Result=$? + if [ $Result -ne 0 ]; then + Output "ERROR: Cannot set ownership on new databases. Error $Result" + Output " Please exit tool, keeping temp files, seek assistance." + Output " Use files: $TMPDIR/*-BACKUP-$TimeStamp" + WriteLog "Repair - Move files - FAIL" + Fail=1 + return 1 + fi # We didn't fail, set CheckedDB status true (passed above checks) CheckedDB=1 @@ -894,8 +1115,8 @@ DoReplace() { # Make certain there is ample free space if ! FreeSpaceAvailable ; then - Output "ERROR: Insufficient free space available on $AppSuppDir. Cannot continue - WriteLog "REPLACE - Insufficient free space available on $AppSuppDir. Aborted. + Output "ERROR: Insufficient free space available on $AppSuppDir. Cannot continue" + WriteLog "REPLACE - Insufficient free space available on $AppSuppDir. Aborted." return 1 fi @@ -1166,15 +1387,17 @@ DoImport(){ # Import viewstates into working copy (Ignore constraint errors during import) printf 'Importing Viewstate & History data...' + DoSetPageSize "$TMPDIR/$CPPL.db-IMPORT-$TimeStamp" "$PLEX_SQLITE" "$TMPDIR/$CPPL.db-IMPORT-$TimeStamp" < "$TMPDIR/Viewstate.sql-$TimeStamp" 2> /dev/null -# # Purge duplicates (violations of unique constraint) -# cat < 1); -#EOF + # Remove duplicates (violations of unique constraint) + if [ $RemoveDuplicates -eq 1 ]; then + cat < /dev/null 2> /dev/null Result=$? + if [ $Result -ne 0 ]; then + Output "Cannot send stop command to PMS, error $Result. Please stop manually." + WriteLog "Cannot send stop command to PMS, error $Result. Please stop manually." + return 1 + fi + Count=10 - while [ $Result -eq 0 ] && IsRunning && [ $Count -gt 0 ] + while IsRunning && [ $Count -gt 0 ] do - sleep 1 + sleep 3 Count=$((Count - 1)) done - if [ $Result -eq 0 ]; then + if ! IsRunning; then WriteLog "Stop - PASS" Output "Stopped PMS." + return 0 else - WriteLog "Stop - FAIL ($Result)" - Output "Could not stop PMS. Error code: $Result" + WriteLog "Stop - FAIL (Timeout)" + Output "Could not stop PMS. PMS did not shutdown within 30 second limit." fi fi return $Result } -##### UpdateTimestamp +# Do command line switches +DoOptions() { + + for i in $@ + do + Opt="$(echo $i | cut -c1-2 | tr [A-Z] [a-z])" + [ "$Opt" = "-i" ] && IgnoreErrors=1 && WriteLog "Opt: Database error checking ignored." + [ "$Opt" = "-f" ] && IgnoreErrors=1 && WriteLog "Opt: Database error checking ignored." + [ "$Opt" = "-p" ] && RemoveDuplicates=1 && WriteLog "Opt: Remove duplidate watch history viewstates." + done +} + +# UpdateTimestamp DoUpdateTimestamp() { TimeStamp="$(date "+%Y-%m-%d_%H.%M.%S")" } +# Get latest version from Github +GetLatestRelease() { + Response=$(curl -s "https://api.github.com/repos/ChuckPa/PlexDBRepair/tags") + if [ $? -eq 0 ]; then + LatestVersion="$(echo "$Response" | grep -oP '"name":\s*"\K[^"]*' | sed -n '1p')" + else + LatestVersion="$Version" + fi + +} + +# Download and update script +DownloadAndUpdate() { + Url="$1" + Filename="$2" + + # Download the file and check if the download was successful + if curl -s "$Url" --output "${Filename}.tmp"; then + # Check if the file was written to and at least 50000 bytes + if [ -f "${Filename}.tmp" ]; then + if [ $(stat $STATFMT $STATBYTES "${Filename}.tmp") -gt 50000 ]; then + Output "Update downloaded successfully" + mv "$Filename" "${Filename}.bak" + mv "${Filename}.tmp" "$Filename" + chmod +x "$Filename" + return 0 + else + Output "Error: Downloaded file incomplete." + rm -f "${Filename}.tmp" + fi + else + Output "Error: Unable to download update." + rm -f "${Filename}.tmp" + fi + else + Output "Error: Download failed." + rm -f "${Filename}.tmp" + fi + return 1 +} + +# Prune old jpg files from the PhotoTranscoder directory (> 30 days -or- DBREPAIR_CACHEAGE days) +DoPrunePhotoTranscoder() { + + TransCacheDir="$AppSuppDir/Plex Media Server/Cache/PhotoTranscoder" + PruneIt=0 + + # Use default cache age of 30 days + CacheAge=30 + + # Does DBREPAIR_CACHEAGE exist and is it a valid positive integer ? + if [ "$DBREPAIR_CACHEAGE" != "" ]; then + if [ "$DBREPAIR_CACHEAGE" != "$(echo "$DBREPAIR_CACHEAGE" | sed 's/[^0-9]*//g')" ]; then + WriteLog "PrunePhotoTranscoder - ERROR: DBREPAIR_CACHEAGE is not a valid integer. Ignoring '$DBREPAIR_CACHEAGE'" + Output "ERROR: DBREPAIR_CACHEAGE is not a valid integer. Ignoring '$DBREPAIR_CACHEAGE'" + return + else + CacheAge=$DBREPAIR_CACHEAGE + fi + fi + + # If scripted / command line options, clean automatically + if [ $Scripted -eq 1 ]; then + PruneIt=1 + else + Output "Counting how many files are more than $CacheAge days old." + FileCount=$(find "$TransCacheDir" \( -name \*.jpg -o -name \*.jpeg -o -name \*.png \) -mtime +${CacheAge} -print | wc -l) + + # If nothing found, continue back to the menu + [ $FileCount -eq 0 ] && Output "No files found to prune." && return + + # Ask if we should remove it + if ConfirmYesNo "OK to prune $FileCount files? "; then + PruneIt=1 + fi + fi + + # Prune old the jpgs/jpegs ? + if [ $PruneIt -eq 1 ]; then + Output "Pruning started." + WriteLog "Prune - Removing $FileCount files over $CacheAge days old." + find "$TransCacheDir" \( -name \*.jpg -o -name \*.jpeg -o -name \*.png \) -mtime +${CacheAge} -delete + Output "Pruning completed." + WriteLog "Prune - PASS." + fi + +} + ############################################################# # Main utility begins here # ############################################################# +# Set Script Path +ScriptPath="$(readlink -f "$0")" +ScriptName="$(basename "$ScriptPath")" +ScriptWorkingDirectory="$(dirname "$ScriptPath")" + # Initialize LastName LastTimestamp SetLast "" "" @@ -1292,11 +1637,21 @@ Scripted=0 # Identify this host if ! HostConfig; then - Output 'Error: Unknown host. Current supported hosts are: QNAP, Syno, Netgear, Mac, ASUSTOR, WD (OS5), Linux wkstn/svr' + Output 'Error: Unknown host. Current supported hosts are: QNAP, Syno, Netgear, Mac, ASUSTOR, WD (OS5), Linux wkstn/svr, SNAP' Output ' Current supported container images: Plexinc, LinuxServer, HotIO, & BINHEX' + Output ' ' + Output 'Are you trying to run the tool from outside the container environment ?' exit 1 fi +# If root required, confirm this script is running as root +if [ $RootRequired -eq 1 ] && [ $(id -u) -ne 0 ]; then + Output "ERROR: Tool running as username '$(whoami)'. '$HostType' requires 'root' user privilege." + Output " (e.g 'sudo -su root' or 'sudo bash')" + Output " Exiting." + exit 2 +fi + # We might not be root but minimally make sure we have write access if [ ! -w "$DBDIR" ]; then echo ERROR: Cannot write to Databases directory. Insufficient privilege. @@ -1308,6 +1663,13 @@ echo " " WriteLog "============================================================" WriteLog "Session start: Host is $HostType" +# Command line hidden options must come before commands +while [ "$(echo $1 | cut -c1)" = "-" ] +do + DoOptions "$1" + shift +done + # Make sure we have a logfile touch "$LOGFILE" @@ -1324,7 +1686,6 @@ mkdir -p "$DBDIR/$DBTMP" export TMPDIR="$DBTMP" export TMP="$DBTMP" - # If command line args then set flag Scripted=0 [ "$1" != "" ] && Scripted=1 @@ -1350,10 +1711,10 @@ cd "$DBDIR" # Get the owning UID/GID before we proceed so we can restore Owner="$(stat $STATFMT '%u:%g' $CPPL.db)" +Perms="$(stat $STATFMT $STATPERMS $CPPL.db)" # Sanity check, We are either owner of the DB or root if [ ! -w $CPPL.db ]; then - Output "Do not have write permission to the Databases. Exiting." WriteLog "No write permission to databases+. Exit." exit 1 @@ -1380,29 +1741,46 @@ do echo "" echo "Select" echo "" - [ $HaveStartStop -gt 0 ] && echo " 1 - 'stop' - Stop PMS" - [ $HaveStartStop -eq 0 ] && echo " 1 - 'stop' - (Not available. Stop manually)" - echo " 2 - 'automatic' - database check, repair/optimize, and reindex in one step." - echo " 3 - 'check' - Perform integrity check of database" - echo " 4 - 'vacuum' - Remove empty space from database" - echo " 5 - 'repair' - Repair/Optimize databases" - echo " 6 - 'reindex' - Rebuild database database indexes" + [ $HaveStartStop -gt 0 ] && echo " 1 - 'stop' - Stop PMS." + [ $HaveStartStop -eq 0 ] && echo " 1 - 'stop' - (Not available. Stop manually.)" + echo " 2 - 'automatic' - Check, Repair/Optimize, and Reindex Database in one step." + echo " 3 - 'check' - Perform integrity check of database." + echo " 4 - 'vacuum' - Remove empty space from database without optimizing." + echo " 5 - 'repair' - Repair/Optimize databases." + echo " 6 - 'reindex' - Rebuild database database indexes." [ $HaveStartStop -gt 0 ] && echo " 7 - 'start' - Start PMS" [ $HaveStartStop -eq 0 ] && echo " 7 - 'start' - (Not available. Start manually)" echo "" - echo " 8 - 'import' - Import watch history from another database independent of Plex. (risky)" - echo " 9 - 'replace' - Replace current databases with newest usable backup copy (interactive)" - echo " 10 - 'show' - Show logfile" - echo " 11 - 'status' - Report status of PMS (run-state and databases)" - echo " 12 - 'undo' - Undo last successful command" - echo "" + echo " 8 - 'import' - Import watch history from another database independent of Plex. (risky)." + echo " 9 - 'replace' - Replace current databases with newest usable backup copy (interactive)." + echo " 10 - 'show' - Show logfile." + echo " 11 - 'status' - Report status of PMS (run-state and databases)." + echo " 12 - 'undo' - Undo last successful command." - echo " 99 - exit" + echo "" + echo " 21 - 'prune' - Prune (remove) old image files (jpeg,jpg,png) from PhotoTranscoder cache." + [ $IgnoreErrors -eq 0 ] && echo " 42 - 'ignore' - Ignore duplicate/constraint errors." + [ $IgnoreErrors -eq 1 ] && echo " 42 - 'honor' - Honor all database errors." + + echo "" + echo " 88 - 'update' - Check for updates." + echo " 99 - 'quit' - Quit immediately. Keep all temporary files." + echo " 'exit' - Exit with cleanup options." fi + if [ $Scripted -eq 0 ]; then echo "" printf "Enter command # -or- command name (4 char min) : " + else + Input="$1" + + # If end of line then force exit + if [ "$Input" = "" ]; then + Input="exit" + Exit=1 + Output "Unexpected EOF / End of command line options. Exiting. Keeping temp files." + fi fi # Watch for null command whether scripted or not. @@ -1416,7 +1794,7 @@ do # Handle EOF/forced exit if [ "$Input" = "" ] ; then if [ $NullCommands -gt 4 ]; then - Output "Unexpected EOF / End of command line options, Exiting" + Output "Unexpected EOF / End of command line options. Exiting. Keeping temp files. " Input="exit" && Exit=1 else NullCommands=$(($NullCommands + 1)) @@ -1689,30 +2067,100 @@ do ;; - # Quit/Exit - 99|exit|quit) + # Remove (prune) unused image files from PhotoTranscoder Cache > 30 days old. + 21|prun*|remo*) - # if cmd line mode, exit clean + # Check if PMS running + if IsRunning; then + WriteLog "Prune - FAIL - PMS runnning" + Output "Unable to prune PhotoTranscoder cache. PMS is running." + continue + fi + + WriteLog "Prune - START" + DoPrunePhotoTranscoder + WriteLog "Prune - PASS" + ;; + + + # Ignore/Honor errors + 42|igno*|hono*) + + IgnoreErrors=$((IgnoreErrors ^ 1)) + [ $IgnoreErrors -eq 0 ] && Output "Honoring database errors." && WriteLog "Honoring database errors." + [ $IgnoreErrors -eq 1 ] && Output "Ignoring database errors." && WriteLog "Ignoring database errors." + ;; + + 88|upda*) + + DoUpdate=0 + Output "Checking for update" + GetLatestRelease + if [ $(VersionDigits $LatestVersion) -gt $(VersionDigits $Version) ]; then + if [ $Scripted -eq 1 ]; then + DoUpdate=1 + elif ConfirmYesNo "Download $LatestVersion and update?"; then + DoUpdate=1 + fi + else + Output "No update available." + fi + + # Check if script path is writable + if [ $DoUpdate -eq 1 ]; then + if [ -w "$ScriptWorkingDirectory" ]; then + Output "Updating from $Version to $LatestVersion" + DownloadAndUpdate "https://raw.githubusercontent.com/ChuckPa/PlexDBRepair/master/DBRepair.sh" "$ScriptWorkingDirectory/$ScriptName" + Result=$? + if [ $Result -eq 0 ]; then + chmod +x "$ScriptWorkingDirectory/$ScriptName" + Output "Restart to launch updated DBRepair.sh ($LatestVersion)" + WriteLog "Update - Updated to version $LatestVersion." + exit 0 + else + Output "Unable to download and update. Error $Result." + fi + else + Output "Script path '${ScriptName}' is not writable." + fi + fi + + [ $Scripted -eq 1 ] && [ $DoUpdate -eq 1 ] && exit 0 + ;; + + # Quit + 99|quit) + + Output "Retaining all temporary work files." + WriteLog "Exit - Retain temp files." + exit 0 + ;; + + # Orderly Exit + exit) + + # If forced exit set, exit and retain + if [ $Exit -eq 1 ]; then + Output "Unexpected exit command. Keeping all temporary work files." + WriteLog "EOFExit - Retain temp files." + exit 1 + fi + + # If cmd line mode, exit clean without asking if [ $Scripted -eq 1 ]; then rm -rf $TMPDIR WriteLog "Exit - Delete temp files." else # Ask questions on interactive exit - if [ $Exit -eq 0 ]; then - # Ask if the user wants to remove the DBTMP directory and all backups thus far - if [ "$Input" = "exit" ] && ConfirmYesNo "Ok to remove temporary databases/workfiles for this session?" ; then - # There it goes - Output "Deleting all temporary work files." - WriteLog "Exit - Delete temp files." - rm -rf "$TMPDIR" - else - Output "Retaining all temporary work files." - WriteLog "Exit - Retain temp files." - fi + if ConfirmYesNo "Ok to remove temporary databases/workfiles for this session?" ; then + # There it goes + Output "Deleting all temporary work files." + WriteLog "Exit - Delete temp files." + rm -rf "$TMPDIR" else - Output "Unexpected exit command. Keeping all temporary work files." - WriteLog "EOFExit - Retain temp files." + Output "Retaining all temporary work files." + WriteLog "Exit - Retain temp files." fi fi diff --git a/remove_lonely_collections.py b/remove_lonely_collections.py index 9811156..78985fd 100644 --- a/remove_lonely_collections.py +++ b/remove_lonely_collections.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- import os -import sys import pathlib import subprocess import requests @@ -47,18 +46,22 @@ class PLEX_INFO(): return '%s%s:%s' % (self.proto, self.address, str(self.port)) +print("Gathering local Plex information.") plex_info = PLEX_INFO() +print("Plex server is running at %s and using token %s" % (plex_info.url, plex_info.token)) +sess = requests.Session() +sess.verify = False +print("Connecting to Plex server API.") +plex = PlexServer(plex_info.url, plex_info.token, session=sess) -print(plex_info.token) -print(plex_info.url) - -#sess = requests.Session() -#sess.verify = False -#plex = PlexServer(plex_info.url, plex_info.token, session=sess) - -#all_libraries = plex.library.sections() -#for library in all_libraries: -# for collection in library.search(libtype="collection"): -# if collection.childCount == 0: -# collection.delete() +all_libraries = plex.library.sections() +print("Checking %s libraries" % len(all_libraries)) +for library in all_libraries: + print("Checking library: %s" % library) + collection_delete_count = 0 + for collection in library.search(libtype="collection"): + if collection.childCount == 0: + collection.delete() + collection_delete_count += 1 + print("Deleted %s collections from %s library" % (collection_delete_count, library))