kv-music/js/storage.js
2025-12-31 14:31:16 -03:00

434 lines
12 KiB
JavaScript

//storage.js
export const apiSettings = {
STORAGE_KEY: 'monochrome-api-instances',
INSTANCES_URL: "../instances.json",
SPEED_TEST_CACHE_KEY: 'monochrome-instance-speeds',
SPEED_TEST_CACHE_DURATION: 1000 * 60 * 60,
defaultInstances: [],
instancesLoaded: false,
async loadInstancesFromGitHub() {
if (this.instancesLoaded) {
return this.defaultInstances;
}
try {
const response = await fetch(this.INSTANCES_URL);
if (!response.ok) throw new Error('Failed to fetch instances');
const data = await response.json();
const allInstances = [];
if (Array.isArray(data)) {
allInstances.push(...data);
} else if (data.api) {
// Legacy support or if structure changes back
for (const [provider, config] of Object.entries(data.api)) {
if (config.cors === false && Array.isArray(config.urls)) {
allInstances.push(...config.urls);
}
}
}
this.defaultInstances = allInstances;
this.instancesLoaded = true;
return allInstances;
} catch (error) {
console.error('Failed to load instances from GitHub:', error);
this.defaultInstances = [
'https://triton.squid.wtf',
'https://wolf.qqdl.site',
'https://maus.qqdl.site',
'https://vogel.qqdl.site',
'https://katze.qqdl.site',
'https://hund.qqdl.site',
'https://tidal-api.binimum.org',
'https://tidal.kinoplus.online',
];
this.instancesLoaded = true;
return this.defaultInstances;
}
},
async speedTestInstance(url) {
const testUrl = url.endsWith('/')
? `${url}track/?id=204567804&quality=HIGH`
: `${url}/track/?id=204567804&quality=HIGH`;
const startTime = performance.now();
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch(testUrl, {
signal: controller.signal,
cache: 'no-store'
});
clearTimeout(timeout);
if (!response.ok) {
return { url, speed: Infinity, error: `HTTP ${response.status}` };
}
const endTime = performance.now();
const speed = endTime - startTime;
return { url, speed, error: null };
} catch (error) {
return { url, speed: Infinity, error: error.message };
}
},
async runSpeedTests(instances) {
console.log('[SpeedTest] Testing', instances.length, 'instances...');
const results = await Promise.all(
instances.map(url => this.speedTestInstance(url))
);
const validResults = results.filter(r => r.speed !== Infinity);
const failedResults = results.filter(r => r.speed === Infinity);
if (failedResults.length > 0) {
console.log('[SpeedTest] Failed instances:', failedResults.map(r => `${r.url} (${r.error})`));
}
validResults.sort((a, b) => a.speed - b.speed);
console.log('[SpeedTest] Results:', validResults.map(r => `${r.url}: ${r.speed.toFixed(0)}ms`));
const sortedInstances = [
...validResults.map(r => r.url),
...failedResults.map(r => r.url)
];
const cacheData = {
timestamp: Date.now(),
speeds: results.reduce((acc, r) => {
acc[r.url] = { speed: r.speed, error: r.error };
return acc;
}, {})
};
try {
localStorage.setItem(this.SPEED_TEST_CACHE_KEY, JSON.stringify(cacheData));
} catch (e) {
console.warn('[SpeedTest] Failed to cache results');
}
return sortedInstances;
},
getCachedSpeedTests() {
try {
const cached = localStorage.getItem(this.SPEED_TEST_CACHE_KEY);
if (!cached) return null;
const data = JSON.parse(cached);
if (Date.now() - data.timestamp > this.SPEED_TEST_CACHE_DURATION) {
return null;
}
return data;
} catch (e) {
return null;
}
},
sortInstancesByCache(instances, cachedData) {
const speeds = cachedData.speeds;
const sorted = [...instances].sort((a, b) => {
const speedA = speeds[a]?.speed ?? Infinity;
const speedB = speeds[b]?.speed ?? Infinity;
return speedA - speedB;
});
console.log('[SpeedTest] Using cached results (age:',
Math.round((Date.now() - cachedData.timestamp) / 1000 / 60), 'minutes)');
return sorted;
},
async getInstances() {
try {
const stored = localStorage.getItem(this.STORAGE_KEY);
if (stored) {
return JSON.parse(stored);
}
const instances = await this.loadInstancesFromGitHub();
const cachedSpeedTests = this.getCachedSpeedTests();
let sortedInstances;
if (cachedSpeedTests) {
sortedInstances = this.sortInstancesByCache(instances, cachedSpeedTests);
} else {
sortedInstances = await this.runSpeedTests(instances);
}
this.saveInstances(sortedInstances);
return sortedInstances;
} catch (e) {
const instances = await this.loadInstancesFromGitHub();
return instances;
}
},
async refreshSpeedTests() {
const instances = await this.loadInstancesFromGitHub();
const sortedInstances = await this.runSpeedTests(instances);
this.saveInstances(sortedInstances);
return sortedInstances;
},
saveInstances(instances) {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(instances));
}
};
export const recentActivityManager = {
STORAGE_KEY: 'monochrome-recent-activity',
LIMIT: 10,
_get() {
try {
const data = localStorage.getItem(this.STORAGE_KEY);
const parsed = data ? JSON.parse(data) : { artists: [], albums: [], playlists: [] };
if (!parsed.playlists) parsed.playlists = [];
return parsed;
} catch (e) {
return { artists: [], albums: [], playlists: [] };
}
},
_save(data) {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data));
},
getRecents() {
return this._get();
},
_add(type, item) {
const data = this._get();
data[type] = data[type].filter(i => i.id !== item.id);
data[type].unshift(item);
data[type] = data[type].slice(0, this.LIMIT);
this._save(data);
},
addArtist(artist) {
this._add('artists', artist);
},
addAlbum(album) {
this._add('albums', album);
},
addPlaylist(playlist) {
this._add('playlists', playlist);
}
};
export const themeManager = {
STORAGE_KEY: 'monochrome-theme',
CUSTOM_THEME_KEY: 'monochrome-custom-theme',
defaultThemes: {
light: {},
dark: {},
monochrome: {},
ocean: {},
purple: {},
forest: {},
mocha: {},
machiatto: {},
frappe: {},
latte: {}
},
getTheme() {
try {
return localStorage.getItem(this.STORAGE_KEY) || 'system';
} catch (e) {
return 'system';
}
},
setTheme(theme) {
localStorage.setItem(this.STORAGE_KEY, theme);
if (theme === 'system') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
},
getCustomTheme() {
try {
const stored = localStorage.getItem(this.CUSTOM_THEME_KEY);
return stored ? JSON.parse(stored) : null;
} catch (e) {
return null;
}
},
setCustomTheme(colors) {
localStorage.setItem(this.CUSTOM_THEME_KEY, JSON.stringify(colors));
this.applyCustomTheme(colors);
},
applyCustomTheme(colors) {
const root = document.documentElement;
for (const [key, value] of Object.entries(colors)) {
root.style.setProperty(`--${key}`, value);
}
}
};
export const lastFMStorage = {
STORAGE_KEY: 'lastfm-enabled',
LOVE_ON_LIKE_KEY: 'lastfm-love-on-like',
isEnabled() {
try {
return localStorage.getItem(this.STORAGE_KEY) === 'true';
} catch (e) {
return false;
}
},
setEnabled(enabled) {
localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false');
},
shouldLoveOnLike() {
try {
return localStorage.getItem(this.LOVE_ON_LIKE_KEY) === 'true';
} catch (e) {
return false;
}
},
setLoveOnLike(enabled) {
localStorage.setItem(this.LOVE_ON_LIKE_KEY, enabled ? 'true' : 'false');
}
};
export const nowPlayingSettings = {
STORAGE_KEY: 'now-playing-mode',
getMode() {
try {
return localStorage.getItem(this.STORAGE_KEY) || 'cover';
} catch (e) {
return 'cover';
}
},
setMode(mode) {
localStorage.setItem(this.STORAGE_KEY, mode);
}
};
export const lyricsSettings = {
DOWNLOAD_WITH_TRACKS: 'lyrics-download-with-tracks',
shouldDownloadLyrics() {
try {
return localStorage.getItem(this.DOWNLOAD_WITH_TRACKS) === 'true';
} catch (e) {
return false;
}
},
setDownloadLyrics(enabled) {
localStorage.setItem(this.DOWNLOAD_WITH_TRACKS, enabled ? 'true' : 'false');
}
};
export const backgroundSettings = {
STORAGE_KEY: 'album-background-enabled',
isEnabled() {
try {
// Default to true if not set
return localStorage.getItem(this.STORAGE_KEY) !== 'false';
} catch (e) {
return true;
}
},
setEnabled(enabled) {
localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false');
}
};
export const trackListSettings = {
STORAGE_KEY: 'track-list-actions-mode',
getMode() {
try {
const mode = localStorage.getItem(this.STORAGE_KEY) || 'dropdown';
document.documentElement.setAttribute('data-track-actions-mode', mode);
return mode;
} catch (e) {
return 'dropdown';
}
},
setMode(mode) {
localStorage.setItem(this.STORAGE_KEY, mode);
document.documentElement.setAttribute('data-track-actions-mode', mode);
}
};
export const queueManager = {
STORAGE_KEY: 'monochrome-queue',
getQueue() {
try {
const data = localStorage.getItem(this.STORAGE_KEY);
return data ? JSON.parse(data) : null;
} catch (e) {
return null;
}
},
saveQueue(queueState) {
try {
// Only save essential data to avoid quota limits
const minimalState = {
queue: queueState.queue,
shuffledQueue: queueState.shuffledQueue,
originalQueueBeforeShuffle: queueState.originalQueueBeforeShuffle,
currentQueueIndex: queueState.currentQueueIndex,
shuffleActive: queueState.shuffleActive,
repeatMode: queueState.repeatMode
};
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(minimalState));
} catch (e) {
console.warn('Failed to save queue to localStorage:', e);
}
}
};
// System theme listener
if (typeof window !== 'undefined' && window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (themeManager.getTheme() === 'system') {
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
}
});
}