feat: add AniLibria parsing

This commit is contained in:
Kentai Radiquum 2025-03-15 15:41:08 +05:00
parent 97c8935a0f
commit 1b765fe857
Signed by: Radiquum
GPG key ID: 858E8EE696525EED
7 changed files with 370 additions and 3616 deletions

View file

@ -0,0 +1,3 @@
export const EpisodeSelector = () => {
return <div>EPISODES</div>
}

View file

@ -1,11 +1,21 @@
"use client";
import { Card } from "flowbite-react";
import Player from "next-video/player";
import { useEffect, useState } from "react";
import { ENDPOINTS } from "#/api/config";
export const ReleasePlayerCustom = (props: { id: number }) => {
import { VoiceoverSelector } from "./VoiceoverSelector";
import { SourceSelector } from "./SourceSelector";
import { EpisodeSelector } from "./EpisodeSelector";
import { Spinner } from "../Spinner/Spinner";
import HlsVideo from "hls-video-element/react";
import MediaThemeSutro from "player.style/sutro/react";
export const ReleasePlayerCustom = (props: {
id: number;
token: string | null;
}) => {
const [voiceover, setVoiceover] = useState({
selected: null,
available: null,
@ -18,10 +28,18 @@ export const ReleasePlayerCustom = (props: { id: number }) => {
selected: null,
available: null,
});
const [playerSrc, SetPlayerSrc] = useState(null);
const [playerProps, SetPlayerProps] = useState({
src: null,
poster: null,
useCustom: false,
});
const _fetchVoiceover = async (release_id: number) => {
const response = await fetch(`${ENDPOINTS.release.episode}/${release_id}`);
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;
};
@ -58,18 +76,21 @@ export const ReleasePlayerCustom = (props: { id: number }) => {
if (urlParamsMatch.length == 0) {
alert("Failed to get urlParams");
return;
};
}
const urlParamsStr = urlParamsMatch[0].replace("var urlParams = '", "").replace("';", "");
const urlParamsStr = urlParamsMatch[0]
.replace("var urlParams = '", "")
.replace("';", "");
const urlParams = JSON.parse(urlParamsStr);
const urlStr = url.replace("https://kodik.info/", "")
const type = urlStr.split("/")[0]
const id = urlStr.split("/")[1]
const hash = urlStr.split("/")[2]
const urlStr = url.replace("https://kodik.info/", "");
const type = urlStr.split("/")[0];
const id = urlStr.split("/")[1];
const hash = urlStr.split("/")[2];
const responseMan = await fetch(
`/api/proxy/${encodeURIComponent("https://kodik.info/ftor")}?isNotAnixart=true`, {
`/api/proxy/${encodeURIComponent("https://kodik.info/ftor")}?isNotAnixart=true`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
@ -86,35 +107,159 @@ export const ReleasePlayerCustom = (props: { id: number }) => {
ref_sign: urlParams.ref_sign,
bad_user: false,
cdn_is_working: true,
info: {}
info: {},
}),
}
);
const dataMan = await responseMan.json();
let manifest = `https:${dataMan.links["360"][0].src.replace("360.mp4:hls:", "")}`;
return manifest;
let poster = `https:${dataMan.links["360"][0].src.replace("360.mp4:hls:manifest.m3u8", "thumb001.jpg")}`;
return { manifest, poster };
};
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 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`}`;
const blob = new Blob([blobTxt], { type: "application/x-mpegURL" });
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 };
};
useEffect(() => {
const __getInfo = async () => {
const vo = await _fetchVoiceover(props.id);
const src = await _fetchSource(props.id, vo.types[0].id);
const episodes = await _fetchEpisode(
props.id,
vo.types[0].id,
src.sources[0].id
);
const manifest = await _fetchKodikManifest(episodes.episodes[0].url);
SetPlayerSrc(manifest);
setVoiceover({
selected: vo.types[0],
available: vo.types,
});
};
__getInfo();
}, []);
useEffect(() => {
const __getInfo = async () => {
const src = await _fetchSource(props.id, voiceover.selected.id);
setSource({
selected: src.sources[0],
available: src.sources,
});
};
if (voiceover.selected) {
__getInfo();
}
}, [voiceover.selected]);
useEffect(() => {
const __getInfo = async () => {
const episodes = await _fetchEpisode(
props.id,
voiceover.selected.id,
source.selected.id
);
setEpisode({
selected: episodes.episodes[0],
available: episodes.episodes,
});
};
if (source.selected) {
__getInfo();
}
}, [source.selected]);
useEffect(() => {
const __getInfo = async () => {
if (source.selected.name == "Kodik") {
const { manifest, poster } = await _fetchKodikManifest(
episode.selected.url
);
SetPlayerProps({
src: manifest,
poster: poster,
useCustom: true,
});
return;
}
if (source.selected.name == "Libria") {
const { manifest, poster } = await _fetchAnilibriaManifest(
episode.selected.url
);
SetPlayerProps({
src: `${manifest}`,
poster: poster,
useCustom: true,
});
return;
}
SetPlayerProps({
src: episode.selected.url,
poster: null,
useCustom: false,
});
};
if (episode.selected) {
__getInfo();
}
}, [episode.selected]);
return (
<Card>
{/* @ts-ignore */}
{!playerSrc ? <p>Loading...</p> : <Player src={playerSrc} />}
<p>ReleasePlayerCustom</p>
{(
!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">
<VoiceoverSelector
availableVoiceover={voiceover.available}
voiceover={voiceover.selected}
setVoiceover={setVoiceover}
/>
<SourceSelector
availableSource={source.available}
source={source.selected}
setSource={setSource}
/>
</div>
{
playerProps.useCustom ?
<MediaThemeSutro>
<HlsVideo
slot="media"
src={playerProps.src}
poster={playerProps.poster}
preload="auto"
muted
crossOrigin=""
/>
</MediaThemeSutro>
// @ts-ignore
// <Player
// src={playerProps.src}
// poster={playerProps.poster}
// className="w-full aspect-video"
// type="hls"
// />
: <iframe src={playerProps.src} className="w-full aspect-video" />
}
</div>
}
</Card>
);
};

