diff --git a/app/danmu/page.tsx b/app/danmu/page.tsx new file mode 100644 index 0000000..8c9cad4 --- /dev/null +++ b/app/danmu/page.tsx @@ -0,0 +1,190 @@ +/* eslint-disable no-console -- debug */ +"use client"; +import { useRef, useState, useEffect } from "react"; +import { query } from "@/app/lib/actions/lyrics"; + +const useLyricsRunner = (runner: (lyric: string) => void, interval: number) => { + const [lyrics, setLyrics] = useState([]); + const [offset, setOffset] = useState(0); + const timerRef = useRef | null>(null); + const startTime = useRef(Date.now()); + const ref = useRef<{ lyrics: string[]; offset: number }>({ lyrics, offset }); + + const clearTimer = (): void => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + + const doLyric = (): void => { + console.log("testing ...", ref.current.lyrics.length); + if (ref.current.lyrics.length > 0) { + const currentTime = Date.now() - startTime.current; + const found = ref.current.lyrics.find((lyric) => { + const m = Number(lyric.slice(1, 3)); + const s = Number(lyric.slice(4, 6)); + const ms10 = Number(lyric.slice(7, 9)); + const ms = (m * 60 + s) * 1000 + ms10 * 10; + return ( + currentTime > ms + ref.current.offset && + ms + ref.current.offset > currentTime - interval + ); + }); + if (found) { + runner(found); + } + } + }; + + useEffect(() => { + ref.current.offset = offset; + ref.current.lyrics = lyrics; + }, [offset, lyrics]); + + const start = (): void => { + clearTimer(); + startTime.current = Date.now(); + timerRef.current = setInterval(doLyric, interval); + }; + const pause = (): void => { + clearTimer(); + }; + const resume = (): void => { + clearTimer(); + timerRef.current = setInterval(doLyric, interval); + }; + return { + offset, + setOffset, + setLyrics, + resume, + start, + pause, + }; +}; + +interface LyricItem { + timestamp: string; + content: string; +} + +export default function Page(): JSX.Element { + const ref = useRef(null); + const [history, setHistory] = useState([]); + + const { start, resume, pause, setLyrics, offset, setOffset } = + useLyricsRunner((line) => { + setHistory((items) => [ + { + content: line.slice(0, 10), + timestamp: line.slice(10), + }, + ...items, + ]); + console.log(line); + }, 200); + + return ( +
+
+
红豆
+
+
+
+
+
+
延迟 {offset / 1000} 秒
+ + + + + +
+
+
+
    + {history.map((item, i) => ( +
  • +
    {item.timestamp}
    +
    + + + +
    +
    + {item.content} +
    +
    +
  • + ))} +
+
+
+
+
+ ); +} diff --git a/app/lib/actions/lyrics.ts b/app/lib/actions/lyrics.ts new file mode 100644 index 0000000..28f9681 --- /dev/null +++ b/app/lib/actions/lyrics.ts @@ -0,0 +1,227 @@ +"use server"; + +import Fuse, { type IFuseOptions } from "fuse.js"; +import axios, { type AxiosResponse } from "axios"; + +const SEARCH_URL = "https://music.163.com/api/search/get"; +const LYRICS_URL = "https://music.163.com/api/song/lyric"; + +enum LyricSource { + GENIUS = "Genius", + LRCLIB = "lrclib.net", + NETEASE = "NetEase", +} + +interface InternetProviderLyricSearchResponse { + artist: string; + id: string; + name: string; + score?: number; + source: LyricSource; +} + +interface InternetProviderLyricResponse { + artist: string; + id: string; + lyrics: string; + name: string; + source: LyricSource; +} + +interface LyricSearchQuery { + album?: string; + artist?: string; + duration?: number; + name?: string; +} + +interface NetEaseResponse { + code: number; + result: Result; +} + +interface Result { + hasMore: boolean; + songCount: number; + songs: Song[]; +} + +interface Song { + album: Album; + alias: string[]; + artists: Artist[]; + copyrightId: number; + duration: number; + fee: number; + ftype: number; + id: number; + mark: number; + mvid: number; + name: string; + rUrl: null; + rtype: number; + status: number; + transNames?: string[]; +} + +interface Album { + artist: Artist; + copyrightId: number; + id: number; + mark: number; + name: string; + picId: number; + publishTime: number; + size: number; + status: number; + transNames?: string[]; +} + +interface Artist { + albumSize: number; + alias: any[]; + fansGroup: null; + id: number; + img1v1: number; + img1v1Url: string; + name: string; + picId: number; + picUrl: null; + trans: null; +} + +export async function getSearchResults( + params: LyricSearchQuery +): Promise { + let result: AxiosResponse; + + const searchQuery = [params.artist, params.name].join(" "); + + if (!searchQuery) { + return null; + } + + try { + result = await axios.get(SEARCH_URL, { + params: { + limit: 5, + offset: 0, + s: searchQuery, + type: "1", + }, + }); + } catch (e) { + console.error("NetEase search request got an error!", e); + return null; + } + + const rawSongsResult = result?.data.result?.songs; + + if (!rawSongsResult) return null; + + const songResults: InternetProviderLyricSearchResponse[] = rawSongsResult.map( + (song) => { + const artist = song.artists + ? song.artists.map((artist) => artist.name).join(", ") + : ""; + + return { + artist, + id: String(song.id), + name: song.name, + source: LyricSource.NETEASE, + }; + } + ); + + return orderSearchResults({ params, results: songResults }); +} + +async function getMatchedLyrics( + params: LyricSearchQuery +): Promise | null> { + const results = await getSearchResults(params); + + const firstMatch = results?.[0]; + + if (!firstMatch || (firstMatch?.score && firstMatch.score > 0.5)) { + return null; + } + + return firstMatch; +} + +export async function getLyricsBySongId( + songId: string +): Promise { + let result: AxiosResponse; + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- debug + result = await axios.get(LYRICS_URL, { + params: { + id: songId, + kv: "-1", + lv: "-1", + }, + }); + } catch (e) { + console.error("NetEase lyrics request got an error!", e); + return null; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- ddd + return result.data.klyric?.lyric || result.data.lrc?.lyric; +} + +export async function query( + params: LyricSearchQuery +): Promise { + const lyricsMatch = await getMatchedLyrics(params); + if (!lyricsMatch) { + console.error("Could not find the song on NetEase!"); + return null; + } + + const lyrics = await getLyricsBySongId(lyricsMatch.id); + if (!lyrics) { + console.error("Could not get lyrics on NetEase!"); + return null; + } + + return { + artist: lyricsMatch.artist, + id: lyricsMatch.id, + lyrics, + name: lyricsMatch.name, + source: LyricSource.NETEASE, + }; +} + +const orderSearchResults = (args: { + params: LyricSearchQuery; + results: InternetProviderLyricSearchResponse[]; +}): InternetProviderLyricSearchResponse[] => { + const { params, results } = args; + + const options: IFuseOptions = { + fieldNormWeight: 1, + includeScore: true, + keys: [ + { getFn: (song) => song.name, name: "name", weight: 3 }, + { getFn: (song) => song.artist, name: "artist" }, + ], + threshold: 1.0, + }; + + const fuse = new Fuse(results, options); + + const searchResults = fuse.search({ + ...(params.artist && { artist: params.artist }), + ...(params.name && { name: params.name }), + }); + + return searchResults.map((result) => ({ + ...result.item, + score: result.score, + })); +}; diff --git a/app/page.tsx b/app/page.tsx index 814256e..14c4a5b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -23,9 +23,9 @@ export default function Home() { > 弹幕测试 - +