Compare commits

12 Commits
Author SHA1 Message Date
fabiodalez-dev c072300969 v2.9.0: Sync upstream 41d3b9c (unified playback API, native OAuth deep link)
- Drop patch #53 (storage.js streaming fallback: upstream removed the hardcoded
  instance list, all old HiFi proxies are down) and #56 (HiFi.ts
  tracks.albums.coverArt — merged upstream). 12/12 remaining patches apply.
- AndroidManifest: add monochrome://auth-callback intent-filter for the native
  OAuth flow (js/accounts/auth.js via @capacitor/browser).
- build-web.sh: recreate dist-web.zip from scratch, prune stale assets/ on deploy.
- versionCode 13, versionName 2.9.0.
- Verified on emulator (Pixel 6 / API 35): search + playback + next track OK.
2026-08-16 15:47:35 +02:00
fabiodalez-devandClaude Sonnet 4.6 bfab3e8f90 v2.8.1: Fix Tidal proxy (tidal-proxy.monochrome.tf)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:40:42 +02:00
fabiodalez-devandClaude Sonnet 4.6 b03298725e v2.8.0: Sync upstream 8cf6740 (Qobuz proxy, theme FOUC fix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 00:48:34 +02:00
fabiodalez-devandClaude Sonnet 4.6 3e8c9e7d98 v2.7.0: Sync upstream 3936c07 (enrichment fallbacks, playlist exports, download fix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 18:17:28 +02:00
fabiodalez-devandClaude Sonnet 4.6 b3730f7a69 v2.6.1: Fix Picsum images on track rows in artist page
Add tracks.albums.coverArt to artist endpoint include in HiFi.ts.
Album items were landing in includedMap without coverArt relationship,
so resolveArtworkId returned null and every track row showed Picsum.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 17:49:18 +02:00
fabiodalez-devandClaude Sonnet 4.6 a83ed74219 Add build-web.sh for music.fabiodalez.it deployment
Builds patched web app (no Android-specific changes) and packages
dist/ as dist-web.zip for manual upload via cPanel File Manager.

Patches applied: artists.profileArt, debounce 800ms, album enrichment,
cache TTL/normalization, search limit=100, streaming instances fallback,
workbox NetworkOnly for audio.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 17:40:15 +02:00
fabiodalez-devandClaude Sonnet 4.6 a935cfbf68 v2.6.0: Sync upstream eb3558a (Tidal API proxy, AAC fix, lyrics)
Upstream changes included:
- All Tidal API calls now routed through tidal-api.geeked.wtf proxy,
  fixing CORS blocks on api.tidal.com in the Android WebView
- AAC 320KBPS download bitrate corrected (HIGH: 256k -> 320k)
- am-lyrics dependency bump
- Parties button can now be disabled from settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 16:58:50 +02:00
fabiodalez-devandClaude Sonnet 4.6 baaca85f5d v2.5.1: Reduce search debounce 3000ms -> 800ms
3 seconds after the last keystroke was too slow for mobile UX.
800ms is the standard for search bars and still avoids firing on
every keystroke. The 'partial query' behavior is normal for search
(same as Spotify/Google) and not a regression.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 11:53:09 +02:00
fabiodalez-devandClaude Sonnet 4.6 71c94caba3 v2.5.0: Fix artist cover images (Picsum → Tidal)
The unified search (q=) was missing 'artists.profileArt' in the
JSON:API include parameter. Artist items in the included array had
no profileArt relationship data and no artworks, so
resolveArtworkId() always returned null → getCoverUrl(null) →
random Picsum images for all artist cards.

Fix: add artists.profileArt to HiFi.ts includeQ. Verified on device:
12/12 artist cards now show resources.tidal.com images, 0 Picsum.

Also fixes fm-logger.js path (public/js/ instead of js/) so Vite
copies it to dist/js/ as a static asset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 11:24:20 +02:00
fabiodalez-devandClaude Sonnet 4.6 9dbd8b2163 v2.4.1: Fix streaming for tracks not on Qobuz
- android-service.js: bootstrap localStorage with 3 live HiFi streaming
  instances (eu-central.monochrome.tf, us-west.monochrome.tf,
  hifi-api.kennyy.com.br) so getStreamUrl() Tidal fallback works on
  tracks missing from Qobuz catalog
- build-android.sh patch #53: replace defunct frankfurt-2 (504) with
  the same 3 live instances in storage.js hardcoded fallback
- bump versionCode 7→8, versionName 2.4.0→2.4.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 10:31:13 +02:00
fabiodalez-devandClaude Sonnet 4.6 c5c87d8c65 gitignore: add android/app/src/main/res/xml/config.xml (generated by cap sync)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 12:27:16 +02:00
fabiodalez-devandClaude Sonnet 4.6 8f67f21b7a v2.4.0: Fix upstream sync for unrelated histories, remove __tidalOriginExtension
- build-android.sh: replace git pull with git archive|tar -x to extract upstream
  web app files without requiring a common git ancestor. Histories are unrelated
  (Android wrapper repo vs upstream web app repo), so git pull/merge would fail.
  Stores last synced upstream SHA in .upstream-sync-sha to skip re-downloads.
- build-android.sh: update cleanup to remove all extracted upstream files instead
  of reverting tracked files (they are not tracked in this repo).
- android/android-service.js: remove window.__tidalOriginExtension flag.
  The upstream proxy-utils.js no longer uses this flag to bypass audio-proxy;
  Capacitor WebView uses monochrome.tf origin (capacitor.config.ts) which already
  satisfies Tidal proxy auth without the extension flag.
- android/fm-logger.js: same — remove __tidalOriginExtension.
- android/app/build.gradle: bump to versionCode 7 / versionName 2.4.0.
- android/app/src/main/res/xml/file_paths.xml: add missing FileProvider paths
  file required by AndroidManifest.xml (was in upstream, missing from our repo).
- .gitignore: add build artifacts (android/.gradle, android/app/build, etc.)
  and .upstream-sync-sha to gitignore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 12:25:02 +02:00
9 changed files with 547 additions and 62 deletions

No files matched your search

+8
View File
@@ -1,3 +1,11 @@
.DS_Store
*.apk
saved-tokens/
.upstream-sync-sha
android/.gradle/
android/build/
android/app/build/
android/capacitor-cordova-android-plugins/
android/app/capacitor.build.gradle
android/app/src/main/assets/
android/app/src/main/res/xml/config.xml
+35 -1
View File
@@ -500,8 +500,42 @@ Studio e guarda l'errore reale. Tipicamente:
## Cronologia delle patch
- **2026-08-16** — v2.9.0: Sync upstream `41d3b9c` (249 commit dopo `abbc2a6`).
Upstream ha riscritto lo stack di riproduzione ("unified API": query TIDAL
native via `HiFiClient` + `music-api.geeked.wtf` con Turnstile + fallback
Deezer; account con better-auth, `@capacitor/browser` per l'OAuth nativo).
Patch rimosse: **#53** `storage.js` fallback istanze streaming (upstream ha
tolto la lista hardcoded, `INSTANCES_URLS` è vuoto e i vecchi proxy HiFi
sono tutti 503/irraggiungibili) e **#56** `HiFi.ts` `tracks.albums.coverArt`
(merged upstream). Le altre 12/12 patch matchano. Aggiunto al manifest del
wrapper l'intent-filter `monochrome://auth-callback` (login nativo).
`build-web.sh`: `rm -f dist-web.zip` prima dello zip (`zip -r` aggiornava
l'archivio vecchio) e prune di `assets/` sul server dopo il deploy.
**Nota importante**: sul dominio `music.fabiodalez.it` la riproduzione NON
funziona più — le API upstream (Turnstile sitekey, CORS del fallback Deezer)
sono bloccate ai soli domini ufficiali (issue #725, risposta del maintainer:
"self-hosted instances won't be able to use the API"). Servono un App API
Key personale, oppure dev-mode + estensione con account TIDAL. L'APK usa
`hostname: monochrome.tf` come origin del WebView e resta nel perimetro:
**verificato su emulatore** (Pixel 6 / API 35, via CDP sul WebView) — ricerca
100 brani, play di "Creep" e brano successivo con MediaSession `PLAYING` e
notifica del foreground service.
- **2026-04-10** — Initial release: 50 + 1 ottimizzazioni implementate
(escluso #31 battery opt su richiesta esplicita dell'utente). Aggiunti
`fm-build.sh`, `verify-patches.sh`, `PATCHES.md`. Rimosso il sed obsoleto
`viewport-fit=cover` perché upstream già lo ship.
- Future update: riservato per quando upstream mergerà le PR candidate
- **2026-05-11** — v2.8.1: Sync upstream `abbc2a6`. Fix proxy Tidal:
`tidal-api.geeked.wtf` → `tidal-proxy.monochrome.tf` (ufficiale su CDN
Cloudflare MXP). Confermato funzionante via test streaming su monochrome.tf.
Rimozione preconnect `fonts.coollabs.io`. Bump am-lyrics.
- **2026-05-08** — v2.8.0: Sync upstream `8cf6740`. Nuovo proxy Qobuz primario
(`trypt-hifi-dl-456461932686.us-west1.run.app`), fix FOUC tema (preload
script in `<head>`), nuova istanza ufficiale `if-it-runs-ship-it.lol`.
Tutti i patch invariati (11/11 pass).
- **2026-05-06** — v2.7.0: Sync upstream `3936c07`. 14 nuovi commit upstream
inclusi senza conflitti (11/11 patch pass). Novità upstream rilevanti:
`enrichArtistsWithPicture` + `enrichTracksWithAlbumCover` come fallback lazy
in `api.js` (complementari ai nostri fix #55/#56 su `HiFi.ts`); fix download
quality Qobuz (LOSSLESS→`'6'`); playlist exports; rimozione browser extension.
Manifest Android già aggiornato (POST_NOTIFICATIONS, READ_MEDIA_AUDIO,
WAKE_LOCK presenti da prima nel wrapper).
+16 -6
View File
@@ -6,12 +6,22 @@
// (fm-logger.js is loaded separately in <head> as a synchronous script
// so it captures ALL console output from the very start, before any module.)
// ── TIDAL ORIGIN BYPASS ──
// Tell the upstream proxy-utils.js that we handle the Origin header natively
// (via TidalWebViewClient.java), so it should use direct CDN URLs instead of
// routing through audio-proxy.binimum.org (which is often down/403).
// This is the same flag the Chrome extension "Monochrome Tidal Bypass" sets.
window.__tidalOriginExtension = true;
// ── STREAMING INSTANCES BOOTSTRAP ──
// Pre-configures HiFi API streaming instances so tracks not on Qobuz
// still play via the Tidal proxy fallback (getStreamUrl → getTrack fallback).
(function () {
var k = 'monochrome-user-api-instances-v1';
var d = JSON.parse(localStorage.getItem(k) || '{}');
if (!d.streaming || d.streaming.length === 0) {
d.streaming = [
{ url: 'https://eu-central.monochrome.tf', isUser: true, version: '2.10' },
{ url: 'https://us-west.monochrome.tf', isUser: true, version: '2.10' },
{ url: 'https://hifi-api.kennyy.com.br', isUser: true, version: '2.10' },
];
localStorage.setItem(k, JSON.stringify(d));
}
})();
// ── UNREGISTER SERVICE WORKER ──
// The upstream workbox SW uses CacheFirst for audio/video, but Tidal CDN
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.monochrome.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
versionCode 13
versionName "2.9.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+8
View File
@@ -25,6 +25,14 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Native OAuth return (js/accounts/auth.js: monochrome://auth-callback via @capacitor/browser + App.appUrlOpen) -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="monochrome" android:host="auth-callback" />
</intent-filter>
</activity>
<service
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
-1
View File
@@ -2,7 +2,6 @@
// Saves all console output to Downloads/FabiodalezMusic/fm-console-log.txt
// Tell upstream we handle Tidal Origin natively — skip audio-proxy.binimum.org.
window.__tidalOriginExtension = true;
// Block Service Worker registration IMMEDIATELY (before any other script runs).
// The upstream VitePWA plugin registers a SW that uses CacheFirst for audio,
+87 -52
View File
@@ -11,6 +11,8 @@ set -euo pipefail
# js/app.js — search debounce 3000→500 ms
# js/api.js — search result limit = 100 (was backend default ≈25)
# js/cache.js — per-type TTL + query normalization (trim/lowercase/diacritics)
# js/app.js — search debounce 3000ms → 800ms
# js/HiFi.ts — add artists.profileArt to unified search include (fix Picsum covers)
#
# All reverted automatically on exit. Upstream repo stays clean.
# ─────────────────────────────────────────────────────────
@@ -29,29 +31,30 @@ cd "$PROJECT_DIR"
cleanup() {
echo ""
echo "▶ Cleaning up patched files..."
# Revert every tracked file we may have touched.
# NOTE: do this file-by-file because `git checkout -- a b c` fails entirely
# if any single file is not tracked (e.g. package-lock.json which upstream
# doesn't track — they use bun.lock).
# Revert tracked files that may have been overwritten by git archive.
for f in \
index.html \
package.json \
vite.config.ts \
js/app.js \
js/api.js \
js/cache.js \
js/ui.js \
js/storage.js \
.gitignore \
android/app/src/main/java/tf/monochrome/music/BackgroundAudioPlugin.java
do
if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
git checkout -- "$f" 2>/dev/null || true
fi
done
# Untracked files we generated during the build
rm -f js/android-service.js js/fm-logger.js package-lock.json
# Remove all upstream web app files we extracted (they are not tracked in this repo).
rm -rf \
index.html package.json package-lock.json bun.lock bun.lockb \
vite.config.ts vite-plugin-auth-gate.js vite-plugin-blob.ts \
vite-plugin-svg-use.ts vite-plugin-upload.js \
styles.css stream-stub.js tsconfig.json tsconfig-eslint.json \
eslint.config.js lhci.yml nginx.conf \
js/ src/ public/ assets/ functions/ images/ dist/ node_modules/ \
.npmrc .prettierrc .stylelintrc.json .htmlhintrc .gitmodules \
.dockerignore .vscode/ .wrangler/ \
android/build.gradle android/capacitor.settings.gradle \
android/gradle.properties android/settings.gradle android/variables.gradle \
android/gradlew android/gradlew.bat android/gradle/ android/.gitignore
rm -rf node_modules/@capgo/capacitor-media-session 2>/dev/null || true
echo " ✓ Source restored to upstream."
echo " ✓ Upstream web app files removed."
}
trap cleanup EXIT
@@ -59,23 +62,46 @@ echo "════════════════════════
echo " Fabiodalez Music — Android Build"
echo "══════════════════════════════════════════"
# ── 1. Pull latest ──
# ── 1. Pull latest upstream web app source ──
# This repo tracks only the Android wrapper. Web app files come from upstream.
# We use git archive (not git pull) because the histories are unrelated.
echo ""
echo "▶ Pulling latest from upstream/main..."
cleanup 2>/dev/null || true
git fetch upstream
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse upstream/main)
if [ "$LOCAL" = "$REMOTE" ]; then
echo " Already up to date."
UPSTREAM_SHA=$(git rev-parse upstream/main)
SYNC_FILE="$PROJECT_DIR/.upstream-sync-sha"
LAST_SHA=$(cat "$SYNC_FILE" 2>/dev/null || echo "")
if [ "$UPSTREAM_SHA" = "$LAST_SHA" ] && [ -f index.html ]; then
echo " Already up to date ($(git rev-parse --short upstream/main))."
read -p " Build anyway? (y/N) " -n 1 -r
echo
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 0
else
echo " $(git rev-list --count HEAD..upstream/main) new commits."
git pull upstream main
echo " ✓ Updated."
N=$(git rev-list --count "${LAST_SHA:-upstream/main^}..upstream/main" 2>/dev/null || echo "?")
echo " $N new commits. Extracting web app from upstream/main..."
# Extract upstream web app files, excluding our Android additions and docs.
git archive upstream/main | tar -x \
--exclude='capacitor.config.ts' \
--exclude='README.md' \
--exclude='android/app' \
--exclude='android/android-service.js' \
--exclude='android/fm-logger.js' \
--exclude='android/capacitor-cordova-android-plugins' \
--exclude='ios' \
--exclude='extension' \
--exclude='docker' \
--exclude='.devcontainer' \
--exclude='.github' \
--exclude='CONTRIBUTING.md' \
--exclude='DOCKER.md' \
--exclude='INSTANCES.md' \
--exclude='THEME_GUIDE.md' \
--exclude='license'
echo "$UPSTREAM_SHA" > "$SYNC_FILE"
echo " ✓ Updated to $(git rev-parse --short upstream/main)."
fi
# ── 2a. Pre-npm patch: fix upstream broken override ──
@@ -167,37 +193,27 @@ def patch(path, before, after, label):
print(" + " + label)
return True
# ── #53: Add working streaming instance to fallback list ──
# The hardcoded fallback in storage.js doesn't include frankfurt-2.monochrome.tf,
# which is often the ONLY working streaming proxy. On a fresh install where the
# uptime worker fetch fails, all hardcoded instances return 403 → no playback.
# ── #53: storage.js streaming fallback — REMOVED 2026-08-16 ──
# Upstream (41d3b9c) dropped the hardcoded instance list: INSTANCES_URLS is
# empty, the offline fallback is `api: [lol.samidy.workers.dev], streaming: []`
# and playback goes through the "unified playback" API (music-api.geeked.wtf)
# plus native TIDAL queries (HiFiClient). The old HiFi proxies are all 503/down.
# ── #54: REMOVED — was forcing native HiFiClient for streaming, which gives
# only preview (1:40) because the browser client credentials aren't premium.
# Streaming MUST go through proxy instances (they have premium credentials).
# ── #1: debounce 3000ms → 800ms ──
# 3 seconds after the last keystroke is too slow for mobile. 800ms is the
# standard for search bars and still avoids firing on every keystroke.
# "Renders partial queries" is normal search behavior (like Spotify/Google).
patch(
"js/storage.js",
""" streaming: [
{ url: 'https://hifi.geeked.wtf', version: '2.7' },""",
""" streaming: [
{ url: 'https://frankfurt-2.monochrome.tf', version: '2.10' },
{ url: 'https://hifi.geeked.wtf', version: '2.7' },""",
"storage.js: add frankfurt-2 to streaming fallback",
"js/app.js",
"}, 3000);",
"}, 800);",
"app.js: search debounce 3000ms -> 800ms",
)
# ── #54: Always try native HiFiClient for streaming (not just proxies) ──
# The upstream code skips HiFiClient for streaming requests (type='streaming')
# and goes straight to proxy instances. But when ALL proxies are down/403,
# there's no fallback. Force shouldTryNative=true so HiFiClient is tried first.
patch(
"js/api.js",
" const shouldTryNative = type !== 'streaming';",
" const shouldTryNative = true; // patched: always try native, including streaming",
"api.js: force native HiFiClient for streaming",
)
# ── #1 + #2: debounce + min-chars REMOVED ──
# The upstream 3000ms debounce works fine in practice — it lets users finish
# typing before navigating. Our attempts to reduce it (500ms, 700ms) all
# caused the search page to render for partial queries.
# Keeping upstream behavior as-is.
#
# The limit=100 and cache normalization patches below still apply.
# ── #52: Enrich albums missing artist/cover from track data ──
@@ -397,6 +413,22 @@ patch(
"vite.config.ts: workbox audio/video CacheFirst -> NetworkOnly",
)
# ── #55: HiFi.ts unified search — add artists.profileArt to include ──
# The unified search (q=) omits 'artists.profileArt' from the include list,
# so artist items in the included array have no profileArt relationships and
# no artwork entries are returned. resolveArtworkId(item,'profileArt') always
# returns null → artist.picture = null → getCoverUrl(null) → random Picsum.
# The per-artist search (a=) already has artists.profileArt — this aligns q=.
patch(
"js/HiFi.ts",
"'albums,albums.coverArt,albums.artists,tracks,tracks.artists,tracks.albums,tracks.albums.coverArt,artists,playlists,videos'",
"'albums,albums.coverArt,albums.artists,tracks,tracks.artists,tracks.albums,tracks.albums.coverArt,artists,artists.profileArt,playlists,videos'",
"HiFi.ts: add artists.profileArt to unified search include (fixes Picsum artist covers)",
)
# ── #56: HiFi.ts artist page tracks.albums.coverArt — UPSTREAM MERGED (2026-08-16) ──
# Upstream now ships 'albums,albums.coverArt,tracks,tracks.albums,tracks.albums.coverArt,biography,profileArt'.
print(" ✓ Upstream JS optimizations applied.")
PYEOF
@@ -404,7 +436,10 @@ echo " ✓ JS upstream optimizations applied (debounce, cache, limits)."
# ── 4. Copy wrapper JS files from android/ storage ──
cp "$PROJECT_DIR/android/android-service.js" js/android-service.js
cp "$PROJECT_DIR/android/fm-logger.js" js/fm-logger.js
# fm-logger must go into public/js/ so Vite copies it to dist/js/ as a static asset.
# Putting it in js/ (source dir) would leave it out of the dist/ bundle entirely.
mkdir -p public/js
cp "$PROJECT_DIR/android/fm-logger.js" public/js/fm-logger.js
echo " ✓ android-service.js + fm-logger.js copied."
# ── 5. Init Capacitor Android if needed ──
Executable
+386
View File
@@ -0,0 +1,386 @@
#!/bin/bash
set -euo pipefail
# ─────────────────────────────────────────────────────────
# build-web.sh — Fabiodalez Music web build + deploy to music.fabiodalez.it
#
# Applies the same functional patches as build-android.sh
# (no Android-specific changes: no android-service.js, no fm-logger,
# no brand rename, no viewport-fit).
#
# Output: dist/ directory + dist-web.zip ready for cPanel upload.
# ─────────────────────────────────────────────────────────
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
DEPLOY_HOST="fabiodalez.it"
# cPanel subdomain document root (standard cPanel layout)
DEPLOY_PATH="music.fabiodalez.it"
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
export PATH="$JAVA_HOME/bin:$PATH"
cd "$PROJECT_DIR"
cleanup() {
echo ""
echo "▶ Cleaning up patched files..."
for f in .gitignore; do
if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
git checkout -- "$f" 2>/dev/null || true
fi
done
rm -rf \
index.html package.json package-lock.json bun.lock bun.lockb \
vite.config.ts vite-plugin-auth-gate.js vite-plugin-blob.ts \
vite-plugin-svg-use.ts vite-plugin-upload.js \
styles.css stream-stub.js tsconfig.json tsconfig-eslint.json \
eslint.config.js lhci.yml nginx.conf \
js/ src/ public/ assets/ functions/ images/ dist/ node_modules/ \
.npmrc .prettierrc .stylelintrc.json .htmlhintrc .gitmodules \
.dockerignore .vscode/ .wrangler/ \
android/build.gradle android/capacitor.settings.gradle \
android/gradle.properties android/settings.gradle android/variables.gradle \
android/gradlew android/gradlew.bat android/gradle/ android/.gitignore
rm -rf node_modules/@capgo/capacitor-media-session 2>/dev/null || true
echo " ✓ Upstream web app files removed."
}
trap cleanup EXIT
echo "══════════════════════════════════════════"
echo " Fabiodalez Music — Web Build"
echo "══════════════════════════════════════════"
# ── 1. Pull latest upstream ──
echo ""
echo "▶ Pulling latest from upstream/main..."
cleanup 2>/dev/null || true
git fetch upstream
UPSTREAM_SHA=$(git rev-parse upstream/main)
SYNC_FILE="$PROJECT_DIR/.upstream-sync-sha"
LAST_SHA=$(cat "$SYNC_FILE" 2>/dev/null || echo "")
if [ "$UPSTREAM_SHA" = "$LAST_SHA" ] && [ -f index.html ]; then
echo " Already up to date ($(git rev-parse --short upstream/main))."
read -p " Build anyway? (y/N) " -n 1 -r
echo
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 0
else
echo " Extracting web app from upstream/main ($(git rev-parse --short upstream/main))..."
git archive upstream/main | tar -x \
--exclude='capacitor.config.ts' \
--exclude='README.md' \
--exclude='android/' \
--exclude='ios/' \
--exclude='extension/' \
--exclude='docker/' \
--exclude='.devcontainer' \
--exclude='.github' \
--exclude='CONTRIBUTING.md' \
--exclude='DOCKER.md' \
--exclude='INSTANCES.md' \
--exclude='THEME_GUIDE.md' \
--exclude='license'
echo "$UPSTREAM_SHA" > "$SYNC_FILE"
echo " ✓ Updated to $(git rev-parse --short upstream/main)."
fi
# ── 2. Fix upstream broken npm override ──
if grep -q '"sourcemap-codec": "\^1.4.14"' package.json; then
sed -i '' 's|"sourcemap-codec": "\^1.4.14"|"sourcemap-codec": "^1.4.8"|' package.json
echo " ✓ package.json: sourcemap-codec override fixed."
fi
# ── 3. Install deps + shim media-session ──
echo ""
echo "▶ Installing dependencies..."
npm install 2>&1 | tail -3
echo " ✓ Done."
SHIM_DIR="node_modules/@capgo/capacitor-media-session"
[ -d "$SHIM_DIR" ] && rm -rf "$SHIM_DIR"
mkdir -p "$SHIM_DIR"
cat > "$SHIM_DIR/index.js" <<'SHIMEOF'
export const MediaSession = {
setActionHandler: async () => {},
setMetadata: async () => {},
setPlaybackState: async () => {},
setPositionState: async () => {},
};
SHIMEOF
cat > "$SHIM_DIR/package.json" <<'SHIMEOF'
{"name":"@capgo/capacitor-media-session","version":"0.0.0-shim","main":"index.js","module":"index.js","type":"module"}
SHIMEOF
echo " ✓ @capgo/capacitor-media-session shimmed."
# ── 4. Apply functional patches ──
echo ""
echo "▶ Patching upstream sources..."
python3 <<'PYEOF'
import sys, os, re
PROJECT_DIR = os.environ.get("PROJECT_DIR", os.getcwd())
def patch(path, before, after, label):
full = os.path.join(PROJECT_DIR, path)
with open(full, "r", encoding="utf-8") as f:
src = f.read()
if before not in src:
print(" ! " + label + ": pattern not found, skipping")
return False
src = src.replace(before, after, 1)
with open(full, "w", encoding="utf-8") as f:
f.write(src)
print(" + " + label)
return True
# ── storage.js streaming fallback: REMOVED 2026-08-16 ──
# Upstream (41d3b9c) dropped the hardcoded instance list: INSTANCES_URLS is
# empty, the offline fallback is `api: [lol.samidy.workers.dev], streaming: []`
# and playback goes through the "unified playback" API (music-api.geeked.wtf)
# plus native TIDAL queries (HiFiClient). The old HiFi proxies are all 503/down.
# ── search debounce 3000ms → 800ms ──
patch(
"js/app.js",
"}, 3000);",
"}, 800);",
"app.js: search debounce 3000ms -> 800ms",
)
# ── enrich albums missing artist/cover from tracks ──
patch(
"js/ui.js",
""" if (finalAlbums.length === 0 && finalTracks.length > 0) {
const albumMap = new Map();
finalTracks.forEach((track) => {
if (track.album && !albumMap.has(track.album.id)) {
albumMap.set(track.album.id, track.album);
}
});
finalAlbums = Array.from(albumMap.values());
}""",
""" if (finalAlbums.length === 0 && finalTracks.length > 0) {
const albumMap = new Map();
finalTracks.forEach((track) => {
if (track.album && !albumMap.has(track.album.id)) {
albumMap.set(track.album.id, track.album);
}
});
finalAlbums = Array.from(albumMap.values());
}
// Enrich albums that have no artist or cover from track data
if (finalAlbums.length > 0 && finalTracks.length > 0) {
const trackInfoMap = new Map();
finalTracks.forEach((track) => {
if (track.album && track.album.id && !trackInfoMap.has(track.album.id)) {
trackInfoMap.set(track.album.id, {
artist: track.artist || (track.artists && track.artists[0]) || null,
cover: (track.album && track.album.cover) || null,
});
}
});
finalAlbums.forEach((album) => {
const info = trackInfoMap.get(album.id);
if (!info) return;
if (!album.artist && !album.artists?.length && info.artist) {
album.artist = info.artist;
album.artists = [info.artist];
}
if (!album.cover && info.cover) {
album.cover = info.cover;
}
});
}""",
"ui.js: enrich albums missing artist/cover from tracks",
)
# ── cache: per-type TTL ──
patch(
"js/cache.js",
" this.ttl = options.ttl || 1000 * 60 * 30;",
""" this.ttl = options.ttl || 1000 * 60 * 30;
this.ttlByType = {
search_all: 1000 * 60 * 10,
search_tracks: 1000 * 60 * 10,
search_artists: 1000 * 60 * 60,
search_albums: 1000 * 60 * 30,
search_playlists: 1000 * 60 * 60,
search_videos: 1000 * 60 * 15,
};""",
"cache.js: per-type TTL map",
)
# ── cache: query normalization ──
patch(
"js/cache.js",
""" generateKey(type, params) {
const paramString = typeof params === 'object' ? JSON.stringify(params) : String(params);
return `${type}:${paramString}`;
}""",
""" generateKey(type, params) {
let normalized = params;
if (typeof params === 'string' && typeof type === 'string' && type.startsWith('search')) {
normalized = params
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/[\\u0300-\\u036f]/g, '');
}
const paramString = typeof normalized === 'object' ? JSON.stringify(normalized) : String(normalized);
return `${type}:${paramString}`;
}""",
"cache.js: query normalization (trim/lowercase/NFD)",
)
# ── cache: per-type TTL lookup in get() ──
patch(
"js/cache.js",
""" async get(type, params) {
const key = this.generateKey(type, params);
if (this.memoryCache.has(key)) {
const cached = this.memoryCache.get(key);
if (Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
this.memoryCache.delete(key);
}
if (this.db) {
try {
const cached = await this.getFromIndexedDB(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {""",
""" async get(type, params) {
const key = this.generateKey(type, params);
const effectiveTtl = (this.ttlByType && this.ttlByType[type]) || this.ttl;
if (this.memoryCache.has(key)) {
const cached = this.memoryCache.get(key);
if (Date.now() - cached.timestamp < effectiveTtl) {
return cached.data;
}
this.memoryCache.delete(key);
}
if (this.db) {
try {
const cached = await this.getFromIndexedDB(key);
if (cached && Date.now() - cached.timestamp < effectiveTtl) {""",
"cache.js: per-type TTL lookup in get()",
)
# ── api.js: search limit=100 ──
patch("js/api.js",
"const response = await this.fetchWithRetry(`/search/?q=${encodeURIComponent(query)}`, options);",
"const response = await this.fetchWithRetry(`/search/?q=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
"api.js: search() unified — limit=100")
patch("js/api.js",
"const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}`, options);",
"const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
"api.js: searchTracks — limit=100")
patch("js/api.js",
"const response = await this.fetchWithRetry(`/search/?al=${encodeURIComponent(query)}`, options);",
"const response = await this.fetchWithRetry(`/search/?al=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
"api.js: searchAlbums — limit=100")
patch("js/api.js",
"const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}`, options);",
"const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
"api.js: searchPlaylists — limit=100")
patch("js/api.js",
"""const response = await this.fetchWithRetry(`/search/?v=${encodeURIComponent(query)}`, {
...options,
});""",
"""const response = await this.fetchWithRetry(`/search/?v=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, {
...options,
});""",
"api.js: searchVideos — limit=100")
try:
with open(os.path.join(PROJECT_DIR, "js/api.js"), "r", encoding="utf-8") as f:
api_src = f.read()
new_src, n = re.subn(
r"(`/search/\?a=\$\{encodeURIComponent\(query\)\})(`)",
r"\1&limit=${(options && options.limit) || 100}\2",
api_src,
)
if n > 0:
with open(os.path.join(PROJECT_DIR, "js/api.js"), "w", encoding="utf-8") as f:
f.write(new_src)
print(" + api.js: searchArtists — limit=100")
else:
print(" ! api.js: searchArtists pattern not found, skipping")
except Exception as e:
print(" ! api.js: searchArtists patch failed: " + str(e))
# ── HiFi.ts: add artists.profileArt to unified search include ──
patch(
"js/HiFi.ts",
"'albums,albums.coverArt,albums.artists,tracks,tracks.artists,tracks.albums,tracks.albums.coverArt,artists,playlists,videos'",
"'albums,albums.coverArt,albums.artists,tracks,tracks.artists,tracks.albums,tracks.albums.coverArt,artists,artists.profileArt,playlists,videos'",
"HiFi.ts: add artists.profileArt to unified search include (fixes Picsum artist covers)",
)
# ── HiFi.ts: tracks.albums.coverArt on artist page — UPSTREAM MERGED (2026-08-16) ──
# Upstream now ships 'albums,albums.coverArt,tracks,tracks.albums,tracks.albums.coverArt,biography,profileArt'.
# ── Workbox: CacheFirst → NetworkOnly for audio/video ──
patch(
"vite.config.ts",
"handler: 'CacheFirst',\n options: {\n cacheName: 'media',",
"handler: 'NetworkOnly',\n options: {\n cacheName: 'media',",
"vite.config.ts: workbox audio/video CacheFirst -> NetworkOnly",
)
print(" ✓ All web patches applied.")
PYEOF
echo " ✓ Patches done."
# ── 5. Build ──
echo ""
echo "▶ Building web app..."
npx vite build 2>&1 | tail -3
echo " ✓ Build complete."
# ── 6. Package for upload ──
echo ""
echo "▶ Packaging dist/ for upload..."
# `zip -r` UPDATES an existing archive (stale entries from older builds survive) — start clean.
rm -f dist-web.zip
(cd dist && zip -r ../dist-web.zip . -x "*.map") 2>&1 | tail -3
SIZE=$(du -h "$PROJECT_DIR/dist-web.zip" | awk '{print $1}')
echo " ✓ dist-web.zip (${SIZE}) ready."
# ── 7. Deploy via rsync over SSH (if accessible) ──
echo ""
echo "▶ Attempting deploy to ${DEPLOY_HOST}:${DEPLOY_PATH}/..."
if rsync -az --no-perms --omit-dir-times \
-e "ssh -o ConnectTimeout=10 -o BatchMode=yes" \
dist/ "${DEPLOY_HOST}:~/${DEPLOY_PATH}/" 2>&1; then
echo " ✓ Deployed to ${DEPLOY_HOST}:~/${DEPLOY_PATH}/"
# assets/ is 100% build output (hashed chunks + public/assets) → safe to prune
# stale files from previous builds there. Root is left untouched (.htaccess, cgi-bin, …).
rsync -az --delete --no-perms --omit-dir-times \
-e "ssh -o ConnectTimeout=10 -o BatchMode=yes" \
dist/assets/ "${DEPLOY_HOST}:~/${DEPLOY_PATH}/assets/" 2>&1 \
&& echo " ✓ Pruned stale files in assets/"
echo ""
echo "══════════════════════════════════════════"
echo " Live at: https://${DEPLOY_PATH}/"
echo "══════════════════════════════════════════"
else
echo " ! rsync failed — use cPanel File Manager instead:"
echo ""
echo " 1. Open cPanel → File Manager → music.fabiodalez.it/"
echo " 2. Upload: $PROJECT_DIR/dist-web.zip"
echo " 3. Extract in place"
echo " 4. Delete dist-web.zip"
echo ""
echo "══════════════════════════════════════════"
echo " ZIP: $PROJECT_DIR/dist-web.zip (${SIZE})"
echo "══════════════════════════════════════════"
fi
# cleanup() runs automatically via trap EXIT