refactor: improve error handling for custom player

This commit is contained in:
Kentai Radiquum 2025-03-22 21:04:31 +05:00
parent fc8fe97da7
commit f94b551cdf
Signed by: Radiquum
GPG key ID: 858E8EE696525EED
2 changed files with 328 additions and 187 deletions

View file

@ -27,6 +27,46 @@ export async function tryCatch<T, E = Error>(
} }
} }
export async function tryCatchPlayer<T, E = Error>(
promise: Promise<any>
): Promise<Result<any, any>> {
try {
const res: Awaited<Response> = await promise;
const data = await res.json();
if (!res.ok) {
if (data.message) {
return {
data: null,
error: {
message: data.message,
code: res.status,
},
};
} else if (data.detail) {
return {
data: null,
error: {
message: data.detail,
code: res.status,
},
};
} else {
return {
data: null,
error: {
message: res.statusText,
code: res.status,
},
};
}
}
return { data, error: null };
} catch (error) {
return { data: null, error: error as E };
}
}
export async function tryCatchAPI<T, E = Error>( export async function tryCatchAPI<T, E = Error>(
promise: Promise<any> promise: Promise<any>
): Promise<Result<any, any>> { ): Promise<Result<any, any>> {

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { Card } from "flowbite-react"; import { Button, Card } from "flowbite-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
@ -14,6 +14,7 @@ import HlsVideo from "hls-video-element/react";
import VideoJS from "videojs-video-element/react"; import VideoJS from "videojs-video-element/react";
import MediaThemeSutro from "./MediaThemeSutro"; import MediaThemeSutro from "./MediaThemeSutro";
import { getAnonEpisodesWatched } from "./ReleasePlayer"; import { getAnonEpisodesWatched } from "./ReleasePlayer";
import { tryCatchPlayer, tryCatchAPI } from "#/api/utils";
export const ReleasePlayerCustom = (props: { export const ReleasePlayerCustom = (props: {
id: number; id: number;
@ -38,48 +39,76 @@ export const ReleasePlayerCustom = (props: {
useCustom: false, useCustom: false,
}); });
const [playbackRate, setPlaybackRate] = useState(1); const [playbackRate, setPlaybackRate] = useState(1);
const [playerError, setPlayerError] = useState(null);
const [isErrorDetailsOpen, setIsErrorDetailsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const playerPreferenceStore = useUserPlayerPreferencesStore(); const playerPreferenceStore = useUserPlayerPreferencesStore();
const preferredVO = playerPreferenceStore.getPreferredVoiceover(props.id); const preferredVO = playerPreferenceStore.getPreferredVoiceover(props.id);
const preferredSource = playerPreferenceStore.getPreferredPlayer(props.id); const preferredSource = playerPreferenceStore.getPreferredPlayer(props.id);
const _fetchVoiceover = async (release_id: number) => { async function _fetchAPI(
let url = `${ENDPOINTS.release.episode}/${release_id}`; url: string,
if (props.token) { onErrorMsg: string,
url += `?token=${props.token}`; onErrorCodes?: Record<number, string>
} ) {
const response = await fetch(url); const { data, error } = await tryCatchAPI(fetch(url));
const data = await response.json(); if (error) {
return data; let errorDetail = "Мы правда не знаем что произошло...";
};
const _fetchSource = async (release_id: number, voiceover_id: number) => { if (error.name) {
const response = await fetch( if (error.name == "TypeError") {
`${ENDPOINTS.release.episode}/${release_id}/${voiceover_id}` errorDetail = "Не удалось подключиться к серверу";
); } else {
const data = await response.json(); errorDetail = `Неизвестная ошибка ${error.name}: ${error.message}`;
return data; }
}; }
if (error.code) {
const _fetchEpisode = async ( if (Object.keys(onErrorCodes).includes(error.code.toString())) {
release_id: number, errorDetail = onErrorCodes[error.code.toString()];
voiceover_id: number, } else {
source_id: number errorDetail = `API вернуло ошибку: ${error.code}`;
) => { }
let url = `${ENDPOINTS.release.episode}/${release_id}/${voiceover_id}/${source_id}`; }
if (props.token) {
url += `?token=${props.token}`; setPlayerError({
message: onErrorMsg,
detail: errorDetail,
});
return null;
} }
const response = await fetch(url);
const data = await response.json();
return data; return data;
}; }
async function _fetchPlayer(url: string) {
const { data, error } = (await tryCatchPlayer(fetch(url))) as any;
if (error) {
let errorDetail = "Мы правда не знаем что произошло...";
if (error.name) {
if (error.name == "TypeError") {
errorDetail = "Не удалось подключиться к серверу";
} else {
errorDetail = `Неизвестная ошибка ${error.name}: ${error.message}`;
}
} else if (error.message) {
errorDetail = error.message;
}
setPlayerError({
message: "Не удалось получить ссылку на видео",
detail: errorDetail,
});
return null;
}
return data;
}
const _fetchKodikManifest = async (url: string) => { const _fetchKodikManifest = async (url: string) => {
const response = await fetch( const data = await _fetchPlayer(
`https://anix-player.wah.su/?url=${url}&player=kodik` `https://anix-player.wah.su/?url=${url}&player=kodik`
); );
const data = await response.json(); if (data) {
let lowQualityLink = data.links["360"][0].src; let lowQualityLink = data.links["360"][0].src;
if (lowQualityLink.includes("https://")) { if (lowQualityLink.includes("https://")) {
lowQualityLink = lowQualityLink.replace("https://", "//"); lowQualityLink = lowQualityLink.replace("https://", "//");
@ -112,7 +141,8 @@ export const ReleasePlayerCustom = (props: {
} }
if (data.links.hasOwnProperty("720")) { if (data.links.hasOwnProperty("720")) {
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n"; blobTxt +=
"#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n";
!data.links["720"][0].src.startsWith("https:") ? !data.links["720"][0].src.startsWith("https:") ?
(blobTxt += `https:${data.links["720"][0].src}\n`) (blobTxt += `https:${data.links["720"][0].src}\n`)
: (blobTxt += `${data.links["720"][0].src}\n`); : (blobTxt += `${data.links["720"][0].src}\n`);
@ -124,14 +154,16 @@ export const ReleasePlayerCustom = (props: {
manifest = URL.createObjectURL(file); manifest = URL.createObjectURL(file);
} }
return { manifest, poster }; return { manifest, poster };
}
return { manifest: null, poster: null };
}; };
const _fetchAnilibriaManifest = async (url: string) => { const _fetchAnilibriaManifest = async (url: string) => {
const id = url.split("?id=")[1].split("&ep=")[0]; const id = url.split("?id=")[1].split("&ep=")[0];
const data = await _fetchPlayer(
const response = await fetch(`https://api.anilibria.tv/v3/title?id=${id}`); `https://api.anilibria.tv/v3/title?id=${id}`
const data = await response.json(); );
if (data) {
const host = `https://${data.player.host}`; const host = `https://${data.player.host}`;
const ep = data.player.list[episode.selected.position]; const ep = data.player.list[episode.selected.position];
@ -142,22 +174,34 @@ export const ReleasePlayerCustom = (props: {
let manifest = URL.createObjectURL(file); let manifest = URL.createObjectURL(file);
let poster = `https://anixart.libria.fun${ep.preview}`; let poster = `https://anixart.libria.fun${ep.preview}`;
return { manifest, poster }; return { manifest, poster };
}
return { manifest: null, poster: null };
}; };
const _fetchSibnetManifest = async (url: string) => { const _fetchSibnetManifest = async (url: string) => {
const response = await fetch( const data = await _fetchPlayer(
`https://sibnet.anix-player.wah.su/?url=${url}` `https://sibnet.anix-player.wah.su/?url=${url}`
); );
const data = await response.json(); if (data) {
let manifest = data.video; let manifest = data.video;
let poster = data.poster; let poster = data.poster;
return { manifest, poster }; return { manifest, poster };
}
return { manifest: null, poster: null };
}; };
useEffect(() => { useEffect(() => {
const __getInfo = async () => { const __getInfo = async () => {
const vo = await _fetchVoiceover(props.id); let url = `${ENDPOINTS.release.episode}/${props.id}`;
if (props.token) {
url += `?token=${props.token}`;
}
const vo = await _fetchAPI(
url,
"Не удалось получить информацию о озвучках",
{ 1: "Просмотр запрещён" }
);
if (vo) {
const selectedVO = const selectedVO =
vo.types.find((voiceover: any) => voiceover.name === preferredVO) || vo.types.find((voiceover: any) => voiceover.name === preferredVO) ||
vo.types[0]; vo.types[0];
@ -165,13 +209,19 @@ export const ReleasePlayerCustom = (props: {
selected: selectedVO, selected: selectedVO,
available: vo.types, available: vo.types,
}); });
}
}; };
__getInfo(); __getInfo();
}, []); }, []);
useEffect(() => { useEffect(() => {
const __getInfo = async () => { const __getInfo = async () => {
const src = await _fetchSource(props.id, voiceover.selected.id); let url = `${ENDPOINTS.release.episode}/${props.id}/${voiceover.selected.id}`;
const src = await _fetchAPI(
url,
"Не удалось получить информацию о источниках"
);
if (src) {
const selectedSrc = const selectedSrc =
src.sources.find((source: any) => source.name === preferredSource) || src.sources.find((source: any) => source.name === preferredSource) ||
src.sources[0]; src.sources[0];
@ -189,6 +239,7 @@ export const ReleasePlayerCustom = (props: {
selected: selectedSrc, selected: selectedSrc,
available: src.sources, available: src.sources,
}); });
}
}; };
if (voiceover.selected) { if (voiceover.selected) {
__getInfo(); __getInfo();
@ -197,12 +248,15 @@ export const ReleasePlayerCustom = (props: {
useEffect(() => { useEffect(() => {
const __getInfo = async () => { const __getInfo = async () => {
const episodes = await _fetchEpisode( let url = `${ENDPOINTS.release.episode}/${props.id}/${voiceover.selected.id}/${source.selected.id}`;
props.id, if (props.token) {
voiceover.selected.id, url += `?token=${props.token}`;
source.selected.id }
const episodes = await _fetchAPI(
url,
"Не удалось получить информацию о эпизодах"
); );
if (episodes) {
let anonEpisodesWatched = getAnonEpisodesWatched( let anonEpisodesWatched = getAnonEpisodesWatched(
props.id, props.id,
source.selected.id, source.selected.id,
@ -225,6 +279,7 @@ export const ReleasePlayerCustom = (props: {
selected: selectedEpisode, selected: selectedEpisode,
available: episodes.episodes, available: episodes.episodes,
}); });
}
}; };
if (source.selected) { if (source.selected) {
__getInfo(); __getInfo();
@ -237,36 +292,45 @@ export const ReleasePlayerCustom = (props: {
const { manifest, poster } = await _fetchKodikManifest( const { manifest, poster } = await _fetchKodikManifest(
episode.selected.url episode.selected.url
); );
if (manifest) {
SetPlayerProps({ SetPlayerProps({
src: manifest, src: manifest,
poster: poster, poster: poster,
useCustom: true, useCustom: true,
type: "hls", type: "hls",
}); });
setIsLoading(false);
}
return; return;
} }
if (source.selected.name == "Libria") { if (source.selected.name == "Libria") {
const { manifest, poster } = await _fetchAnilibriaManifest( const { manifest, poster } = await _fetchAnilibriaManifest(
episode.selected.url episode.selected.url
); );
if (manifest) {
SetPlayerProps({ SetPlayerProps({
src: manifest, src: manifest,
poster: poster, poster: poster,
useCustom: true, useCustom: true,
type: "hls", type: "hls",
}); });
setIsLoading(false);
}
return; return;
} }
if (source.selected.name == "Sibnet") { if (source.selected.name == "Sibnet") {
const { manifest, poster } = await _fetchSibnetManifest( const { manifest, poster } = await _fetchSibnetManifest(
episode.selected.url episode.selected.url
); );
if (manifest) {
SetPlayerProps({ SetPlayerProps({
src: manifest, src: manifest,
poster: poster, poster: poster,
useCustom: true, useCustom: true,
type: "mp4", type: "mp4",
}); });
setIsLoading(false);
}
return; return;
} }
SetPlayerProps({ SetPlayerProps({
@ -275,6 +339,7 @@ export const ReleasePlayerCustom = (props: {
useCustom: false, useCustom: false,
type: null, type: null,
}); });
setIsLoading(false);
}; };
if (episode.selected) { if (episode.selected) {
__getInfo(); __getInfo();
@ -283,31 +348,47 @@ export const ReleasePlayerCustom = (props: {
return ( return (
<Card className="aspect-video min-h-min-h-[300px] sm:min-h-[466px] md:min-h-[540px] lg:min-h-[512px] xl:min-h-[608px] 2xl:min-h-[712px]"> <Card className="aspect-video min-h-min-h-[300px] sm:min-h-[466px] md:min-h-[540px] lg:min-h-[512px] xl:min-h-[608px] 2xl:min-h-[712px]">
{(
!voiceover.selected ||
!source.selected ||
!episode.selected ||
!playerProps.src
) ?
<div className="flex items-center justify-center w-full aspect-video">
<Spinner />
</div>
: <div className="flex flex-col gap-4">
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
{voiceover.selected && (
<VoiceoverSelector <VoiceoverSelector
availableVoiceover={voiceover.available} availableVoiceover={voiceover.available}
voiceover={voiceover.selected} voiceover={voiceover.selected}
setVoiceover={setVoiceover} setVoiceover={setVoiceover}
release_id={props.id} release_id={props.id}
/> />
)}
{source.selected && (
<SourceSelector <SourceSelector
availableSource={source.available} availableSource={source.available}
source={source.selected} source={source.selected}
setSource={setSource} setSource={setSource}
release_id={props.id} release_id={props.id}
/> />
)}
</div> </div>
{playerProps.useCustom ?
<div className="flex items-center justify-center w-full h-full">
{isLoading ?
!playerError ?
<Spinner />
: <div className="flex flex-col gap-2">
<p className="text-lg font-bold">Ошибка: {playerError.message}</p>
{!isErrorDetailsOpen ?
<Button
color="light"
size="xs"
onClick={() => setIsErrorDetailsOpen(true)}
>
Подробнее
</Button>
: <p className="text-gray-600 dark:text-gray-100">
{playerError.detail}
</p>
}
</div>
: playerProps.useCustom ?
!playerError ?
<MediaThemeSutro className="object-none w-full aspect-video"> <MediaThemeSutro className="object-none w-full aspect-video">
{playerProps.type == "hls" ? {playerProps.type == "hls" ?
<HlsVideo <HlsVideo
@ -334,7 +415,27 @@ export const ReleasePlayerCustom = (props: {
></VideoJS> ></VideoJS>
} }
</MediaThemeSutro> </MediaThemeSutro>
: <div className="flex flex-col gap-2">
<p className="text-lg font-bold">Ошибка: {playerError.message}</p>
{!isErrorDetailsOpen ?
<Button
color="light"
size="xs"
onClick={() => setIsErrorDetailsOpen(true)}
>
Подробнее
</Button>
: <p className="text-gray-600 dark:text-gray-100">
{playerError.detail}
</p>
}
</div>
: <iframe src={playerProps.src} className="w-full aspect-video" />} : <iframe src={playerProps.src} className="w-full aspect-video" />}
</div>
<div>
{episode.selected && source.selected && voiceover.selected && (
<EpisodeSelector <EpisodeSelector
availableEpisodes={episode.available} availableEpisodes={episode.available}
episode={episode.selected} episode={episode.selected}
@ -344,8 +445,8 @@ export const ReleasePlayerCustom = (props: {
voiceover={voiceover.selected} voiceover={voiceover.selected}
token={props.token} token={props.token}
/> />
)}
</div> </div>
}
</Card> </Card>
); );
}; };