View file

@ -0,0 +1,71 @@
"use client";
import { Dropdown } from "flowbite-react";
import { numberDeclension } from "#/api/utils";
interface Source {
id: number;
name: string;
episodes_count: number;
}
const DropdownTrigger = ({ name }: Source) => {
return (
<div className="flex items-center gap-1 cursor-pointer">
<span className="w-6 h-6 iconify material-symbols--motion-play"></span>
<p>{name}</p>
<span className="w-6 h-6 iconify material-symbols--arrow-drop-down"></span>
</div>
);
};
const DropdownItem = ({
name,
episodes_count,
}: Source) => {
return (
<div className="flex flex-col gap-2 cursor-pointer">
<div className="flex items-center gap-2">
<p>{name}</p>
</div>
<div className="flex items-center gap-2">
<p>
{episodes_count}{" "}
{numberDeclension(episodes_count, "серия", "серии", "серий")}
</p>
</div>
</div>
);
};
export const SourceSelector = (props: {
availableSource: Source[];
source: Source;
setSource: any;
}) => {
return (
<Dropdown
label=""
dismissOnClick={true}
renderTrigger={() => (
<span>
<DropdownTrigger {...props.source} />
</span>
)}
>
{props.availableSource.map((source: Source) => (
<Dropdown.Item
key={`source_${source.id}`}
onClick={() =>
props.setSource({
selected: source,
available: props.availableSource,
})
}
>
<DropdownItem {...source} />
</Dropdown.Item>
))}
</Dropdown>
);
};

View file

@ -0,0 +1,88 @@
"use client";
import { Dropdown } from "flowbite-react";
import { numberDeclension } from "#/api/utils";
interface Voiceover {
id: number;
name: string;
icon: string;
episodes_count: number;
view_count: number;
pinned: boolean;
}
const DropdownTrigger = ({ icon, name, pinned }: Voiceover) => {
return (
<div className="flex items-center gap-2 cursor-pointer">
{icon && <img className="w-6 h-6 rounded-full" src={icon}></img>}
<p>{name}</p>
{pinned && (
<span className="h-6 bg-gray-700 dark:bg-gray-300 iconify material-symbols--push-pin"></span>
)}
<span className="w-6 h-6 -ml-2 iconify material-symbols--arrow-drop-down"></span>
</div>
);
};
const DropdownItem = ({
icon,
name,
pinned,
episodes_count,
view_count,
}: Voiceover) => {
return (
<div className="flex flex-col gap-2 cursor-pointer">
<div className="flex items-center gap-2">
{icon && <img className="w-6 h-6 rounded-full" src={icon}></img>}
<p>{name}</p>
{pinned && (
<span className="h-6 iconify material-symbols--push-pin"></span>
)}
</div>
<div className="flex items-center gap-2">
<p>
{episodes_count}{" "}
{numberDeclension(episodes_count, "серия", "серии", "серий")}
</p>
<p>
{view_count}{" "}
{numberDeclension(view_count, "просмотр", "просмотра", "просмотров")}
</p>
</div>
</div>
);
};
export const VoiceoverSelector = (props: {
availableVoiceover: Voiceover[];
voiceover: Voiceover;
setVoiceover: any;
}) => {
return (
<Dropdown
label=""
dismissOnClick={true}
renderTrigger={() => (
<span>
<DropdownTrigger {...props.voiceover} />
</span>
)}
>
{props.availableVoiceover.map((voiceover: Voiceover) => (
<Dropdown.Item
key={`voiceover_${voiceover.id}`}
onClick={() =>
props.setVoiceover({
selected: voiceover,
available: props.availableVoiceover,
})
}
>
<DropdownItem {...voiceover} />
</Dropdown.Item>
))}
</Dropdown>
);
};

View file

@ -99,7 +99,7 @@ export const ReleasePage = (props: any) => {
data.release.status.name.toLowerCase() != "анонс" && (
<div className="[grid-column:1] [grid-row:span_12]">
{preferenceStore.params.experimental.newPlayer ? (
<ReleasePlayerCustom id={props.id} />
<ReleasePlayerCustom id={props.id} token={userStore.token} />
) : (
<ReleasePlayer id={props.id} />
)}

3625
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,10 +13,12 @@
"deepmerge-ts": "^7.1.0",
"flowbite": "^2.4.1",
"flowbite-react": "^0.10.1",
"hls-video-element": "^1.5.0",
"markdown-to-jsx": "^7.4.7",
"media-chrome": "^4.8.0",
"next": "^14.2.13",
"next-plausible": "^3.12.1",
"next-video": "^2.1.0",
"player.style": "^0.1.6",
"react": "^18",
"react-cropper": "^2.3.3",
"react-dom": "^18",