From d662cbd74a040babaed1162bfcc0ca6940e691cd Mon Sep 17 00:00:00 2001 From: Yuka <128908897+yukazakiri@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:02:04 +0800 Subject: [PATCH] Fix Spicetify wrapper repair in Spotify setup --- scripts/colors/apply-spicetify-theme.sh | 56 ++++++ scripts/setup/README.md | 19 ++ scripts/setup/_lib.sh | 10 +- scripts/setup/spotify.sh | 251 ++++++++++++++++++++++-- 4 files changed, 317 insertions(+), 19 deletions(-) diff --git a/scripts/colors/apply-spicetify-theme.sh b/scripts/colors/apply-spicetify-theme.sh index d27e4fa69..d02ef9e8c 100755 --- a/scripts/colors/apply-spicetify-theme.sh +++ b/scripts/colors/apply-spicetify-theme.sh @@ -494,6 +494,53 @@ sync_live_user_css() { cp "$user_css" "$live_user_css" } +find_spicetify_wrapper_source() { + local candidate base + + # setup/spotify.sh builds this asset for source-built Spicetify packages that + # omit it. This theme script does not build dependencies; it only preserves a + # working patched Spotify bundle across later theme reapplies. + if [[ -s "/opt/spicetify-cli/jsHelper/spicetifyWrapper.js" ]]; then + printf '%s\n' "/opt/spicetify-cli/jsHelper/spicetifyWrapper.js" + return 0 + fi + + for base in "$HOME/.cache/paru/clone/spicetify-cli" "$HOME/.cache/yay/spicetify-cli"; do + [[ -d "$base" ]] || continue + candidate="$( + find "$base" -maxdepth 5 -type f -path '*/jsHelper/spicetifyWrapper.js' -print 2>/dev/null \ + | sort -Vr \ + | head -n1 + )" + if [[ -s "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done +} + +sync_spicetify_wrapper() { + local xpui_dir="$1" + local index_html="$xpui_dir/index.html" + local live_wrapper="$xpui_dir/helper/spicetifyWrapper.js" + + [[ -f "$index_html" ]] || return 1 + grep -q "helper/spicetifyWrapper.js" "$index_html" || return 0 + [[ -s "$live_wrapper" ]] && return 0 + + local wrapper_source="" + wrapper_source="$(find_spicetify_wrapper_source || true)" + + if [[ -z "$wrapper_source" ]]; then + log "spicetifyWrapper.js is missing; run setup-spotify or pnpm build:wrapper in the Spicetify source tree" + return 1 + fi + + mkdir -p "$xpui_dir/helper" + cp "$wrapper_source" "$live_wrapper" + log "Synced missing spicetifyWrapper.js from $wrapper_source" +} + # ─── Main logic ──────────────────────────────────────────────────────────────── main() { @@ -521,6 +568,10 @@ main() { local spotify_running=false is_process_running "spotify" && spotify_running=true + if [[ -n "$xpui_dir" ]]; then + sync_spicetify_wrapper "$xpui_dir" || true + fi + if [[ -n "$xpui_dir" ]] && is_live_install_patched "$xpui_dir"; then if sync_live_user_css "$theme_dir/user.css" "$xpui_dir"; then log "Synced live Spotify user.css" @@ -544,6 +595,11 @@ main() { exit 1 fi + xpui_dir="$(get_spotify_xpui_dir "$spicetify_config")" + if [[ -n "$xpui_dir" ]]; then + sync_spicetify_wrapper "$xpui_dir" || true + fi + # Some spicetify versions launch Spotify as a side effect despite -n. # Kill it if it wasn't running before we called apply. if ! $spotify_running && is_process_running "spotify"; then diff --git a/scripts/setup/README.md b/scripts/setup/README.md index 646cc5a4d..5da9b9708 100644 --- a/scripts/setup/README.md +++ b/scripts/setup/README.md @@ -114,3 +114,22 @@ Constants exposed after sourcing: `DISTRO_ID`, `DISTRO_LIKE`, `SETUP_TAG`, the user can recover from manually should print actionable instructions. - **One concern per script.** If a recipe grows beyond ~80 lines, factor a helper into `_lib.sh` instead of duplicating logic. + +## Spotify / Spicetify Notes + +`spotify.sh` intentionally repairs a missing `spicetifyWrapper.js` before it +runs `spicetify backup apply`. Spicetify v2.44+ generates this file during the +release build from `src/jsHelper/spicetifyWrapper`. Some source-built Linux +packages have shipped `/opt/spicetify-cli/jsHelper` without running +`scripts/build-wrapper.mjs`; Spicetify still injects +`helper/spicetifyWrapper.js` into Spotify's `index.html`, and Spotify opens to +a blank XPUI surface when that helper is absent. + +The recipe only builds the wrapper when `/opt/spicetify-cli/jsHelper/ +spicetifyWrapper.js` is missing. It first reuses cached AUR source from `paru` +or `yay`; if no cache exists and the installed Spicetify version is a release +tag, it downloads the matching source tarball from GitHub. Node/npm are only +installed on-demand for this repair path. After Spicetify applies, the recipe +also verifies Spotify's patched XPUI helper directory contains the wrapper, +because a broken package asset directory can otherwise recreate the blank-window +state on every reapply. diff --git a/scripts/setup/_lib.sh b/scripts/setup/_lib.sh index aa932bfef..f7db1256b 100755 --- a/scripts/setup/_lib.sh +++ b/scripts/setup/_lib.sh @@ -69,10 +69,18 @@ setup_fail() { setup_notify "$msg" "dialog-error" } +setup_err_trap() { + local status="$1" + local line="$2" + setup_fail "$SETUP_TITLE failed at line $line" + setup_finish_pause + exit "$status" +} + setup_init() { SETUP_TAG="setup-$1" SETUP_TITLE="$2" - trap 'setup_fail "$SETUP_TITLE failed at line $LINENO"' ERR + trap 'setup_err_trap "$?" "$LINENO"' ERR setup_notify "Starting…" "download" printf '\033[1;35m▶ %s\033[0m (distro: %s)\n' "$SETUP_TITLE" "$DISTRO_ID" } diff --git a/scripts/setup/spotify.sh b/scripts/setup/spotify.sh index 0d92c8655..96f556ee9 100755 --- a/scripts/setup/spotify.sh +++ b/scripts/setup/spotify.sh @@ -7,13 +7,14 @@ # @meta icon: music_note # @meta keywords: spotify music spicetify aur flatpak # -# Arch family : `spotify` (AUR) + `spicetify-cli` (AUR). Tries -# `spicetify backup apply` first; only launches Spotify -# (so the user can sign in and Spotify can generate its +# Arch family : `spotify` (AUR) + `spicetify-cli` (AUR). Repairs the +# Spicetify v2.44+ wrapper asset when source-built packages omit +# it, then tries `spicetify backup apply` first. It only launches +# Spotify (so the user can sign in and Spotify can generate its # prefs file) if the first apply fails. Then sets prefs_path, -# retries, installs the Marketplace, and — only if the user -# has enabled `appearance.wallpaperTheming.enableSpicetify` -# in config.json — applies the iNiR Spicetify theme. +# retries, installs the Marketplace, and — only if the user has +# enabled `appearance.wallpaperTheming.enableSpicetify` in +# config.json — applies the iNiR Spicetify theme. # Other distros: falls back to the Flatpak build of Spotify. Spicetify is # skipped because it cannot patch the Flatpak install reliably. @@ -28,6 +29,186 @@ _find_prefs() { find "$HOME" -path '*/spotify/prefs' -print -quit 2>/dev/null } +_have_spotify_and_spicetify() { + have_cmd spotify && have_cmd spicetify +} + +_run_may_fail() { + set +e + "$@" + local status=$? + set -e + return "$status" +} + +_spicetify_version() { + local version + version="$(spicetify -v 2>/dev/null | sed -E 's/\x1b\[[0-9;]*m//g; s/^v//; s/[^0-9.].*$//' | head -n1)" + [[ -n "$version" ]] && printf '%s\n' "$version" +} + +_find_spicetify_source() { + local version="${1:-}" + local base candidate + + if [[ -n "$version" ]]; then + for candidate in \ + "$HOME/.cache/paru/clone/spicetify-cli/src/spicetify-cli-$version" \ + "$HOME/.cache/yay/spicetify-cli/src/spicetify-cli-$version"; do + if [[ -f "$candidate/scripts/build-wrapper.mjs" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done + fi + + for base in "$HOME/.cache/paru/clone/spicetify-cli" "$HOME/.cache/yay/spicetify-cli"; do + [[ -d "$base" ]] || continue + find "$base" -maxdepth 4 -type f -path '*/scripts/build-wrapper.mjs' -print 2>/dev/null + done | sed 's|/scripts/build-wrapper.mjs$||' | sort -Vr | head -n1 +} + +_download_spicetify_source() { + local version="$1" + local target_base="$2" + local archive="$target_base/spicetify-cli-$version.tar.gz" + local source_dir="$target_base/spicetify-cli-$version" + + mkdir -p "$target_base" + curl -fsSL \ + -o "$archive" \ + "https://github.com/spicetify/cli/archive/refs/tags/v$version.tar.gz" + tar -xzf "$archive" -C "$target_base" + if [[ -d "$target_base/cli-$version" ]]; then + mv "$target_base/cli-$version" "$source_dir" + fi + [[ -f "$source_dir/scripts/build-wrapper.mjs" ]] || return 1 + printf '%s\n' "$source_dir" +} + +_build_spicetify_wrapper() { + local source_dir="$1" + + if [[ -s "$source_dir/jsHelper/spicetifyWrapper.js" ]]; then + return 0 + fi + + if ! have_cmd node || ! have_cmd npm; then + echo " · Installing Node.js/npm to build Spicetify's wrapper…" + install_arch nodejs npm + fi + + echo " · Building missing spicetifyWrapper.js…" + if have_cmd pnpm && (cd "$source_dir" && _run_may_fail pnpm install --frozen-lockfile && _run_may_fail pnpm build:wrapper); then + [[ -s "$source_dir/jsHelper/spicetifyWrapper.js" ]] + return + fi + + echo " · pnpm build failed or is unavailable; trying npm fallback…" + if ! ( + cd "$source_dir" && + _run_may_fail env NPM_CONFIG_ENGINE_STRICT=false npm install --include=optional --no-audit --no-fund && + _run_may_fail env NPM_CONFIG_ENGINE_STRICT=false npm run build:wrapper + ); then + return 1 + fi + [[ -s "$source_dir/jsHelper/spicetifyWrapper.js" ]] +} + +_ensure_spicetify_wrapper_asset() { + local wrapper_dest="/opt/spicetify-cli/jsHelper/spicetifyWrapper.js" + local version source_dir tmp_base + + if [[ -s "$wrapper_dest" ]]; then + echo " · Spicetify wrapper asset exists." + return 0 + fi + + # Spicetify v2.44 made spicetifyWrapper.js a generated release asset. + # Some source-built packages installed jsHelper without running + # scripts/build-wrapper.mjs, so the generated Spotify index.html references + # helper/spicetifyWrapper.js but the file is absent. That leaves injected + # Spicetify code without the global Spicetify object and Spotify opens as a + # blank XPUI window. Repair the package asset before applying Spicetify. + version="$(_spicetify_version || true)" + if [[ -n "$version" ]]; then + echo " · Spicetify wrapper asset missing; repairing package assets for v$version." + else + echo " · Spicetify wrapper asset missing; repairing package assets from cached source." + fi + + source_dir="$(_find_spicetify_source "$version")" + if [[ -z "$source_dir" ]]; then + if [[ -z "$version" ]]; then + echo " · Could not determine a release version and no cached source was found." >&2 + return 1 + fi + tmp_base="$(mktemp -d)" + if ! source_dir="$(_download_spicetify_source "$version" "$tmp_base")"; then + echo " · Failed to download Spicetify source for v$version." >&2 + return 1 + fi + fi + + _build_spicetify_wrapper "$source_dir" || return 1 + sudo install -Dm644 "$source_dir/jsHelper/spicetifyWrapper.js" "$wrapper_dest" + echo " · Installed $wrapper_dest" +} + +_spotify_xpui_dir() { + local root="$1" + for d in "$root/Apps/xpui" "$root/xpui"; do + [[ -f "$d/index.html" ]] && echo "$d" && return 0 + done +} + +_sync_spotify_wrapper_asset() { + local spotify_root="$1" + local xpui_dir index_html live_wrapper source_wrapper + + xpui_dir="$(_spotify_xpui_dir "$spotify_root")" + [[ -n "$xpui_dir" ]] || return 0 + + index_html="$xpui_dir/index.html" + grep -q "helper/spicetifyWrapper.js" "$index_html" 2>/dev/null || return 0 + + live_wrapper="$xpui_dir/helper/spicetifyWrapper.js" + [[ -s "$live_wrapper" ]] && return 0 + + source_wrapper="/opt/spicetify-cli/jsHelper/spicetifyWrapper.js" + if [[ ! -s "$source_wrapper" ]]; then + _ensure_spicetify_wrapper_asset || return 1 + fi + + mkdir -p "$xpui_dir/helper" + install -m 600 "$source_wrapper" "$live_wrapper" 2>/dev/null \ + || sudo install -m 600 "$source_wrapper" "$live_wrapper" + echo " · Synced missing Spotify XPUI wrapper asset." +} + +_ensure_spotify_writable() { + local spotify_root="$1" + local apps_dir="$spotify_root/Apps" + + if [[ -w "$spotify_root" && -w "$apps_dir" ]]; then + return 0 + fi + + echo " · Granting write access so Spicetify can patch Spotify…" + if _run_may_fail sudo chmod a+wr "$spotify_root" && + _run_may_fail sudo chmod a+wr "$apps_dir" -R; then + return 0 + fi + + if [[ -w "$spotify_root" && -w "$apps_dir" ]]; then + echo " · chmod reported an error, but Spotify paths are writable; continuing." >&2 + return 0 + fi + + echo " · Could not make Spotify install writable: $spotify_root" >&2 + return 1 +} + _await_or_force_close_spotify() { echo echo " ┌─────────────────────────────────────────────────────────────┐" @@ -69,10 +250,20 @@ _theme_enabled_in_config() { setup_init "spotify" "Setup Spotify + Spicetify" if is_arch_like; then - TOTAL=5 + TOTAL=6 setup_progress 1 $TOTAL "Installing Spotify (AUR) and Spicetify CLI" - install_arch -- spotify spicetify-cli + if _have_spotify_and_spicetify; then + echo " · Spotify and Spicetify are already installed." + elif ! _run_may_fail install_arch -- spotify spicetify-cli; then + if _have_spotify_and_spicetify; then + echo " · Package install reported an error, but Spotify and Spicetify are available; continuing." >&2 + else + setup_fail "Could not install Spotify and Spicetify CLI." + setup_finish_pause + exit 1 + fi + fi # Detect the Spotify install directory. Prefer /opt/spotify (AUR package, # has .spa files spicetify needs) over the spotify-launcher expanded dir. @@ -92,10 +283,20 @@ if is_arch_like; then echo " · Spotify at: $spotify_dir" # Ensure spicetify points to the .spa-based install, not a launcher dir spicetify config spotify_path "$spotify_dir" >/dev/null 2>&1 || true - sudo chmod a+wr "$spotify_dir" - sudo chmod a+wr "$spotify_dir/Apps" -R + if ! _ensure_spotify_writable "$spotify_dir"; then + setup_fail "Could not make Spotify writable for Spicetify patching." + setup_finish_pause + exit 1 + fi - setup_progress 3 $TOTAL "Applying Spicetify backup" + setup_progress 3 $TOTAL "Repairing Spicetify wrapper assets" + if ! _ensure_spicetify_wrapper_asset; then + setup_fail "Could not build/install Spicetify wrapper asset." + setup_finish_pause + exit 1 + fi + + setup_progress 4 $TOTAL "Applying Spicetify backup" prefs="$(_find_prefs)" if [[ -n "$prefs" ]]; then echo " · prefs already exists at $prefs" @@ -103,9 +304,15 @@ if is_arch_like; then fi _spicetify_apply() { - if spicetify backup apply; then return 0; fi + if _run_may_fail spicetify backup apply; then + _sync_spotify_wrapper_asset "$spotify_dir" + return 0 + fi # Stale backup — try restore then redo - if spicetify restore backup apply; then return 0; fi + if _run_may_fail spicetify restore backup apply; then + _sync_spotify_wrapper_asset "$spotify_dir" + return 0 + fi # Deadlocked (version mismatch) — nuke backup state and retry local cfg_dir cfg_dir="$(dirname "$(spicetify -c 2>/dev/null)" 2>/dev/null)" @@ -116,7 +323,11 @@ if is_arch_like; then sed -i '/^\[Backup\]/,/^\[/{/^\[Backup\]/!{/^\[/!d}}' \ "${cfg_dir}/config-xpui.ini" 2>/dev/null || true fi - spicetify backup apply + if _run_may_fail spicetify backup apply; then + _sync_spotify_wrapper_asset "$spotify_dir" + return 0 + fi + return 1 } if ! _spicetify_apply; then @@ -136,10 +347,14 @@ if is_arch_like; then fi echo " · Found prefs at $prefs" spicetify config prefs_path "$prefs" >/dev/null 2>&1 || true - _spicetify_apply + if ! _spicetify_apply; then + setup_fail "Spicetify backup/apply failed after Spotify generated prefs." + setup_finish_pause + exit 1 + fi fi - setup_progress 4 $TOTAL "Installing Spicetify Marketplace" + setup_progress 5 $TOTAL "Installing Spicetify Marketplace" if curl -fsSL https://raw.githubusercontent.com/spicetify/marketplace/main/resources/install.sh \ | sh; then echo "Marketplace installed." @@ -148,7 +363,7 @@ if is_arch_like; then fi if _theme_enabled_in_config; then - setup_progress 5 $TOTAL "Applying iNiR Spicetify theme" + setup_progress 6 $TOTAL "Applying iNiR Spicetify theme" theme_script="$SCRIPT_DIR/../colors/apply-spicetify-theme.sh" if [[ -x "$theme_script" ]]; then if "$theme_script"; then @@ -160,7 +375,7 @@ if is_arch_like; then echo "warning: $theme_script not found or not executable; skipping theme." >&2 fi else - setup_progress 5 $TOTAL "Skipping iNiR theme (appearance.wallpaperTheming.enableSpicetify is off)" + setup_progress 6 $TOTAL "Skipping iNiR theme (appearance.wallpaperTheming.enableSpicetify is off)" echo " · Enable it in Settings → Themes → 'Spotify theming' to apply the iNiR theme." fi