From c3b88da0542ee8d331b240bfb1a247f3e096e719 Mon Sep 17 00:00:00 2001 From: Samidy Date: Sun, 8 Feb 2026 22:50:41 +0300 Subject: [PATCH 1/4] feat(UI): Font Selection --- index.html | 16 ++++++++++++++++ js/app.js | 14 ++++++++++++++ js/ui.js | 2 ++ styles.css | 2 +- 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 9379a9c..5d25713 100644 --- a/index.html +++ b/index.html @@ -2087,6 +2087,22 @@ +
+
+ Font + Choose the application font +
+ +
Waveform Seekbar diff --git a/js/app.js b/js/app.js index 58e975d..ae561cb 100644 --- a/js/app.js +++ b/js/app.js @@ -1432,6 +1432,20 @@ document.addEventListener('DOMContentLoaded', async () => { showKeyboardShortcuts(); }); + // Font Settings + const fontSelect = document.getElementById('font-select'); + if (fontSelect) { + const savedFont = localStorage.getItem('monochrome-font'); + if (savedFont) { + fontSelect.value = savedFont; + } + fontSelect.addEventListener('change', (e) => { + const font = e.target.value; + document.documentElement.style.setProperty('--font-family', font); + localStorage.setItem('monochrome-font', font); + }); + } + // Listener for Pocketbase Sync updates window.addEventListener('library-changed', () => { const path = window.location.pathname; diff --git a/js/ui.js b/js/ui.js index 8f97063..3336680 100644 --- a/js/ui.js +++ b/js/ui.js @@ -42,6 +42,8 @@ import { createProjectCardHTML, createTrackFromSong, } from './tracker.js'; +const savedFont = localStorage.getItem('monochrome-font'); +if (savedFont) document.documentElement.style.setProperty('--font-family', savedFont); function sortTracks(tracks, sortType) { if (sortType === 'custom') return [...tracks]; diff --git a/styles.css b/styles.css index 4be47ee..0056733 100644 --- a/styles.css +++ b/styles.css @@ -280,7 +280,7 @@ html { body { background-color: var(--background); color: var(--foreground); - font-family: Inter, sans-serif; + font-family: var(--font-family, 'Inter', sans-serif); overflow: hidden; transition: background-color 0.3s ease, From d21680ee2c7eefcea6e651367d1e97c375551dbb Mon Sep 17 00:00:00 2001 From: SamidyFR <168582143+SamidyFR@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:51:03 +0000 Subject: [PATCH 2/4] style: auto-fix linting issues --- index.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index b2a0eef..9bcd855 100644 --- a/index.html +++ b/index.html @@ -2099,7 +2099,11 @@ - +
From 133f484e4ef19161b4ba048de604e11ec834e8d2 Mon Sep 17 00:00:00 2001 From: binimum Date: Sun, 8 Feb 2026 20:00:53 +0000 Subject: [PATCH 3/4] Urgently fix API spam issues --- index.html | 5 +- js/api.js | 98 +++++++++++++++++++----------- js/settings.js | 2 +- js/storage.js | 152 +++++----------------------------------------- js/ui.js | 12 ---- package-lock.json | 15 +++++ 6 files changed, 96 insertions(+), 188 deletions(-) diff --git a/index.html b/index.html index 9379a9c..1872de3 100644 --- a/index.html +++ b/index.html @@ -3136,12 +3136,11 @@
API Instances Manage and prioritize API instances. Automatically sorted by - speed.Manage and prioritize API instances.
diff --git a/js/api.js b/js/api.js index 23cca36..0e602cb 100644 --- a/js/api.js +++ b/js/api.js @@ -48,7 +48,7 @@ export class LosslessAPI { const maxTotalAttempts = instances.length * 2; // Allow some retries across instances let lastError = null; - let instanceIndex = 0; + let instanceIndex = Math.floor(Math.random() * instances.length); for (let attempt = 1; attempt <= maxTotalAttempts; attempt++) { const baseUrl = instances[instanceIndex % instances.length]; @@ -174,7 +174,7 @@ export class LosslessAPI { return artist; } - async enrichTracksWithAlbumDates(tracks) { + async enrichTracksWithAlbumDates(tracks, maxRequests = 20) { if (!trackDateSettings.useAlbumYear()) return tracks; const albumIdsToFetch = []; @@ -186,13 +186,26 @@ export class LosslessAPI { if (albumIdsToFetch.length === 0) return tracks; - const albumDateMap = new Map(); - const results = await Promise.allSettled(albumIdsToFetch.map((id) => this.getAlbum(id))); + // Limit the number of albums to fetch to prevent spamming + const limitedIds = albumIdsToFetch.slice(0, maxRequests); + if (albumIdsToFetch.length > maxRequests) { + console.warn(`[Enrich] Too many albums to fetch (${albumIdsToFetch.length}). limiting to ${maxRequests}.`); + } - for (let i = 0; i < results.length; i++) { - const result = results[i]; - if (result.status === 'fulfilled' && result.value.album?.releaseDate) { - albumDateMap.set(albumIdsToFetch[i], result.value.album.releaseDate); + const albumDateMap = new Map(); + + // Chunk requests to avoid spamming + const chunkSize = 5; + for (let i = 0; i < limitedIds.length; i += chunkSize) { + const chunk = limitedIds.slice(i, i + chunkSize); + const results = await Promise.allSettled(chunk.map((id) => this.getAlbum(id))); + + for (let j = 0; j < results.length; j++) { + const result = results[j]; + const id = chunk[j]; + if (result.status === 'fulfilled' && result.value.album?.releaseDate) { + albumDateMap.set(id, result.value.album.releaseDate); + } } } @@ -304,10 +317,11 @@ export class LosslessAPI { const data = await response.json(); const normalized = this.normalizeSearchResponse(data, 'tracks'); const preparedTracks = normalized.items.map((t) => this.prepareTrack(t)); - const enrichedTracks = await this.enrichTracksWithAlbumDates(preparedTracks); + // Skip enrichment for search to be fast and lightweight + // const enrichedTracks = await this.enrichTracksWithAlbumDates(preparedTracks); const result = { ...normalized, - items: enrichedTracks, + items: preparedTracks, }; await this.cache.set('search_tracks', query, result); @@ -616,7 +630,8 @@ export class LosslessAPI { } // Enrich tracks with album release dates - tracks = await this.enrichTracksWithAlbumDates(tracks); + // Removed to reduce API load. Playlists can be very large. + // tracks = await this.enrichTracksWithAlbumDates(tracks); const result = { playlist, tracks }; @@ -641,7 +656,8 @@ export class LosslessAPI { let tracks = items.map((i) => this.prepareTrack(i.item || i)); // Enrich tracks with album release dates - tracks = await this.enrichTracksWithAlbumDates(tracks); + // Limited to reduce API load + tracks = await this.enrichTracksWithAlbumDates(tracks, 10); const mix = { id: mixData.id, @@ -657,8 +673,9 @@ export class LosslessAPI { return result; } - async getArtist(artistId) { - const cached = await this.cache.get('artist', artistId); + async getArtist(artistId, options = {}) { + const cacheKey = options.lightweight ? `artist_${artistId}_light` : `artist_${artistId}`; + const cached = await this.cache.get('artist', cacheKey); if (cached) return cached; const [primaryResponse, contentResponse] = await Promise.all([ @@ -709,25 +726,27 @@ export class LosslessAPI { entries.forEach((entry) => scan(entry)); - // Attempt to find more albums/EPs via search since the direct feed might be limited - try { - const searchResults = await this.searchAlbums(artist.name); - if (searchResults && searchResults.items) { - const numericArtistId = Number(artistId); + if (!options.lightweight) { + // Attempt to find more albums/EPs via search since the direct feed might be limited + try { + const searchResults = await this.searchAlbums(artist.name); + if (searchResults && searchResults.items) { + const numericArtistId = Number(artistId); - for (const item of searchResults.items) { - const itemArtistId = item.artist?.id; - const matchesArtist = - itemArtistId === numericArtistId || - (Array.isArray(item.artists) && item.artists.some((a) => a.id === numericArtistId)); + for (const item of searchResults.items) { + const itemArtistId = item.artist?.id; + const matchesArtist = + itemArtistId === numericArtistId || + (Array.isArray(item.artists) && item.artists.some((a) => a.id === numericArtistId)); - if (matchesArtist && !albumMap.has(item.id)) { - albumMap.set(item.id, item); + if (matchesArtist && !albumMap.has(item.id)) { + albumMap.set(item.id, item); + } } } + } catch (e) { + console.warn('Failed to fetch additional albums via search:', e); } - } catch (e) { - console.warn('Failed to fetch additional albums via search:', e); } const rawReleases = Array.from(albumMap.values()); @@ -743,11 +762,11 @@ export class LosslessAPI { .slice(0, 15); // Enrich tracks with album release dates - const tracks = await this.enrichTracksWithAlbumDates(topTracks); + const tracks = options.lightweight ? topTracks : await this.enrichTracksWithAlbumDates(topTracks); const result = { ...artist, albums, eps, tracks }; - await this.cache.set('artist', artistId, result); + await this.cache.set('artist', cacheKey, result); return result; } @@ -851,23 +870,32 @@ export class LosslessAPI { const artistsToProcess = artists.slice(0, Math.min(5, artists.length)); console.log(`Processing ${artistsToProcess.length} artists for recommendations`); - for (const artist of artistsToProcess) { + const artistPromises = artistsToProcess.map(async (artist) => { try { console.log(`Fetching tracks for artist: ${artist.name} (ID: ${artist.id})`); - const artistData = await this.getArtist(artist.id); + const artistData = await this.getArtist(artist.id, { lightweight: true }); if (artistData && artistData.tracks && artistData.tracks.length > 0) { const newTracks = artistData.tracks.filter((track) => !seenTrackIds.has(track.id)).slice(0, 4); console.log(`Found ${newTracks.length} new tracks from ${artist.name}`); - recommendedTracks.push(...newTracks); - seenTrackIds.add(...newTracks.map((t) => t.id)); + return newTracks; } else { console.warn(`No tracks found for artist ${artist.name}`); + return []; } } catch (e) { console.warn(`Failed to get tracks for artist ${artist.name}:`, e); + return []; } - } + }); + + const results = await Promise.all(artistPromises); + results.forEach((tracks) => { + if (tracks.length > 0) { + recommendedTracks.push(...tracks); + seenTrackIds.add(...tracks.map((t) => t.id)); + } + }); console.log(`Total recommended tracks found: ${recommendedTracks.length}`); diff --git a/js/settings.js b/js/settings.js index 0d00c08..be6c6eb 100644 --- a/js/settings.js +++ b/js/settings.js @@ -1054,7 +1054,7 @@ export function initializeSettings(scrobbler, player, api, ui) { btn.disabled = true; try { - await api.settings.refreshSpeedTests(); + await api.settings.refreshInstances(); ui.renderApiSettings(); btn.textContent = 'Done!'; setTimeout(() => { diff --git a/js/storage.js b/js/storage.js index 13aa69a..8ebcc65 100644 --- a/js/storage.js +++ b/js/storage.js @@ -2,8 +2,6 @@ export const apiSettings = { STORAGE_KEY: 'monochrome-api-instances-v6', INSTANCES_URL: 'instances.json', - SPEED_TEST_CACHE_KEY: 'monochrome-instance-speeds', - SPEED_TEST_CACHE_DURATION: 1000 * 60 * 60, defaultInstances: { api: [], streaming: [] }, instancesLoaded: false, @@ -88,98 +86,6 @@ export const apiSettings = { } }, - async speedTestInstance(url, type = 'api') { - let testUrl; - // API instances might not support /track/ endpoint (which checks for streamability) - // So we test API instances with a lightweight metadata endpoint - if (type === 'streaming') { - testUrl = url.endsWith('/') - ? `${url}track/?id=204567804&quality=HIGH` - : `${url}/track/?id=204567804&quality=HIGH`; - } else { - testUrl = url.endsWith('/') - ? `${url}artist/?id=3532302` // Daft Punk - : `${url}/artist/?id=3532302`; - } - - 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, type, speed: Infinity, error: `HTTP ${response.status}` }; - } - - const endTime = performance.now(); - const speed = endTime - startTime; - - return { url, type, speed, error: null }; - } catch (error) { - return { url, type, speed: Infinity, error: error.message }; - } - }, - - getCachedSpeedTests() { - try { - const cached = localStorage.getItem(this.SPEED_TEST_CACHE_KEY); - if (!cached) return { speeds: {}, timestamp: Date.now() }; - - const data = JSON.parse(cached); - - if (Date.now() - data.timestamp > this.SPEED_TEST_CACHE_DURATION) { - return { speeds: {}, timestamp: Date.now() }; - } - - return data; - } catch { - return { speeds: {}, timestamp: Date.now() }; - } - }, - - updateSpeedCache(newResults) { - const currentCache = this.getCachedSpeedTests(); - - newResults.forEach((r) => { - // Use distinct keys for streaming tests to avoid overwriting API tests for same URL - // API tests use raw URL as key (for backward compatibility with UI) - const key = r.type === 'streaming' ? `${r.url}#streaming` : r.url; - currentCache.speeds[key] = { speed: r.speed, error: r.error }; - }); - - currentCache.timestamp = Date.now(); - - try { - localStorage.setItem(this.SPEED_TEST_CACHE_KEY, JSON.stringify(currentCache)); - } catch { - console.warn('[SpeedTest] Failed to cache results'); - } - - return currentCache; - }, - - async testSpecificUrls(urls, type) { - if (!urls || urls.length === 0) return []; - console.log(`[SpeedTest] Testing ${urls.length} instances for ${type}...`); - - const results = await Promise.all(urls.map((url) => this.speedTestInstance(url, type))); - - const validResults = results.filter((r) => r.speed !== Infinity); - console.log( - `[SpeedTest] ${type} Results:`, - validResults.map((r) => `${r.url}: ${r.speed.toFixed(0)}ms`) - ); - - return results; - }, async getInstances(type = 'api', sortBySpeed = false) { let instancesObj; @@ -207,60 +113,32 @@ export const apiSettings = { const targetUrls = instancesObj[type] || instancesObj.api || []; if (targetUrls.length === 0) return []; - const speedCache = this.getCachedSpeedTests(); - // Construct cache key based on type - const getCacheKey = (u) => (type === 'streaming' ? `${u}#streaming` : u); - - const urlsToTest = targetUrls.filter((url) => !speedCache.speeds[getCacheKey(url)]); - - if (urlsToTest.length > 0) { - const results = await this.testSpecificUrls(urlsToTest, type); - this.updateSpeedCache(results); - Object.assign(speedCache, this.getCachedSpeedTests()); - } - - // Default: return instances in their stored/manual order (respects manual reordering) - // Only sort by speed when explicitly requested (e.g., refresh speed test) - if (!sortBySpeed) { - return targetUrls; - } - - const sortList = (list) => { - return [...list].sort((a, b) => { - const speedA = speedCache.speeds[getCacheKey(a)]?.speed ?? Infinity; - const speedB = speedCache.speeds[getCacheKey(b)]?.speed ?? Infinity; - return speedA - speedB; - }); - }; - - const sortedList = sortList(targetUrls); - - // Persist the sorted order - instancesObj[type] = sortedList; - this.saveInstances(instancesObj); - - return sortedList; + return targetUrls; }, - async refreshSpeedTests() { + async refreshInstances() { const instances = await this.loadInstancesFromGitHub(); - const promises = []; + + const shuffle = (array) => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + }; if (instances.api && instances.api.length) { - promises.push(this.testSpecificUrls(instances.api, 'api')); + instances.api = shuffle([...instances.api]); } - + if (instances.streaming && instances.streaming.length) { - promises.push(this.testSpecificUrls(instances.streaming, 'streaming')); + instances.streaming = shuffle([...instances.streaming]); } - const resultsArray = await Promise.all(promises); - const allResults = resultsArray.flat(); - - this.updateSpeedCache(allResults); + this.saveInstances(instances); // Return API instances for the UI to render (default view) - return this.getInstances('api', true); + return this.getInstances('api'); }, saveInstances(instances, type) { if (type) { diff --git a/js/ui.js b/js/ui.js index 8f97063..a88cf12 100644 --- a/js/ui.js +++ b/js/ui.js @@ -3156,27 +3156,15 @@ export class UIRenderer { const container = document.getElementById('api-instance-list'); Promise.all([this.api.settings.getInstances('api'), this.api.settings.getInstances('streaming')]).then( ([apiInstances, streamingInstances]) => { - const cachedData = this.api.settings.getCachedSpeedTests(); - const speeds = cachedData?.speeds || {}; - const renderGroup = (instances, type) => { if (!instances || instances.length === 0) return ''; const listHtml = instances .map((url, index) => { - const cacheKey = type === 'streaming' ? `${url}#streaming` : url; - const speedInfo = speeds[cacheKey]; - const speedText = speedInfo - ? speedInfo.speed === Infinity || typeof speedInfo.speed !== 'number' - ? `Failed` - : `${speedInfo.speed.toFixed(0)}ms` - : ''; - return `
  • ${url}
    - ${speedText}