feat(web): download the Android APK from Settings

Backend serves a staged APK so co-users self-serve the latest build:
- GET /api/apk streams APK_PATH (default /data/novasonic.apk) with the
  android package content-type + attachment disposition; 404 if absent.
- GET /api/apk/info reports availability, size, mtime, and an optional
  version from /data/novasonic.version.txt.
- Settings gets a "🤖 Android App" section that appears only when an APK is
  present and shows version/size/date.

Maintainer updates the APK with:
  docker cp app-release.apk novasonic-backend:/data/novasonic.apk

Android source is already tracked; only build artifacts stay out of git.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-08-25 18:00:36 +02:00
1 parent 7996971ba0
commit 908b50bcd9
4 files changed
+67

No files matched your search

+7
View File
@@ -71,6 +71,13 @@ like Android), concert events become **reliable across web/Android**, several An
**YouTube downloads are recovered** after they broke on two independent fronts. **YouTube downloads are recovered** after they broke on two independent fronts.
### Added ### Added
- **Android APK download in Settings (Web)** — a "🤖 Android App" section in Settings offers the
latest APK so co-users self-serve instead of the maintainer hand-delivering builds. The backend
serves the staged APK (`GET /api/apk`, metadata at `/api/apk/info`); the button appears only when an
APK is present and shows version/size/date. **Maintainer workflow:** drop the release build into the
backend's data volume — `docker cp app-release.apk novasonic-backend:/data/novasonic.apk` (optional
`docker cp version.txt novasonic-backend:/data/novasonic.version.txt`). The Android *source* is
already in the repo; only build artifacts stay out of git.
- **Unified Global Search field (Web)** — the separate Song-Importer page/sidebar entry is gone. The - **Unified Global Search field (Web)** — the separate Song-Importer page/sidebar entry is gone. The
one search field now auto-detects: a **link** (Spotify/SoundCloud/YouTube) opens the importer one search field now auto-detects: a **link** (Spotify/SoundCloud/YouTube) opens the importer
preview, plain **text** runs a Deezer search. Cleaner left nav, same behaviour as Android. preview, plain **text** runs a Deezer search. Cleaner left nav, same behaviour as Android.
+27
View File
@@ -466,6 +466,33 @@ app.get('/api/events-debug/:artist', (req, res) => {
}).on('error', e => res.json({ error: e.message })); }).on('error', e => res.json({ error: e.message }));
}); });
// ── NovaSonic Android APK distribution ──────────────────────────────────────
// The web app offers a "download the Android app" button that points here, so co-users always get
// the latest APK without the maintainer hand-delivering it. Drop the latest release build into the
// writable data volume (APK_PATH, default /data/novasonic.apk):
// docker cp app-release.apk novasonic-backend:/data/novasonic.apk
// Optional: a sidecar /data/novasonic.version.txt with a version string (e.g. "2.2") shown in the UI.
const APK_PATH = process.env.APK_PATH || path.join(CONFIG.dataPath, 'novasonic.apk');
const APK_VER_PATH = path.join(CONFIG.dataPath, 'novasonic.version.txt');
app.get('/api/apk/info', (req, res) => {
fs.stat(APK_PATH, (err, st) => {
if (err) return res.json({ available: false });
let version = null;
try { version = fs.readFileSync(APK_VER_PATH, 'utf8').trim() || null; } catch {}
res.json({ available: true, size: st.size, updatedAt: st.mtimeMs, version });
});
});
app.get('/api/apk', (req, res) => {
fs.stat(APK_PATH, (err) => {
if (err) return res.status(404).json({ error: 'Keine APK hinterlegt' });
res.setHeader('Content-Type', 'application/vnd.android.package-archive');
res.setHeader('Content-Disposition', 'attachment; filename="NovaSonic.apk"');
res.sendFile(APK_PATH);
});
});
app.get('/api/health', (req, res) => { app.get('/api/health', (req, res) => {
res.json({ res.json({
status: 'ok', ytdlpVersion, status: 'ok', ytdlpVersion,
+12
View File
@@ -605,6 +605,18 @@
<div id="s-cover-color-bg" class="toggle" onclick="this.classList.toggle('on')"></div> <div id="s-cover-color-bg" class="toggle" onclick="this.classList.toggle('on')"></div>
</div> </div>
</div> </div>
<div class="settings-section" id="s-apk-section" style="display:none">
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🤖</span>
Android App
</h3>
<div style="font-size:12px;color:var(--text2);margin-bottom:10px;line-height:1.6">Installiere die NovaSonic Android-App direkt aufs Handy. <span id="s-apk-meta" style="color:var(--text3)"></span></div>
<a id="s-apk-btn" href="#" download style="display:inline-flex;align-items:center;gap:8px;padding:10px 16px;border-radius:10px;background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#fff;font-weight:700;font-size:13px;text-decoration:none;box-shadow:0 4px 12px var(--accent-glow)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
APK herunterladen
</a>
<div style="font-size:11px;color:var(--text3);margin-top:8px">Danach im Handy „Installation aus unbekannten Quellen" für den Browser erlauben.</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px"> <h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">📊</span> <span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">📊</span>
+21
View File
@@ -54,6 +54,27 @@ export function openSettings() {
_renderThemePicker(); _renderThemePicker();
document.getElementById('settings-modal').classList.add('open'); document.getElementById('settings-modal').classList.add('open');
checkBackend(); checkBackend();
_loadApkInfo();
}
// Show the "download the Android APK" button only if the backend actually has an APK staged.
async function _loadApkInfo() {
const section = document.getElementById('s-apk-section');
const btn = document.getElementById('s-apk-btn');
const meta = document.getElementById('s-apk-meta');
if (!section || !btn) return;
section.style.display = 'none';
const base = S.cfg.backendUrl;
if (!base) return;
try {
const info = await fetch(`${base}/api/apk/info`).then(r => r.json());
if (!info?.available) return;
btn.href = `${base}/api/apk`;
const mb = info.size ? (info.size / 1048576).toFixed(1) + ' MB' : '';
const date = info.updatedAt ? new Date(info.updatedAt).toLocaleDateString('de-DE') : '';
if (meta) meta.textContent = [info.version && `v${info.version}`, mb, date && `Stand ${date}`].filter(Boolean).join(' · ');
section.style.display = '';
} catch {}
} }
function _renderThemePicker() { function _renderThemePicker() {