From 648e47e1d89b82af82b59fe6e646d4ea9cbeb8dc Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:05:29 -0500 Subject: [PATCH] fix(linting): fix js linting issues --- functions/about/index.js | 2 +- functions/donate/index.js | 2 +- functions/library/index.js | 2 +- functions/parties/index.js | 2 +- functions/podcasts/[id].js | 2 +- functions/recent/index.js | 2 +- functions/settings/index.js | 2 +- .../unreleased/[sheetId]/[projectName].js | 6 +- functions/unreleased/index.js | 2 +- js/HiFi.test.ts | 45 +- js/HiFi.ts | 11 +- js/ModernSettings.ts | 12 +- js/accounts/auth.js | 2 +- js/accounts/pocketbase.js | 11 +- js/api.js | 12 +- js/api.test.ts | 43 +- js/app.js | 165 +++---- js/bulk-download-writer.ts | 10 +- js/cache.js | 2 +- js/commandPalette.js | 43 +- js/container-classes.ts | 3 +- js/dash-downloader.ts | 4 +- js/db.js | 1 - js/doTimed.ts | 38 +- js/downloads.js | 28 +- js/events.js | 334 +++++++------- js/ffmpeg.js | 2 +- js/ffmpeg.test.ts | 4 +- js/ffmpegFormats.ts | 9 +- js/global.d.ts | 4 + js/indexedIterator.ts | 16 + js/lastfm.js | 8 +- js/librefm.js | 8 +- js/listenbrainz.js | 8 +- js/listening-party.js | 72 +-- js/lyrics.js | 17 +- js/maloja.js | 8 +- js/metadata.js | 2 +- js/metadata.mp4.js | 4 +- js/multi-scrobbler.js | 36 +- js/music-api.js | 57 ++- js/player.js | 236 +++++----- js/playlist-importer.js | 4 +- js/profile.js | 258 ++++++----- js/progressEvents.ts | 4 +- js/settings.js | 57 +-- js/side-panel.js | 10 +- js/storage.js | 8 +- js/taglib.ts | 28 +- js/taglib.types.ts | 1 - js/taglib.worker.ts | 24 +- js/themeStore.js | 21 +- js/tracker.js | 2 +- js/ui-interactions.js | 60 +-- js/ui.js | 421 ++++++++++-------- js/utils.js | 2 +- js/visualizer.js | 4 +- js/visualizers/butterchurn.js | 6 +- js/visualizers/kawarp.js | 2 +- js/visualizers/particles.js | 2 +- test-search.js | 2 +- vite-plugin-auth-gate.js | 1 - vite-plugin-blob.ts | 2 +- vite-plugin-svg-use.ts | 41 +- vite.config.ts | 2 +- 65 files changed, 1202 insertions(+), 1037 deletions(-) create mode 100644 js/indexedIterator.ts diff --git a/functions/about/index.js b/functions/about/index.js index 6ebf029..463b901 100644 --- a/functions/about/index.js +++ b/functions/about/index.js @@ -1,5 +1,5 @@ export async function onRequest(context) { - const { request, env } = context; + const { request } = context; const pageUrl = request.url; const metaHtml = ` diff --git a/functions/donate/index.js b/functions/donate/index.js index 7e11bf4..93e334b 100644 --- a/functions/donate/index.js +++ b/functions/donate/index.js @@ -1,5 +1,5 @@ export async function onRequest(context) { - const { request, env } = context; + const { request } = context; const pageUrl = request.url; const metaHtml = ` diff --git a/functions/library/index.js b/functions/library/index.js index 14a4180..5ed5c78 100644 --- a/functions/library/index.js +++ b/functions/library/index.js @@ -1,5 +1,5 @@ export async function onRequest(context) { - const { request, env } = context; + const { request } = context; const pageUrl = request.url; const metaHtml = ` diff --git a/functions/parties/index.js b/functions/parties/index.js index 8087f1a..db05542 100644 --- a/functions/parties/index.js +++ b/functions/parties/index.js @@ -1,5 +1,5 @@ export async function onRequest(context) { - const { request, env } = context; + const { request } = context; const pageUrl = request.url; const metaHtml = ` diff --git a/functions/podcasts/[id].js b/functions/podcasts/[id].js index 25586fe..40fbe98 100644 --- a/functions/podcasts/[id].js +++ b/functions/podcasts/[id].js @@ -50,7 +50,7 @@ export async function onRequest(context) { const title = feed.title; const author = feed.author || feed.ownerName || ''; const episodeCount = feed.episodeCount || 0; - const rawDescription = feed.description || ''; + const _rawDescription = feed.description || ''; const description = author ? `Podcast by ${author} • ${episodeCount} Episodes\nListen on Monochrome` : `Podcast • ${episodeCount} Episodes\nListen on Monochrome`; diff --git a/functions/recent/index.js b/functions/recent/index.js index 4507e29..48797cc 100644 --- a/functions/recent/index.js +++ b/functions/recent/index.js @@ -1,5 +1,5 @@ export async function onRequest(context) { - const { request, env } = context; + const { request } = context; const pageUrl = request.url; const metaHtml = ` diff --git a/functions/settings/index.js b/functions/settings/index.js index f11f4ba..80a81b3 100644 --- a/functions/settings/index.js +++ b/functions/settings/index.js @@ -1,5 +1,5 @@ export async function onRequest(context) { - const { request, env } = context; + const { request } = context; const pageUrl = request.url; const metaHtml = ` diff --git a/functions/unreleased/[sheetId]/[projectName].js b/functions/unreleased/[sheetId]/[projectName].js index 6213856..d6837c8 100644 --- a/functions/unreleased/[sheetId]/[projectName].js +++ b/functions/unreleased/[sheetId]/[projectName].js @@ -1,7 +1,7 @@ // functions/unreleased/[sheetId]/[projectName].js const ARTISTS_NDJSON_URL = 'https://assets.artistgrid.cx/artists.ndjson'; -const ASSETS_BASE_URL = 'https://assets.artistgrid.cx'; +const _ASSETS_BASE_URL = 'https://assets.artistgrid.cx'; const TRACKER_API_ENDPOINTS = [ 'https://trackerapi-1.artistgrid.cx/get/', 'https://trackerapi-2.artistgrid.cx/get/', @@ -14,7 +14,7 @@ function getSheetId(url) { return match ? match[1] : null; } -function normalizeArtistName(name) { +function _normalizeArtistName(name) { return name.toLowerCase().replace(/[^a-z0-9]/g, ''); } @@ -62,7 +62,7 @@ async function fetchTrackerData(sheetId) { } return data; } catch (e) { - console.warn(`Failed to fetch from ${baseUrl}, trying next...`); + console.warn(`Failed to fetch from ${baseUrl}, trying next...`, e); } } return null; diff --git a/functions/unreleased/index.js b/functions/unreleased/index.js index e9e5f84..cd1513c 100644 --- a/functions/unreleased/index.js +++ b/functions/unreleased/index.js @@ -1,5 +1,5 @@ export async function onRequest(context) { - const { request, env } = context; + const { request } = context; const pageUrl = request.url; const metaHtml = ` diff --git a/js/HiFi.test.ts b/js/HiFi.test.ts index 7cf99f9..6a1fecf 100644 --- a/js/HiFi.test.ts +++ b/js/HiFi.test.ts @@ -1,11 +1,12 @@ -import { expect, suite, test } from 'vitest'; +import { expect, test } from 'vitest'; import { HiFiClient, TidalResponse } from './HiFi'; +import type { Album, PlaybackInfo, Track } from './container-classes'; const ARTIST_ID = 3523908; // deadmau5 const ALBUM_ID = 433360012; // deadmau5 - 4x4=12 -const ALBUM_ATMOS = 463900719; // Taylor Swift - The Life of a Showgirl +const _ALBUM_ATMOS = 463900719; // Taylor Swift - The Life of a Showgirl const TRACK_ATMOS = 463900720; // Taylor Swift - The Fate of Ophelia -const TRACK_NO_LOSSLESS = 31097959; // deadmau5 - while(1<2) +const _TRACK_NO_LOSSLESS = 31097959; // deadmau5 - while(1<2) const TRACK_VIDEO = 466464180; // Taylow Swift - The Fate of Ophelia const TRACK_LOSSLESS = 31097949; // deadmau5 - Avaritia const PLAYLIST_ID = '36ea71a8-445e-41a4-82ab-6628c581535d'; // Pop Hits @@ -19,25 +20,25 @@ function checkVersion({ version }: { version?: string }) { expect(version).equals(HiFiClient.API_VERSION); } -async function getJson(res: Response | Promise) { +async function _getJson(res: Response | Promise) { res = await res; expect(res).toBeInstanceOf(Response); expect(res.ok).toBeTruthy(); - return await res.json(); + return (await res.json()) as object; } async function checkRoute( route: string, - routeResult: () => Promise, - checks: (data: any) => Promise, + routeResult: () => Promise, + checks: (data: object) => Promise, mainKey: string | null = 'data' ) { const routeData = await instance.query(route); - const routeRes = await routeResult(); + const routeRes = (await routeResult()) as unknown; expect(routeData).toBeInstanceOf(TidalResponse); expect(routeData).toEqual(routeRes); - const json = await routeData.json(); + const json = (await routeData.json()) as object; checkVersion(json); if (mainKey != null) { @@ -71,7 +72,7 @@ test('Fetch atmos track info', async () => { await checkRoute( `/info/?id=${TRACK_ATMOS}`, () => instance.getInfo(TRACK_ATMOS), - async (info) => { + async (info: { data: Track }) => { expect(info.data.audioModes).toContain('DOLBY_ATMOS'); } ); @@ -81,8 +82,8 @@ test('Fetch track', async () => { await checkRoute( `/track/?id=${TRACK_LOSSLESS}`, () => instance.getTrack(TRACK_LOSSLESS), - async (track) => { - expect(track.data.trackId).toBe(TRACK_LOSSLESS); + async (track: { data: PlaybackInfo }) => { + expect(track?.data?.trackId).toBe(TRACK_LOSSLESS); expect(track.data.assetPresentation).toBeTypeOf('string'); expect(track.data.audioQuality).toBeTypeOf('string'); expect(track.data.manifestMimeType).toBeTypeOf('string'); @@ -102,7 +103,7 @@ test.skipIf(!instance.refreshToken)('Fetch recommendations', async () => { await checkRoute( `/recommendations/?id=${ARTIST_ID}`, () => instance.getRecommendations(ARTIST_ID), - async (rec) => {} + async (_data) => {} ); }); @@ -110,7 +111,7 @@ test('Fetch similar artists', async () => { await checkRoute( `/artist/similar/?id=${ARTIST_ID}`, () => instance.getSimilarArtists(ARTIST_ID), - async (rec) => {}, + async (_data) => {}, 'artists' ); }); @@ -119,7 +120,7 @@ test('Fetch similar albums', async () => { await checkRoute( `/album/similar/?id=${ALBUM_ID}`, () => instance.getSimilarAlbums(ALBUM_ID), - async (rec) => {}, + async (_data) => {}, 'albums' ); }); @@ -128,7 +129,7 @@ test('Fetch artist info', async () => { await checkRoute( `/artist/?id=${ARTIST_ID}`, () => instance.getArtist(ARTIST_ID), - async (info) => { + async (info: { cover: string }) => { expect(info).toHaveProperty('cover'); expect(info.cover).not.toBeUndefined(); }, @@ -144,7 +145,7 @@ test('Search', async () => { instance.search({ q: query, }), - async (res) => {} + async (_res) => {} ); }); @@ -152,7 +153,7 @@ test('Fetch album info', async () => { await checkRoute( `/album/?id=${ALBUM_ID}`, () => instance.getAlbum(ALBUM_ID), - async (info) => { + async (info: { data: Album }) => { expect(info.data).toHaveProperty('cover'); expect(info.data.cover).not.toBeUndefined(); } @@ -163,7 +164,7 @@ test('Fetch playlist info', async () => { await checkRoute( `/playlist/?id=${PLAYLIST_ID}`, () => instance.getPlaylist(PLAYLIST_ID), - async (info) => { + async (info: { playlist: { image: string } }) => { expect(info.playlist).toHaveProperty('image'); expect(info.playlist.image).not.toBeUndefined(); }, @@ -175,7 +176,7 @@ test.skipIf(!instance.refreshToken)('Fetch lyrics ', async () => { await checkRoute( `/lyrics/?id=${TRACK_ATMOS}`, () => instance.getLyrics(TRACK_ATMOS), - async (info) => {}, + async (_info) => {}, 'lyrics' ); }); @@ -184,7 +185,7 @@ test('Fetch video ', async () => { await checkRoute( `/video/?id=${TRACK_VIDEO}`, () => instance.getVideo(TRACK_VIDEO), - async (info) => {}, + async (_info) => {}, 'video' ); }); @@ -193,7 +194,7 @@ test('Fetch track manifests ', async () => { await checkRoute( `/trackManifests/?id=${TRACK_LOSSLESS}`, () => instance.getTrackManifest(TRACK_LOSSLESS), - async (info) => {}, + async (_info) => {}, 'data' ); }); diff --git a/js/HiFi.ts b/js/HiFi.ts index e903a58..0ea4e8d 100644 --- a/js/HiFi.ts +++ b/js/HiFi.ts @@ -1,3 +1,10 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + import { EventEmitter } from 'events'; type Params = Record; @@ -170,7 +177,7 @@ class HiFiClient { scope?: string; signal?: AbortSignal; force?: boolean; - }) { + }): Promise { if (!force && this.token && (this.appTokenExpiry < 0 || Date.now() < this.appTokenExpiry)) return this.token; return await (this.#tokenPromise ??= (async () => { @@ -654,7 +661,7 @@ class HiFiClient { }; const data = await this.#fetchJson(url, params, signal); - return HiFiClient.#jsonResponse({ version: API_VERSION, data: data }); + return HiFiClient.#jsonResponse({ version: HiFiClient.API_VERSION, data: data }); } #buildCoverEntry(cover_slug: string, name?: string | null, track_id?: number | null) { diff --git a/js/ModernSettings.ts b/js/ModernSettings.ts index c114181..0abc2ce 100644 --- a/js/ModernSettings.ts +++ b/js/ModernSettings.ts @@ -12,9 +12,9 @@ import { db } from './db'; * * @template C The accumulated shape of the settings object. */ -class ModernSettings { +class ModernSettings { /** Internal map of pending async operations keyed by unique symbols. */ - #pending: Record> = {}; + #pending: Record> = {}; /** Whether new properties are prevented from being added. */ #finalized: boolean = false; @@ -51,7 +51,7 @@ class ModernSettings { * @param callback Function producing the promise to track. * @returns The created promise. */ - #addPending>(callback: () => C): C { + #addPending>(callback: () => C): C { const sym = Symbol(); return (this.#pending[sym] = callback().finally(() => { @@ -145,14 +145,14 @@ class ModernSettings { const legacyValue = localStorage.getItem(legacy?.key ?? backingKey ?? key); if (legacyValue !== null) { - db.saveSetting(backingKey ?? key, legacy.transformer!(legacyValue)); + await db.saveSetting(backingKey ?? key, legacy.transformer(legacyValue)); localStorage.removeItem(legacy?.key ?? backingKey ?? key); } } } try { - value = (await db.getSetting(backingKey ?? key)) ?? defaultValue; + value = ((await db.getSetting(backingKey ?? key)) as T) ?? defaultValue; } catch { value = defaultValue; } @@ -162,7 +162,7 @@ class ModernSettings { get: () => (getter ? getter(value, typed as ModernSettings & C & Record) : value), set: (newValue: T) => { value = setter ? setter(newValue, typed as ModernSettings & C & Record) : newValue; - this.#addPending(() => db.saveSetting(backingKey ?? key, value)); + void this.#addPending(() => db.saveSetting(backingKey ?? key, value)); }, enumerable: true, }); diff --git a/js/accounts/auth.js b/js/accounts/auth.js index 8fd23df..7e25269 100644 --- a/js/accounts/auth.js +++ b/js/accounts/auth.js @@ -5,7 +5,7 @@ export class AuthManager { constructor() { this.user = null; this.authListeners = []; - this.init(); + this.init().catch(console.error); } async init() { diff --git a/js/accounts/pocketbase.js b/js/accounts/pocketbase.js index 79c793c..4b697c4 100644 --- a/js/accounts/pocketbase.js +++ b/js/accounts/pocketbase.js @@ -128,7 +128,7 @@ const syncManager = { } }, - safeParseInternal(str, fieldName, fallback) { + safeParseInternal(str, _fieldName, fallback) { if (!str) return fallback; if (typeof str !== 'string') return str; try { @@ -136,7 +136,7 @@ const syncManager = { } catch { try { // Recovery attempt: replace illegal internal quotes in name/title fields - const recovered = str.replace(/(:\s*")(.+?)("(?=\s*[,}\n\r]))/g, (match, p1, p2, p3) => { + const recovered = str.replace(/(:\s*")(.+?)("(?=\s*[,}\n\r]))/g, (_match, p1, p2, p3) => { const escapedContent = p2.replace(/(?|window|document|alert|eval/) ) { + // TODO: maybe this could be parsed as json5? + // eslint-disable-next-line @typescript-eslint/no-implied-eval return new Function('return ' + jsFriendly)(); } } @@ -565,11 +567,6 @@ const syncManager = { if (cloudData) { let database = db; - if (typeof database === 'function') { - database = await database(); - } else { - database = await database; - } const localData = { tracks: (await database.getAll('favorites_tracks')) || [], diff --git a/js/api.js b/js/api.js index c8ed8a3..d5ae23d 100644 --- a/js/api.js +++ b/js/api.js @@ -5,10 +5,7 @@ import { delay, isTrackUnavailable, getExtensionFromBlob, - getTrackTitle, - getFullArtistString, getTrackDiscNumber, - getMimeType, } from './utils.js'; import { preferDolbyAtmosSettings, trackDateSettings } from './storage.js'; import { APICache } from './cache.js'; @@ -36,7 +33,6 @@ import { export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE'; export { resolveDownloadTotalBytes }; -const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25'; export class LosslessAPI { constructor(settings) { @@ -48,8 +44,8 @@ export class LosslessAPI { this.streamCache = new Map(); setInterval( - () => { - this.cache.clearExpired(); + async () => { + await this.cache.clearExpired(); this.pruneStreamCache(); }, 1000 * 60 * 5 @@ -492,7 +488,7 @@ export class LosslessAPI { await this.cache.set('search_all', query, results); return results; - } catch (error) { + } catch (_error) { // Fallback to individual searches if the backend proxy doesn't support ?q= or throws const [tracks, videos, artists, albums, playlists] = await Promise.all([ this.searchTracks(query, options).catch(() => ({ items: [] })), @@ -1558,7 +1554,7 @@ export class LosslessAPI { } else { throw new Error('No URI in trackManifests response'); } - } catch (err) { + } catch (_err) { // Fallback to /track endpoint } diff --git a/js/api.test.ts b/js/api.test.ts index 6bb12ad..b2a8b29 100644 --- a/js/api.test.ts +++ b/js/api.test.ts @@ -2,7 +2,6 @@ import { expect, test, suite, vi } from 'vitest'; import { apiSettings, preferDolbyAtmosSettings, losslessContainerSettings } from './storage.js'; import { MusicAPI } from './music-api.js'; import { LyricsManager } from './lyrics.js'; -import type { LosslessAPI } from './api.js'; import { HiFiClient } from './HiFi.js'; import { FileRef } from '!/@dantheman827/taglib-ts/src/fileRef.js'; import { Mp4File } from '!/@dantheman827/taglib-ts/src/mp4/mp4File.js'; @@ -13,6 +12,7 @@ import { ByteVector, StringType } from '!/@dantheman827/taglib-ts/src/byteVector import { Mp4Codec } from '!/@dantheman827/taglib-ts/src/mp4/mp4Properties.js'; import { OggFile } from '!/@dantheman827/taglib-ts/src/ogg/oggFile.js'; import { ffmpeg } from './ffmpeg.js'; +import type { Track } from './container-classes.js'; vi.mock(import('./storage.js'), async (importOriginal) => { const mod = await importOriginal(); @@ -46,26 +46,25 @@ vi.mock(import('./doTimed.js'), async (importOriginal) => { return { ...mod, - doTimed: (label: string, fn: () => any) => { - return fn() as any; + doTimed: function (_label: string, fn: () => T): T { + return fn(); }, doTimedAsync ? Promise : T>( - message: string, + _message: string, callback: () => R, throwError: boolean = false ): R { - return new Promise(async (resolve, reject) => { - try { - const ret = await callback(); - resolve(ret); - } catch (err) { - if (throwError) { - reject(err); - return; - } - - resolve(undefined); - } + return new Promise((resolve, reject) => { + Promise.resolve() + .then(callback) + .then(resolve) + .catch((err) => { + if (throwError) { + reject(err as Error); + } else { + resolve(undefined); + } + }); }) as R; }, } satisfies typeof import('./doTimed.js'); @@ -99,15 +98,14 @@ suite('Track Downloads', async () => { const TRACK_ATMOS = 463900720; // Taylor Swift - The Fate of Ophelia const TRACK_NO_LOSSLESS = 31097959; // deadmau5 - while(1<2) - const { LosslessAPI } = await import('./api.js'); await MusicAPI.initialize(apiSettings); await LyricsManager.initialize(apiSettings); await HiFiClient.initialize(); - const api: LosslessAPI = MusicAPI.instance.tidalAPI; + const api = MusicAPI.instance.tidalAPI; async function downloadTrack(trackId: number, quality: string) { - const track = await (await HiFiClient.instance.getInfo(trackId)).json(); + const track = (await (await HiFiClient.instance.getInfo(trackId)).json()) as { data: Track }; return await api.downloadTrack(trackId.toString(), quality, undefined, { track: track.data, triggerDownload: false, @@ -276,7 +274,9 @@ suite('Track Downloads', async () => { ffmpegCalls: 1, }, ])('$display_quality', async ({ quality, container, preferDolbyAtmos, trackId, detection, ffmpegCalls }) => { + // eslint-disable-next-line @typescript-eslint/unbound-method vi.mocked(preferDolbyAtmosSettings.isEnabled).mockReturnValue(preferDolbyAtmos); + // eslint-disable-next-line @typescript-eslint/unbound-method vi.mocked(losslessContainerSettings.getContainer).mockReturnValue(container); const blob = await downloadTrack(trackId, quality); @@ -286,7 +286,6 @@ suite('Track Downloads', async () => { expect(file.isValid).toBe(true); - let trak: Mp4Atom | null = null; let stsd: Mp4Atom | null = null; let stsdData: ByteVector | null = null; @@ -313,13 +312,13 @@ suite('Track Downloads', async () => { trak = null; } expect(trak).toBeInstanceOf(Mp4Atom); - stsd = trak!.find('mdia', 'minf', 'stbl', 'stsd'); + stsd = trak.find('mdia', 'minf', 'stbl', 'stsd'); expect(stsd).toBeInstanceOf(Mp4Atom); await stream.seek(stsd.offset); stsdData = await stream.readBlock(stsd.length); } - stream.seek(streamPosition); + await stream.seek(streamPosition); switch (detection) { case Detection.DolbyAtmos: { diff --git a/js/app.js b/js/app.js index 074929e..a3272e3 100644 --- a/js/app.js +++ b/js/app.js @@ -33,7 +33,6 @@ import { authManager } from './accounts/auth.js'; import { registerSW } from 'virtual:pwa-register'; import { openEditProfile } from './profile.js'; import { ThemeStore } from './themeStore.js'; -import { partyManager } from './listening-party.js'; import './commandPalette.js'; import { initTracker } from './tracker.js'; import { @@ -406,6 +405,7 @@ document.addEventListener('DOMContentLoaded', async () => { // Populate commit info { const repo = 'https://github.com/monochrome-music/monochrome'; + // eslint-disable-next-line no-undef const hash = typeof __COMMIT_HASH__ !== 'undefined' ? __COMMIT_HASH__ : 'dev'; const commitLink = hash !== 'dev' && hash !== 'unknown' @@ -469,8 +469,7 @@ document.addEventListener('DOMContentLoaded', async () => { await Player.initialize(audioPlayer, MusicAPI.instance, currentQuality); // Initialize tracker - initTracker(); - + initTracker().catch(console.error); const castBtn = document.getElementById('cast-btn'); initializeCasting(audioPlayer, castBtn); @@ -585,7 +584,7 @@ document.addEventListener('DOMContentLoaded', async () => { // checks for a saved handle and (in browser mode) requests read permission, // so this is a silent no-op when no folder is configured or permission is not // yet granted. - scanLocalMediaFolder(); + scanLocalMediaFolder().catch(console.error); const scrobbler = new MultiScrobbler(); window.monochromeScrobbler = scrobbler; @@ -995,9 +994,9 @@ document.addEventListener('DOMContentLoaded', async () => { } }); - document.getElementById('download-current-btn')?.addEventListener('click', () => { + document.getElementById('download-current-btn')?.addEventListener('click', async () => { if (Player.instance.currentTrack) { - handleTrackAction( + await handleTrackAction( 'download', Player.instance.currentTrack, Player.instance, @@ -1420,14 +1419,14 @@ document.addEventListener('DOMContentLoaded', async () => { if (editingId) { // Edit const cover = document.getElementById('playlist-cover-input').value.trim(); - db.getPlaylist(editingId).then(async (playlist) => { + await db.getPlaylist(editingId).then(async (playlist) => { if (playlist) { playlist.name = name; playlist.cover = cover; playlist.description = description; await handlePublicStatus(playlist); await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); - syncManager.syncUserPlaylist(playlist, 'update'); + await syncManager.syncUserPlaylist(playlist, 'update'); UIRenderer.instance.renderLibraryPage(); // Also update current page if we are on it if (window.location.pathname === `/userplaylist/${editingId}`) { @@ -1948,7 +1947,7 @@ document.addEventListener('DOMContentLoaded', async () => { console.log(`Added ${tracks.length} tracks (including pending)`); } - db.createPlaylist(name, tracks, cover, description).then(async (playlist) => { + await db.createPlaylist(name, tracks, cover, description).then(async (playlist) => { await handlePublicStatus(playlist); // Update DB again with isPublic flag await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); @@ -1971,7 +1970,7 @@ document.addEventListener('DOMContentLoaded', async () => { if (e.target.closest('.edit-playlist-btn')) { const card = e.target.closest('.user-playlist'); const playlistId = card.dataset.userPlaylistId; - db.getPlaylist(playlistId).then(async (playlist) => { + await db.getPlaylist(playlistId).then(async (playlist) => { if (playlist) { const modal = document.getElementById('playlist-modal'); document.getElementById('playlist-modal-title').textContent = 'Edit Playlist'; @@ -1991,7 +1990,10 @@ document.addEventListener('DOMContentLoaded', async () => { shareBtn.style.display = playlist.isPublic ? 'flex' : 'none'; shareBtn.onclick = () => { const url = getShareUrl(`/userplaylist/${playlist.id}`); - navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!')); + navigator.clipboard + .writeText(url) + .then(() => alert('Link copied to clipboard!')) + .catch(console.error); }; } @@ -2030,73 +2032,77 @@ document.addEventListener('DOMContentLoaded', async () => { const card = e.target.closest('.user-playlist'); const playlistId = card.dataset.userPlaylistId; if (confirm('Are you sure you want to delete this playlist?')) { - db.deletePlaylist(playlistId).then(() => { - syncManager.syncUserPlaylist({ id: playlistId }, 'delete'); - UIRenderer.instance.renderLibraryPage(); - }); + await db.deletePlaylist(playlistId); + await syncManager.syncUserPlaylist({ id: playlistId }, 'delete'); + UIRenderer.instance.renderLibraryPage(); } } if (e.target.closest('#edit-playlist-btn')) { const playlistId = window.location.pathname.split('/')[2]; - db.getPlaylist(playlistId).then((playlist) => { - if (playlist) { - const modal = document.getElementById('playlist-modal'); - document.getElementById('playlist-modal-title').textContent = 'Edit Playlist'; - document.getElementById('playlist-name-input').value = playlist.name; - document.getElementById('playlist-cover-input').value = playlist.cover || ''; - document.getElementById('playlist-description-input').value = playlist.description || ''; + await db + .getPlaylist(playlistId) + .then((playlist) => { + if (playlist) { + const modal = document.getElementById('playlist-modal'); + document.getElementById('playlist-modal-title').textContent = 'Edit Playlist'; + document.getElementById('playlist-name-input').value = playlist.name; + document.getElementById('playlist-cover-input').value = playlist.cover || ''; + document.getElementById('playlist-description-input').value = playlist.description || ''; - const publicToggle = document.getElementById('playlist-public-toggle'); - const shareBtn = document.getElementById('playlist-share-btn'); + const publicToggle = document.getElementById('playlist-public-toggle'); + const shareBtn = document.getElementById('playlist-share-btn'); - if (publicToggle) publicToggle.checked = !!playlist.isPublic; - if (shareBtn) { - shareBtn.style.display = playlist.isPublic ? 'flex' : 'none'; - shareBtn.onclick = () => { - const url = getShareUrl(`/userplaylist/${playlist.id}`); - navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!')); - }; + if (publicToggle) publicToggle.checked = !!playlist.isPublic; + if (shareBtn) { + shareBtn.style.display = playlist.isPublic ? 'flex' : 'none'; + shareBtn.onclick = async () => { + const url = getShareUrl(`/userplaylist/${playlist.id}`); + await navigator.clipboard + .writeText(url) + .then(() => alert('Link copied to clipboard!')) + .catch(console.error); + }; + } + + // Set cover upload state - show URL input if there's an existing cover + const coverUploadBtn = document.getElementById('playlist-cover-upload-btn'); + const coverUrlInput = document.getElementById('playlist-cover-input'); + const coverToggleUrlBtn = document.getElementById('playlist-cover-toggle-url-btn'); + if (playlist.cover) { + if (coverUploadBtn) coverUploadBtn.style.display = 'none'; + if (coverUrlInput) coverUrlInput.style.display = 'block'; + if (coverToggleUrlBtn) { + coverToggleUrlBtn.textContent = 'Upload'; + coverToggleUrlBtn.title = 'Switch to file upload'; + } + } else { + if (coverUploadBtn) { + coverUploadBtn.style.flex = '1'; + coverUploadBtn.style.display = 'flex'; + } + if (coverUrlInput) coverUrlInput.style.display = 'none'; + if (coverToggleUrlBtn) { + coverToggleUrlBtn.textContent = 'or URL'; + coverToggleUrlBtn.title = 'Switch to URL input'; + } + } + + modal.dataset.editingId = playlistId; + document.getElementById('import-section').style.display = 'none'; + modal.classList.add('active'); + document.getElementById('playlist-name-input').focus(); } - - // Set cover upload state - show URL input if there's an existing cover - const coverUploadBtn = document.getElementById('playlist-cover-upload-btn'); - const coverUrlInput = document.getElementById('playlist-cover-input'); - const coverToggleUrlBtn = document.getElementById('playlist-cover-toggle-url-btn'); - if (playlist.cover) { - if (coverUploadBtn) coverUploadBtn.style.display = 'none'; - if (coverUrlInput) coverUrlInput.style.display = 'block'; - if (coverToggleUrlBtn) { - coverToggleUrlBtn.textContent = 'Upload'; - coverToggleUrlBtn.title = 'Switch to file upload'; - } - } else { - if (coverUploadBtn) { - coverUploadBtn.style.flex = '1'; - coverUploadBtn.style.display = 'flex'; - } - if (coverUrlInput) coverUrlInput.style.display = 'none'; - if (coverToggleUrlBtn) { - coverToggleUrlBtn.textContent = 'or URL'; - coverToggleUrlBtn.title = 'Switch to URL input'; - } - } - - modal.dataset.editingId = playlistId; - document.getElementById('import-section').style.display = 'none'; - modal.classList.add('active'); - document.getElementById('playlist-name-input').focus(); - } - }); + }) + .catch(console.error); } if (e.target.closest('#delete-playlist-btn')) { const playlistId = window.location.pathname.split('/')[2]; if (confirm('Are you sure you want to delete this playlist?')) { - db.deletePlaylist(playlistId).then(() => { - syncManager.syncUserPlaylist({ id: playlistId }, 'delete'); - navigate('/library'); - }); + await db.deletePlaylist(playlistId); + await syncManager.syncUserPlaylist({ id: playlistId }, 'delete'); + navigate('/library'); } } @@ -2105,7 +2111,7 @@ document.addEventListener('DOMContentLoaded', async () => { const btn = e.target.closest('.remove-from-playlist-btn'); const playlistId = window.location.pathname.split('/')[2]; - db.getPlaylist(playlistId).then(async (playlist) => { + await db.getPlaylist(playlistId).then(async (playlist) => { let trackId = null; let trackType = null; @@ -2124,7 +2130,7 @@ document.addEventListener('DOMContentLoaded', async () => { if (trackId) { const updatedPlaylist = await db.removeTrackFromPlaylist(playlistId, trackId, trackType); - syncManager.syncUserPlaylist(updatedPlaylist, 'update'); + await syncManager.syncUserPlaylist(updatedPlaylist, 'update'); const scrollTop = document.querySelector('.main-content').scrollTop; await UIRenderer.instance.renderPlaylistPage(playlistId, 'user'); document.querySelector('.main-content').scrollTop = scrollTop; @@ -2645,7 +2651,7 @@ document.addEventListener('DOMContentLoaded', async () => { // PWA Update Logic if (window.__AUTH_GATE__) { - disablePwaForAuthGate(); + await disablePwaForAuthGate().catch(console.error); } else { const updateSW = registerSW({ onNeedRefresh() { @@ -2763,10 +2769,10 @@ document.addEventListener('DOMContentLoaded', async () => { ); }); } else { - headerAccountBtn.addEventListener('click', (e) => { + headerAccountBtn.addEventListener('click', async (e) => { e.stopPropagation(); headerAccountDropdown.classList.toggle('active'); - updateAccountDropdown(); + await updateAccountDropdown(); }); } @@ -2839,8 +2845,8 @@ document.addEventListener('DOMContentLoaded', async () => { `; - document.getElementById('header-create-profile').onclick = () => { - openEditProfile(); + document.getElementById('header-create-profile').onclick = async () => { + openEditProfile().catch(console.error); headerAccountDropdown.classList.remove('active'); }; } @@ -2928,7 +2934,7 @@ function showMissingTracksNotification(missingTracks, playlistName) { const newCopyBtn = copyBtn.cloneNode(true); copyBtn.parentNode.replaceChild(newCopyBtn, copyBtn); - newCopyBtn.addEventListener('click', () => { + newCopyBtn.addEventListener('click', async () => { const header = `Missing songs from ${playlistName} import:\n\n`; const textToCopy = header + @@ -2940,11 +2946,14 @@ function showMissingTracksNotification(missingTracks, playlistName) { }) .join('\n'); - navigator.clipboard.writeText(textToCopy).then(() => { - const originalText = newCopyBtn.textContent; - newCopyBtn.textContent = 'Copied!'; - setTimeout(() => (newCopyBtn.textContent = originalText), 2000); - }); + await navigator.clipboard + .writeText(textToCopy) + .then(async () => { + const originalText = newCopyBtn.textContent; + newCopyBtn.textContent = 'Copied!'; + setTimeout(() => (newCopyBtn.textContent = originalText), 2000); + }) + .catch(console.error); }); } diff --git a/js/bulk-download-writer.ts b/js/bulk-download-writer.ts index 4b6eae4..7efea34 100644 --- a/js/bulk-download-writer.ts +++ b/js/bulk-download-writer.ts @@ -69,9 +69,7 @@ export class ZipStreamWriter implements IBulkDownloadWriter { constructor(private readonly suggestedFilename: string) {} async write(files: AsyncIterable): Promise { - // showSaveFilePicker is part of the File System Access API (not yet in all TS DOM libs) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fileHandle = await (window as any).showSaveFilePicker({ + const fileHandle = await window.showSaveFilePicker({ suggestedName: this.suggestedFilename, types: [{ description: 'ZIP Archive', accept: { 'application/zip': ['.zip'] } }], }); @@ -134,8 +132,7 @@ export class FolderPickerWriter implements IBulkDownloadWriter { // Try to re-use a saved handle first if (savedHandle) { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const permission = await (savedHandle as any).requestPermission({ mode: 'readwrite' }); + const permission = await savedHandle.requestPermission({ mode: 'readwrite' }); if (permission === 'granted') { return new FolderPickerWriter(savedHandle); } @@ -145,9 +142,8 @@ export class FolderPickerWriter implements IBulkDownloadWriter { } // showDirectoryPicker is part of the File System Access API (not yet in all TS DOM libs) - // eslint-disable-next-line @typescript-eslint/no-explicit-any try { - const dirHandle: FileSystemDirectoryHandle = await (window as any).showDirectoryPicker({ + const dirHandle: FileSystemDirectoryHandle = await window.showDirectoryPicker({ mode: 'readwrite', }); return new FolderPickerWriter(dirHandle); diff --git a/js/cache.js b/js/cache.js index 38501ba..7f7dfd5 100644 --- a/js/cache.js +++ b/js/cache.js @@ -7,7 +7,7 @@ export class APICache { this.dbName = 'monochrome-cache'; this.dbVersion = 1; this.db = null; - this.initDB(); + this.initDB().catch(console.error); } async initDB() { diff --git a/js/commandPalette.js b/js/commandPalette.js index 5d45981..ca095db 100644 --- a/js/commandPalette.js +++ b/js/commandPalette.js @@ -338,9 +338,9 @@ class CommandPalette { icon: 'trash', label: 'Clear Queue', keywords: ['wipe', 'clear', 'empty', 'queue'], - action: () => { + action: async () => { Player.instance.wipeQueue(); - this.notify('Queue cleared'); + await this.notify('Queue cleared'); }, }, { @@ -674,7 +674,7 @@ class CommandPalette { keywords: ['edit', 'profile', 'username', 'avatar', 'display name'], action: async () => { const { openEditProfile } = await import('./profile.js'); - openEditProfile(); + await openEditProfile(); }, }, { @@ -780,7 +780,7 @@ class CommandPalette { this.updateSelection(); } else if (e.key === 'Enter') { e.preventDefault(); - this.executeSelected(); + this.executeSelected().catch(console.error); } else if (e.key === 'Escape') { if (this.settingsMode) { this.settingsMode = false; @@ -1036,9 +1036,9 @@ class CommandPalette { el.innerHTML = `${iconHtml}
${escapeHtml(item.label)}${descHtml}
${shortcutHtml}`; - el.addEventListener('click', () => { + el.addEventListener('click', async () => { this.selectedIndex = index; - this.executeSelected(); + await this.executeSelected(); }); el.addEventListener('mouseenter', () => { @@ -1171,14 +1171,14 @@ class CommandPalette { if (opt.dataset.theme === theme) opt.classList.add('active'); else opt.classList.remove('active'); }); - this.notify(`Theme set to ${theme}`); + await this.notify(`Theme set to ${theme}`); } async toggleVisualizer() { const { visualizerSettings } = await import('./storage.js'); const current = visualizerSettings.isEnabled(); visualizerSettings.setEnabled(!current); - this.notify(`Visualizer ${!current ? 'enabled' : 'disabled'}`); + await this.notify(`Visualizer ${!current ? 'enabled' : 'disabled'}`); const overlay = document.getElementById('fullscreen-cover-overlay'); if (overlay && getComputedStyle(overlay).display !== 'none') { @@ -1192,7 +1192,7 @@ class CommandPalette { if (UIRenderer.instance.visualizer) { UIRenderer.instance.visualizer.setPreset(preset); } - this.notify(`Visualizer preset: ${preset}`); + await this.notify(`Visualizer preset: ${preset}`); } async setQuality(quality) { @@ -1225,13 +1225,13 @@ class CommandPalette { const downloadSelect = document.getElementById('download-quality-setting'); if (downloadSelect) downloadSelect.value = dlQuality; - this.notify(`Quality set to ${qualityNames[quality] || quality}`); + await this.notify(`Quality set to ${qualityNames[quality] || quality}`); } - setSleepTimer(minutes) { + async setSleepTimer(minutes) { if (Player.instance) { Player.instance.setSleepTimer(minutes); - this.notify(`Sleep timer: ${minutes} minutes`); + await this.notify(`Sleep timer: ${minutes} minutes`); } } @@ -1242,7 +1242,7 @@ class CommandPalette { const queue = player.getCurrentQueue(); if (queue.length === 0) { - this.notify('Queue is empty'); + await this.notify('Queue is empty'); return; } @@ -1250,7 +1250,7 @@ class CommandPalette { const scrobbler = window.monochromeScrobbler; let likedCount = 0; - this.notify('Liking all tracks in queue...'); + await this.notify('Liking all tracks in queue...'); for (const track of queue) { const isLiked = await db.isFavorite('track', track.id); if (!isLiked) { @@ -1258,7 +1258,7 @@ class CommandPalette { likedCount++; } } - this.notify(`Liked ${likedCount} new track(s)`); + await this.notify(`Liked ${likedCount} new track(s)`); } async downloadQueue() { @@ -1268,40 +1268,39 @@ class CommandPalette { const queue = player.getCurrentQueue(); if (queue.length === 0) { - this.notify('Queue is empty'); + await this.notify('Queue is empty'); return; } const { downloadTracks } = await import('./downloads.js'); const { downloadQualitySettings } = await import('./storage.js'); - downloadTracks(queue, ui.api, downloadQualitySettings.getQuality(), ui.lyricsManager); + await downloadTracks(queue, ui.api, downloadQualitySettings.getQuality(), ui.lyricsManager); } async createPlaylist() { const name = `New Playlist ${new Date().toLocaleDateString()}`; await db.createPlaylist(name); navigate('/library'); - this.notify('Playlist created'); + await this.notify('Playlist created'); } async createFolder() { const name = `New Folder ${new Date().toLocaleDateString()}`; await db.createFolder(name); navigate('/library'); - this.notify('Folder created'); + await this.notify('Folder created'); } async clearCache() { const api = UIRenderer.instance.api; if (api) { await api.clearCache(); - this.notify('Cache cleared'); + await this.notify('Cache cleared'); } } async notify(message) { - const { showNotification } = await import('./downloads.js'); - showNotification(message); + await import('./downloads.js').then((m) => m.showNotification(message)).catch(console.error); } } diff --git a/js/container-classes.ts b/js/container-classes.ts index 57b3df9..bd74872 100644 --- a/js/container-classes.ts +++ b/js/container-classes.ts @@ -85,7 +85,7 @@ export class MediaMetadata extends BaseContainer { } export class Artist extends BaseContainer { - handle: any; + handle: unknown; id: number; name: string; picture: string; @@ -99,6 +99,7 @@ export class Artist extends BaseContainer { export class EnrichedTrack extends Track { declare album: TrackAlbum | EnrichedAlbum; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-redundant-type-constituents declare replayGain: any | ReplayGain; constructor(data: object) { diff --git a/js/dash-downloader.ts b/js/dash-downloader.ts index 21391ce..7ac3a93 100644 --- a/js/dash-downloader.ts +++ b/js/dash-downloader.ts @@ -204,13 +204,13 @@ export class DashDownloader { const resolveTemplate = (template: string, number: number, time: number): string => { return template .replace(/\$RepresentationID\$/g, repId ?? '') - .replace(/\$Number(?:%0([0-9]+)d)?\$/g, (_, width) => { + .replace(/\$Number(?:%0([0-9]+)d)?\$/g, (_, width: string) => { if (width) { return number.toString().padStart(parseInt(width), '0'); } return number.toString(); }) - .replace(/\$Time(?:%0([0-9]+)d)?\$/g, (_, width) => { + .replace(/\$Time(?:%0([0-9]+)d)?\$/g, (_, width: string) => { if (width) { return time.toString().padStart(parseInt(width), '0'); } diff --git a/js/db.js b/js/db.js index 9017296..95c306b 100644 --- a/js/db.js +++ b/js/db.js @@ -783,7 +783,6 @@ export class MusicDatabase { } // Return lightweight copy without tracks - // eslint-disable-next-line no-unused-vars const { tracks, ...minified } = playlist; return minified; }); diff --git a/js/doTimed.ts b/js/doTimed.ts index 1e5d123..58b6a7c 100644 --- a/js/doTimed.ts +++ b/js/doTimed.ts @@ -24,24 +24,30 @@ export function doTimedAsync ? Promise : T>( throwError: boolean = false ): R { if (import.meta.env.DEV) { - return new Promise(async (resolve, reject) => { - const hiddenId = InvisibleCodec.encode(v7()); - console.time(message + hiddenId); - try { - const output = await callback(); - resolve(output); - } catch (err) { - console.error(`Error in timed operation "${message}":`, err); - if (throwError) { - reject(err); - } else { - resolve(undefined as R); + return new Promise((resolve, reject) => { + (async () => { + const hiddenId = InvisibleCodec.encode(v7()); + console.time(message + hiddenId); + try { + const output = await callback(); + resolve(output); + } catch (err) { + console.error(`Error in timed operation "${message}":`, err); + if (throwError) { + if (err instanceof Error) { + reject(err); + } else { + reject(new Error(String(err))); + } + } else { + resolve(undefined as R); + } + } finally { + console.timeEnd(message + hiddenId); } - } finally { - console.timeEnd(message + hiddenId); - } + })().catch(reject); }) as R; } else { - return callback() as R; + return callback(); } } diff --git a/js/downloads.js b/js/downloads.js index 1230808..b8ec561 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -17,10 +17,10 @@ import { ZipStreamWriter, ZipBlobWriter, FolderPickerWriter, SequentialFileWrite import { FfmpegProgress } from './ffmpeg.types.js'; import { DownloadProgress, ProgressMessage, SegmentedDownloadProgress } from './progressEvents.js'; import { db } from './db.js'; -import { modernSettings } from './ModernSettings.js'; +import { BulkDownloadMethod, modernSettings } from './ModernSettings.js'; import { SVG_CLOSE } from './icons.ts'; -import { LyricsManager } from './lyrics.js'; import { MusicAPI } from './music-api.js'; +import { LyricsManager } from './lyrics.js'; const downloadTasks = new Map(); const bulkDownloadTasks = new Map(); @@ -167,7 +167,7 @@ export function showNotification(message) { }, 1500); } -export function addDownloadTask(trackId, track, filename, api, abortController) { +export function addDownloadTask(trackId, track, _filename, api, abortController) { const container = createDownloadNotification(); const taskEl = document.createElement('div'); @@ -508,7 +508,7 @@ async function createSingleTrackFolderWriter() { const method = modernSettings.bulkDownloadMethod; const hasFolderPicker = 'showDirectoryPicker' in window; - if (method === 'local') { + if (method === BulkDownloadMethod.LocalMedia) { const localHandle = await db.getSetting('local_folder_handle'); if (hasFolderPicker && localHandle && typeof localHandle.requestPermission === 'function') { try { @@ -521,7 +521,7 @@ async function createSingleTrackFolderWriter() { return null; } - if (method === 'folder' && hasFolderPicker) { + if (method === BulkDownloadMethod.Folder && hasFolderPicker) { const rememberFolder = modernSettings.rememberBulkDownloadFolder; const savedHandle = rememberFolder ? await db.getSetting('bulk_download_folder_handle') : null; // Try to reuse the saved handle silently first. @@ -564,7 +564,7 @@ async function createBulkWriter(folderName) { const hasFolderPicker = 'showDirectoryPicker' in window; // ── Local Media Folder method ──────────────────────────────────────────── - if (method === 'local') { + if (method === BulkDownloadMethod.LocalMedia) { const localHandle = await db.getSetting('local_folder_handle'); if (hasFolderPicker) { // Browser mode: try to reuse the stored handle with write permission @@ -594,7 +594,7 @@ async function createBulkWriter(folderName) { } // ── Folder Picker method ───────────────────────────────────────────────── - if (method === 'folder' && hasFolderPicker) { + if (method === BulkDownloadMethod.Folder && hasFolderPicker) { const rememberFolder = modernSettings.rememberBulkDownloadFolder; const savedHandle = rememberFolder ? await db.getSetting('bulk_download_folder_handle') : null; try { @@ -614,7 +614,7 @@ async function createBulkWriter(folderName) { } } - if (method === 'individual') { + if (method === BulkDownloadMethod.Individual) { return SequentialFileWriter; } // method === 'zip' (or folder picker unavailable as fallback) @@ -659,7 +659,7 @@ async function startBulkDownload({ completeBulkDownload(notification, true); // If the download went to the local media folder, refresh the local library. - if (modernSettings.bulkDownloadMethod === 'local') { + if (modernSettings.bulkDownloadMethod === BulkDownloadMethod.LocalMedia) { window.refreshLocalMediaFolder?.(); } } catch (error) { @@ -672,7 +672,7 @@ async function startBulkDownload({ } } -export async function downloadTracks(tracks, api, quality, lyricsManager = null) { +export async function downloadTracks(tracks, api, quality, _lyricsManager = null) { const folderName = `Queue - ${new Date().toISOString().slice(0, 10)}`; await startBulkDownload({ tracks, @@ -687,7 +687,7 @@ export async function downloadTracks(tracks, api, quality, lyricsManager = null) }); } -export async function downloadAlbum(album, tracks, api, quality, lyricsManager = null) { +export async function downloadAlbum(album, tracks, api, quality, _lyricsManager = null) { const releaseDateStr = album.releaseDate || (tracks[0]?.streamStartDate ? tracks[0].streamStartDate.split('T')[0] : ''); const releaseDate = releaseDateStr ? new Date(releaseDateStr) : null; @@ -712,7 +712,7 @@ export async function downloadAlbum(album, tracks, api, quality, lyricsManager = }); } -export async function downloadPlaylist(playlist, tracks, api, quality, lyricsManager = null) { +export async function downloadPlaylist(playlist, tracks, api, quality, _lyricsManager = null) { const folderName = formatPathTemplate(modernSettings.folderTemplate, { albumTitle: playlist.title, albumArtist: 'Playlist', @@ -1120,7 +1120,7 @@ export async function downloadTrackWithMetadata( // If the target is the local media folder, do a cheap partial update: // pass the downloaded blob and base filename so only this one track's metadata // is read and inserted into localFilesCache instead of re-walking the whole folder. - if (modernSettings.bulkDownloadMethod === 'local') { + if (modernSettings.bulkDownloadMethod === BulkDownloadMethod.LocalMedia) { window.refreshLocalMediaFolder?.(blob, finalFilename); } @@ -1136,7 +1136,7 @@ export async function downloadTrackWithMetadata( } } -export async function downloadLikedTracks(tracks, api, quality, lyricsManager = null) { +export async function downloadLikedTracks(tracks, api, quality, _lyricsManager = null) { const folderName = `Liked Tracks - ${new Date().toISOString().slice(0, 10)}`; await startBulkDownload({ tracks, diff --git a/js/events.js b/js/events.js index 36e6ca2..54186cf 100644 --- a/js/events.js +++ b/js/events.js @@ -56,6 +56,9 @@ import { } from './analytics.js'; import { SVG_BIN, SVG_MUTE, SVG_PAUSE, SVG_PLAY, SVG_VOLUME, SVG_CHECKBOX, SVG_CHECKBOX_CHECKED } from './icons.js'; import { partyManager } from './listening-party.js'; +import { MusicAPI } from './music-api.js'; +import { LyricsManager } from './lyrics.js'; +import { Player } from './player.js'; let currentTrackIdForWaveform = null; @@ -78,21 +81,21 @@ function handleTrackTouchStart(e) { isLongPress = false; longPressTrackItem = trackItem; - longPressTimer = setTimeout(() => { + longPressTimer = setTimeout(async () => { isLongPress = true; toggleTrackSelection(trackItem, true, false); - hapticLongPress(); + await hapticLongPress(); }, LONG_PRESS_DURATION); } -function handleTrackTouchMove(e) { +function handleTrackTouchMove(_e) { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } } -function handleTrackTouchEnd(e) { +function handleTrackTouchEnd(_e) { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; @@ -204,7 +207,7 @@ function toggleTrackSelection(trackItem, ctrlHeld, shiftHeld) { document.body.classList.toggle('multi-select-mode', trackSelection.isSelecting); } -function showMultiSelectPlaylistModal(tracks) { +async function showMultiSelectPlaylistModal(tracks) { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.style.cssText = @@ -237,7 +240,7 @@ function showMultiSelectPlaylistModal(tracks) { document.body.appendChild(modal); document.body.style.overflow = 'hidden'; - db.getPlaylists(true).then((playlists) => { + await db.getPlaylists(true).then((playlists) => { const listEl = modal.querySelector('.playlist-list'); if (playlists.length === 0) { listEl.innerHTML = '
No playlists yet
'; @@ -260,17 +263,17 @@ function showMultiSelectPlaylistModal(tracks) { for (const track of tracks) { await db.addTrackToPlaylist(playlistId, track); } - syncManager.syncUserPlaylist(await db.getPlaylist(playlistId), 'update'); + await syncManager.syncUserPlaylist(await db.getPlaylist(playlistId), 'update'); showNotification(`Added ${tracks.length} tracks to playlist`); closeModal(); }); }); }); - modal.querySelector('.create-new-playlist').addEventListener('click', () => { + modal.querySelector('.create-new-playlist').addEventListener('click', async () => { const name = prompt('Playlist name:'); if (name) { - db.createPlaylist(name, tracks).then((playlist) => { + await db.createPlaylist(name, tracks).then((_playlist) => { showNotification(`Created playlist "${name}" with ${tracks.length} tracks`); closeModal(); }); @@ -278,127 +281,132 @@ function showMultiSelectPlaylistModal(tracks) { }); } -export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { - const playPauseBtn = document.querySelector('.now-playing-bar .play-pause-btn'); - const nextBtn = document.getElementById('next-btn'); - const prevBtn = document.getElementById('prev-btn'); - const shuffleBtn = document.getElementById('shuffle-btn'); - const repeatBtn = document.getElementById('repeat-btn'); - const homeStartRadioBtn = document.getElementById('home-start-infinite-radio-btn'); - const sleepTimerBtnDesktop = document.getElementById('sleep-timer-btn-desktop'); +const playPauseBtn = document.querySelector('.now-playing-bar .play-pause-btn'); +const nextBtn = document.getElementById('next-btn'); +const prevBtn = document.getElementById('prev-btn'); +const shuffleBtn = document.getElementById('shuffle-btn'); +const repeatBtn = document.getElementById('repeat-btn'); +const homeStartRadioBtn = document.getElementById('home-start-infinite-radio-btn'); +const sleepTimerBtnDesktop = document.getElementById('sleep-timer-btn-desktop'); - const volumeBar = document.getElementById('volume-bar'); - const volumeFill = document.getElementById('volume-fill'); - const volumeBtn = document.getElementById('volume-btn'); +const _volumeBar = document.getElementById('volume-bar'); +const volumeFill = document.getElementById('volume-fill'); +const volumeBtn = document.getElementById('volume-btn'); - const updateVolumeUI = () => { - const activeEl = player.activeElement; - const { muted } = activeEl; - const volume = player.userVolume; - volumeBtn.innerHTML = muted || volume === 0 ? SVG_MUTE(20) : SVG_VOLUME(20); - const effectiveVolume = muted ? 0 : volume * 100; - volumeFill.style.setProperty('--volume-level', `${effectiveVolume}%`); - volumeFill.style.width = `${effectiveVolume}%`; - }; +const updateVolumeUI = () => { + const activeEl = Player.instance.activeElement; + const { muted } = activeEl; + const volume = Player.instance.userVolume; + volumeBtn.innerHTML = muted || volume === 0 ? SVG_MUTE(20) : SVG_VOLUME(20); + const effectiveVolume = muted ? 0 : volume * 100; + volumeFill.style.setProperty('--volume-level', `${effectiveVolume}%`); + volumeFill.style.width = `${effectiveVolume}%`; +}; - function clearSelection() { - trackSelection.selectedIds.clear(); - trackSelection.lastClickedId = null; - trackSelection.isSelecting = false; - document.body.classList.remove('multi-select-mode'); - document.querySelectorAll('.track-item.selected').forEach((el) => { - el.classList.remove('selected'); - }); - document.querySelectorAll('.track-checkbox').forEach((checkbox) => { - checkbox.innerHTML = SVG_CHECKBOX(18); - checkbox.classList.remove('checked'); - }); - updateSelectionBar(); - } +function clearSelection() { + trackSelection.selectedIds.clear(); + trackSelection.lastClickedId = null; + trackSelection.isSelecting = false; + document.body.classList.remove('multi-select-mode'); + document.querySelectorAll('.track-item.selected').forEach((el) => { + el.classList.remove('selected'); + }); + document.querySelectorAll('.track-checkbox').forEach((checkbox) => { + checkbox.innerHTML = SVG_CHECKBOX(18); + checkbox.classList.remove('checked'); + }); + updateSelectionBar(); +} - function updateSelectionBar() { - let bar = document.getElementById('selection-bar'); - if (!bar) { - bar = document.createElement('div'); - bar.id = 'selection-bar'; - bar.className = 'selection-bar'; - bar.innerHTML = ` - 0 selected -
- - - - - -
- +function updateSelectionBar() { + let bar = document.getElementById('selection-bar'); + if (!bar) { + bar = document.createElement('div'); + bar.id = 'selection-bar'; + bar.className = 'selection-bar'; + bar.innerHTML = ` + 0 selected +
+ + + + + +
+ `; - document.body.appendChild(bar); + document.body.appendChild(bar); - bar.querySelectorAll('button').forEach((btn) => { - btn.addEventListener('click', () => handleSelectionAction(btn.dataset.action)); - }); - } - - const count = trackSelection.selectedIds.size; - bar.querySelector('.selection-count').textContent = `${count} selected`; - bar.classList.toggle('visible', count > 0); - } - - function handleSelectionAction(action) { - const selectedIds = getSelectedTracks(); - if (selectedIds.length === 0) return; - - const mainContent = document.getElementById('main-content'); - const selectedTracks = []; - mainContent.querySelectorAll('.track-item').forEach((item) => { - if (trackSelection.selectedIds.has(item.dataset.trackId)) { - const track = trackDataStore.get(item); - if (track) selectedTracks.push(track); - } + bar.querySelectorAll('button').forEach((btn) => { + btn.addEventListener('click', () => handleSelectionAction(btn.dataset.action)); }); - - switch (action) { - case 'play-selected': - if (selectedTracks.length > 0) { - player.setQueue(selectedTracks, 0); - document.getElementById('shuffle-btn').classList.remove('active'); - player.playTrackFromQueue(); - } - break; - case 'add-to-queue-selected': - if (selectedTracks.length > 0) { - player.addToQueue(selectedTracks); - if (window.renderQueueFunction) window.renderQueueFunction(); - showNotification(`Added ${selectedTracks.length} tracks to queue`); - } - break; - case 'add-to-playlist-selected': - if (selectedTracks.length > 0) { - showMultiSelectPlaylistModal(selectedTracks); - } - break; - case 'download-selected': - if (selectedTracks.length > 0) { - selectedTracks.forEach((track) => { - downloadTrackWithMetadata(track, downloadQualitySettings.getQuality(), api, lyricsManager); - }); - showNotification(`Downloading ${selectedTracks.length} tracks`); - } - break; - case 'like-selected': - selectedTracks.forEach(async (track) => { - const added = await db.toggleFavorite('track', track); - syncManager.syncLibraryItem('track', track, added); - }); - showNotification(`Liked ${selectedTracks.length} tracks`); - break; - case 'clear-selection': - clearSelection(); - break; - } } + const count = trackSelection.selectedIds.size; + bar.querySelector('.selection-count').textContent = `${count} selected`; + bar.classList.toggle('visible', count > 0); +} + +async function handleSelectionAction(action) { + const selectedIds = getSelectedTracks(); + if (selectedIds.length === 0) return; + + const mainContent = document.getElementById('main-content'); + const selectedTracks = []; + mainContent.querySelectorAll('.track-item').forEach((item) => { + if (trackSelection.selectedIds.has(item.dataset.trackId)) { + const track = trackDataStore.get(item); + if (track) selectedTracks.push(track); + } + }); + + switch (action) { + case 'play-selected': + if (selectedTracks.length > 0) { + Player.instance.setQueue(selectedTracks, 0); + document.getElementById('shuffle-btn').classList.remove('active'); + Player.instance.playTrackFromQueue(); + } + break; + case 'add-to-queue-selected': + if (selectedTracks.length > 0) { + Player.instance.addToQueue(selectedTracks); + if (window.renderQueueFunction) await window.renderQueueFunction(); + showNotification(`Added ${selectedTracks.length} tracks to queue`); + } + break; + case 'add-to-playlist-selected': + if (selectedTracks.length > 0) { + await showMultiSelectPlaylistModal(selectedTracks); + } + break; + case 'download-selected': + if (selectedTracks.length > 0) { + showNotification(`Downloading ${selectedTracks.length} tracks`); + for (const track of selectedTracks) { + await downloadTrackWithMetadata( + track, + downloadQualitySettings.getQuality(), + MusicAPI.instance.tidalAPI, + LyricsManager.instance + ); + } + } + break; + case 'like-selected': + for (const track of selectedTracks) { + const added = await db.toggleFavorite('track', track); + await syncManager.syncLibraryItem('track', track, added); + } + showNotification(`Liked ${selectedTracks.length} tracks`); + break; + case 'clear-selection': + clearSelection(); + break; + } +} + +export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { if (homeStartRadioBtn) { homeStartRadioBtn.addEventListener('click', async () => { await player.enableRadio(); @@ -417,14 +425,14 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { } }); - element.addEventListener('play', () => { + element.addEventListener('play', async () => { if (player.activeElement !== element) return; // Initialize audio context manager for EQ (only once) if (!audioContextManager.isReady()) { audioContextManager.init(element); } - audioContextManager.resume(); + await audioContextManager.resume(); if (player.currentTrack) { // Track play event @@ -435,7 +443,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { scrobbler.updateNowPlaying(player.currentTrack); } - updateWaveform(); + await updateWaveform(); } playPauseBtn.innerHTML = SVG_PAUSE(20); @@ -479,7 +487,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { if (currentTime >= 10 && player.currentTrack && player.currentTrack.id !== historyLoggedTrackId) { historyLoggedTrackId = player.currentTrack.id; const historyEntry = await db.addToHistory(player.currentTrack); - syncManager.syncHistoryItem(historyEntry); + await syncManager.syncHistoryItem(historyEntry); if (window.location.hash === '#recent') { ui.renderRecentPage(); @@ -554,31 +562,31 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { setupMediaListeners(player.video); } - playPauseBtn.addEventListener('click', () => { - hapticMedium(); + playPauseBtn.addEventListener('click', async () => { + await hapticMedium(); player.handlePlayPause(); }); - nextBtn.addEventListener('click', () => { - hapticMedium(); + nextBtn.addEventListener('click', async () => { + await hapticMedium(); trackSkipTrack(player.currentTrack, 'next'); player.playNext(); }); - prevBtn.addEventListener('click', () => { - hapticMedium(); + prevBtn.addEventListener('click', async () => { + await hapticMedium(); trackSkipTrack(player.currentTrack, 'previous'); player.playPrev(); }); - shuffleBtn.addEventListener('click', () => { - hapticLight(); + shuffleBtn.addEventListener('click', async () => { + await hapticLight(); player.toggleShuffle(); trackToggleShuffle(player.shuffleActive); shuffleBtn.classList.toggle('active', player.shuffleActive); - if (window.renderQueueFunction) window.renderQueueFunction(); + if (window.renderQueueFunction) await window.renderQueueFunction(); }); - repeatBtn.addEventListener('click', () => { - hapticLight(); + repeatBtn.addEventListener('click', async () => { + await hapticLight(); const mode = player.toggleRepeat(); trackToggleRepeat(mode === REPEAT_MODE.OFF ? 'off' : mode === REPEAT_MODE.ALL ? 'all' : 'one'); repeatBtn.classList.toggle('active', mode !== REPEAT_MODE.OFF); @@ -709,7 +717,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { } }; - window.addEventListener('waveform-toggle', (e) => { + window.addEventListener('waveform-toggle', async (e) => { if (!e.detail.enabled) { const progressBar = document.getElementById('progress-bar'); const playerControls = document.querySelector('.player-controls'); @@ -722,7 +730,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { playerControls.classList.remove('waveform-loaded'); } } - updateWaveform(); + await updateWaveform(); }); if (volumeBtn) { @@ -1102,7 +1110,7 @@ export async function showAddToPlaylistModal(track) { e.stopPropagation(); await db.removeTrackFromPlaylist(playlistId, track.id); const updatedPlaylist = await db.getPlaylist(playlistId); - syncManager.syncUserPlaylist(updatedPlaylist, 'update'); + await syncManager.syncUserPlaylist(updatedPlaylist, 'update'); showNotification(`Removed from playlist: ${option.querySelector('span').textContent}`); await renderModal(); } else { @@ -1110,7 +1118,7 @@ export async function showAddToPlaylistModal(track) { await db.addTrackToPlaylist(playlistId, track); const updatedPlaylist = await db.getPlaylist(playlistId); - syncManager.syncUserPlaylist(updatedPlaylist, 'update'); + await syncManager.syncUserPlaylist(updatedPlaylist, 'update'); showNotification(`Added to playlist: ${option.querySelector('span').textContent}`); closeModal(); } @@ -1272,14 +1280,14 @@ export async function handleTrackAction( if (action === 'add-to-queue') { player.addToQueue(tracks); - if (window.renderQueueFunction) window.renderQueueFunction(); + if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Added ${tracks.length} tracks to queue`); return; } if (action === 'play-next') { player.addNextToQueue(tracks); - if (window.renderQueueFunction) window.renderQueueFunction(); + if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Playing next: ${tracks.length} tracks`); return; } @@ -1345,12 +1353,12 @@ export async function handleTrackAction( if (action === 'add-to-queue') { trackAddToQueue(item, 'end'); player.addToQueue(item); - if (window.renderQueueFunction) window.renderQueueFunction(); + if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Added to queue: ${item.title}`); } else if (action === 'play-next') { trackPlayNext(item); player.addNextToQueue(item); - if (window.renderQueueFunction) window.renderQueueFunction(); + if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Playing next: ${item.title}`); } else if (action === 'play-card') { player.setQueue([item], 0); @@ -1368,7 +1376,7 @@ export async function handleTrackAction( await downloadTrackWithMetadata(item, downloadQualitySettings.getQuality(), api, lyricsManager); } else if (action === 'toggle-like') { const added = await db.toggleFavorite(type, item); - syncManager.syncLibraryItem(type, item, added); + await syncManager.syncLibraryItem(type, item, added); // Track like/unlike if (added) { @@ -1624,7 +1632,7 @@ export async function handleTrackAction( e.stopPropagation(); await db.removeTrackFromPlaylist(playlistId, item.id); const updatedPlaylist = await db.getPlaylist(playlistId); - syncManager.syncUserPlaylist(updatedPlaylist, 'update'); + await syncManager.syncUserPlaylist(updatedPlaylist, 'update'); showNotification(`Removed from playlist: ${option.querySelector('span').textContent}`); await renderModal(); } else { @@ -1632,7 +1640,7 @@ export async function handleTrackAction( await db.addTrackToPlaylist(playlistId, item); const updatedPlaylist = await db.getPlaylist(playlistId); - syncManager.syncUserPlaylist(updatedPlaylist, 'update'); + await syncManager.syncUserPlaylist(updatedPlaylist, 'update'); showNotification(`Added to playlist: ${option.querySelector('span').textContent}`); closeModal(); } @@ -1670,9 +1678,12 @@ export async function handleTrackAction( const url = getShareUrl(storedHref ? storedHref : `/${typeForUrl}/${item.id || item.uuid}`); trackCopyLink(type, item.id || item.uuid); - navigator.clipboard.writeText(url).then(() => { - showNotification('Link copied to clipboard!'); - }); + await navigator.clipboard + .writeText(url) + .then(() => { + showNotification('Link copied to clipboard!'); + }) + .catch(console.error); } else if (action === 'open-in-new-tab') { // Use stored href from card if available, otherwise construct URL const contextMenu = document.getElementById('context-menu'); @@ -2412,7 +2423,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen positionMenu(contextMenu, e.clientX, e.clientY); }); - document.addEventListener('click', (e) => { + document.addEventListener('click', async (e) => { if (contextMenu.style.display === 'block') { if (contextMenu._originalHTML) { contextMenu.innerHTML = contextMenu._originalHTML; @@ -2432,7 +2443,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen } }); - document.addEventListener('keydown', (e) => { + document.addEventListener('keydown', async (e) => { if (e.key === 'Escape' && trackSelection.isSelecting) { clearSelection(); } @@ -2488,34 +2499,39 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen trackPlayNext(t); player.addNextToQueue(t); }); - if (window.renderQueueFunction) window.renderQueueFunction(); + if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Playing next: ${selectedTracks.length} tracks`); clearSelection(); break; case 'add-to-queue': player.addToQueue(selectedTracks); - if (window.renderQueueFunction) window.renderQueueFunction(); + if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Added ${selectedTracks.length} tracks to queue`); clearSelection(); break; case 'toggle-like': selectedTracks.forEach(async (t) => { const added = await db.toggleFavorite('track', t); - syncManager.syncLibraryItem('track', t, added); + await syncManager.syncLibraryItem('track', t, added); }); showNotification(`Liked ${selectedTracks.length} tracks`); clearSelection(); break; case 'add-to-playlist': - showMultiSelectPlaylistModal(selectedTracks); + await showMultiSelectPlaylistModal(selectedTracks); clearSelection(); break; case 'download': - selectedTracks.forEach((t) => { - downloadTrackWithMetadata(t, downloadQualitySettings.getQuality(), api, lyricsManager); - }); showNotification(`Downloading ${selectedTracks.length} tracks`); clearSelection(); + for (const track of selectedTracks) { + await downloadTrackWithMetadata( + track, + downloadQualitySettings.getQuality(), + api, + lyricsManager + ); + } break; default: clearSelection(); diff --git a/js/ffmpeg.js b/js/ffmpeg.js index e01157c..d218aa4 100644 --- a/js/ffmpeg.js +++ b/js/ffmpeg.js @@ -114,7 +114,7 @@ async function ffmpegWorker( reject(new FfmpegError('Worker failed: ' + error.message)); }; - (async () => { + void (async () => { const transferables = []; if (audioData) transferables.push(audioData); for (const f of extraFiles) { diff --git a/js/ffmpeg.test.ts b/js/ffmpeg.test.ts index 547b513..5931d06 100644 --- a/js/ffmpeg.test.ts +++ b/js/ffmpeg.test.ts @@ -1,9 +1,9 @@ -import { expect, test, suite } from 'vitest'; +import { expect, test } from 'vitest'; import { ffmpeg } from './ffmpeg'; test('Run `ffmpeg --help`', async () => { const lines: string[] = []; - const info = await ffmpeg(null, { + await ffmpeg(null, { rawArgs: ['--help'], logConsole: false, outputName: null, diff --git a/js/ffmpegFormats.ts b/js/ffmpegFormats.ts index 2d45089..0e1e22c 100644 --- a/js/ffmpegFormats.ts +++ b/js/ffmpegFormats.ts @@ -183,6 +183,11 @@ export function getContainerFormat(internalName: string): ContainerFormat | unde return containerFormats[internalName]; } +export interface ExtraFile { + name: string; + data: ArrayBuffer | Uint8Array; +} + /** * Transcodes an audio blob using the specified custom format via ffmpeg. * Throws if ffmpeg fails during transcoding. @@ -192,7 +197,7 @@ export async function transcodeWithCustomFormat( format: CustomFormat, onProgress: ((progress: ProgressEvent) => void) | null = null, signal: AbortSignal | null = null, - extraFiles: any[] = [] + extraFiles: ExtraFile[] = [] ): Promise { return ffmpeg(audioBlob, { args: format.ffmpegArgs, @@ -213,7 +218,7 @@ export async function transcodeWithContainerFormat( format: ContainerFormat, onProgress: ((progress: ProgressEvent) => void) | null = null, signal: AbortSignal | null = null, - extraFiles: any[] = [] + extraFiles: ExtraFile[] = [] ): Promise { return ffmpeg(audioBlob, { args: format.ffmpegArgs, diff --git a/js/global.d.ts b/js/global.d.ts index 838a09d..3a8480a 100644 --- a/js/global.d.ts +++ b/js/global.d.ts @@ -31,3 +31,7 @@ declare module 'https://cdn.jsdelivr.net/npm/client-zip@2.4.5/+esm' { type WithRequiredKeys = { [K in keyof T]-?: T[K] | undefined; }; + +declare global { + const __COMMIT_HASH__: string | undefined; +} diff --git a/js/indexedIterator.ts b/js/indexedIterator.ts new file mode 100644 index 0000000..89e3e9f --- /dev/null +++ b/js/indexedIterator.ts @@ -0,0 +1,16 @@ +/** + * A generic iterator that yields the index, total count, and item for any finite iterable. + * + * @template T - The type of items in the iterable. + * @param iterable - The iterable to process. + * @returns A generator that yields an object with index, total, and item. + */ +export default function* indexedIterator( + iterable: Iterable +): Generator<{ index: number; total: number; item: T }> { + const array = Array.from(iterable); // Convert the iterable to an array + const total = array.length; // Get the total count of items + for (let index = 0; index < total; index++) { + yield { index, total, item: array[index] }; // Yield index, total, and item + } +} diff --git a/js/lastfm.js b/js/lastfm.js index e240877..dfd7ea1 100644 --- a/js/lastfm.js +++ b/js/lastfm.js @@ -284,8 +284,8 @@ export class LastFMScrobbler { scheduleScrobble(delay) { this.clearScrobbleTimer(); - this.scrobbleTimer = setTimeout(() => { - this.scrobbleCurrentTrack(); + this.scrobbleTimer = setTimeout(async () => { + await this.scrobbleCurrentTrack(); }, delay); } @@ -350,9 +350,9 @@ export class LastFMScrobbler { } } - onTrackChange(track) { + async onTrackChange(track) { if (!this.isAuthenticated()) return; - this.updateNowPlaying(track); + await this.updateNowPlaying(track); } onPlaybackStop() { diff --git a/js/librefm.js b/js/librefm.js index 83368f8..bfab9e3 100644 --- a/js/librefm.js +++ b/js/librefm.js @@ -216,8 +216,8 @@ export class LibreFmScrobbler { scheduleScrobble(delay) { this.clearScrobbleTimer(); - this.scrobbleTimer = setTimeout(() => { - this.scrobbleCurrentTrack(); + this.scrobbleTimer = setTimeout(async () => { + await this.scrobbleCurrentTrack(); }, delay); } @@ -282,9 +282,9 @@ export class LibreFmScrobbler { } } - onTrackChange(track) { + async onTrackChange(track) { if (!this.isAuthenticated()) return; - this.updateNowPlaying(track); + await this.updateNowPlaying(track); } onPlaybackStop() { diff --git a/js/listenbrainz.js b/js/listenbrainz.js index 0310c16..1f54eaa 100644 --- a/js/listenbrainz.js +++ b/js/listenbrainz.js @@ -209,8 +209,8 @@ export class ListenBrainzScrobbler { scheduleScrobble(delay) { this.clearScrobbleTimer(); - this.scrobbleTimer = setTimeout(() => { - this.scrobbleCurrentTrack(); + this.scrobbleTimer = setTimeout(async () => { + await this.scrobbleCurrentTrack(); }, delay); } @@ -235,8 +235,8 @@ export class ListenBrainzScrobbler { } } - onTrackChange(track) { - this.updateNowPlaying(track); + async onTrackChange(track) { + await this.updateNowPlaying(track); } onPlaybackStop() { diff --git a/js/listening-party.js b/js/listening-party.js index 9cddb6c..b0d3991 100644 --- a/js/listening-party.js +++ b/js/listening-party.js @@ -96,7 +96,7 @@ export class ListeningPartyManager { document.getElementById('copy-party-link-btn')?.addEventListener('click', () => this.copyInviteLink()); document.getElementById('party-chat-send-btn')?.addEventListener('click', () => this.sendChatMessage()); document.getElementById('party-chat-input')?.addEventListener('keypress', (e) => { - if (e.key === 'Enter') this.sendChatMessage(); + if (e.key === 'Enter') this.sendChatMessage().catch(console.error); }); } @@ -104,13 +104,13 @@ export class ListeningPartyManager { const nameInput = document.getElementById('party-name-input'); const user = authManager.user; if (!user) { - Modal.alert('Login Required', 'You must be logged in to host a listening party.'); + await Modal.alert('Login Required', 'You must be logged in to host a listening party.'); return; } const pbUser = await syncManager._getUserRecord(user.$id); if (!pbUser) { - Modal.alert('Sync Error', 'Failed to sync user data. Please try again.'); + await Modal.alert('Sync Error', 'Failed to sync user data. Please try again.'); return; } @@ -171,19 +171,19 @@ export class ListeningPartyManager { this.setupSubscriptions(partyId); this.startHeartbeat(); this.renderPartyUI(); - this.loadInitialData(partyId); + await this.loadInitialData(partyId); if (!this.isHost) { this.lockControls(); this.setupGuestSyncInterception(); if (party.current_track) { await audioContextManager.resume(); - this.syncWithHost(party); + await this.syncWithHost(party); } } } catch (error) { console.error('Join error:', error); - Modal.alert('Error', 'Failed to join the party. It may have ended.'); + await Modal.alert('Error', 'Failed to join the party. It may have ended.'); navigate('/parties'); } finally { this.isJoining = false; @@ -199,7 +199,7 @@ export class ListeningPartyManager { ); return confirmed ? { profile: null } : false; } else { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const cached = localStorage.getItem('party_guest_profile'); const defaultName = cached ? JSON.parse(cached).name : ''; @@ -225,7 +225,9 @@ export class ListeningPartyManager { }, { label: 'Cancel', type: 'secondary', callback: () => false }, ], - }).then(resolve); + }) + .then(resolve) + .catch(reject); }); } } @@ -262,29 +264,31 @@ export class ListeningPartyManager { pb.collection('parties') .subscribe( partyId, - (e) => { + async (e) => { if (e.action === 'update') { this.currentParty = e.record; - if (!this.isHost) this.syncWithHost(e.record); + if (!this.isHost) await this.syncWithHost(e.record); this.updatePartyHeader(); } else if (e.action === 'delete') { - Modal.alert('Party Ended', 'The host has ended the listening party.'); - this.leaveParty(false); + await Modal.alert('Party Ended', 'The host has ended the listening party.'); + await this.leaveParty(false); } }, { f_id } ) - .then((unsub) => this.unsubscribeFunctions.push(unsub)); + .then((unsub) => this.unsubscribeFunctions.push(unsub)) + .catch(console.error); pb.collection('party_members') .subscribe( '*', - (e) => { - if (e.record.party === partyId) this.loadMembers(); + async (e) => { + if (e.record.party === partyId) await this.loadMembers(); }, { f_id } ) - .then((unsub) => this.unsubscribeFunctions.push(unsub)); + .then((unsub) => this.unsubscribeFunctions.push(unsub)) + .catch(console.error); pb.collection('party_messages') .subscribe( @@ -294,23 +298,25 @@ export class ListeningPartyManager { }, { f_id } ) - .then((unsub) => this.unsubscribeFunctions.push(unsub)); + .then((unsub) => this.unsubscribeFunctions.push(unsub)) + .catch(console.error); pb.collection('party_requests') .subscribe( '*', - (e) => { - if (e.record.party === partyId) this.loadRequests(); + async (e) => { + if (e.record.party === partyId) await this.loadRequests(); }, { f_id } ) - .then((unsub) => this.unsubscribeFunctions.push(unsub)); + .then((unsub) => this.unsubscribeFunctions.push(unsub)) + .catch(console.error); } - async loadInitialData(partyId) { - this.loadMembers(); - this.loadMessages(); - this.loadRequests(); + async loadInitialData(_partyId) { + await this.loadMembers(); + await this.loadMessages(); + await this.loadRequests(); } async loadMembers() { @@ -439,7 +445,7 @@ export class ListeningPartyManager { ${this.isHost ? `` : ''} `; - } catch (e) { + } catch (_e) { return ''; } }) @@ -510,7 +516,7 @@ export class ListeningPartyManager { await pb .collection('party_messages') .create({ party: this.currentParty.id, sender_name: profile.name, content }, { f_id }); - } catch (e) {} + } catch (_e) {} } async requestSong(track) { @@ -560,7 +566,7 @@ export class ListeningPartyManager { if (party.is_playing) { if (el.paused) { - const success = await player.safePlay(el); + const _success = await player.safePlay(el); } const latency = (Date.now() - party.playback_timestamp) / 1000; const targetTime = party.is_playing ? party.playback_time + latency : party.playback_time; @@ -640,7 +646,7 @@ export class ListeningPartyManager { }, { f_id: authManager.user?.$id } ); - } catch (e) {} + } catch (_e) {} }; ['play', 'pause', 'seeked'].forEach((ev) => { player.audio.addEventListener(ev, updateParty); @@ -667,7 +673,7 @@ export class ListeningPartyManager { 'danger' ); if (!leave) return; - this.leaveParty(); + await this.leaveParty(); } return await originalPlayTrackFromQueue(...args); }; @@ -680,7 +686,7 @@ export class ListeningPartyManager { await pb .collection('party_members') .update(this.memberId, { last_seen: Date.now() }, { f_id: authManager.user?.$id || 'guest' }); - } catch (e) {} + } catch (_e) {} }, 30000); } @@ -705,11 +711,11 @@ export class ListeningPartyManager { await cleanup('party_messages'); await cleanup('party_requests'); await pb.collection('parties').delete(this.currentParty.id, { f_id }); - } catch (e) {} + } catch (_e) {} } else if (this.memberId) { try { await pb.collection('party_members').delete(this.memberId, { f_id }); - } catch (e) {} + } catch (_e) {} } this.restorePlayerMethods(); this.unlockControls(); @@ -733,7 +739,7 @@ export class ListeningPartyManager { } copyInviteLink() { - navigator.clipboard.writeText(`${window.location.origin}/party/${this.currentParty.id}`); + navigator.clipboard.writeText(`${window.location.origin}/party/${this.currentParty.id}`).catch(console.error); showNotification('Invite link copied!'); } diff --git a/js/lyrics.js b/js/lyrics.js index 1b8caa2..e59ffae 100644 --- a/js/lyrics.js +++ b/js/lyrics.js @@ -10,7 +10,7 @@ import { SVG_GLOBE, } from './icons.js'; import { sidePanelManager } from './side-panel.js'; -import('@uimaxbai/am-lyrics/am-lyrics.js'); +import('@uimaxbai/am-lyrics/am-lyrics.js').catch(console.error); // Check if text contains Japanese, Chinese, or Korean characters function containsAsianText(text) { @@ -246,6 +246,7 @@ export class LyricsManager { // Monkey-patch XMLHttpRequest to redirect dictionary requests to CDN // Kuromoji uses XHR, not fetch, for loading dictionary files if (!window._originalXHROpen) { + // eslint-disable-next-line @typescript-eslint/unbound-method window._originalXHROpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, ...rest) { const urlStr = url.toString(); @@ -264,7 +265,7 @@ export class LyricsManager { if (!window._originalFetch) { window._originalFetch = window.fetch; window.fetch = async (url, options) => { - const urlStr = url.toString(); + const urlStr = url instanceof URL ? url.toString() : url.url; if (urlStr.includes('/dict/') && urlStr.includes('.dat.gz')) { const filename = urlStr.split('/').pop(); const cdnUrl = `https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/${filename}`; @@ -527,7 +528,7 @@ export class LyricsManager { } // Setup MutationObserver to convert lyrics in am-lyrics component - setupLyricsObserver(amLyricsElement) { + async setupLyricsObserver(amLyricsElement) { this.stopLyricsObserver(); if (!amLyricsElement) return; @@ -575,7 +576,7 @@ export class LyricsManager { await this.convertLyricsContent(amLyricsElement); } if (this.isGeniusMode && this.currentGeniusData) { - this.applyGeniusAnnotations(amLyricsElement, this.currentGeniusData.referents); + await this.applyGeniusAnnotations(amLyricsElement, this.currentGeniusData.referents); } }, 100); }); @@ -591,10 +592,10 @@ export class LyricsManager { // Initial conversion if Romaji mode is enabled - single attempt, no periodic polling if (this.isRomajiMode) { - this.convertLyricsContent(amLyricsElement); + await this.convertLyricsContent(amLyricsElement); } if (this.isGeniusMode && this.currentGeniusData) { - this.applyGeniusAnnotations(amLyricsElement, this.currentGeniusData.referents); + await this.applyGeniusAnnotations(amLyricsElement, this.currentGeniusData.referents); } } @@ -692,7 +693,7 @@ export class LyricsManager { if (amLyricsElement) { if (this.isRomajiMode) { // Turning ON: Setup observer and convert immediately - this.setupLyricsObserver(amLyricsElement); + await this.setupLyricsObserver(amLyricsElement); await this.convertLyricsContent(amLyricsElement); } else { // Turning OFF: Stop observer @@ -1238,7 +1239,7 @@ export function clearFullscreenLyricsSync(container) { } } -export function clearLyricsPanelSync(audioPlayer, panel) { +export function clearLyricsPanelSync(_audioPlayer, panel) { if (panel && panel.lyricsCleanup) { panel.lyricsCleanup(); panel.lyricsCleanup = null; diff --git a/js/maloja.js b/js/maloja.js index cbc809e..5720748 100644 --- a/js/maloja.js +++ b/js/maloja.js @@ -135,8 +135,8 @@ export class MalojaScrobbler { scheduleScrobble(delay) { this.clearScrobbleTimer(); - this.scrobbleTimer = setTimeout(() => { - this.scrobbleCurrentTrack(); + this.scrobbleTimer = setTimeout(async () => { + await this.scrobbleCurrentTrack(); }, delay); } @@ -161,8 +161,8 @@ export class MalojaScrobbler { } } - onTrackChange(track) { - this.updateNowPlaying(track); + async onTrackChange(track) { + await this.updateNowPlaying(track); } onPlaybackStop() { diff --git a/js/metadata.js b/js/metadata.js index b826ffd..eab6ac9 100644 --- a/js/metadata.js +++ b/js/metadata.js @@ -37,7 +37,7 @@ export function prefetchMetadataObjects(track, api, coverBlob = null) { * @param {string} quality - Audio quality * @returns {Promise} - Audio blob with embedded metadata */ -export async function addMetadataToAudio(audioBlob, track, api, _quality, prefetchPromises) { +export async function addMetadataToAudio(audioBlob, track, _api, _quality, prefetchPromises) { const { coverFetch, lyricsFetch } = prefetchPromises; /** diff --git a/js/metadata.mp4.js b/js/metadata.mp4.js index 21e6682..c1f8f57 100644 --- a/js/metadata.mp4.js +++ b/js/metadata.mp4.js @@ -546,9 +546,9 @@ export function createStringAtom(type, value, truncateType = true) { export function createUserAtom(namespace, name, value) { const encoder = new TextEncoder(); - const dashBytes = encoder.encode('----'); // User-defined atom type + const _dashBytes = encoder.encode('----'); // User-defined atom type const namespaceBytes = encoder.encode(namespace); - const meanBytes = encoder.encode('mean'); // Standard 'mean' atom for namespace + const _meanBytes = encoder.encode('mean'); // Standard 'mean' atom for namespace const nameBytes = encoder.encode(name); const valueBytes = encoder.encode('\x00\x00\x00\x01\x00\x00\x00\x00' + value); diff --git a/js/multi-scrobbler.js b/js/multi-scrobbler.js index d4616ac..8fe9bcf 100644 --- a/js/multi-scrobbler.js +++ b/js/multi-scrobbler.js @@ -32,18 +32,26 @@ export class MultiScrobbler { ); } - updateNowPlaying(track) { - this.lastfm.updateNowPlaying(track); - this.listenbrainz.updateNowPlaying(track); - this.maloja.updateNowPlaying(track); - this.librefm.updateNowPlaying(track); + async updateNowPlaying(track) { + await Promise.allSettled( + [ + this.lastfm.updateNowPlaying(track), + this.listenbrainz.updateNowPlaying(track), + this.maloja.updateNowPlaying(track), + this.librefm.updateNowPlaying(track), + ].map((p) => p.catch(console.error)) + ); } - onTrackChange(track) { - this.lastfm.onTrackChange(track); - this.listenbrainz.onTrackChange(track); - this.maloja.onTrackChange(track); - this.librefm.onTrackChange(track); + async onTrackChange(track) { + await Promise.allSettled( + [ + this.lastfm.onTrackChange(track), + this.listenbrainz.onTrackChange(track), + this.maloja.onTrackChange(track), + this.librefm.onTrackChange(track), + ].map((p) => p.catch(console.error)) + ); } onPlaybackStop() { @@ -55,9 +63,11 @@ export class MultiScrobbler { // Love/Like tracks on all services that support it async loveTrack(track) { - await this.lastfm.loveTrack(track); - await this.librefm.loveTrack(track); - await this.listenbrainz.loveTrack(track); + await Promise.allSettled( + [this.lastfm.loveTrack(track), this.librefm.loveTrack(track), this.listenbrainz.loveTrack(track)].map((p) => + p.catch(console.error) + ) + ); // Maloja feedback could be added here when supported } } diff --git a/js/music-api.js b/js/music-api.js index 33d0ff4..0888c9f 100644 --- a/js/music-api.js +++ b/js/music-api.js @@ -4,8 +4,45 @@ import { LosslessAPI } from './api.js'; import { PodcastsAPI } from './podcasts-api.js'; import { musicProviderSettings } from './storage.js'; +/** + * MusicAPI - Singleton class that provides a unified interface for accessing music streaming services. + * + * Supports multiple providers (primarily Tidal) and includes functionality for searching, + * retrieving metadata, streaming, and managing playlists, artists, albums, tracks, and podcasts. + * + * @class MusicAPI + * @classdesc Manages API interactions with music providers and provides caching mechanisms + * for cover artwork and video metadata. + * + * @example + * // Initialize the MusicAPI + * await MusicAPI.initialize(settings); + * + * // Get the singleton instance + * const api = MusicAPI.instance; + * + * // Search for tracks + * const results = await api.search('query'); + * + * // Get a specific track + * const track = await api.getTrack('track-id'); + * + * // Get stream URL + * const streamUrl = await api.getStreamUrl('track-id', 'HIGH'); + * + * @property {LosslessAPI} tidalAPI - The Tidal API instance + * @property {PodcastsAPI} podcastsAPI - The Podcasts API instance + * @property {Object} _settings - Configuration settings + * @property {Map} videoArtworkCache - Cache for video artwork data + * + * @throws {Error} Throws if instance is accessed before initialization + * @throws {Error} Throws if initialize is called more than once + */ export class MusicAPI { static #instance = null; + /** + * @type {MusicAPI} + */ static get instance() { if (!MusicAPI.#instance) { throw new Error('MusicAPI not initialized. Call MusicAPI.initialize(settings) first.'); @@ -35,7 +72,7 @@ export class MusicAPI { } // Get the appropriate API based on provider - getAPI(provider = null) { + getAPI() { return this.tidalAPI; } @@ -101,31 +138,31 @@ export class MusicAPI { } // Get methods - async getTrack(id, quality, provider = null) { + async getTrack(id, quality) { const api = this.getAPI(); const cleanId = this.stripProviderPrefix(id); return api.getTrack(cleanId, quality); } - async getTrackMetadata(id, provider = null) { + async getTrackMetadata(id) { const api = this.getAPI(); const cleanId = this.stripProviderPrefix(id); return api.getTrackMetadata(cleanId); } - async getAlbum(id, provider = null) { + async getAlbum(id) { const api = this.getAPI(); const cleanId = this.stripProviderPrefix(id); return api.getAlbum(cleanId); } - async getArtist(id, provider = null) { + async getArtist(id) { const api = this.getAPI(); const cleanId = this.stripProviderPrefix(id); return api.getArtist(cleanId); } - async getArtistBiography(id, provider = null) { + async getArtistBiography(id) { const api = this.getAPI(); const cleanId = this.stripProviderPrefix(id); if (typeof api.getArtistBiography === 'function') { @@ -134,13 +171,13 @@ export class MusicAPI { return null; } - async getVideo(id, provider = null) { + async getVideo(id) { const api = this.getAPI(); const cleanId = this.stripProviderPrefix(id); return api.getVideo(cleanId); } - async getVideoStreamUrl(id, provider = null) { + async getVideoStreamUrl(id) { const api = this.getAPI(); const cleanId = this.stripProviderPrefix(id); if (typeof api.getVideoStreamUrl === 'function') { @@ -157,7 +194,7 @@ export class MusicAPI { return this.tidalAPI.getPlaylist(id); } - async getMix(id, _provider = null) { + async getMix(id) { // Mixes are always Tidal for now return this.tidalAPI.getMix(id); } @@ -172,7 +209,7 @@ export class MusicAPI { } // Stream methods - async getStreamUrl(id, quality, provider = null) { + async getStreamUrl(id, quality) { const api = this.getAPI(); const cleanId = this.stripProviderPrefix(id); return api.getStreamUrl(cleanId, quality); diff --git a/js/player.js b/js/player.js index 02cab2a..be3a0a6 100644 --- a/js/player.js +++ b/js/player.js @@ -133,7 +133,7 @@ export class Player { } this.loadQueueState(); - this.setupMediaSession(); + await this.setupMediaSession(); this.radioEnabled = radioSettings.isEnabled(); this.radioSeeds = []; @@ -142,19 +142,19 @@ export class Player { this.playbackSequence = 0; - window.addEventListener('beforeunload', () => { - this.saveQueueState(); + window.addEventListener('beforeunload', async () => { + await this.saveQueueState(); }); // Handle visibility change for iOS - AudioContext gets suspended when screen locks - document.addEventListener('visibilitychange', () => { + document.addEventListener('visibilitychange', async () => { const el = this.activeElement; if (document.visibilityState === 'visible' && !el.paused) { // Ensure audio context is resumed when user returns to the app if (!audioContextManager.isReady()) { audioContextManager.init(el); } - audioContextManager.resume(); + await audioContextManager.resume(); } if (document.visibilityState === 'visible' && this.autoplayBlocked) { this.autoplayBlocked = false; @@ -370,7 +370,7 @@ export class Player { } } - saveQueueState() { + async saveQueueState() { queueManager.saveQueue({ queue: this.queue, shuffledQueue: this.shuffledQueue, @@ -381,14 +381,14 @@ export class Player { }); if (window.renderQueueFunction) { - window.renderQueueFunction(); + await window.renderQueueFunction(); } } - setupMediaSession() { + async setupMediaSession() { if (!('mediaSession' in navigator)) return; - const setHandlers = () => { + const setHandlers = async () => { navigator.mediaSession.setActionHandler('play', async () => { const el = this.activeElement; // Initialize and resume audio context first (required for iOS lock screen) @@ -404,7 +404,7 @@ export class Player { } catch (e) { console.error('MediaSession play failed:', e); // If play fails, try to handle it like a regular play/pause - this.handlePlayPause(); + await this.handlePlayPause(); } }); @@ -429,7 +429,7 @@ export class Player { this.applyReplayGain(); } await audioContextManager.resume(); - this.playNext(); + await this.playNext(); }); if (!this.isIOS) { @@ -465,7 +465,7 @@ export class Player { this.video.addEventListener('playing', () => setHandlers(), { once: true }); } } else { - setHandlers(); + await setHandlers(); } } @@ -542,7 +542,7 @@ export class Player { video.play().catch(() => {}); await this.setupVideoQualitySelector(); }); - this.hls.on(Hls.Events.ERROR, (event, data) => { + this.hls.on(Hls.Events.ERROR, (_event, data) => { if (data.fatal) { console.warn('HLS fatal error:', data.type); if (fallbackImg) video.replaceWith(fallbackImg); @@ -578,7 +578,7 @@ export class Player { const levels = this.hls.levels; const qualityLabels = [ 'Auto', - ...levels.map((level, i) => { + ...levels.map((level) => { const height = level.height || 0; const bandwidth = level.bitrate || 0; if (height >= 1080) return '1080p'; @@ -645,7 +645,7 @@ export class Player { artist: video.artist || (video.artists && video.artists[0]) || 'Unknown Artist', album: video.album || { title: 'Video', cover: video.image || video.cover }, }; - this.setQueue([videoTrack], 0); + await this.setQueue([videoTrack], 0); await this.playTrackFromQueue(); } @@ -663,7 +663,7 @@ export class Player { const track = currentQueue[this.currentQueueIndex]; if (track.isUnavailable) { console.warn(`Attempted to play unavailable track: ${track.title}. Skipping...`); - this.playNext(); + await this.playNext(); return; } @@ -671,7 +671,7 @@ export class Player { const { contentBlockingSettings } = await import('./storage.js'); if (contentBlockingSettings.shouldHideTrack(track)) { console.warn(`Attempted to play blocked track: ${track.title}. Skipping...`); - this.playNext(); + await this.playNext(); return; } @@ -694,15 +694,15 @@ export class Player { this.currentQueueIndex >= currentQueue.length - 1 ) { console.log('[playTrackFromQueue] Fetching more tracks!'); - this.fetchMoreArtistPopularTracks().then((newTracks) => { + await this.fetchMoreArtistPopularTracks().then(async (newTracks) => { console.log('[playTrackFromQueue] Got tracks:', newTracks?.length); if (newTracks && newTracks.length > 0) { - this.addToQueue(newTracks); + await this.addToQueue(newTracks); } }); } - this.saveQueueState(); + await this.saveQueueState(); this.currentTrack = track; @@ -818,7 +818,7 @@ export class Player { if (!streamUrl) { console.warn(`Podcast episode ${trackTitle} audio URL is missing. Skipping.`); track.isUnavailable = true; - this.playNext(); + await this.playNext(); return; } @@ -851,7 +851,7 @@ export class Player { if (!streamUrl) { console.warn(`Track ${trackTitle} audio URL is missing. Skipping.`); track.isUnavailable = true; - this.playNext(); + await this.playNext(); return; } @@ -1018,7 +1018,7 @@ export class Player { } } - this.preloadNextTracks(); + void this.preloadNextTracks().catch(console.error); } catch (error) { if (this.playbackSequence !== currentSequence) return; if (error && (error.name === 'NotAllowedError' || error.name === 'AbortError')) { @@ -1041,6 +1041,8 @@ export class Player { this.isFallbackRetry = false; this.isFallbackInProgress = false; } + + return; } console.error(`Could not play track: ${trackTitle}`, error); @@ -1051,33 +1053,33 @@ export class Player { } } - playAtIndex(index) { + async playAtIndex(index) { const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; if (index >= 0 && index < currentQueue.length) { this.currentQueueIndex = index; - this.playTrackFromQueue(0, 0); + await this.playTrackFromQueue(0, 0); } } - playNext(recursiveCount = 0) { + async playNext(recursiveCount = 0) { const currentQueue = this.getCurrentQueue(); const isLastTrack = this.currentQueueIndex >= currentQueue.length - 1; if (recursiveCount > currentQueue.length) { if (this.radioEnabled && isLastTrack) { - this.fetchRadioRecommendations().then(() => { + this.fetchRadioRecommendations().then(async () => { const updatedQueue = this.getCurrentQueue(); if (this.currentQueueIndex < updatedQueue.length - 1) { - this.playNext(0); + await this.playNext(0); } }); return; } if (this.artistPopularTracksState.artistId && this.artistPopularTracksState.hasMore) { - this.fetchMoreArtistPopularTracks().then((newTracks) => { + await this.fetchMoreArtistPopularTracks().then(async (newTracks) => { if (newTracks && newTracks.length > 0) { - this.addToQueue(newTracks); - this.playNext(0); + await this.addToQueue(newTracks); + await this.playNext(0); } else { this.activeElement.pause(); } @@ -1088,52 +1090,54 @@ export class Player { return; } - import('./storage.js').then(({ contentBlockingSettings }) => { - if ( - this.repeatMode === REPEAT_MODE.ONE && - !currentQueue[this.currentQueueIndex]?.isUnavailable && - !contentBlockingSettings.shouldHideTrack(currentQueue[this.currentQueueIndex]) - ) { - this.playTrackFromQueue(0, recursiveCount); - return; - } - - if (!isLastTrack) { - this.currentQueueIndex++; - const track = currentQueue[this.currentQueueIndex]; - if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) { - return this.playNext(recursiveCount + 1); + import('./storage.js') + .then(async ({ contentBlockingSettings }) => { + if ( + this.repeatMode === REPEAT_MODE.ONE && + !currentQueue[this.currentQueueIndex]?.isUnavailable && + !contentBlockingSettings.shouldHideTrack(currentQueue[this.currentQueueIndex]) + ) { + await this.playTrackFromQueue(0, recursiveCount); + return; } - } else if (this.radioEnabled) { - this.fetchRadioRecommendations().then(() => { - const updatedQueue = this.getCurrentQueue(); - if (this.currentQueueIndex < updatedQueue.length - 1) { - this.playNext(0); - } - }); - return; - } else if (this.artistPopularTracksState.artistId && this.artistPopularTracksState.hasMore) { - this.fetchMoreArtistPopularTracks().then((newTracks) => { - if (newTracks && newTracks.length > 0) { - this.addToQueue(newTracks); - } - // Now play the next track (which is now at currentQueueIndex + 1 if tracks were added) + + if (!isLastTrack) { this.currentQueueIndex++; - this.playTrackFromQueue(0, recursiveCount); - }); - return; - } else if (this.repeatMode === REPEAT_MODE.ALL) { - this.currentQueueIndex = 0; - const track = currentQueue[this.currentQueueIndex]; - if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) { - return this.playNext(recursiveCount + 1); + const track = currentQueue[this.currentQueueIndex]; + if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) { + return this.playNext(recursiveCount + 1); + } + } else if (this.radioEnabled) { + this.fetchRadioRecommendations().then(async () => { + const updatedQueue = this.getCurrentQueue(); + if (this.currentQueueIndex < updatedQueue.length - 1) { + await this.playNext(0); + } + }); + return; + } else if (this.artistPopularTracksState.artistId && this.artistPopularTracksState.hasMore) { + await this.fetchMoreArtistPopularTracks().then(async (newTracks) => { + if (newTracks && newTracks.length > 0) { + await this.addToQueue(newTracks); + } + // Now play the next track (which is now at currentQueueIndex + 1 if tracks were added) + this.currentQueueIndex++; + await this.playTrackFromQueue(0, recursiveCount); + }); + return; + } else if (this.repeatMode === REPEAT_MODE.ALL) { + this.currentQueueIndex = 0; + const track = currentQueue[this.currentQueueIndex]; + if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) { + return this.playNext(recursiveCount + 1); + } + } else { + return; } - } else { - return; - } - this.playTrackFromQueue(0, recursiveCount); - }); + await this.playTrackFromQueue(0, recursiveCount); + }) + .catch(console.error); } async enableRadio(seeds = []) { @@ -1141,20 +1145,20 @@ export class Player { radioSettings.setEnabled(true); if (seeds.length === 0) { - this.wipeQueue(); + await this.wipeQueue(); const pickedSeeds = await this.pickRadioSeeds(); if (pickedSeeds.length > 0) { this.radioSeeds = pickedSeeds; const initialQueue = [...pickedSeeds].sort(() => 0.5 - Math.random()).slice(0, 5); - this.setQueue(initialQueue, 0, true); - this.playAtIndex(0); + await this.setQueue(initialQueue, 0, true); + await this.playAtIndex(0); } } else { this.radioSeeds = Array.isArray(seeds) ? seeds : [seeds]; - this.wipeQueue(); + await this.wipeQueue(); const initialQueue = Array.isArray(seeds) ? seeds.slice(0, 5) : [seeds]; - this.setQueue(initialQueue, 0, true); - this.playAtIndex(0); + await this.setQueue(initialQueue, 0, true); + await this.playAtIndex(0); } const currentQueue = this.getCurrentQueue(); @@ -1217,7 +1221,7 @@ export class Player { if (newTracks.length > 0) { const tracksToAdd = newTracks.sort(() => 0.5 - Math.random()).slice(0, 5); - this.addToQueue(tracksToAdd); + await this.addToQueue(tracksToAdd); } } } catch (error) { @@ -1304,13 +1308,15 @@ export class Player { return; } - import('./storage.js').then(({ contentBlockingSettings }) => { - const track = currentQueue[this.currentQueueIndex]; - if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) { - return this.playPrev(recursiveCount + 1); - } - this.playTrackFromQueue(0, recursiveCount); - }); + import('./storage.js') + .then(async ({ contentBlockingSettings }) => { + const track = currentQueue[this.currentQueueIndex]; + if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) { + return this.playPrev(recursiveCount + 1); + } + await this.playTrackFromQueue(0, recursiveCount); + }) + .catch(console.error); } } @@ -1318,28 +1324,28 @@ export class Player { return this.currentTrack?.type === 'video' ? this.video : this.audio; } - handlePlayPause() { + async handlePlayPause() { const el = this.activeElement; const hasSource = el.src || el.currentSrc || el.srcObject || this.shakaInitialized; if (!hasSource || el.error) { if (this.currentTrack) { - this.playTrackFromQueue(0, 0); + await this.playTrackFromQueue(0, 0); } return; } if (el.paused) { - this.safePlay(el).catch((e) => { + this.safePlay(el).catch(async (e) => { if (e.name === 'NotAllowedError' || e.name === 'AbortError') return; console.error('Play failed, reloading track:', e); if (this.currentTrack) { - this.playTrackFromQueue(0, 0); + await this.playTrackFromQueue(0, 0); } }); } else { el.pause(); - this.saveQueueState(); + await this.saveQueueState(); } } @@ -1358,7 +1364,7 @@ export class Player { this.updateMediaSessionPositionState(); } - toggleShuffle() { + async toggleShuffle() { this.shuffleActive = !this.shuffleActive; if (this.shuffleActive) { @@ -1389,17 +1395,17 @@ export class Player { } this.preloadCache.clear(); - this.preloadNextTracks(); - this.saveQueueState(); + void this.preloadNextTracks().catch(console.error); + await this.saveQueueState(); } - toggleRepeat() { + async toggleRepeat() { this.repeatMode = (this.repeatMode + 1) % 3; - this.saveQueueState(); + await this.saveQueueState(); return this.repeatMode; } - setQueue(tracks, startIndex = 0, isRadio = false) { + async setQueue(tracks, startIndex = 0, isRadio = false) { if (!isRadio) { this.disableRadio(); } @@ -1407,7 +1413,7 @@ export class Player { this.currentQueueIndex = startIndex; this.shuffleActive = false; this.preloadCache.clear(); - this.saveQueueState(); + await this.saveQueueState(); } setArtistPopularTracksContext(artistId, initialTracks, offset = 15, hasMore = true) { @@ -1474,7 +1480,7 @@ export class Player { } } - addToQueue(trackOrTracks) { + async addToQueue(trackOrTracks) { const tracks = Array.isArray(trackOrTracks) ? trackOrTracks : [trackOrTracks]; this.queue.push(...tracks); @@ -1485,12 +1491,12 @@ export class Player { if (!this.currentTrack || this.currentQueueIndex === -1) { this.currentQueueIndex = this.getCurrentQueue().length - tracks.length; - this.playTrackFromQueue(0, 0); + await this.playTrackFromQueue(0, 0); } - this.saveQueueState(); + await this.saveQueueState(); } - addNextToQueue(trackOrTracks) { + async addNextToQueue(trackOrTracks) { const tracks = Array.isArray(trackOrTracks) ? trackOrTracks : [trackOrTracks]; const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; const insertIndex = this.currentQueueIndex + 1; @@ -1504,11 +1510,11 @@ export class Player { this.originalQueueBeforeShuffle.push(...tracks); // Sync original queue } - this.saveQueueState(); - this.preloadNextTracks(); // Update preload since next track changed + await this.saveQueueState(); + void this.preloadNextTracks().catch(console.error); // Update preload since next track changed } - removeFromQueue(index) { + async removeFromQueue(index) { const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; // If removing current track @@ -1532,11 +1538,11 @@ export class Player { } } - this.saveQueueState(); - this.preloadNextTracks(); + await this.saveQueueState(); + void this.preloadNextTracks().catch(console.error); } - clearQueue() { + async clearQueue() { if (this.currentTrack) { this.queue = [this.currentTrack]; @@ -1556,10 +1562,10 @@ export class Player { } this.preloadCache.clear(); - this.saveQueueState(); + await this.saveQueueState(); } - wipeQueue() { + async wipeQueue() { const el = this.activeElement; el.pause(); el.src = ''; @@ -1568,16 +1574,16 @@ export class Player { this.shuffledQueue = []; this.originalQueueBeforeShuffle = []; this.currentQueueIndex = -1; - this.saveQueueState(); + await this.saveQueueState(); if (UIRenderer.instance) { UIRenderer.instance.setCurrentTrack(null); } if (window.renderQueueFunction) { - window.renderQueueFunction(); + await window.renderQueueFunction(); } } - moveInQueue(fromIndex, toIndex) { + async moveInQueue(fromIndex, toIndex) { const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; if (fromIndex < 0 || fromIndex >= currentQueue.length) return; @@ -1593,7 +1599,7 @@ export class Player { } else if (fromIndex > this.currentQueueIndex && toIndex <= this.currentQueueIndex) { this.currentQueueIndex++; } - this.saveQueueState(); + await this.saveQueueState(); } getCurrentQueue() { diff --git a/js/playlist-importer.js b/js/playlist-importer.js index 08332c6..a680497 100644 --- a/js/playlist-importer.js +++ b/js/playlist-importer.js @@ -41,11 +41,11 @@ function getTrackArtists(track) { /** * Generates CSV playlist export - * @param {Object} playlist - Playlist metadata + * @param {Object} _playlist - Playlist metadata * @param {Array} tracks - Array of track objects * @returns {string} CSV content */ -export function generateCSV(playlist, tracks) { +export function generateCSV(_playlist, tracks) { const headers = ['Track Name', 'Artist Name(s)', 'Album', 'Duration']; let content = headers.map((h) => `"${h}"`).join(',') + '\n'; diff --git a/js/profile.js b/js/profile.js index eeec04f..b9e8f42 100644 --- a/js/profile.js +++ b/js/profile.js @@ -248,30 +248,31 @@ export async function loadProfile(username) { } if (profile.lastfm_username && profile.privacy?.lastfm !== 'private') { - fetchLastFmRecentTracks(profile.lastfm_username).then(async (tracks) => { - if (tracks.length > 0) { - recentSection.style.display = 'block'; - recentContainer.innerHTML = tracks - .map((track, index) => { - const isNowPlaying = track['@attr']?.nowplaying === 'true'; - let image = getLastFmImage(track.image); - const hasImage = !!image; - if (!image) image = '/assets/appicon.png'; + fetchLastFmRecentTracks(profile.lastfm_username) + .then(async (tracks) => { + if (tracks.length > 0) { + recentSection.style.display = 'block'; + recentContainer.innerHTML = tracks + .map((track, index) => { + const isNowPlaying = track['@attr']?.nowplaying === 'true'; + let image = getLastFmImage(track.image); + const hasImage = !!image; + if (!image) image = '/assets/appicon.png'; - track._imgId = `scrobble-img-${index}`; - track._needsCover = !hasImage; + track._imgId = `scrobble-img-${index}`; + track._needsCover = !hasImage; - let dateDisplay = ''; - if (isNowPlaying) dateDisplay = 'Scrobbling now'; - else if (track.date) { - const date = new Date(track.date.uts * 1000); - dateDisplay = - date.toLocaleDateString() + - ' ' + - date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } + let dateDisplay = ''; + if (isNowPlaying) dateDisplay = 'Scrobbling now'; + else if (track.date) { + const date = new Date(track.date.uts * 1000); + dateDisplay = + date.toLocaleDateString() + + ' ' + + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } - return ` + return `
@@ -283,39 +284,45 @@ export async function loadProfile(username) {
${dateDisplay}
`; - }) - .join(''); + }) + .join(''); - recentContainer.querySelectorAll('.track-item').forEach((item) => { - item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist)); - item.addEventListener('contextmenu', (e) => { - e.preventDefault(); - return false; + recentContainer.querySelectorAll('.track-item').forEach((item) => { + item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist)); + item.addEventListener('contextmenu', (e) => { + e.preventDefault(); + return false; + }); }); - }); - for (const track of tracks) { - if (track._needsCover) { - fetchFallbackCover(track.name, track.artist?.['#text'] || track.artist?.name, track._imgId); + for (const track of tracks) { + if (track._needsCover) { + await fetchFallbackCover( + track.name, + track.artist?.['#text'] || track.artist?.name, + track._imgId + ); + } } } - } - }); + }) + .catch(console.error); - fetchLastFmTopArtists(profile.lastfm_username).then(async (artists) => { - if (artists.length > 0 && topArtistsSection && topArtistsContainer) { - topArtistsSection.style.display = 'block'; - topArtistsContainer.innerHTML = artists - .map((artist, index) => { - let image = getLastFmImage(artist.image); - const hasImage = !!image; - if (!image) image = '/assets/appicon.png'; + fetchLastFmTopArtists(profile.lastfm_username) + .then(async (artists) => { + if (artists.length > 0 && topArtistsSection && topArtistsContainer) { + topArtistsSection.style.display = 'block'; + topArtistsContainer.innerHTML = artists + .map((artist, index) => { + let image = getLastFmImage(artist.image); + const hasImage = !!image; + if (!image) image = '/assets/appicon.png'; - const imgId = `top-artist-img-${index}`; - artist._imgId = imgId; - artist._needsCover = !hasImage; + const imgId = `top-artist-img-${index}`; + artist._imgId = imgId; + artist._needsCover = !hasImage; - return ` + return `
@@ -326,45 +333,47 @@ export async function loadProfile(username) {
`; - }) - .join(''); + }) + .join(''); - topArtistsContainer.querySelectorAll('.card').forEach((card) => { - card.addEventListener('click', () => handleArtistClick(card.dataset.name)); - card.addEventListener('contextmenu', (e) => { - e.preventDefault(); - return false; + topArtistsContainer.querySelectorAll('.card').forEach((card) => { + card.addEventListener('click', () => handleArtistClick(card.dataset.name)); + card.addEventListener('contextmenu', (e) => { + e.preventDefault(); + return false; + }); }); - }); - for (const artist of artists) { - if (artist._needsCover) { - fetchFallbackArtistImage(artist.name, artist._imgId); + for (const artist of artists) { + if (artist._needsCover) { + await fetchFallbackArtistImage(artist.name, artist._imgId); + } } } - } - }); + }) + .catch(console.error); - fetchLastFmTopAlbums(profile.lastfm_username).then(async (albums) => { - if (albums.length > 0 && topAlbumsSection && topAlbumsContainer) { - topAlbumsSection.style.display = 'block'; - topAlbumsContainer.innerHTML = albums - .map((album, index) => { - let image = getLastFmImage(album.image); - const hasImage = !!image; - if (!image) image = '/assets/appicon.png'; + fetchLastFmTopAlbums(profile.lastfm_username) + .then(async (albums) => { + if (albums.length > 0 && topAlbumsSection && topAlbumsContainer) { + topAlbumsSection.style.display = 'block'; + topAlbumsContainer.innerHTML = albums + .map((album, index) => { + let image = getLastFmImage(album.image); + const hasImage = !!image; + if (!image) image = '/assets/appicon.png'; - const imgId = `top-album-img-${index}`; - album._imgId = imgId; - album._needsCover = !hasImage; + const imgId = `top-album-img-${index}`; + album._imgId = imgId; + album._needsCover = !hasImage; - const artistName = - album.artist?.name || - album.artist?.['#text'] || - (typeof album.artist === 'string' ? album.artist : 'Unknown Artist'); - album._artistName = artistName; + const artistName = + album.artist?.name || + album.artist?.['#text'] || + (typeof album.artist === 'string' ? album.artist : 'Unknown Artist'); + album._artistName = artistName; - return ` + return `
@@ -375,45 +384,47 @@ export async function loadProfile(username) {
`; - }) - .join(''); + }) + .join(''); - topAlbumsContainer.querySelectorAll('.card').forEach((card) => { - card.addEventListener('click', () => handleAlbumClick(card.dataset.name, card.dataset.artist)); - card.addEventListener('contextmenu', (e) => { - e.preventDefault(); - return false; + topAlbumsContainer.querySelectorAll('.card').forEach((card) => { + card.addEventListener('click', () => handleAlbumClick(card.dataset.name, card.dataset.artist)); + card.addEventListener('contextmenu', (e) => { + e.preventDefault(); + return false; + }); }); - }); - for (const album of albums) { - if (album._needsCover) { - fetchFallbackAlbumCover(album.name, album._artistName, album._imgId); + for (const album of albums) { + if (album._needsCover) { + await fetchFallbackAlbumCover(album.name, album._artistName, album._imgId); + } } } - } - }); + }) + .catch(console.error); - fetchLastFmTopTracks(profile.lastfm_username).then(async (tracks) => { - if (tracks.length > 0 && topTracksSection && topTracksContainer) { - topTracksSection.style.display = 'block'; - topTracksContainer.innerHTML = tracks - .map((track, index) => { - let image = getLastFmImage(track.image); - const hasImage = !!image; - if (!image) image = '/assets/appicon.png'; + fetchLastFmTopTracks(profile.lastfm_username) + .then(async (tracks) => { + if (tracks.length > 0 && topTracksSection && topTracksContainer) { + topTracksSection.style.display = 'block'; + topTracksContainer.innerHTML = tracks + .map((track, index) => { + let image = getLastFmImage(track.image); + const hasImage = !!image; + if (!image) image = '/assets/appicon.png'; - const imgId = `top-track-img-${index}`; - track._imgId = imgId; - track._needsCover = !hasImage; + const imgId = `top-track-img-${index}`; + track._imgId = imgId; + track._needsCover = !hasImage; - const artistName = - track.artist?.name || - track.artist?.['#text'] || - (typeof track.artist === 'string' ? track.artist : 'Unknown Artist'); - track._artistName = artistName; + const artistName = + track.artist?.name || + track.artist?.['#text'] || + (typeof track.artist === 'string' ? track.artist : 'Unknown Artist'); + track._artistName = artistName; - return ` + return `
@@ -425,24 +436,25 @@ export async function loadProfile(username) {
${parseInt(track.playcount).toLocaleString()} plays
`; - }) - .join(''); + }) + .join(''); - topTracksContainer.querySelectorAll('.track-item').forEach((item) => { - item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist)); - item.addEventListener('contextmenu', (e) => { - e.preventDefault(); - return false; + topTracksContainer.querySelectorAll('.track-item').forEach((item) => { + item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist)); + item.addEventListener('contextmenu', (e) => { + e.preventDefault(); + return false; + }); }); - }); - for (const track of tracks) { - if (track._needsCover) { - fetchFallbackCover(track.name, track._artistName, track._imgId); + for (const track of tracks) { + if (track._needsCover) { + await fetchFallbackCover(track.name, track._artistName, track._imgId); + } } } - } - }); + }) + .catch(console.error); } const currentUser = await syncManager.getUserData(); @@ -483,8 +495,8 @@ export async function loadProfile(username) { } } -export function openEditProfile() { - syncManager.getUserData().then((data) => { +export async function openEditProfile() { + await syncManager.getUserData().then((data) => { if (!data || !data.profile) return; const p = data.profile; @@ -566,7 +578,7 @@ async function saveProfile() { try { await syncManager.updateProfile(data); editProfileModal.classList.remove('active'); - loadProfile(newUsername); + await loadProfile(newUsername); if (window.location.pathname.includes('/user/@')) { window.history.replaceState(null, '', `/user/@${newUsername}`); @@ -589,7 +601,7 @@ viewMyProfileBtn.addEventListener('click', async () => { if (data && data.profile && data.profile.username) { navigate(`/user/@${data.profile.username}`); } else { - openEditProfile(); + await openEditProfile(); } }); diff --git a/js/progressEvents.ts b/js/progressEvents.ts index d3618b0..61be090 100644 --- a/js/progressEvents.ts +++ b/js/progressEvents.ts @@ -1,9 +1,9 @@ declare global { - type MonochromeProgress = { + type MonochromeProgress = { stage: string; } & T; - type MonochromeProgressMessage = { + type MonochromeProgressMessage<_T = MonochromeProgress> = { message: string; }; diff --git a/js/settings.js b/js/settings.js index 19951e6..7900e13 100644 --- a/js/settings.js +++ b/js/settings.js @@ -48,7 +48,7 @@ import { db } from './db.js'; import { authManager } from './accounts/auth.js'; import { syncManager } from './accounts/pocketbase.js'; import { containerFormats, customFormats } from './ffmpegFormats.ts'; -import { modernSettings } from './ModernSettings.js'; +import { BulkDownloadMethod, modernSettings } from './ModernSettings.js'; async function getButterchurnPresets(...args) { const butterchurnModule = await import('./visualizers/butterchurn.js'); @@ -943,10 +943,10 @@ export async function initializeSettings(scrobbler, player, api, ui) { const showQualityBadgesToggle = document.getElementById('show-quality-badges-toggle'); if (showQualityBadgesToggle) { showQualityBadgesToggle.checked = qualityBadgeSettings.isEnabled(); - showQualityBadgesToggle.addEventListener('change', (e) => { + showQualityBadgesToggle.addEventListener('change', async (e) => { qualityBadgeSettings.setEnabled(e.target.checked); // Re-render queue if available, but don't force navigation to library - if (window.renderQueueFunction) window.renderQueueFunction(); + if (window.renderQueueFunction) await window.renderQueueFunction(); }); } @@ -979,15 +979,15 @@ export async function initializeSettings(scrobbler, player, api, ui) { if (!forceZipBlobSettingItem) return; const method = modernSettings.bulkDownloadMethod; // Only relevant when zip method is selected and the browser supports streaming - const visible = method === 'zip' && hasFileSystemAccess; + const visible = method === BulkDownloadMethod.Zip && hasFileSystemAccess; forceZipBlobSettingItem.style.display = visible ? '' : 'none'; } /** Shows/hides folder-picker-specific and folder-method settings */ async function updateFolderMethodVisibility() { const method = modernSettings.bulkDownloadMethod; - const isFolderMethod = method === 'folder'; - const isFolderOrLocal = isFolderMethod || method === 'local'; + const isFolderMethod = method === BulkDownloadMethod.Folder; + const isFolderOrLocal = isFolderMethod || method === BulkDownloadMethod.LocalMedia; if (rememberFolderSetting) { rememberFolderSetting.style.display = isFolderMethod && hasFolderPicker ? '' : 'none'; @@ -1022,8 +1022,8 @@ export async function initializeSettings(scrobbler, player, api, ui) { } // If the stored method is 'folder' or 'local' without native support, fall back to 'zip' const currentMethod = modernSettings.bulkDownloadMethod; - if (currentMethod === 'folder' || currentMethod === 'local') { - modernSettings.bulkDownloadMethod = 'zip'; + if (currentMethod === BulkDownloadMethod.Folder || currentMethod === BulkDownloadMethod.LocalMedia) { + modernSettings.bulkDownloadMethod = BulkDownloadMethod.Zip; } } bulkDownloadMethod.value = modernSettings.bulkDownloadMethod; @@ -1033,7 +1033,7 @@ export async function initializeSettings(scrobbler, player, api, ui) { modernSettings.bulkDownloadMethod = newMethod; // When switching to 'local', prompt to select the local media folder if not yet configured - if (newMethod === 'local') { + if (newMethod === BulkDownloadMethod.LocalMedia) { const existingHandle = await db.getSetting('local_folder_handle'); if (!existingHandle) { let picked = false; @@ -1329,12 +1329,12 @@ export async function initializeSettings(scrobbler, player, api, ui) { autoeqHeadphoneSelect.appendChild(optgroup); // When user picks a popular headphone from the dropdown, load it - autoeqHeadphoneSelect.addEventListener('change', () => { + autoeqHeadphoneSelect.addEventListener('change', async () => { const selected = autoeqHeadphoneSelect.value; if (!selected) return; const popularEntry = POPULAR_HEADPHONES.find((hp) => hp.name === selected); if (popularEntry && (!autoeqSelectedEntry || autoeqSelectedEntry.name !== selected)) { - loadHeadphoneEntry(popularEntry); + await loadHeadphoneEntry(popularEntry); } }); } @@ -2433,7 +2433,12 @@ export async function initializeSettings(scrobbler, player, api, ui) { } const x = freqToX(f, pw); const y = mid - (Math.max(-dbRange, Math.min(dbRange, total)) / dbRange) * mid * 0.9; - first ? (ctx.moveTo(x, y), (first = false)) : ctx.lineTo(x, y); + if (first) { + ctx.moveTo(x, y); + first = false; + } else { + ctx.lineTo(x, y); + } } ctx.strokeStyle = 'rgba(255,255,255,0.9)'; ctx.lineWidth = 2; @@ -2650,7 +2655,7 @@ export async function initializeSettings(scrobbler, player, api, ui) { modelMap.get(baseName).push(entry); }); - modelMap.forEach((variants, name) => { + modelMap.forEach(async (variants, name) => { const wrapper = document.createElement('div'); const rawFirstChar = name[0]?.toUpperCase() || '#'; const firstLetter = /^[A-Z]$/.test(rawFirstChar) ? rawFirstChar : '#'; @@ -2676,19 +2681,19 @@ export async function initializeSettings(scrobbler, player, api, ui) { const subList = document.createElement('div'); subList.className = 'autoeq-db-sub-list'; - variants.forEach((entry) => { + for (const entry of variants) { const subItem = document.createElement('div'); subItem.className = 'autoeq-db-sub-item'; // Extract source from parentheses - const sourceMatch = entry.name.match(/\(([^)]+)\)\s*$/); + const sourceMatch = await entry.name.match(/\(([^)]+)\)\s*$/); const source = sourceMatch ? sourceMatch[1] : entry.type; subItem.innerHTML = `${entry.name}${source}`; - subItem.addEventListener('click', (e) => { + subItem.addEventListener('click', async (e) => { e.stopPropagation(); - loadHeadphoneEntry(entry); + await loadHeadphoneEntry(entry); }); subList.appendChild(subItem); - }); + } wrapper.appendChild(subList); @@ -4409,7 +4414,7 @@ export async function initializeSettings(scrobbler, player, api, ui) { if (initParaProfiles) initParaProfiles.style.display = 'none'; // Auto-load headphone database - loadFullDatabase(); + await loadFullDatabase(); // Auto-load default popular headphone if no saved profile is active const activeProfileId = equalizerSettings.getActiveAutoEQProfile(); @@ -4432,7 +4437,7 @@ export async function initializeSettings(scrobbler, player, api, ui) { if (autoeqRunBtn) autoeqRunBtn.disabled = false; requestAnimationFrame(drawAutoEQGraph); } else if (POPULAR_HEADPHONES.length > 0) { - loadHeadphoneEntry(POPULAR_HEADPHONES[0]); + await loadHeadphoneEntry(POPULAR_HEADPHONES[0]); } } @@ -4990,7 +4995,7 @@ export async function initializeSettings(scrobbler, player, api, ui) { const currentSource = homePageSettings.getEditorsPicksSource(); editorsPicksSourceSelect.value = currentSource; } - populateEditorsPicksSource(); + await populateEditorsPicksSource(); editorsPicksSourceSelect.addEventListener('change', (e) => { homePageSettings.setEditorsPicksSource(e.target.value); @@ -5365,7 +5370,7 @@ export async function initializeSettings(scrobbler, player, api, ui) { try { await syncManager.clearCloudData(); alert('Cloud data cleared successfully.'); - authManager.signOut(); + await authManager.signOut(); } catch (error) { console.error('Failed to clear cloud data:', error); alert('Failed to clear cloud data: ' + error.message); @@ -5716,7 +5721,7 @@ function initializeFontSettings() { }); // Google Fonts apply - fontGoogleApply.addEventListener('click', () => { + fontGoogleApply.addEventListener('click', async () => { const input = fontGoogleInput.value.trim(); if (!input) return; @@ -5735,16 +5740,16 @@ function initializeFontSettings() { // Not a URL, treat as font name } - fontSettings.loadGoogleFont(fontName); + await fontSettings.loadGoogleFont(fontName); }); // URL font apply - fontUrlApply.addEventListener('click', () => { + fontUrlApply.addEventListener('click', async () => { const url = fontUrlInput.value.trim(); const name = fontUrlName.value.trim(); if (!url) return; - fontSettings.loadFontFromUrl(url, name || 'CustomFont'); + await fontSettings.loadFontFromUrl(url, name || 'CustomFont'); }); // File upload diff --git a/js/side-panel.js b/js/side-panel.js index c3b6d8f..0f399ea 100644 --- a/js/side-panel.js +++ b/js/side-panel.js @@ -118,25 +118,25 @@ export class SidePanelManager { return this.currentView === view && this.panel.classList.contains('active'); } - refresh(view, renderControlsCallback, renderContentCallback, options = {}) { + async refresh(view, renderControlsCallback, renderContentCallback, options = {}) { if (this.isActive(view)) { if (renderControlsCallback) { this.controlsElement.innerHTML = ''; - renderControlsCallback(this.controlsElement); + await renderControlsCallback(this.controlsElement); } if (renderContentCallback) { if (!options.noClear) { this.contentElement.innerHTML = ''; } - renderContentCallback(this.contentElement); + await renderContentCallback(this.contentElement); } } } - updateContent(view, renderContentCallback) { + async updateContent(view, renderContentCallback) { if (this.isActive(view)) { this.contentElement.innerHTML = ''; - renderContentCallback(this.contentElement); + await renderContentCallback(this.contentElement); } } } diff --git a/js/storage.js b/js/storage.js index 78cb6b0..edce8ba 100644 --- a/js/storage.js +++ b/js/storage.js @@ -2653,18 +2653,18 @@ export const fontSettings = { document.documentElement.style.setProperty('--font-family', "'SF Pro Display', sans-serif"); }, - applyFont() { + async applyFont() { const config = this.getConfig(); switch (config.type) { case 'google': - this.loadGoogleFont(config.family); + await this.loadGoogleFont(config.family); break; case 'url': - this.loadFontFromUrl(config.url, config.family); + await this.loadFontFromUrl(config.url, config.family); break; case 'uploaded': - this.loadUploadedFont(config.fontId); + await this.loadUploadedFont(config.fontId); break; case 'preset': default: diff --git a/js/taglib.ts b/js/taglib.ts index 8866aa2..d22c8f3 100644 --- a/js/taglib.ts +++ b/js/taglib.ts @@ -22,7 +22,11 @@ export async function withTimeout(callback: () => Promise, timeout: number }) .catch((err) => { clearTimeout(timer); - reject(err); + if (err instanceof Error) { + reject(err); + } else { + reject(new Error(String(err))); + } }); }); } @@ -33,7 +37,7 @@ function toUint8Array(audioData: ArrayBufferLike | Uint8Array) { } return doTimed( - `Converting audio data (${(audioData as any)?.constructor?.name}) to Uint8Array`, + `Converting audio data (${(audioData as object)?.constructor?.name}) to Uint8Array`, () => new Uint8Array(audioData) ); } @@ -60,7 +64,7 @@ async function convertInputToTaglib( return (await doTimedAsync('Reading File from FileSystemHandle as Uint8Array', async () => { const file = await audioData.getFile(); const arrayBuffer = await file.arrayBuffer(); - return await toUint8Array(arrayBuffer); + return toUint8Array(arrayBuffer); })) as R; } else if ( !(audioData instanceof Uint8Array) && @@ -69,7 +73,7 @@ async function convertInputToTaglib( !('FileSystemFileEntry' in globalThis && audioData instanceof FileSystemFileEntry) && !('FileSystemFileHandle' in globalThis && audioData instanceof FileSystemFileHandle) ) { - return toUint8Array(audioData as any) as R; + return toUint8Array(audioData as unknown as ArrayBufferLike) as R; } return audioData as R; @@ -114,19 +118,19 @@ export async function addMetadataWithTagLib( if (error) { reject(new Error(error)); } else { - resolve(data!); + resolve(data); } }; worker.onerror = reject; worker.onmessageerror = reject; const transferables: Transferable[] = []; - if ((audioData as any)?.buffer instanceof ArrayBuffer) { - transferables.push((audioData as any).buffer); + if ((audioData as Uint8Array)?.buffer instanceof ArrayBuffer) { + transferables.push((audioData as Uint8Array).buffer); } - if ((data as any).cover?.data?.buffer instanceof ArrayBuffer) { - transferables.push((data as any).cover.data.buffer); + if (data.cover?.data?.buffer instanceof ArrayBuffer) { + transferables.push(data.cover.data.buffer); } worker.postMessage({ ...data, type: 'Add', audioData, filename }, transferables); @@ -168,15 +172,15 @@ export async function getMetadataWithTagLib( if (error) { reject(new Error(error)); } else { - resolve(data!); + resolve(data); } }; worker.onerror = reject; worker.onmessageerror = reject; const transferables: Transferable[] = []; - if ((audioData as any)?.buffer instanceof ArrayBuffer) { - transferables.push((audioData as any).buffer); + if ((audioData as Uint8Array)?.buffer instanceof ArrayBuffer) { + transferables.push((audioData as Uint8Array).buffer); } worker.postMessage({ type: 'Get', audioData, filename }, transferables); }), diff --git a/js/taglib.types.ts b/js/taglib.types.ts index 45b3b72..6bdde9a 100644 --- a/js/taglib.types.ts +++ b/js/taglib.types.ts @@ -52,7 +52,6 @@ export enum Mp4Stik { WhackedBookmark = 5, MusicVideo = 6, Movie = 9, - ShortFilm = 9, TVShow = 10, Booklet = 11, } diff --git a/js/taglib.worker.ts b/js/taglib.worker.ts index b50b5f0..71a0183 100644 --- a/js/taglib.worker.ts +++ b/js/taglib.worker.ts @@ -1,8 +1,8 @@ // filepath: /workspaces/monochrome/js/taglib.worker.ts -declare var self: DedicatedWorkerGlobalScope; +declare let self: DedicatedWorkerGlobalScope; import { ByteVector } from '!/@dantheman827/taglib-ts/src/byteVector.js'; -import { Mp4Tag, Mp4Item } from '!/@dantheman827/taglib-ts/src/mp4/mp4Tag.js'; +import { Mp4Item } from '!/@dantheman827/taglib-ts/src/mp4/mp4Tag.js'; import { Variant } from '!/@dantheman827/taglib-ts/src/toolkit/variant.js'; import { doTimed, doTimedAsync } from './doTimed'; import { @@ -10,7 +10,6 @@ import { type _AddMetadataMessage, type _GetMetadataMessage, type AddMetadataMessage, - type GetMetadataMessage, type TagLibFileResponse, type TagLibMetadata, type TagLibMetadataResponse, @@ -18,6 +17,7 @@ import { type TagLibWorkerMessage, type TagLibWorkerResponse, } from './taglib.types'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars import { File as TagLibFile } from '!/@dantheman827/taglib-ts/src/file.js'; import { FileRef } from '!/@dantheman827/taglib-ts/src/fileRef.js'; import { ChunkedByteVectorStream } from '!/@dantheman827/taglib-ts/src/toolkit/chunkedByteVectorStream.js'; @@ -38,7 +38,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise< const { audioData, audioRef, - filename, + filename: _filename, title, artist, writeArtistsSeparately = false, @@ -79,7 +79,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise< const isMp4 = underlying instanceof Mp4File; const isMpeg = underlying instanceof MpegFile; const isOgg = underlying instanceof OggVorbisFile; - const isWav = underlying instanceof WavFile; + const _isWav = underlying instanceof WavFile; const needsCombinedTrackDisc = isMp4 || isMpeg; @@ -137,7 +137,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise< if (copyright) props.replace('COPYRIGHT', [copyright]); if (isrc) props.replace('ISRC', [isrc]); if (isrc && isMp4) { - const mp4Tag = (underlying as Mp4File).tag() as Mp4Tag; + const mp4Tag = underlying.tag(); mp4Tag.setItem('xid ', Mp4Item.fromStringList([`:isrc:${isrc}`])); } if (upc) props.replace('UPC', [upc]); @@ -145,8 +145,8 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise< if (explicit !== undefined) { if (isMp4) { - // rtng is a byte item - must be set directly on the Mp4Tag - const mp4Tag = (underlying as Mp4File).tag() as Mp4Tag; + // rtng is a byte item — must be set directly on the Mp4Tag + const mp4Tag = underlying.tag(); mp4Tag.setItem('rtng', Mp4Item.fromByte(explicit ? 1 : 0)); } else { props.replace('ITUNESADVISORY', [explicit ? '1' : '0']); @@ -154,7 +154,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise< } if (stik != null && isMp4) { - const mp4Tag = (underlying as Mp4File).tag() as Mp4Tag; + const mp4Tag = underlying.tag(); mp4Tag.setItem('stik', Mp4Item.fromByte(stik)); } @@ -177,7 +177,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise< await ref.save(); }); - const file = ref.file() as TagLibFile; + const file = ref.file(); if (!file) return audioData; const stream = file.stream(); @@ -207,7 +207,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise< } export async function getMetadataFromAudio(message: _GetMetadataMessage): Promise { - const { audioData, audioRef, filename } = message; + const { audioData, audioRef } = message; const data: TagLibReadMetadata = { duration: 0 }; const ref = @@ -263,7 +263,7 @@ export async function getMetadataFromAudio(message: _GetMetadataMessage): Promis data.isrc = props.get('ISRC')?.[0] || undefined; if (isMp4) { - const mp4Tag = (underlying as Mp4File).tag() as Mp4Tag; + const mp4Tag = underlying.tag(); data.explicit = mp4Tag.item('rtng')?.toByte() === 1; } else { data.explicit = props.get('ITUNESADVISORY')?.[0] === '1'; diff --git a/js/themeStore.js b/js/themeStore.js index 2f2cd19..27d2bb3 100644 --- a/js/themeStore.js +++ b/js/themeStore.js @@ -48,9 +48,9 @@ export class ThemeStore { } init() { - document.getElementById('open-theme-store-btn')?.addEventListener('click', () => { + document.getElementById('open-theme-store-btn')?.addEventListener('click', async () => { this.modal.classList.add('active'); - this.loadThemes(); + await this.loadThemes(); }); this.modal?.querySelector('.close-modal-btn')?.addEventListener('click', () => { @@ -59,14 +59,14 @@ export class ThemeStore { const tabs = this.modal?.querySelectorAll('.search-tab'); tabs?.forEach((tab) => { - tab.addEventListener('click', () => { + tab.addEventListener('click', async () => { tabs.forEach((t) => t.classList.remove('active')); this.modal.querySelectorAll('.search-tab-content').forEach((c) => c.classList.remove('active')); tab.classList.add('active'); const contentId = tab.dataset.tab === 'browse' ? 'theme-store-browse' : 'theme-store-upload'; document.getElementById(contentId)?.classList.add('active'); if (tab.dataset.tab === 'upload') { - this.checkAuth(); + await this.checkAuth(); } else { this.resetEditState(); } @@ -82,9 +82,9 @@ export class ThemeStore { this.uploadForm?.addEventListener('submit', (e) => this.handleUpload(e)); if (authManager) { - authManager.onAuthStateChanged(() => { + authManager.onAuthStateChanged(async () => { if (this.modal.classList.contains('active')) { - this.checkAuth(); + await this.checkAuth(); } }); } @@ -231,10 +231,10 @@ export class ThemeStore {
`; - div.addEventListener('click', (e) => { + div.addEventListener('click', async (e) => { if (e.target.closest('.delete-theme-btn')) { e.stopPropagation(); - this.deleteTheme(theme.id); + await this.deleteTheme(theme.id); return; } if (e.target.closest('.edit-theme-btn')) { @@ -266,7 +266,7 @@ export class ThemeStore { await this.pb.collection('themes').delete(themeId, { f_id: fbUser.$id }); alert('Theme deleted successfully.'); - this.loadThemes(); + await this.loadThemes(); } catch (err) { console.error('Failed to delete theme:', err); alert('Failed to delete theme. You might not have permission.'); @@ -467,6 +467,7 @@ export class ThemeStore { // Force reflow to ensure theme changes are applied immediately document.documentElement.style.display = 'none'; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions document.documentElement.offsetHeight; document.documentElement.style.display = ''; @@ -572,7 +573,7 @@ export class ThemeStore { } this.modal.querySelector('[data-tab="browse"]').click(); - this.loadThemes(); + await this.loadThemes(); } catch (err) { console.error('Upload failed:', err); console.error('Response data:', err.data); diff --git a/js/tracker.js b/js/tracker.js index 7ae76e1..de42676 100644 --- a/js/tracker.js +++ b/js/tracker.js @@ -280,7 +280,7 @@ function renderTrackerTracks(container, tracks) { } // Create project card HTML - EXACTLY like album cards -export function createProjectCardHTML(era, artist, sheetId, trackCount) { +export function createProjectCardHTML(era, _artist, sheetId, trackCount) { const playBtnHTML = `