ts #1

Closed
opened 2024-04-06 01:47:08 +08:00 by sealday · 2 comments
Owner
/* 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,
    }));
};
```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, })); }; ```
Author
Owner
/* 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>
  );
}
```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> ); } ```
Author
Owner
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>
  );
}
```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> ); } ```
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: sealday/danmu-sim#1
No description provided.