diff --git a/DOCKER.md b/DOCKER.md index 38e6ba2e..1603c6d5 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -99,6 +99,7 @@ The application is configured via environment variables. Copy `.env.example` to ### Authentication (Better Auth) Monochrome uses Better Auth for user authentication (defaulting to `https://auth.monochrome.st`). You can configure a custom auth instance using: + - `AUTH_URL`: Your Better Auth endpoint (e.g., `https://auth.monochrome.st`). ### Database (PocketBase) diff --git a/INSTANCES.md b/INSTANCES.md index 66ffd6d5..74773615 100644 --- a/INSTANCES.md +++ b/INSTANCES.md @@ -27,15 +27,15 @@ PLEASE do not use any rehost of monochrome and complain to us about features not ### Official & Community APIs -| Provider | URL | Notes | -| ----------------- | ----------------------------------- | ----------------------------------------------------------------------- | -| **Monochrome** | `https://api.monochrome.tf` | Official API | -| **Lucida (QQDL)** | `https://wolf.qqdl.site` | Community hosted | -| | `https://maus.qqdl.site` | Community hosted | -| | `https://vogel.qqdl.site` | Community hosted | -| | `https://katze.qqdl.site` | Community hosted | -| | `https://hund.qqdl.site` | Community hosted | -| **Kinoplus** | `https://tidal.kinoplus.online` | Community hosted - [Limited/No-Sub](https://rentry.co/limitedtidalaccs) | +| Provider | URL | Notes | +| ----------------- | ------------------------------- | ----------------------------------------------------------------------- | +| **Monochrome** | `https://api.monochrome.tf` | Official API | +| **Lucida (QQDL)** | `https://wolf.qqdl.site` | Community hosted | +| | `https://maus.qqdl.site` | Community hosted | +| | `https://vogel.qqdl.site` | Community hosted | +| | `https://katze.qqdl.site` | Community hosted | +| | `https://hund.qqdl.site` | Community hosted | +| **Kinoplus** | `https://tidal.kinoplus.online` | Community hosted - [Limited/No-Sub](https://rentry.co/limitedtidalaccs) | --- diff --git a/index.html b/index.html index dcdc4fe9..226e015f 100644 --- a/index.html +++ b/index.html @@ -1279,7 +1279,12 @@

- +
@@ -4220,24 +4225,12 @@
@@ -5528,24 +5521,12 @@ > @@ -6176,8 +6157,8 @@

Version 2.5.0

- This is an independent client and is not affiliated with or endorsed by any - music streaming service. + This is an independent client and is not affiliated with or endorsed by any music + streaming service.

