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>(
promise: Promise<any>
): Promise<Result<any, any>> {

View file

@ -1,6 +1,6 @@
"use client";
import { Card } from "flowbite-react";
import { Button, Card } from "flowbite-react";
import { useEffect, useState } from "react";
import { ENDPOINTS } from "#/api/config";
@ -14,6 +14,7 @@ import HlsVideo from "hls-video-element/react";
import VideoJS from "videojs-video-element/react";
import MediaThemeSutro from "./MediaThemeSutro";
import { getAnonEpisodesWatched } from "./ReleasePlayer";
import { tryCatchPlayer, tryCatchAPI } from "#/api/utils";
export const ReleasePlayerCustom = (props: {
id: number;
@ -38,48 +39,76 @@ export const ReleasePlayerCustom = (props: {
useCustom: false,
});
const [playbackRate, setPlaybackRate] = useState(1);
const [playerError, setPlayerError] = useState(null);
const [isErrorDetailsOpen, setIsErrorDetailsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const playerPreferenceStore = useUserPlayerPreferencesStore();
const preferredVO = playerPreferenceStore.getPreferredVoiceover(props.id);
const preferredSource = playerPreferenceStore.getPreferredPlayer(props.id);
const _fetchVoiceover = async (release_id: number) => {
let url = `${ENDPOINTS.release.episode}/${release_id}`;
if (props.token) {
url += `?token=${props.token}`;
}
const response = await fetch(url);
const data = await response.json();
return data;
};
async function _fetchAPI(
url: string,
onErrorMsg: string,
onErrorCodes?: Record<number, string>
) {
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
let errorDetail = "Мы правда не знаем что произошло...";
const _fetchSource = async (release_id: number, voiceover_id: number) => {
const response = await fetch(
`${ENDPOINTS.release.episode}/${release_id}/${voiceover_id}`
);
const data = await response.json();
return data;
};
const _fetchEpisode = async (
release_id: number,
voiceover_id: number,
source_id: number
) => {
let url = `${ENDPOINTS.release.episode}/${release_id}/${voiceover_id}/${source_id}`;
if (props.token) {
url += `?token=${props.token}`;
if (error.name) {
if (error.name == "TypeError") {
errorDetail = "Не удалось подключиться к серверу";
} else {
errorDetail = `Неизвестная ошибка ${error.name}: ${error.message}`;
}
}
if (error.code) {
if (Object.keys(onErrorCodes).includes(error.code.toString())) {
errorDetail = onErrorCodes[error.code.toString()];
} else {
errorDetail = `API вернуло ошибку: ${error.code}`;
}
}
setPlayerError({
message: onErrorMsg,
detail: errorDetail,
});
return null;
}
const response = await fetch(url);
const data = await response.json();
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 response = await fetch(
const data = await _fetchPlayer(
`https://anix-player.wah.su/?url=${url}&player=kodik`
);
const data = await response.json();
if (data) {
let lowQualityLink = data.links["360"][0].src;
if (lowQualityLink.includes("https://")) {
lowQualityLink = lowQualityLink.replace("https://", "//");
@ -112,7 +141,8 @@ export const ReleasePlayerCustom = (props: {
}
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:") ?
(blobTxt += `https:${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);
}
return { manifest, poster };
}
return { manifest: null, poster: null };
};
const _fetchAnilibriaManifest = async (url: string) => {
const id = url.split("?id=")[1].split("&ep=")[0];
const response = await fetch(`https://api.anilibria.tv/v3/title?id=${id}`);
const data = await response.json();
const data = await _fetchPlayer(
`https://api.anilibria.tv/v3/title?id=${id}`
);
if (data) {
const host = `https://${data.player.host}`;
const ep = data.player.list[episode.selected.position];
@ -142,22 +174,34 @@ export const ReleasePlayerCustom = (props: {
let manifest = URL.createObjectURL(file);
let poster = `https://anixart.libria.fun${ep.preview}`;
return { manifest, poster };
}
return { manifest: null, poster: null };
};
const _fetchSibnetManifest = async (url: string) => {
const response = await fetch(
const data = await _fetchPlayer(
`https://sibnet.anix-player.wah.su/?url=${url}`
);
const data = await response.json();
if (data) {
let manifest = data.video;
let poster = data.poster;
return { manifest, poster };
}
return { manifest: null, poster: null };
};
useEffect(() => {
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 =
vo.types.find((voiceover: any) => voiceover.name === preferredVO) ||
vo.types[0];
@ -165,13 +209,19 @@ export const ReleasePlayerCustom = (props: {
selected: selectedVO,
available: vo.types,
});
}
};
__getInfo();
}, []);
useEffect(() => {
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 =
src.sources.find((source: any) => source.name === preferredSource) ||
src.sources[0];
@ -189,6 +239,7 @@ export const ReleasePlayerCustom = (props: {
selected: selectedSrc,
available: src.sources,
});
}
};
if (voiceover.selected) {
__getInfo();
@ -197,12 +248,15 @@ export const ReleasePlayerCustom = (props: {
useEffect(() => {
const __getInfo = async () => {
const episodes = await _fetchEpisode(
props.id,
voiceover.selected.id,
source.selected.id
let url = `${ENDPOINTS.release.episode}/${props.id}/${voiceover.selected.id}/${source.selected.id}`;
if (props.token) {
url += `?token=${props.token}`;
}
const episodes = await _fetchAPI(
url,
"Не удалось получить информацию о эпизодах"
);
if (episodes) {
let anonEpisodesWatched = getAnonEpisodesWatched(
props.id,
source.selected.id,
@ -225,6 +279,7 @@ export const ReleasePlayerCustom = (props: {
selected: selectedEpisode,
available: episodes.episodes,
});
}
};
if (source.selected) {
__getInfo();
@ -237,36 +292,45 @@ export const ReleasePlayerCustom = (props: {
const { manifest, poster } = await _fetchKodikManifest(
episode.selected.url
);
if (manifest) {
SetPlayerProps({
src: manifest,
poster: poster,
useCustom: true,
type: "hls",
});
setIsLoading(false);
}
return;
}
if (source.selected.name == "Libria") {
const { manifest, poster } = await _fetchAnilibriaManifest(
episode.selected.url
);
if (manifest) {
SetPlayerProps({
src: manifest,
poster: poster,
useCustom: true,
type: "hls",
});
setIsLoading(false);
}
return;
}
if (source.selected.name == "Sibnet") {
const { manifest, poster } = await _fetchSibnetManifest(
episode.selected.url
);
if (manifest) {
SetPlayerProps({
src: manifest,
poster: poster,
useCustom: true,
type: "mp4",
});
setIsLoading(false);
}
return;
}
SetPlayerProps({
@ -275,6 +339,7 @@ export const ReleasePlayerCustom = (props: {
useCustom: false,
type: null,
});
setIsLoading(false);
};
if (episode.selected) {
__getInfo();
@ -283,31 +348,47 @@ export const ReleasePlayerCustom = (props: {
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]">
{(
!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">
{voiceover.selected && (
<VoiceoverSelector
availableVoiceover={voiceover.available}
voiceover={voiceover.selected}
setVoiceover={setVoiceover}
release_id={props.id}
/>
)}
{source.selected && (
<SourceSelector
availableSource={source.available}
source={source.selected}
setSource={setSource}
release_id={props.id}
/>
)}
</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">
{playerProps.type == "hls" ?
<HlsVideo
@ -334,7 +415,27 @@ export const ReleasePlayerCustom = (props: {
></VideoJS>
}
</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" />}
</div>
<div>
{episode.selected && source.selected && voiceover.selected && (
<EpisodeSelector
availableEpisodes={episode.available}
episode={episode.selected}
@ -344,8 +445,8 @@ export const ReleasePlayerCustom = (props: {
voiceover={voiceover.selected}
token={props.token}
/>
)}
</div>
}
</Card>
);
};