From 0705e8a65a4d1cd82ccdc7b4fe54a86bf7c254ec Mon Sep 17 00:00:00 2001 From: Gabe Date: Sat, 22 Nov 2025 00:20:58 +0800 Subject: [PATCH] fix: add baidu audio for youdao dict --- src/hooks/Audio.js | 117 ++++++++++++++++++++--------- src/libs/cache.js | 44 ++++++++--- src/libs/fetch.js | 2 +- src/views/Selection/AudioBtn.js | 10 ++- src/views/Selection/DictHandler.js | 4 +- 5 files changed, 129 insertions(+), 48 deletions(-) diff --git a/src/hooks/Audio.js b/src/hooks/Audio.js index ba28c12..5b93c8e 100644 --- a/src/hooks/Audio.js +++ b/src/hooks/Audio.js @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { apiBaiduTTS } from "../apis"; -import { kissLog } from "../libs/log"; +import { logger } from "../libs/log"; +import { fetchData } from "../libs/fetch"; /** * 声音播放hook @@ -12,50 +12,97 @@ export function useAudio(src) { const [error, setError] = useState(null); const [ready, setReady] = useState(false); const [playing, setPlaying] = useState(false); + const [loading, setLoading] = useState(false); - const onPlay = useCallback(() => { - audioRef.current?.play(); + const onPlay = useCallback(async () => { + if (!audioRef.current) return; + try { + await audioRef.current.play(); + } catch (err) { + logger.info("Playback failed:", err); + setPlaying(false); + } + }, []); + + const onPause = useCallback(() => { + audioRef.current?.pause(); }, []); useEffect(() => { - if (!src) { - return; - } - const audio = new Audio(src); - audio.addEventListener("error", (err) => setError(err)); - audio.addEventListener("canplaythrough", () => setReady(true)); - audio.addEventListener("play", () => setPlaying(true)); - audio.addEventListener("ended", () => setPlaying(false)); + if (!src) return; + + let ignore = false; + let objectUrl = null; + + setReady(false); + setError(null); + setPlaying(false); + setLoading(true); + + const audio = new Audio(); audioRef.current = audio; + + const handleCanPlay = () => setReady(true); + const handlePlay = () => setPlaying(true); + const handlePause = () => setPlaying(false); + const handleEnded = () => setPlaying(false); + const handleError = (e) => { + if (!ignore) { + setError(audio.error || e); + setReady(false); + setLoading(false); + } + }; + + audio.addEventListener("canplaythrough", handleCanPlay); + audio.addEventListener("play", handlePlay); + audio.addEventListener("pause", handlePause); + audio.addEventListener("ended", handleEnded); + audio.addEventListener("error", handleError); + + const loadAudio = async () => { + try { + const data = await fetchData(src, {}, { expect: "audio" }); + if (ignore) return; + + audio.src = data; + + setLoading(false); + } catch (err) { + if (!ignore) { + logger.info("Audio fetch failed:", err); + setError(err); + setLoading(false); + } + } + }; + + loadAudio(); + + return () => { + ignore = true; + + audio.pause(); + audio.removeAttribute("src"); + + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + + audio.removeEventListener("canplaythrough", handleCanPlay); + audio.removeEventListener("play", handlePlay); + audio.removeEventListener("pause", handlePause); + audio.removeEventListener("ended", handleEnded); + audio.removeEventListener("error", handleError); + }; }, [src]); return { + loading, error, ready, playing, onPlay, + onPause, }; } - -/** - * 获取语音hook - * @param {*} text - * @param {*} lan - * @param {*} spd - * @returns - */ -export function useTextAudio(text, lan = "uk", spd = 3) { - const [src, setSrc] = useState(""); - - useEffect(() => { - (async () => { - try { - setSrc(await apiBaiduTTS(text, lan, spd)); - } catch (err) { - kissLog("baidu tts", err); - } - })(); - }, [text, lan, spd]); - - return useAudio(src); -} diff --git a/src/libs/cache.js b/src/libs/cache.js index 9a9239a..2e30a6d 100644 --- a/src/libs/cache.js +++ b/src/libs/cache.js @@ -50,13 +50,13 @@ const newCacheReq = async (input, init) => { * @param {*} init * @returns */ -export const getHttpCache = async ({ input, init }) => { +export const getHttpCache = async ({ input, init, expect }) => { try { const request = await newCacheReq(input, init); const cache = await caches.open(CACHE_NAME); const response = await cache.match(request); if (response) { - const res = await parseResponse(response); + const res = await parseResponse(response, expect); return res; } } catch (err) { @@ -99,7 +99,7 @@ export const putHttpCache = async ({ * @param {*} res * @returns */ -export const parseResponse = async (res) => { +export const parseResponse = async (res, expect = null) => { if (!res) { throw new Error("Response object does not exist"); } @@ -108,21 +108,45 @@ export const parseResponse = async (res) => { const msg = { url: res.url, status: res.status, + statusText: res.statusText, }; - if (res.headers.get("Content-Type")?.includes("json")) { - msg.response = await res.json(); + + try { + const errorText = await res.clone().text(); + try { + msg.response = JSON.parse(errorText); + } catch { + msg.response = errorText; + } + } catch (e) { + msg.response = "Unable to read error body"; } + throw new Error(JSON.stringify(msg)); } - const contentType = res.headers.get("Content-Type"); - if (contentType?.includes("json")) { - return res.json(); - } else if (contentType?.includes("audio")) { + const contentType = res.headers.get("Content-Type") || ""; + if (expect === "blob") return res.blob(); + if (expect === "text") return res.text(); + if (expect === "json") return res.json(); + if ( + expect === "audio" || + contentType.includes("audio") || + contentType.includes("image") || + contentType.includes("video") + ) { const blob = await res.blob(); return blobToBase64(blob); } - return res.text(); + + const text = await res.text(); + if (!text) return null; + + try { + return JSON.parse(text); + } catch (err) { + return text; + } }; /** diff --git a/src/libs/fetch.js b/src/libs/fetch.js index 5ea2a96..551d88b 100644 --- a/src/libs/fetch.js +++ b/src/libs/fetch.js @@ -99,7 +99,7 @@ export const fetchPatcher = async (input, init = {}, opts) => { */ export const fetchHandle = async ({ input, init, opts }) => { const res = await fetchPatcher(input, init, opts); - return parseResponse(res); + return parseResponse(res, opts.expect); }; /** diff --git a/src/views/Selection/AudioBtn.js b/src/views/Selection/AudioBtn.js index 17810bb..3bcd21d 100644 --- a/src/views/Selection/AudioBtn.js +++ b/src/views/Selection/AudioBtn.js @@ -1,8 +1,9 @@ import IconButton from "@mui/material/IconButton"; import VolumeUpIcon from "@mui/icons-material/VolumeUp"; import { useAudio } from "../../hooks/Audio"; +import queryString from "query-string"; -export default function AudioBtn({ src }) { +export function AudioBtn({ src }) { const { error, ready, playing, onPlay } = useAudio(src); if (error || !ready) { @@ -27,3 +28,10 @@ export default function AudioBtn({ src }) { ); } + +export function BaiduAudioBtn({ text, lan = "uk", spd = 3 }) { + if (!text) return null; + + const src = `https://fanyi.baidu.com/gettts?${queryString.stringify({ lan, text, spd })}`; + return ; +} diff --git a/src/views/Selection/DictHandler.js b/src/views/Selection/DictHandler.js index 6720ea3..6adf32a 100644 --- a/src/views/Selection/DictHandler.js +++ b/src/views/Selection/DictHandler.js @@ -1,5 +1,5 @@ import Typography from "@mui/material/Typography"; -import AudioBtn from "./AudioBtn"; +import { AudioBtn, BaiduAudioBtn } from "./AudioBtn"; import { OPT_DICT_BING, OPT_DICT_YOUDAO } from "../../config"; import { apiMicrosoftDict, apiYoudaoDict } from "../../apis"; @@ -48,12 +48,14 @@ export const dictHandlers = { style={{ display: "inline-block", paddingRight: "1em" }} > {`UK [${data?.ec?.word?.ukphone}]`} + {`US [${data?.ec?.word?.usphone}]`} + ),