mirror of
https://github.com/fabiodalez-dev/Monochrome-Android-APK.git
synced 2026-10-21 00:00:22 -05:00
Compare commits
No files matched your search
@@ -1,2 +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
@@ -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).
|
||||
+66
-108
@@ -3,6 +3,50 @@
|
||||
// Injected into the Monochrome web app at build time.
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// (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.)
|
||||
|
||||
|
||||
// ── 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
|
||||
// streams don't serve CORS headers → workbox can't read the response →
|
||||
// "no-response" error → audio won't play. The Android WebView already has
|
||||
// its own HTTP cache (configured in MainActivity), so we don't need SW at all.
|
||||
if ('serviceWorker' in navigator) {
|
||||
// 1. Deregister any existing service worker
|
||||
navigator.serviceWorker.getRegistrations().then((regs) => {
|
||||
regs.forEach((r) => r.unregister());
|
||||
}).catch(() => {});
|
||||
// 2. Block future registrations — upstream VitePWA calls registerSW()
|
||||
// which would re-register the SW after our deregister, causing a race
|
||||
// condition where the SW intercepts audio fetches and breaks playback.
|
||||
navigator.serviceWorker.register = function () {
|
||||
return Promise.resolve({ unregister: function () { return Promise.resolve(true); } });
|
||||
};
|
||||
// 3. Clear SW cache storage
|
||||
if (typeof caches !== 'undefined') {
|
||||
caches.keys().then((names) => {
|
||||
names.forEach((name) => caches.delete(name));
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ── CLIPBOARD FALLBACK ──
|
||||
if (window.AndroidBridge) {
|
||||
const origClipboard = navigator.clipboard?.writeText?.bind(navigator.clipboard);
|
||||
@@ -509,27 +553,6 @@ if (window.AndroidLocalFiles) {
|
||||
}
|
||||
|
||||
|
||||
#fm-pull-refresh {
|
||||
position: fixed;
|
||||
top: calc(var(--fm-safe-top) + 4px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-60px);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(20, 20, 20, 0.92);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 99998;
|
||||
transition: transform 200ms ease-out;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||
pointer-events: none;
|
||||
}
|
||||
#fm-pull-refresh.fm-active { transform: translateX(-50%) translateY(10px); }
|
||||
#fm-pull-refresh svg { color: #fff; }
|
||||
#fm-pull-refresh.fm-loading svg { animation: fmSpin 800ms linear infinite; }
|
||||
@keyframes fmSpin { to { transform: rotate(360deg); } }
|
||||
|
||||
@keyframes fmToastIn {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(20px); }
|
||||
@@ -670,6 +693,26 @@ if (window.AndroidLocalFiles) {
|
||||
window.history.back();
|
||||
});
|
||||
|
||||
// Scroll to top on navigation (scroll .main-content, NOT window —
|
||||
// window.scrollTo would push content under the notch/safe-area).
|
||||
const origPushState = history.pushState.bind(history);
|
||||
history.pushState = function () {
|
||||
origPushState.apply(this, arguments);
|
||||
updateBackButton();
|
||||
const mc = document.querySelector('.main-content');
|
||||
if (mc) mc.scrollTop = 0;
|
||||
};
|
||||
const origReplaceState = history.replaceState.bind(history);
|
||||
history.replaceState = function () {
|
||||
origReplaceState.apply(this, arguments);
|
||||
updateBackButton();
|
||||
};
|
||||
window.addEventListener('popstate', () => {
|
||||
updateBackButton();
|
||||
const mc = document.querySelector('.main-content');
|
||||
if (mc) mc.scrollTop = 0;
|
||||
});
|
||||
|
||||
updateBackButton();
|
||||
} // end if (hamburger)
|
||||
|
||||
@@ -680,93 +723,8 @@ if (window.AndroidLocalFiles) {
|
||||
// The upstream debounce (700ms) + limit=100 + min-3-char patch handles
|
||||
// the search UX entirely; no wrapper-side JS needed.
|
||||
|
||||
// (search hooks block starts here — will be removed)
|
||||
// ── pull-to-refresh ──
|
||||
const pullEl = document.createElement('div');
|
||||
pullEl.id = 'fm-pull-refresh';
|
||||
pullEl.appendChild(
|
||||
_mkSvg(
|
||||
{
|
||||
width: '22',
|
||||
height: '22',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '2.2',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
},
|
||||
['M21 12a9 9 0 1 1-3-6.7L21 8', 'M21 3v5h-5'],
|
||||
),
|
||||
);
|
||||
document.body.appendChild(pullEl);
|
||||
|
||||
let _pullStartY = 0;
|
||||
let _isPulling = false;
|
||||
let _pullActive = false;
|
||||
document.addEventListener(
|
||||
'touchstart',
|
||||
(e) => {
|
||||
const mc = document.querySelector('.main-content');
|
||||
if (!mc || mc.scrollTop > 2) return;
|
||||
if (e.target.closest('.side-panel, #queue-modal, #fm-search-preview, #search-history, .modal')) return;
|
||||
_pullStartY = e.touches[0].clientY;
|
||||
_isPulling = true;
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
document.addEventListener(
|
||||
'touchmove',
|
||||
(e) => {
|
||||
if (!_isPulling) return;
|
||||
const dy = e.touches[0].clientY - _pullStartY;
|
||||
if (dy > 70) {
|
||||
if (!_pullActive) {
|
||||
_pullActive = true;
|
||||
pullEl.classList.add('fm-active');
|
||||
}
|
||||
} else if (dy < 40 && _pullActive) {
|
||||
_pullActive = false;
|
||||
pullEl.classList.remove('fm-active');
|
||||
}
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
document.addEventListener(
|
||||
'touchend',
|
||||
() => {
|
||||
if (!_isPulling) return;
|
||||
_isPulling = false;
|
||||
if (_pullActive) {
|
||||
pullEl.classList.add('fm-loading');
|
||||
try {
|
||||
const path = window.location.pathname;
|
||||
if (typeof window.ui?.renderLibraryPage === 'function' && path.startsWith('/library')) {
|
||||
window.ui.renderLibraryPage();
|
||||
} else {
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
setTimeout(() => {
|
||||
pullEl.classList.remove('fm-active');
|
||||
pullEl.classList.remove('fm-loading');
|
||||
_pullActive = false;
|
||||
}, 900);
|
||||
}
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
document.addEventListener(
|
||||
'touchcancel',
|
||||
() => {
|
||||
_isPulling = false;
|
||||
_pullActive = false;
|
||||
pullEl.classList.remove('fm-active');
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
// ── pull-to-refresh REMOVED ──
|
||||
// Was triggering false reloads when scrolling up on album/artist pages.
|
||||
|
||||
// ── MEDIA COMMANDS FROM NOTIFICATION ──
|
||||
AudioService.addListener('mediaCommand', (data) => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,6 +41,9 @@ public class MainActivity extends BridgeActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// ── #32: WebView hardening (cache, DOM storage, text zoom, media playback) ──
|
||||
// Enable Chrome remote debugging (chrome://inspect on desktop)
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
|
||||
try {
|
||||
WebView webView = getBridge().getWebView();
|
||||
WebSettings settings = webView.getSettings();
|
||||
@@ -65,6 +68,12 @@ public class MainActivity extends BridgeActivity {
|
||||
Log.w(TAG, "WebView hardening skipped", e);
|
||||
}
|
||||
|
||||
// ── Tidal CDN Origin fix ──
|
||||
// Use custom WebViewClient that proxies Tidal audio requests with
|
||||
// Origin: https://listen.tidal.com (same as the Chrome extension).
|
||||
// Extends BridgeWebViewClient so Capacitor's bridge still works.
|
||||
getBridge().setWebViewClient(new TidalWebViewClient(getBridge()));
|
||||
|
||||
// Register bridges
|
||||
getBridge().getWebView().addJavascriptInterface(
|
||||
new DownloadBridge(this), "AndroidDownload");
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.monochrome.app;
|
||||
|
||||
import android.util.Log;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import com.getcapacitor.Bridge;
|
||||
import com.getcapacitor.BridgeWebViewClient;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Extends Capacitor's BridgeWebViewClient to intercept audio requests to
|
||||
* Tidal CDN and re-send them with Origin: https://listen.tidal.com.
|
||||
*
|
||||
* Without this, the CDN returns 403 because the WebView sends
|
||||
* Origin: https://localhost (Capacitor's local server).
|
||||
*
|
||||
* This is the native equivalent of the Chrome extension
|
||||
* "Monochrome Tidal Bypass".
|
||||
*/
|
||||
public class TidalWebViewClient extends BridgeWebViewClient {
|
||||
|
||||
private static final String TAG = "TidalWebViewClient";
|
||||
|
||||
public TidalWebViewClient(Bridge bridge) {
|
||||
super(bridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||
String url = request.getUrl().toString();
|
||||
|
||||
if (isTidalAudioUrl(url)) {
|
||||
try {
|
||||
return proxyWithTidalOrigin(url, request);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Tidal CDN proxy failed, falling back: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else: delegate to Capacitor's bridge client
|
||||
return super.shouldInterceptRequest(view, request);
|
||||
}
|
||||
|
||||
private boolean isTidalAudioUrl(String url) {
|
||||
return url.contains(".audio.tidal.com/")
|
||||
|| url.contains(".tidal.com/mediatracks/");
|
||||
}
|
||||
|
||||
private WebResourceResponse proxyWithTidalOrigin(String url, WebResourceRequest request)
|
||||
throws Exception {
|
||||
URL audioUrl = new URL(url);
|
||||
HttpURLConnection conn = (HttpURLConnection) audioUrl.openConnection();
|
||||
conn.setRequestMethod(request.getMethod());
|
||||
|
||||
// The key fix: set Origin that Tidal CDN expects
|
||||
conn.setRequestProperty("Origin", "https://listen.tidal.com");
|
||||
conn.setRequestProperty("Referer", "https://listen.tidal.com/");
|
||||
|
||||
// Copy headers from original request (except Origin/Referer)
|
||||
for (Map.Entry<String, String> header : request.getRequestHeaders().entrySet()) {
|
||||
String key = header.getKey();
|
||||
if (!key.equalsIgnoreCase("Origin")
|
||||
&& !key.equalsIgnoreCase("Referer")) {
|
||||
conn.setRequestProperty(key, header.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(60000);
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
conn.connect();
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
String responseMessage = conn.getResponseMessage();
|
||||
if (responseMessage == null) responseMessage = "OK";
|
||||
|
||||
String contentType = conn.getContentType();
|
||||
String mimeType = (contentType != null)
|
||||
? contentType.split(";")[0].trim()
|
||||
: "audio/flac";
|
||||
|
||||
Map<String, String> responseHeaders = new HashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry : conn.getHeaderFields().entrySet()) {
|
||||
if (entry.getKey() != null && entry.getValue() != null && !entry.getValue().isEmpty()) {
|
||||
responseHeaders.put(entry.getKey(), entry.getValue().get(0));
|
||||
}
|
||||
}
|
||||
// Allow the WebView to read the response
|
||||
responseHeaders.put("Access-Control-Allow-Origin", "*");
|
||||
|
||||
InputStream stream;
|
||||
if (responseCode >= 400) {
|
||||
stream = conn.getErrorStream();
|
||||
if (stream == null) stream = conn.getInputStream();
|
||||
} else {
|
||||
stream = conn.getInputStream();
|
||||
}
|
||||
|
||||
return new WebResourceResponse(mimeType, "identity",
|
||||
responseCode, responseMessage, responseHeaders, stream);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,161 @@
|
||||
// fm-logger.js — Console logger to file (loaded FIRST, before any other scripts)
|
||||
// Saves all console output to Downloads/FabiodalezMusic/fm-console-log.txt
|
||||
|
||||
// Tell upstream we handle Tidal Origin natively — skip audio-proxy.binimum.org.
|
||||
|
||||
// Block Service Worker registration IMMEDIATELY (before any other script runs).
|
||||
// The upstream VitePWA plugin registers a SW that uses CacheFirst for audio,
|
||||
// which breaks Tidal streaming due to CORS. Must be blocked here (earliest
|
||||
// possible point) to prevent the race condition on fresh installs.
|
||||
if (navigator.serviceWorker) {
|
||||
navigator.serviceWorker.register = function () {
|
||||
return Promise.resolve({ unregister: function () { return Promise.resolve(true); } });
|
||||
};
|
||||
navigator.serviceWorker.getRegistrations().then(function (regs) {
|
||||
regs.forEach(function (r) { r.unregister(); });
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
(function () {
|
||||
var _logBuffer = [];
|
||||
var _MAX = 3000;
|
||||
var _origLog = console.log;
|
||||
var _origWarn = console.warn;
|
||||
var _origError = console.error;
|
||||
var _origInfo = console.info;
|
||||
var _origDebug = console.debug;
|
||||
var _hasErrors = false;
|
||||
|
||||
var _ts = function () {
|
||||
return new Date().toISOString().slice(11, 23);
|
||||
};
|
||||
|
||||
var _capture = function (level, args) {
|
||||
try {
|
||||
var parts = [];
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
try {
|
||||
if (typeof args[i] === 'object' && args[i] !== null) {
|
||||
if (args[i] instanceof Error) {
|
||||
parts.push(args[i].message + (args[i].stack ? '\n' + args[i].stack : ''));
|
||||
} else {
|
||||
parts.push(JSON.stringify(args[i], null, 0));
|
||||
}
|
||||
} else {
|
||||
parts.push(String(args[i]));
|
||||
}
|
||||
} catch (e) {
|
||||
parts.push('[unserializable]');
|
||||
}
|
||||
}
|
||||
var msg = _ts() + ' [' + level + '] ' + parts.join(' ');
|
||||
if (_logBuffer.length >= _MAX) _logBuffer.shift();
|
||||
_logBuffer.push(msg);
|
||||
if (level === 'ERR' || level === 'UNCAUGHT' || level === 'UNHANDLED') {
|
||||
_hasErrors = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// logger itself must never throw
|
||||
}
|
||||
};
|
||||
|
||||
console.log = function () {
|
||||
_capture('LOG', arguments);
|
||||
return _origLog.apply(console, arguments);
|
||||
};
|
||||
console.warn = function () {
|
||||
_capture('WARN', arguments);
|
||||
return _origWarn.apply(console, arguments);
|
||||
};
|
||||
console.error = function () {
|
||||
_capture('ERR', arguments);
|
||||
return _origError.apply(console, arguments);
|
||||
};
|
||||
console.info = function () {
|
||||
_capture('INFO', arguments);
|
||||
return _origInfo.apply(console, arguments);
|
||||
};
|
||||
console.debug = function () {
|
||||
_capture('DBG', arguments);
|
||||
return _origDebug.apply(console, arguments);
|
||||
};
|
||||
|
||||
window.addEventListener('error', function (e) {
|
||||
_capture('UNCAUGHT', [
|
||||
e.message || 'unknown error',
|
||||
'at ' + (e.filename || '?') + ':' + (e.lineno || '?') + ':' + (e.colno || '?'),
|
||||
]);
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', function (e) {
|
||||
var reason = e.reason;
|
||||
if (reason instanceof Error) {
|
||||
_capture('UNHANDLED', [reason.message, reason.stack || '']);
|
||||
} else {
|
||||
_capture('UNHANDLED', [reason]);
|
||||
}
|
||||
});
|
||||
|
||||
// Intercept fetch errors globally
|
||||
var _origFetch = window.fetch;
|
||||
if (_origFetch) {
|
||||
window.fetch = function () {
|
||||
var url = arguments[0];
|
||||
if (typeof url === 'object' && url.url) url = url.url;
|
||||
return _origFetch.apply(this, arguments).then(
|
||||
function (resp) {
|
||||
if (!resp.ok && resp.status >= 400) {
|
||||
_capture('FETCH', [resp.status + ' ' + resp.statusText + ' ' + url]);
|
||||
}
|
||||
return resp;
|
||||
},
|
||||
function (err) {
|
||||
_capture('FETCH_ERR', [String(url).substring(0, 200) + ' → ' + (err.message || err)]);
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Save function
|
||||
var _saveLog = function () {
|
||||
if (!_logBuffer.length) return;
|
||||
// Wait for AndroidDownload bridge to be available
|
||||
if (!window.AndroidDownload) {
|
||||
// Retry in 2s
|
||||
setTimeout(_saveLog, 2000);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var text =
|
||||
'=== Fabiodalez Music Console Log ===\n' +
|
||||
'Saved: ' + new Date().toISOString() + '\n' +
|
||||
'URL: ' + location.href + '\n' +
|
||||
'UA: ' + navigator.userAgent + '\n' +
|
||||
'Entries: ' + _logBuffer.length + '\n' +
|
||||
'Errors: ' + (_hasErrors ? 'YES' : 'none') + '\n' +
|
||||
'===================================\n\n' +
|
||||
_logBuffer.join('\n');
|
||||
var base64 = btoa(unescape(encodeURIComponent(text)));
|
||||
window.AndroidDownload.saveBase64(base64, 'fm-console-log.txt', 'text/plain');
|
||||
} catch (err) {
|
||||
_origError.call(console, '[fm-logger] save failed:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Save on visibility change (app minimized)
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'hidden') _saveLog();
|
||||
});
|
||||
|
||||
// Save every 30 seconds
|
||||
setInterval(_saveLog, 30000);
|
||||
|
||||
// Expose manual save
|
||||
window._fmSaveLog = _saveLog;
|
||||
window._fmGetLog = function () {
|
||||
return _logBuffer.join('\n');
|
||||
};
|
||||
|
||||
_capture('INFO', ['fm-logger v2 initialized (early-load, fetch interceptor, max ' + _MAX + ')']);
|
||||
})();
|
||||
+152
-69
@@ -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,26 +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 \
|
||||
js/app.js \
|
||||
js/api.js \
|
||||
js/cache.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 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
|
||||
|
||||
@@ -56,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 ──
|
||||
@@ -118,11 +147,13 @@ echo " ✓ @capgo/capacitor-media-session shimmed (no-op → our AudioForegroun
|
||||
echo ""
|
||||
echo "▶ Patching for Android build..."
|
||||
|
||||
# 3a. Add script tag
|
||||
# 3a. Add script tags
|
||||
# Logger FIRST (synchronous, non-module) in <head> — captures ALL console output from start
|
||||
sed -i '' 's|</head>|<script src="./js/fm-logger.js"></script></head>|' index.html
|
||||
# Service JS LAST (module) in <body> — loads after DOM
|
||||
sed -i '' 's|</body>|<script type="module" src="./js/android-service.js"></script></body>|' index.html
|
||||
|
||||
# 3b. (upstream already has viewport-fit=cover — previous pinch-zoom-disable
|
||||
# sed was a noop and removed; rely on meta viewport as upstream ships it.)
|
||||
# 3b. (workbox audio/video CacheFirst → NetworkOnly is patched via Python below)
|
||||
|
||||
# 3c. Brand: "Monochrome" → "Fabiodalez" in sidebar logo
|
||||
sed -i '' 's|<span>Monochrome</span>|<span>Fabiodalez</span>|' index.html
|
||||
@@ -162,56 +193,78 @@ def patch(path, before, after, label):
|
||||
print(" + " + label)
|
||||
return True
|
||||
|
||||
# ── #1: search debounce 3000 → 700 ms ──
|
||||
# 3000ms was absurdly slow; 500ms felt too twitchy (navigated for partial
|
||||
# queries typed 1 letter at a time). 700ms is the sweet spot between
|
||||
# "instantly after pasting" and "give me a beat to finish typing".
|
||||
# ── #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/app.js",
|
||||
""" const debouncedSearch = debounce((query) => {
|
||||
if (query && query === searchInput.value.trim()) {
|
||||
performSearch(query);
|
||||
}
|
||||
}, 3000);""",
|
||||
""" const debouncedSearch = debounce((query) => {
|
||||
if (query && query === searchInput.value.trim()) {
|
||||
performSearch(query);
|
||||
}
|
||||
}, 700);""",
|
||||
"app.js: debounce 3000ms -> 700ms",
|
||||
"}, 3000);",
|
||||
"}, 800);",
|
||||
"app.js: search debounce 3000ms -> 800ms",
|
||||
)
|
||||
|
||||
# ── #2: auto-navigate with min 3 chars ──
|
||||
# Previously #2 disabled auto-navigate entirely, forcing users to hit Enter.
|
||||
# Restored, but gated on queries >= 3 chars so the search page doesn't render
|
||||
# for "r"/"ra" and flash an empty result state.
|
||||
# The limit=100 and cache normalization patches below still apply.
|
||||
|
||||
# ── #52: Enrich albums missing artist/cover from track data ──
|
||||
# The v2 API often returns albums without artist or cover in search results,
|
||||
# but the tracks in the same response DO have this data. Copy it over.
|
||||
patch(
|
||||
"js/app.js",
|
||||
""" searchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value.trim();
|
||||
if (!query) return;
|
||||
"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());
|
||||
}
|
||||
|
||||
if (handleExternalLink(query)) {
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedSearch(query);
|
||||
});""",
|
||||
""" searchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value.trim();
|
||||
if (!query) return;
|
||||
|
||||
if (handleExternalLink(query)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only auto-navigate for typed-out queries. Short prefixes produce
|
||||
// empty backend results and flash a confusing "no artists found".
|
||||
if (query.length >= 3) {
|
||||
debouncedSearch(query);
|
||||
}
|
||||
});""",
|
||||
"app.js: auto-navigate with min 3 chars",
|
||||
// Enrich albums that have no artist or cover from track data
|
||||
// (v2 API search often omits these for album entries, but tracks have them)
|
||||
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",
|
||||
)
|
||||
|
||||
# ── #3 + #9 + #10: cache.js — TTL per type, query normalization ──
|
||||
@@ -350,14 +403,44 @@ try:
|
||||
except Exception as e:
|
||||
print(" ! api.js: searchArtists patch failed: " + str(e))
|
||||
|
||||
# ── Workbox: CacheFirst → NetworkOnly for audio/video ──
|
||||
# Tidal CDN streams don't serve CORS headers → CacheFirst fails with
|
||||
# "no-response" in the service worker → audio won't play.
|
||||
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",
|
||||
)
|
||||
|
||||
# ── #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
|
||||
|
||||
echo " ✓ JS upstream optimizations applied (debounce, cache, limits)."
|
||||
|
||||
# ── 4. Copy android-service.js from android/ storage ──
|
||||
# ── 4. Copy wrapper JS files from android/ storage ──
|
||||
cp "$PROJECT_DIR/android/android-service.js" js/android-service.js
|
||||
echo " ✓ android-service.js copied."
|
||||
# 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 ──
|
||||
if [ ! -d "$PROJECT_DIR/android" ]; then
|
||||
|
||||
Executable
+386
@@ -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
|
||||
+6
-1
@@ -4,9 +4,14 @@ const config: CapacitorConfig = {
|
||||
appId: 'com.monochrome.app',
|
||||
appName: 'Fabiodalez Music',
|
||||
webDir: 'dist',
|
||||
// Use monochrome.tf as the WebView origin so that Tidal API proxies
|
||||
// and CDN accept our requests (they reject Origin: https://localhost).
|
||||
server: {
|
||||
hostname: 'monochrome.tf',
|
||||
androidScheme: 'https',
|
||||
},
|
||||
android: {
|
||||
adjustMarginsForEdgeToEdge: 'auto',
|
||||
// Keep WebView HTTP connections alive between fetches (reduces TCP handshake churn).
|
||||
allowMixedContent: false,
|
||||
},
|
||||
plugins: {
|
||||
|
||||
+2
-1
@@ -31,9 +31,10 @@ cp "$SCRIPT_DIR/build-android.sh" "$TARGET/"
|
||||
cp "$SCRIPT_DIR/capacitor.config.ts" "$TARGET/"
|
||||
chmod +x "$TARGET/build-android.sh"
|
||||
|
||||
# Copy android-service.js
|
||||
# Copy wrapper JS files
|
||||
mkdir -p "$TARGET/android"
|
||||
cp "$SCRIPT_DIR/android/android-service.js" "$TARGET/android/"
|
||||
cp "$SCRIPT_DIR/android/fm-logger.js" "$TARGET/android/"
|
||||
|
||||
# Copy Java sources
|
||||
JAVA_DEST="$TARGET/android/app/src/main/java/com/monochrome/app"
|
||||
|
||||
Reference in new issue
Block a user