diff --git a/js/app.js b/js/app.js
index fa727ae..0494a62 100644
--- a/js/app.js
+++ b/js/app.js
@@ -389,8 +389,8 @@ document.addEventListener('DOMContentLoaded', async () => {
initAnalytics();
new ThemeStore();
+ await MusicAPI.initialize(apiSettings);
- const api = new MusicAPI(apiSettings);
const audioPlayer = document.getElementById('audio-player');
// i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only playback
@@ -418,7 +418,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}
const currentQuality = localStorage.getItem('playback-quality') || 'HI_RES_LOSSLESS';
- await Player.initialize(audioPlayer, api, currentQuality);
+ await Player.initialize(audioPlayer, MusicAPI.instance, currentQuality);
// Initialize tracker
initTracker();
@@ -497,7 +497,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const castBtn = document.getElementById('cast-btn');
initializeCasting(audioPlayer, castBtn);
- const ui = new UIRenderer(api, Player.instance);
+ const ui = new UIRenderer(MusicAPI.instance, Player.instance);
window.monochromeUi = ui;
/**
@@ -654,7 +654,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const scrobbler = new MultiScrobbler();
window.monochromeScrobbler = scrobbler;
- const lyricsManager = new LyricsManager(api);
+ const lyricsManager = new LyricsManager(MusicAPI.instance);
ui.lyricsManager = lyricsManager;
managers.lyricsManager = lyricsManager;
@@ -693,7 +693,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// Load settings module and initialize
const { initializeSettings } = await loadSettingsModule();
- await initializeSettings(scrobbler, Player.instance, api, ui);
+ await initializeSettings(scrobbler, Player.instance, MusicAPI.instance, ui);
// Track sidebar navigation clicks
document.querySelectorAll('.sidebar-nav a').forEach((link) => {
@@ -709,14 +709,14 @@ document.addEventListener('DOMContentLoaded', async () => {
initializePlayerEvents(Player.instance, audioPlayer, scrobbler, ui);
initializeTrackInteractions(
Player.instance,
- api,
+ MusicAPI.instance,
document.querySelector('.main-content'),
document.getElementById('context-menu'),
lyricsManager,
ui,
scrobbler
);
- initializeUIInteractions(Player.instance, api, ui);
+ initializeUIInteractions(Player.instance, MusicAPI.instance, ui);
initializeKeyboardShortcuts(Player.instance, audioPlayer);
// Restore UI state for the current track (like button, theme)
@@ -1073,7 +1073,7 @@ document.addEventListener('DOMContentLoaded', async () => {
'download',
Player.instance.currentTrack,
Player.instance,
- api,
+ MusicAPI.instance,
lyricsManager,
'track',
ui
@@ -1147,7 +1147,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (!albumId) return;
try {
- const { tracks } = await api.getAlbum(albumId);
+ const { tracks } = await MusicAPI.instance.getAlbum(albumId);
if (tracks && tracks.length > 0) {
// Sort tracks by disc and track number for consistent playback
const sortedTracks = [...tracks].sort((a, b) => {
@@ -1185,7 +1185,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (!albumId) return;
try {
- const { tracks } = await api.getAlbum(albumId);
+ const { tracks } = await MusicAPI.instance.getAlbum(albumId);
if (tracks && tracks.length > 0) {
const shuffledTracks = [...tracks].sort(() => Math.random() - 0.5);
Player.instance.setQueue(shuffledTracks, 0);
@@ -1215,7 +1215,7 @@ document.addEventListener('DOMContentLoaded', async () => {
btn.innerHTML = `${SVG_ANIMATE_SPIN(18)}Shuffling...`;
try {
- const artist = await api.getArtist(artistId);
+ const artist = await MusicAPI.instance.getArtist(artistId);
const allReleases = [...(artist.albums || []), ...(artist.eps || [])];
const trackSet = new Set();
const allTracks = [];
@@ -1227,7 +1227,7 @@ document.addEventListener('DOMContentLoaded', async () => {
await Promise.all(
chunk.map(async (album) => {
try {
- const { tracks } = await api.getAlbum(album.id);
+ const { tracks } = await MusicAPI.instance.getAlbum(album.id);
tracks.forEach((track) => {
if (!trackSet.has(track.id)) {
trackSet.add(track.id);
@@ -1287,9 +1287,15 @@ document.addEventListener('DOMContentLoaded', async () => {
btn.innerHTML = `${SVG_ANIMATE_SPIN(20)}Downloading...`;
try {
- const { mix, tracks } = await api.getMix(mixId);
+ const { mix, tracks } = await MusicAPI.instance.getMix(mixId);
const { downloadPlaylistAsZip } = await loadDownloadsModule();
- await downloadPlaylistAsZip(mix, tracks, api, downloadQualitySettings.getQuality(), lyricsManager);
+ await downloadPlaylistAsZip(
+ mix,
+ tracks,
+ MusicAPI.instance,
+ downloadQualitySettings.getQuality(),
+ lyricsManager
+ );
} catch (error) {
console.error('Mix download failed:', error);
alert('Failed to download mix: ' + error.message);
@@ -1326,13 +1332,19 @@ document.addEventListener('DOMContentLoaded', async () => {
playlist = { ...userPlaylist, title: userPlaylist.name || userPlaylist.title };
tracks = userPlaylist.tracks || [];
} else {
- const data = await api.getPlaylist(playlistId);
+ const data = await MusicAPI.instance.getPlaylist(playlistId);
playlist = data.playlist;
tracks = data.tracks;
}
const { downloadPlaylistAsZip } = await loadDownloadsModule();
- await downloadPlaylistAsZip(playlist, tracks, api, downloadQualitySettings.getQuality(), lyricsManager);
+ await downloadPlaylistAsZip(
+ playlist,
+ tracks,
+ MusicAPI.instance,
+ downloadQualitySettings.getQuality(),
+ lyricsManager
+ );
} catch (error) {
console.error('Playlist download failed:', error);
alert('Failed to download playlist: ' + error.message);
@@ -1569,7 +1581,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const result = await parseCSV(
csvText,
- api,
+ MusicAPI.instance,
(progress) => {
const percentage = totalTracks > 0 ? (progress.current / totalTracks) * 100 : 0;
progressFill.style.width = `${Math.min(percentage, 100)}%`;
@@ -1630,7 +1642,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const jspfText = await file.text();
- const result = await parseJSPF(jspfText, api, (progress) => {
+ const result = await parseJSPF(jspfText, MusicAPI.instance, (progress) => {
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
progressFill.style.width = `${Math.min(percentage, 100)}%`;
progressCurrent.textContent = progress.current.toString();
@@ -1718,7 +1730,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const result = await parseDynamicCSV(
csvText,
- api,
+ MusicAPI.instance,
(progress) => {
const percentage = totalItems > 0 ? (progress.current / totalItems) * 100 : 0;
progressFill.style.width = `${Math.min(percentage, 100)}%`;
@@ -1829,7 +1841,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const xspfText = await file.text();
- const result = await parseXSPF(xspfText, api, (progress) => {
+ const result = await parseXSPF(xspfText, MusicAPI.instance, (progress) => {
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
progressFill.style.width = `${Math.min(percentage, 100)}%`;
progressCurrent.textContent = progress.current.toString();
@@ -1888,7 +1900,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const xmlText = await file.text();
- const result = await parseXML(xmlText, api, (progress) => {
+ const result = await parseXML(xmlText, MusicAPI.instance, (progress) => {
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
progressFill.style.width = `${Math.min(percentage, 100)}%`;
progressCurrent.textContent = progress.current.toString();
@@ -1947,7 +1959,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const m3uText = await file.text();
- const result = await parseM3U(m3uText, api, (progress) => {
+ const result = await parseM3U(m3uText, MusicAPI.instance, (progress) => {
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
progressFill.style.width = `${Math.min(percentage, 100)}%`;
progressCurrent.textContent = progress.current.toString();
@@ -2194,7 +2206,7 @@ document.addEventListener('DOMContentLoaded', async () => {
} else {
// Try API, if fail, try Public Pocketbase
try {
- const { tracks: apiTracks } = await api.getPlaylist(playlistId);
+ const { tracks: apiTracks } = await MusicAPI.instance.getPlaylist(playlistId);
tracks = apiTracks;
} catch (e) {
const publicPlaylist = await syncManager.getPublicPlaylist(playlistId);
@@ -2228,9 +2240,15 @@ document.addEventListener('DOMContentLoaded', async () => {
btn.innerHTML = `${SVG_ANIMATE_SPIN(20)}Downloading...`;
try {
- const { album, tracks } = await api.getAlbum(albumId);
+ const { album, tracks } = await MusicAPI.instance.getAlbum(albumId);
const { downloadAlbumAsZip } = await loadDownloadsModule();
- await downloadAlbumAsZip(album, tracks, api, downloadQualitySettings.getQuality(), lyricsManager);
+ await downloadAlbumAsZip(
+ album,
+ tracks,
+ MusicAPI.instance,
+ downloadQualitySettings.getQuality(),
+ lyricsManager
+ );
} catch (error) {
console.error('Album download failed:', error);
alert('Failed to download album: ' + error.message);
@@ -2248,7 +2266,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (!albumId) return;
try {
- const { tracks } = await api.getAlbum(albumId);
+ const { tracks } = await MusicAPI.instance.getAlbum(albumId);
if (!tracks || tracks.length === 0) {
const { showNotification } = await loadDownloadsModule();
@@ -2351,7 +2369,7 @@ document.addEventListener('DOMContentLoaded', async () => {
btn.innerHTML = `${SVG_ANIMATE_SPIN(20)}Loading...`;
try {
- const artist = await api.getArtist(artistId);
+ const artist = await MusicAPI.instance.getArtist(artistId);
const allReleases = [...(artist.albums || []), ...(artist.eps || [])];
if (allReleases.length === 0) {
@@ -2373,7 +2391,7 @@ document.addEventListener('DOMContentLoaded', async () => {
await Promise.all(
chunk.map(async (album) => {
try {
- const { tracks } = await api.getAlbum(album.id);
+ const { tracks } = await MusicAPI.instance.getAlbum(album.id);
tracks.forEach((track) => {
if (!trackSet.has(track.id)) {
trackSet.add(track.id);
@@ -2445,7 +2463,12 @@ document.addEventListener('DOMContentLoaded', async () => {
return;
}
const { downloadLikedTracks } = await loadDownloadsModule();
- await downloadLikedTracks(likedTracks, api, downloadQualitySettings.getQuality(), lyricsManager);
+ await downloadLikedTracks(
+ likedTracks,
+ MusicAPI.instance,
+ downloadQualitySettings.getQuality(),
+ lyricsManager
+ );
} catch (error) {
console.error('Liked tracks download failed:', error);
alert('Failed to download liked tracks: ' + error.message);
@@ -2463,8 +2486,14 @@ document.addEventListener('DOMContentLoaded', async () => {
if (!artistId) return;
try {
- const artist = await api.getArtist(artistId);
- showDiscographyDownloadModal(artist, api, downloadQualitySettings.getQuality(), lyricsManager, btn);
+ const artist = await MusicAPI.instance.getArtist(artistId);
+ showDiscographyDownloadModal(
+ artist,
+ MusicAPI.instance,
+ downloadQualitySettings.getQuality(),
+ lyricsManager,
+ btn
+ );
} catch (error) {
console.error('Failed to load artist for discography download:', error);
alert('Failed to load artist: ' + error.message);
diff --git a/js/music-api.js b/js/music-api.js
index 221c2f5..870d88c 100644
--- a/js/music-api.js
+++ b/js/music-api.js
@@ -6,6 +6,15 @@ import { QobuzAPI } from './qobuz-api.js';
import { musicProviderSettings } from './storage.js';
export class MusicAPI {
+ static #instance = null;
+ static get instance() {
+ if (!MusicAPI.#instance) {
+ throw new Error('MusicAPI not initialized. Call MusicAPI.initialize(settings) first.');
+ }
+ return MusicAPI.#instance;
+ }
+
+ /** @private */
constructor(settings) {
this.tidalAPI = new LosslessAPI(settings);
this.qobuzAPI = new QobuzAPI();
@@ -13,6 +22,15 @@ export class MusicAPI {
this.videoArtworkCache = new Map();
}
+ static async initialize(settings) {
+ if (MusicAPI.#instance) {
+ throw new Error('MusicAPI is already initialized');
+ }
+
+ const api = new MusicAPI(settings);
+ return (MusicAPI.#instance = api);
+ }
+
getCurrentProvider() {
return musicProviderSettings.getProvider();
}