Last active
October 3, 2026 18:11
-
-
Save jetfir3/f620e44fc246c1bed45ed040bbfa2d68 to your computer and use it in GitHub Desktop.
Uninstallify -- remove Spotify + app/cache data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Uninstallify -- remove Spotify + app/cache data | |
| case $(uname | tr '[:upper:]' '[:lower:]') in | |
| darwin*) platformType='macOS' ;; | |
| linux*) platformType='Linux' ;; | |
| *) echo "Unsupported operating system" >&2; exit 1 ;; | |
| esac | |
| ((EUID == 0)) && { echo "Run this script without sudo. It requests elevated permission only when needed." >&2; exit 1; } | |
| safe_path() { | |
| case "${1}/" in | |
| /|//|*/../*|*/./*|*//*|*$'\n'*|*$'\r'*) return 1 ;; | |
| /*) return 0 ;; | |
| *) return 1 ;; | |
| esac | |
| } | |
| safe_path "${HOME}" && [[ -d "${HOME}" ]] || { echo "Invalid home directory" >&2; exit 1; } | |
| { : </dev/tty; } 2>/dev/null || { echo "This script requires an interactive terminal" >&2; exit 1; } | |
| trap 'printf "\nCancelled.\n" >&2; exit 130' INT | |
| trap 'printf "\nCancelled.\n" >&2; exit 143' TERM | |
| checkedPaths=() keptPaths=() keptPackages=() | |
| removed=0 failures=0 unverified=0 nativeKept='' | |
| read_yn() { | |
| local yn | |
| while : ; do | |
| read -r -p "$* [y/n, then Enter] " yn </dev/tty || { printf '\nCancelled: terminal input ended.\n' >&2; exit 130; } | |
| case "${yn}" in | |
| [Yy]|[Yy][Ee][Ss]) return 0 ;; | |
| [Nn]|[Nn][Oo]) return 1 ;; | |
| *) echo "Please enter [y]es or [n]o." ;; | |
| esac | |
| done | |
| } | |
| sudo_run() { | |
| command -v sudo >/dev/null 2>&1 || { echo "sudo is required for this operation." >&2; return 1; } | |
| sudo -- "$@" | |
| } | |
| display_path() { | |
| case "${1}" in | |
| "${HOME}") printf '~' ;; | |
| "${HOME}"/*) printf '~/%s' "${1#"${HOME}/"}" ;; | |
| *) printf '%s' "${1}" ;; | |
| esac | |
| } | |
| path_state() { | |
| local parent="${1%/*}" | |
| [[ -e "${1}" || -L "${1}" ]] && return 0 | |
| while [[ -n "${parent}" && "${parent}" != "/" ]]; do | |
| [[ -d "${parent}" ]] && { [[ -x "${parent}" ]] && return 1; return 2; } | |
| [[ -e "${parent}" || -L "${parent}" ]] && return 2 | |
| parent="${parent%/*}" | |
| done | |
| return 1 | |
| } | |
| remove_package() { | |
| local kind="${1}" scope="${2}" package="${3}" | |
| case "${kind}" in | |
| snap) sudo_run snap remove --purge spotify ;; | |
| flatpak) flatpak uninstall "${scope}" --noninteractive --delete-data -y "${package}" ;; | |
| deb) | |
| command -v apt-get >/dev/null 2>&1 && { sudo_run apt-get purge -y "${package}"; return; } | |
| sudo_run dpkg --purge "${package}" | |
| ;; | |
| arch) sudo_run pacman -Rn --noconfirm "${package}" ;; | |
| rpm) | |
| command -v dnf >/dev/null 2>&1 && { sudo_run dnf --setopt=clean_requirements_on_remove=False remove -y "${package}"; return; } | |
| command -v yum >/dev/null 2>&1 && { sudo_run yum --setopt=clean_requirements_on_remove=False remove -y "${package}"; return; } | |
| command -v zypper >/dev/null 2>&1 && { sudo_run zypper --non-interactive remove "${package}"; return; } | |
| echo "No supported RPM package manager found." >&2 | |
| return 1 | |
| ;; | |
| esac | |
| } | |
| delete_paths() { | |
| local pathName="${1}" path shown state previous duplicate | |
| shift | |
| for path in "$@"; do | |
| duplicate='' | |
| for previous in "${checkedPaths[@]}"; do [[ "${path}" == "${previous}" ]] && duplicate='true'; done | |
| [[ "${duplicate}" ]] && continue | |
| safe_path "${path}" && [[ "${path}" != "${HOME}" ]] || { echo "Unsafe cleanup path refused." >&2; failures=$((failures + 1)); continue; } | |
| checkedPaths+=("${path}") | |
| shown=$(display_path "${path}") | |
| path_state "${path}"; state=$? | |
| [[ "${state}" == 1 ]] && continue | |
| [[ "${state}" == 2 ]] && { echo "Could not inspect: ${shown}" >&2; continue; } | |
| [[ "${pathName}" == App && "${nativeKept}" ]] && { echo "Kept: ${shown} (package removal incomplete)"; keptPaths+=("${path}"); continue; } | |
| read_yn "Delete ${pathName}: ${shown}?" || { echo "Kept: ${shown}"; keptPaths+=("${path}"); continue; } | |
| [[ -L "${path}" ]] && { echo "Removing the link only; its target will not be erased: ${shown}"; unverified=$((unverified + 1)); } | |
| rm -rf -- "${path}" 2>/dev/null | |
| path_state "${path}"; state=$? | |
| [[ "${state}" != 1 ]] && { sudo_run rm -rf -- "${path}" 2>/dev/null; path_state "${path}"; state=$?; } | |
| [[ "${state}" == 1 ]] && { echo "Removed: ${shown}"; removed=$((removed + 1)); } || { echo "Removal failed or could not be verified: ${shown}" >&2; failures=$((failures + 1)); } | |
| done | |
| } | |
| list_packages() { | |
| local data package scope ref status | |
| command -v snap >/dev/null 2>&1 && { | |
| data=$(LC_ALL=C snap list 2>/dev/null) && { | |
| while read -r package status; do [[ "${package}" == spotify ]] && echo 'snap||spotify'; done <<< "${data}" | |
| : | |
| } || echo '?|snap|Could not query Snap packages' | |
| } | |
| command -v flatpak >/dev/null 2>&1 && { | |
| data=$(LC_ALL=C flatpak list --app --columns=ref,installation 2>/dev/null) && { | |
| while read -r ref scope; do | |
| case "${ref}" in | |
| app/com.spotify.Client/*|com.spotify.Client/*) | |
| case "${scope}" in | |
| user) scope='--user' ;; | |
| system|default) scope='--system' ;; | |
| ''|*[!a-zA-Z0-9_.-]*) echo '?|flatpak|Could not identify a Spotify Flatpak installation'; continue ;; | |
| *) scope="--installation=${scope}" ;; | |
| esac | |
| printf 'flatpak|%s|%s\n' "${scope}" "${ref}" | |
| ;; | |
| esac | |
| done <<< "${data}" | |
| : | |
| } || echo '?|flatpak|Could not query Flatpak installations' | |
| } | |
| command -v dpkg-query >/dev/null 2>&1 && { | |
| data=$(dpkg-query -W -f='${Package} ${Status}\n' 2>/dev/null) && { | |
| while read -r package status; do | |
| case "${package}" in | |
| spotify|spotify-client) [[ "${status}" == *' not-installed' ]] || printf 'deb||%s\n' "${package}" ;; | |
| esac | |
| done <<< "${data}" | |
| : | |
| } || echo '?|deb|Could not query DEB packages' | |
| } | |
| command -v pacman >/dev/null 2>&1 && { | |
| data=$(pacman -Qq 2>/dev/null) && { | |
| while read -r package; do case "${package}" in spotify|spotify-launcher) printf 'arch||%s\n' "${package}" ;; esac; done <<< "${data}" | |
| : | |
| } || echo '?|arch|Could not query Arch packages' | |
| } | |
| command -v rpm >/dev/null 2>&1 && { | |
| data=$(rpm -qa --qf '%{NAME}\n' 2>/dev/null) && { | |
| while read -r package; do case "${package}" in spotify|spotify-client) printf 'rpm||%s\n' "${package}" ;; esac; done <<< "${data}" | |
| : | |
| } || echo '?|rpm|Could not query RPM packages' | |
| } | |
| return 0 | |
| } | |
| remove_linux_packages() { | |
| local entries kind scope package key after | |
| entries=$(list_packages) | |
| while IFS='|' read -r kind scope package; do | |
| [[ -z "${kind}" ]] && continue | |
| [[ "${kind}" == '?' ]] && { echo "${package}" >&2; nativeKept='true'; unverified=$((unverified + 1)); continue; } | |
| key="${kind}|${scope}|${package}" | |
| read_yn "Remove ${kind} package ${package} ${scope} (including its package-managed data)?" || { | |
| keptPackages+=("${key}") | |
| echo "Kept package: ${package}" | |
| case "${kind}" in deb|arch|rpm) nativeKept='true' ;; esac | |
| continue | |
| } | |
| remove_package "${kind}" "${scope}" "${package}" </dev/tty || { echo "Package removal failed: ${package}" >&2; failures=$((failures + 1)); } | |
| after=$(list_packages) | |
| case $'\n'"${after}"$'\n' in | |
| *$'\n'"${key}"$'\n'*|*$'\n?|'"${kind}"'|'*) | |
| echo "Package removal not verified: ${package}" >&2 | |
| case "${kind}" in deb|arch|rpm) nativeKept='true' ;; esac | |
| ;; | |
| *) echo "Removed package: ${package}"; removed=$((removed + 1)) ;; | |
| esac | |
| done <<< "${entries}" | |
| } | |
| stop_spotify() { | |
| local state attempt | |
| command -v pgrep >/dev/null 2>&1 && command -v pkill >/dev/null 2>&1 || { echo "Cannot verify Spotify processes. Install pgrep/pkill and try again." >&2; exit 1; } | |
| pgrep -u "${UID}" -x 'Spotify|spotify|Spotify Helper.*|spotify-launcher' >/dev/null 2>&1; state=$? | |
| [[ "${state}" == 1 ]] && return 0 | |
| [[ "${state}" != 0 ]] && { echo "Could not inspect Spotify processes." >&2; exit 1; } | |
| echo "Closing Spotify for the current user..." | |
| pkill -TERM -u "${UID}" -x 'Spotify|spotify|Spotify Helper.*|spotify-launcher' 2>/dev/null | |
| for attempt in 1 2 3 4 5; do | |
| sleep 1 | |
| pgrep -u "${UID}" -x 'Spotify|spotify|Spotify Helper.*|spotify-launcher' >/dev/null 2>&1; state=$? | |
| [[ "${state}" == 1 ]] && return 0 | |
| [[ "${state}" != 0 ]] && break | |
| done | |
| [[ "${state}" == 0 ]] && { pkill -KILL -u "${UID}" -x 'Spotify|spotify|Spotify Helper.*|spotify-launcher' 2>/dev/null; sleep 1; } | |
| pgrep -u "${UID}" -x 'Spotify|spotify|Spotify Helper.*|spotify-launcher' >/dev/null 2>&1 | |
| [[ $? == 1 ]] || { echo "Spotify is still running or could not be checked. Cleanup cancelled." >&2; exit 1; } | |
| } | |
| verify_cleanup() { | |
| local path state kept item kind scope package key entries | |
| local remaining=0 intentional=0 | |
| printf '\nChecking for leftovers...\n' | |
| for path in "${checkedPaths[@]}"; do | |
| path_state "${path}"; state=$? | |
| [[ "${state}" == 1 ]] && continue | |
| [[ "${state}" == 2 ]] && { echo "Could not verify: $(display_path "${path}")"; unverified=$((unverified + 1)); continue; } | |
| kept='' | |
| for item in "${keptPaths[@]}"; do [[ "${path}" == "${item}" ]] && kept='true'; done | |
| [[ "${kept}" ]] && { echo "Intentionally kept: $(display_path "${path}")"; intentional=$((intentional + 1)); } || { echo "Still present: $(display_path "${path}")"; remaining=$((remaining + 1)); } | |
| done | |
| [[ "${platformType}" == Linux ]] && { | |
| entries=$(list_packages) | |
| while IFS='|' read -r kind scope package; do | |
| [[ -z "${kind}" ]] && continue | |
| [[ "${kind}" == '?' ]] && { echo "${package}"; unverified=$((unverified + 1)); continue; } | |
| key="${kind}|${scope}|${package}"; kept='' | |
| for item in "${keptPackages[@]}"; do [[ "${key}" == "${item}" ]] && kept='true'; done | |
| [[ "${kept}" ]] && { echo "Intentionally kept package: ${package} ${scope}"; intentional=$((intentional + 1)); } || { echo "Package still present: ${package} ${scope}"; remaining=$((remaining + 1)); } | |
| done <<< "${entries}" | |
| } | |
| pgrep -u "${UID}" -x 'Spotify|spotify|Spotify Helper.*|spotify-launcher' >/dev/null 2>&1; state=$? | |
| [[ "${state}" == 0 ]] && { echo "Spotify started again during cleanup. Close it and rerun this script."; remaining=$((remaining + 1)); } | |
| [[ "${state}" != 0 && "${state}" != 1 ]] && { echo "Could not verify Spotify processes."; unverified=$((unverified + 1)); } | |
| printf '\nRemoved: %s; kept: %s; still present: %s; failed operations: %s; unverified: %s.\n' "${removed}" "${intentional}" "${remaining}" "${failures}" "${unverified}" | |
| echo "Checked known Spotify locations for this user and supported package managers. Unlisted custom locations and other users are not covered." | |
| ((remaining + failures + unverified > 0)) && { echo "Cleanup incomplete. Review the messages above."; return 1; } | |
| ((intentional > 0)) && echo "Finished with the items you chose to keep." || echo "Finished. No leftovers found in the checked locations." | |
| return 0 | |
| } | |
| echo "This removes Spotify and its local settings, logins, downloads, and caches. You will be asked before each removal." | |
| read_yn "Continue?" || exit 0 | |
| stop_spotify | |
| [[ "${platformType}" == "Linux" ]] && { | |
| remove_linux_packages | |
| delete_paths "App" \ | |
| "/opt/spotify" \ | |
| "/usr/share/spotify" \ | |
| "/usr/lib/spotify" \ | |
| "/usr/lib64/spotify" \ | |
| "${HOME}/.local/share/spotify" \ | |
| "${HOME}/.local/share/spotify-launcher" | |
| delete_paths "App cache" \ | |
| "${HOME}/.cache/spotify" \ | |
| "${HOME}/.cache/spotify-launcher" \ | |
| "${HOME}/.cache/spotx-bash" | |
| delete_paths "App data" \ | |
| "${HOME}/.config/spotify" \ | |
| "${HOME}/.var/app/com.spotify.Client" \ | |
| "${HOME}/snap/spotify" \ | |
| "/var/snap/spotify" | |
| delete_paths "App data" "${HOME}/.local/state/spotify" "${HOME}/.local/state/spotify-launcher" | |
| for xdgName in XDG_CONFIG_HOME XDG_CACHE_HOME XDG_DATA_HOME XDG_STATE_HOME; do | |
| xdgRoot="${!xdgName}" | |
| [[ -z "${xdgRoot}" ]] && continue | |
| xdgRoot="${xdgRoot%/}" | |
| safe_path "${xdgRoot}" || { echo "Invalid ${xdgName}; custom location not checked." >&2; unverified=$((unverified + 1)); continue; } | |
| xdgLabel='App data' | |
| [[ "${xdgName}" == XDG_DATA_HOME ]] && xdgLabel=App | |
| delete_paths "${xdgLabel}" "${xdgRoot}/spotify" "${xdgRoot}/spotify-launcher" | |
| [[ "${xdgName}" == XDG_CACHE_HOME ]] && delete_paths Cache "${xdgRoot}/spotx-bash" | |
| done | |
| command -v snap >/dev/null 2>&1 && echo "Snap note: --purge prevents a new backup. Existing Snap snapshots are not deleted; review 'snap saved' if needed." | |
| } | |
| [[ "${platformType}" == "macOS" ]] && { | |
| delete_paths "App" \ | |
| "/Applications/Spotify.app" \ | |
| "${HOME}/Applications/Spotify.app" | |
| delete_paths "App cache" \ | |
| "${HOME}/Library/Caches/com.spotify.client" \ | |
| "${HOME}/Library/Caches/com.spotify.client.ShipIt" \ | |
| "${HOME}/Library/Caches/com.spotify.client.helper" | |
| delete_paths "App data" \ | |
| "${HOME}/Library/Application Support/Spotify" \ | |
| "${HOME}/Library/Application Support/com.spotify.client.ShipIt" \ | |
| "${HOME}/Library/Preferences/com.spotify.client.plist" \ | |
| "${HOME}/Library/Preferences/com.spotify.client.helper.plist" \ | |
| "${HOME}/Library/Saved Application State/com.spotify.client.savedState" \ | |
| "${HOME}/Library/HTTPStorages/com.spotify.client" \ | |
| "${HOME}/Library/HTTPStorages/com.spotify.client.helper" \ | |
| "${HOME}/Library/HTTPStorages/com.spotify.client.binarycookies" \ | |
| "${HOME}/Library/HTTPStorages/com.spotify.client.helper.binarycookies" \ | |
| "${HOME}/Library/Cookies/com.spotify.client.binarycookies" \ | |
| "${HOME}/Library/WebKit/com.spotify.client" \ | |
| "${HOME}/Library/Logs/Spotify" | |
| for darwinName in DARWIN_USER_CACHE_DIR DARWIN_USER_TEMP_DIR; do | |
| darwinRoot=$(getconf "${darwinName}" 2>/dev/null) | |
| darwinRoot="${darwinRoot%/}" | |
| safe_path "${darwinRoot}" || { echo "Could not resolve ${darwinName}." >&2; unverified=$((unverified + 1)); continue; } | |
| delete_paths Cache "${darwinRoot}/com.spotify.client" "${darwinRoot}/com.spotify.client.helper" | |
| done | |
| } | |
| verify_cleanup | |
| exit $? |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Run directly with: