Compare commits

...
13 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
4 changed files with 801 additions and 108 deletions
+1
View File
@@ -0,0 +1 @@
.DS_Store
+13 -1
View File
@@ -12,7 +12,7 @@
<center> <center>
<h4 align="center">Adblock for the Spotify desktop client on Linux & macOS</h4> <h4 align="center">Adblock for the Spotify desktop client on Linux & macOS</h4>
<p align="center"> <p align="center">
<strong>Latest supported version:</strong> 1.2.93.667.g7b5cc0ce <strong>Latest supported version:</strong> 1.2.95.453.g0eeebbed
</p> </p>
</center> </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 - 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) - 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: ### Options:
<details> <details>
<summary>Click to expand!</summary> <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
+445 -107
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
buildVer="1.2.94.583.g60394bd5" buildVer="1.2.95.453.g0eeebbed"
rollbackVer="1.2.93.667.g7b5cc0ce" rollbackVer="1.2.94.583.g60394bd5"
latestB_X="4881" latestB_X="3672"
latestB_A="4872" latestB_A="3672"
rollbackB_X="2388" rollbackB_X="4881"
rollbackB_A="2385" rollbackB_A="4872"
clr='\033[0m' clr='\033[0m'
green='\033[0;32m' green='\033[0;32m'
@@ -47,45 +47,45 @@ show_help() {
} }
latestA_X=$(printf "%s" \ latestA_X=$(printf "%s" \
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODY4MDk0ODgsIm5iZiI6" \ "eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODg1NjYyOTMsIm5iZiI6" \
"MTc4NDIxNzQ4OCwicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gteDg2XzY0L3Nw" \ "MTc4NTk3NDI5MywicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gteDg2XzY0L3Nw" \
"b3RpZnktYXV0b3VwZGF0ZS0xLjIuOTQuNTgzLmc2MDM5NGJkNS00ODgxLnRieiJ9" \ "b3RpZnktYXV0b3VwZGF0ZS0xLjIuOTUuNDUzLmcwZWVlYmJlZC0zNjcyLnRieiJ9" \
".kxtysiyM75Sk0uZ2wZjnWnDrf40Hhjw8qUkW_Ott7-aa-Z3BEmRO7px-QDf25Of" \ ".FQBMTRH3659zzbVTZn-IqCqpqBJwyd7h3l33pZ8gYrO-ko8cmdS0w2bxzO46h9n" \
"fox48sICQGMnbTdw-ysa0G3XRqeHqfwnvH_LI4Y2kB2hG-Lv3DHAnVQMY7W9vZR1" \ "7lV-ES8-aVXyzt0S57c8T3eNGkgz049amd-AgMJrtMSnhpGkmxnrzNwPUtvwWZUd" \
"syDeM94UOe6M6SGNDMquAh0LbbPg-TwNhiXWgGBdCK5BxPTirN0ABK3XYKTfkreK" \ "HqH4edpN2WYHqSmclz8Xt82FY2FeKkfzF-pE0RzJEBVdW3ZXqvQcPNtO_0az4LGW" \
"UYVC7MsODGWRRypMmJuO36dUvY8UCmptQ0r_cJVuXTZWmS2FiB1mouKYMXxRBh4v" \ "qjbA7ZKd404-AGzBqrvJ7G_48XttT8xndYkVT2dTuNypZPoYzFCQ5Oh4I5XudlLB" \
"eJvXV8v-gCKePxFTV5wIBBVaioNjlNSUnphKlwoibRswnWVE1tmxDjVsYcl3Wpn-" \ "uztQkf9kweo3LLwgfy1k2wlj012Iqiz7WUDofHLzYSTjR_1FA4JDGCAH5E_Uoaa6" \
"GsP5spgl8Q6hDnWljPFEz1w") "Nnq-EChcnipD5WLpUon6AWQ")
latestA_A=$(printf "%s" \ latestA_A=$(printf "%s" \
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODY4MDk0ODgsIm5iZiI6" \ "eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODg1NjYyOTMsIm5iZiI6" \
"MTc4NDIxNzQ4OCwicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gtYXJtNjQvc3Bv" \ "MTc4NTk3NDI5MywicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gtYXJtNjQvc3Bv" \
"dGlmeS1hdXRvdXBkYXRlLTEuMi45NC41ODMuZzYwMzk0YmQ1LTQ4NzIudGJ6In0." \ "dGlmeS1hdXRvdXBkYXRlLTEuMi45NS40NTMuZzBlZWViYmVkLTM2NzIudGJ6In0." \
"dgikJbvCtP540BHTmB-PCnZR7bagMsocETWW64nGYaJiz_dOxNqszB2MkBDPaboQ" \ "fRBIOAjThS9W-m_0EEw7oxd2tpIViU26gBsVdScwrnaT1XtJxhLp28bP26kd6pF4" \
"LJpPtyocB92z-gVpbZeQNTkaK0jYBp4yIzF5RBk6AUMQNmnKkmTaggELxtit0euc" \ "DsFxz5lmb5UogRCMhjdLMVvwUrJlMYR_IDKOkWxiOzFXG-CBEBCbylkm-hT5JG85" \
"bT0_ieDyqTl3tskBo68rrw1ifNnwZVRPsy07dL_RYQrY3meXuQqf8r0KxhqTysgj" \ "fy7u5ZtXq-fUiyl5VEchP1vYH8JMZiHHC4i2V1SM9O1ExOeqxNccu_4m29Hh4gFK" \
"1pN7OQwO_sAS8WpNpZZ1vIeTMVf9ZQGmqh19Bx6dbCcDgrzTPgG7BrRHtcOa6tXG" \ "5qPIMnlD8egTqy_wstsZa7M5sHkmdjsJ4-SXxGy87ONtnbO9UxPOEQYemNOf9Zqq" \
"WbfqVVyKVYSwDNVazIRR4NAjK4n0mfy_7t5r3eVfe04JTITNo0yX_kbybLMReRCh" \ "iGRs10px09Mb4Mk-ZnBiLl9jhLrTsVHSnBHr4Alet1D7kcMmxRnFxkBAeUHCaTw3" \
"lACVioAhwrvZWfirYjK8jw") "xUtqn8T49yF538bgsKK5kw")
rollbackA_X=$(printf "%s" \ rollbackA_X=$(printf "%s" \
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODY4MDU4ODQsIm5iZiI6" \ "eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODg0Mzg0ODksIm5iZiI6" \
"MTc4NDIxMzg4NCwicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gteDg2XzY0L3Nw" \ "MTc4NTg0NjQ4OSwicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gteDg2XzY0L3Nw" \
"b3RpZnktYXV0b3VwZGF0ZS0xLjIuOTMuNjY3Lmc3YjVjYzBjZS0yMzg4LnRieiJ9" \ "b3RpZnktYXV0b3VwZGF0ZS0xLjIuOTQuNTgzLmc2MDM5NGJkNS00ODgxLnRieiJ9" \
".TImdHMAe0XIJldwwf_pHsz4K-C-dcaYg2cJqrXZovmmB8WRxRCPvYwSJnduwGi8" \ ".VjPA-sh1Tjg72unZXmb-Mn-NRL64czXwOMkQzy0IN6gsRaYrwqsIXAMPldiJHgo" \
"Y-aiEMTqnLul0DF0AquzXFRpu8u1aX9lET64wv9PYdUZ7ijBCskNWIZKL8uyf5WN" \ "SSp6HtSBkepcI9bagoM6US0rT9FEDkrjUn03kp4wl0tHxyahfDVFCbAMskc5VCVf" \
"eJeA2qNMXxdwQtwX2bYwCCcwpq13Fi6iIjSHo5tnexz1vgmujHd-4PW4nfqE6SPV" \ "yb1LK_GhNX2bvzTz9lLq1tSlNXclQNQB-NH21IfnvUViNF6-eXoTkvPSMNsLNg-n" \
"I2gLbvTAmtlgpvFkO8dSPbDEzMb-mGSD2H_qRdfmAbD6bdhyljwHG3ZJeMwGsdc3" \ "t8kOePUNm1ypOZ4SgJxxUyz4QaIftyvIH-7PEeyrbgIyKYukEB8RbtjjPlH3DfUi" \
"Vffh-GSjMG5viu3nbEXo6KXj2yzAUg1GOhpNefTPTTXkaGFUqeqi7KtNMQVLkyz7" \ "lmYMaJKOuDz2dww_r-4nPMIdCABdwyPj7RzZ1Yx5wMGVpAVpbNYg5nqRrTOs2PxO" \
"n49X1xTa1tK-50c2Gyv5HPw") "lmmjgMDdLBfx6PKz1eww6Xw")
rollbackA_A=$(printf "%s" \ rollbackA_A=$(printf "%s" \
"eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODY4MDU4ODMsIm5iZiI6" \ "eyJpc3MiOiJzY2RuLXVybC1zaWduZXIiLCJleHAiOjE3ODg0Mzg0ODksIm5iZiI6" \
"MTc4NDIxMzg4MywicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gtYXJtNjQvc3Bv" \ "MTc4NTg0NjQ4OSwicGF0aCI6Ii91cGdyYWRlL2NsaWVudC9vc3gtYXJtNjQvc3Bv" \
"dGlmeS1hdXRvdXBkYXRlLTEuMi45My42NjcuZzdiNWNjMGNlLTIzODUudGJ6In0." \ "dGlmeS1hdXRvdXBkYXRlLTEuMi45NC41ODMuZzYwMzk0YmQ1LTQ4NzIudGJ6In0." \
"gzHERnomKyNB7L2bZjz3wOt_W6NoDPk-bHvNQRsWecdinnv1mXchC92ICK8dBJeh" \ "Ch1itbFNAno5o7YDJ_Eh2ZjHuA12PIvkWrBIJmbHAH6AqdUXC2gbD4znygHVWtPn" \
"W2yfoFb-GaANn1Ky48Eie5FeO_rE0C8Y1u6FJnPbz9qgvFEx_2ebVF3c6cPF5hag" \ "wc74tChZKO_-a79N0v3d26P6_rjPUir3hbtRn09YFzd4Tz4IZXHKY_bGg8YZI4rO" \
"i3b3pCjsvWbYVsi4tPIJaJ3S7WX_mmg-KiVeseUFDvEOvdT_Rf_H0YM2hNwGdjhw" \ "0pijbtgVQ1dnLOG90aTROC8jCSpwj4dBmzEBwKgE68J_n5sEJJMxNNHCzpV5P-mC" \
"74y0O2H_a-4jgG2qSSMBByI5ldfdypm_S-GiH7uIajNtHEmxt-XpoeHbbnunKoMy" \ "MHQiardZdR1zShvBy6d4CSzcaIiTZFTfeCnnyc8eyV9cY2wf18f4uEDgaZs90KKd" \
"XCpCq1x1j1FYrMfNaWFkJtf7LEY2STMa33lXoIEnkxmW1VwwKkywoLBlsfxFE0a0" \ "J-MpU41Anjpa5yk2SUVsTSHJsKbR4s4vbW25jw2Z5rlfBJsegbxLuNAu00t9Y14R" \
"0wFx8b66ioOoAGY07D8wFg") "8eXZeGV3NgCrQCddw1sIzA")
while getopts ':BcdefF:hilopP:SvV:-:' flag; do while getopts ':BcdefF:hilopP:SvV:-:' flag; do
case "${flag}" in case "${flag}" in
@@ -107,7 +107,7 @@ while getopts ':BcdefF:hilopP:SvV:-:' flag; do
noexp) excludeExp='true' ;; noexp) excludeExp='true' ;;
oldui) oldUi='true' ;; oldui) oldUi='true' ;;
premium) paidPremium='true' ;; premium) paidPremium='true' ;;
rollback) [[ "${platformType}" == "macOS" ]] && rollback='true'; installMac='true' ;; rollback) [[ "${platformType}" == "macOS" ]] && { rollback='true'; installMac='true'; } ;;
skipcodesign) [[ "${platformType}" == "macOS" ]] && skipCodesign='true' ;; skipcodesign) [[ "${platformType}" == "macOS" ]] && skipCodesign='true' ;;
stable) [[ "${platformType}" == "Linux" ]] && stableVar='true' ;; stable) [[ "${platformType}" == "Linux" ]] && stableVar='true' ;;
uninstall) uninstallSpotx='true' ;; uninstall) uninstallSpotx='true' ;;
@@ -142,7 +142,7 @@ sxbLiveVer=$(printf "%s" \
"kcwwmW0V0VPRXQ6dlb1MEWyF1RYV3dxs0a4xGTjR3QaNWNDhlcRdEWvhTeKdWVtJGd" \ "kcwwmW0V0VPRXQ6dlb1MEWyF1RYV3dxs0a4xGTjR3QaNWNDhlcRdEWvhTeKdWVtJGd" \
"BNkY5Z1Rjd2dIlUaw42YspVMadjUpl0Z3BzY0F0UjRXQDJWeWNTW" \ "BNkY5Z1Rjd2dIlUaw42YspVMadjUpl0Z3BzY0F0UjRXQDJWeWNTW" \
| rev | base64 --decode | base64 --decode) | rev | base64 --decode | base64 --decode)
sxbLive=$(eval "${sxbLiveVer}") [[ "${SPOTX_BUILD_MODE}" ]] && sxbLive="${buildVer}" || sxbLive=$(eval "${sxbLiveVer}")
sxbVer=$(echo ${buildVer} | perl -ne '/(.*)\./ && print "$1"') sxbVer=$(echo ${buildVer} | perl -ne '/(.*)\./ && print "$1"')
verCk=$(printf "%s" \ verCk=$(printf "%s" \
"9QzRYNGayMGaKdUZwkzRjpXODplb1k3YwJ0QRdWVHJWaGdkYwZUbkhmQ5NGcCNl" \ "9QzRYNGayMGaKdUZwkzRjpXODplb1k3YwJ0QRdWVHJWaGdkYwZUbkhmQ5NGcCNl" \
@@ -277,16 +277,18 @@ macos_prepare() {
} }
linux_client_variant() { linux_client_variant() {
[[ "${installPath}" == *"flatpak"* ]] && { [[ "${clientVariant}" == "flatpak" || "${installPath}" == *"flatpak"* ]] && {
command -v flatpak >/dev/null && flatpak list | grep spotify >/dev/null && { command -v flatpak >/dev/null && flatpak info com.spotify.Client >/dev/null 2>&1 && {
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"') 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+x}" ]] && versionFailed='true' || { clientVer="${flatpakVer}"; flatpakClient='true'; } [[ -z "${flatpakVer}" ]] && 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) cachePath="${HOME}/.var/app/com.spotify.Client/cache/spotify"
[[ -d "${cachePath}" ]] || unset cachePath
} }
return 0 return 0
} }
[[ "${installPath}" == *"opt/spotify"* || "${installPath}" == *"spotify-launcher"* || "${installPath}" == *"usr/share/spotify"* ]] && { [[ "${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
} }
return 0 return 0
@@ -312,7 +314,7 @@ linux_deb_prepare() {
linux_no_client() { linux_no_client() {
command -v snap >/dev/null && snap list spotify &>/dev/null && { 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 exit 1
} }
command -v apt >/dev/null && { command -v apt >/dev/null && {
@@ -326,16 +328,56 @@ linux_no_client() {
exit 1 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() { 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 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) [[ -d "${path}" ]] || continue
[[ -n "${installPath}" ]] && return 0 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 done
return 1 return 1
} }
linux_set_path() { linux_set_path() {
local requestedPath
[[ "${installDeb}" ]] && { linux_deb_prepare; return; } [[ "${installDeb}" ]] && { linux_deb_prepare; return; }
[[ -z "${installPath+x}" ]] && { [[ -z "${installPath+x}" ]] && {
echo -e "Searching for client directory...\n" echo -e "Searching for client directory...\n"
@@ -347,11 +389,13 @@ linux_set_path() {
} || linux_no_client } || linux_no_client
return return
} }
[[ "${installPath}" == *"snapd/snap"* || "${installPath}" == *"snap/spotify"* || "${installPath}" == *"snap/bin"* ]] && { requestedPath="${installPath%/}"
echo -e "${red}Error:${clr} Snap client not supported. See FAQ for more info.\n" >&2 [[ "${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 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" echo -e "Using client Directory: ${installOutput}\n"
linux_client_variant linux_client_variant
} || { } || {
@@ -368,7 +412,6 @@ linux_prepare() {
appBak="${appBinary}.bak" appBak="${appBinary}.bak"
snapshotBinary="${appPath}/v8_context_snapshot.bin" snapshotBinary="${appPath}/v8_context_snapshot.bin"
xpuiPath="${appPath}/Apps" 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} $(cat /etc/*release | grep PRETTY_NAME | cut -d '"' -f2)"
[[ "${debug}" ]] && echo -e "${green}Debug:${clr} $(uname -m) detected" [[ "${debug}" ]] && echo -e "${green}Debug:${clr} $(uname -m) detected"
[[ "${debug}" ]] && command -v apt >/dev/null && echo -e "${green}Debug:${clr} APT 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" (($(ver "${clientVer}") > $(ver "${legacyMaxVer}"))) && macos_legacy_notice "toohigh"
client_version_output client_version_output
ver_check 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}") [[ -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() { 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 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 || { 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 echo -e "${yellow}Warning:${clr} SpotX-Bash does not have write permission in client directory.\nRequesting sudo permission..." >&2
sudo -v || { sudo -v || {
@@ -453,17 +509,172 @@ check_write_permission() {
exit 1 exit 1
} }
} }
sudo chown -R "${target_user}" "${path_to_check}"
sudo chmod -R u+rwX,go-w "${path_to_check}"
} }
done 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() { uninstall_spotx() {
rm -f "${appBinary}" 2>/dev/null atomic_copy "${appBak}" "${appBinary}" && atomic_copy "${xpuiBak}" "${xpuiSpa}" || return 1
mv -f "${appBak}" "${appBinary}" rm -f "${appBak}" "${xpuiBak}" 2>/dev/null
rm -f "${xpuiSpa}" 2>/dev/null
mv -f "${xpuiBak}" "${xpuiSpa}"
rm -rf "${xpuiDir}" 2>/dev/null rm -rf "${xpuiDir}" 2>/dev/null
} }
@@ -474,12 +685,23 @@ run_uninstall_check() {
exit 1 exit 1
} }
check_write_permission "${appPath}" "${appBinary}" "${xpuiPath}" "${xpuiSpa}" check_write_permission "${appPath}" "${appBinary}" "${xpuiPath}" "${xpuiSpa}"
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && protected_stage_prepare
[[ "${cleanAB}" ]] && { [[ "${cleanAB}" ]] && {
echo -e "${yellow}Warning:${clr} SpotX-Bash has detected abnormal behavior.\nClient reinstallation may be required...\n" >&2 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 "${appBak}" 2>/dev/null
rm -f "${xpuiBak}" 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" 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 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() { linux_deb_install() {
sudo_check sudo_check
@@ -587,6 +835,7 @@ linux_deb_install() {
} }
macos_client_install() { macos_client_install() {
local downloadPath="${HOME}/Downloads/${fileVar}" stagedApp
[[ ! -w "${installPath}" ]] && { [[ ! -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 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 exit 1
@@ -605,22 +854,54 @@ macos_client_install() {
"alHZyIWeChFT0F0UjRXQDJWeWNTW" \ "alHZyIWeChFT0F0UjRXQDJWeWNTW" \
| rev | base64 --decode | base64 --decode) | rev | base64 --decode | base64 --decode)
eval "${mc01}"; eval "${mc02}" eval "${mc01}"; eval "${mc02}"
tar -tf "$HOME/Downloads/${fileVar}" >/dev/null 2>&1 || { tar -tf "${downloadPath}" >/dev/null 2>&1 || {
rm "$HOME/Downloads/${fileVar}" 2>/dev/null rm "${downloadPath}" 2>/dev/null
echo -e "\n${red}Error:${clr} Downloaded client archive is corrupt or incomplete. Exiting...\n" >&2 echo -e "\n${red}Error:${clr} Downloaded client archive is corrupt or incomplete. Exiting...\n" >&2
exit 1 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" 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 macInstallTemp=$(mktemp -d "${installPath}/.spotx-install.XXXXXXXX") || {
mkdir -p "${appPath}" rm "${downloadPath}" 2>/dev/null
tar -xpf "$HOME/Downloads/${fileVar}" -C "${appPath}" && unset notInstalled versionFailed || {
rm "$HOME/Downloads/${fileVar}" 2>/dev/null
echo -e "\n${red}Error:${clr} Client install failed. Exiting...\n" >&2 echo -e "\n${red}Error:${clr} Client install failed. Exiting...\n" >&2
exit 1 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" 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"') clientVer=$(echo "${fileVar}" | perl -ne '/te-(.*)\..*\./ && print "$1"')
unset notInstalled versionFailed
} }
run_install_check() { 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() { run_cache_check() {
[[ "${clearCache}" ]] && { [[ "${clearCache}" ]] && {
[[ -n "${cachePath}" && -d "${cachePath}" ]] && { [[ -n "${cachePath}" && -d "${cachePath}" ]] && {
@@ -645,6 +940,7 @@ run_cache_check() {
final_setup_check() { final_setup_check() {
[[ "${notInstalled}" ]] && { echo -e "${red}Error:${clr} Client not found\n" >&2; exit 1; } [[ "${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; } [[ ! -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; } [[ "${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=("$@") local A=("$@")
for cmd in "${A[@]}"; do for cmd in "${A[@]}"; do
IFS='&' read -r -a a <<< "${cmd}" 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[5]}" ]] || (( $(ver "${clientVer}") >= $(ver "${a[5]}") )); } &&
{ [[ -z "${a[6]}" ]] || (( $(ver "${clientVer}") <= $(ver "${a[6]}") )); } && { [[ -z "${a[6]}" ]] || (( $(ver "${clientVer}") <= $(ver "${a[6]}") )); } &&
{ [[ -z "${a[7]}" ]] || [[ "${a[7]}" =~ (^|\|)"${platformType}"($|\|) ]]; } && { [[ -z "${a[7]}" ]] || [[ "${a[7]}" =~ (^|\|)"${platformType}"($|\|) ]]; } &&
{ [[ -z "${a[8]}" ]] || [[ "${a[8]}" =~ (^|\|)"${archVar}"($|\|) ]]; } { [[ -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 done
} }
xpui_detect() { xpui_detect() {
[[ (-f "${appBak}" || -f "${xpuiBak}") && "${cleanAB}" ]] && { [[ (-f "${appBak}" || -f "${xpuiBak}") && "${cleanAB}" ]] && {
rm -f "${appBak}" 2>/dev/null; rm -f "${xpuiBak}" 2>/dev/null backup_spotx || {
cp "${xpuiSpa}" "${xpuiBak}"; cp "${appBinary}" "${appBak}" 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" printf "\xE2\x9C\x94\x20\x43\x72\x65\x61\x74\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n"
return return
} }
[[ (-f "${appBak}" || -f "${xpuiBak}") && "${forceSpotx}" ]] && { [[ (-f "${appBak}" || -f "${xpuiBak}") && "${forceSpotx}" ]] && {
[[ -f "${appBak}" ]] && { rm -f "${appBinary}"; cp "${appBak}" "${appBinary}"; } { [[ ! -f "${appBak}" ]] || atomic_copy "${appBak}" "${appBinary}"; } &&
[[ -f "${xpuiBak}" ]] && { rm -f "${xpuiSpa}"; cp "${xpuiBak}" "${xpuiSpa}"; } { [[ ! -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" 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 return
} }
[[ (-f "${appBak}" || -f "${xpuiBak}") && -z "${forceSpotx+x}" ]] && { [[ (-f "${appBak}" || -f "${xpuiBak}") && -z "${forceSpotx+x}" ]] && {
rm -rf "${xpuiDir}" 2>/dev/null
xpuiSkip='true' xpuiSkip='true'
printf "\xE2\x9C\x94\x20\x44\x65\x74\x65\x63\x74\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n" 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 "\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 echo -e "Use the '-f' flag to force SpotX-Bash to run.\n" >&2
return return
} }
cp "${xpuiSpa}" "${xpuiBak}" backup_spotx || {
cp "${appBinary}" "${appBak}" 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" 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() { xpui_open() {
rm -rf "${xpuiDir}" 2>/dev/null
mkdir -p "${xpuiDir}" mkdir -p "${xpuiDir}"
xpuiTempCreated='true'
unzip -qq "${xpuiSpa}" -d "${xpuiDir}" || { unzip -qq "${xpuiSpa}" -d "${xpuiDir}" || {
rm -rf "${xpuiDir}" 2>/dev/null rm -rf "${xpuiDir}" 2>/dev/null
echo -e "\n${red}Error:${clr} Failed to unpack xpui.spa. Reinstall client. Exiting...\n" >&2 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() { run_core_start() {
final_setup_check final_setup_check
check_write_permission "${appPath}" "${appBinary}" "${xpuiPath}" "${xpuiSpa}" check_write_permission "${appPath}" "${appBinary}" "${xpuiPath}" "${xpuiSpa}"
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && protected_stage_prepare
xpui_detect xpui_detect
[[ "${xpuiSkip}" ]] && { printf "\xE2\x9C\x94\x20\x46\x69\x6E\x69\x73\x68\x65\x64\n\n"; exit 0; } [[ "${xpuiSkip}" ]] && { printf "\xE2\x9C\x94\x20\x46\x69\x6E\x69\x73\x68\x65\x64\n\n"; exit 0; }
xpui_open xpui_open
@@ -839,20 +1147,40 @@ run_patches() {
} }
run_finish() { run_finish() {
local spaTemp
echo -e "\n//# SpotX was here" >> "${xpuiJs}" echo -e "\n//# SpotX was here" >> "${xpuiJs}"
rm -f "${xpuiSpa}" spaTempDir=$(mktemp -d "${xpuiPath}/.spotx-spa.XXXXXXXX") || {
(cd "${xpuiDir}" && zip -qq -r ../xpui.spa .) || { uninstall_spotx && \
echo -e "\n${red}Error:${clr} Failed to repackage client." >&2 echo -e "\n${red}Error:${clr} Failed to create temporary SPA directory. Original client restored.\n" >&2 || \
echo -e "Spotify is now in a broken state. Please reinstall client.\n" >&2 echo -e "\n${red}Error:${clr} Failed to create temporary SPA directory or restore client. Backups were preserved.\n" >&2
exit 1 exit 1
} }
rm -rf "${xpuiDir}" spaTemp="${spaTempDir}/xpui.spa"
[[ "${platformType}" == "macOS" ]] && { (cd "${xpuiDir}" && zip -qq -r "${spaTemp}" .) &&
[[ "${skipCodesign}" ]] && /usr/bin/xattr -cr "${appPath}" 2>/dev/null || { unzip -tqq "${spaTemp}" &&
/usr/bin/xattr -cr "${appPath}" 2>/dev/null mv -f "${spaTemp}" "${xpuiSpa}" || {
codesign -f --deep -s - "${appPath}" 2>/dev/null rm -rf "${spaTempDir}" 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" 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,7 +1212,7 @@ 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' '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' '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' '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' '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' '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&1.2.93.667' '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'
@@ -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' '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' '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' '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&1.2.93.667' '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' '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' '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=( podEx=(
@@ -924,7 +1252,7 @@ lyricsBgEx=(
) )
aoEx=( 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' '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' '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' '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' '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' 'bLogic&\x00\K\x61(?=\x64\x2D\x6C\x6F\x67\x69\x63\x2F\x73)&\x00&&appBinary&1.1.70.610&1.2.28.581'
@@ -955,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' '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' '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' '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' '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' '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' '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' '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' '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' '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' 'enableNewAdsNpvCanvasAds&Enable Canvas ads for new ads NPV",default:\K!.(?=})&false&s&xpuiJs&1.2.28.581&1.2.51.345'
@@ -972,8 +1302,10 @@ aoEx=(
'enablePodcastSponsoredContent&Enable sponsored content information for podcasts",default:\K!.(?=})&false&s&xpuiJs&1.2.30.1135&1.2.50.335' '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' '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' 'enableSaxLeaderboardAds&Enable SAX Leaderboard Ad Format",default:\K!.(?=})&false&s&xpuiJs&1.2.62.575&1.2.82.428'
'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' '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' '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' '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' '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' 'enableUserFraudCanvas&Enable user fraud Canvas Fingerprinting",default:\K!.(?=})&false&s&xpuiJs&1.2.13.656&1.2.63.394'
@@ -1050,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' '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' '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' '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' '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' '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' 'enableExtraTracklistColumns&extra tracklist columns",default:\K!1&true&s&xpuiJs&1.2.44.405&1.2.71.421'
@@ -1078,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' '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' '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' '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' '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' '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' 'enableNewEpisodes&new episodes view",default:\K!1&true&s&xpuiJs&1.1.84.716&1.2.62.580'
@@ -1101,13 +1434,14 @@ expEx=(
'enablePlaybackBarAnimation&animation of the playback bar",default:\K!1&true&s&xpuiJs&1.2.34.783&1.2.82.428' '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' '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' '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' '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&1.2.93.667' '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' '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' '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' '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' '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' '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' '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' 'enableQueueOnRightPanelAnimations&animations for Queue on the right panel.",default:\K!.(?=})&true&s&xpuiJs&1.2.32.985&1.2.50.335'
@@ -1147,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' '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' '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' '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' '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' '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' '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'
@@ -1165,7 +1500,7 @@ expEx=(
'enableYLXPrereleaseAlbums&album pre-releases in YLX",default:\K!1&true&s&xpuiJs&1.2.32.985' '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' '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' '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' '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' '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' 'isVideoQualityEnabled&video quality settings and the in-player quality picker",default:\K!1&true&s&xpuiJs&1.2.84.194'
@@ -1182,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' '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_prepare
run_uninstall_check run_uninstall_check
run_interactive_check run_interactive_check