diff --git a/package.json b/package.json index 6f6948d..ea227e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spotify-lyrics", - "version": "1.6.3", + "version": "1.6.4", "description": "Desktop Spotify Web Player Instant Synchronized Lyrics", "scripts": { "lint": "tsc --noEmit && eslint --ext .ts --fix src/", diff --git a/public/manifest.json b/public/manifest.json index f967b43..0b126a3 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/extend-chrome/manifest-json-schema/main/schema/manifest.schema.json", "name": "__MSG_extensionName__", - "version": "1.6.3", + "version": "1.6.4", "manifest_version": 3, "description": "__MSG_extensionDescription__", "default_locale": "en", diff --git a/src/page/lyrics.ts b/src/page/lyrics.ts index bb17032..555d165 100644 --- a/src/page/lyrics.ts +++ b/src/page/lyrics.ts @@ -10,8 +10,6 @@ import { captureException } from './utils'; export interface Query { name: string; artists: string; - /**sec */ - duration?: number; } export interface Artist { @@ -177,7 +175,7 @@ async function fetchSongList(s: string, fetchOptions?: RequestInit): Promise HTMLAudioElement | Promise; + getDuration?: () => Promise; fetchData?: (s: string, fetchOptions?: RequestInit) => Promise; fetchTransName?: (s: string, fetchOptions?: RequestInit) => Promise>; fetchOptions?: RequestInit; @@ -188,21 +186,14 @@ export async function matchingLyrics( ): Promise<{ list: Song[]; id: number; score: number }> { const { name = '', artists = '' } = query; const { - getAudioElement, + getDuration, onlySearchName = false, fetchData = fetchSongList, fetchTransName = fetchChineseName, fetchOptions, } = options; - let duration = query.duration || 0; - if (getAudioElement && !duration) { - const audio = await getAudioElement(); - if (!audio.duration) { - await new Promise((res) => audio.addEventListener('loadedmetadata', res, { once: true })); - duration = audio.duration; - } - } + const duration = (await getDuration?.()) || 0; const queryName = normalize(name); const queryName1 = queryName.toLowerCase(); @@ -361,7 +352,7 @@ export async function matchingLyrics( list: listForMissingName, score: scoreForMissingName, } = await matchingLyrics(query, { - getAudioElement, + getDuration, onlySearchName: true, fetchData, fetchTransName: async () => singerAlias, diff --git a/src/page/observer.ts b/src/page/observer.ts index b25becf..015e804 100644 --- a/src/page/observer.ts +++ b/src/page/observer.ts @@ -130,42 +130,43 @@ const originFetch = globalThis.fetch; let latestHeader = new Headers(); -// Priority to detect track switching through API -// Priority to use build-in lyrics through API globalThis.fetch = async (...rest) => { const res = await originFetch(...rest); const url = new URL(rest[0] instanceof Request ? rest[0].url : rest[0], location.origin); - latestHeader = new Headers(rest[0] instanceof Request ? rest[0].headers : rest[1]?.headers); const spotifyAPI = 'https://spclient.wg.spotify.com'; if (url.origin === spotifyAPI && url.pathname.startsWith('/metadata/4/track/')) { - const metadata: SpotifyTrackMetadata = await res.clone().json(); - const { name = '', artist = [], duration = 0, canonical_uri, has_lyrics } = metadata || {}; - const trackId = canonical_uri?.match(/spotify:track:([^:]*)/)?.[1]; - // match artists element textContent - const artists = artist?.map((e) => e?.name).join(', '); - sharedData.cacheTrackAndLyrics({ - name, - artists, - duration: duration / 1000, - getLyrics: has_lyrics - ? async () => { - const res = await fetch(`${spotifyAPI}/lyrics/v1/track/${trackId}?market=from_token`, { - headers: latestHeader, - }); - const spLyrics: SpotifyTrackLyrics = await res.json(); - if (spLyrics.kind === 'LINE') { - return spLyrics.lines - .map(({ time, words }) => - words.map(({ string }) => ({ - startTime: time / 1000, - text: string, - })), - ) - .flat(); - } - } - : undefined, - }); + latestHeader = new Headers(rest[0] instanceof Request ? rest[0].headers : rest[1]?.headers); + + // const metadata: SpotifyTrackMetadata = await res.clone().json(); + // const { name = '', artist = [], duration = 0, canonical_uri, has_lyrics } = metadata || {}; + // const trackId = canonical_uri?.match(/spotify:track:([^:]*)/)?.[1]; + // // match artists element textContent + // const artists = artist?.map((e) => e?.name).join(', '); + // sharedData.cacheTrackAndLyrics({ + // name, + // artists, + // duration: duration / 1000, + // getLyrics: has_lyrics + // ? async ({ signal }) => { + // const res = await fetch(`${spotifyAPI}/lyrics/v1/track/${trackId}?market=from_token`, { + // headers: latestHeader, + // signal, + // }); + // if (!res.ok) return ''; + // const spLyrics: SpotifyTrackLyrics = await res.json(); + // if (spLyrics.kind !== 'LINE') return ''; + // return spLyrics.lines + // .map(({ time, words }) => + // words.map(({ string }) => { + // const sec = time / 1000; + // return `[${Math.floor(sec / 60)}:${sec % 60}]\n${string}`; + // }), + // ) + // .flat() + // .join('\n'); + // } + // : undefined, + // }); } return res; }; diff --git a/src/page/rate.ts b/src/page/rate.ts index 51bc62e..c213802 100644 --- a/src/page/rate.ts +++ b/src/page/rate.ts @@ -267,16 +267,16 @@ const key = 'spotify.lyrics.test'; const listPromise = new Promise((res, rej) => { const fn = () => { - const querys = [...document.querySelectorAll('.tracklist li')].map((item) => { + const queryList = [...document.querySelectorAll('.tracklist li')].map((item) => { return { name: item.querySelector('.tracklist-name')?.textContent || '', artists: item.querySelector('.TrackListRow__artists')?.textContent || '', }; }); - if (!querys.length) { + if (!queryList.length) { setTimeout(fn, 100); } else { - res(querys); + res(queryList); } }; setTimeout(rej, 5000); @@ -300,14 +300,14 @@ window.addEventListener('load', async () => { detail: [], }; - const querys = await listPromise; + const queryList = await listPromise; // test - querys.length = 1; + queryList.length = 1; await Promise.all( - querys.map(async (query, i) => { - console.log(`${i + 1}/${querys.length} matching: `, query); + queryList.map(async (query, i) => { + console.log(`${i + 1}/${queryList.length} matching: `, query); const { id } = await matchingLyrics(query); if (id === 0) { data[location.pathname].noMatch++; diff --git a/src/page/share-data.ts b/src/page/share-data.ts index 40f5b70..0b78ce2 100644 --- a/src/page/share-data.ts +++ b/src/page/share-data.ts @@ -1,16 +1,6 @@ /** * Used to update the data and synchronize with Popup and Lyrics Editor, * while the data is rendered into the PIP - * - * 1. The built -in lyrics cache through the API intercept - * (cannot be sure that this call is the current played song) - * 2. Triggering lyrics update based on UI update - * 1. get cache - * 2. fetch NetEase data - * 3. fetch Google Firebase data - * 4. fetch Genius data - * 3. Sync to popup,response popup user interaction - * 4. Response lyrics editor user interaction */ import { Cache } from 'duoyun-ui/lib/cache'; @@ -27,23 +17,39 @@ import { captureException } from './utils'; import { audioPromise } from './element'; import { configPromise } from './config'; -interface CacheItem { +interface CacheReq { name: string; artists: string; duration: number; - lyrics?: Lyric; - promiseLyrics?: Promise; - getLyrics?: () => Promise; + getLyrics?: (fetchOptions: RequestInit) => Promise; +} + +interface CacheItem { + name: string; + artists: string; + resolveDuration: (duration: number) => void; + durationPromise: Promise; + getLyrics: (fetchOptions: RequestInit) => Promise; } const cacheStore = new Cache({ max: 100, renewal: true }); -const setCache = (info: CacheItem) => cacheStore.set([info.name, info.artists].join(), info); -const getCache = (name: string, artists: string) => cacheStore.get([name, artists].join()); +const getCache = (name: string, artists: string) => { + return cacheStore.get([name, artists].join(), () => { + let _resolveDuration = (_: number) => { + // + }; + const _durationPromise = new Promise((res) => (_resolveDuration = res)); + return { + name, + artists, + resolveDuration: _resolveDuration, + durationPromise: _durationPromise, + getLyrics: async () => '', + }; + }); +}; export class SharedData { - // optional - private _duration = 0; - // Popup data private _name = ''; private _artists = ''; @@ -69,7 +75,7 @@ export class SharedData { } get req() { - return { name: this._name, artists: this._artists, duration: this._duration }; + return { name: this._name, artists: this._artists }; } get text() { @@ -116,30 +122,38 @@ export class SharedData { this._id = 0; this._name = ''; this._artists = ''; - this._duration = 0; this._aId = 0; this._list = []; this._text = ''; this._highlightLyrics = []; } - // can only modify `lyrics` - private async _updateLyrics(fetchOptions: RequestInit) { + private async _getParseLyricsOptions() { + const options = await optionsPromise; + return { + cleanLyrics: options['clean-lyrics'] === 'on', + lyricsTransform: options['lyrics-transform'], + }; + } + + private async _getLyricsFromNetEase(fetchOptions: RequestInit) { if (this._id === 0) { - this._lyrics = null; - } else { - const options = await optionsPromise; - const lyricsStr = await fetchLyric(this._id, fetchOptions); - if (lyricsStr === '') { - sendEvent(options.cid, events.noLyrics, { cd1: this.cd1, cd2: this.cd2 }); - this._lyrics = null; - } else { - this._lyrics = parseLyrics(lyricsStr, { - cleanLyrics: options['clean-lyrics'] === 'on', - lyricsTransform: options['lyrics-transform'], - }); - } + return null; + } + const options = await optionsPromise; + const lyricsStr = await fetchLyric(this._id, fetchOptions); + if (lyricsStr === '') { + sendEvent(options.cid, events.noLyrics, { cd1: this.cd1, cd2: this.cd2 }); + return null; } + return parseLyrics(lyricsStr, await this._getParseLyricsOptions()); + } + + private async _getLyricsFromBuiltIn(fetchOptions: RequestInit) { + return parseLyrics( + await getCache(this.name, this.artists).getLyrics(fetchOptions), + await this._getParseLyricsOptions(), + ); } private async _fetchHighlight(fetchOptions: RequestInit) { @@ -160,23 +174,33 @@ export class SharedData { } } + cacheTrackAndLyrics(info: CacheReq) { + const cache = getCache(info.name, info.artists); + cache.resolveDuration(info.duration); + if (info.getLyrics) cache.getLyrics = info.getLyrics; + } + // can only modify `lyrics`/`id`/`aId`/`list` private async _matching(fetchOptions: RequestInit) { const audio = await audioPromise; const startTime = audio.currentSrc ? performance.now() : null; const options = await optionsPromise; - const parseLyricsOptions = { - cleanLyrics: options['clean-lyrics'] === 'on', - lyricsTransform: options['lyrics-transform'], - }; + const parseLyricsOptions = await this._getParseLyricsOptions(); const [{ list, id }, remoteData] = await Promise.all([ matchingLyrics(this.req, { - getAudioElement: () => audio, + getDuration: async () => { + const audioMetadataLoaded = new Promise((res) => + audio.addEventListener('loadedmetadata', res, { once: true }), + ); + return Promise.any([ + getCache(this._name, this._artists).durationPromise, + audio.duration || (await audioMetadataLoaded) || audio.duration, + ]); + }, fetchOptions, }), getSong(this.req, fetchOptions), ]); - if (id === 0 && (await this._restoreLyrics(true))) return; this._list = list; const reviewed = options['use-unreviewed-lyrics'] === 'on' || remoteData?.reviewed; const isSelf = remoteData?.user === options.cid; @@ -186,14 +210,25 @@ export class SharedData { } else if (isSelf && remoteData?.neteaseID) { this._id = remoteData.neteaseID; this._aId = this._id; - await this._updateLyrics(fetchOptions); + this._lyrics = await this._getLyricsFromNetEase(fetchOptions); } else if (reviewed && remoteData?.lyric) { this._lyrics = parseLyrics(remoteData.lyric, parseLyricsOptions); sendEvent(options.cid, events.useRemoteLyrics); } else { this._id = (reviewed ? remoteData?.neteaseID || id : id || remoteData?.neteaseID) || 0; this._aId = this._id; - await this._updateLyrics(fetchOptions); + const getLyricsList = [ + this._getLyricsFromBuiltIn.bind(this), + this._getLyricsFromNetEase.bind(this), + ]; + try { + this._lyrics = await getLyricsList[0](fetchOptions); + } catch { + // + } + if (this._lyrics === null) { + this._lyrics = await getLyricsList[1](fetchOptions); + } } if (this._lyrics && this._id !== id) { sendEvent(options.cid, events.useRemoteMatch); @@ -234,7 +269,7 @@ export class SharedData { await this._matching(fetchOptions); this.sendToContentScript(); } else { - await this._updateLyrics(fetchOptions); + this._lyrics = await this._getLyricsFromNetEase(fetchOptions); } } catch (e) { if (e.name !== 'AbortError') { @@ -261,10 +296,7 @@ export class SharedData { this.resetData(); this._name = name; this._artists = artists; - // case1: spotify metadata API call before of UI update - if (!(await this._restoreLyrics())) { - await this._matching({ signal: this._abortController.signal }); - } + await this._matching({ signal: this._abortController.signal }); } catch (e) { if (e.name !== 'AbortError') { this._error = e; @@ -274,39 +306,6 @@ export class SharedData { this.sendToContentScript(); } - async cacheTrackAndLyrics(info: CacheItem) { - if (getCache(info.name, info.artists)) return; - setCache(info); - // case2: spotify metadata API call after of UI update - // current behavior - if (this.name === info.name && this.artists === info.artists) { - if (await this._restoreLyrics()) { - this._cancelRequest(); - } - } - } - - private async _restoreLyrics(isForce = false) { - const cache = getCache(this.name, this.artists); - if (cache) { - this._duration = cache.duration; - if (cache.lyrics || cache.getLyrics) { - try { - const lyrics = cache.lyrics || (await (cache.promiseLyrics ||= cache.getLyrics?.())); - if (lyrics) { - cache.lyrics = lyrics; - // 如果使用简体歌词,那么只更新缓存 - if (!isForce && (await optionsPromise)['lyrics-transform'] === 'Simplified') return; - this._lyrics = lyrics; - return true; - } - } catch { - // - } - } - } - } - sendToContentScript() { const { _name, _artists, _id, _aId, _list } = this; const msg: Message = {