This application does not host, store, or distribute any media files.
diff --git a/js/music-api.js b/js/music-api.js index c37ddccb..c5778975 100644 --- a/js/music-api.js +++ b/js/music-api.js @@ -9,11 +9,7 @@ import { normalizeAppleArtist, normalizeAppleSearchResults, } from './apple-music-api.js'; -import { - TracksStreamerAPI, - tracksStreamerAPI, - normalizeTracksSearchResults, -} from './tracks-api.js'; +import { TracksStreamerAPI, tracksStreamerAPI, normalizeTracksSearchResults } from './tracks-api.js'; import { getCommunityPlaylist } from './community-playlists.js'; /** @@ -169,7 +165,8 @@ export class MusicAPI { } } catch (error) { if (error.name === 'AbortError') throw error; - if (import.meta.env.DEV) console.warn('[searchTracks] Tracks Streamer unavailable, trying fallbacks', error); + if (import.meta.env.DEV) + console.warn('[searchTracks] Tracks Streamer unavailable, trying fallbacks', error); } return this.searchSection('tracks', 'songs', query, options, () => this.getAPI().searchTracks(query, options)); } @@ -185,7 +182,8 @@ export class MusicAPI { } } catch (error) { if (error.name === 'AbortError') throw error; - if (import.meta.env.DEV) console.warn('[searchArtists] Tracks Streamer unavailable, trying fallbacks', error); + if (import.meta.env.DEV) + console.warn('[searchArtists] Tracks Streamer unavailable, trying fallbacks', error); } return this.searchSection('artists', 'artists', query, options, () => this.getAPI().searchArtists(query, options) @@ -203,7 +201,8 @@ export class MusicAPI { } } catch (error) { if (error.name === 'AbortError') throw error; - if (import.meta.env.DEV) console.warn('[searchAlbums] Tracks Streamer unavailable, trying fallbacks', error); + if (import.meta.env.DEV) + console.warn('[searchAlbums] Tracks Streamer unavailable, trying fallbacks', error); } return this.searchSection('albums', 'albums', query, options, () => this.getAPI().searchAlbums(query, options)); } @@ -488,10 +487,7 @@ export class MusicAPI { // Stream methods async getStreamUrl(id, quality, options = {}) { - let track = - options?.track || - this.getCachedTracksTrack(id) || - this.getCachedAppleTrack(id); + let track = options?.track || this.getCachedTracksTrack(id) || this.getCachedAppleTrack(id); if (!track && (this.isTracksId(id) || this.isAppleId(id) || /^\d{17,20}$/.test(String(id)))) { track = await this.getTrackMetadata(id).catch(() => null); @@ -660,11 +656,7 @@ export class MusicAPI { if (type === 'artist' && this.tracksArtistIds.has(id)) return true; if (type === 'track' && this.tracksTrackCache.has(id)) return true; if (!type) { - return ( - this.tracksTrackCache.has(id) || - this.tracksAlbumIds.has(id) || - this.tracksArtistIds.has(id) - ); + return this.tracksTrackCache.has(id) || this.tracksAlbumIds.has(id) || this.tracksArtistIds.has(id); } } return false; diff --git a/js/player.js b/js/player.js index 086dabb2..9ecd6998 100644 --- a/js/player.js +++ b/js/player.js @@ -1509,7 +1509,6 @@ export class Player { let resolvedStreamInfo = await streamInfoPromise; if (this.playbackSequence !== currentSequence) return; - streamUrl = resolvedStreamInfo.url; this.currentStreamInfo = resolvedStreamInfo; this.currentStreamProvider = resolvedStreamInfo.provider || null; diff --git a/js/tracks-api.js b/js/tracks-api.js index e99fdbe3..f51e6ca0 100644 --- a/js/tracks-api.js +++ b/js/tracks-api.js @@ -33,16 +33,9 @@ export function normalizeTracksTrack(item) { const trackId = String(item.trackId || item.id || ''); const id = trackId; - const artistId = - item.artistIds?.[0] || - item.artists?.[0]?.artistId || - item.artists?.[0]?.id || - ''; + const artistId = item.artistIds?.[0] || item.artists?.[0]?.artistId || item.artists?.[0]?.id || ''; const artistName = - item.artistNames?.[0] || - item.artists?.[0]?.name || - item.artists?.[0]?.displayName || - 'Unknown Artist'; + item.artistNames?.[0] || item.artists?.[0]?.name || item.artists?.[0]?.displayName || 'Unknown Artist'; const primaryArtist = { id: artistId ? String(artistId) : '', @@ -86,11 +79,7 @@ export function normalizeTracksTrack(item) { const releaseId = item.releaseId ? String(item.releaseId) : ''; const artwork = item.artwork || item.cover || ''; - const albumTitle = - item.albumTitle || - item.releaseTitle || - (item.release && item.release.title) || - ''; + const albumTitle = item.albumTitle || item.releaseTitle || (item.release && item.release.title) || ''; const album = { id: releaseId, @@ -148,16 +137,9 @@ export function normalizeTracksRelease(item) { const releaseId = String(item.releaseId || item.id || ''); const id = releaseId; - const artistId = - item.artistIds?.[0] || - item.artists?.[0]?.artistId || - item.artists?.[0]?.id || - ''; + const artistId = item.artistIds?.[0] || item.artists?.[0]?.artistId || item.artists?.[0]?.id || ''; const artistName = - item.artistNames?.[0] || - item.artists?.[0]?.name || - item.artists?.[0]?.displayName || - 'Unknown Artist'; + item.artistNames?.[0] || item.artists?.[0]?.name || item.artists?.[0]?.displayName || 'Unknown Artist'; const primaryArtist = { id: artistId ? String(artistId) : '', @@ -186,9 +168,7 @@ export function normalizeTracksRelease(item) { : [primaryArtist]; const artwork = item.artwork || item.cover || ''; - const trackCount = - item.trackCount || - (Array.isArray(item.tracks) ? item.tracks.length : 0); + const trackCount = item.trackCount || (Array.isArray(item.tracks) ? item.tracks.length : 0); return { id, @@ -251,10 +231,7 @@ export function normalizeTracksPlaylist(item) { if (!item) return null; const playlistId = String(item.playlistId || item.id || ''); const id = playlistId; - const image = - typeof item.thumbnail === 'string' && item.thumbnail.startsWith('http') - ? item.thumbnail - : ''; + const image = typeof item.thumbnail === 'string' && item.thumbnail.startsWith('http') ? item.thumbnail : ''; return { id, @@ -336,9 +313,7 @@ export function extractTracksSuggestions(results, query) { seen.add(normalizedQuery); } - const rawTracks = Array.isArray(results) - ? results - : results?.tracks?.items || results?.tracks || []; + const rawTracks = Array.isArray(results) ? results : results?.tracks?.items || results?.tracks || []; for (const raw of rawTracks) { if (!raw) continue; @@ -387,24 +362,18 @@ export function scoreTrackCandidate(candidate, target) { if (targetTitleClean && candidateTitleClean) { if (targetTitleClean === candidateTitleClean) { score += 100; - } else if ( - targetTitleClean.includes(candidateTitleClean) || - candidateTitleClean.includes(targetTitleClean) - ) { + } else if (targetTitleClean.includes(candidateTitleClean) || candidateTitleClean.includes(targetTitleClean)) { score += 65; } } - const targetArtistName = cleanString( - target.artist?.name || target.artists?.[0]?.name || target.artist || '' - ); - const candidateArtistNames = ( - candidate.artistNames || [candidate.artist?.name || ''] - ).map(cleanString); + const targetArtistName = cleanString(target.artist?.name || target.artists?.[0]?.name || target.artist || ''); + const candidateArtistNames = (candidate.artistNames || [candidate.artist?.name || '']).map(cleanString); if (targetArtistName) { const hasArtistMatch = candidateArtistNames.some( - (cName) => cName === targetArtistName || cName.includes(targetArtistName) || targetArtistName.includes(cName) + (cName) => + cName === targetArtistName || cName.includes(targetArtistName) || targetArtistName.includes(cName) ); if (hasArtistMatch) { score += 50; @@ -412,14 +381,9 @@ export function scoreTrackCandidate(candidate, target) { } // Check duration similarity (within 6 seconds) - const targetDur = - target.duration > 1000 - ? Math.round(target.duration / 1000) - : Math.round(target.duration || 0); + const targetDur = target.duration > 1000 ? Math.round(target.duration / 1000) : Math.round(target.duration || 0); const candDur = - candidate.duration > 1000 - ? Math.round(candidate.duration / 1000) - : Math.round(candidate.duration || 0); + candidate.duration > 1000 ? Math.round(candidate.duration / 1000) : Math.round(candidate.duration || 0); if (targetDur > 0 && candDur > 0) { const diff = Math.abs(targetDur - candDur); @@ -460,9 +424,7 @@ export class TracksStreamerAPI { }); if ( - (response.status === 502 || - response.status === 503 || - response.status === 504) && + (response.status === 502 || response.status === 503 || response.status === 504) && attempt < retries ) { await new Promise((r) => setTimeout(r, 200 * (attempt + 1))); @@ -854,12 +816,8 @@ export class TracksStreamerAPI { * @returns {Promise} Stream info or null if unresolved */ async resolveTrackStream(idOrTrack, quality = 'LOSSLESS', options = {}) { - const inputTrack = - options.track || (typeof idOrTrack === 'object' ? idOrTrack : null); - const id = - typeof idOrTrack === 'string' - ? idOrTrack - : String(inputTrack?.id || ''); + const inputTrack = options.track || (typeof idOrTrack === 'object' ? idOrTrack : null); + const id = typeof idOrTrack === 'string' ? idOrTrack : String(inputTrack?.id || ''); // 1. Direct match: Already a tracks.monochrome.st track if (inputTrack?.tracksTrackId || inputTrack?.recordingId) { diff --git a/js/ui.js b/js/ui.js index eb180ec8..f640bade 100644 --- a/js/ui.js +++ b/js/ui.js @@ -3691,10 +3691,8 @@ export class UIRenderer { `Community Year-End ${year}${communityAgg.totalLists ? ` · ${communityAgg.totalLists} lists` : ''}`, communityAgg.items ); - if (topRated?.items?.length) - this.renderAOTYRankedRows(contentDiv, `Top Rated ${year}`, topRated.items); - if (!contentDiv.children.length) - contentDiv.innerHTML = createPlaceholder('No chart data found.'); + if (topRated?.items?.length) this.renderAOTYRankedRows(contentDiv, `Top Rated ${year}`, topRated.items); + if (!contentDiv.children.length) contentDiv.innerHTML = createPlaceholder('No chart data found.'); } catch (e) { console.error(e); contentDiv.innerHTML = createPlaceholder('Failed to load charts.'); @@ -3872,7 +3870,15 @@ export class UIRenderer { this.renderAOTYSection(contentDiv, `Albums matching “${q.trim()}”`, data.albums); return; } - const rows = data.artists || data.labels || data.lists || data.items || data.news || data.tags || data.users || []; + const rows = + data.artists || + data.labels || + data.lists || + data.items || + data.news || + data.tags || + data.users || + []; if (!rows.length) { contentDiv.innerHTML = createPlaceholder('No results found.'); return; @@ -3920,7 +3926,10 @@ export class UIRenderer { }; container.querySelector('#aoty-search-form').addEventListener('submit', (e) => { e.preventDefault(); - void runSearch(container.querySelector('#aoty-search-q').value, container.querySelector('#aoty-search-scope').value); + void runSearch( + container.querySelector('#aoty-search-q').value, + container.querySelector('#aoty-search-scope').value + ); }); } @@ -4031,11 +4040,7 @@ export class UIRenderer { `; row.querySelector('.row-title').textContent = itemTitle || item.title || ''; - const metaParts = [ - itemArtist, - item.date, - ...(item.genres?.slice(0, 2) || []), - ].filter(Boolean); + const metaParts = [itemArtist, item.date, ...(item.genres?.slice(0, 2) || [])].filter(Boolean); row.querySelector('.row-meta').textContent = metaParts.join(' · '); row.appendChild(scoreEl); if (embeddedScore == null) scoreTargets.push({ item, scoreEl }); diff --git a/page_content.html b/page_content.html index f46af52e..e596c6bf 100644 --- a/page_content.html +++ b/page_content.html @@ -1562,7 +1562,12 @@

- +