Merge pull request #6 from tejaspanchall/dev

resolved servers
This commit is contained in:
Tejas Panchal
2026-02-20 01:41:28 +05:30
committed by GitHub
5 changed files with 337 additions and 163 deletions

View File

@@ -1,15 +1,3 @@
#Refer https://github.com/itzzzme/anime-api to host your backend API
VITE_API_URL=<your_hosted_api>/api
#Refer this gist to setup proxy server https://gist.github.com/itzzzme/180813be2c7b45eedc8ce8344c8dea3b
VITE_PROXY_URL=<proxy_server_name>/?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_server_name>/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=<hd_1_anime_paradise>/m3u8-proxy?url=

View File

@@ -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 <div ref={artRef} className="w-full h-full"></div>;
return <div ref={artRef} className="w-full h-full" />;
}

View File

@@ -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;
}

View File

@@ -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() {
<div ref={playerRef} className="player w-full h-fit bg-black flex flex-col rounded-xl overflow-hidden shadow-2xl">
<div ref={videoContainerRef} className="w-full relative aspect-video bg-black">
{!buffering ? (
["hd-1", "hd-4"].includes(activeServerName.toLowerCase()) ? (
<IframePlayer
episodeId={episodeId}
servertype={activeServerType}
serverName={activeServerName}
animeInfo={animeInfo}
episodeNum={activeEpisodeNum}
episodes={episodes}
playNext={setEpisodeId}
autoNext={autoNext}
/>
) : (
<Player
streamUrl={streamUrl}
subtitles={subtitles}
intro={intro}
outro={outro}
serverName={activeServerName.toLowerCase()}
thumbnail={thumbnail}
autoSkipIntro={autoSkipIntro}
autoPlay={autoPlay}
autoNext={autoNext}
episodeId={episodeId}
episodes={episodes}
playNext={setEpisodeId}
animeInfo={animeInfo}
episodeNum={activeEpisodeNum}
streamInfo={streamInfo}
/>
)
<Player
streamUrl={streamUrl}
subtitles={subtitles}
intro={intro}
outro={outro}
activeServerName={activeServerName}
thumbnail={thumbnail}
autoSkipIntro={autoSkipIntro}
autoPlay={autoPlay}
autoNext={autoNext}
episodeId={episodeId}
episodes={episodes}
playNext={setEpisodeId}
animeInfo={animeInfo}
episodeNum={activeEpisodeNum}
streamInfo={streamInfo}
/>
) : (
<div className="absolute inset-0 flex justify-center items-center bg-black">
<div className="absolute inset-0 flex justify-center items-center bg-black bg-opacity-50">
<BouncingLoader />
</div>
)}
{!buffering && !activeServerType && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50 backdrop-blur-sm pointer-events-none">
<p className="text-center text-gray-300 font-medium p-4 max-w-sm">
Streaming server seems to be down. Please try another server or reload the page.
</p>
</div>
)}
<p className="text-center underline font-medium text-[15px] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none">
{!buffering && !streamInfo ? (
servers ? (
<>
Probably this server is down, try other servers
<br />
Either reload or try again after sometime
</>
) : (
<>
Probably streaming server is down
<br />
Either reload or try again after sometime
</>
)
) : null}
</p>
</div>
<div className="bg-[#121212]">

14
src/utils/title.utils.js Normal file
View File

@@ -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");
}
};