ts #1
sealday
commented 2024-04-06 01:47:08 +08:00
Owner
```ts
/* eslint-disable no-console -- debug */
'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';
// Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/netease.ts
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<InternetProviderLyricSearchResponse[] | null> {
let result: AxiosResponse<NetEaseResponse>;
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<Omit<InternetProviderLyricResponse, 'lyrics'> | 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<string | null> {
let result: AxiosResponse<any, any>;
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<InternetProviderLyricResponse | null> {
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<InternetProviderLyricSearchResponse> = {
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<InternetProviderLyricSearchResponse>({
...(params.artist && { artist: params.artist }),
...(params.name && { name: params.name }),
});
return searchResults.map((result) => ({
...result.item,
score: result.score,
}));
};
```
sealday
commented 2024-04-06 01:47:26 +08:00
Author
Owner
```tsx
/* eslint-disable no-console -- debug */
"use client";
import { useRef, useState, useEffect } from "react";
import { query } from "./actions";
const useLyricsRunner = (runner: (lyric: string) => void, interval: number) => {
const [lyrics, setLyrics] = useState<string[]>([]);
const [offset, setOffset] = useState<number>(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const startTime = useRef<number>(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 Modal(): JSX.Element {
const ref = useRef<HTMLDialogElement | null>(null);
const [history, setHistory] = useState<LyricItem[]>([]);
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 (
<div className="columns-1">
<div className="w-full">
<div className="join">
<div className="stat join-item">延迟 {offset / 1000} 秒</div>
<button
className="btn join-item"
type="button"
onClick={() => {
pause();
}}
>
暂停输出
</button>
<button
className="btn join-item"
type="button"
onClick={() => {
resume();
}}
>
继续输出
</button>
<button
className="btn join-item"
type="button"
onClick={() => {
setOffset(offset + 1000);
}}
>
+1秒
</button>
<button
className="btn join-item"
type="button"
onClick={() => {
setOffset(offset - 1000);
}}
>
-1秒
</button>
<button
className="btn join-item"
onClick={() => {
query({
name: "红豆",
})
.then((result) => {
// eslint-disable-next-line no-console -- temp
console.log(result);
if (result) {
setLyrics(result.lyrics.split("\n"));
start();
}
})
.catch((err) => {
// eslint-disable-next-line no-console -- temp
console.error(err);
});
}}
type="button"
>
开始
</button>
</div>
</div>
<div className="w-full mt-8">
<ul className="timeline timeline-vertical">
{history.map((item, i) => (
<li key={i}>
<div className="timeline-start">{item.timestamp}</div>
<div className="timeline-middle">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="w-5 h-5"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="timeline-end timeline-box">
{item.content}
</div>
<hr />
</li>
))}
</ul>
</div>
{/* <button
className="btn"
onClick={() => ref.current?.showModal()}
type="button"
>
open modal
</button> */}
<dialog className="modal" id="my_modal_1" ref={ref}>
<div className="modal-box">
<h3 className="font-bold text-lg">Hello!</h3>
<p className="py-4">
Press ESC key or click the button below to close
</p>
<div className="modal-action">
<form method="dialog">
{/* if there is a button in form, it will close the modal */}
<button className="btn" type="submit">
Close
</button>
</form>
</div>
</div>
</dialog>
</div>
);
}
```
sealday
commented 2024-04-06 01:47:51 +08:00
Author
Owner
```tsx
import Modal from "./modal";
export default function Page(): JSX.Element {
return (
<main>
<div className="mockup-browser border">
<div className="mockup-browser-toolbar">
<div className="input">红豆</div>
</div>
<div className="flex justify-center px-4 py-16 border-t border-base-300">
{/* <h2>hello pages</h2> */}
{/* <button className="btn w-64 rounded-full" type="button">
Click me
</button> */}
<Modal />
</div>
</div>
</main>
);
}
```
sealday
closed this issue 2024-04-07 03:36:00 +08:00
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?