Skip to content

Instantly share code, notes, and snippets.

@jetfir3
Last active October 3, 2026 18:11
Show Gist options
  • Select an option

  • Save jetfir3/f620e44fc246c1bed45ed040bbfa2d68 to your computer and use it in GitHub Desktop.

Select an option

Save jetfir3/f620e44fc246c1bed45ed040bbfa2d68 to your computer and use it in GitHub Desktop.
Uninstallify -- remove Spotify + app/cache data
#!/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 $?
@jetfir3

jetfir3 commented Nov 21, 2023

Copy link
Copy Markdown
Author
  • Run script to completely uninstall Spotify on Linux and macOS
  • If found, script will ask before deleting app, app data, cache data

Run directly with:

bash <(curl -sSL https://gist.github.com/jetfir3/f620e44fc246c1bed45ed040bbfa2d68/raw/uninstallify.sh)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment