"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 { 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, })); };