Migrate to Vite + ES module architecture

Split monolithic app.js (~1850 lines) into maintainable ES modules:
- src/state.js  — shared mutable state (S object)
- src/utils.js  — md5, esc, formatDur, toast
- src/api.js    — Subsonic API, Navidrome REST, iTunes API
- src/player.js — playback, transport, queue, media session, preview
- src/views.js  — navigation, page loaders, card makers, search
- src/ui.js     — all modals, overlays, sidebar, settings
- src/pwa.js    — service worker registration + update banner
- src/main.js   — app init, connect, window globals

Updated Dockerfile to multi-stage (Node build → nginx serve).
Static assets (sw.js, manifest.json, icon.svg) moved to public/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-04-26 13:01:34 +02:00
1 parent ce53c48835
commit 9aded56713
18 files changed
+3134 -2

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
import { S } from './state.js';
import { md5 } from './utils.js';
// ─── SUBSONIC ───
export function apiUrl(method, extra='') {
const salt = Math.random().toString(36).slice(2,10);
const token = md5(S.cfg.pass + salt);
return `${S.cfg.url}/rest/${method}?u=${encodeURIComponent(S.cfg.user)}&t=${token}&s=${salt}&v=1.16.1&c=NovaSonic&f=json${extra}`;
}
export async function api(method, extra='') {
const res = await fetch(apiUrl(method, extra));
if(!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const r = data['subsonic-response'];
if(r.status !== 'ok') throw new Error(r.error?.message || 'API Error');
return r;
}
export function streamUrl(id) { return apiUrl('stream', `&id=${id}&format=raw`); }
export function downloadUrl(id) { return apiUrl('download', `&id=${id}`); }
export function coverUrl(id, size=300) { return id ? apiUrl('getCoverArt', `&id=${id}&size=${size}`) : null; }
// ─── NAVIDROME REST AUTH ───
export async function ndAuth() {
if(S._ndToken) return S._ndToken;
const res = await fetch(`${S.cfg.url}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: S.cfg.user, password: S.cfg.pass }),
});
if(!res.ok) throw new Error('Navidrome login fehlgeschlagen');
const d = await res.json();
S._ndToken = d.token;
return S._ndToken;
}
// ─── ITUNES ───
const ITUNES = 'https://itunes.apple.com';
export async function itunesSearch(query, entity='musicTrack', limit=25) {
const r = await fetch(`${ITUNES}/search?term=${encodeURIComponent(query)}&media=music&entity=${entity}&limit=${limit}&country=de`);
const d = await r.json();
return d.results || [];
}
export async function itunesLookupAlbum(collectionId) {
const r = await fetch(`${ITUNES}/lookup?id=${collectionId}&entity=song`);
const d = await r.json();
return (d.results || []).filter(x => x.wrapperType === 'track');
}
export async function itunesTopCharts() {
const urls = [
'https://rss.applemarketingtools.com/api/v2/de/music/most-played/50/albums.json',
'https://rss.applemarketingtools.com/api/v2/us/music/most-played/50/albums.json',
];
for(const url of urls) {
try {
const r = await fetch(url, { signal: AbortSignal.timeout(6000) });
if(!r.ok) continue;
const d = await r.json();
if(d.feed?.results?.length) return d.feed.results;
} catch {}
}
try {
const r = await fetch('https://itunes.apple.com/search?term=new+music&media=music&entity=album&limit=50&country=de');
const d = await r.json();
return (d.results||[]).map(a => ({
name: a.collectionName, artistName: a.artistName,
artworkUrl: a.artworkUrl100?.replace('100x100','300x300'), artworkUrl100: a.artworkUrl100,
}));
} catch { return []; }
}
export async function itunesTopSongs() {
const urls = [
'https://rss.applemarketingtools.com/api/v2/de/music/most-played/50/songs.json',
'https://rss.applemarketingtools.com/api/v2/us/music/most-played/50/songs.json',
];
for(const url of urls) {
try {
const r = await fetch(url, { signal: AbortSignal.timeout(6000) });
if(!r.ok) continue;
const d = await r.json();
if(d.feed?.results?.length) return d.feed.results;
} catch {}
}
try {
const r = await fetch('https://itunes.apple.com/search?term=top+hits&media=music&entity=musicTrack&limit=50&country=de');
const d = await r.json();
return (d.results||[]).map(t => ({ name: t.trackName, artistName: t.artistName, artworkUrl: t.artworkUrl100 }));
} catch { return []; }
}