diff --git a/app/api/utils.ts b/app/api/utils.ts index 51a2d1e..af3a4c3 100644 --- a/app/api/utils.ts +++ b/app/api/utils.ts @@ -27,6 +27,46 @@ export async function tryCatch( } } +export async function tryCatchPlayer( + promise: Promise +): Promise> { + try { + const res: Awaited = 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( promise: Promise ): Promise> { diff --git a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx b/app/components/ReleasePlayer/ReleasePlayerCustom.tsx index 9e89055..feec08c 100644 --- a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx +++ b/app/components/ReleasePlayer/ReleasePlayerCustom.tsx @@ -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,157 +39,207 @@ 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 + ) { + 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; - }; + 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}`; + } + } - 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}`; + 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(); - let lowQualityLink = data.links["360"][0].src; - if (lowQualityLink.includes("https://")) { - lowQualityLink = lowQualityLink.replace("https://", "//"); + if (data) { + let lowQualityLink = data.links["360"][0].src; + if (lowQualityLink.includes("https://")) { + lowQualityLink = lowQualityLink.replace("https://", "//"); + } + let manifest = `https:${lowQualityLink.replace("360.mp4:hls:", "")}`; + let poster = `https:${lowQualityLink.replace("360.mp4:hls:manifest.m3u8", "thumb001.jpg")}`; + + if (lowQualityLink.includes("animetvseries")) { + let blobTxt = "#EXTM3U\n"; + + if (data.links.hasOwnProperty("240")) { + blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=427x240,BANDWIDTH=200000\n"; + !data.links["240"][0].src.startsWith("https:") ? + (blobTxt += `https:${data.links["240"][0].src}\n`) + : (blobTxt += `${data.links["240"][0].src}\n`); + } + + if (data.links.hasOwnProperty("360")) { + blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=578x360,BANDWIDTH=400000\n"; + !data.links["360"][0].src.startsWith("https:") ? + (blobTxt += `https:${data.links["360"][0].src}\n`) + : (blobTxt += `${data.links["360"][0].src}\n`); + } + + if (data.links.hasOwnProperty("480")) { + blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=854x480,BANDWIDTH=596000\n"; + !data.links["480"][0].src.startsWith("https:") ? + (blobTxt += `https:${data.links["480"][0].src}\n`) + : (blobTxt += `${data.links["480"][0].src}\n`); + } + + if (data.links.hasOwnProperty("720")) { + 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`); + } + + let file = new File([blobTxt], "manifest.m3u8", { + type: "application/x-mpegURL", + }); + manifest = URL.createObjectURL(file); + } + return { manifest, poster }; } - let manifest = `https:${lowQualityLink.replace("360.mp4:hls:", "")}`; - let poster = `https:${lowQualityLink.replace("360.mp4:hls:manifest.m3u8", "thumb001.jpg")}`; - - if (lowQualityLink.includes("animetvseries")) { - let blobTxt = "#EXTM3U\n"; - - if (data.links.hasOwnProperty("240")) { - blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=427x240,BANDWIDTH=200000\n"; - !data.links["240"][0].src.startsWith("https:") ? - (blobTxt += `https:${data.links["240"][0].src}\n`) - : (blobTxt += `${data.links["240"][0].src}\n`); - } - - if (data.links.hasOwnProperty("360")) { - blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=578x360,BANDWIDTH=400000\n"; - !data.links["360"][0].src.startsWith("https:") ? - (blobTxt += `https:${data.links["360"][0].src}\n`) - : (blobTxt += `${data.links["360"][0].src}\n`); - } - - if (data.links.hasOwnProperty("480")) { - blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=854x480,BANDWIDTH=596000\n"; - !data.links["480"][0].src.startsWith("https:") ? - (blobTxt += `https:${data.links["480"][0].src}\n`) - : (blobTxt += `${data.links["480"][0].src}\n`); - } - - if (data.links.hasOwnProperty("720")) { - 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`); - } - - let file = new File([blobTxt], "manifest.m3u8", { - type: "application/x-mpegURL", - }); - 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 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]; - const response = await fetch(`https://api.anilibria.tv/v3/title?id=${id}`); - const data = await response.json(); - - const host = `https://${data.player.host}`; - const ep = data.player.list[episode.selected.position]; - - const blobTxt = `#EXTM3U\n${ep.hls.sd && `#EXT-X-STREAM-INF:RESOLUTION=854x480,BANDWIDTH=596000\n${host}${ep.hls.sd}\n`}${ep.hls.hd && `#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n${host}${ep.hls.hd}\n`}${ep.hls.fhd && `#EXT-X-STREAM-INF:RESOLUTION=1920x1080,BANDWIDTH=2560000\n${host}${ep.hls.fhd}\n`}`; - let file = new File([blobTxt], "manifest.m3u8", { - type: "application/x-mpegURL", - }); - let manifest = URL.createObjectURL(file); - let poster = `https://anixart.libria.fun${ep.preview}`; - return { manifest, poster }; + const blobTxt = `#EXTM3U\n${ep.hls.sd && `#EXT-X-STREAM-INF:RESOLUTION=854x480,BANDWIDTH=596000\n${host}${ep.hls.sd}\n`}${ep.hls.hd && `#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n${host}${ep.hls.hd}\n`}${ep.hls.fhd && `#EXT-X-STREAM-INF:RESOLUTION=1920x1080,BANDWIDTH=2560000\n${host}${ep.hls.fhd}\n`}`; + let file = new File([blobTxt], "manifest.m3u8", { + type: "application/x-mpegURL", + }); + 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(); - - let manifest = data.video; - let poster = data.poster; - return { manifest, poster }; + 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); - const selectedVO = - vo.types.find((voiceover: any) => voiceover.name === preferredVO) || - vo.types[0]; - setVoiceover({ - selected: selectedVO, - available: vo.types, - }); + 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]; + setVoiceover({ + selected: selectedVO, + available: vo.types, + }); + } }; __getInfo(); }, []); useEffect(() => { const __getInfo = async () => { - const src = await _fetchSource(props.id, voiceover.selected.id); - const selectedSrc = - src.sources.find((source: any) => source.name === preferredSource) || - src.sources[0]; - if (selectedSrc.episodes_count == 0) { - const remSources = src.sources.filter( - (source: any) => source.id !== selectedSrc.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]; + if (selectedSrc.episodes_count == 0) { + const remSources = src.sources.filter( + (source: any) => source.id !== selectedSrc.id + ); + setSource({ + selected: remSources[0], + available: remSources, + }); + return; + } setSource({ - selected: remSources[0], - available: remSources, + selected: selectedSrc, + available: src.sources, }); - return; } - setSource({ - selected: selectedSrc, - available: src.sources, - }); }; if (voiceover.selected) { __getInfo(); @@ -197,34 +248,38 @@ 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, + voiceover.selected.id + ); + let lastEpisodeWatched = Math.max.apply( + 0, + Object.keys( + anonEpisodesWatched[props.id][source.selected.id][ + voiceover.selected.id + ] + ) + ); + let selectedEpisode = + episodes.episodes.find( + (episode: any) => episode.position == lastEpisodeWatched + ) || episodes.episodes[0]; - let anonEpisodesWatched = getAnonEpisodesWatched( - props.id, - source.selected.id, - voiceover.selected.id - ); - let lastEpisodeWatched = Math.max.apply( - 0, - Object.keys( - anonEpisodesWatched[props.id][source.selected.id][ - voiceover.selected.id - ] - ) - ); - let selectedEpisode = - episodes.episodes.find( - (episode: any) => episode.position == lastEpisodeWatched - ) || episodes.episodes[0]; - - setEpisode({ - selected: selectedEpisode, - available: episodes.episodes, - }); + setEpisode({ + 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 ); - SetPlayerProps({ - src: manifest, - poster: poster, - useCustom: true, - type: "hls", - }); + 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 ); - SetPlayerProps({ - src: manifest, - poster: poster, - useCustom: true, - type: "hls", - }); + 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 ); - SetPlayerProps({ - src: manifest, - poster: poster, - useCustom: true, - type: "mp4", - }); + 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 ( - {( - !voiceover.selected || - !source.selected || - !episode.selected || - !playerProps.src - ) ? -
- -
- :
-
- - -
- {playerProps.useCustom ? +
+ {voiceover.selected && ( + + )} + {source.selected && ( + + )} +
+ +
+ {isLoading ? + !playerError ? + + :
+

Ошибка: {playerError.message}

+ {!isErrorDetailsOpen ? + + :

+ {playerError.detail} +

+ } +
+ + : playerProps.useCustom ? + !playerError ? {playerProps.type == "hls" ? } - :