diff --git a/app/danmu/controls.tsx b/app/danmu/controls.tsx new file mode 100644 index 0000000..08fd1a7 --- /dev/null +++ b/app/danmu/controls.tsx @@ -0,0 +1,188 @@ +"use client"; +import { useRef, useState, useEffect } from "react"; +import { query } from "@/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 Controls(): 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/danmu/danmu.tsx b/app/danmu/danmu.tsx new file mode 100644 index 0000000..ce692d5 --- /dev/null +++ b/app/danmu/danmu.tsx @@ -0,0 +1,71 @@ +"use client"; +import { useState, useEffect, useCallback } from "react"; +import useWebSocket, { ReadyState } from "react-use-websocket"; +import MessageDispatcher from "./message"; +import { Token } from "@/lib/types"; + +const MOMO_IM_URL = "wss://live-ws.immomo.com/ws/im"; + +export const Danmu: React.FC<{ token: Token }> = ({ token }) => { + const [messageHistory, setMessageHistory] = useState[]>([]); + + const { sendMessage, lastMessage, readyState } = useWebSocket(MOMO_IM_URL, { + heartbeat: { + message: () => { + const pingMessage = { + msg_id: 2, + client_time: Date.now(), + type: "Ping", + data: {}, + }; + return JSON.stringify(pingMessage); + }, + interval: 5000, + }, + }); + + const momoId = token.uid; + const roomId = "1476810196216"; + const momoToken = token.token; + + const clientTime = Date.now(); + const data = { + msg_id: 1, + client_time: clientTime, + type: "Sauth", + data: { + momoid: momoId, + roomid: roomId, + role: 6, + isVisitor: false, + token: momoToken, + }, + }; + + useEffect(() => { + if (ReadyState.OPEN === readyState) { + sendMessage(JSON.stringify(data)); + } + }, [readyState]); + + useEffect(() => { + if (lastMessage !== null) { + if (lastMessage.data) { + const message = JSON.parse(lastMessage.data); + if (message.type === "pong") { + return; + } + console.log(message); + setMessageHistory((prev) => prev.concat(message)); + } + } + }, [lastMessage]); + + return ( +
+ {messageHistory.map((message, idx) => ( + + ))} +
+ ); +}; diff --git a/app/danmu/message.tsx b/app/danmu/message.tsx new file mode 100644 index 0000000..6675970 --- /dev/null +++ b/app/danmu/message.tsx @@ -0,0 +1,21 @@ +export default function MessageDispatcher({ message }: { message: any }) { + const messageType = message?.groups?.[0]?.units?.[0]?.type; + let nick = "wait"; + let content = "wait"; + + if (message?.type === 'sauth_ret') { + return
{message?.data?.em}
+ } + + if (messageType === "Message") { + nick = message?.groups?.[0]?.nick; + content = message?.groups?.[0]?.units?.[0]?.["Msg.Message.data"]?.text; + } + return ( +
+
+ {nick}: {content} +
+
+ ); +} diff --git a/app/danmu/page.tsx b/app/danmu/page.tsx index b201e02..04bf24c 100644 --- a/app/danmu/page.tsx +++ b/app/danmu/page.tsx @@ -1,191 +1,30 @@ -/* eslint-disable no-console -- debug */ -"use client"; -import { useRef, useState, useEffect } from "react"; -import { query } from "@/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); +import { getToken } from "@/lib/actions/token"; +import Controls from "./controls"; +import { Danmu } from "./danmu"; +export default async function Page() { + const token = await getToken(); return ( -
-
-
红豆
-
-
-
-
-
-
延迟 {offset / 1000} 秒
- - - - - +
+
+
直播间列表
+
+
+
歌曲清单
+
+
+
+
+
+
红豆
+
+
+ +
-
-
    - {history.map((item, i) => ( -
  • -
    {item.timestamp}
    -
    - - - -
    -
    - {item.content} -
    -
    -
  • - ))} -
