danmu-sim/lib/actions/lyrics.ts
2024-04-06 18:08:51 +08:00

223 lines
4.7 KiB
TypeScript

"use server";
import Fuse, { type IFuseOptions } from "fuse.js";
import axios, { type AxiosResponse } from "axios";
import { LyricSource } from "../types";
const SEARCH_URL = "https://music.163.com/api/search/get";
const LYRICS_URL = "https://music.163.com/api/song/lyric";
export 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,
}));
};