Ergaenzt Backend und Scrobble um die Navidrome-Adresse: alle drei kommen als buildConfigField aus dem Build und greifen, wenn im Geraetespeicher nichts steht. Der Anmeldebildschirm ist damit vorbelegt — beim ersten Start bleiben nur noch Benutzer und Passwort. Die Felder in den Einstellungen bleiben sichtbar und ueberschreibbar, ein Umzug ist also ohne neues APK zu korrigieren. Zugangsdaten bleiben aussen vor. Das Publish-Skript reicht die Werte aus scripts/.env.release durch, damit kuenftige Builds sie nicht verlieren. Am erzeugten BuildConfig geprueft, alle vier Werte stehen darin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
152 lines
8.3 KiB
JavaScript
152 lines
8.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Build the NovaSonic Android APK and upload it to the fixed Gitea release ("apk-latest"), replacing
|
|
// the previous .apk asset — so the web "Android App" download button always serves the latest build.
|
|
//
|
|
// One command after your changes: npm run publish-apk
|
|
//
|
|
// Config (env, or a gitignored scripts/.env.release file with KEY=VALUE lines):
|
|
// GITEA_URL internal/LAN Gitea base reachable from THIS machine, e.g. http://192.168.1.10:3000
|
|
// GITEA_REPO owner/repo, e.g. Docker/NovaSonic
|
|
// GITEA_TOKEN Gitea token with write access to the repo (repo/releases)
|
|
// GITEA_APK_TAG fixed release tag (default: apk-latest)
|
|
// APK_GRADLE_TASK gradle task (default: assembleDebug — installable; release is unsigned here)
|
|
// APK_FILE override the built APK path if you use a custom task
|
|
//
|
|
// The debug build is used by default because the release build type has no signingConfig yet, so a
|
|
// release APK would be unsigned and refuse to install. Same dev machine ⇒ stable debug key ⇒ updates
|
|
// install over each other. Add a keystore + signingConfig later to switch APK_GRADLE_TASK=assembleRelease.
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join, resolve } from 'node:path';
|
|
|
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
|
// ── tiny .env.release loader (never committed) ───────────────────────────────
|
|
const envFile = join(root, 'scripts', '.env.release');
|
|
if (existsSync(envFile)) {
|
|
for (const line of readFileSync(envFile, 'utf8').split('\n')) {
|
|
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)\s*$/i);
|
|
if (m && !(m[1] in process.env)) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
}
|
|
}
|
|
|
|
const GITEA_URL = (process.env.GITEA_URL || '').replace(/\/$/, '');
|
|
const GITEA_REPO = process.env.GITEA_REPO || '';
|
|
const GITEA_TOKEN = process.env.GITEA_TOKEN || '';
|
|
const TAG = process.env.GITEA_APK_TAG || 'apk-latest';
|
|
const TASK = process.env.APK_GRADLE_TASK || 'assembleDebug';
|
|
// Host fuer die App-Links geteilter Songs (https://<host>/?song=…). Steht fest im Manifest, muss
|
|
// also beim BAUEN mitgegeben werden — ohne ihn traegt das APK den Platzhalter aus build.gradle.kts
|
|
// und eingehende Links landen im Browser statt in der App. NOVASONIC_HOST2 optional fuer einen
|
|
// Domainwechsel (beide Adressen oeffnen dann in der App), siehe docs/share-links.md.
|
|
const HOST = process.env.NOVASONIC_HOST || '';
|
|
const HOST2 = process.env.NOVASONIC_HOST2 || '';
|
|
// Vorgabe-Adressen, die ins APK gebacken werden, damit auf einem frischen Geraet nur noch Benutzer
|
|
// und Passwort einzutippen sind. Zugangsdaten kommen hier bewusst NICHT vor.
|
|
const ND_URL = process.env.NOVASONIC_NAVIDROME || '';
|
|
const BACKEND_URL = process.env.NOVASONIC_BACKEND || '';
|
|
const SCROBBLE_URL= process.env.NOVASONIC_SCROBBLE || '';
|
|
|
|
function die(msg) { console.error(`\n✖ ${msg}`); process.exit(1); }
|
|
if (!GITEA_URL || !GITEA_REPO || !GITEA_TOKEN)
|
|
die('GITEA_URL, GITEA_REPO und GITEA_TOKEN müssen gesetzt sein (env oder scripts/.env.release).');
|
|
|
|
const API = `${GITEA_URL}/api/v1/repos/${GITEA_REPO}`;
|
|
const authHeaders = { Authorization: `token ${GITEA_TOKEN}` };
|
|
|
|
// ── bump versionCode + read versionName from the Gradle config ───────────────
|
|
// The in-app updater compares versionCode, so every published build MUST increment it — we do that
|
|
// here automatically (pass --version X.Y to also set the versionName). Commit the changed
|
|
// build.gradle.kts afterwards so the repo reflects what's live.
|
|
const gradlePath = join(root, 'android', 'app', 'build.gradle.kts');
|
|
let gradle = readFileSync(gradlePath, 'utf8');
|
|
const curCode = parseInt(gradle.match(/versionCode\s*=\s*(\d+)/)?.[1] || '0', 10);
|
|
const versionCode = curCode + 1;
|
|
const versionArg = process.argv.slice(2).find(a => a.startsWith('--version='))?.split('=')[1]
|
|
|| (process.argv.includes('--version') ? process.argv[process.argv.indexOf('--version') + 1] : null);
|
|
let version = versionArg || gradle.match(/versionName\s*=\s*"([^"]+)"/)?.[1] || `0.${versionCode}`;
|
|
gradle = gradle
|
|
.replace(/versionCode\s*=\s*\d+/, `versionCode = ${versionCode}`)
|
|
.replace(/versionName\s*=\s*"[^"]*"/, `versionName = "${version}"`);
|
|
writeFileSync(gradlePath, gradle);
|
|
console.log(`▶ versionCode → ${versionCode}, versionName → ${version} (build.gradle.kts danach committen!)`);
|
|
|
|
const relBody = `versionCode=${versionCode}\nversionName=${version}`;
|
|
|
|
// ── 1) build ─────────────────────────────────────────────────────────────────
|
|
const isWin = process.platform === 'win32';
|
|
const gradlew = join(root, 'android', isWin ? 'gradlew.bat' : 'gradlew');
|
|
console.log(`▶ Baue APK: ${TASK} …`);
|
|
if (!HOST) {
|
|
console.warn('⚠ NOVASONIC_HOST ist nicht gesetzt — das APK bekommt den Platzhalter-Host aus');
|
|
console.warn(' build.gradle.kts, geteilte Links oeffnen dann NICHT in der App.');
|
|
console.warn(' In scripts/.env.release eintragen: NOVASONIC_HOST=music.deine-domain.de');
|
|
}
|
|
const gradleArgs = [`:app:${TASK}`];
|
|
if (HOST) gradleArgs.push(`-PnovasonicHost=${HOST}`);
|
|
if (HOST2) gradleArgs.push(`-PnovasonicHost2=${HOST2}`);
|
|
if (ND_URL) gradleArgs.push(`-PnovasonicNavidrome=${ND_URL}`);
|
|
if (BACKEND_URL) gradleArgs.push(`-PnovasonicBackend=${BACKEND_URL}`);
|
|
if (SCROBBLE_URL) gradleArgs.push(`-PnovasonicScrobble=${SCROBBLE_URL}`);
|
|
const build = spawnSync(gradlew, gradleArgs, {
|
|
cwd: join(root, 'android'), stdio: 'inherit', shell: isWin,
|
|
});
|
|
if (build.status !== 0) die('Gradle-Build fehlgeschlagen.');
|
|
|
|
// derive the APK path from the task (assembleDebug → debug/app-debug.apk)
|
|
const variant = TASK.replace(/^assemble/, '').toLowerCase() || 'debug';
|
|
const apkPath = process.env.APK_FILE
|
|
|| join(root, 'android', 'app', 'build', 'outputs', 'apk', variant, `app-${variant}.apk`);
|
|
if (!existsSync(apkPath)) die(`APK nicht gefunden: ${apkPath} (APK_FILE setzen, falls Task abweicht).`);
|
|
const apkBytes = readFileSync(apkPath);
|
|
console.log(`✓ Gebaut: ${apkPath} (${(apkBytes.length / 1048576).toFixed(1)} MB)`);
|
|
|
|
// ── 2) get-or-create the fixed release ───────────────────────────────────────
|
|
async function api(path, opts = {}) {
|
|
const r = await fetch(`${API}${path}`, { ...opts, headers: { ...authHeaders, ...(opts.headers || {}) } });
|
|
return r;
|
|
}
|
|
|
|
let rel;
|
|
{
|
|
const r = await api(`/releases/tags/${encodeURIComponent(TAG)}`);
|
|
if (r.status === 200) {
|
|
rel = await r.json();
|
|
} else if (r.status === 404) {
|
|
console.log(`▶ Release "${TAG}" existiert nicht — lege sie an …`);
|
|
const cr = await api('/releases', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ tag_name: TAG, target: 'main', name: `NovaSonic ${version}`, body: relBody }),
|
|
});
|
|
if (!cr.ok) die(`Release anlegen fehlgeschlagen: ${cr.status} ${await cr.text()}`);
|
|
rel = await cr.json();
|
|
} else {
|
|
die(`Release-Abfrage fehlgeschlagen: ${r.status} ${await r.text()}`);
|
|
}
|
|
}
|
|
|
|
// ── 3) delete old .apk assets, upload the new one ────────────────────────────
|
|
for (const a of rel.assets || []) {
|
|
if (/\.apk$/i.test(a.name)) {
|
|
const d = await api(`/releases/${rel.id}/assets/${a.id}`, { method: 'DELETE' });
|
|
console.log(d.ok ? `✓ Altes Asset gelöscht: ${a.name}` : `⚠ Löschen von ${a.name} fehlgeschlagen (${d.status})`);
|
|
}
|
|
}
|
|
|
|
const form = new FormData();
|
|
form.append('attachment', new Blob([apkBytes], { type: 'application/vnd.android.package-archive' }), 'NovaSonic.apk');
|
|
const up = await api(`/releases/${rel.id}/assets?name=NovaSonic.apk`, { method: 'POST', body: form });
|
|
if (!up.ok) die(`Upload fehlgeschlagen: ${up.status} ${await up.text()}`);
|
|
|
|
// keep the release title + versionCode body in sync
|
|
await api(`/releases/${rel.id}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: `NovaSonic ${version}`, body: relBody }),
|
|
}).catch(() => {});
|
|
|
|
console.log(`\n✔ NovaSonic ${version} hochgeladen → Release "${TAG}". Der Web-Button serviert jetzt diese APK.`);
|