diff --git a/.dockerignore b/.dockerignore index 2eff99b..a9d7db9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -64,5 +64,6 @@ API-Trace/* .env player-parsers +api-prox docs .git \ No newline at end of file diff --git a/.env.sample b/.env.sample index dcb7e8b..a775549 100644 --- a/.env.sample +++ b/.env.sample @@ -1,5 +1,3 @@ # пример заполнения: https://example.com, http://0.0.0.0:80 -NEXT_PUBLIC_KODIK_PARSER_URL= # Домен парсера кодика, требуется для просмотра с данного источника -NEXT_PUBLIC_ANILIBRIA_PARSER_URL= # Домен парсера анилибрии, если не заполнено, используется официальное апи -NEXT_PUBLIC_SIBNET_PARSER_URL= # Домен парсера сибнет, требуется для просмотра с данного источника +NEXT_PUBLIC_PLAYER_PARSER_URL= # Домен сервиса player-parsers, требуется для работы встроенного плеера # --- \ No newline at end of file diff --git a/DEPLOYMENT.RU.md b/DEPLOYMENT.RU.md index 78f43a3..6605fc7 100644 --- a/DEPLOYMENT.RU.md +++ b/DEPLOYMENT.RU.md @@ -26,11 +26,9 @@ ![vercel import button](./docs/deploy/vercel_import.png) -5. (опционально) добавьте переменные для использования своего плеера: +5. (опционально) добавьте переменную для использования своего плеера: - - NEXT_PUBLIC_KODIK_PARSER_URL - - NEXT_PUBLIC_ANILIBRIA_PARSER_URL - - NEXT_PUBLIC_SIBNET_PARSER_URL + - NEXT_PUBLIC_PLAYER_PARSER_URL на те которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md) @@ -75,11 +73,9 @@ ![netlify project name](./docs/deploy/netlify_project_name.png) -7. (опционально) добавьте переменные для использования своего плеера: +7. (опционально) добавьте переменную для использования своего плеера: - - NEXT_PUBLIC_KODIK_PARSER_URL - - NEXT_PUBLIC_ANILIBRIA_PARSER_URL - - NEXT_PUBLIC_SIBNET_PARSER_URL + - NEXT_PUBLIC_PLAYER_PARSER_URL на те которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md) @@ -124,7 +120,7 @@ - -p - порт контейнера который будет доступен извне. ПОРТ:3000 > [!NOTE] -> для переменных которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md), необходимо использовать `-e ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ` до слова anix +> для переменных которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md), необходимо использовать `-e ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ` до последнего слова anix [команда docker run](https://docs.docker.com/reference/cli/docker/container/run/) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index f7ae21b..2b5dce6 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -26,11 +26,9 @@ Requirements: ![vercel import button](./docs/deploy/vercel_import.png) -5. (optional) Add variables to use your own player: +5. (optional) Add variable to use your own player: - - NEXT_PUBLIC_KODIK_PARSER_URL - - NEXT_PUBLIC_ANILIBRIA_PARSER_URL - - NEXT_PUBLIC_SIBNET_PARSER_URL + - NEXT_PUBLIC_PLAYER_PARSER_URL Use the ones you received if you deployed [anix-player-parsers](./player-parsers/README.md) @@ -77,9 +75,7 @@ Requirements: 7. (optional) Add variables to use your own player: - - NEXT_PUBLIC_KODIK_PARSER_URL - - NEXT_PUBLIC_ANILIBRIA_PARSER_URL - - NEXT_PUBLIC_SIBNET_PARSER_URL + - NEXT_PUBLIC_PLAYER_PARSER_URL Use the ones you received if you deployed [anix-player-parsers](./player-parsers/README.md) @@ -124,7 +120,7 @@ Additional Requirements: - -p - container port to be exposed externally. PORT:3000 > [!NOTE] -> For variables you received if you deployed [anix-player-parsers](./player-parsers/README.md), you need to use `-e VARIABLE=VALUE` before the word anix +> For variables you received if you deployed [anix-player-parsers](./player-parsers/README.md), you need to use `-e VARIABLE=VALUE` before the last word anix [docker run command](https://docs.docker.com/reference/cli/docker/container/run/) diff --git a/app/api/config.ts b/app/api/config.ts index ec8fffd..ad32b6f 100644 --- a/app/api/config.ts +++ b/app/api/config.ts @@ -1,9 +1,9 @@ export const CURRENT_APP_VERSION = "3.7.0"; -export const API_URL = "https://api.anixart.tv"; +export const API_URL = "https://api.anixart.app"; export const API_PREFIX = "/api/proxy"; export const USER_AGENT = - "AnixartApp/8.2.1-23121216 (Android 9; SDK 28; arm64-v8a; samsung SM-G975N; en)"; + "AnixartApp/9.0 BETA 5-25062213 (Android 9; SDK 28; arm64-v8a; samsung SM-G975N; en)"; export const ENDPOINTS = { release: { diff --git a/app/components/ReleasePlayer/PlayerParsing.ts b/app/components/ReleasePlayer/PlayerParsing.ts index bd85ff4..c5cf87b 100644 --- a/app/components/ReleasePlayer/PlayerParsing.ts +++ b/app/components/ReleasePlayer/PlayerParsing.ts @@ -62,148 +62,35 @@ export async function _fetchPlayer( return data; } -function decryptKodikLink(enc: string) { - const decryptedBase64 = enc.replace(/[a-zA-Z]/g, (e: any) => { - return String.fromCharCode( - (e <= "Z" ? 90 : 122) >= (e = e.charCodeAt(0) + 18) ? e : e - 26 - ); - }); - return atob(decryptedBase64); -} export const _fetchKodikManifest = async ( url: string, setPlayerError: (state) => void ) => { - // Fetch episode links via edge function - const NEXT_PUBLIC_KODIK_PARSER_URL = env("NEXT_PUBLIC_KODIK_PARSER_URL") - if (!NEXT_PUBLIC_KODIK_PARSER_URL) { + const NEXT_PUBLIC_PLAYER_PARSER_URL = env("NEXT_PUBLIC_PLAYER_PARSER_URL") + if (!NEXT_PUBLIC_PLAYER_PARSER_URL) { setPlayerError({ - message: "Источник не настроен", - detail: "переменная 'NEXT_PUBLIC_KODIK_PARSER_URL' не обнаружена", + message: "Плеер не настроен", + detail: "переменная 'NEXT_PUBLIC_PLAYER_PARSER_URL' не обнаружена", }); return { manifest: null, poster: null }; } const data = await _fetchPlayer( - `${NEXT_PUBLIC_KODIK_PARSER_URL}/?url=${url}&player=kodik`, + `${NEXT_PUBLIC_PLAYER_PARSER_URL}/?url=${url}&player=kodik`, setPlayerError ); + if (data) { - let lowQualityLink = data.links["360"][0].src; // we assume that 360p is always present - - if (!lowQualityLink.includes("//")) { - // check if link is encrypted, else do nothing - lowQualityLink = decryptKodikLink(lowQualityLink); + let manifest: string = data.manifest + if (!manifest.startsWith("http")) { + let file = new File([manifest], "manifest.m3u8", { + type: "application/x-mpegURL", + }); + manifest = URL.createObjectURL(file); + } + return { manifest, poster: data.poster }; } - - if (lowQualityLink.includes("https://")) { - // strip the https prefix, since we add it manually - 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") || - lowQualityLink.includes("tvseries") - ) { - // if link includes "animetvseries" or "tvseries" we need to construct manifest ourselves - let blobTxt = "#EXTM3U\n"; - - if (data.links.hasOwnProperty("240")) { - blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=427x240,BANDWIDTH=200000\n"; - let link = data.links["240"][0].src; - let dec = null; - link.includes("//") ? - link.startsWith("https:") ? - (blobTxt += `${link}\n`) - : (blobTxt += `https:${link}\n`) - : (dec = decryptKodikLink(link)); - - dec ? - dec.startsWith("https:") ? - (blobTxt += `${dec}\n`) - : (blobTxt += `https:${dec}\n`) - : null; - } - - if (data.links.hasOwnProperty("360")) { - blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=578x360,BANDWIDTH=400000\n"; - let link = data.links["360"][0].src; - let dec = null; - link.includes("//") ? - link.startsWith("https:") ? - (blobTxt += `${link}\n`) - : (blobTxt += `https:${link}\n`) - : (dec = decryptKodikLink(link)); - - dec ? - dec.startsWith("https:") ? - (blobTxt += `${dec}\n`) - : (blobTxt += `https:${dec}\n`) - : null; - } - - if (data.links.hasOwnProperty("480")) { - blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=854x480,BANDWIDTH=596000\n"; - let link = data.links["480"][0].src; - let dec = null; - link.includes("//") ? - link.startsWith("https:") ? - (blobTxt += `${link}\n`) - : (blobTxt += `https:${link}\n`) - : (dec = decryptKodikLink(link)); - - dec ? - dec.startsWith("https:") ? - (blobTxt += `${dec}\n`) - : (blobTxt += `https:${dec}\n`) - : null; - } - - if (data.links.hasOwnProperty("720")) { - blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n"; - let link = data.links["720"][0].src; - let dec = null; - link.includes("//") ? - link.startsWith("https:") ? - (blobTxt += `${link}\n`) - : (blobTxt += `https:${link}\n`) - : (dec = decryptKodikLink(link)); - - dec ? - dec.startsWith("https:") ? - (blobTxt += `${dec}\n`) - : (blobTxt += `https:${dec}\n`) - : null; - } - - if (data.links.hasOwnProperty("1080")) { - blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=1920x1080,BANDWIDTH=2560000\n"; - let link = data.links["1080"][0].src; - let dec = null; - link.includes("//") ? - link.startsWith("https:") ? - (blobTxt += `${link}\n`) - : (blobTxt += `https:${link}\n`) - : (dec = decryptKodikLink(link)); - - dec ? - dec.startsWith("https:") ? - (blobTxt += `${dec}\n`) - : (blobTxt += `https:${dec}\n`) - : null; - } - - let file = new File([blobTxt], "manifest.m3u8", { - type: "application/x-mpegURL", - }); - manifest = URL.createObjectURL(file); - } - return { manifest, poster }; - } return { manifest: null, poster: null }; }; @@ -211,32 +98,26 @@ export const _fetchAnilibriaManifest = async ( url: string, setPlayerError: (state) => void ) => { - const id = url.split("?id=")[1].split("&ep=")[0]; - const epid = url.split("?id=")[1].split("&ep=")[1]; - const _url = `https://api.anilibria.tv/v3/title?id=${id}`; - let data = null; - const NEXT_PUBLIC_ANILIBRIA_PARSER_URL = env("NEXT_PUBLIC_ANILIBRIA_PARSER_URL") - if (NEXT_PUBLIC_ANILIBRIA_PARSER_URL) { - data = await _fetchPlayer( - `${NEXT_PUBLIC_ANILIBRIA_PARSER_URL}/?url=${_url}&player=libria`, - setPlayerError - ); - } else { - data = await _fetchPlayer(_url, setPlayerError); + const NEXT_PUBLIC_PLAYER_PARSER_URL = env("NEXT_PUBLIC_PLAYER_PARSER_URL") + if (!NEXT_PUBLIC_PLAYER_PARSER_URL) { + setPlayerError({ + message: "Плеер не настроен", + detail: "переменная 'NEXT_PUBLIC_PLAYER_PARSER_URL' не обнаружена", + }); + return { manifest: null, poster: null }; } - if (data) { - const host = `https://${data.player.host}`; - const ep = data.player.list[epid]; + const data = await _fetchPlayer( + `${NEXT_PUBLIC_PLAYER_PARSER_URL}/?url=${encodeURIComponent(url)}&player=libria`, + setPlayerError + ); - // we need to manually construct a manifest file for a hls player - 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", { + if (data) { + let file = new File([data.manifest], "manifest.m3u8", { type: "application/x-mpegURL", }); let manifest = URL.createObjectURL(file); - let poster = `https://anixart.libria.fun${ep.preview}`; - return { manifest, poster }; + return { manifest, poster: data.poster }; } return { manifest: null, poster: null }; }; @@ -245,23 +126,22 @@ export const _fetchSibnetManifest = async ( url: string, setPlayerError: (state) => void ) => { - // Fetch data via cloud endpoint - const NEXT_PUBLIC_SIBNET_PARSER_URL = env("NEXT_PUBLIC_SIBNET_PARSER_URL") - if (!NEXT_PUBLIC_SIBNET_PARSER_URL) { + const NEXT_PUBLIC_PLAYER_PARSER_URL = env("NEXT_PUBLIC_PLAYER_PARSER_URL") + if (!NEXT_PUBLIC_PLAYER_PARSER_URL) { setPlayerError({ - message: "Источник не настроен", - detail: "переменная 'NEXT_PUBLIC_SIBNET_PARSER_URL' не обнаружена", + message: "Плеер не настроен", + detail: "переменная 'NEXT_PUBLIC_PLAYER_PARSER_URL' не обнаружена", }); return { manifest: null, poster: null }; } + const data = await _fetchPlayer( - `${NEXT_PUBLIC_SIBNET_PARSER_URL}/?url=${url}&player=sibnet`, + `${NEXT_PUBLIC_PLAYER_PARSER_URL}/?url=${url}&player=sibnet`, setPlayerError ); + if (data) { - let manifest = data.video; - let poster = data.poster; - return { manifest, poster }; + return { manifest: data.manifest, poster: data.poster }; } return { manifest: null, poster: null }; }; diff --git a/app/components/SettingsModal/SettingsModal.tsx b/app/components/SettingsModal/SettingsModal.tsx index 90ee3c3..56d9291 100644 --- a/app/components/SettingsModal/SettingsModal.tsx +++ b/app/components/SettingsModal/SettingsModal.tsx @@ -16,6 +16,8 @@ import { useThemeMode, } from "flowbite-react"; import Link from "next/link"; +import { env } from "next-runtime-env"; +import { useEffect, useState } from "react"; const HomeCategory = { last: "Последние релизы", @@ -51,6 +53,14 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => { const userStore = useUserStore(); const { computedMode, setMode } = useThemeMode(); + const [isPlayerConfigured, setIsPlayerConfigured] = useState(false); + + useEffect(() => { + const NEXT_PUBLIC_PLAYER_PARSER_URL = env("NEXT_PUBLIC_PLAYER_PARSER_URL") || null; + if (NEXT_PUBLIC_PLAYER_PARSER_URL) { + setIsPlayerConfigured(true); + } + }, []); return ( { }) } checked={preferenceStore.params.experimental.newPlayer} + disabled={!isPlayerConfigured} /> diff --git a/player-parsers/README.RU.md b/player-parsers/README.RU.md index d206410..406611d 100644 --- a/player-parsers/README.RU.md +++ b/player-parsers/README.RU.md @@ -22,9 +22,6 @@ - VIDEO_URL - ссылка на видео от источника - PLAYER_SOURCE - источник, один из: kodik, sibnet, libria -> [!NOTE] -> Если используется источник libria, ссылка должна быть ссылкой на API anilibria, а не на плеер - Ответ: - 500|400: произошла ошибка, подробнее в строке `message` в теле ответа diff --git a/player-parsers/README.md b/player-parsers/README.md index a0bc338..495870f 100644 --- a/player-parsers/README.md +++ b/player-parsers/README.md @@ -22,9 +22,6 @@ where: - VIDEO_URL - the link to the video from the source - PLAYER_SOURCE - the source, one of: kodik, sibnet, libria -> [!NOTE] -> When using libria source, url should be the url to the anilibria api, not player directly - Response: - 500|400: an error occurred, see the `message` field in the response body for details diff --git a/player-parsers/kodik.ts b/player-parsers/kodik.ts index e28444d..2b033d6 100644 --- a/player-parsers/kodik.ts +++ b/player-parsers/kodik.ts @@ -90,6 +90,88 @@ export async function getKodikURL(res, url: string) { return; } - asJSON(res, await linksRes.json(), 200); + let data = stripResponse(await linksRes.json()); + if (isEncrypted(data)) { + for (const [key] of Object.entries(data.links)) { + data.links[key][0].src = decryptSrc(data.links[key][0].src); + } + } + + if (!hasProto(data)) { + for (const [key] of Object.entries(data.links)) { + data.links[key][0].src = addProto(data.links[key][0].src); + } + } + + if (!isAnimeTvSeries(data)) { + data["manifest"] = data.links[data.default][0].src.replace( + `${data.default}.mp4:hls:`, + "" + ); + } else { + data["manifest"] = createManifest(data); + } + + data["poster"] = data.links[data.default][0].src.replace( + `${data.default}.mp4:hls:manifest.m3u8`, + "thumb001.jpg" + ); + + asJSON(res, data, 200); return; } + +function stripResponse(data) { + return { + default: data.default, + links: data.links, + }; +} + +function isEncrypted(data) { + return !data.links[data.default][0].src.includes("//"); +} + +function decryptSrc(enc: string) { + const decryptedBase64 = enc.replace(/[a-zA-Z]/g, (e: any) => { + return String.fromCharCode( + (e <= "Z" ? 90 : 122) >= (e = e.charCodeAt(0) + 18) ? e : e - 26 + ); + }); + return atob(decryptedBase64); +} + +function hasProto(data) { + return data.links[data.default][0].src.startsWith("http"); +} + +function addProto(string) { + return `https:${string}`; +} + +function isAnimeTvSeries(data) { + return ( + data.links[data.default][0].src.includes("animetvseries") || + data.links[data.default][0].src.includes("tvseries") + ); +} + +function createManifest(data) { + const resolutions = { + 240: "427x240", + 360: "578x360", + 480: "854x480", + 720: "1280x720", + 1080: "1920x1080", + }; + + const stringBuilder: string[] = []; + + stringBuilder.push("#EXTM3U"); + for (const [key] of Object.entries(data.links)) { + stringBuilder.push(`#EXT-X-STREAM-INF:RESOLUTION=${resolutions[key]}`); + stringBuilder.push(data.links[key][0].src); + } + + return stringBuilder.join("\n"); +} diff --git a/player-parsers/libria.ts b/player-parsers/libria.ts index a907074..73a23bf 100644 --- a/player-parsers/libria.ts +++ b/player-parsers/libria.ts @@ -1,17 +1,72 @@ import { asJSON } from "./shared"; +const API_URL = "https://api.anilibria.tv/v3/title" export async function getAnilibriaURL(res, url: string) { - - if (!url.includes("anilibria")) { + if (!url.includes("libria")) { asJSON(res, { message: "Wrong url provided for player libria" }, 400); return } - let apiRes = await fetch(url); + const decodedUrl = new URL(url) + + const releaseId = decodedUrl.searchParams.get("id") || null + const releaseEp = decodedUrl.searchParams.get("ep") || null + + let apiRes = await fetch(`${API_URL}?id=${releaseId}`); if (!apiRes.ok) { asJSON(res, { message: "LIBRIA: failed to get api response" }, 500); return } - asJSON(res, await apiRes.json(), 200); + + let data = stripResponse(await apiRes.json(), releaseEp); + + if (releaseEp) { + data["manifest"] = createManifest(data, releaseEp) + data["poster"] = getPoster(data, releaseEp) + } + + + asJSON(res, data, 200); return } + +function stripResponse(data, releaseEp) { + const resp = {} + resp["posters"] = data.posters + + resp["player"] = {} + resp["player"]["host"] = data.player.host + resp["player"]["list"] = data.player.list + + if (releaseEp) { + resp["player"]["list"] = {} + resp["player"]["list"][releaseEp] = data.player.list[releaseEp] + } + + return resp +} + +function createManifest(data, releaseEp) { + const resolutions = { + sd: "854x480", + hd: "1280x720", + fhd: "1920x1080", + }; + + const stringBuilder: string[] = []; + + stringBuilder.push("#EXTM3U"); + for (const [key, value] of Object.entries(data.player.list[releaseEp].hls).reverse()) { + if (!value) continue + stringBuilder.push(`#EXT-X-STREAM-INF:RESOLUTION=${resolutions[key]}`); + stringBuilder.push(`https://${data.player.host}${value}`); + } + + return stringBuilder.join("\n"); +} + +function getPoster(data, releaseEp) { + if (data.player.list[releaseEp].preview) return `https://anixart.libria.fun${data.player.list[releaseEp].preview}` + if (data.posters.medium.raw_base64_file) return data.posters.medium.url + return `https://anilibria.top${data.posters.medium.url}` +} \ No newline at end of file diff --git a/player-parsers/sibnet.ts b/player-parsers/sibnet.ts index 5df85b8..ab9b752 100644 --- a/player-parsers/sibnet.ts +++ b/player-parsers/sibnet.ts @@ -46,7 +46,7 @@ export async function getSibnetURL(res, url: string) { return } - const video = actualVideoRes.headers.get("location"); + const video = `https:${actualVideoRes.headers.get("location")}`; const poster = posterMatch ? posterMatch.length > 0 ? @@ -54,6 +54,6 @@ export async function getSibnetURL(res, url: string) { : null : null; - asJSON(res, { video, poster }, 200) + asJSON(res, { manifest: video, poster }, 200) return }