-
-
+
); } diff --git a/app/layout.tsx b/app/layout.tsx index f2210a5..af25327 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,6 +6,8 @@ import Logo from "@/components/logo"; import BackButton from "@/components/back-button"; import Link from "next/link"; import { AdminButton } from "@/components/admin-button"; +import TokenButton from "@/components/token-button"; +import { getToken } from "@/lib/actions/token"; const inter = Inter({ subsets: ["latin"] }); @@ -14,11 +16,12 @@ export const metadata: Metadata = { description: "搜索歌曲,选择直播间,智能发送弹幕", }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const token = await getToken(); return ( @@ -38,7 +41,7 @@ export default function RootLayout({ 歌词曲库 - + 直播间列表 @@ -49,8 +52,8 @@ export default function RootLayout({
- +
diff --git a/app/music/danmu.tsx b/app/music/danmu.tsx deleted file mode 100644 index 76502bf..0000000 --- a/app/music/danmu.tsx +++ /dev/null @@ -1,100 +0,0 @@ -"use client"; -import { useState, useEffect, useCallback } from "react"; -import useWebSocket, { ReadyState } from "react-use-websocket"; - -const MOMO_IM_URL = "wss://live-ws.immomo.com/ws/im"; - -export const Danmu: React.FC = () => { - //Public API that will echo messages sent to it back to the client - const [socketUrl, setSocketUrl] = useState(MOMO_IM_URL); - const [messageHistory, setMessageHistory] = useState[]>([]); - - // { "msg_id": 1, "client_time": 1712305590234, "type": "Sauth", "data": { "momoid": "404821828", "roomid": "14515460054", "role": 6, "isVisitor": false, "token": "97e721d0bd3bf2e63a9797f9daf50919", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" } } - const { sendMessage, lastMessage, readyState } = useWebSocket(socketUrl, { - heartbeat: { - message: () => { - const pingMessage = { - msg_id: 2, - client_time: Date.now(), - type: "Ping", - data: {}, - }; - return JSON.stringify(pingMessage); - }, - interval: 5000, - }, - }); - - const momoId = "404821828"; - const roomId = "14515460054"; - const momoToken = "97e721d0bd3bf2e63a9797f9daf50919"; - - const clientTime = Date.now(); - const data = { - msg_id: 1, - client_time: clientTime, - type: "Sauth", - data: { - momoid: momoId, - roomid: roomId, - role: 6, - isVisitor: false, - token: momoToken, - }, - }; - - useEffect(() => { - if (ReadyState.OPEN === readyState) { - sendMessage(JSON.stringify(data)); - } - }, [readyState]); - - useEffect(() => { - if (lastMessage !== null) { - if (lastMessage.data) { - const message = JSON.parse(lastMessage.data); - if (message.type === "pong") { - return; - } - setMessageHistory((prev) => prev.concat(message)); - } - } - }, [lastMessage]); - - const connectionStatus = { - [ReadyState.CONNECTING]: "Connecting", - [ReadyState.OPEN]: "Open", - [ReadyState.CLOSING]: "Closing", - [ReadyState.CLOSED]: "Closed", - [ReadyState.UNINSTANTIATED]: "Uninstantiated", - }[readyState]; - - return ( -
- {messageHistory.map((message, idx) => ( - - ))} -
-
You underestimate my power!
-
-
- ); -}; - -const MessageDispatcher: React.FC<{ message: any }> = ({ message }) => { - const messageType = message?.groups?.[0]?.units?.[0]?.type; - let nick = "wait"; - let content = "wait"; - - if (messageType === "Message") { - nick = message?.groups?.[0]?.nick; - content = message?.groups?.[0]?.units?.[0]?.["Msg.Message.data"]?.text; - } - return ( -
-
- {nick}: {content} -
-
- ); -}; diff --git a/app/music/page.tsx b/app/music/page.tsx deleted file mode 100644 index c957c1f..0000000 --- a/app/music/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Danmu } from "./danmu"; - -export default function Page() { - return ; -} diff --git a/app/page.tsx b/app/page.tsx index 14c4a5b..3ccb362 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import Link from "next/link"; import musicSvg from "@/app/music.svg"; -export default function Home() { +export default async function Home() { return (
@@ -18,12 +18,12 @@ export default function Home() {

弹幕测试 - + 歌词曲库
diff --git a/app/signin/page.tsx b/app/signin/page.tsx index 7d1cbf1..90fdf16 100644 --- a/app/signin/page.tsx +++ b/app/signin/page.tsx @@ -23,7 +23,7 @@ export default function Page() {

登录 diff --git a/app/signup/page.tsx b/app/signup/page.tsx index c42ad54..67c814b 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -22,7 +22,7 @@ export default function Page() {

注册 diff --git a/components/token-button.tsx b/components/token-button.tsx new file mode 100644 index 0000000..9bc113a --- /dev/null +++ b/components/token-button.tsx @@ -0,0 +1,53 @@ +"use client"; +import { addToken } from "@/lib/actions/token"; +import { Token } from "@/lib/types"; +import { useEffect, useRef } from "react"; +import { useFormState } from "react-dom"; + +export default function TokenButton({ + className, + token, +}: { + className?: string | undefined; + token: Token; +}) { + const ref = useRef(null); + const [returnType, dispatch] = useFormState(addToken, token); + // TODO 有没有更合适的方式? + if (returnType) { + ref.current?.close(); + } + return ( + <> + + +
+

Token 设定

+ + + + +
+
+ +
+ +
+
+
+ +
+
+ + ); +} diff --git a/lib/actions/token.ts b/lib/actions/token.ts new file mode 100644 index 0000000..e1ba491 --- /dev/null +++ b/lib/actions/token.ts @@ -0,0 +1,57 @@ +"use server"; + +import { revalidateTag } from "next/cache"; +import { cookies } from "next/headers"; +import { Token } from "../types"; + +export async function addToken(_: Token, item: FormData): Promise { + const token = cookies().get("token"); + const result = await fetch( + "https://tachy.daoyoucloud.com/api/tokens:create", + { + method: "POST", + headers: { + "X-App": "danmu-sim", + "X-Authenticator": "basic", + authorization: "Bearer " + token?.value, + "content-type": "application/json", + }, + body: JSON.stringify({ + uid: item.get("uid") as string, + token: item.get("token") as string, + }), + } + ); + const latest = await result.json(); + revalidateTag("tokens"); + return latest.data +} + +export async function getToken(): Promise { + const token = cookies().get("token"); + const params = new URLSearchParams(); + params.append("pageSize", "20"); + params.append("limit", "1"); + params.append("sort", "-createdAt"); + params.append( + "filter", + JSON.stringify({ $and: [{ createdBy: { id: { $eq: "{{$user.id}}" } } }] }) + ); + const liveTokens = await fetch( + "https://tachy.daoyoucloud.com/api/tokens:list?" + params.toString(), + { + method: "GET", + headers: { + "X-App": "danmu-sim", + "X-Authenticator": "basic", + authorization: "Bearer " + token?.value, + "content-type": "application/json", + }, + next: { + tags: ["tokens"], + }, + } + ); + const result = await liveTokens.json(); + return result?.data?.[0] ?? { uid: "", token: "" }; +} diff --git a/lib/types.ts b/lib/types.ts index 1def8d7..d5e67f2 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -4,4 +4,9 @@ export enum LyricSource { LRCLIB = "lrclib.net", NETEASE = "NetEase", QQMUSIC = "QQMusic", -} \ No newline at end of file +} + +export interface Token { + uid: string; + token: string; +}