style: auto-fix linting issues

This commit is contained in:
SamidyFR 2026-01-24 12:13:44 +00:00 committed by github-actions[bot]
parent b7bc90f4f1
commit 4ede3b2664
23 changed files with 413 additions and 273 deletions

View file

@ -22,4 +22,4 @@ UI:
| QQDL | https://tidal.qqdl.site/ |
| Arjix | https://music.arjix.dev/ |
| Spofree | https://spo.free.nf |
| Mappl | https://mappl.tv/music |
| Mappl | https://mappl.tv/music |

View file

@ -67,14 +67,16 @@ class ServerAPI {
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(userAgent);
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
userAgent
);
const albumId = params.id;
if (isBot && albumId) {
try {
const api = new ServerAPI();
const data = await api.getAlbumMetadata(albumId);
const album = data.data || data.album || data;
const album = data.data || data.album || data;
const tracks = album.items || data.tracks || [];
if (album && (album.title || album.name)) {
@ -82,9 +84,11 @@ export async function onRequest(context) {
const artist = album.artist?.name || 'Unknown Artist';
const year = album.releaseDate ? new Date(album.releaseDate).getFullYear() : '';
const trackCount = album.numberOfTracks || tracks.length;
const description = `Album by ${artist}${year}${trackCount} Tracks\nListen on Monochrome`;
const imageUrl = album.cover ? api.getCoverUrl(album.cover, '1280') : 'https://monochrome.samidy.com/assets/appicon.png';
const imageUrl = album.cover
? api.getCoverUrl(album.cover, '1280')
: 'https://monochrome.samidy.com/assets/appicon.png';
const pageUrl = new URL(request.url).href;
const metaHtml = `

View file

@ -67,7 +67,9 @@ class ServerAPI {
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(userAgent);
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
userAgent
);
const artistId = params.id;
if (isBot && artistId) {
@ -79,7 +81,9 @@ export async function onRequest(context) {
if (artist && (artist.name || artist.title)) {
const name = artist.name || artist.title;
const description = `Listen to ${name} on Monochrome`;
const imageUrl = artist.picture ? api.getArtistPictureUrl(artist.picture, '750') : 'https://monochrome.samidy.com/assets/appicon.png';
const imageUrl = artist.picture
? api.getArtistPictureUrl(artist.picture, '750')
: 'https://monochrome.samidy.com/assets/appicon.png';
const pageUrl = new URL(request.url).href;
const metaHtml = `

View file

@ -68,7 +68,9 @@ class ServerAPI {
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(userAgent);
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
userAgent
);
const playlistId = params.id;
if (isBot && playlistId) {
@ -82,7 +84,9 @@ export async function onRequest(context) {
const trackCount = playlist.numberOfTracks;
const description = `Playlist • ${trackCount} Tracks\nListen on Monochrome`;
const imageId = playlist.squareImage || playlist.image;
const imageUrl = imageId ? api.getCoverUrl(imageId, '1080') : 'https://monochrome.samidy.com/assets/appicon.png';
const imageUrl = imageId
? api.getCoverUrl(imageId, '1080')
: 'https://monochrome.samidy.com/assets/appicon.png';
const pageUrl = new URL(request.url).href;
const metaHtml = `

View file

@ -64,7 +64,7 @@ class ServerAPI {
const json = await response.json();
const data = json.data || json;
const items = Array.isArray(data) ? data : [data];
const found = items.find(i => (i.id == id) || (i.item && i.item.id == id));
const found = items.find((i) => i.id == id || (i.item && i.item.id == id));
if (found) {
return found.item || found;
}
@ -87,7 +87,9 @@ class ServerAPI {
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(userAgent);
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
userAgent
);
const trackId = params.id;
if (isBot && trackId) {
@ -112,12 +114,14 @@ export async function onRequest(context) {
}
}
// this prob wont work im js winging it
const audioMeta = audioUrl ? `
const audioMeta = audioUrl
? `
<meta property="og:audio" content="${audioUrl}">
<meta property="og:audio:type" content="audio/mp4">
<meta property="og:video" content="${audioUrl}">
<meta property="og:video:type" content="audio/mp4">
` : '';
`
: '';
const metaHtml = `
<!DOCTYPE html>
@ -164,4 +168,4 @@ export async function onRequest(context) {
const url = new URL(request.url);
url.pathname = '/';
return env.ASSETS.fetch(new Request(url, request));
}
}

View file

@ -1,24 +1,25 @@
// functions/userplaylist/[id].js
// note that, since this NEEDS a playlist to yknow, be public, this only works for PUBLIC playlists (and you will need an account)
export async function onRequest(context) {
const { request, params, env } = context;
const userAgent = request.headers.get('User-Agent') || '';
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(userAgent);
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
userAgent
);
const playlistId = params.id;
if (isBot && playlistId) {
try {
let pbUrl = `https://monodb.samidy.com/api/collections/user_playlists/records/${playlistId}`;
let response = await fetch(pbUrl);
if (!response.ok) {
pbUrl = `https://monodb.samidy.com/api/collections/public_playlists/records?filter=(uuid='${playlistId}')`;
response = await fetch(pbUrl);
pbUrl = `https://monodb.samidy.com/api/collections/public_playlists/records?filter=(uuid='${playlistId}')`;
response = await fetch(pbUrl);
}
if (response.ok) {
let playlist = await response.json();
if (playlist.items && Array.isArray(playlist.items) && playlist.items.length > 0) {
@ -30,14 +31,18 @@ export async function onRequest(context) {
const title = playlist.name || playlist.title || 'User Playlist';
let tracks = [];
try {
tracks = Array.isArray(playlist.tracks) ? playlist.tracks : (playlist.tracks ? JSON.parse(playlist.tracks) : []);
tracks = Array.isArray(playlist.tracks)
? playlist.tracks
: playlist.tracks
? JSON.parse(playlist.tracks)
: [];
} catch (e) {
console.error('Failed to parse tracks JSON', e);
}
const trackCount = tracks.length;
const trackCount = tracks.length;
const description = `User Playlist • ${trackCount} Tracks\nListen on Monochrome`;
let imageUrl = 'https://monochrome.samidy.com/assets/appicon.png';
if (playlist.cover) {
if (playlist.cover.startsWith('http')) {
@ -45,7 +50,12 @@ export async function onRequest(context) {
} else {
imageUrl = `https://monodb.samidy.com/api/files/${playlist.collectionId}/${playlist.id}/${playlist.cover}`;
}
} else if (tracks.length > 0 && typeof tracks[0] === 'object' && tracks[0].album && tracks[0].album.cover) {
} else if (
tracks.length > 0 &&
typeof tracks[0] === 'object' &&
tracks[0].album &&
tracks[0].album.cover
) {
const cover = tracks[0].album.cover;
imageUrl = `https://resources.tidal.com/images/${cover.replace(/-/g, '/')}/1280x1280.jpg`;
}

View file

@ -843,11 +843,11 @@ export class LosslessAPI {
const response = await this.fetchWithRetry(`/info/?id=${id}`, { type: 'api' });
const json = await response.json();
const data = json.data || json;
let track;
const items = Array.isArray(data) ? data : [data];
const found = items.find(i => (i.id == id) || (i.item && i.item.id == id));
const found = items.find((i) => i.id == id || (i.item && i.item.id == id));
if (found) {
track = this.prepareTrack(found.item || found);
await this.cache.set('track', cacheKey, track);

View file

@ -15,7 +15,13 @@ import { createRouter, updateTabTitle, navigate } from './router.js';
import { initializeSettings } from './settings.js';
import { initializePlayerEvents, initializeTrackInteractions, handleTrackAction } from './events.js';
import { initializeUIInteractions } from './ui-interactions.js';
import { downloadAlbumAsZip, downloadDiscography, downloadPlaylistAsZip, downloadLikedTracks, showNotification } from './downloads.js';
import {
downloadAlbumAsZip,
downloadDiscography,
downloadPlaylistAsZip,
downloadLikedTracks,
showNotification,
} from './downloads.js';
import { debounce, SVG_PLAY } from './utils.js';
import { sidePanelManager } from './side-panel.js';
import { db } from './db.js';
@ -192,12 +198,13 @@ document.addEventListener('DOMContentLoaded', async () => {
// i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only
const ua = navigator.userAgent.toLowerCase();
const isIOS = /iphone|ipad|ipod/.test(ua) || (ua.includes('mac') && navigator.maxTouchPoints > 1);
const isSafari = ua.includes('safari') && !ua.includes('chrome') && !ua.includes('crios') && !ua.includes('android');
const isSafari =
ua.includes('safari') && !ua.includes('chrome') && !ua.includes('crios') && !ua.includes('android');
if (isIOS || isSafari) {
const qualitySelect = document.getElementById('streaming-quality-setting');
const downloadSelect = document.getElementById('download-quality-setting');
const removeHiRes = (select) => {
if (!select) return;
const option = select.querySelector('option[value="HI_RES_LOSSLESS"]');
@ -228,7 +235,7 @@ document.addEventListener('DOMContentLoaded', async () => {
try {
const playlist = await db.getPlaylist(id);
const imgElement = document.getElementById('playlist-detail-image');
if (!imgElement) return;
let container = imgElement.parentElement;
@ -239,7 +246,7 @@ document.addEventListener('DOMContentLoaded', async () => {
container.className = 'detail-header-cover-container';
imgElement.parentNode.insertBefore(container, imgElement);
container.appendChild(imgElement);
collageElement = document.createElement('div');
collageElement.id = 'playlist-detail-collage';
collageElement.className = 'detail-header-collage';
@ -389,7 +396,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const isCollapsed = document.body.classList.contains('sidebar-collapsed');
const toggleBtn = document.getElementById('sidebar-toggle');
if (toggleBtn) {
toggleBtn.innerHTML = isCollapsed
toggleBtn.innerHTML = isCollapsed
? '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>'
: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>';
}
@ -806,10 +813,10 @@ document.addEventListener('DOMContentLoaded', async () => {
e.stopPropagation();
const btn = e.target.closest('.remove-from-playlist-btn');
const playlistId = window.location.pathname.split('/')[2];
db.getPlaylist(playlistId).then(async (playlist) => {
let trackId = null;
// Prefer ID if available (from sorted view)
if (btn.dataset.trackId) {
trackId = btn.dataset.trackId;
@ -919,11 +926,15 @@ document.addEventListener('DOMContentLoaded', async () => {
return;
}
list.innerHTML = playlists.map(p => `
list.innerHTML = playlists
.map(
(p) => `
<div class="modal-option" data-id="${p.id}">
<span>${p.name}</span>
</div>
`).join('');
`
)
.join('');
const closeModal = () => {
modal.classList.remove('active');
@ -1207,7 +1218,6 @@ document.addEventListener('DOMContentLoaded', async () => {
updateTabTitle(player);
};
await handleRouteChange();
window.addEventListener('popstate', handleRouteChange);
@ -1215,7 +1225,12 @@ document.addEventListener('DOMContentLoaded', async () => {
document.body.addEventListener('click', (e) => {
const link = e.target.closest('a');
if (link && link.origin === window.location.origin && link.target !== '_blank' && !link.hasAttribute('download')) {
if (
link &&
link.origin === window.location.origin &&
link.target !== '_blank' &&
!link.hasAttribute('download')
) {
e.preventDefault();
navigate(link.pathname);
}

View file

@ -77,7 +77,7 @@ export class DashDownloader {
adaptationSets.sort((a, b) => {
const getMaxBandwidth = (set) => {
const reps = Array.from(set.querySelectorAll('Representation'));
return reps.length ? Math.max(...reps.map(r => parseInt(r.getAttribute('bandwidth') || '0', 10))) : 0;
return reps.length ? Math.max(...reps.map((r) => parseInt(r.getAttribute('bandwidth') || '0', 10))) : 0;
};
return getMaxBandwidth(b) - getMaxBandwidth(a);
});

View file

@ -545,7 +545,7 @@ export class MusicDatabase {
cover: cover,
playlists: [],
createdAt: Date.now(),
updatedAt: Date.now()
updatedAt: Date.now(),
};
await this.performTransaction('user_folders', 'readwrite', (store) => store.put(folder));
return folder;

View file

@ -545,7 +545,16 @@ function createBulkDownloadNotification(type, name, _totalItems) {
notifEl.dataset.bulkType = type;
notifEl.dataset.bulkName = name;
const typeLabel = type === 'album' ? 'Album' : type === 'playlist' ? 'Playlist' : type === 'liked' ? 'Liked Tracks' : type === 'queue' ? 'Queue' : 'Discography';
const typeLabel =
type === 'album'
? 'Album'
: type === 'playlist'
? 'Playlist'
: type === 'liked'
? 'Liked Tracks'
: type === 'queue'
? 'Queue'
: 'Discography';
notifEl.innerHTML = `
<div style="display: flex; align-items: start; gap: 0.75rem;">

View file

@ -170,7 +170,10 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
const progressBar = document.getElementById('progress-bar');
const playerControls = document.querySelector('.player-controls');
const isTracker = player.currentTrack && (player.currentTrack.isTracker || (player.currentTrack.id && String(player.currentTrack.id).startsWith('tracker-')));
const isTracker =
player.currentTrack &&
(player.currentTrack.isTracker ||
(player.currentTrack.id && String(player.currentTrack.id).startsWith('tracker-')));
if (!waveformSettings.isEnabled() || !player.currentTrack || isTracker) {
if (progressBar) {
@ -624,7 +627,7 @@ export async function handleTrackAction(
if (nowPlayingLikeBtn && type === 'track' && player?.currentTrack?.id === item.id) {
elementsToUpdate.push(nowPlayingLikeBtn);
}
const fsLikeBtn = document.getElementById('fs-like-btn');
if (fsLikeBtn && type === 'track' && player?.currentTrack?.id === item.id) {
elementsToUpdate.push(fsLikeBtn);

View file

@ -12,28 +12,28 @@ class GeniusManager {
}
getToken() {
return "QmS9OvsS-7ifRBKx_ochIPQU7oejIS9Eo_z5iWHmCPyhwLVQID3pYTHJmJTa6z8z"; // idgaf anymore im js hardcoding this lmaooo
return 'QmS9OvsS-7ifRBKx_ochIPQU7oejIS9Eo_z5iWHmCPyhwLVQID3pYTHJmJTa6z8z'; // idgaf anymore im js hardcoding this lmaooo
}
async searchTrack(title, artist) {
const cleanTitle = title.split('(')[0].split('-')[0].trim();
const query = encodeURIComponent(`${cleanTitle} ${artist}`);
const url = `https://api.genius.com/search?q=${query}`;
const token = this.getToken();
const response = await fetch(`https://corsproxy.io/?${encodeURIComponent(url)}`, {
headers: { 'Authorization': `Bearer ${token}` }
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) throw new Error('Failed to search Genius');
const data = await response.json();
if (data.response.hits.length === 0) return null;
const normalize = str => str.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
const normalize = (str) => str.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
const targetArtist = normalize(artist);
const hit = data.response.hits.find(h => {
const hit = data.response.hits.find((h) => {
const hitArtist = normalize(h.result.primary_artist.name);
return hitArtist.includes(targetArtist) || targetArtist.includes(hitArtist);
});
@ -45,7 +45,7 @@ class GeniusManager {
const token = this.getToken();
const url = `https://api.genius.com/referents?song_id=${songId}&text_format=plain&per_page=50`;
const response = await fetch(`https://corsproxy.io/?${encodeURIComponent(url)}`, {
headers: { 'Authorization': `Bearer ${token}` }
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) throw new Error('Failed to fetch annotations');
@ -61,7 +61,7 @@ class GeniusManager {
this.loading = true;
const artist = Array.isArray(track.artists) ? track.artists[0].name : track.artist.name;
const song = await this.searchTrack(track.title, artist);
if (!song) {
this.loading = false;
return null;
@ -69,7 +69,7 @@ class GeniusManager {
const referents = await this.getReferents(song.id);
const result = { song, referents };
this.cache.set(track.id, result);
this.loading = false;
return result;
@ -82,25 +82,32 @@ class GeniusManager {
findAnnotations(lineText, referents) {
if (!referents || !lineText) return [];
const normalize = str => str.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, '').replace(/\s+/g, ' ').trim();
const normalize = (str) =>
str
.toLowerCase()
.replace(/[^\p{L}\p{N}\s]/gu, '')
.replace(/\s+/g, ' ')
.trim();
const normLine = normalize(lineText);
const getWordSet = (str) => new Set(str.split(' ').filter(w => w.length > 0));
const getWordSet = (str) => new Set(str.split(' ').filter((w) => w.length > 0));
const lineWords = getWordSet(normLine);
return referents.filter(ref => {
return referents.filter((ref) => {
const normFragment = normalize(ref.fragment);
if (normLine.includes(normFragment) || normFragment.includes(normLine)) return true;
const fragmentWords = getWordSet(normFragment);
if (fragmentWords.size === 0 || lineWords.size === 0) return false;
let matchCount = 0;
fragmentWords.forEach(w => { if (lineWords.has(w)) matchCount++; });
fragmentWords.forEach((w) => {
if (lineWords.has(w)) matchCount++;
});
return (matchCount / Math.min(fragmentWords.size, lineWords.size)) > 0.6;
return matchCount / Math.min(fragmentWords.size, lineWords.size) > 0.6;
});
}
}
@ -454,19 +461,20 @@ export class LyricsManager {
this.romajiObserver = new MutationObserver((mutations) => {
// Check if any relevant mutation occurred
const hasRelevantChange = mutations.some((mutation) => {
if (mutation.type === 'childList') {
let relevant = false;
if (mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('genius-indicator')) continue;
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('genius-indicator'))
continue;
relevant = true;
break;
}
}
if (!relevant && mutation.removedNodes.length > 0) {
for (const node of mutation.removedNodes) {
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('genius-indicator')) continue;
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('genius-indicator'))
continue;
relevant = true;
break;
}
@ -628,34 +636,38 @@ export class LyricsManager {
if (lineElements.length === 0) return;
lineElements.forEach(el => {
lineElements.forEach((el) => {
el.classList.remove('genius-annotated', 'genius-multi-start', 'genius-multi-end', 'genius-multi-mid');
delete el.__geniusAnnotations;
});
const normalize = (str) =>
str
.toLowerCase()
.replace(/[^\p{L}\p{N}\s]/gu, '')
.replace(/\s+/g, ' ')
.trim();
const normalize = str => str.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, '').replace(/\s+/g, ' ').trim();
referents.forEach(ref => {
referents.forEach((ref) => {
const fragment = normalize(ref.fragment);
if (!fragment) return;
for (let i = 0; i < lineElements.length; i++) {
let combinedText = "";
let combinedText = '';
let currentLines = [];
for (let j = i; j < lineElements.length; j++) {
const line = lineElements[j];
const lineClone = line.cloneNode(true);
lineClone.querySelectorAll('.time, .timestamp, [class*="time"], .genius-indicator').forEach(n => n.remove());
const text = normalize(lineClone.textContent || "");
lineClone
.querySelectorAll('.time, .timestamp, [class*="time"], .genius-indicator')
.forEach((n) => n.remove());
const text = normalize(lineClone.textContent || '');
if (!text) continue;
if (currentLines.length > 0) combinedText += " ";
if (currentLines.length > 0) combinedText += ' ';
combinedText += text;
currentLines.push(line);
@ -664,7 +676,7 @@ export class LyricsManager {
el.classList.add('genius-annotated');
if (!el.__geniusAnnotations) el.__geniusAnnotations = [];
if (!el.__geniusAnnotations.some(a => a.id === ref.id)) {
if (!el.__geniusAnnotations.some((a) => a.id === ref.id)) {
el.__geniusAnnotations.push(ref);
}
@ -683,10 +695,9 @@ export class LyricsManager {
el.appendChild(smiley);
}
});
break;
break;
}
if (combinedText.length > fragment.length + 50) break;
}
}
@ -754,19 +765,22 @@ export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = f
geniusBtn.addEventListener('click', async () => {
manager.isGeniusMode = !manager.isGeniusMode;
const enabled = manager.isGeniusMode;
geniusBtn.classList.toggle('active-genius', enabled);
geniusBtn.style.color = enabled ? '#ffff64' : '';
geniusBtn.innerHTML = enabled ? SVG_GENIUS_ACTIVE : SVG_GENIUS_INACTIVE;
if (enabled) {
try {
geniusBtn.style.opacity = '0.5';
await manager.geniusManager.getDataForTrack(track);
manager.currentGeniusData = manager.geniusManager.cache.get(track.id);
const amLyrics = sidePanelManager.panel.querySelector('am-lyrics');
if (amLyrics) manager.applyGeniusAnnotations(amLyrics, manager.geniusManager.cache.get(track.id)?.referents);
if (amLyrics)
manager.applyGeniusAnnotations(
amLyrics,
manager.geniusManager.cache.get(track.id)?.referents
);
} catch (e) {
alert(e.message);
manager.isGeniusMode = false;
@ -776,13 +790,17 @@ export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = f
geniusBtn.style.opacity = '1';
}
} else {
const amLyrics = sidePanelManager.panel.querySelector('am-lyrics');
if (amLyrics) {
const root = amLyrics.shadowRoot || amLyrics;
const lineElements = Array.from(root.querySelectorAll('.genius-annotated'));
lineElements.forEach(el => {
el.classList.remove('genius-annotated', 'genius-multi-start', 'genius-multi-end', 'genius-multi-mid');
lineElements.forEach((el) => {
el.classList.remove(
'genius-annotated',
'genius-multi-start',
'genius-multi-end',
'genius-multi-mid'
);
delete el.__geniusAnnotations;
});
}
@ -848,18 +866,22 @@ async function renderLyricsComponent(container, track, audioPlayer, lyricsManage
await lyricsManager.loadKuroshiro();
}
lyricsManager.fetchLyrics(track.id, track).then(async () => {
if (lyricsManager.isGeniusMode) {
try {
const data = await lyricsManager.geniusManager.getDataForTrack(track);
if (data) {
lyricsManager.currentGeniusData = data;
lyricsManager.applyGeniusAnnotations(amLyrics, data.referents);
lyricsManager
.fetchLyrics(track.id, track)
.then(async () => {
if (lyricsManager.isGeniusMode) {
try {
const data = await lyricsManager.geniusManager.getDataForTrack(track);
if (data) {
lyricsManager.currentGeniusData = data;
lyricsManager.applyGeniusAnnotations(amLyrics, data.referents);
}
} catch (e) {
console.warn('Genius auto-load failed', e);
}
} catch (e) { console.warn('Genius auto-load failed', e); }
}
}).catch(e => console.warn('Background lyrics fetch failed', e));
}
})
.catch((e) => console.warn('Background lyrics fetch failed', e));
// Wait for lyrics to appear, then do an immediate conversion
const waitForLyrics = () => {
@ -955,20 +977,21 @@ function setupSync(track, audioPlayer, amLyrics, lyricsManager) {
const onLineClick = (e) => {
if (e.detail && e.detail.timestamp !== undefined) {
const manager = lyricsManager || sidePanelManager.panel.lyricsManager;
if (manager && manager.isGeniusMode) {
const timestampSeconds = e.detail.timestamp / 1000;
const lyricsData = manager.lyricsCache.get(track.id);
if (lyricsData && lyricsData.subtitles) {
const parsed = manager.parseSyncedLyrics(lyricsData.subtitles);
const line = parsed.find(l => Math.abs(l.time - timestampSeconds) < 1.0);
const line = parsed.find((l) => Math.abs(l.time - timestampSeconds) < 1.0);
if (line && line.text && manager.currentGeniusData) {
const annotations = manager.geniusManager.findAnnotations(line.text, manager.currentGeniusData.referents);
const annotations = manager.geniusManager.findAnnotations(
line.text,
manager.currentGeniusData.referents
);
showGeniusAnnotations(annotations, line.text);
}
}
@ -1003,13 +1026,12 @@ function setupSync(track, audioPlayer, amLyrics, lyricsManager) {
}
function showGeniusAnnotations(annotations, lineText) {
const existing = document.querySelector('.genius-annotation-modal');
if (existing) existing.remove();
const modal = document.createElement('div');
modal.className = 'genius-annotation-modal';
let contentHtml = `
<div class="genius-modal-content">
<div class="genius-header">
@ -1026,7 +1048,7 @@ function showGeniusAnnotations(annotations, lineText) {
</div>
`;
} else {
annotations.forEach(ann => {
annotations.forEach((ann) => {
const body = ann.annotations[0].body.plain;
contentHtml += `
<div class="annotation-item">
@ -1043,7 +1065,9 @@ function showGeniusAnnotations(annotations, lineText) {
modal.querySelector('.close-genius').addEventListener('click', () => modal.remove());
modal.addEventListener('click', (e) => { if(e.target === modal) modal.remove(); });
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
}
export async function renderLyricsInFullscreen(track, audioPlayer, lyricsManager, container) {

View file

@ -22,22 +22,22 @@ export async function addMetadataToAudio(audioBlob, track, api, quality) {
const buffer = await audioBlob.slice(0, 4).arrayBuffer();
const view = new DataView(buffer);
const isFlac = view.byteLength >= 4 &&
const isFlac =
view.byteLength >= 4 &&
view.getUint8(0) === 0x66 && // f
view.getUint8(1) === 0x4c && // L
view.getUint8(2) === 0x61 && // a
view.getUint8(3) === 0x43; // C
view.getUint8(3) === 0x43; // C
const mime = audioBlob.type;
const mime = audioBlob.type;
if (mime === 'audio/flac') {
return await addFlacMetadata(audioBlob, track, api);
}
if (mime === 'audio/mp4') {
return await addM4aMetadata(audioBlob, track, api);
}
}
/**

View file

@ -319,7 +319,10 @@ export class Player {
}
streamUrl = track.audioUrl;
if ((!streamUrl || (typeof streamUrl === 'string' && streamUrl.startsWith('blob:'))) && track.remoteUrl) {
if (
(!streamUrl || (typeof streamUrl === 'string' && streamUrl.startsWith('blob:'))) &&
track.remoteUrl
) {
streamUrl = track.remoteUrl;
}

View file

@ -11,7 +11,6 @@ export function navigate(path) {
export function createRouter(ui) {
const router = async () => {
if (window.location.hash && window.location.hash.length > 1) {
const hash = window.location.hash.substring(1);
if (hash.includes('/')) {
@ -21,7 +20,7 @@ export function createRouter(ui) {
}
let path = window.location.pathname;
if (path.startsWith('/')) path = path.substring(1);
if (path.endsWith('/')) path = path.substring(0, path.length - 1);
if (path === '' || path === 'index.html') path = 'home';

View file

@ -626,7 +626,7 @@ export const visualizerSettings = {
getSensitivity() {
try {
const val = localStorage.getItem(this.SENSITIVITY_KEY);
if (val === null) return 1.0;
if (val === null) return 1.0;
return parseFloat(val);
} catch {
return 1.0;

View file

@ -10,14 +10,20 @@ async function loadArtistsData() {
const response = await fetch('./artists.ndjson');
if (!response.ok) throw new Error('Network response was not ok');
const text = await response.text();
artistsData = text.trim().split('\n')
.filter(line => line.trim())
.map(line => {
try { return JSON.parse(line); } catch (e) { return null; }
artistsData = text
.trim()
.split('\n')
.filter((line) => line.trim())
.map((line) => {
try {
return JSON.parse(line);
} catch (e) {
return null;
}
})
.filter(item => item !== null);
.filter((item) => item !== null);
} catch (e) {
console.error("Failed to load Artists LIst:", e);
console.error('Failed to load Artists LIst:', e);
}
}
@ -29,11 +35,13 @@ function getSheetId(url) {
async function fetchTrackerData(sheetId) {
try {
const response = await fetch(`https://corsproxy.io/?${encodeURIComponent(`https://tracker.israeli.ovh/get/${sheetId}`)}`);
const response = await fetch(
`https://corsproxy.io/?${encodeURIComponent(`https://tracker.israeli.ovh/get/${sheetId}`)}`
);
if (!response.ok) return null;
return await response.json();
} catch (e) {
console.error("Failed to fetch tracker data", e);
console.error('Failed to fetch tracker data', e);
return null;
}
}
@ -62,21 +70,21 @@ function getDirectUrl(rawUrl) {
function renderLoadButton(container, sheetId, artistName) {
container.innerHTML = '';
container.style.display = 'block';
const wrapper = document.createElement('div');
wrapper.style.textAlign = 'center';
wrapper.style.padding = '2rem';
const button = document.createElement('button');
button.className = 'btn-primary';
button.textContent = 'Load Unreleased Projects';
button.style.fontSize = '1.1rem';
button.style.padding = '1rem 2rem';
button.onclick = async () => {
button.textContent = 'Loading...';
button.disabled = true;
const trackerData = await fetchTrackerData(sheetId);
if (trackerData) {
renderTracker(trackerData, container, artistName);
@ -88,7 +96,7 @@ function renderLoadButton(container, sheetId, artistName) {
}, 2000);
}
};
wrapper.appendChild(button);
container.appendChild(wrapper);
}
@ -100,7 +108,7 @@ function renderTracker(trackerData, container, artistName) {
Unreleased Songs & Info Provided By <a href="https://artistgrid.cx" target="_blank" style="text-decoration: underline;">ArtistGrid</a>. Consider Donating to Them.
</p>
`;
const erasContainer = document.createElement('div');
erasContainer.className = 'card-grid';
erasContainer.style.opacity = '0';
@ -110,11 +118,11 @@ function renderTracker(trackerData, container, artistName) {
if (!trackerData.eras) return;
Object.values(trackerData.eras).forEach(era => {
Object.values(trackerData.eras).forEach((era) => {
const card = document.createElement('div');
card.className = 'card';
card.style.cursor = 'pointer';
const imgWrapper = document.createElement('div');
imgWrapper.className = 'card-image-wrapper';
@ -123,13 +131,13 @@ function renderTracker(trackerData, container, artistName) {
img.src = era.image ? `https://corsproxy.io/?${encodeURIComponent(era.image)}` : 'assets/logo.svg';
img.alt = era.name;
img.loading = 'lazy';
imgWrapper.appendChild(img);
const title = document.createElement('div');
title.className = 'card-title';
title.textContent = era.name;
const subtitle = document.createElement('div');
subtitle.className = 'card-subtitle';
subtitle.textContent = era.timeline || 'Unreleased';
@ -137,9 +145,9 @@ function renderTracker(trackerData, container, artistName) {
card.appendChild(imgWrapper);
card.appendChild(title);
card.appendChild(subtitle);
card.onclick = () => showEraSongs(era, artistName);
erasContainer.appendChild(card);
});
@ -153,7 +161,6 @@ function showEraSongs(era, artistName) {
const modal = document.getElementById('tracker-modal');
const overlay = modal.querySelector('.modal-overlay');
const closeBtn = document.getElementById('close-tracker-modal');
const img = document.getElementById('tracker-header-image');
const title = document.getElementById('tracker-header-title');
@ -166,7 +173,7 @@ function showEraSongs(era, artistName) {
const trackList = document.getElementById('tracker-tracklist');
const filterContainer = document.getElementById('tracker-filters');
filterContainer.innerHTML = '';
while (trackList.lastElementChild && !trackList.lastElementChild.classList.contains('track-list-header')) {
trackList.removeChild(trackList.lastElementChild);
@ -178,15 +185,15 @@ function showEraSongs(era, artistName) {
{ label: 'Special', emoji: '✨' },
{ label: 'Grails', emoji: '🏆' },
{ label: 'Wanted', emoji: '🥇' },
{ label: 'Worst Of', emoji: '🗑️' }
{ label: 'Worst Of', emoji: '🗑️' },
];
let activeFilter = '';
const applyFilter = () => {
const items = trackList.querySelectorAll('.track-item');
items.forEach(item => {
items.forEach((item) => {
const titleEl = item.querySelector('.title');
if (titleEl) {
const title = titleEl.textContent.trim();
@ -199,37 +206,37 @@ function showEraSongs(era, artistName) {
});
const categories = trackList.querySelectorAll('h4');
categories.forEach(cat => {
categories.forEach((cat) => {
let next = cat.nextElementSibling;
let hasVisibleItems = false;
while(next && next.tagName !== 'H4') {
while (next && next.tagName !== 'H4') {
if (next.classList.contains('track-item') && next.style.display !== 'none') {
hasVisibleItems = true;
break;
}
next = next.nextElementSibling;
}
cat.style.display = hasVisibleItems ? 'block' : 'none';
});
};
filters.forEach(filter => {
filters.forEach((filter) => {
const btn = document.createElement('button');
btn.className = 'btn-secondary';
btn.textContent = filter.emoji ? `${filter.emoji} ${filter.label}` : filter.label;
btn.style.fontSize = '0.85rem';
btn.style.padding = '0.4rem 0.8rem';
btn.style.borderRadius = '2rem';
if (filter.emoji === '') {
btn.style.backgroundColor = 'var(--primary)';
btn.style.color = 'var(--primary-foreground)';
btn.style.backgroundColor = 'var(--primary)';
btn.style.color = 'var(--primary-foreground)';
}
btn.onclick = () => {
Array.from(filterContainer.children).forEach(b => {
Array.from(filterContainer.children).forEach((b) => {
b.style.backgroundColor = '';
b.style.color = '';
});
@ -248,7 +255,7 @@ function showEraSongs(era, artistName) {
if (era.data) {
Object.entries(era.data).forEach(([category, songs]) => {
if (!songs || songs.length === 0) return;
const catTitle = document.createElement('h4');
catTitle.textContent = category;
catTitle.style.padding = '1rem 0.5rem 0.5rem';
@ -260,10 +267,10 @@ function showEraSongs(era, artistName) {
const isValidUrl = (u) => u && typeof u === 'string' && u.trim().length > 0;
songs.forEach(song => {
songs.forEach((song) => {
const trackItem = document.createElement('div');
trackItem.className = 'track-item';
trackItem.innerHTML = `
<div class="track-number">${globalIndex++}</div>
<div class="track-item-info">
@ -287,27 +294,40 @@ function showEraSongs(era, artistName) {
e.preventDefault();
const contextMenu = document.getElementById('context-menu');
if (contextMenu) {
const rawUrl = (isValidUrl(song.url) ? song.url : null) || (song.urls ? song.urls.find(isValidUrl) : null);
const rawUrl =
(isValidUrl(song.url) ? song.url : null) || (song.urls ? song.urls.find(isValidUrl) : null);
const directUrl = getDirectUrl(rawUrl);
const track = {
id: `tracker-${song.name}`,
title: song.name,
artist: { name: artistName || document.getElementById('artist-detail-name')?.textContent || 'Unknown Artist' },
artists: [{ name: artistName || document.getElementById('artist-detail-name')?.textContent || 'Unknown Artist' }],
artist: {
name:
artistName ||
document.getElementById('artist-detail-name')?.textContent ||
'Unknown Artist',
},
artists: [
{
name:
artistName ||
document.getElementById('artist-detail-name')?.textContent ||
'Unknown Artist',
},
],
album: {
title: era.name,
cover: era.image
cover: era.image,
},
duration: parseDuration(song.track_length),
isTracker: true,
audioUrl: directUrl,
remoteUrl: directUrl
remoteUrl: directUrl,
};
contextMenu._contextTrack = track;
['go-to-album', 'go-to-artist', 'toggle-like', 'download', 'track-mix'].forEach(action => {
['go-to-album', 'go-to-artist', 'toggle-like', 'download', 'track-mix'].forEach((action) => {
const item = contextMenu.querySelector(`[data-action="${action}"]`);
if (item) item.style.display = 'none';
});
@ -326,7 +346,7 @@ function showEraSongs(era, artistName) {
if (hasValidUrl) {
trackItem.onclick = async () => {
if (song.track_length === '-') {
const targetUrl = (song.urls && song.urls.length > 0) ? song.urls[0] : song.url;
const targetUrl = song.urls && song.urls.length > 0 ? song.urls[0] : song.url;
if (targetUrl) window.open(targetUrl, '_blank');
return;
}
@ -337,8 +357,9 @@ function showEraSongs(era, artistName) {
trackItem.classList.add('loading');
const trackNumEl = trackItem.querySelector('.track-number');
const originalNum = trackNumEl.textContent;
trackNumEl.innerHTML = '<svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle></svg>';
trackNumEl.innerHTML =
'<svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle></svg>';
let urlsToTry = [];
if (isValidUrl(song.url)) {
urlsToTry.push(song.url);
@ -349,12 +370,12 @@ function showEraSongs(era, artistName) {
let audioUrl = null;
let successfulUrl = null;
for (let rawUrl of urlsToTry) {
console.log(`Trying: ${rawUrl}`);
let downloadUrl = rawUrl;
if (rawUrl.includes('pillows.su/f/')) {
const match = rawUrl.match(/pillows\.su\/f\/([a-f0-9]+)/);
if (match) {
@ -370,12 +391,14 @@ function showEraSongs(era, artistName) {
try {
console.log(`Fetching: ${downloadUrl}`);
const response = await fetch(downloadUrl);
if (response.ok) {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('audio/') ||
if (
contentType.includes('audio/') ||
contentType.includes('mpeg') ||
contentType.includes('octet-stream')) {
contentType.includes('octet-stream')
) {
const arrayBuffer = await response.arrayBuffer();
if (arrayBuffer.byteLength > 1000) {
const blob = new Blob([arrayBuffer], { type: 'audio/mpeg' });
@ -394,26 +417,43 @@ function showEraSongs(era, artistName) {
document.body.style.cursor = 'default';
trackItem.classList.remove('loading');
trackNumEl.textContent = originalNum;
if (!audioUrl) {
alert(`Unable to load this track! :( The source may be unavailable.\n\nTried ${urlsToTry.length} URL(s)`);
alert(
`Unable to load this track! :( The source may be unavailable.\n\nTried ${urlsToTry.length} URL(s)`
);
return;
}
if (globalPlayer) {
const track = {
id: `tracker-${song.name}`,
title: song.name,
artist: { name: artistName || document.getElementById('artist-detail-name')?.textContent || 'Unknown Artist' },
artists: [{ name: artistName || document.getElementById('artist-detail-name')?.textContent || 'Unknown Artist' }],
artist: {
name:
artistName ||
document.getElementById('artist-detail-name')?.textContent ||
'Unknown Artist',
},
artists: [
{
name:
artistName ||
document.getElementById('artist-detail-name')?.textContent ||
'Unknown Artist',
},
],
album: {
title: era.name,
cover: era.image
cover: era.image,
},
duration: parseDuration(song.track_length),
isTracker: true,
audioUrl: audioUrl,
remoteUrl: successfulUrl || (urlsToTry.length > 0 ? getDirectUrl(urlsToTry[0]) : null) || getDirectUrl(song.url)
remoteUrl:
successfulUrl ||
(urlsToTry.length > 0 ? getDirectUrl(urlsToTry[0]) : null) ||
getDirectUrl(song.url),
};
globalPlayer.setQueue([track], 0);
@ -455,18 +495,18 @@ export async function initTracker(player, ui) {
const observer = new MutationObserver(async () => {
const artistNameEl = document.getElementById('artist-detail-name');
const trackerSection = document.getElementById('artist-tracker-section');
if (artistNameEl && trackerSection && artistNameEl.textContent) {
const artistName = artistNameEl.textContent.trim();
if (trackerSection.dataset.artist === artistName) return;
trackerSection.dataset.artist = artistName;
trackerSection.innerHTML = '';
trackerSection.style.display = 'none';
const artistEntry = artistsData.find(a => a.name.toLowerCase() === artistName.toLowerCase());
const artistEntry = artistsData.find((a) => a.name.toLowerCase() === artistName.toLowerCase());
if (artistEntry && artistEntry.url) {
const sheetId = getSheetId(artistEntry.url);
if (sheetId) {
@ -480,4 +520,4 @@ export async function initTracker(player, ui) {
if (artistPage) {
observer.observe(artistPage, { attributes: true, childList: true, subtree: true });
}
}
}

View file

@ -414,7 +414,7 @@ export function initializeUIInteractions(player, api, ui) {
if (sidePanelManager.isActive('queue')) {
refreshQueuePanel();
}
const overlay = document.getElementById('fullscreen-cover-overlay');
if (overlay && getComputedStyle(overlay).display !== 'none') {
ui.updateFullscreenMetadata(player.currentTrack, player.getNextTrack());

126
js/ui.js
View file

@ -685,7 +685,7 @@ export class UIRenderer {
const nextTrackEl = document.getElementById('fullscreen-next-track');
const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280');
const fsLikeBtn = document.getElementById('fs-like-btn');
if (fsLikeBtn) {
this.updateLikeState(fsLikeBtn.parentElement, 'track', track.id);
@ -787,7 +787,7 @@ export class UIRenderer {
closeFullscreenCover() {
const overlay = document.getElementById('fullscreen-cover-overlay');
overlay.style.display = 'none';
const playerBar = document.querySelector('.now-playing-bar');
if (playerBar) playerBar.style.removeProperty('display');
@ -824,34 +824,38 @@ export class UIRenderer {
lastPausedState = isPaused;
if (isPaused) {
playBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>';
playBtn.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>';
} else {
playBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
playBtn.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
}
};
updatePlayBtn();
playBtn.onclick = () => {
this.player.handlePlayPause();
updatePlayBtn();
};
prevBtn.onclick = () => this.player.playPrev();
nextBtn.onclick = () => this.player.playNext();
shuffleBtn.onclick = () => {
this.player.toggleShuffle();
shuffleBtn.classList.toggle('active', this.player.shuffleActive);
};
repeatBtn.onclick = () => {
const mode = this.player.toggleRepeat();
repeatBtn.classList.toggle('active', mode !== 0);
if (mode === 2) {
repeatBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/><path d="M11 10h1v4"/></svg>';
repeatBtn.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/><path d="M11 10h1v4"/></svg>';
} else {
repeatBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>';
repeatBtn.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>';
}
};
@ -883,26 +887,27 @@ export class UIRenderer {
const mode = this.player.repeatMode;
repeatBtn.classList.toggle('active', mode !== 0);
if (mode === 2) {
repeatBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/><path d="M11 10h1v4"/></svg>';
repeatBtn.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/><path d="M11 10h1v4"/></svg>';
}
const update = () => {
if (document.getElementById('fullscreen-cover-overlay').style.display === 'none') return;
const duration = audioPlayer.duration || 0;
const current = audioPlayer.currentTime || 0;
if (duration > 0) {
const percent = (current / duration) * 100;
progressFill.style.width = `${percent}%`;
currentTimeEl.textContent = formatTime(current);
totalDurationEl.textContent = formatTime(duration);
}
updatePlayBtn();
this.fullscreenUpdateInterval = requestAnimationFrame(update);
};
if (this.fullscreenUpdateInterval) cancelAnimationFrame(this.fullscreenUpdateInterval);
this.fullscreenUpdateInterval = requestAnimationFrame(update);
}
@ -913,7 +918,10 @@ export class UIRenderer {
});
document.querySelectorAll('.sidebar-nav a').forEach((link) => {
link.classList.toggle('active', link.pathname === `/${pageId}` || (pageId === 'home' && link.pathname === '/'));
link.classList.toggle(
'active',
link.pathname === `/${pageId}` || (pageId === 'home' && link.pathname === '/')
);
});
document.querySelector('.main-content').scrollTop = 0;
@ -939,7 +947,7 @@ export class UIRenderer {
const yearEl = document.getElementById('track-detail-year');
const albumSection = document.getElementById('track-album-section');
const albumTracksContainer = document.getElementById('track-detail-album-tracks');
const playBtn = document.getElementById('play-track-btn');
const lyricsBtn = document.getElementById('track-lyrics-btn');
const shareBtn = document.getElementById('share-track-btn');
@ -957,11 +965,11 @@ export class UIRenderer {
try {
const trackData = await this.api.getTrack(trackId);
const track = trackData.track;
const coverUrl = this.api.getCoverUrl(track.album?.cover);
imageEl.src = coverUrl;
imageEl.style.backgroundColor = '';
this.setPageBackground(coverUrl);
if (backgroundSettings.isEnabled() && track.album?.cover) {
this.extractAndApplyColor(this.api.getCoverUrl(track.album.cover, '80'));
@ -974,7 +982,7 @@ export class UIRenderer {
artistEl.innerHTML = `<a href="/artist/${track.artist.id}">${escapeHtml(track.artist.name)}</a>`;
albumEl.innerHTML = `<a href="/album/${track.album.id}">${escapeHtml(track.album.title)}</a>`;
if (track.album.releaseDate) {
const date = new Date(track.album.releaseDate);
yearEl.textContent = date.getFullYear();
@ -1004,7 +1012,7 @@ export class UIRenderer {
this.updateLikeState(likeBtn, 'track', track.id);
trackDataStore.set(likeBtn, track);
downloadBtn.dataset.action = 'download';
downloadBtn.classList.add('track-action-btn');
trackDataStore.set(downloadBtn, track);
@ -1012,10 +1020,10 @@ export class UIRenderer {
if (track.album.id) {
const albumData = await this.api.getAlbum(track.album.id);
const tracks = albumData.tracks;
if (tracks.length > 1) {
albumSection.style.display = 'block';
const otherTracks = tracks.filter(t => t.id !== track.id);
const otherTracks = tracks.filter((t) => t.id !== track.id);
this.renderListWithTracks(albumTracksContainer, otherTracks, false);
} else {
albumSection.style.display = 'none';
@ -1121,7 +1129,7 @@ export class UIRenderer {
const folders = await db.getFolders();
if (foldersContainer) {
foldersContainer.innerHTML = folders.map(f => this.createFolderCardHTML(f)).join('');
foldersContainer.innerHTML = folders.map((f) => this.createFolderCardHTML(f)).join('');
foldersContainer.style.display = folders.length ? 'grid' : 'none';
}
@ -1965,7 +1973,7 @@ export class UIRenderer {
removeBtn.innerHTML =
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>';
removeBtn.dataset.trackId = currentTracks[index].id;
const menuBtn = actionsDiv.querySelector('.track-menu-btn');
actionsDiv.insertBefore(removeBtn, menuBtn);
});
@ -1990,7 +1998,9 @@ export class UIRenderer {
} else if (sortType === 'added-oldest') {
currentTracks = [...originalTracks].sort((a, b) => (a.addedAt || 0) - (b.addedAt || 0));
} else if (sortType === 'title') {
currentTracks = [...originalTracks].sort((a, b) => (a.title || '').localeCompare(b.title || ''));
currentTracks = [...originalTracks].sort((a, b) =>
(a.title || '').localeCompare(b.title || '')
);
} else if (sortType === 'artist') {
currentTracks = [...originalTracks].sort((a, b) => {
const artistA = a.artist?.name || a.artists?.[0]?.name || '';
@ -2015,7 +2025,13 @@ export class UIRenderer {
}
// Render Actions (Shuffle, Edit, Delete, Share, Sort)
this.updatePlaylistHeaderActions(playlistData, !!ownedPlaylist, tracks, false, !!ownedPlaylist ? applySort : null);
this.updatePlaylistHeaderActions(
playlistData,
!!ownedPlaylist,
tracks,
false,
ownedPlaylist ? applySort : null
);
playBtn.onclick = () => {
this.player.setQueue(currentTracks, 0);
@ -2137,9 +2153,11 @@ export class UIRenderer {
if (!folder) throw new Error('Folder not found');
imageEl.src = folder.cover || '/assets/folder.png';
imageEl.onerror = () => { imageEl.src = '/assets/folder.png'; };
imageEl.onerror = () => {
imageEl.src = '/assets/folder.png';
};
imageEl.style.backgroundColor = '';
titleEl.textContent = folder.name;
metaEl.textContent = `Created ${new Date(folder.createdAt).toLocaleDateString()}`;
@ -2467,7 +2485,13 @@ export class UIRenderer {
const actionsDiv = document.getElementById('page-playlist').querySelector('.detail-header-actions');
// Cleanup existing dynamic buttons
['shuffle-playlist-btn', 'edit-playlist-btn', 'delete-playlist-btn', 'share-playlist-btn', 'sort-playlist-btn'].forEach((id) => {
[
'shuffle-playlist-btn',
'edit-playlist-btn',
'delete-playlist-btn',
'share-playlist-btn',
'sort-playlist-btn',
].forEach((id) => {
const btn = actionsDiv.querySelector(`#${id}`);
if (btn) btn.remove();
});
@ -2493,11 +2517,11 @@ export class UIRenderer {
sortBtn.className = 'btn-secondary';
sortBtn.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg><span>Sort</span>';
sortBtn.onclick = (e) => {
e.stopPropagation();
const menu = document.getElementById('sort-menu');
const rect = sortBtn.getBoundingClientRect();
menu.style.top = `${rect.bottom + 5}px`;
menu.style.left = `${rect.left}px`;
@ -2517,7 +2541,7 @@ export class UIRenderer {
};
menu.onclick = handleSort;
setTimeout(() => document.addEventListener('click', closeMenu), 0);
};
fragment.appendChild(sortBtn);
@ -2776,7 +2800,8 @@ export class UIRenderer {
document.body.classList.add('sidebar-collapsed');
const toggleBtn = document.getElementById('sidebar-toggle');
if (toggleBtn) {
toggleBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>';
toggleBtn.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>';
}
const imageEl = document.getElementById('track-detail-image');
@ -2788,7 +2813,7 @@ export class UIRenderer {
const albumTracksContainer = document.getElementById('track-detail-album-tracks');
const similarSection = document.getElementById('track-similar-section');
const similarTracksContainer = document.getElementById('track-detail-similar-tracks');
const playBtn = document.getElementById('play-track-btn');
const lyricsBtn = document.getElementById('track-lyrics-btn');
const shareBtn = document.getElementById('share-track-btn');
@ -2813,11 +2838,11 @@ export class UIRenderer {
try {
const track = await this.api.getTrackMetadata(trackId);
const coverUrl = this.api.getCoverUrl(track.album?.cover);
imageEl.src = coverUrl;
imageEl.style.backgroundColor = '';
this.setPageBackground(coverUrl);
if (backgroundSettings.isEnabled() && track.album?.cover) {
this.extractAndApplyColor(this.api.getCoverUrl(track.album.cover, '80'));
@ -2852,7 +2877,7 @@ export class UIRenderer {
const date = new Date(track.album.releaseDate);
yearEl.textContent = date.getFullYear();
}
if (track.copyright || track.album.copyright) {
yearEl.textContent += `${track.copyright || track.album.copyright}`;
}
@ -2882,7 +2907,7 @@ export class UIRenderer {
this.updateLikeState(likeBtn, 'track', track.id);
trackDataStore.set(likeBtn, track);
downloadBtn.dataset.action = 'download';
downloadBtn.classList.add('track-action-btn');
trackDataStore.set(downloadBtn, track);
@ -2893,7 +2918,7 @@ export class UIRenderer {
const tracks = albumData.tracks;
if (tracks.length > 1) {
albumSection.style.display = 'block';
const otherTracks = tracks.filter(t => t.id != track.id);
const otherTracks = tracks.filter((t) => t.id != track.id);
this.renderListWithTracks(albumTracksContainer, otherTracks, false, false, true);
}
} catch (err) {
@ -2901,14 +2926,17 @@ export class UIRenderer {
}
}
this.api.getRecommendedTracksForPlaylist([track], 5).then(similarTracks => {
if (similarTracks.length > 0) {
this.renderListWithTracks(similarTracksContainer, similarTracks, true);
similarSection.style.display = 'block';
} else {
similarSection.style.display = 'none';
}
}).catch(() => similarSection.style.display = 'none');
this.api
.getRecommendedTracksForPlaylist([track], 5)
.then((similarTracks) => {
if (similarTracks.length > 0) {
this.renderListWithTracks(similarTracksContainer, similarTracks, true);
similarSection.style.display = 'block';
} else {
similarSection.style.display = 'none';
}
})
.catch(() => (similarSection.style.display = 'none'));
document.title = `${displayTitle} - ${artistName}`;
} catch (e) {

View file

@ -285,10 +285,14 @@ function resizeImageBlob(blob, size) {
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, size, size);
canvas.toBlob((resizedBlob) => {
if (resizedBlob) resolve(resizedBlob);
else reject(new Error('Canvas toBlob failed'));
}, blob.type || 'image/jpeg', 0.9);
canvas.toBlob(
(resizedBlob) => {
if (resizedBlob) resolve(resizedBlob);
else reject(new Error('Canvas toBlob failed'));
},
blob.type || 'image/jpeg',
0.9
);
};
img.onerror = (e) => {
URL.revokeObjectURL(url);
@ -309,18 +313,18 @@ export async function getCoverBlob(api, coverId) {
if (sizeStr.includes('x')) {
sizeStr = sizeStr.split('x')[0];
}
let requestedSize = parseInt(sizeStr, 10);
if (isNaN(requestedSize) || requestedSize <= 0) requestedSize = 1280;
const cacheKey = `${coverId}-${requestedSize}`;
if (coverCache.has(cacheKey)) return coverCache.get(cacheKey);
// Tidal seems to only support these soooo
// Tidal seems to only support these soooo
const supportedSizes = [80, 160, 320, 640, 1280];
let fetchSize = 1280;
const bestSize = supportedSizes.find(s => s >= requestedSize);
const bestSize = supportedSizes.find((s) => s >= requestedSize);
if (bestSize) {
fetchSize = bestSize;
}

View file

@ -11,7 +11,7 @@ export class Visualizer {
this.isActive = false;
this.animationId = null;
this.particles = [];
this.kick = 0;
this.lastIntensity = 0;
this.lastBeatTime = 0;
@ -33,7 +33,7 @@ export class Visualizer {
this.source.connect(this.analyser);
this.analyser.connect(this.audioContext.destination);
} catch (e) {
console.warn("Visualizer init failed (likely CORS or already connected):", e);
console.warn('Visualizer init failed (likely CORS or already connected):', e);
}
}
@ -45,11 +45,11 @@ export class Visualizer {
if (this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
this.resize();
window.addEventListener('resize', this.resizeBound);
this.canvas.style.display = 'block';
this.particles = [];
this.energyAverage = 0.3;
this.kick = 0;
@ -61,7 +61,7 @@ export class Visualizer {
this.isActive = false;
if (this.animationId) cancelAnimationFrame(this.animationId);
window.removeEventListener('resize', this.resizeBound);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.canvas.style.display = 'none';
}
@ -93,15 +93,12 @@ export class Visualizer {
for (let i = 0; i < 4; i++) bassSum += dataArray[i];
const bass = bassSum / 4 / 255;
const intensity = bass * bass;
const intensity = bass * bass;
this.energyAverage = this.energyAverage * 0.99 + intensity * 0.01;
this.upbeatSmoother = this.upbeatSmoother * 0.92 + intensity * 0.08;
if (visualizerSettings.isSmartIntensityEnabled()) {
let target = 0.1;
if (this.energyAverage > 0.4) {
@ -113,18 +110,14 @@ export class Visualizer {
sensitivity = target;
}
let threshold = 0.5;
if (this.energyAverage < 0.3) {
threshold = 0.5 + (0.3 - this.energyAverage) * 2;
}
const now = Date.now();
if (intensity > threshold) {
if (intensity > this.lastIntensity + 0.05 && now - this.lastBeatTime > 50) {
this.kick = 1.0;
this.lastBeatTime = now;
@ -145,7 +138,6 @@ export class Visualizer {
}
this.lastIntensity = intensity;
let shakeX = 0;
let shakeY = 0;
if (this.kick > 0.1) {
@ -154,8 +146,8 @@ export class Visualizer {
shakeY = (Math.random() - 0.5) * shakeAmt;
}
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#ffffff';
const primaryColor =
getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#ffffff';
const particleCount = 180;
if (this.particles.length !== particleCount) {
@ -167,22 +159,21 @@ export class Visualizer {
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
size: Math.random() * 3 + 1,
baseSize: Math.random() * 3 + 1
baseSize: Math.random() * 3 + 1,
});
}
}
ctx.save();
ctx.translate(shakeX, shakeY);
ctx.fillStyle = primaryColor;
ctx.strokeStyle = primaryColor;
for (let i = 0; i < this.particles.length; i++) {
let p = this.particles[i];
const speedMult = 1 + (intensity * 2) + this.kick * 8 * sensitivity;
const speedMult = 1 + intensity * 2 + this.kick * 8 * sensitivity;
p.x += p.vx * speedMult;
p.y += p.vy * speedMult;
@ -196,9 +187,8 @@ export class Visualizer {
if (p.y < 0) p.y = h;
if (p.y > h) p.y = 0;
const size = p.baseSize * (1 + intensity * 0.5 + this.kick * 0.8 * sensitivity);
ctx.globalAlpha = 0.4 + (intensity * 0.2) + this.kick * 0.15 * sensitivity;
ctx.globalAlpha = 0.4 + intensity * 0.2 + this.kick * 0.15 * sensitivity;
ctx.beginPath();
ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
ctx.fill();
@ -207,8 +197,8 @@ export class Visualizer {
const p2 = this.particles[j];
const dx = p.x - p2.x;
const dy = p.y - p2.y;
const distSq = dx*dx + dy*dy;
const distSq = dx * dx + dy * dy;
const maxDist = 150 + intensity * 50 + this.kick * 50 * sensitivity;
const maxDistSq = maxDist * maxDist;
@ -225,4 +215,4 @@ export class Visualizer {
}
ctx.restore();
}
}
}

View file

@ -2076,7 +2076,7 @@ input:checked + .slider::before {
.fullscreen-progress-container .progress-bar {
flex: 1;
height: 6px;
background: rgba(255, 255, 255, 0.2);
background: rgb(255, 255, 255, 0.2);
border-radius: 3px;
cursor: pointer;
position: relative;
@ -2110,7 +2110,7 @@ input:checked + .slider::before {
}
.fullscreen-buttons button:hover {
background: rgba(255, 255, 255, 0.1);
background: rgb(255, 255, 255, 0.1);
transform: scale(1.1);
}
@ -3193,7 +3193,7 @@ img[src=''] {
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6rem 2rem 2rem 2rem;
padding: 6rem 2rem 2rem;
transition: flex 0.3s ease;
}
@ -3627,7 +3627,7 @@ img[src=''] {
padding: var(--spacing-lg);
}
.now-playing-bar {
.now-playing-bar {
width: calc(96% - 160px) !important;
left: calc(160px + 2%);
}
@ -4348,7 +4348,6 @@ body:has(#fullscreen-cover-overlay:not([style*='display: none'])) .now-playing-b
text-overflow: ellipsis;
}
/* Genius i love genius brah!! */
.genius-annotation-modal {
position: fixed;
@ -4357,7 +4356,7 @@ body:has(#fullscreen-cover-overlay:not([style*='display: none'])) .now-playing-b
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
background: rgb(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
animation: fade-in 0.2s ease;
}
@ -4424,14 +4423,14 @@ body:has(#fullscreen-cover-overlay:not([style*='display: none'])) .now-playing-b
}
.genius-annotated {
background-color: rgba(255, 255, 100, 0.1);
background-color: rgb(255, 255, 100, 0.1);
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
}
.genius-annotated:hover {
background-color: rgba(255, 255, 100, 0.2);
background-color: rgb(255, 255, 100, 0.2);
}
.genius-multi-start {
@ -4491,7 +4490,7 @@ body.sidebar-collapsed #sidebar-toggle {
#page-track .detail-header-image {
width: 350px;
height: 350px;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.5);
box-shadow: 0 30px 60px rgb(0, 0, 0, 0.5);
}
#page-track .detail-header-info {
@ -4526,12 +4525,12 @@ body.sidebar-collapsed #sidebar-toggle {
width: 250px;
height: 250px;
}
#page-track .detail-header-info .title {
font-size: 2rem;
}
}
.tracker-modal .track-item.loading {
opacity: 0.7;
pointer-events: none;
@ -4608,4 +4607,4 @@ body.sidebar-collapsed #sidebar-toggle {
overflow-y: auto;
display: block;
}
}
}