From d47a013dd7be43ea2ed47202dc500440f36f9522 Mon Sep 17 00:00:00 2001 From: Tejas Panchal Date: Fri, 20 Feb 2026 01:32:39 +0530 Subject: [PATCH] resolved servers --- .env.example | 14 +- src/components/player/Player.jsx | 291 +++++++++++++++++++++++-------- src/hooks/useWatch.js | 107 ++++++++---- src/pages/watch/Watch.jsx | 74 ++++---- src/utils/title.utils.js | 14 ++ 5 files changed, 337 insertions(+), 163 deletions(-) create mode 100644 src/utils/title.utils.js diff --git a/.env.example b/.env.example index 995c7d7..610508f 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,3 @@ -#Refer https://github.com/itzzzme/anime-api to host your backend API VITE_API_URL=/api - -#Refer this gist to setup proxy server https://gist.github.com/itzzzme/180813be2c7b45eedc8ce8344c8dea3b -VITE_PROXY_URL=/?url= - -#Refer https://github.com/itzzzme/m3u8proxy to host you m3u8 proxy server though it's optional but if you don't set it up you may get CORS error for some servers if you set up from the given repo then only the url structure will look like this VITE_M3U8_PROXY_URL=/m3u8-proxy?url= - -#totaly optional / if you don't want to setup worker just change the code of getQtip.utils.js following the pattern of any other utils file -VITE_WORKER_URL=https://worker1.workers.dev,https://worker2.workers.dev,https://worker3.workers.dev,... - -VITE_BASE_IFRAME_URL=https://megaplay.buzz/stream/s-2 - -VITE_BASE_IFRAME_URL_2=https://vidwish.live/stream/s-2 +VITE_HD_1_PROXY_URL=/m3u8-proxy?url= diff --git a/src/components/player/Player.jsx b/src/components/player/Player.jsx index 844edd4..e469669 100644 --- a/src/components/player/Player.jsx +++ b/src/components/player/Player.jsx @@ -1,3 +1,5 @@ +/* eslint-disable no-empty */ +/* eslint-disable no-unused-vars */ /* eslint-disable react/prop-types */ import Hls from "hls.js"; import { useEffect, useRef, useState } from "react"; @@ -28,20 +30,22 @@ import website_name from "@/src/config/website"; import getChapterStyles from "./getChapterStyle"; import artplayerPluginHlsControl from "artplayer-plugin-hls-control"; import artplayerPluginUploadSubtitle from "./artplayerPluginUploadSubtitle"; +import { getTitle } from "@/src/utils/title.utils"; Artplayer.LOG_VERSION = false; Artplayer.CONTEXTMENU = false; const KEY_CODES = { - M: "m", - I: "i", - F: "f", - V: "v", - SPACE: " ", - ARROW_UP: "arrowup", - ARROW_DOWN: "arrowdown", - ARROW_RIGHT: "arrowright", - ARROW_LEFT: "arrowleft", + M: "KeyM", + I: "KeyI", + F: "KeyF", + V: "KeyV", + SPACE: "Space", + SPACE_LEGACY: "Spacebar", + ARROW_UP: "ArrowUp", + ARROW_DOWN: "ArrowDown", + ARROW_RIGHT: "ArrowRight", + ARROW_LEFT: "ArrowLeft", }; export default function Player({ @@ -59,17 +63,15 @@ export default function Player({ animeInfo, episodeNum, streamInfo, - serverName, + activeServerName, }) { const artRef = useRef(null); const leftAtRef = useRef(0); + const boundKeydownRef = useRef(null); const proxy = import.meta.env.VITE_PROXY_URL; const m3u8proxy = import.meta.env.VITE_M3U8_PROXY_URL?.split(",") || []; - const m3u8proxyHD3 = import.meta.env.VITE_M3U8_PROXY_HD3; const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState( - episodes?.findIndex( - (episode) => episode.id.match(/ep=(\d+)/)?.[1] === episodeId - ) + episodes?.findIndex((episode) => episode.id.match(/ep=(\d+)/)?.[1] === episodeId) ); useEffect(() => { @@ -80,11 +82,10 @@ export default function Player({ setCurrentEpisodeIndex(newIndex); } }, [episodeId, episodes]); + useEffect(() => { const applyChapterStyles = () => { - const existingStyles = document.querySelectorAll( - "style[data-chapter-styles]" - ); + const existingStyles = document.querySelectorAll("style[data-chapter-styles]"); existingStyles.forEach((style) => style.remove()); const styleElement = document.createElement("style"); styleElement.setAttribute("data-chapter-styles", "true"); @@ -112,18 +113,13 @@ export default function Player({ art.on("destroy", () => hls.destroy()); - // hls.on(Hls.Events.ERROR, (event, data) => { - // console.error("HLS.js error:", data); - // }); video.addEventListener("timeupdate", () => { const currentTime = Math.round(video.currentTime); const duration = Math.round(video.duration); if (duration > 0 && currentTime >= duration) { art.pause(); if (currentEpisodeIndex < episodes?.length - 1 && autoNext) { - playNext( - episodes[currentEpisodeIndex + 1].id.match(/ep=(\d+)/)?.[1] - ); + playNext(episodes[currentEpisodeIndex + 1].id.match(/ep=(\d+)/)?.[1]); } } }); @@ -135,9 +131,7 @@ export default function Player({ if (duration > 0 && currentTime >= duration) { art.pause(); if (currentEpisodeIndex < episodes?.length - 1 && autoNext) { - playNext( - episodes[currentEpisodeIndex + 1].id.match(/ep=(\d+)/)?.[1] - ); + playNext(episodes[currentEpisodeIndex + 1].id.match(/ep=(\d+)/)?.[1]); } } }); @@ -157,29 +151,76 @@ export default function Player({ return chapters; }; + const isEditableElement = (el) => { + if (!el) return false; + const tagName = el.tagName?.toLowerCase(); + if (tagName === "input" || tagName === "textarea" || el.isContentEditable) return true; + if (el.closest) { + const editable = el.closest("input, textarea, [contenteditable='true']"); + return !!editable; + } + return false; + }; + const handleKeydown = (event, art) => { - const tagName = event.target.tagName.toLowerCase(); + const container = artRef.current; + if (!container || !art) return; - if (tagName === "input" || tagName === "textarea") return; + const target = event.target; + if (isEditableElement(target)) return; - switch (event.key.toLowerCase()) { + const eventIsInsidePlayer = + container.contains(target) || container.contains(document.activeElement); + + if (!eventIsInsidePlayer) return; + + const code = event.code; + + switch (code) { case KEY_CODES.M: art.muted = !art.muted; break; case KEY_CODES.I: art.pip = !art.pip; break; - case KEY_CODES.F: + case KEY_CODES.F: { event.preventDefault(); event.stopPropagation(); - art.fullscreen = !art.fullscreen; + + const container = artRef.current; + const doc = document; + const fsEl = + doc.fullscreenElement || + doc.webkitFullscreenElement || + doc.mozFullScreenElement || + doc.msFullscreenElement; + + if (fsEl && (fsEl === container || container.contains(fsEl))) { + if (doc.exitFullscreen) doc.exitFullscreen(); + else if (doc.webkitExitFullscreen) doc.webkitExitFullscreen(); + else if (doc.mozCancelFullScreen) doc.mozCancelFullScreen(); + else if (doc.msExitFullscreen) doc.msExitFullscreen(); + } else { + if (container.requestFullscreen) container.requestFullscreen(); + else if (container.webkitRequestFullscreen) container.webkitRequestFullscreen(); + else if (container.mozRequestFullScreen) container.mozRequestFullScreen(); + else if (container.msRequestFullscreen) container.msRequestFullscreen(); + } + + try { + art.fullscreen = !art.fullscreen; + } catch (e) { + // ignore if art not available + } break; + } case KEY_CODES.V: event.preventDefault(); event.stopPropagation(); art.subtitle.show = !art.subtitle.show; break; case KEY_CODES.SPACE: + case KEY_CODES.SPACE_LEGACY: event.preventDefault(); event.stopPropagation(); art.playing ? art.pause() : art.play(); @@ -211,20 +252,34 @@ export default function Player({ useEffect(() => { if (!streamUrl || !artRef.current) return; + const iframeUrl = streamInfo?.streamingLink?.iframe; - const headers = {}; - headers.referer = new URL(iframeUrl).origin + "/"; - const finalProxy = (serverName === "hd-3" && m3u8proxyHD3) - ? m3u8proxyHD3 + const headers = { + referer: iframeUrl ? new URL(iframeUrl).origin + "/" : window.location.origin + "/", + }; + + const container = artRef.current; + let fullscreenRefocusTimeout = null; + + try { + if (!container.hasAttribute("tabindex")) container.setAttribute("tabindex", "0"); + else { + const current = parseInt(container.getAttribute("tabindex"), 10); + if (isNaN(current) || current < 0) container.setAttribute("tabindex", "0"); + } + container.style.outline = "none"; + } catch (e) { + // ignore + } + + const hd1Proxy = import.meta.env.VITE_HD_1_PROXY_URL; + const currentProxy = (activeServerName === "HD-1" && hd1Proxy) + ? hd1Proxy : m3u8proxy[Math.floor(Math.random() * m3u8proxy?.length)]; - console.log(finalProxy + - encodeURIComponent(streamUrl) + - "&headers=" + - encodeURIComponent(JSON.stringify(headers))); const art = new Artplayer({ url: - finalProxy + + currentProxy + encodeURIComponent(streamUrl) + "&headers=" + encodeURIComponent(JSON.stringify(headers)), @@ -245,8 +300,8 @@ export default function Player({ fastForward: true, aspectRatio: true, moreVideoAttr: { - crossOrigin: 'anonymous', - preload: 'none', + crossOrigin: "anonymous", + preload: "none", playsInline: true, }, plugins: [ @@ -374,13 +429,55 @@ export default function Player({ }, customType: { m3u8: playM3u8 }, }); + art.on("resize", () => { art.subtitle.style({ - fontSize: - (art.width > 500 ? art.width * 0.02 : art.width * 0.03) + "px", + fontSize: (art.width > 500 ? art.width * 0.02 : art.width * 0.03) + "px", }); }); + + const refocusIfNeeded = (delay = 30) => { + try { + if (!container) return; + const active = document.activeElement; + if (!container.contains(active)) { + fullscreenRefocusTimeout = setTimeout(() => { + try { + container.focus(); + } catch (e) { + // ignore + } + }, delay); + } + } catch (e) { + // ignore + } + }; + + const onFullscreenChange = () => { + if (!document.fullscreenElement && !document.webkitIsFullScreen && !document.mozFullScreen && !document.msFullscreenElement) { + refocusIfNeeded(40); + } else { + refocusIfNeeded(20); + } + }; + + // vendor event wrappers + const fullscreenEvents = [ + "fullscreenchange", + "webkitfullscreenchange", + "mozfullscreenchange", + "MSFullscreenChange", + ]; + fullscreenEvents.forEach((ev) => document.addEventListener(ev, onFullscreenChange)); + art.on("ready", () => { + try { + container.focus(); + } catch (e) { + // ignore + } + const continueWatchingList = JSON.parse(localStorage.getItem("continueWatching")) || []; const currentEntry = continueWatchingList.find((item) => item.episodeId === episodeId); if (currentEntry?.leftAt) art.currentTime = currentEntry.leftAt; @@ -393,7 +490,15 @@ export default function Player({ art.layers[website_name].style.opacity = 0; }, 2000); - const defaultSubtitle = subtitles?.find((sub) => sub.label.toLowerCase() === "english"); + const subs = (subtitles || []).map((s) => ({ ...s })); + + for (const sub of subs) { + const encodedUrl = encodeURIComponent(sub.file); + const encodedHeaders = encodeURIComponent(JSON.stringify(headers)); + sub.file = `${proxy}${encodedUrl}&headers=${encodedHeaders}`; + } + + const defaultSubtitle = subs?.find((sub) => sub.label.toLowerCase() === "english"); if (defaultSubtitle) { art.subtitle.switch(defaultSubtitle.file, { name: defaultSubtitle.label, @@ -402,12 +507,38 @@ export default function Player({ } const skipRanges = [ - ...(intro.start != null && intro.end != null ? [[intro.start + 1, intro.end - 1]] : []), - ...(outro.start != null && outro.end != null ? [[outro.start + 1, outro.end]] : []), + ...(intro?.start != null && intro?.end != null ? [[intro.start + 1, intro.end - 1]] : []), + ...(outro?.start != null && outro?.end != null ? [[outro.start + 1, outro.end]] : []), ]; autoSkipIntro && art.plugins.add(autoSkip(skipRanges)); - document.addEventListener("keydown", (event) => handleKeydown(event, art)); + const boundKeydown = (event) => handleKeydown(event, art); + boundKeydownRef.current = boundKeydown; + document.addEventListener("keydown", boundKeydown); + + const focusOnPointerDown = () => { + try { + container.focus(); + } catch (err) { + // ignore + } + }; + container.addEventListener("pointerdown", focusOnPointerDown, { passive: true }); + + const onWindowFocus = () => refocusIfNeeded(30); + window.addEventListener("focus", onWindowFocus); + + art.on("destroy", () => { + try { + document.removeEventListener("keydown", boundKeydown); + } catch (e) { } + try { + container.removeEventListener("pointerdown", focusOnPointerDown); + } catch (e) { } + try { + window.removeEventListener("focus", onWindowFocus); + } catch (e) { } + }); art.subtitle.style({ fontSize: (art.width > 500 ? art.width * 0.02 : art.width * 0.03) + "px", @@ -420,6 +551,7 @@ export default function Player({ }) ); } + const $rewind = art.layers["rewind"]; const $forward = art.layers["forward"]; Artplayer.utils.isMobile && @@ -438,10 +570,11 @@ export default function Player({ art.layers["forwardIcon"].style.opacity = 0; }, 300); }); - if (subtitles?.length > 0) { + + if (subs?.length > 0) { const defaultEnglishSub = - subtitles.find((sub) => sub.label.toLowerCase() === "english" && sub.default) || - subtitles.find((sub) => sub.label.toLowerCase() === "english"); + subs.find((sub) => sub.label.toLowerCase() === "english" && sub.default) || + subs.find((sub) => sub.label.toLowerCase() === "english"); art.setting.add({ name: "captions", @@ -459,7 +592,7 @@ export default function Player({ return !item.switch; }, }, - ...subtitles.map((sub) => ({ + ...subs.map((sub) => ({ default: sub.label.toLowerCase() === "english" && sub === defaultEnglishSub, html: sub.label, url: sub.file, @@ -477,32 +610,42 @@ export default function Player({ if (art && art.destroy) { art.destroy(false); } - document.removeEventListener("keydown", handleKeydown); - const continueWatching = JSON.parse(localStorage.getItem("continueWatching")) || []; - const newEntry = { - id: animeInfo?.id, - data_id: animeInfo?.data_id, - episodeId, - episodeNum, - adultContent: animeInfo?.adultContent, - poster: animeInfo?.poster, - title: animeInfo?.title, - japanese_title: animeInfo?.japanese_title, - leftAt: leftAtRef.current, - }; - if (!newEntry.data_id) return; - - const existingIndex = continueWatching.findIndex((item) => item.data_id === newEntry.data_id); - if (existingIndex !== -1) { - continueWatching[existingIndex] = newEntry; - } else { - continueWatching.push(newEntry); + fullscreenEvents.forEach((ev) => document.removeEventListener(ev, onFullscreenChange)); + if (boundKeydownRef.current) { + try { + document.removeEventListener("keydown", boundKeydownRef.current); + } catch (e) { } + boundKeydownRef.current = null; + } + if (fullscreenRefocusTimeout) clearTimeout(fullscreenRefocusTimeout); + + try { + const continueWatching = JSON.parse(localStorage.getItem("continueWatching")) || []; + const newEntry = { + id: animeInfo?.id, + data_id: animeInfo?.data_id, + episodeId, + episodeNum, + adultContent: animeInfo?.adultContent, + poster: animeInfo?.poster, + title: getTitle(animeInfo, "EN"), + japanese_title: getTitle(animeInfo, "JP"), + leftAt: leftAtRef.current, + updatedAt: Date.now(), + }; + + if (!newEntry.data_id) return; + + const filtered = continueWatching.filter((item) => item.data_id !== newEntry.data_id); + filtered.unshift(newEntry); + localStorage.setItem("continueWatching", JSON.stringify(filtered)); + } catch (err) { + console.error("Failed to save continueWatching:", err); } - localStorage.setItem("continueWatching", JSON.stringify(continueWatching)); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [streamUrl, subtitles, intro, outro]); - return
; + return
; } \ No newline at end of file diff --git a/src/hooks/useWatch.js b/src/hooks/useWatch.js index 2058f5b..a333758 100644 --- a/src/hooks/useWatch.js +++ b/src/hooks/useWatch.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ /* eslint-disable react-hooks/exhaustive-deps */ import { useState, useEffect, useRef } from "react"; import getAnimeInfo from "@/src/utils/getAnimeInfo.utils"; @@ -113,58 +114,91 @@ export const useWatch = (animeId, initialEpisodeId) => { useEffect(() => { if (!episodeId || !episodes || isServerFetchInProgress.current) return; + let mounted = true; + const controller = new AbortController(); + isServerFetchInProgress.current = true; + setServerLoading(true); + const fetchServers = async () => { - isServerFetchInProgress.current = true; - setServerLoading(true); try { - const data = await getServers(animeId, episodeId); - console.log(data); + const data = await getServers(animeId, episodeId, { signal: controller.signal }); + if (!mounted) return; const filteredServers = data?.filter( (server) => server.serverName === "HD-1" || server.serverName === "HD-2" || - server.serverName === "HD-3" - ); - if (filteredServers.some((s) => s.type === "sub")) { - filteredServers.push({ - type: "sub", - data_id: "69696969", - server_id: "41", - serverName: "HD-4", - }); + // server.serverName === "HD-3" || + server.serverName === "Vidstreaming" || + server.serverName === "Vidcloud" || + server.serverName === "DouVideo" + ) || []; + + let serversList = [...filteredServers]; + + if (serversList.some((s) => s.type === "sub")) { + if (!serversList.some((s) => s.serverName === "HD-4" && s.type === "sub")) { + serversList.push({ + type: "sub", + data_id: "69696968", + server_id: "41", + serverName: "HD-4", + }); + } } - if (filteredServers.some((s) => s.type === "dub")) { - filteredServers.push({ - type: "dub", - data_id: "96969696", - server_id: "42", - serverName: "HD-4", - }); + + if (serversList.some((s) => s.type === "dub")) { + if (!serversList.some((s) => s.serverName === "HD-4" && s.type === "dub")) { + serversList.push({ + type: "dub", + data_id: "96969696", + server_id: "42", + serverName: "HD-4", + }); + } } + const savedServerName = localStorage.getItem("server_name"); const savedServerType = localStorage.getItem("server_type"); - const initialServer = - filteredServers.find(s => s.serverName === savedServerName && s.type === savedServerType) || - filteredServers.find(s => s.serverName === savedServerName) || - filteredServers.find(s => s.type === savedServerType && ["HD-1", "HD-2", "HD-3", "HD-4"].includes(s.serverName)) || - filteredServers.find(s => s.serverName === "HD-2") || - filteredServers[0]; - setServers(filteredServers); + const initialServer = + serversList.find(s => s.serverName === savedServerName && s.type === savedServerType) || + serversList.find(s => s.serverName === savedServerName) || + serversList.find(s => s.serverName === "HD-2") || + serversList.find( + s => + s.type === savedServerType && + ["HD-1", "HD-2", "HD-3", "HD-4", "Vidstreaming", "Vidcloud", "DouVideo"].includes(s.serverName) + ) || + serversList[0]; + + setServers(serversList); setActiveServerType(initialServer?.type); setActiveServerName(initialServer?.serverName); setActiveServerId(initialServer?.data_id); - } catch (error) { - console.error("Error fetching servers:", error); - setError(error.message || "An error occurred."); + } catch (err) { + if (err?.name === "AbortError") return; + console.error("Error fetching servers:", err); + if (mounted) setError(err.message || "An error occurred."); } finally { - setServerLoading(false); - isServerFetchInProgress.current = false; + if (mounted) { + setServerLoading(false); + isServerFetchInProgress.current = false; + } } }; + fetchServers(); + + return () => { + mounted = false; + try { controller.abort(); } catch (e) { + // console.log(e.message); + } + isServerFetchInProgress.current = false; + }; }, [episodeId, episodes]); + // Fetch stream info only when episodeId, activeServerId, and servers are ready useEffect(() => { if ( @@ -175,11 +209,10 @@ export const useWatch = (animeId, initialEpisodeId) => { isStreamFetchInProgress.current ) return; - if ( - (activeServerName?.toLowerCase() === "hd-1" || activeServerName?.toLowerCase() === "hd-4") - && - !serverLoading - ) { + const iframeServers = []; + // const iframeServers = ["hd-1", "hd-4", "vidstreaming", "vidcloud", "douvideo"]; + + if (iframeServers.includes(activeServerName?.toLowerCase()) && !serverLoading) { setBuffering(false); return; } diff --git a/src/pages/watch/Watch.jsx b/src/pages/watch/Watch.jsx index 3af6777..8953387 100644 --- a/src/pages/watch/Watch.jsx +++ b/src/pages/watch/Watch.jsx @@ -4,7 +4,6 @@ import { useLocation, useParams, Link, useNavigate } from "react-router-dom"; import { useLanguage } from "@/src/context/LanguageContext"; import { useWatch } from "@/src/hooks/useWatch"; import BouncingLoader from "@/src/components/ui/bouncingloader/Bouncingloader"; -import IframePlayer from "@/src/components/player/IframePlayer"; import Episodelist from "@/src/components/episodelist/Episodelist"; import website_name from "@/src/config/website"; import Sidecard from "@/src/components/sidecard/Sidecard"; @@ -200,48 +199,45 @@ export default function Watch() {
{!buffering ? ( - ["hd-1", "hd-4"].includes(activeServerName.toLowerCase()) ? ( - - ) : ( - - ) + ) : ( -
+
)} - {!buffering && !activeServerType && ( -
-

- Streaming server seems to be down. Please try another server or reload the page. -

-
- )} +

+ {!buffering && !streamInfo ? ( + servers ? ( + <> + Probably this server is down, try other servers +
+ Either reload or try again after sometime + + ) : ( + <> + Probably streaming server is down +
+ Either reload or try again after sometime + + ) + ) : null} +

diff --git a/src/utils/title.utils.js b/src/utils/title.utils.js new file mode 100644 index 0000000..007f3bd --- /dev/null +++ b/src/utils/title.utils.js @@ -0,0 +1,14 @@ +export const getTitle = (item, language) => { + if (!item) return "N/A"; + + const title = item.title; + const japanese_title = item.japanese_title || item.japaneseTitle; + + if (language === "EN") { + if (typeof title === "string") return title; + return title?.english || title?.romaji || "N/A"; + } else { + // JP + return japanese_title || (typeof title === "string" ? title : title?.romaji || title?.english || "N/A"); + } +};