diff --git a/functions/src/index.ts b/functions/src/index.ts index 9540ede..c2914ad 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,7 +1,7 @@ import * as functions from 'firebase-functions'; import * as admin from 'firebase-admin'; -import { Lyric, LyricsResponse, Config } from './type'; +import { LyricRecord, LyricsResponse, Config } from './type'; admin.initializeApp(); const db = admin.firestore(); @@ -23,14 +23,14 @@ const corsHandler = (req: functions.https.Request, res: functions.Response) => { } }; -const isValidRequest = (params: Lyric) => { +const isValidRequest = (params: LyricRecord) => { return params?.user && params.name && params.artists && params.platform; }; export const getLyric = functions.https.onRequest( async (req, res: functions.Response>) => { if (corsHandler(req, res)) return; - const params: Lyric = req.body; + const params: LyricRecord = req.body; if (!isValidRequest(params)) { res.status(400).send({ message: 'Params error' }); return; @@ -43,11 +43,11 @@ export const getLyric = functions.https.onRequest( .where('platform', '==', params.platform); let snapshot = await query.where('user', '==', params.user).get(); let doc = snapshot.docs[0]; - let data = doc?.data() as Lyric | undefined; + let data = doc?.data() as LyricRecord | undefined; if (snapshot.empty || (!data?.lyric && !data?.neteaseID)) { snapshot = await query.get(); doc = snapshot.docs[0]; - data = doc?.data() as Lyric | undefined; + data = doc?.data() as LyricRecord | undefined; } res.send({ data, message: 'OK' }); }, @@ -56,7 +56,7 @@ export const getLyric = functions.https.onRequest( export const setLyric = functions.https.onRequest( async (req, res: functions.Response>) => { if (corsHandler(req, res)) return; - const params: Lyric = req.body; + const params: LyricRecord = req.body; if (!isValidRequest(params)) { return; } @@ -71,10 +71,10 @@ export const setLyric = functions.https.onRequest( if (snapshot.empty) { if (params.neteaseID || params.lyric) { await lyricsRef.add( - Object.assign({ neteaseID: 0, lyric: '' } as Lyric, params, { + Object.assign({ neteaseID: 0, lyric: '' } as LyricRecord, params, { reviewed, createdTime: Date.now(), - } as Lyric), + } as LyricRecord), ); } } else { @@ -82,10 +82,10 @@ export const setLyric = functions.https.onRequest( const data = Object.assign(doc.data(), params); if (data.neteaseID || data.lyric) { await doc.ref.update( - Object.assign({ neteaseID: 0, lyric: '' } as Lyric, params, { + Object.assign({ neteaseID: 0, lyric: '' } as LyricRecord, params, { reviewed, updatedTime: Date.now(), - } as Lyric), + } as LyricRecord), ); } else { await doc.ref.delete(); diff --git a/functions/src/type.ts b/functions/src/type.ts index 8668a36..49945f3 100644 --- a/functions/src/type.ts +++ b/functions/src/type.ts @@ -1,4 +1,4 @@ -export interface Lyric { +export interface LyricRecord { name: string; artists: string; platform: string; diff --git a/package.json b/package.json index 295ce8d..19e12f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spotify-lyrics", - "version": "1.6.1", + "version": "1.6.2", "description": "Desktop Spotify Web Player Instant Synchronized Lyrics", "scripts": { "lint": "tsc --noEmit && eslint --ext .ts --fix src/", @@ -25,6 +25,7 @@ "@sentry/browser": "^5.25.0", "@webcomponents/webcomponentsjs": "^2.8.0", "chinese-conv": "^1.0.1", + "duoyun-ui": "^1.1.20", "webextension-polyfill": "^0.12.0" }, "devDependencies": { diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index c2fa504..5480e99 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -135,7 +135,7 @@ }, "optionsToggleShortcutDetail": { - "message": "When webapp is in focus, you can use shortcuts to open and close lyrics", + "message": "When webapp is in focus, you can use shortcuts to open and close lyrics, global shortcut: chrome://extensions/shortcuts", "description": "Toggle show lyrics shortcut detail" }, diff --git a/public/_locales/zh/messages.json b/public/_locales/zh/messages.json index 66e26c2..86fd591 100644 --- a/public/_locales/zh/messages.json +++ b/public/_locales/zh/messages.json @@ -104,7 +104,7 @@ }, "optionsToggleShortcutDetail": { - "message": "当 WebApp 处于焦点时,可以使用快捷方式来打开歌词和关闭歌词" + "message": "当 WebApp 处于焦点时,可以使用快捷方式来打开歌词和关闭歌词,全局快捷键:chrome://extensions/shortcuts" }, "menusFeedback": { diff --git a/public/manifest.json b/public/manifest.json index 7a9fc8b..8209892 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.1", + "version": "1.6.2", "manifest_version": 3, "description": "__MSG_extensionDescription__", "default_locale": "en", diff --git a/src/page/btn.ts b/src/page/btn.ts index 29756b1..ed49e00 100644 --- a/src/page/btn.ts +++ b/src/page/btn.ts @@ -138,7 +138,7 @@ export const insetLyricsBtn = async () => { sharedData.resetData(); } else { await openLyrics(); - sharedData.updateTrack(true); + sharedData.dispatchTrackElementUpdateEvent(true); } } catch (e) { captureException(e); diff --git a/src/page/lyrics.ts b/src/page/lyrics.ts index 0624cf7..759b597 100644 --- a/src/page/lyrics.ts +++ b/src/page/lyrics.ts @@ -1,4 +1,4 @@ -import { sify, tify } from 'chinese-conv'; +import { sify as toSimplified, tify as toTraditional } from 'chinese-conv'; import { isProd } from '../common/constants'; @@ -10,6 +10,8 @@ import { captureException } from './utils'; export interface Query { name: string; artists: string; + /**sec */ + duration?: number; } export interface Artist { @@ -83,7 +85,7 @@ const ignoreAccented = (s: string) => { }; const simplifiedText = (s: string) => { - return ignoreAccented(plainText(sify(normalize(s)).toLowerCase())); + return ignoreAccented(plainText(toSimplified(normalize(s)).toLowerCase())); }; const removeSongFeat = (s: string) => { @@ -132,9 +134,9 @@ async function fetchChineseName(s: string, fetchOptions?: RequestInit) { artists.forEach((artist) => { const alias = [...artist.alias, ...(artist.transNames || [])].map(simplifiedText).sort(); // Chinese singer's English name as an alias - alias.forEach((alia) => { - if (s.includes(alia)) { - singerAlias[alia] = artist.name; + alias.forEach((n) => { + if (s.includes(n)) { + singerAlias[n] = artist.name; } }); }); @@ -179,6 +181,7 @@ export async function matchingLyrics( query: Query, options: MatchingLyricsOptions = {}, ): Promise<{ list: Song[]; id: number; score: number }> { + const { name = '', artists = '' } = query; const { getAudioElement, onlySearchName = false, @@ -187,18 +190,18 @@ export async function matchingLyrics( fetchOptions, } = options; - let audio: HTMLAudioElement | null = null; - if (getAudioElement) { - audio = await getAudioElement(); + 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 })); + await new Promise((res) => audio.addEventListener('loadedmetadata', res, { once: true })); + duration = audio.duration; } } - const { name = '', artists = '' } = query; const queryName = normalize(name); const queryName1 = queryName.toLowerCase(); - const queryName2 = sify(queryName1); + const queryName2 = toSimplified(queryName1); const queryName3 = plainText(queryName2); const queryName4 = ignoreAccented(queryName3); const queryName5 = removeSongFeat(queryName4); @@ -208,7 +211,7 @@ export async function matchingLyrics( .map((e) => normalize(e.trim())) .sort(); const queryArtistsArr1 = queryArtistsArr.map((e) => e.toLowerCase()); - const queryArtistsArr2 = queryArtistsArr1.map((e) => sify(e)); + const queryArtistsArr2 = queryArtistsArr1.map((e) => toSimplified(e)); const queryArtistsArr3 = queryArtistsArr2.map((e) => ignoreAccented(plainText(e))); const singerAlias = await fetchTransName( @@ -219,7 +222,7 @@ export async function matchingLyrics( const queryArtistsArr4 = queryArtistsArr3 .map((e) => singerAlias[e] || buildInSingerAlias[e] || e) - .map((e) => sify(e).toLowerCase()); + .map((e) => toSimplified(e).toLowerCase()); const searchString = onlySearchName ? removeSongFeat(name) @@ -235,10 +238,10 @@ export async function matchingLyrics( let currentScore = 0; if ( - !audio || - (!isProd && audio.duration < 40) || + !duration || + (!isProd && duration < 40) || !song.duration || - Math.abs(audio.duration - song.duration / 1000) < 2 + Math.abs(duration - song.duration / 1000) < 2 ) { currentScore += DURATION_WEIGHT; } @@ -251,7 +254,7 @@ export async function matchingLyrics( if (songName === queryName1) { currentScore += 9.1; } else { - songName = sify(songName); + songName = toSimplified(songName); if ( songName === queryName2 || songName.endsWith(`(${queryName2})`) || @@ -273,7 +276,7 @@ export async function matchingLyrics( } else { songName = getText( // without `plainText` - removeSongFeat(ignoreAccented(sify(normalize(song.name).toLowerCase()))), + removeSongFeat(ignoreAccented(toSimplified(normalize(song.name).toLowerCase()))), ); if (songName === queryName6) { // name & name (abc) @@ -305,7 +308,7 @@ export async function matchingLyrics( } else if (new Set([...queryArtistsArr1, ...songArtistsArr]).size < len) { currentScore += 5.4; } else { - songArtistsArr = songArtistsArr.map((e) => sify(e)); + songArtistsArr = songArtistsArr.map((e) => toSimplified(e)); if (queryArtistsArr2.join() === songArtistsArr.join()) { currentScore += 5.3; } else { @@ -381,6 +384,7 @@ export async function fetchLyric(songId: number, fetchOptions?: RequestInit) { } class Line { + /**sec */ startTime: number | null = null; text = ''; constructor(text = '', starTime: number | null = null) { @@ -425,20 +429,20 @@ export function parseLyrics(lyricStr: string, options: ParseLyricsOptions = {}) if (textIndex > -1) { text = matchResult.splice(textIndex, 1)[0]; text = capitalize(normalize(text, false)); - text = sify(text).replace(/\.|,|\?|!|;$/u, ''); + text = toSimplified(text).replace(/\.|,|\?|!|;$/u, ''); } if (!matchResult.length && options.keepPlainText) { return [new Line(text)]; } return matchResult.map((slice) => { const result = new Line(); - const matchResut = slice.match(/[^\[\]]+/g); - const [key, value] = matchResut?.[0].split(':') || []; + const matchResult = slice.match(/[^\[\]]+/g); + const [key, value] = matchResult?.[0].split(':') || []; const [min, sec] = [parseFloat(key), parseFloat(value)]; if (!isNaN(min)) { if (!options.cleanLyrics || !otherInfoRegexp.test(text)) { result.startTime = min * 60 + sec; - result.text = options.useTChinese ? tify(text) : text; + result.text = options.useTChinese ? toTraditional(text) : text; } } else if (!options.cleanLyrics && key && value) { result.text = `${key.toUpperCase()}: ${value}`; diff --git a/src/page/observer.ts b/src/page/observer.ts index 78354bf..b25becf 100644 --- a/src/page/observer.ts +++ b/src/page/observer.ts @@ -4,6 +4,7 @@ import { insetLyricsBtn } from './btn'; import { sharedData } from './share-data'; import { generateCover } from './cover'; import { captureException, documentQueryHasSelector } from './utils'; +import { SpotifyTrackLyrics, SpotifyTrackMetadata } from './types'; let loginResolve: (value?: unknown) => void; export const loggedPromise = new Promise((res) => (loginResolve = res)); @@ -74,13 +75,14 @@ configPromise.then( anonymous.src = this.currentSrc || this.src; } - const update = () => { + const infoElementUpdate = () => { // Assuming that cover is loaded after the song information is updated const cover = document.querySelector(ALBUM_COVER_SELECTOR) as HTMLImageElement | null; if (cover) { cover.addEventListener('load', coverUpdated); } + if (!lyricVideoIsOpen) return; const likeBtn = documentQueryHasSelector(BTN_LIKE_SELECTOR); const likeBtnRect = likeBtn?.getBoundingClientRect(); if (!likeBtnRect?.width || !likeBtnRect.height) { @@ -88,9 +90,9 @@ configPromise.then( return sharedData.resetData(); } - sharedData.updateTrack(); + sharedData.dispatchTrackElementUpdateEvent(); - if (lyricVideoIsOpen && !cover) { + if (!cover) { captureException(new Error('Cover not found')); } }; @@ -104,10 +106,10 @@ configPromise.then( const prevInfoElement = infoElement; infoElement = document.querySelector(TRACK_INFO_SELECTOR); if (!infoElement) return; - if (!prevInfoElement || prevInfoElement !== infoElement) update(); + if (!prevInfoElement || prevInfoElement !== infoElement) infoElementUpdate(); if (!weakMap.has(infoElement)) { - const infoEleObserver = new MutationObserver(update); + const infoEleObserver = new MutationObserver(infoElementUpdate); infoEleObserver.observe(infoElement, { childList: true, characterData: true, @@ -123,3 +125,47 @@ configPromise.then( htmlEleObserver.observe(document.documentElement, { childList: true, subtree: true }); }, ); + +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, + }); + } + return res; +}; diff --git a/src/page/share-data.ts b/src/page/share-data.ts index 7654784..efb1dfe 100644 --- a/src/page/share-data.ts +++ b/src/page/share-data.ts @@ -1,3 +1,19 @@ +/** + * 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'; + import { Message, Event } from '../common/constants'; import { sendEvent, events } from '../common/ga'; @@ -8,19 +24,40 @@ import { fetchSongList, fetchGeniusLyrics } from './genius'; import { setSong, getSong } from './store'; import { optionsPromise } from './options'; import { captureException } from './utils'; -import { audioPromise, lyricVideoIsOpen } from './element'; +import { audioPromise } from './element'; import { configPromise } from './config'; +interface CacheItem { + name: string; + artists: string; + duration: number; + lyrics?: Lyric; + promiseLyrics?: Promise; + getLyrics?: () => 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()); + export class SharedData { + // optional + private _duration = 0; + + // Popup data private _name = ''; private _artists = ''; private _id = 0; private _aId = 0; private _list: Song[] = []; - private _lyrics: Lyric = []; - private _error: Error | null = null; + + // PIP data private _text = ''; private _highlightLyrics: string[] | null = []; + // length 0 is loading + // null is no lyrics + private _lyrics: Lyric = []; + private _error: Error | null = null; private _abortController = new AbortController(); get cd1() { @@ -31,8 +68,8 @@ export class SharedData { return `${this._id}`; } - get query() { - return { name: this._name, artists: this._artists }; + get req() { + return { name: this._name, artists: this._artists, duration: this._duration }; } get text() { @@ -43,14 +80,14 @@ export class SharedData { return this._highlightLyrics; } - get error() { - return this._error; - } - get lyrics() { return this._lyrics; } + get error() { + return this._error; + } + get name() { return this._name; } @@ -63,18 +100,23 @@ export class SharedData { this._lyrics = lyrics && [...lyrics]; } - resetLyrics() { - this._lyrics = []; - this._error = null; + private _cancelRequest() { this._abortController.abort(); this._abortController = new AbortController(); } + private _resetLyrics() { + this._lyrics = []; + this._error = null; + this._cancelRequest(); + } + resetData() { - this.resetLyrics(); + this._resetLyrics(); this._id = 0; this._name = ''; this._artists = ''; + this._duration = 0; this._aId = 0; this._list = []; this._text = ''; @@ -82,7 +124,7 @@ export class SharedData { } // can only modify `lyrics` - async updateLyrics(fetchOptions: RequestInit) { + private async _updateLyrics(fetchOptions: RequestInit) { if (this._id === 0) { this._lyrics = null; } else { @@ -100,9 +142,9 @@ export class SharedData { } } - async fetchHighlight(fetchOptions: RequestInit) { + private async _fetchHighlight(fetchOptions: RequestInit) { const fetchTransName = async () => ({}); - const { id } = await matchingLyrics(this.query, { + const { id } = await matchingLyrics(this.req, { onlySearchName: false, fetchData: fetchSongList, fetchTransName, @@ -119,7 +161,7 @@ export class SharedData { } // can only modify `lyrics`/`id`/`aId`/`list` - async matching(fetchOptions: RequestInit) { + private async _matching(fetchOptions: RequestInit) { const audio = await audioPromise; const startTime = audio.currentSrc ? performance.now() : null; const options = await optionsPromise; @@ -127,12 +169,12 @@ export class SharedData { cleanLyrics: options['clean-lyrics'] === 'on', useTChinese: options['traditional-chinese-lyrics'] === 'on', }; - const { list, id } = await matchingLyrics(this.query, { + const { list, id } = await matchingLyrics(this.req, { getAudioElement: () => audio, fetchOptions, }); this._list = list; - const remoteData = await getSong(this.query, fetchOptions); + const remoteData = await getSong(this.req, fetchOptions); const reviewed = options['use-unreviewed-lyrics'] === 'on' || remoteData?.reviewed; const isSelf = remoteData?.user === options.cid; if (isSelf && remoteData?.lyric) { @@ -141,14 +183,14 @@ export class SharedData { } else if (isSelf && remoteData?.neteaseID) { this._id = remoteData.neteaseID; this._aId = this._id; - await this.updateLyrics(fetchOptions); + await this._updateLyrics(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); + await this._updateLyrics(fetchOptions); } if (this._lyrics && this._id !== id) { sendEvent(options.cid, events.useRemoteMatch); @@ -160,7 +202,7 @@ export class SharedData { const ev = (performance.now() - startTime).toFixed(); sendEvent(options.cid, { ev, ...events.loadLyrics }, { cd1: this.cd1 }); } - this.fetchHighlight(fetchOptions); + this._fetchHighlight(fetchOptions); } async confirmedMId() { @@ -180,16 +222,16 @@ export class SharedData { if (id === this._id) return; if (name !== this._name || artists !== this._artists) return; this._id = id; - this.resetLyrics(); + this._resetLyrics(); try { const fetchOptions = { signal: this._abortController.signal }; if (id === 0) { // reset await setSong({ name, artists, id }); - await this.matching(fetchOptions); + await this._matching(fetchOptions); this.sendToContentScript(); } else { - await this.updateLyrics(fetchOptions); + await this._updateLyrics(fetchOptions); } } catch (e) { if (e.name !== 'AbortError') { @@ -198,9 +240,7 @@ export class SharedData { } } - async updateTrack(isTrust = false) { - if (!lyricVideoIsOpen) return; - + async dispatchTrackElementUpdateEvent(isUserAction = false) { const { TRACK_NAME_SELECTOR, TRACK_ARTIST_SELECTOR } = await configPromise; const name = document.querySelector(TRACK_NAME_SELECTOR)?.textContent; const artists = document.querySelector(TRACK_ARTIST_SELECTOR)?.textContent; @@ -210,7 +250,7 @@ export class SharedData { return; } if (!name || !artists) { - if (isTrust) { + if (isUserAction) { throw new Error(`Track info not found`); } return; @@ -218,7 +258,11 @@ export class SharedData { this.resetData(); this._name = name; this._artists = artists; - await this.matching({ signal: this._abortController.signal }); + // case1: spotify metadata API call before of UI update + const succuss = await this._restoreCurrentTrackAndLyrics(); + if (!succuss) { + await this._matching({ signal: this._abortController.signal }); + } } catch (e) { if (e.name !== 'AbortError') { this._error = e; @@ -228,6 +272,36 @@ 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 + if (this.name === info.name && this.artists === info.artists) { + const succuss = await this._restoreCurrentTrackAndLyrics(); + if (succuss) { + this._cancelRequest(); + } + } + } + + private async _restoreCurrentTrackAndLyrics() { + 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) { + this._lyrics = cache.lyrics = lyrics; + return true; + } + } catch { + // + } + } + } + } + sendToContentScript() { const { _name, _artists, _id, _aId, _list } = this; const msg: Message = { diff --git a/src/page/store.ts b/src/page/store.ts index 848632a..ad643b2 100644 --- a/src/page/store.ts +++ b/src/page/store.ts @@ -1,9 +1,5 @@ -/** - * temporary plan: Stored in webpage localStorage - */ -import type { Lyric, LyricsResponse } from '../../functions/src/type'; +import type { LyricRecord, LyricsResponse } from '../../functions/src/type'; -import { Query } from './lyrics'; import { optionsPromise } from './options'; import { currentPlatform } from './config'; import { request } from './request'; @@ -12,7 +8,11 @@ import { request } from './request'; const API_HOST = 'https://files.xianqiao.wang/https://us-central1-spotify-lyrics-ef482.cloudfunctions.net'; -async function fetchData(pathname: string, params: Lyric | Lyric[], fetchOptions?: RequestInit) { +async function fetchData( + pathname: string, + params: LyricRecord | LyricRecord[], + fetchOptions?: RequestInit, +) { return await request(`${API_HOST}${pathname}`, { method: 'post', headers: { 'content-type': 'application/json' }, @@ -21,12 +21,15 @@ async function fetchData(pathname: string, params: Lyric | Lyric[], fetchOptions }); } -// Previously used localStorage -// const KEY = 'spotify.lyrics.extension'; - -export async function getSong(data: Query, fetchOptions: RequestInit) { +export async function getSong( + data: { + name: string; + artists: string; + }, + fetchOptions: RequestInit, +) { const { cid } = await optionsPromise; - const res: LyricsResponse = await fetchData( + const res: LyricsResponse = await fetchData( '/getLyric', { name: data.name, @@ -39,7 +42,12 @@ export async function getSong(data: Query, fetchOptions: RequestInit) { return res.data; } -export async function setSong(data: Query & { id?: number; lyric?: string }) { +export async function setSong(data: { + name: string; + artists: string; + id?: number; + lyric?: string; +}) { const { cid } = await optionsPromise; await fetchData('/setLyric', { name: data.name, diff --git a/src/page/types.ts b/src/page/types.ts new file mode 100644 index 0000000..5a31e22 --- /dev/null +++ b/src/page/types.ts @@ -0,0 +1,421 @@ +const spotifyTrackMetadataTemp = { + gid: '198e8ba0801d4a178bdc86641c3ce250', + name: 'ルカルカ☆ナイトフィーバー', + album: { + gid: '66e6491eefbf45158dece248622a8b38', + name: 'いつかの約束を君に', + artist: [ + { + gid: '4434c579bdf64c7c9f8bde0fcb4f6d5b', + name: 'Kano', + }, + ], + label: 'インペリアルレコード', + date: { + year: 2019, + month: 9, + day: 25, + }, + cover_group: { + image: [ + { + file_id: 'ab67616d00001e02a4733ef65db4687224ef7e2b', + size: 'DEFAULT', + width: 300, + height: 300, + }, + { + file_id: 'ab67616d00004851a4733ef65db4687224ef7e2b', + size: 'SMALL', + width: 64, + height: 64, + }, + { + file_id: 'ab67616d0000b273a4733ef65db4687224ef7e2b', + size: 'LARGE', + width: 640, + height: 640, + }, + ], + }, + licensor: { + uuid: '96b66d74451e43138b00b985fb652c29', + }, + }, + artist: [ + { + gid: '4434c579bdf64c7c9f8bde0fcb4f6d5b', + name: 'Kano', + }, + ], + number: 2, + disc_number: 1, + duration: 230038, + popularity: 26, + external_id: [ + { + type: 'isrc', + id: 'JPTE01906080', + }, + ], + file: [ + { + file_id: '6f87af07d363590544abbcd525eb951ce3fbdb43', + format: 'OGG_VORBIS_320', + }, + { + file_id: '9014b9448944cda136af379b4e3b134d0bf5958b', + format: 'OGG_VORBIS_160', + }, + { + file_id: '81b2779f5652c823716594672a64a87fa15a079f', + format: 'OGG_VORBIS_96', + }, + { + file_id: '62d066f02648a18743cfcbbcff9f5ba622ce1a4e', + format: 'MP4_256_DUAL', + }, + { + file_id: 'ce211cb1e2ea05c7e1253b887d1e957e4459699d', + format: 'MP4_256', + }, + { + file_id: '433c78fff2c85e8ca5412633f966604c2d1e96b3', + format: 'MP4_128_DUAL', + }, + { + file_id: '89af6bc92e9bb49a2fc9310e8423838a92b4c2d2', + format: 'MP4_128', + }, + { + file_id: '7f13c561bbe57c2f35ed2411f89336e20ef5f257', + format: 'AAC_24', + }, + ], + preview: [ + { + file_id: 'fa1d008a427b432cb730e188a4b29fe090b4e3ea', + format: 'MP3_96', + }, + ], + earliest_live_timestamp: 1569322800, + has_lyrics: true, + licensor: { + uuid: '96b66d74451e43138b00b985fb652c29', + }, + language_of_performance: ['ja'], + original_audio: { + uuid: '4312bc96eeea456da72e323b10db08d8', + }, + original_title: 'ルカルカ☆ナイトフィーバー', + version_title: '', + artist_with_role: [ + { + artist_gid: '4434c579bdf64c7c9f8bde0fcb4f6d5b', + artist_name: 'Kano', + role: 'ARTIST_ROLE_MAIN_ARTIST', + }, + ], + canonical_uri: 'spotify:track:0MdWZZWyk7BsIlaSRFTdqo', +}; + +export type SpotifyTrackMetadata = typeof spotifyTrackMetadataTemp; + +const spotifyTrackLyrics = { + provider: 'Musixmatch', + kind: 'LINE', + trackId: '0DqCmQRIETn2nCpX3m1KdP', + lines: [ + { + time: 12890, + words: [ + { + string: '冷たい雨音', + }, + ], + }, + { + time: 16640, + words: [ + { + string: '窓の外響く夜は一人が怖い', + }, + ], + }, + { + time: 24620, + words: [ + { + string: '離れないように', + }, + ], + }, + { + time: 28370, + words: [ + { + string: '君を抱きしめる 強く', + }, + ], + }, + { + time: 35220, + words: [ + { + string: 'だから時計の針', + }, + ], + }, + { + time: 38830, + words: [ + { + string: '天を仰いでも', + }, + ], + }, + { + time: 41940, + words: [ + { + string: '目を逸らさないで', + }, + ], + }, + { + time: 44630, + words: [ + { + string: 'ただ私を見て', + }, + ], + }, + { + time: 50390, + words: [ + { + string: '優しさなんて偽りでいい', + }, + ], + }, + { + time: 56790, + words: [ + { + string: '夜が明けるその時まで', + }, + ], + }, + { + time: 59790, + words: [ + { + string: '嘘をつき通して', + }, + ], + }, + { + time: 62360, + words: [ + { + string: '刹那の時に溺れる様に', + }, + ], + }, + { + time: 68160, + words: [ + { + string: '君を感じさせていて 今だけは...', + }, + ], + }, + { + time: 78250, + words: [ + { + string: '♪', + }, + ], + }, + { + time: 86730, + words: [ + { + string: 'すべてに等しく', + }, + ], + }, + { + time: 90540, + words: [ + { + string: '終わりは訪れるけど そんなことは', + }, + ], + }, + { + time: 98740, + words: [ + { + string: '涙になるから', + }, + ], + }, + { + time: 102520, + words: [ + { + string: '考えることをやめた', + }, + ], + }, + { + time: 109470, + words: [ + { + string: '二人闇の中', + }, + ], + }, + { + time: 113080, + words: [ + { + string: '身体を溶かして', + }, + ], + }, + { + time: 115900, + words: [ + { + string: 'もっと深くへ', + }, + ], + }, + { + time: 118640, + words: [ + { + string: 'ただ堕ちて行こう', + }, + ], + }, + { + time: 124500, + words: [ + { + string: '映し出される悲劇のアリス', + }, + ], + }, + { + time: 130770, + words: [ + { + string: '罪の色で飾り付けた', + }, + ], + }, + { + time: 133560, + words: [ + { + string: '愛で縛り付けて', + }, + ], + }, + { + time: 136480, + words: [ + { + string: '忘れぬ様に失くさぬ様に', + }, + ], + }, + { + time: 142370, + words: [ + { + string: 'そっと何度もつぶやく 君の名を...', + }, + ], + }, + { + time: 149730, + words: [ + { + string: '止まない雨は', + }, + ], + }, + { + time: 153500, + words: [ + { + string: 'すべてを流してくれるでしょうか', + }, + ], + }, + { + time: 162330, + words: [ + { + string: '♪', + }, + ], + }, + { + time: 186630, + words: [ + { + string: '優しさなんて偽りでいい', + }, + ], + }, + { + time: 192600, + words: [ + { + string: '夜が明けるその時まで', + }, + ], + }, + { + time: 195820, + words: [ + { + string: '嘘をつき通して', + }, + ], + }, + { + time: 198730, + words: [ + { + string: '刹那の時に溺れる様に', + }, + ], + }, + { + time: 204440, + words: [ + { + string: '君を感じさせていて 今だけは...', + }, + ], + }, + { + time: 215140, + words: [ + { + string: '', + }, + ], + }, + ], + language: 'ja', +}; + +export type SpotifyTrackLyrics = typeof spotifyTrackLyrics; diff --git a/src/types.d.ts b/src/types.d.ts index 8b1d9ec..f439a68 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -3,6 +3,7 @@ declare module 'chinese-conv' { export const tify: (s: string) => string; } +// web api interface FontMetadata { family: string; fullName: string; diff --git a/yarn.lock b/yarn.lock index 9cf73f6..0799be5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1428,6 +1428,20 @@ css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +"d3-array@2.5.0 - 3": + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +d3-geo@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.1.tgz#6027cf51246f9b2ebd64f99e01dc7c3364033a4d" + integrity sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q== + dependencies: + d3-array "2.5.0 - 3" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -1539,6 +1553,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deep-query-selector@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/deep-query-selector/-/deep-query-selector-1.0.2.tgz#42487af5e13e14142920a449b26c5c293bb9c103" + integrity sha512-yLIhd0Rcwvxen5OymvdE5Ah5KB/MGfln1vSkw0t0yoQdnh2SEKCEz8Ir+rBLPYwDzs08nterVC8vOgi0irfv4w== + deepcopy@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/deepcopy/-/deepcopy-2.1.0.tgz#2deb0dd52d079c2ecb7924b640a7c3abd4db1d6d" @@ -1661,6 +1680,15 @@ dtrace-provider@~0.8: dependencies: nan "^2.14.0" +duoyun-ui@^1.1.20: + version "1.1.20" + resolved "https://registry.yarnpkg.com/duoyun-ui/-/duoyun-ui-1.1.20.tgz#b9d2857dc1bf11c91d90e3e766903887041f52ca" + integrity sha512-8jCSNNBGT4ipNy8XXYsvDUENuRKYBukeJJNCqKSVaup3WDbThNxpzXj8lDAx1wHav9TmliRYELOPPDmWPm5Vcw== + dependencies: + d3-geo "^3.0.1" + deep-query-selector "^1.0.1" + elkjs "^0.7.1" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -1681,6 +1709,11 @@ ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer "^5.0.1" +elkjs@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.7.1.tgz#4751c5e918a4988139baf7f214e010aea22de969" + integrity sha512-lD86RWdh480/UuRoHhRcnv2IMkIcK6yMDEuT8TPBIbO3db4HfnVF+1lgYdQi99Ck0yb+lg5Eb46JCHI5uOsmAw== + emoji-regex@^10.3.0: version "10.3.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" @@ -2794,6 +2827,11 @@ internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + invert-kv@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-3.0.1.tgz#a93c7a3d4386a1dc8325b97da9bb1620c0282523"