Compare commits

..
14 Commits
Author SHA1 Message Date
jetfir3andGitHub a68b29fd9d Support 1.2.95.453.g0eeebbed 2026-08-05 20:10:27 -04:00
jetfir3andGitHub 307db7769d Fix typo regression 2026-08-04 13:30:02 -04:00
jetfir3andGitHub 73b74cbd65 Support 1.2.95.452.g5c9bdf32 2026-08-04 13:23:23 -04:00
jetfir3 03207b6117 Add binary validation 2026-07-29 15:38:54 -04:00
jetfir3 b6df3cdcc5 Fix typos 2026-07-29 15:20:56 -04:00
jetfir3 876e4dc0cd Improve macOS installation and codesigning 2026-07-29 14:53:59 -04:00
jetfir3 f4b9f7d20f Fix Linux rollback handling 2026-07-29 14:19:27 -04:00
jetfir3 852acb2acb Improve SPA safety 2026-07-29 13:57:25 -04:00
jetfir3 7f190c6660 Harden Linux client patching 2026-07-29 12:40:29 -04:00
jetfir3 8a2e356e8d Add Snap package support 2026-07-28 18:55:50 -04:00
jetfir3andGitHub 140f86488d Support 1.2.94.583.g60394bd5 2026-07-28 16:45:20 -04:00
jetfir3 b350dbe57d Add Nix build mode 2026-07-27 13:29:42 -04:00
jetfir3 38c1844f28 Improve flatpak detection and handling 2026-07-27 11:23:53 -04:00
jetfir3andGitHub b65e1b7d19 Support 1.2.94.583.g60394bd5 2026-07-16 15:02:27 -04:00
4 changed files with 811 additions and 117 deletions
+1
View File
@@ -0,0 +1 @@
.DS_Store
+13 -1
View File
@@ -12,7 +12,7 @@
<center>
<h4 align="center">Adblock for the Spotify desktop client on Linux & macOS</h4>
<p align="center">
<strong>Latest supported version:</strong> 1.2.93.667.g7b5cc0ce
<strong>Latest supported version:</strong> 1.2.95.453.g0eeebbed
</p>
</center>
@@ -44,6 +44,18 @@ bash <(curl -sSL https://raw.githubusercontent.com/SpotX-Official/SpotX-Bash/mai
- View additional flags/options and examples in the `Options` section below
- For more information, see the [FAQ](https://github.com/SpotX-Official/SpotX-Bash/wiki/SpotX%E2%80%90Bash-FAQ)
### Snap / NixOS:
Spotify installations using Snap require the included `spotx-snap.sh` helper. Download the repo so `spotx-snap.sh` and `spotx.sh` remain in the same directory:
```
git clone https://github.com/SpotX-Official/SpotX-Bash.git
cd SpotX-Bash
bash spotx-snap.sh
```
Run `bash spotx-snap.sh --help` for additional Snap options or see the [FAQ](https://github.com/SpotX-Official/SpotX-Bash/wiki/SpotX%E2%80%90Bash-FAQ#is-the-snap-version-of-spotify-supported).
NixOS users should use [SpotX-Nix](https://github.com/SpotX-Official/SpotX-Nix), which applies SpotX-Bash while building the Spotify package without modifying the Nix store.
### Options:
<details>
<summary>Click to expand!</summary>
Executable
+342
View File
@@ -0,0 +1,342 @@
#!/usr/bin/env bash
set -euo pipefail
clr='\033[0m'
green='\033[0;32m'
red='\033[0;31m'
yellow='\033[0;33m'
show_help() {
echo -e \
"Usage: bash spotx-snap.sh [helper options] [SpotX-Bash options]
Helper options:
--allow-unverified : allow an unverifiable snap set by '--snap-file'
--build-only : create patched snap without installing
--channel <channel> : select stable, candidate, beta, or edge with '--download'
--download : download Spotify snap instead of using a local source
--help : print this help message
--output-dir <path> : set output directory for '--build-only'
--restore : restore official store-managed Spotify snap
--snap-file <path> : use a specific local Spotify snap
--uninstall : same as '--restore'
All other supported options are passed to SpotX-Bash.
"
}
error() {
echo -e "${red}Error:${clr} $*\n" >&2
exit 1
}
latest_cached_snap() {
local version="${1:-}" candidate latest=''
for candidate in "${cacheDir}"/spotify_*.snap; do
[[ -f "${candidate}" ]] || continue
[[ -z "${version}" || "${candidate##*/}" == "spotify_${version}_"*.snap ]] || continue
[[ -z "${latest}" || "${candidate}" -nt "${latest}" ]] && latest="${candidate}"
done
[[ -n "${latest}" ]] && printf '%s\n' "${latest}"
}
has_spotx_marker() {
local spa="${1}" entry markerFile="${workDir}/marker.js"
for entry in xpui.js xpui-snapshot.js; do
"${sudoCmd[@]}" unzip -p "${spa}" "${entry}" > "${markerFile}" 2>/dev/null || true
grep -Fq "//# SpotX was here" "${markerFile}" && return 0
done
return 1
}
verify_snap_source() {
local snap="${1}" digest size assertion assertedDigest snapId assertedSize declaration declaredName candidate remoteAssertion
digest=$("${sudoCmd[@]}" snap info --verbose "${snap}" 2>/dev/null | awk '$1 == "sha3-384:" { print $2; exit }')
size=$("${sudoCmd[@]}" stat -c '%s' "${snap}")
[[ -n "${digest}" ]] || return 1
assertion=$(LC_ALL=C snap known snap-revision snap-sha3-384="${digest}" 2>/dev/null || true)
[[ -n "${assertion}" ]] || {
for candidate in "$(dirname -- "${sourceSnap}")"/*.assert; do
[[ -f "${candidate}" ]] || continue
"${sudoCmd[@]}" grep -Fqx "snap-sha3-384: ${digest}" "${candidate}" 2>/dev/null || continue
"${sudoCmd[@]}" snap ack "${candidate}" >/dev/null 2>&1 || continue
assertion=$(LC_ALL=C snap known snap-revision snap-sha3-384="${digest}" 2>/dev/null || true)
[[ -n "${assertion}" ]] && break
done
}
[[ -n "${assertion}" ]] || {
remoteAssertion=$(LC_ALL=C timeout 20 snap known --remote snap-revision snap-sha3-384="${digest}" 2>/dev/null || true)
[[ -n "${remoteAssertion}" ]] && {
printf '%s\n' "${remoteAssertion}" > "${workDir}/snap-revision.assert"
"${sudoCmd[@]}" snap ack "${workDir}/snap-revision.assert" >/dev/null 2>&1 || true
assertion="${remoteAssertion}"
}
}
[[ -n "${assertion}" ]] || return 1
assertedDigest=$(awk '$1 == "snap-sha3-384:" { print $2; exit }' <<< "${assertion}")
snapId=$(awk '$1 == "snap-id:" { print $2; exit }' <<< "${assertion}")
assertedSize=$(awk '$1 == "snap-size:" { print $2; exit }' <<< "${assertion}")
[[ "${assertedDigest}" == "${digest}" && -n "${snapId}" && "${assertedSize}" == "${size}" ]] || return 1
declaration=$(LC_ALL=C snap known snap-declaration snap-id="${snapId}" 2>/dev/null || true)
[[ -n "${declaration}" ]] || {
remoteAssertion=$(LC_ALL=C timeout 20 snap known --remote snap-declaration snap-id="${snapId}" 2>/dev/null || true)
[[ -n "${remoteAssertion}" ]] && {
printf '%s\n' "${remoteAssertion}" > "${workDir}/snap-declaration.assert"
"${sudoCmd[@]}" snap ack "${workDir}/snap-declaration.assert" >/dev/null 2>&1 || true
declaration="${remoteAssertion}"
}
}
declaredName=$(awk '$1 == "snap-name:" { print $2; exit }' <<< "${declaration}")
[[ "${declaredName}" == "spotify" ]] || return 1
sourceVerified='true'
}
cleanup() {
[[ -n "${workDir:-}" && -d "${workDir}" && "${workDir}" == "${tempBase}/spotx-snap."* ]] && "${sudoCmd[@]}" rm -rf -- "${workDir}"
}
snapChannel='stable'
snapFile=''
allowUnverified=''
downloadSnap=''
buildOnly=''
restoreSnap=''
channelSet=''
outputSet=''
outputDir="${PWD}"
spotxArgs=()
while (($#)); do
case "${1}" in
--allow-unverified) allowUnverified='true' ;;
--build-only) buildOnly='true' ;;
--channel)
(($# > 1)) || error "'--channel' requires an argument."
snapChannel="${2}"
channelSet='true'
shift
;;
--channel=*)
snapChannel="${1#*=}"
channelSet='true'
;;
--download) downloadSnap='true' ;;
--help) show_help; exit 0 ;;
--output-dir)
(($# > 1)) || error "'--output-dir' requires an argument."
outputDir="${2}"
outputSet='true'
shift
;;
--output-dir=*)
outputDir="${1#*=}"
outputSet='true'
;;
--restore|--uninstall) restoreSnap='true' ;;
--snap-file)
(($# > 1)) || error "'--snap-file' requires an argument."
snapFile="${2}"
shift
;;
--snap-file=*) snapFile="${1#*=}" ;;
-P*|-F*|--installdeb|--installmac|--rollback|--stable)
error "'${1}' cannot be used with spotx-snap.sh."
;;
-c|--clearcache)
error "Snap cache clearing is not supported by spotx-snap.sh."
;;
-v|--version|--logo)
error "'${1}' does not patch a snap and cannot be used with spotx-snap.sh."
;;
--) error "'--' is not supported by spotx-snap.sh." ;;
*) spotxArgs+=("${1}") ;;
esac
shift
done
[[ "$(uname -s)" == "Linux" ]] || error "spotx-snap.sh requires Linux."
[[ "$(uname -m)" == "x86_64" || "$(uname -m)" == "amd64" ]] || error "Spotify snap requires an x86_64 Linux system."
[[ -z "${allowUnverified}" || -n "${snapFile}" ]] || error "'--allow-unverified' requires '--snap-file'."
[[ -z "${snapFile}" || -z "${downloadSnap}" ]] || error "'--snap-file' and '--download' cannot be used together."
[[ -z "${channelSet}" || -n "${downloadSnap}" ]] || error "'--channel' requires '--download'."
[[ -z "${outputSet}" || -n "${buildOnly}" ]] || error "'--output-dir' requires '--build-only'."
[[ "${restoreSnap}" ]] && {
[[ -z "${snapFile}${downloadSnap}${buildOnly}${channelSet}${outputSet}" && ${#spotxArgs[@]} -eq 0 ]] || error "'--restore' cannot be combined with other options."
}
[[ "${snapChannel}" =~ ^(stable|candidate|beta|edge)$ ]] || error "Invalid snap channel '${snapChannel}'."
scriptDir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
spotxScript="${scriptDir}/spotx.sh"
[[ -f "${spotxScript}" ]] || error "spotx.sh not found beside spotx-snap.sh."
for requirement in snap unsquashfs unzip zip perl awk grep sha256sum mktemp getent install stat timeout; do
command -v "${requirement}" >/dev/null || error "${requirement} command not found."
done
runUser="${SUDO_USER:-$(id -un)}"
runUid=$(id -u "${runUser}")
runGid=$(id -g "${runUser}")
runHome=$(getent passwd "${runUser}" 2>/dev/null | awk -F: 'NR == 1 { print $6 }')
[[ -n "${runHome}" ]] || runHome="${HOME}"
cacheBase="${runHome}/.cache/spotx-bash"
cacheDir="${cacheBase}/snap"
sudoCmd=()
((EUID == 0)) || {
command -v sudo >/dev/null || error "sudo command not found."
sudo -n true >/dev/null 2>&1 || {
echo -e "This script requires sudo permission to preserve and install snap files.\nPlease enter your sudo password..."
sudo -v || error "Failed to obtain sudo permission."
}
sudoCmd=(sudo)
}
snap version >/dev/null 2>&1 || error "snapd is not available."
[[ "${restoreSnap}" ]] && {
LC_ALL=C snap list spotify >/dev/null 2>&1 || error "Spotify snap is not installed."
command pkill -9 '[sS]potify' 2>/dev/null || true
echo -e "Restoring official store-managed Spotify snap...\n"
"${sudoCmd[@]}" snap refresh --amend --channel=latest/stable spotify || error "Official Spotify snap restore failed."
echo
echo -e "${green}Finished${clr}\n"
exit 0
}
tempBase="${TMPDIR:-/tmp}"
tempBase="${tempBase%/}"
workDir=$(mktemp -d "${tempBase}/spotx-snap.XXXXXXXX")
trap cleanup EXIT
trap 'exit 130' HUP INT TERM
sourceDir="${workDir}/source"
snapRoot="${workDir}/squashfs-root"
packedDir="${workDir}/packed"
sourceCopy="${sourceDir}/spotify.snap"
mkdir -p "${sourceDir}" "${packedDir}"
sourceSnap=''
sourceOutput=''
[[ -n "${snapFile}" ]] && {
[[ -f "${snapFile}" ]] || error "Snap file not found: ${snapFile}"
sourceSnap=$(cd -- "$(dirname -- "${snapFile}")" && pwd)/$(basename -- "${snapFile}")
sourceOutput="${sourceSnap}"
}
[[ "${downloadSnap}" ]] && {
downloadDir="${workDir}/download"
mkdir -p "${downloadDir}"
echo -e "Downloading Spotify snap from ${snapChannel} channel...\n"
downloadOutput=$(cd "${downloadDir}" && snap download spotify --channel="${snapChannel}" 2>&1) || {
echo -e "${downloadOutput}\n" >&2
error "Spotify snap download failed."
}
for candidate in "${downloadDir}"/spotify_*.snap; do
[[ -f "${candidate}" ]] || continue
[[ -z "${sourceSnap}" ]] || error "Multiple Spotify snap downloads found."
sourceSnap="${candidate}"
done
[[ -n "${sourceSnap}" ]] || error "Spotify snap download not found."
sourceOutput="downloaded ${snapChannel} snap"
}
[[ -z "${sourceSnap}" ]] && {
installedOutput=$(LC_ALL=C snap list spotify 2>/dev/null || true)
installedVersion=$(awk 'NR == 2 { print $2 }' <<< "${installedOutput}")
installedRev=$(awk 'NR == 2 { print $3 }' <<< "${installedOutput}")
[[ -n "${installedRev}" && "${installedRev}" != x* ]] && {
installedSnap="/var/lib/snapd/snaps/spotify_${installedRev}.snap"
[[ -f "${installedSnap}" ]] && {
sourceSnap="${installedSnap}"
sourceOutput="installed Spotify snap revision ${installedRev}"
}
}
}
[[ -z "${sourceSnap}" ]] && {
installedCacheVersion=''
[[ -n "${installedVersion:-}" ]] && installedCacheVersion=$(printf '%s' "${installedVersion}" | tr -c 'A-Za-z0-9._-' '_')
cachedSnap=$(latest_cached_snap "${installedCacheVersion}" || true)
[[ -n "${cachedSnap}" ]] && {
sourceSnap="${cachedSnap}"
sourceOutput="cached original snap"
}
[[ -z "${cachedSnap}" && -n "${installedVersion:-}" ]] && error "No cached original found for installed Spotify ${installedVersion}.\nUse '--snap-file <path>' or '--download'."
}
[[ -n "${sourceSnap}" ]] || error "No unmodified Spotify snap source found.\nUse '--snap-file <path>' or '--download'."
echo -e "Using ${sourceOutput}\n"
"${sudoCmd[@]}" cp -p -- "${sourceSnap}" "${sourceCopy}"
sourceVerified=''
verify_snap_source "${sourceCopy}" && {
echo -e "${green}Verified official Spotify snap${clr}\n"
} || {
[[ "${allowUnverified}" ]] || error "Selected snap could not be authenticated as an official Spotify snap.\nConnect to the internet, place its matching assertion beside it or use '--allow-unverified'."
echo -e "${yellow}Warning:${clr} Selected snap could not be authenticated and will not be cached.\n"
}
"${sudoCmd[@]}" unsquashfs -d "${snapRoot}" "${sourceCopy}" >/dev/null
spotifyPath="${snapRoot}/usr/share/spotify"
xpuiSpa="${spotifyPath}/Apps/xpui.spa"
snapYaml="${snapRoot}/meta/snap.yaml"
[[ -f "${xpuiSpa}" && -f "${spotifyPath}/spotify" ]] || error "Spotify client not found inside snap."
[[ -f "${snapYaml}" ]] || error "Snap metadata not found."
snapName=$("${sudoCmd[@]}" awk '$1 == "name:" { print $2; exit }' "${snapYaml}")
snapName="${snapName#\"}"
snapName="${snapName%\"}"
snapName="${snapName#\'}"
snapName="${snapName%\'}"
[[ "${snapName}" == "spotify" ]] || error "Selected snap is not the Spotify snap."
has_spotx_marker "${xpuiSpa}" && error "Selected snap source is already patched.\nUse an original snap file or '--download'."
snapVersion=$("${sudoCmd[@]}" awk '$1 == "version:" { print $2; exit }' "${snapYaml}")
snapVersion="${snapVersion#\"}"
snapVersion="${snapVersion%\"}"
snapVersion="${snapVersion#\'}"
snapVersion="${snapVersion%\'}"
[[ -n "${snapVersion}" ]] || error "Unable to determine Spotify snap version."
forcedVersion="${snapVersion%%.g*}"
safeVersion=$(printf '%s' "${snapVersion}" | tr -c 'A-Za-z0-9._-' '_')
sourceHash=$("${sudoCmd[@]}" sha256sum "${sourceCopy}" | awk '{ print $1 }')
cacheFile="${cacheDir}/spotify_${safeVersion}_${sourceHash:0:12}.snap"
[[ "${sourceVerified}" ]] && {
"${sudoCmd[@]}" install -d -o "${runUid}" -g "${runGid}" -m 0755 "${cacheBase}" "${cacheDir}"
[[ -f "${cacheFile}" ]] || {
"${sudoCmd[@]}" install -o "${runUid}" -g "${runGid}" -m 0644 "${sourceCopy}" "${cacheFile}"
echo -e "Cached original snap: ${cacheFile}\n"
}
}
echo -e "Patching Spotify ${forcedVersion} with SpotX-Bash...\n"
"${sudoCmd[@]}" env SPOTX_BUILD_MODE=true bash "${spotxScript}" -P "${spotifyPath}" -F "${forcedVersion}" "${spotxArgs[@]}"
"${sudoCmd[@]}" rm -f -- "${spotifyPath}/spotify.bak" "${spotifyPath}/Apps/xpui.bak"
"${sudoCmd[@]}" unzip -tqq "${xpuiSpa}" || error "Patched xpui.spa validation failed."
has_spotx_marker "${xpuiSpa}" || error "SpotX marker not found in patched xpui.spa."
echo -e "Packing patched Spotify snap...\n"
"${sudoCmd[@]}" snap pack "${snapRoot}" "${packedDir}" >/dev/null
packedSnap=''
for candidate in "${packedDir}"/*.snap; do
[[ -f "${candidate}" ]] || continue
[[ -z "${packedSnap}" ]] || error "Multiple packed snap files found."
packedSnap="${candidate}"
done
[[ -n "${packedSnap}" ]] || error "Patched snap was not created."
"${sudoCmd[@]}" unsquashfs -s "${packedSnap}" >/dev/null || error "Patched snap validation failed."
outputName="Spotify.v${forcedVersion}.Linux.x64-SPOTX.snap"
[[ "${buildOnly}" ]] && {
mkdir -p "${outputDir}"
outputDir=$(cd -- "${outputDir}" && pwd)
outputFile="${outputDir}/${outputName}"
[[ ! -e "${outputFile}" ]] || error "Output file already exists: ${outputFile}"
"${sudoCmd[@]}" install -o "${runUid}" -g "${runGid}" -m 0644 "${packedSnap}" "${outputFile}"
echo -e "${green}Created:${clr} ${outputFile}\n"
exit 0
}
command pkill -9 '[sS]potify' 2>/dev/null || true
echo -e "${yellow}Warning:${clr} This locally installed snap will not receive automatic Spotify updates."
echo -e "Re-run this helper to update it or use '--restore' to return to the official snap.\n"
echo -e "Installing patched Spotify snap...\n"
"${sudoCmd[@]}" snap install --dangerous "${packedSnap}"
installedOutput=$(LC_ALL=C snap list spotify 2>/dev/null || true)
installedVersion=$(awk 'NR == 2 { print $2 }' <<< "${installedOutput}")
[[ -n "${installedVersion}" ]] || error "Spotify snap installation could not be verified."
echo -e "\n${green}Installed:${clr} Spotify ${installedVersion} with SpotX-Bash"
echo -e "Re-run this helper to rebuild from the cached original snap."
echo -e "Restore the official snap with: ${yellow}bash spotx-snap.sh --restore${clr}\n"
exit 0
+455 -116
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env bash
buildVer="1.2.93.667.g7b5cc0ce"
rollbackVer="1.2.92.148.g882cc571"
buildVer="1.2.95.453.g0eeebbed"
rollbackVer="1.2.94.583.g60394bd5"
latestB_X="2388"
latestB_A="2385"
rollbackB_X="26"
rollbackB_A="26"
latestB_X="3672"
latestB_A="3672"
rollbackB_X="4881"
rollbackB_A="4872"
clr='\033[0m'
green='\033[0;32m'
@@ -47,45 +47,45 @@ show_help() {
}
latestA_X=$(printf "%s" \
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODU2NzE4OTAsIm5iZiI6" \
"MTc4MzA3OTg5MCwicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gteDg2XzY0L3Nw" \
"b3RpZnktYXV0b3VwZGF0ZS0xLjIuOTMuNjY3Lmc3YjVjYzBjZS0yMzg4LnRieiJ9" \
".Ms9wZE1ISP7F3VkbBj5W4aCB4OhuG-TW9Crd99p0ruuzr9KQ38V-8JxntTZ_5YJ" \
"-uVvhomfiR-ZiSHe2VLP1HEbrnfGqPiU7UN-SLw5IV6pHRp2uRU28VX__Bw06OBT" \
"VUptwrVuaCKxooHkhrSFM49px0dWJPg3QwS0oNNK-lcVmchEvw4Dh9Jm9Gd8EpWq" \
"3_lCDmZKKyoBPJE2PGkQAjCl1J2YJNcppsNaebWbYF0WDdlH627tu-v07O2rVmxJ" \
"OyHUslFvpITv1CwDNg7plxFEbdbDeZM1XKjlGeggpkJy-3pV4DzzPfBe1E1dTGUc" \
"y_8YZEw6uil72fLs653LqQA")
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODg1NjYyOTMsIm5iZiI6" \
"MTc4NTk3NDI5MywicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gteDg2XzY0L3Nw" \
"b3RpZnktYXV0b3VwZGF0ZS0xLjIuOTUuNDUzLmcwZWVlYmJlZC0zNjcyLnRieiJ9" \
".FQBMTRH3659zzbVTZn-IqCqpqBJwyd7h3l33pZ8gYrO-ko8cmdS0w2bxzO46h9n" \
"7lV-ES8-aVXyzt0S57c8T3eNGkgz049amd-AgMJrtMSnhpGkmxnrzNwPUtvwWZUd" \
"HqH4edpN2WYHqSmclz8Xt82FY2FeKkfzF-pE0RzJEBVdW3ZXqvQcPNtO_0az4LGW" \
"qjbA7ZKd404-AGzBqrvJ7G_48XttT8xndYkVT2dTuNypZPoYzFCQ5Oh4I5XudlLB" \
"uztQkf9kweo3LLwgfy1k2wlj012Iqiz7WUDofHLzYSTjR_1FA4JDGCAH5E_Uoaa6" \
"Nnq-EChcnipD5WLpUon6AWQ")
latestA_A=$(printf "%s" \
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODU2NzE4ODksIm5iZiI6" \
"MTc4MzA3OTg4OSwicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gtYXJtNjQvc3Bv" \
"dGlmeS1hdXRvdXBkYXRlLTEuMi45My42NjcuZzdiNWNjMGNlLTIzODUudGJ6In0." \
"VEKJVarKFhm6__4P0LCuu8zma7NcFGu__Gwqsb97WG3phWkE9G6Bk1EvyOGxZRu0" \
"GAkqh0UZQm2OCTOio6kUbrmzudYYN5nd6bHht8yCh0U7u9y5YngS9zb7k8eh6yaN" \
"Z1UjtULj4YKaekRDk90Nqzukf88U6PSoKuVvLywpymjMb10tLGKCvpC84sZyKyAm" \
"08C0tOxB4lW2ci5nrvcRDcCV4sRMM_FXs3WSS4Byst0_4XpCbCs5Zu0QGpcTd_xv" \
"n6T7nqOl4JCQoaDcbNLpqbAmPyKana2CNJc4l-iaqNp4QDcS_48dTOoF5rPbAcOU" \
"0ca-VIBUueYh1NEeSLV-jw")
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODg1NjYyOTMsIm5iZiI6" \
"MTc4NTk3NDI5MywicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gtYXJtNjQvc3Bv" \
"dGlmeS1hdXRvdXBkYXRlLTEuMi45NS40NTMuZzBlZWViYmVkLTM2NzIudGJ6In0." \
"fRBIOAjThS9W-m_0EEw7oxd2tpIViU26gBsVdScwrnaT1XtJxhLp28bP26kd6pF4" \
"DsFxz5lmb5UogRCMhjdLMVvwUrJlMYR_IDKOkWxiOzFXG-CBEBCbylkm-hT5JG85" \
"fy7u5ZtXq-fUiyl5VEchP1vYH8JMZiHHC4i2V1SM9O1ExOeqxNccu_4m29Hh4gFK" \
"5qPIMnlD8egTqy_wstsZa7M5sHkmdjsJ4-SXxGy87ONtnbO9UxPOEQYemNOf9Zqq" \
"iGRs10px09Mb4Mk-ZnBiLl9jhLrTsVHSnBHr4Alet1D7kcMmxRnFxkBAeUHCaTw3" \
"xUtqn8T49yF538bgsKK5kw")
rollbackA_X=$(printf "%s" \
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODU2NjQ2ODQsIm5iZiI6" \
"MTc4MzA3MjY4NCwicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gteDg2XzY0L3Nw" \
"b3RpZnktYXV0b3VwZGF0ZS0xLjIuOTIuMTQ4Lmc4ODJjYzU3MS0yNi50YnoifQ.X" \
"NWUbG3S_HJnu8qGgny--Qax5UThn-FqjBR9fQ_MjTsahJcs1It7Vp34QMlhuO7ny" \
"qOpql68OpvYdILcsJZgZeSqub0SV4VcTH1uY1d5yfDG8YDVF5kHgR0Nl3oKqCHSm" \
"EASEuwnhTONspAwKdyr6PhrWR5IpznUWtYsQe_OMwGNb3ycS6eJVuBOOQT-q-m6x" \
"XV5aQEcWXbB0D_eR9MCYvQnWJ0qsPRKT-fPj8pVc1arF0GsUx73dszYvqKGItsWl" \
"PXMakY48wkhSidqji8FnhAxJIA0OPXkscsjPz5Mk3li6QeiRNxNi_ZUqloGJzhZH" \
"RjFeEXP_PVoQqJlSg5bFQ")
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODg0Mzg0ODksIm5iZiI6" \
"MTc4NTg0NjQ4OSwicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gteDg2XzY0L3Nw" \
"b3RpZnktYXV0b3VwZGF0ZS0xLjIuOTQuNTgzLmc2MDM5NGJkNS00ODgxLnRieiJ9" \
".VjPA-sh1Tjg72unZXmb-Mn-NRL64czXwOMkQzy0IN6gsRaYrwqsIXAMPldiJHgo" \
"SSp6HtSBkepcI9bagoM6US0rT9FEDkrjUn03kp4wl0tHxyahfDVFCbAMskc5VCVf" \
"yb1LK_GhNX2bvzTz9lLq1tSlNXclQNQB-NH21IfnvUViNF6-eXoTkvPSMNsLNg-n" \
"t8kOePUNm1ypOZ4SgJxxUyz4QaIftyvIH-7PEeyrbgIyKYukEB8RbtjjPlH3DfUi" \
"lmYMaJKOuDz2dww_r-4nPMIdCABdwyPj7RzZ1Yx5wMGVpAVpbNYg5nqRrTOs2PxO" \
"lmmjgMDdLBfx6PKz1eww6Xw")
rollbackA_A=$(printf "%s" \
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODU2NjQ2ODMsIm5iZiI6" \
"MTc4MzA3MjY4MywicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gtYXJtNjQvc3Bv" \
"dGlmeS1hdXRvdXBkYXRlLTEuMi45Mi4xNDguZzg4MmNjNTcxLTI2LnRieiJ9.tbq" \
"9qTZDPSDOUnK19LOAM87WNg-_48dLplrgwllprPNdZSP47bG0KZ0BSZHEbrR9v5O" \
"rV9RWJZLinVA4hYQ2N5eH0wUPXz7JnIBL7hiAzg032vfZ4SGIn0xPtKSRwzQxJXJ" \
"Wc9aaVM63i2WRelRlet7U12asFyjNg4JAoQs_azaKQHjVIPPzfaUgNt929CiX2kn" \
"EPvrDe0OxZT7vjfqm9MRHZaAl0hllOOZ-dD1UPR49Q6P_Wwi7gJjMmEIzfDD1ZHI" \
"iiLod5-N-ez5MpCX2rWt1LAq8zLPR-jAnUw2ERIblcmp_ROgrpTc5po1mkKz1L_g" \
"GQCFP7QJzhYBeiAyJUQ")
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODg0Mzg0ODksIm5iZiI6" \
"MTc4NTg0NjQ4OSwicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gtYXJtNjQvc3Bv" \
"dGlmeS1hdXRvdXBkYXRlLTEuMi45NC41ODMuZzYwMzk0YmQ1LTQ4NzIudGJ6In0." \
"Ch1itbFNAno5o7YDJ_Eh2ZjHuA12PIvkWrBIJmbHAH6AqdUXC2gbD4znygHVWtPn" \
"wc74tChZKO_-a79N0v3d26P6_rjPUir3hbtRn09YFzd4Tz4IZXHKY_bGg8YZI4rO" \
"0pijbtgVQ1dnLOG90aTROC8jCSpwj4dBmzEBwKgE68J_n5sEJJMxNNHCzpV5P-mC" \
"MHQiardZdR1zShvBy6d4CSzcaIiTZFTfeCnnyc8eyV9cY2wf18f4uEDgaZs90KKd" \
"J-MpU41Anjpa5yk2SUVsTSHJsKbR4s4vbW25jw2Z5rlfBJsegbxLuNAu00t9Y14R" \
"8eXZeGV3NgCrQCddw1sIzA")
while getopts ':BcdefF:hilopP:SvV:-:' flag; do
case "${flag}" in
@@ -107,7 +107,7 @@ while getopts ':BcdefF:hilopP:SvV:-:' flag; do
noexp) excludeExp='true' ;;
oldui) oldUi='true' ;;
premium) paidPremium='true' ;;
rollback) [[ "${platformType}" == "macOS" ]] && rollback='true'; installMac='true' ;;
rollback) [[ "${platformType}" == "macOS" ]] && { rollback='true'; installMac='true'; } ;;
skipcodesign) [[ "${platformType}" == "macOS" ]] && skipCodesign='true' ;;
stable) [[ "${platformType}" == "Linux" ]] && stableVar='true' ;;
uninstall) uninstallSpotx='true' ;;
@@ -142,7 +142,7 @@ sxbLiveVer=$(printf "%s" \
"kcwwmW0V0VPRXQ6dlb1MEWyF1RYV3dxs0a4xGTjR3QaNWNDhlcRdEWvhTeKdWVtJGd" \
"BNkY5Z1Rjd2dIlUaw42YspVMadjUpl0Z3BzY0F0UjRXQDJWeWNTW" \
| rev | base64 --decode | base64 --decode)
sxbLive=$(eval "${sxbLiveVer}")
[[ "${SPOTX_BUILD_MODE}" ]] && sxbLive="${buildVer}" || sxbLive=$(eval "${sxbLiveVer}")
sxbVer=$(echo ${buildVer} | perl -ne '/(.*)\./ && print "$1"')
verCk=$(printf "%s" \
"9QzRYNGayMGaKdUZwkzRjpXODplb1k3YwJ0QRdWVHJWaGdkYwZUbkhmQ5NGcCNl" \
@@ -277,16 +277,18 @@ macos_prepare() {
}
linux_client_variant() {
[[ "${installPath}" == *"flatpak"* ]] && {
command -v flatpak >/dev/null && flatpak list | grep spotify >/dev/null && {
flatpakVer=$(LANG=C.UTF-8 flatpak info com.spotify.Client | grep Version: | perl -ne '/Version: (1\.[0-9]+\.[0-9]+\.[0-9]+)\.g[0-9a-f]+/ && print "$1"')
[[ -z "${flatpakVer+x}" ]] && versionFailed='true' || { clientVer="${flatpakVer}"; flatpakClient='true'; }
cachePath=$(timeout 10 find /var/lib/flatpak/ $HOME/.var/app -type d -path "*com.spotify.Client/cache/spotify*" -name "spotify" -print -quit 2>/dev/null)
[[ "${clientVariant}" == "flatpak" || "${installPath}" == *"flatpak"* ]] && {
command -v flatpak >/dev/null && flatpak info com.spotify.Client >/dev/null 2>&1 && {
flatpakVer=$(LC_ALL=C flatpak info com.spotify.Client 2>/dev/null | perl -ne '/Version: (1\.[0-9]+\.[0-9]+\.[0-9]+)\.g[0-9a-f]+/ && print "$1"')
[[ -z "${flatpakVer}" ]] && versionFailed='true' || { clientVer="${flatpakVer}"; flatpakClient='true'; }
cachePath="${HOME}/.var/app/com.spotify.Client/cache/spotify"
[[ -d "${cachePath}" ]] || unset cachePath
}
return 0
}
[[ "${installPath}" == *"opt/spotify"* || "${installPath}" == *"spotify-launcher"* || "${installPath}" == *"usr/share/spotify"* ]] && {
cachePath=$(timeout 10 find $HOME/.cache/ -type d -path "*.cache/spotify*" -not -path "*snap/spotify*" -name "spotify" -print -quit 2>/dev/null)
cachePath="${HOME}/.cache/spotify"
[[ -d "${cachePath}" ]] || unset cachePath
return 0
}
return 0
@@ -312,7 +314,7 @@ linux_deb_prepare() {
linux_no_client() {
command -v snap >/dev/null && snap list spotify &>/dev/null && {
echo -e "${red}Error:${clr} Snap client not supported. See FAQ for more info.\nIf another Spotify package is installed, set directory path with '-P' flag.\n" >&2
echo -e "${red}Error:${clr} Snap client requires spotx-snap.sh. See FAQ for more info.\nIf another Spotify package is installed, set directory path with '-P' flag.\n" >&2
exit 1
}
command -v apt >/dev/null && {
@@ -326,16 +328,56 @@ linux_no_client() {
exit 1
}
linux_resolve_client_path() {
local base="${1%/}" candidate
[[ -n "${base}" ]] || return 1
for candidate in \
"${base}" \
"${base}/extra/share/spotify" \
"${base}/share/spotify" \
"${base}/files/extra/share/spotify" \
"${base}/files/share/spotify"; do
[[ -f "${candidate}/Apps/xpui.spa" ]] && {
installPath="${candidate}"
return 0
}
done
return 1
}
linux_search_path() {
local paths=("/opt" "/usr/share" "/var/lib/flatpak" "$HOME/.local/share" "/")
local paths=("/opt" "/usr/share" "/usr/lib" "$HOME/.local/share" "/var/lib/flatpak/app/com.spotify.Client")
local flatpakPath spotifyBinary xpuiFile path
linux_resolve_client_path "/opt/spotify" && return 0
linux_resolve_client_path "/usr/share/spotify" && return 0
linux_resolve_client_path "$HOME/.local/share/spotify-launcher/install/usr/share/spotify" && return 0
spotifyBinary=$(command -v spotify 2>/dev/null)
[[ -n "${spotifyBinary}" ]] && {
spotifyBinary=$(readlink -f "${spotifyBinary}" 2>/dev/null || printf '%s' "${spotifyBinary}")
linux_resolve_client_path "${spotifyBinary%/*}" && return 0
}
command -v flatpak >/dev/null && {
flatpakPath=$(flatpak info --show-location com.spotify.Client 2>/dev/null)
[[ -n "${flatpakPath}" ]] && linux_resolve_client_path "${flatpakPath}" && {
clientVariant='flatpak'
return 0
}
}
for path in "${paths[@]}"; do
installPath=$(timeout 6 find "${path}" -type f -path "*/spotify*Apps/*" -not -path "*snapd/snap*" -not -path "*snap/spotify*" -not -path "*snap/bin*" -not -path "*flatpak/.removed*" -name "xpui.spa" -size -20M -size +3M -print -quit 2>/dev/null | rev | cut -d/ -f3- | rev)
[[ -n "${installPath}" ]] && return 0
[[ -d "${path}" ]] || continue
xpuiFile=$(timeout 6 find "${path}" \
\( -path "*/flatpak/.removed" -o -path "*/snap" -o -path "*/snapd/snap" \) -prune -o \
-type f -path "*/Apps/xpui.spa" -print -quit 2>/dev/null)
[[ -n "${xpuiFile}" ]] && {
installPath="${xpuiFile%/Apps/xpui.spa}"
return 0
}
done
return 1
}
linux_set_path() {
local requestedPath
[[ "${installDeb}" ]] && { linux_deb_prepare; return; }
[[ -z "${installPath+x}" ]] && {
echo -e "Searching for client directory...\n"
@@ -347,11 +389,13 @@ linux_set_path() {
} || linux_no_client
return
}
[[ "${installPath}" == *"snapd/snap"* || "${installPath}" == *"snap/spotify"* || "${installPath}" == *"snap/bin"* ]] && {
echo -e "${red}Error:${clr} Snap client not supported. See FAQ for more info.\n" >&2
requestedPath="${installPath%/}"
[[ "${requestedPath}" == *"snapd/snap"* || "${requestedPath}" == *"snap/spotify"* || "${requestedPath}" == *"snap/bin"* ]] && {
echo -e "${red}Error:${clr} Snap client requires spotx-snap.sh. See FAQ for more info.\n" >&2
exit 1
}
[[ -f "${installPath}/Apps/xpui.spa" ]] && {
linux_resolve_client_path "${requestedPath}" && {
installOutput=$(echo "${installPath}" | perl -pe 's|^$ENV{HOME}|~|')
echo -e "Using client Directory: ${installOutput}\n"
linux_client_variant
} || {
@@ -368,7 +412,6 @@ linux_prepare() {
appBak="${appBinary}.bak"
snapshotBinary="${appPath}/v8_context_snapshot.bin"
xpuiPath="${appPath}/Apps"
[[ -z "${cachePath}" ]] && cachePath=$(timeout 10 find / -type d -path "*cache/spotify*" -not -path "*snap/spotify*" -name "spotify" -print -quit 2>/dev/null)
[[ "${debug}" ]] && echo -e "${green}Debug:${clr} $(cat /etc/*release | grep PRETTY_NAME | cut -d '"' -f2)"
[[ "${debug}" ]] && echo -e "${green}Debug:${clr} $(uname -m) detected"
[[ "${debug}" ]] && command -v apt >/dev/null && echo -e "${green}Debug:${clr} APT detected"
@@ -438,14 +481,27 @@ run_prepare() {
(($(ver "${clientVer}") > $(ver "${legacyMaxVer}"))) && macos_legacy_notice "toohigh"
client_version_output
ver_check
command pkill -9 '[sS]potify' 2>/dev/null
[[ -z "${SPOTX_BUILD_MODE}" ]] && command pkill -9 '[sS]potify' 2>/dev/null
[[ -f "${appBinary}" ]] && cleanAB=$(perl -ne '$found1 = 1 if /\x00\x73\x6C\x6F\x74\x73\x00/; $found2 = 1 if /\x2D\x70\x72\x65\x72\x6F\x6C\x6C/; END { print "true" if $found1 && $found2 }' "${appBinary}")
}
check_write_permission() {
local target_user="${SUDO_USER:-$(id -un)}"
local writePath
[[ "${platformType}" == "Linux" && -z "${SPOTX_BUILD_MODE}" ]] && ((EUID == 0)) && {
stagedInstall='true'
protectedInstall='true'
}
for path_to_check in "$@"; do
[[ ! -w "${path_to_check}" ]] && {
[[ -d "${path_to_check}" ]] && writePath="${path_to_check}" || writePath="${path_to_check%/*}"
[[ ! -w "${path_to_check}" ]] && stagedInstall='true'
[[ ! -w "${writePath}" ]] && {
stagedInstall='true'
protectedInstall='true'
((EUID == 0)) && continue
command -v sudo >/dev/null || {
echo -e "\n${red}Error:${clr} sudo command not found. Install sudo or run this script as root.\n" >&2
exit 1
}
sudo -n true 2>/dev/null || {
echo -e "${yellow}Warning:${clr} SpotX-Bash does not have write permission in client directory.\nRequesting sudo permission..." >&2
sudo -v || {
@@ -453,17 +509,172 @@ check_write_permission() {
exit 1
}
}
sudo chown -R "${target_user}" "${path_to_check}"
sudo chmod -R u+rwX,go-w "${path_to_check}"
}
done
}
sudo_run() {
if ((EUID == 0)) || [[ -z "${protectedInstall+x}" ]]; then
command "$@"
else
sudo "$@"
fi
}
protected_stage_copy() {
local source="${1}" destination="${2}"
sudo_run cat -- "${source}" > "${destination}" || {
rm -f -- "${destination}"
return 1
}
chmod 600 "${destination}"
}
protected_stage_prepare() {
targetAppBinary="${appBinary}"
targetAppBak="${appBak}"
targetXpuiPath="${xpuiPath}"
targetXpuiSpa="${xpuiSpa}"
targetXpuiBak="${xpuiBak}"
linux_working_dir
appPath="${workDir}/client"
appBinary="${appPath}/spotify"
appBak="${appBinary}.bak"
xpuiPath="${appPath}/Apps"
xpuiBak="${xpuiPath}/xpui.bak"
xpuiDir="${xpuiPath}/xpui"
xpuiSpa="${xpuiPath}/xpui.spa"
dwpPanelSectionJs="${xpuiDir}/dwp-panel-section.js"
homeHptoJs="${xpuiDir}/home-hpto.js"
homeV2Js="${xpuiDir}/home-v2.js"
indexHtml="${xpuiDir}/index.html"
vendorXpuiJs="${xpuiDir}/vendor~xpui.js"
xpuiCss="${xpuiDir}/xpui.css"
xpuiDesktopModalsJs="${xpuiDir}/xpui-desktop-modals.js"
xpuiJs="${xpuiDir}/xpui.js"
xpuiSnapshotJs="${xpuiDir}/xpui-snapshot.js"
mkdir -p "${xpuiPath}" || exit 1
protected_stage_copy "${targetAppBinary}" "${appBinary}" || exit 1
protected_stage_copy "${targetXpuiSpa}" "${xpuiSpa}" || exit 1
[[ -f "${targetAppBak}" ]] && protected_stage_copy "${targetAppBak}" "${appBak}"
[[ -f "${targetXpuiBak}" ]] && protected_stage_copy "${targetXpuiBak}" "${xpuiBak}"
}
protected_prepare_file() {
local source="${1}" destination="${2}" reference="${3}" tempFile
tempFile=$(sudo_run mktemp "${destination}.spotx.XXXXXXXX") || return 1
sudo_run cp -a -- "${reference}" "${tempFile}" &&
sudo_run cp -- "${source}" "${tempFile}" &&
sudo_run chown --reference="${reference}" "${tempFile}" &&
sudo_run chmod --reference="${reference}" "${tempFile}" &&
sudo_run touch -r "${reference}" "${tempFile}" || {
sudo_run rm -f -- "${tempFile}"
return 1
}
printf '%s\n' "${tempFile}"
}
protected_save_destination() {
local destination="${1}" tempFile
[[ -e "${destination}" ]] || return 0
tempFile=$(sudo_run mktemp "${destination}.spotx-rollback.XXXXXXXX") || return 1
sudo_run cp -a -- "${destination}" "${tempFile}" || {
sudo_run rm -f -- "${tempFile}"
return 1
}
printf '%s\n' "${tempFile}"
}
protected_restore_destination() {
local backup="${1}" destination="${2}" existed="${3}"
[[ "${existed}" ]] && sudo_run mv -f -- "${backup}" "${destination}" || sudo_run rm -f -- "${destination}"
}
protected_remove_files() {
local file
for file in "$@"; do
[[ -n "${file}" ]] && sudo_run rm -f -- "${file}"
done
}
protected_commit() {
local uninstall="${1:-}" appFile spaFile appBakFile='' xpuiBakFile=''
local appRollback='' spaRollback='' appBakRollback='' xpuiBakRollback=''
local appExisted='' spaExisted='' appBakExisted='' xpuiBakExisted=''
local appBakReference="${targetAppBinary}" xpuiBakReference="${targetXpuiSpa}"
[[ -f "${appBinary}" && -f "${xpuiSpa}" ]] || return 1
unzip -tqq "${xpuiSpa}" || return 1
appFile=$(protected_prepare_file "${appBinary}" "${targetAppBinary}" "${targetAppBinary}") || return 1
spaFile=$(protected_prepare_file "${xpuiSpa}" "${targetXpuiSpa}" "${targetXpuiSpa}") || {
sudo_run rm -f -- "${appFile}"
return 1
}
[[ -z "${uninstall}" ]] && {
[[ -e "${targetAppBak}" ]] && appBakReference="${targetAppBak}"
[[ -e "${targetXpuiBak}" ]] && xpuiBakReference="${targetXpuiBak}"
appBakFile=$(protected_prepare_file "${appBak}" "${targetAppBak}" "${appBakReference}") || {
sudo_run rm -f -- "${appFile}" "${spaFile}"
return 1
}
xpuiBakFile=$(protected_prepare_file "${xpuiBak}" "${targetXpuiBak}" "${xpuiBakReference}") || {
sudo_run rm -f -- "${appFile}" "${spaFile}" "${appBakFile}"
return 1
}
}
[[ -e "${targetAppBinary}" ]] && appExisted='true'
[[ -e "${targetXpuiSpa}" ]] && spaExisted='true'
[[ -e "${targetAppBak}" ]] && appBakExisted='true'
[[ -e "${targetXpuiBak}" ]] && xpuiBakExisted='true'
{
appRollback=$(protected_save_destination "${targetAppBinary}") &&
spaRollback=$(protected_save_destination "${targetXpuiSpa}")
[[ "${uninstall}" ]] || {
appBakRollback=$(protected_save_destination "${targetAppBak}") &&
xpuiBakRollback=$(protected_save_destination "${targetXpuiBak}")
}
} || {
protected_remove_files "${appFile}" "${spaFile}" "${appBakFile}" "${xpuiBakFile}"
protected_remove_files "${appRollback}" "${spaRollback}" "${appBakRollback}" "${xpuiBakRollback}"
return 1
}
{
{ [[ "${uninstall}" ]] || sudo_run mv -f -- "${appBakFile}" "${targetAppBak}"; } &&
{ [[ "${uninstall}" ]] || sudo_run mv -f -- "${xpuiBakFile}" "${targetXpuiBak}"; } &&
sudo_run mv -f -- "${appFile}" "${targetAppBinary}" &&
sudo_run mv -f -- "${spaFile}" "${targetXpuiSpa}"
} || {
protected_restore_destination "${appRollback}" "${targetAppBinary}" "${appExisted}"
protected_restore_destination "${spaRollback}" "${targetXpuiSpa}" "${spaExisted}"
[[ "${uninstall}" ]] || protected_restore_destination "${appBakRollback}" "${targetAppBak}" "${appBakExisted}"
[[ "${uninstall}" ]] || protected_restore_destination "${xpuiBakRollback}" "${targetXpuiBak}" "${xpuiBakExisted}"
protected_remove_files "${appFile}" "${spaFile}" "${appBakFile}" "${xpuiBakFile}"
return 1
}
protected_remove_files "${appRollback}" "${spaRollback}" "${appBakRollback}" "${xpuiBakRollback}"
[[ "${uninstall}" ]] && sudo_run rm -f -- "${targetAppBak}" "${targetXpuiBak}"
return 0
}
atomic_copy() {
local source="${1}" destination="${2}" tempFile result
copyTempDir=$(mktemp -d "${destination}.spotx.XXXXXXXX") || return 1
tempFile="${copyTempDir}/${destination##*/}"
cp "${source}" "${tempFile}" && mv -f "${tempFile}" "${destination}"
result=$?
rm -rf "${copyTempDir}" 2>/dev/null
[[ ! -d "${copyTempDir}" ]] && unset copyTempDir
return "${result}"
}
backup_spotx() {
atomic_copy "${xpuiSpa}" "${xpuiBak}" && atomic_copy "${appBinary}" "${appBak}" && return 0
rm -f "${appBak}" "${xpuiBak}" 2>/dev/null
return 1
}
uninstall_spotx() {
rm -f "${appBinary}" 2>/dev/null
mv -f "${appBak}" "${appBinary}"
rm -f "${xpuiSpa}" 2>/dev/null
mv -f "${xpuiBak}" "${xpuiSpa}"
atomic_copy "${appBak}" "${appBinary}" && atomic_copy "${xpuiBak}" "${xpuiSpa}" || return 1
rm -f "${appBak}" "${xpuiBak}" 2>/dev/null
rm -rf "${xpuiDir}" 2>/dev/null
}
@@ -474,12 +685,23 @@ run_uninstall_check() {
exit 1
}
check_write_permission "${appPath}" "${appBinary}" "${xpuiPath}" "${xpuiSpa}"
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && protected_stage_prepare
[[ "${cleanAB}" ]] && {
echo -e "${yellow}Warning:${clr} SpotX-Bash has detected abnormal behavior.\nClient reinstallation may be required...\n" >&2
rm -f "${appBak}" 2>/dev/null
rm -f "${xpuiBak}" 2>/dev/null
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && sudo_run rm -f -- "${targetAppBak}" "${targetXpuiBak}"
} || {
uninstall_spotx
uninstall_spotx || {
echo -e "\n${red}Error:${clr} Failed to restore client. Backups were preserved.\n" >&2
exit 1
}
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && {
protected_commit 'true' || {
echo -e "\n${red}Error:${clr} Failed to restore client. Original files restored.\n" >&2
exit 1
}
}
}
printf "\xE2\x9C\x94\x20\x46\x69\x6E\x69\x73\x68\x65\x64\x20\x75\x6E\x69\x6E\x73\x74\x61\x6C\x6C\n\n"
exit 0
@@ -541,7 +763,33 @@ sudo_check() {
}
}
linux_working_dir() { [[ -d "/tmp" ]] && workDir="/tmp" || workDir="$HOME"; }
cleanup_temp_dirs() {
[[ -n "${macInstallOld:-}" && -d "${macInstallOld}" && "${macInstallOld%/*}" == "${installPath:-}" && "${macInstallOld##*/}" == .spotx-previous.* ]] && {
[[ "${macInstallOldMoved:-}" && -z "${macInstallCommitted:-}" && ! -e "${appPath:-}" ]] && mv -- "${macInstallOld}" "${appPath}"
[[ -z "${macInstallOldMoved:-}" && -d "${macInstallOld}" ]] && rm -rf -- "${macInstallOld}"
[[ "${macInstallCommitted:-}" && -d "${macInstallOld}" ]] && rm -rf -- "${macInstallOld}"
}
[[ -n "${macInstallTemp:-}" && -d "${macInstallTemp}" && "${macInstallTemp%/*}" == "${installPath:-}" && "${macInstallTemp##*/}" == .spotx-install.* ]] && rm -rf -- "${macInstallTemp}"
[[ -n "${macDownloadPath:-}" && -f "${macDownloadPath}" && "${macDownloadPath%/*}" == "${HOME}/Downloads" && "${macDownloadPath##*/}" == "${fileVar:-}" ]] && rm -f -- "${macDownloadPath}"
[[ -n "${workDir:-}" && -d "${workDir}" && "${workDir##*/}" == spotx-bash.* ]] && rm -rf -- "${workDir}"
[[ -n "${copyTempDir:-}" && -d "${copyTempDir}" && "${copyTempDir##*/}" == *.spotx.* ]] && rm -rf -- "${copyTempDir}"
[[ -n "${spaTempDir:-}" && -d "${spaTempDir}" && "${spaTempDir##*/}" == .spotx-spa.* ]] && rm -rf -- "${spaTempDir}"
[[ "${xpuiTempCreated:-}" && -n "${xpuiDir:-}" && -d "${xpuiDir}" && "${xpuiDir##*/}" == "xpui" ]] && rm -rf -- "${xpuiDir}"
}
linux_working_dir() {
local tempBase="${TMPDIR:-/tmp}"
[[ -n "${workDir:-}" && -d "${workDir}" && "${workDir##*/}" == spotx-bash.* ]] && return
[[ "${tempBase}" == /* && -d "${tempBase}" && -w "${tempBase}" ]] || tempBase="/tmp"
workDir=$(mktemp -d "${tempBase%/}/spotx-bash.XXXXXXXX") || {
echo -e "${red}Error:${clr} Failed to create temporary working directory.\n" >&2
exit 1
}
chmod 700 "${workDir}" || {
echo -e "${red}Error:${clr} Failed to secure temporary working directory.\n" >&2
exit 1
}
}
linux_deb_install() {
sudo_check
@@ -587,6 +835,7 @@ linux_deb_install() {
}
macos_client_install() {
local downloadPath="${HOME}/Downloads/${fileVar}" stagedApp
[[ ! -w "${installPath}" ]] && {
echo -e "${red}Error:${clr} SpotX-Bash does not have write permission in ${installOutput}.\nConfirm permissions or set custom install path to writable directory.\n" >&2
exit 1
@@ -605,22 +854,54 @@ macos_client_install() {
"alHZyIWeChFT0F0UjRXQDJWeWNTW" \
| rev | base64 --decode | base64 --decode)
eval "${mc01}"; eval "${mc02}"
tar -tf "$HOME/Downloads/${fileVar}" >/dev/null 2>&1 || {
rm "$HOME/Downloads/${fileVar}" 2>/dev/null
tar -tf "${downloadPath}" >/dev/null 2>&1 || {
rm "${downloadPath}" 2>/dev/null
echo -e "\n${red}Error:${clr} Downloaded client archive is corrupt or incomplete. Exiting...\n" >&2
exit 1
}
macDownloadPath="${downloadPath}"
printf "\xE2\x9C\x94\x20\x44\x6F\x77\x6E\x6C\x6F\x61\x64\x65\x64\x20\x61\x6E\x64\x20\x69\x6E\x73\x74\x61\x6C\x6C\x69\x6E\x67\x20\x53\x70\x6F\x74\x69\x66\x79\n"
rm -rf "${appPath}" 2>/dev/null
mkdir -p "${appPath}"
tar -xpf "$HOME/Downloads/${fileVar}" -C "${appPath}" && unset notInstalled versionFailed || {
rm "$HOME/Downloads/${fileVar}" 2>/dev/null
macInstallTemp=$(mktemp -d "${installPath}/.spotx-install.XXXXXXXX") || {
rm "${downloadPath}" 2>/dev/null
echo -e "\n${red}Error:${clr} Client install failed. Exiting...\n" >&2
exit 1
}
stagedApp="${macInstallTemp}/Spotify.app"
mkdir -p "${stagedApp}" &&
tar -xpf "${downloadPath}" -C "${stagedApp}" &&
[[ -x "${stagedApp}/Contents/MacOS/Spotify" ]] &&
[[ -f "${stagedApp}/Contents/Info.plist" ]] &&
[[ -f "${stagedApp}/Contents/Resources/Apps/xpui.spa" ]] || {
rm "${downloadPath}" 2>/dev/null
echo -e "\n${red}Error:${clr} Client install failed. Exiting...\n" >&2
exit 1
}
[[ -e "${appPath}" ]] && {
macInstallOld=$(mktemp -d "${installPath}/.spotx-previous.XXXXXXXX") || {
rm "${downloadPath}" 2>/dev/null
echo -e "\n${red}Error:${clr} Client install failed. Exiting...\n" >&2
exit 1
}
rmdir "${macInstallOld}" && macInstallOldMoved='true' && mv "${appPath}" "${macInstallOld}" || {
rm "${downloadPath}" 2>/dev/null
echo -e "\n${red}Error:${clr} Client install failed. Exiting...\n" >&2
exit 1
}
}
mv "${stagedApp}" "${appPath}" && macInstallCommitted='true' || {
[[ -n "${macInstallOld:-}" && -d "${macInstallOld}" && ! -e "${appPath}" ]] && mv "${macInstallOld}" "${appPath}"
rm "${downloadPath}" 2>/dev/null
echo -e "\n${red}Error:${clr} Client install failed. Exiting...\n" >&2
exit 1
}
rm -rf "${macInstallTemp}"
[[ ! -d "${macInstallTemp}" ]] && unset macInstallTemp
[[ -n "${macInstallOld:-}" ]] && rm -rf "${macInstallOld}"
[[ -z "${macInstallOld:-}" || ! -d "${macInstallOld}" ]] && unset macInstallOld macInstallOldMoved macInstallCommitted
printf "\xE2\x9C\x94\x20\x49\x6E\x73\x74\x61\x6C\x6C\x65\x64\x20\x69\x6E\x20'"${installOutput}"'\n"
rm "$HOME/Downloads/${fileVar}"
rm "${downloadPath}" && unset macDownloadPath
clientVer=$(echo "${fileVar}" | perl -ne '/te-(.*)\..*\./ && print "$1"')
unset notInstalled versionFailed
}
run_install_check() {
@@ -630,6 +911,20 @@ run_install_check() {
}
}
macos_codesign() {
/usr/bin/xattr -cr "${appPath}" 2>/dev/null || {
echo -e "\n${red}Error:${clr} Failed to clear Spotify security attributes. Exiting...\n" >&2
exit 1
}
[[ "${skipCodesign}" ]] && return
codesign -f --deep -s - "${appPath}" >/dev/null 2>&1 &&
codesign --verify --deep --strict "${appPath}" >/dev/null 2>&1 || {
echo -e "\n${red}Error:${clr} Failed to codesign Spotify. Exiting...\n" >&2
exit 1
}
printf "\xE2\x9C\x94\x20\x43\x6F\x64\x65\x73\x69\x67\x6E\x65\x64\x20\x53\x70\x6F\x74\x69\x66\x79\n"
}
run_cache_check() {
[[ "${clearCache}" ]] && {
[[ -n "${cachePath}" && -d "${cachePath}" ]] && {
@@ -645,6 +940,7 @@ run_cache_check() {
final_setup_check() {
[[ "${notInstalled}" ]] && { echo -e "${red}Error:${clr} Client not found\n" >&2; exit 1; }
[[ ! -f "${appBinary}" || ! -s "${appBinary}" || ! -r "${appBinary}" || ! -x "${appBinary}" ]] && { echo -e "${red}Error:${clr} Client executable not found or invalid.\nReinstall client then try again.\n" >&2; exit 1; }
[[ ! -f "${xpuiSpa}" ]] && { echo -e "${red}Error:${clr} Detected a modified client installation!\nReinstall client then try again.\n" >&2; exit 1; }
[[ "${clientVer}" ]] && (($(ver "${clientVer}") < $(ver "1.1.59.710"))) && { echo -e "${red}Error:${clr} ${clientVer} not supported by SpotX-Bash\n" >&2; exit 1; }
}
@@ -653,42 +949,51 @@ perlVar() {
local A=("$@")
for cmd in "${A[@]}"; do
IFS='&' read -r -a a <<< "${cmd}"
local f="${a[4]}"
local p="${!f}"
[[ ! -f "${p}" && "${debug}" && "${devMode}" && "${t}" ]] && {
echo -e "${red}Error:${clr} ${a[0]} invalid entry"
continue
}
{ { [[ -z "${a[5]}" ]] || (( $(ver "${clientVer}") >= $(ver "${a[5]}") )); } &&
{ [[ -z "${a[6]}" ]] || (( $(ver "${clientVer}") <= $(ver "${a[6]}") )); } &&
{ [[ -z "${a[7]}" ]] || [[ "${a[7]}" =~ (^|\|)"${platformType}"($|\|) ]]; } &&
{ [[ -z "${a[8]}" ]] || [[ "${a[8]}" =~ (^|\|)"${archVar}"($|\|) ]]; }
} && perlvar "${xpuiSpa}"
} || continue
local f="${a[4]}"
local p="${!f}"
[[ ! -f "${p}" ]] && {
[[ "${debug}" && "${devMode}" && "${t}" ]] && echo -e "${red}Error:${clr} ${a[0]} invalid entry"
continue
}
perlvar "${xpuiSpa}"
done
}
xpui_detect() {
[[ (-f "${appBak}" || -f "${xpuiBak}") && "${cleanAB}" ]] && {
rm -f "${appBak}" 2>/dev/null; rm -f "${xpuiBak}" 2>/dev/null
cp "${xpuiSpa}" "${xpuiBak}"; cp "${appBinary}" "${appBak}"
backup_spotx || {
echo -e "\n${red}Error:${clr} Failed to create client backup. Exiting...\n" >&2
exit 1
}
printf "\xE2\x9C\x94\x20\x43\x72\x65\x61\x74\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n"
return
}
[[ (-f "${appBak}" || -f "${xpuiBak}") && "${forceSpotx}" ]] && {
[[ -f "${appBak}" ]] && { rm -f "${appBinary}"; cp "${appBak}" "${appBinary}"; }
[[ -f "${xpuiBak}" ]] && { rm -f "${xpuiSpa}"; cp "${xpuiBak}" "${xpuiSpa}"; }
{ [[ ! -f "${appBak}" ]] || atomic_copy "${appBak}" "${appBinary}"; } &&
{ [[ ! -f "${xpuiBak}" ]] || atomic_copy "${xpuiBak}" "${xpuiSpa}"; } || {
echo -e "\n${red}Error:${clr} Failed to restore client backup. Exiting...\n" >&2
exit 1
}
printf "\xE2\x9C\x94\x20\x44\x65\x74\x65\x63\x74\x65\x64\x20\x26\x20\x72\x65\x73\x74\x6F\x72\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n"
return
}
[[ (-f "${appBak}" || -f "${xpuiBak}") && -z "${forceSpotx+x}" ]] && {
rm -rf "${xpuiDir}" 2>/dev/null
xpuiSkip='true'
printf "\xE2\x9C\x94\x20\x44\x65\x74\x65\x63\x74\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n"
echo -e "\n${yellow}Warning:${clr} SpotX-Bash has already been installed." >&2
echo -e "Use the '-f' flag to force SpotX-Bash to run.\n" >&2
return
}
cp "${xpuiSpa}" "${xpuiBak}"
cp "${appBinary}" "${appBak}"
backup_spotx || {
echo -e "\n${red}Error:${clr} Failed to create client backup. Exiting...\n" >&2
exit 1
}
printf "\xE2\x9C\x94\x20\x43\x72\x65\x61\x74\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n"
}
@@ -745,7 +1050,9 @@ snapshot_check() {
}
xpui_open() {
rm -rf "${xpuiDir}" 2>/dev/null
mkdir -p "${xpuiDir}"
xpuiTempCreated='true'
unzip -qq "${xpuiSpa}" -d "${xpuiDir}" || {
rm -rf "${xpuiDir}" 2>/dev/null
echo -e "\n${red}Error:${clr} Failed to unpack xpui.spa. Reinstall client. Exiting...\n" >&2
@@ -783,6 +1090,7 @@ xpui_open() {
run_core_start() {
final_setup_check
check_write_permission "${appPath}" "${appBinary}" "${xpuiPath}" "${xpuiSpa}"
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && protected_stage_prepare
xpui_detect
[[ "${xpuiSkip}" ]] && { printf "\xE2\x9C\x94\x20\x46\x69\x6E\x69\x73\x68\x65\x64\n\n"; exit 0; }
xpui_open
@@ -839,20 +1147,40 @@ run_patches() {
}
run_finish() {
local spaTemp
echo -e "\n//# SpotX was here" >> "${xpuiJs}"
rm -f "${xpuiSpa}"
(cd "${xpuiDir}" && zip -qq -r ../xpui.spa .) || {
echo -e "\n${red}Error:${clr} Failed to repackage client." >&2
echo -e "Spotify is now in a broken state. Please reinstall client.\n" >&2
spaTempDir=$(mktemp -d "${xpuiPath}/.spotx-spa.XXXXXXXX") || {
uninstall_spotx && \
echo -e "\n${red}Error:${clr} Failed to create temporary SPA directory. Original client restored.\n" >&2 || \
echo -e "\n${red}Error:${clr} Failed to create temporary SPA directory or restore client. Backups were preserved.\n" >&2
exit 1
}
rm -rf "${xpuiDir}"
[[ "${platformType}" == "macOS" ]] && {
[[ "${skipCodesign}" ]] && /usr/bin/xattr -cr "${appPath}" 2>/dev/null || {
/usr/bin/xattr -cr "${appPath}" 2>/dev/null
codesign -f --deep -s - "${appPath}" 2>/dev/null
printf "\xE2\x9C\x94\x20\x43\x6F\x64\x65\x73\x69\x67\x6E\x65\x64\x20\x53\x70\x6F\x74\x69\x66\x79\n"
spaTemp="${spaTempDir}/xpui.spa"
(cd "${xpuiDir}" && zip -qq -r "${spaTemp}" .) &&
unzip -tqq "${spaTemp}" &&
mv -f "${spaTemp}" "${xpuiSpa}" || {
rm -rf "${spaTempDir}" 2>/dev/null
uninstall_spotx && {
echo -e "\n${red}Error:${clr} Failed to repackage client." >&2
echo -e "Original client restored.\n" >&2
} || {
echo -e "\n${red}Error:${clr} Failed to repackage or restore client." >&2
echo -e "Backups were preserved.\n" >&2
}
exit 1
}
rm -rf "${spaTempDir}" 2>/dev/null
[[ ! -d "${spaTempDir}" ]] && unset spaTempDir
rm -rf "${xpuiDir}"
unset xpuiTempCreated
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && protected_commit || {
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && {
echo -e "\n${red}Error:${clr} Failed to install patched client. Original files restored.\n" >&2
exit 1
}
}
[[ "${platformType}" == "macOS" ]] && {
macos_codesign
}
}
@@ -884,10 +1212,10 @@ freeEx=(
'hideDlQual&(\(.,..jsxs\)\(.{1,3}|(.\(\).|..)createElement\(.{1,4}),\{(filterMatchQuery|filter:.,title|(variant:"viola",semanticColor:"textSubdued"|..:"span",variant:.{3,6}mesto,color:.{3,6}),htmlFor:"desktop.settings.downloadQuality.+?).{1,6}get\("desktop.settings.downloadQuality.title.+?(children:.{1,2}\(.,.\).+?,|\(.,.\){3,4},|,.\)}},.\(.,.\)\),)&&&xpuiJs&1.1.59.710&1.2.29.605'
'hideUpgradeButton&(return|.=.=>)"free"===(.+?)(return|.=.=>)"premium"===&$1"premium"===$2$3"free"===&g&xpuiJs&1.1.59.710&1.1.92.647'
'hideUpgradeButton2&(?|(===")free(")|(")free("===))&$1premium$2&g&xpuiJs&1.2.55.235'
'hptoEnabled&hptoEnabled:!\K0&1&s&xpuiJs'
'hptoEnabled&hptoEnabled:!\K0&1&s&xpuiJs&&1.2.94.583'
'hptoShown&isHptoShown:!\K0&1&gs&homeHptoJs&1.1.85.884&1.2.20.1218'
'hptoShown2&(ADS_PREMIUM,isPremium:)\w(.*?ADS_HPTO_HIDDEN,isHptoHidden:)\w&$1true$2true&&xpuiJs&1.2.21.1104'
'payloadS&\x3F\x70\x61\x79\x6C\x6F\x61\x64&\x00\x00\x00\x00\x00\x00\x00\x00&gs&appBinary&1.2.53.437'
'payloadS&\x3F\x70\x61\x79\x6C\x6F\x61\x64&\x00\x00\x00\x00\x00\x00\x00\x00&gs&appBinary&1.2.53.437&1.2.93.667'
'stateS1&\x69\x6E\x69\x74\x69\x61\x6C\x5F(?=\x48)&\x00\x00\x00\x00\x00\x00\x00\x00&s&appBinary&1.2.53.437&1.2.55.235&macOS'
'stateS2&\x69\x6E\x69\x74\x69\x61\x6C\x5F(?=\x48)&\x00\x00\x00\x00\x00\x00\x00\x00&s&appBinary&1.2.53.437&1.2.84.476&Linux'
'stateS3&[\x00\x0A\x1A]\K\x69\x6E\x69\x74\x69\x61\x6C\x5F(?=\x73\x74\x61\x74\x65\x00)&\x00\x00\x00\x00\x00\x00\x00\x00&s&appBinary&1.2.55.235&&macOS'
@@ -907,8 +1235,8 @@ newUiEx=(
'enableNavAltExperiment&Enable the new home structure and navigation",values:.,default:\K..DISABLED&true&&xpuiJs&1.1.94.864&1.1.96.785'
'enableNavAltExperiment2&Enable the new home structure and navigation",values:.,default:.\K.DISABLED&.ENABLED_CENTER&&xpuiJs&1.1.97.956&1.2.2.582'
'enablePanelSizeCoordination&Enable Panel Size Coordination between the left sidebar, the main view and the right sidebar",default:\K!.(?=})&true&s&xpuiJs&1.2.7.1264&1.2.50.335'
'enableRightSidebar&Enable the view on the right sidebar",default:\K!1&true&s&xpuiJs&1.1.98.683&1.2.23.1125'
'enableRightSidebarLyrics&Show lyrics in the right sidebar",default:\K!1&true&s&xpuiJs&1.2.0.1165'
'enableRightSidebar&Enable the view on the right sidebar",default:\K!1&true&s&xpuiJs&1.1.98.683&1.2.93.667'
'enableRightSidebarLyrics&Show lyrics in the right sidebar",default:\K!1&true&s&xpuiJs&1.2.0.1165&1.2.94.583'
'enableYLXSidebar&Enable Your Library X view of the left sidebar",default:\K!1&true&s&xpuiJs&1.1.97.962&1.2.13.661'
)
podEx=(
@@ -924,8 +1252,8 @@ lyricsBgEx=(
)
aoEx=(
'aboutSpotX&((..createElement|children:\(.{1,7}\))\(.{1,7},\{source:).{1,7}get\("about.copyright",.\),paragraphClassName:("[^"]+"|.)(?=\}\))&$1"<h3>About SpotX / SpotX-Bash</h3><br><details><summary><svg xmlns='\''http://www.w3.org/2000/svg'\'' width='\''20'\'' height='\''20'\'' viewBox='\''0 0 24 24'\''><path d='\''M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z'\'' fill='\''#fff'\''/></svg> Github</summary><a href='\''https://github.com/SpotX-Official/SpotX'\''>SpotX \(Windows\)</a><br><a href='\''https://github.com/SpotX-Official/SpotX-Bash'\''>SpotX-Bash \(Linux/macOS\)</a><br><br/></details><details><summary><svg xmlns='\''http://www.w3.org/2000/svg'\'' width='\''20'\'' height='\''20'\'' viewBox='\''0 0 24 24'\''><path id='\''telegram-1'\'' d='\''M18.384,22.779c0.322,0.228 0.737,0.285 1.107,0.145c0.37,-0.141 0.642,-0.457 0.724,-0.84c0.869,-4.084 2.977,-14.421 3.768,-18.136c0.06,-0.28 -0.04,-0.571 -0.26,-0.758c-0.22,-0.187 -0.525,-0.241 -0.797,-0.14c-4.193,1.552 -17.106,6.397 -22.384,8.35c-0.335,0.124 -0.553,0.446 -0.542,0.799c0.012,0.354 0.25,0.661 0.593,0.764c2.367,0.708 5.474,1.693 5.474,1.693c0,0 1.452,4.385 2.209,6.615c0.095,0.28 0.314,0.5 0.603,0.576c0.288,0.075 0.596,-0.004 0.811,-0.207c1.216,-1.148 3.096,-2.923 3.096,-2.923c0,0 3.572,2.619 5.598,4.062Zm-11.01,-8.677l1.679,5.538l0.373,-3.507c0,0 6.487,-5.851 10.185,-9.186c0.108,-0.098 0.123,-0.262 0.033,-0.377c-0.089,-0.115 -0.253,-0.142 -0.376,-0.064c-4.286,2.737 -11.894,7.596 -11.894,7.596Z'\'' fill='\''#fff'\''/></svg> Telegram</summary><a href='\''https://t.me/spotify_windows_mod'\''>SpotX Channel</a><br><a href='\''https://t.me/SpotxCommunity'\''>SpotX Community</a><br><br/></details><details><summary><svg xmlns='\''http://www.w3.org/2000/svg'\'' width='\''20'\'' height='\''20'\'' viewBox='\''0 0 24 24'\''><path d='\''M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm1.25 17c0 .69-.559 1.25-1.25 1.25-.689 0-1.25-.56-1.25-1.25s.561-1.25 1.25-1.25c.691 0 1.25.56 1.25 1.25zm1.393-9.998c-.608-.616-1.515-.955-2.551-.955-2.18 0-3.59 1.55-3.59 3.95h2.011c0-1.486.829-2.013 1.538-2.013.634 0 1.307.421 1.364 1.226.062.847-.39 1.277-.962 1.821-1.412 1.343-1.438 1.993-1.432 3.468h2.005c-.013-.664.03-1.203.935-2.178.677-.73 1.519-1.638 1.536-3.022.011-.924-.284-1.719-.854-2.297z'\'' fill='\''#fff'\''/></svg> FAQ</summary><a href='\''https://te.legra.ph/SpotX-FAQ-09-19'\''>Windows</a><br><a href='\''https://github.com/SpotX-Official/SpotX-Bash/wiki/SpotX%E2%80%90Bash-FAQ'\''>Linux/macOS</a></details><br><h4>DISCLAIMER</h4>SpotX is a modified version of the official Spotify\x26reg; client, provided \x26quot;as is\x26quot; for the purpose of evaluation at user'\''s own risk. Source code for SpotX is available separately and free of charge under open source software license agreements. SpotX is not affiliated with Spotify\x26reg;, Spotify AB or Spotify Group.<br><br>Spotify\x26reg; is a registered trademark of Spotify Group."&&xpuiDesktopModalsJs&1.1.79.763'
'allowSwitchingBetweenHomeAdsAndHpto&opposed to only showing the legacy HPTO format.",default:\K!.(?=})&false&s&xpuiJs&1.2.34.783'
'betamaxFilterNegativeDuration&for duration that is negative",default:\K!.(?=})&false&s&xpuiJs'
'allowSwitchingBetweenHomeAdsAndHpto&opposed to only showing the legacy HPTO format.",default:\K!.(?=})&false&s&xpuiJs&1.2.34.783&1.2.94.583'
'betamaxFilterNegativeDuration&for duration that is negative",default:\K!.(?=})&false&s&xpuiJs&1.1.59.001&1.2.93.667'
'bGabo&\x00\K\x67(?=\x61\x62\x6F\x2D\x72\x65\x63\x65\x69\x76\x65\x72\x2D\x73\x65\x72\x76\x69\x63\x65\x2F\x70)&\x00&g&appBinary&1.1.84.716'
'bLogic&\x00\K\x61(?=\x64\x2D\x6C\x6F\x67\x69\x63\x2F\x73)&\x00&&appBinary&1.1.70.610&1.2.28.581'
'bSlot&\x00\K\x73(?=\x6C\x6F\x74\x73\x00)&\x00&g&appBinary&1.1.70.610'
@@ -941,12 +1269,13 @@ aoEx=(
'enableCanvasAds&Enable Canvas for ads",default:\K!.(?=})&false&s&xpuiJs&1.2.52.442&1.2.92.148'
'enableConnectedStateObserver&observer that logs errors related to connected state and ad info",default:\K!.(?=})&false&s&xpuiJs&1.2.53.437'
'enableCulturalMoments&Cultural Moment pagess",default:\K!.(?=})&false&s&xpuiJs&1.2.7.1264&1.2.50.335'
'enableDesktopMusicLeavebehinds&Enable music leavebehinds on eligible playlists for desktop",default:\K!.(?=})&false&s&xpuiJs&1.2.10.751'
'enableDesktopMusicLeavebehinds&Enable music leavebehinds on eligible playlists for desktop",default:\K!.(?=})&false&s&xpuiJs&1.2.10.751&1.2.93.667'
'enableDsaAds&Enable showing DSA .Digital Services Act. context menu and modal for ads",default:\K!.(?=})&false&s&xpuiJs&1.2.20.1210&1.2.52.442'
'enableDSASetting&Enable DSA .Digital Service Act. features for desktop and web",default:\K!.(?=})&false&s&xpuiJs&1.2.20.1210'
'enableEnhancedAdsClientDeconfliction&Enable refactored version of ads orchestrator middleware",default:\K!.(?=})&false&s&xpuiJs&1.2.57.460&1.2.61.443'
'enableEmbeddedAdsCarousel&embedded ads carousel for the NPV",default:\K!.(?=})&false&s&xpuiJs&1.2.73.451'
'enableEmbeddedAdsFetchingOverCanvas&embedded ads fetching when canvas track is playing. Defaults to true since this is currently existing behavior",default:\K!.(?=})&false&s&xpuiJs&1.2.72.435&1.2.77.358'
'enableEmbeddedAdHtmlDisplay&Enable HTML display ads in the embedded NPV",default:\K!.(?=})&false&s&xpuiJs&1.2.94.0'
'enableEmbeddedAdVisibilityLogging&When enabled, enhanced visibility logs will be sent for embedded ads",default:\K!.(?=})&false&s&xpuiJs&1.2.64.407&1.2.77.358'
'enableEmbeddedNpvAds&Enable embedded display ads on NPV",default:\K!.(?=})&false&s&xpuiJs&1.2.57.460&1.2.77.358'
'enableEsperantoMigration&Enable esperanto Migration for (HPTO\s)?Ad Formats?",default:\K!.(?=})&false&s&xpuiJs&1.2.6.861&1.2.50.335'
@@ -954,11 +1283,13 @@ aoEx=(
'enableFraudLoadSignals&Enable user fraud signals emitted on page load",default:\K!.(?=})&false&s&xpuiJs&1.2.22.975&1.2.62.580'
'enableHomeAds&Enable Fist Impression Takeover ads on Home Page",default:\K!.(?=})&false&s&xpuiJs&1.2.31.1205&1.2.84.477'
'enableHomeAdStaticBanner&Enables temporary home banner, static version",default:\K!.(?=})&false&s&xpuiJs&1.2.25.1009&1.2.53.440'
'enableHpto&Hpto announcements on Home",default:\K!.(?=})&false&s&xpuiJs&1.2.65.255'
'enableHpto&Hpto announcements on Home",default:\K!.(?=})&false&s&xpuiJs&1.2.65.255&1.2.94.583'
'enableHptoLayoutRewrite&Enable the new HomeAdCard flexbox layout rewrite",default:\K!.(?=})&false&s&xpuiJs&1.2.92.0'
'enableHptoLocationRefactor&Enable new permanent location for HPTO iframe to HptoHtml.js",default:\K!.(?=})&false&s&xpuiJs&1.2.1.958&1.2.20.1218'
'enableImageOptimizationSentrySpanMeasurement&Sentry image resource span attributes for image optimization rollout measurement",default:\K!.(?=})&false&s&xpuiJs&1.2.94.0'
'enableInAppMessaging&Enables quicksilver in-app messaging modal",default:\K!.(?=})&false&s&xpuiJs&1.1.70.610'
'enableInteractionLogger&Enables the old interaction logger",default:\K!.(?=})&false&s&xpuiJs&1.2.41.434&1.2.64.408'
'enableLeaderboardEmptySlotHandling&Config for clearing the current leaderboard ad and hiding the leaderboard container when the ad slot returns an empty response",default:\K!.(?=})&true&s&xpuiJs&1.2.95.200'
'enableLeavebehindsMockData&Use the mock endpoint to fetch Leavebehinds from AP4P",default:\K!.(?=})&false&s&xpuiJs&1.2.30.1135'
'enableNewAdsNpv&Enable showing new ads NPV",default:\K!.(?=})&false&s&xpuiJs&1.2.18.997&1.2.50.335'
'enableNewAdsNpvCanvasAds&Enable Canvas ads for new ads NPV",default:\K!.(?=})&false&s&xpuiJs&1.2.28.581&1.2.51.345'
@@ -971,8 +1302,10 @@ aoEx=(
'enablePodcastSponsoredContent&Enable sponsored content information for podcasts",default:\K!.(?=})&false&s&xpuiJs&1.2.30.1135&1.2.50.335'
'enablePromotions&Enables promotions on home",default:\K!.(?=})&false&s&xpuiJs&1.2.38.720&1.2.45.454'
'enableSaxLeaderboardAds&Enable SAX Leaderboard Ad Format",default:\K!.(?=})&false&s&xpuiJs&1.2.62.575&1.2.82.428'
'enableShowLeavebehindConsolidation&Enable show leavebehinds consolidated experience",default:\K!.(?=})&false&s&xpuiJs&1.2.23.1114'
'enableSentryReactRouterV6Routing&Sentry React Router v6 route instrumentation for Web Player SPA transactions",default:\K!.(?=})&false&s&xpuiJs&1.2.94.0'
'enableShowLeavebehindConsolidation&Enable show leavebehinds consolidated experience",default:\K!.(?=})&false&s&xpuiJs&1.2.23.1114&1.2.93.667'
'enableSponsoredPlaylistEsperantoMigration&Enable esperanto Migration for Sponsored Playlist Ad Formats",default:\K!.(?=})&false&s&xpuiJs&1.2.32.985&1.2.50.335'
'enableSponsoredPlaylistHorizontalVideo&horizontal video layout for sponsored playlist headers on desktop",default:\K!.(?=})&false&s&xpuiJs&1.2.95.200'
'enableSurveyAds&Enable Spotify Brand Lift .SBL. Surveys in the NPV",default:\K!.(?=})&false&s&xpuiJs&1.2.43.420&1.2.63.394'
'enableUnderAgeBlockingModal&Enables the underage blocking modal for accounts in blocked/pending disabled state",default:\K!.(?=})&false&s&xpuiJs&1.2.78.397'
'enableUserFraudCanvas&Enable user fraud Canvas Fingerprinting",default:\K!.(?=})&false&s&xpuiJs&1.2.13.656&1.2.63.394'
@@ -1017,7 +1350,7 @@ expEx=(
'enableAttackOnTitanEasterEgg&Titan Easter egg turning progress bar red when playing official soundtrack",default:\K!.(?=})&true&s&xpuiJs&1.2.6.861&1.2.50.335'
'enableAudiobookPrerelease&audiobook prerelease pages",default:\K!1&true&s&xpuiJs&1.2.33.1039&1.2.47.366'
'enableAudiobooks&Audiobooks feature on ClientX",default:\K!1&true&s&xpuiJs&1.1.74.631&1.2.46.462'
'enableAutoSeekToVideoBufferedStartPosition&avoid initial seek if the initial position is not buffered",default:\K!1&true&s&xpuiJs&1.2.31.1205'
'enableAutoSeekToVideoBufferedStartPosition&avoid initial seek if the initial position is not buffered",default:\K!1&true&s&xpuiJs&1.2.31.1205&1.2.93.667'
'enableBackendSearchHistory&Enable backend search history",default:\K!1&true&s&xpuiJs&1.2.60.564&1.2.85.519'
'enableBanArtistAction&context menu action to ban/unban artists",default:\K!1&true&s&xpuiJs&1.2.28.581&1.2.42.290'
'enableBetamaxSdkSubtitlesDesktopX&rendering subtitles on the betamax SDK on DesktopX",default:\K!.(?=})&true&s&xpuiJs&1.1.70.610'
@@ -1049,6 +1382,7 @@ expEx=(
'enableEightShortcuts&Increase max number of shortcuts on home to 8",default:\K!1&true&s&xpuiJs&1.2.26.1180&1.2.45.454'
'enableEncoreCards&all cards throughout app to be Encore Cards",default:\K!1&true&s&xpuiJs&1.2.21.1104&1.2.33.1042'
'enableEncorePlaybackButtons&Use Encore components in playback control components",default:\K!1&true&s&xpuiJs&1.2.20.1210&1.2.43.420'
'enableEntityHeaderNew&Enable the new entity header design",default:\K!.(?=})&true&s&xpuiJs&1.2.95.200'
'enableEqualizer&audio equalizer for Desktop and Web Player",default:\K!1&true&s&xpuiJs&1.1.88.595'
'enableExcludeTrackFromTasteProfile&option to exclude track from taste profile via context menu",default:\K!1&true&s&xpuiJs&1.2.73.451'
'enableExtraTracklistColumns&extra tracklist columns",default:\K!1&true&s&xpuiJs&1.2.44.405&1.2.71.421'
@@ -1061,7 +1395,7 @@ expEx=(
'enableHomePin&pinning of home shelves",default:\K!1&true&s&xpuiJs&1.2.45.451'
'enableIgnoreInRecommendations&Ignore In Recommendations for desktop and web",default:\K!.(?=})&true&s&xpuiJs&1.1.87.612&1.2.50.335'
'enableInlineCuration&new inline playlist curation tools",default:\K!1&true&s&xpuiJs&1.1.70.610&1.2.25.1011'
'enableLikedSongsAsPlaylist&Liked Songs on list platform with playlist uri",default:\K!1&true&s&xpuiJs&1.2.75.499'
'enableLikedSongsAsPlaylist&Liked Songs on list platform with playlist uri",default:\K!1&true&s&xpuiJs&1.2.75.499&1.2.93.667'
'enableLikedSongsFilterTags&Show filter tags on the Liked Songs entity view",default:\K!1&true&s&xpuiJs&1.2.32.985'
#'enableLikedSongsListPlatform&Liked Songs on list platform",default:\K!1&true&s&xpuiJs&1.2.41.434'
'enableListPrivateByDefaultSetting&List Private By Default setting in Desktop Social Settings",default:\K!1&true&s&xpuiJs&1.2.78.397'
@@ -1077,7 +1411,7 @@ expEx=(
'enableMoreLikeThisPlaylist&More Like This playlist for playlists the user cannot edit",default:\K!1&true&s&xpuiJs&1.2.32.985&1.2.73.474'
'enableNearbyJams&support for Nearby Jams feature in the Device Picker",default:\K!1&true&s&xpuiJs&1.2.52.442'
'enableNewArtistEventsPage&Display the new Artist events page",default:\K!1&true&s&xpuiJs&1.2.18.997&1.2.32.997'
'enableNewConcertFeed&Enables new concert feed experience",default:\K!1&true&s&xpuiJs&1.2.37.701&1.2.42.290&1.2.50.335'
'enableNewConcertFeed&Enables new concert feed experience",default:\K!1&true&s&xpuiJs&1.2.37.701&1.2.50.335'
'enableNewConcertLocationExperience&new concert location experience modal selector.",default:\K!1&true&s&xpuiJs&1.2.34.783&1.2.42.290'
'enableNewEntityHeaders&New Entity Headers",default:\K!1&true&s&xpuiJs&1.2.15.826&1.2.28.0'
'enableNewEpisodes&new episodes view",default:\K!1&true&s&xpuiJs&1.1.84.716&1.2.62.580'
@@ -1100,13 +1434,14 @@ expEx=(
'enablePlaybackBarAnimation&animation of the playback bar",default:\K!1&true&s&xpuiJs&1.2.34.783&1.2.82.428'
'enablePlaylistCreationFlow&new playlist creation flow in Web Player and DesktopX",default:\K!1&true&s&xpuiJs&1.1.70.610&1.1.93.896'
'enablePlaylistPermissionsProd&Playlist Permissions flows for Prod",default:\K!.(?=})&true&s&xpuiJs&1.1.75.572&1.2.50.335'
'enablePlaylistReleaseDateColumn&Enables the release date column in playlist tracklists",default:\K!.(?=})&true&s&xpuiJs&1.2.95.200'
'enablePodcastChaptersInNpv&showing podcast chapters in NPV",default:\K!.(?=})&true&s&xpuiJs&1.2.22.975&1.2.50.335'
'enablePodcastChapterPage&the podcast chapter entity page",default:\K!.(?=})&true&s&xpuiJs&1.2.85.504'
'enablePodcastChapterPage&the podcast chapter entity page",default:\K!.(?=})&true&s&xpuiJs&1.2.85.504&1.2.93.667'
'enablePodcastDescriptionAutomaticLinkification&Linkifies anything looking like a url in a podcast description.",default:\K!1&true&s&xpuiJs&1.2.19.937'
'enablePremiumUserForMiniPlayer&premium user flag for mini player",default:\K!1&true&s&xpuiJs&1.2.32.985'
'enablePrereleaseRadar&Show a curated list of upcoming albums to a user",default:\K!1&true&s&xpuiJs&1.2.39.578&1.2.45.454'
'enableProfileVisibilityControls&profile visibility controls in the settings . profile page",default:\K!1&true&s&xpuiJs&1.2.74.462&1.2.85.519'
'enableProgressBarEpisodeChapters&pisode chapters markers in the progress bar",default:\K!1&true&s&xpuiJs&1.2.68.525&1.2.74&1.2.74.477'
'enableProgressBarEpisodeChapters&pisode chapters markers in the progress bar",default:\K!1&true&s&xpuiJs&1.2.68.525&1.2.74.477'
'enableProgressBarRefactorWithChapters&refactored ProgressBar implementation with chapter support",default:\K!1&true&s&xpuiJs&1.2.74.462&1.2.82.428'
'enableQueueOnRightPanel&Enable Queue on the right panel.",default:\K!.(?=})&true&s&xpuiJs&1.2.26.1180&1.2.61.443'
'enableQueueOnRightPanelAnimations&animations for Queue on the right panel.",default:\K!.(?=})&true&s&xpuiJs&1.2.32.985&1.2.50.335'
@@ -1126,14 +1461,14 @@ expEx=(
'enableSearchV3&new Search experience",default:\K!1&true&s&xpuiJs&1.1.87.612&1.2.34.783'
'enableScrollDrivenAnimations&croll driven animations for cards and shelved",default:\K!1&true&s&xpuiJs&1.2.39.578'
'enableShareActionBarButton&Shows a share button in entity page action bars that opens the share dialog",default:\K!.(?=})&true&s&xpuiJs&1.2.85.504'
'enableShareDialog&the share dialog modal instead of the share submenu",default:\K!.(?=})&true&s&xpuiJs&1.2.85.504'
'enableShareDialog&the share dialog modal instead of the share submenu",default:\K!.(?=})&true&s&xpuiJs&1.2.85.504&1.2.93.667'
'enableSharingButtonOnMiniPlayer&sharing button on MiniPlayer .this also moves the ... icon close to the title.",default:\K!1&true&s&xpuiJs&1.2.39.578&1.2.43.420'
'enableShortLinks&short links for sharing",default:\K!1&true&s&xpuiJs&1.2.34.783'
'enableShowFollowsSetting&control if followers and following lists are shown on profile",default:\K!.(?=})&true&s&xpuiJs&1.2.1.958&1.2.50.335'
'enableShowRating&new UI for rating books and podcasts",default:\K!1&true&s&xpuiJs&1.2.32.985&1.2.62.580'
'enableShuffleSettings&shuffle settings section in advanced settings",default:\K!1&true&s&xpuiJs&1.2.75.499'
'enableSidebarAnimations&animations on the left and right on the sidebars and makes the right sidebar collapsible",default:\K!1&true&s&xpuiJs&1.2.34.783&1.2.37.701'
'enableSilenceTrimmer&silence trimming in podcasts",default:\K!1&true&s&xpuiJs&1.1.99.871'
'enableSilenceTrimmer&silence trimming in podcasts",default:\K!1&true&s&xpuiJs&1.1.99.871&1.2.93.667'
'enableSkipNextTooltip&tooltip that shows a preview of the next item in queue.",values:.{1,3},default:.{1,4}\KDisabled&Expanded&s&xpuiJs&1.2.65.255&1.2.85.519'
'enableSocialConnectOnDesktop&the Social Connect API that powers group listening sessions for Desktop",values:.{1,3},default:.{1,4}\KDISABLED&ENABLED&s&xpuiJs&1.2.21.1104&1.2.45.454'
'enableSmallerLineHeight&line height 1.5 on the .body ..",default:\K!1&true&s&xpuiJs&1.2.18.997&1.2.23.1125'
@@ -1146,6 +1481,7 @@ expEx=(
'enableTiltable3DArtwork&tiltable 3D parallax effect on artwork .Cinema Mode and Cover Art Modal.",default:\K!1&true&s&xpuiJs&1.2.76.256'
'enableTogglePlaylistColumns&ability to toggle playlist column visibility",default:\K!1&true&s&xpuiJs&1.2.17.832&1.2.66.447'
'enableTracklistColumnsSorting&column reordering functionality in tracklists",default:\K!1&true&s&xpuiJs&1.2.69.448'
'enableTranscriptTextSelection&text selection and copy in episode transcripts on desktop",default:\K!.(?=})&true&s&xpuiJs&1.2.95.200'
'enableUserCommentsForEpisodes&user comments for podcast episodes",default:\K!1&true&s&xpuiJs&1.2.49.439'
'enableUserCreatedArtwork&user created artworks for playlists",default:\K!1&true&s&xpuiJs&1.2.34.783&1.2.40.599'
'enableUserProfileEdit&editing of user.s own profile in Web Player and DesktopX",default:\K!1&true&s&xpuiJs&1.1.87.612&1.2.25.1011'
@@ -1164,7 +1500,7 @@ expEx=(
'enableYLXPrereleaseAlbums&album pre-releases in YLX",default:\K!1&true&s&xpuiJs&1.2.32.985'
'enableYLXPrereleaseAudiobooks&audiobook pre-releases in YLX",default:\K!1&true&s&xpuiJs&1.2.32.985&1.2.47.366'
'enableYLXPrereleases&album pre-releases in YLX",default:\K!1&true&s&xpuiJs&1.2.31.1205&1.2.31.1205'
'enableYlxReverseSorting&Enable reverse sort direction in Your Library",default:\K!1&true&s&xpuiJs&1.2.60.564'
'enableYlxReverseSorting&Enable reverse sort direction in Your Library",default:\K!1&true&s&xpuiJs&1.2.60.564&1.2.94.583'
'enableYLXTypeaheadSearch&jump to the first matching item",default:\K!1&true&s&xpuiJs&1.2.13.656'
'enableZoomSettingsUIDesktop&zoom settings from the settings page on Desktop",default:\K!1&true&s&xpuiJs&1.2.17.832&1.2.53.437'
'isVideoQualityEnabled&video quality settings and the in-player quality picker",default:\K!1&true&s&xpuiJs&1.2.84.194'
@@ -1181,6 +1517,9 @@ premiumExpEx=(
'enableYourSoundCapsuleModal&showing a modal on desktop to users who have clicked on a Your Sound Capsule share link",default:\K!1&true&s&xpuiJs&1.2.38.720'
)
trap cleanup_temp_dirs EXIT
trap 'exit 130' HUP INT TERM
run_prepare
run_uninstall_check
run_interactive_check