From 4c0345ffab68cfecd92db05ae160c6ddf8bd6415 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Tue, 8 Apr 2025 01:04:21 +0500 Subject: [PATCH 01/32] move selectors into player --- .../ReleasePlayer/EpisodeSelector.tsx | 5 +- .../ReleasePlayer/ReleasePlayerCustom.tsx | 114 ++++++++++++------ .../ReleasePlayer/SourceSelector.tsx | 8 +- .../ReleasePlayer/VoiceoverSelector.tsx | 8 +- 4 files changed, 88 insertions(+), 47 deletions(-) diff --git a/app/components/ReleasePlayer/EpisodeSelector.tsx b/app/components/ReleasePlayer/EpisodeSelector.tsx index 958555a..752bfbf 100644 --- a/app/components/ReleasePlayer/EpisodeSelector.tsx +++ b/app/components/ReleasePlayer/EpisodeSelector.tsx @@ -57,7 +57,7 @@ export const EpisodeSelector = (props: { } return ( -
+
@@ -82,7 +83,7 @@ export const EpisodeSelector = (props: { + ); + }) + : ""} +
+ ); +}; diff --git a/package-lock.json b/package-lock.json index 0af315f..0758888 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "eslint-config-next": "14.2.5", "eslint-plugin-react-refresh": "^0.4.19", "postcss": "^8", + "tailwind-scrollbar": "^3.1.0", "tailwindcss": "^3.4.1" } }, @@ -6664,6 +6665,19 @@ "url": "https://github.com/sponsors/dcastil" } }, + "node_modules/tailwind-scrollbar": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.1.0.tgz", + "integrity": "sha512-pmrtDIZeHyu2idTejfV59SbaJyvp1VRjYxAjZBH0jnyrPRo6HL1kD5Glz8VPagasqr6oAx6M05+Tuw429Z8jxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "tailwindcss": "3.x" + } + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", diff --git a/package.json b/package.json index 35d5b6e..3b6982f 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "eslint-config-next": "14.2.5", "eslint-plugin-react-refresh": "^0.4.19", "postcss": "^8", + "tailwind-scrollbar": "^3.1.0", "tailwindcss": "^3.4.1" } } diff --git a/tailwind.config.js b/tailwind.config.js index 1864db8..d3ac4d9 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -15,7 +15,8 @@ module.exports = { ], plugins: [ addIconSelectors(["mdi", "material-symbols", "twemoji", "fa6-brands"]), - flowbiteReact + flowbiteReact, + require("tailwind-scrollbar") ], darkMode: "selector", theme: { From 0168daa6cc6c66864ac25b47c7e448fd7eb5e70c Mon Sep 17 00:00:00 2001 From: Radiquum Date: Wed, 9 Apr 2025 06:24:08 +0500 Subject: [PATCH 06/32] add label --- .../ReleasePlayer/MediaPlayer.module.css | 2 +- .../ReleasePlayer/VoiceoverSelectorMenu.tsx | 73 ++++++++++--------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/app/components/ReleasePlayer/MediaPlayer.module.css b/app/components/ReleasePlayer/MediaPlayer.module.css index e772ab0..082e3c5 100644 --- a/app/components/ReleasePlayer/MediaPlayer.module.css +++ b/app/components/ReleasePlayer/MediaPlayer.module.css @@ -116,7 +116,7 @@ --media-settings-menu-min-width: calc(10 * var(--base)); --media-menu-transform-in: translateY(0) scale(1); --media-menu-transform-out: translateY(20px) rotate(3deg) scale(1); - background: rgb(30 30 30 / .8); + background: rgb(30 30 30 / .6); min-width: var(--media-settings-menu-min-width, 170px); position: absolute; right: 10px; diff --git a/app/components/ReleasePlayer/VoiceoverSelectorMenu.tsx b/app/components/ReleasePlayer/VoiceoverSelectorMenu.tsx index de208e7..2d6b357 100644 --- a/app/components/ReleasePlayer/VoiceoverSelectorMenu.tsx +++ b/app/components/ReleasePlayer/VoiceoverSelectorMenu.tsx @@ -63,43 +63,46 @@ export const VoiceoverSelectorMenu = ({ }, [release_id, token]); return ( -
- {voiceoverList && voiceoverList.length > 0 ? - voiceoverList.map((vo: Voiceover) => { - return ( - - ); - }) - : ""} + + ); + }) + : ""} +
); }; From 0a5b8a59e6aa2186f8ca730b5d41fe2ddbd60080 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Wed, 9 Apr 2025 14:48:16 +0500 Subject: [PATCH 07/32] return source selection --- .../ReleasePlayer/ReleasePlayerCustom.tsx | 48 ++------ .../ReleasePlayer/SourceSelector.tsx | 76 ------------ .../ReleasePlayer/SourceSelectorMenu.tsx | 115 ++++++++++++++++++ .../ReleasePlayer/VoiceoverSelector.tsx | 104 ---------------- .../ReleasePlayer/VoiceoverSelectorMenu.tsx | 9 +- 5 files changed, 128 insertions(+), 224 deletions(-) delete mode 100644 app/components/ReleasePlayer/SourceSelector.tsx create mode 100644 app/components/ReleasePlayer/SourceSelectorMenu.tsx delete mode 100644 app/components/ReleasePlayer/VoiceoverSelector.tsx diff --git a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx b/app/components/ReleasePlayer/ReleasePlayerCustom.tsx index 5b04cbf..22ee72a 100644 --- a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx +++ b/app/components/ReleasePlayer/ReleasePlayerCustom.tsx @@ -42,6 +42,7 @@ import { MediaSettingsMenuItem, } from "media-chrome/react/menu"; import { VoiceoverSelectorMenu } from "./VoiceoverSelectorMenu"; +import { SourceSelectorMenu } from "./SourceSelectorMenu"; export const ReleasePlayerCustom = (props: { id: number; @@ -70,45 +71,6 @@ export const ReleasePlayerCustom = (props: { // 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); - - // old info fetching - - // useEffect(() => { - // const __getInfo = async () => { - // 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: selectedSrc, - // available: src.sources, - // }); - // } - // }; - // if (voiceover.selected) { - // __getInfo(); - // } - // // eslint-disable-next-line react-hooks/exhaustive-deps - // }, [voiceover.selected]); - // useEffect(() => { // const __getInfo = async () => { // let url = `${ENDPOINTS.release.episode}/${props.id}/${voiceover.selected.id}/${source.selected.id}`; @@ -364,6 +326,14 @@ export const ReleasePlayerCustom = (props: { setVoiceover={setVoiceover} setPlayerError={setPlayerError} /> + diff --git a/app/components/ReleasePlayer/SourceSelector.tsx b/app/components/ReleasePlayer/SourceSelector.tsx deleted file mode 100644 index f79e327..0000000 --- a/app/components/ReleasePlayer/SourceSelector.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import { Dropdown, DropdownItem } from "flowbite-react"; -import { numberDeclension } from "#/api/utils"; -import { useUserPlayerPreferencesStore } from "#/store/player"; - -interface Source { - id: number; - name: string; - episodes_count: number; -} - -const DropdownTrigger = ({ name }: Source) => { - return ( -
- -

{name}

- -
- ); -}; - -const DropdownItemInternal = ({ name, episodes_count }: Source) => { - return ( -
-
-

{name}

-
-
-

- {episodes_count}{" "} - {numberDeclension(episodes_count, "серия", "серии", "серий")} -

-
-
- ); -}; - -export const SourceSelector = (props: { - availableSource: Source[]; - source: Source; - setSource: any; - release_id: any; -}) => { - const playerPreferenceStore = useUserPlayerPreferencesStore(); - - return ( - ( - - - - )} - > - {props.availableSource.map((source: Source) => ( - { - playerPreferenceStore.setPreferredPlayer( - props.release_id, - source.name - ); - props.setSource({ - selected: source, - available: props.availableSource, - }); - }} - > - - - ))} - - ); -}; diff --git a/app/components/ReleasePlayer/SourceSelectorMenu.tsx b/app/components/ReleasePlayer/SourceSelectorMenu.tsx new file mode 100644 index 0000000..ab5f10e --- /dev/null +++ b/app/components/ReleasePlayer/SourceSelectorMenu.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { ENDPOINTS } from "#/api/config"; +import { useEffect } from "react"; +import { _fetchAPI } from "./PlayerParsing"; +import { useUserPlayerPreferencesStore } from "#/store/player"; +import { numberDeclension } from "#/api/utils"; +import { Voiceover } from "./VoiceoverSelectorMenu"; + +export interface Source { + id: number; + name: string; + episodes_count: number; +} + +interface SourceSelectorMenuProps { + release_id: number; + setSource: (state) => void; + voiceover: Voiceover; + source: Source; + sourceList: Source[]; + setPlayerError: (state) => void; +} + +export const SourceSelectorMenu = ({ + release_id, + setSource, + voiceover, + source, + sourceList, + setPlayerError, +}: SourceSelectorMenuProps) => { + const playerPreferenceStore = useUserPlayerPreferencesStore(); + const preferredSource = playerPreferenceStore.getPreferredPlayer(release_id); + + useEffect(() => { + const __getInfo = async () => { + let url = `${ENDPOINTS.release.episode}/${release_id}/${voiceover.id}`; + const src = await _fetchAPI( + url, + "Не удалось получить информацию о источниках", + setPlayerError + ); + if (src) { + const selectedSrc = + src.sources.find( + (source: Source) => 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: selectedSrc, + available: src.sources, + }); + } + }; + if (voiceover) { + __getInfo(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [voiceover]); + + return ( +
+

Источник

+
+ {sourceList && sourceList.length > 0 ? + sourceList.map((src: Source) => { + return ( + + ); + }) + : ""} +
+
+ ); +}; diff --git a/app/components/ReleasePlayer/VoiceoverSelector.tsx b/app/components/ReleasePlayer/VoiceoverSelector.tsx deleted file mode 100644 index 203ceb1..0000000 --- a/app/components/ReleasePlayer/VoiceoverSelector.tsx +++ /dev/null @@ -1,104 +0,0 @@ -"use client"; - -import { Dropdown, DropdownItem } from "flowbite-react"; -import { numberDeclension } from "#/api/utils"; -import { useUserPlayerPreferencesStore } from "#/store/player"; - -interface Voiceover { - id: number; - name: string; - icon: string; - episodes_count: number; - view_count: number; - pinned: boolean; -} - -const DropdownTrigger = ({ icon, name, pinned }: Voiceover) => { - return ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {icon && } -

{name}

- {pinned && ( - - )} - -
- ); -}; - -const DropdownItemInternal = ({ - icon, - name, - pinned, - episodes_count, - view_count, -}: Voiceover) => { - return ( -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {icon && } -

{name}

- {pinned && ( - - )} -
-
-

- {episodes_count}{" "} - {numberDeclension(episodes_count, "серия", "серии", "серий")} -

-

- {view_count}{" "} - {numberDeclension(view_count, "просмотр", "просмотра", "просмотров")} -

-
-
- ); -}; - -const DropdownTheme = { - content: "md:grid md:grid-cols-2 xl:grid-cols-4 gap-2 w-full container", -}; - -export const VoiceoverSelector = (props: { - availableVoiceover: Voiceover[]; - voiceover: Voiceover; - setVoiceover: any; - release_id: number; -}) => { - const playerPreferenceStore = useUserPlayerPreferencesStore(); - - return ( - ( - - - - )} - > - {props.availableVoiceover.map((voiceover: Voiceover) => ( - { - playerPreferenceStore.setPreferredVoiceover( - props.release_id, - voiceover.name - ); - props.setVoiceover({ - selected: voiceover, - available: props.availableVoiceover, - }); - }} - > - - - ))} - - ); -}; diff --git a/app/components/ReleasePlayer/VoiceoverSelectorMenu.tsx b/app/components/ReleasePlayer/VoiceoverSelectorMenu.tsx index 2d6b357..1fd76df 100644 --- a/app/components/ReleasePlayer/VoiceoverSelectorMenu.tsx +++ b/app/components/ReleasePlayer/VoiceoverSelectorMenu.tsx @@ -4,10 +4,9 @@ import { ENDPOINTS } from "#/api/config"; import { useEffect } from "react"; import { _fetchAPI } from "./PlayerParsing"; import { useUserPlayerPreferencesStore } from "#/store/player"; -import { Button } from "flowbite-react"; import { numberDeclension } from "#/api/utils"; -interface Voiceover { +export interface Voiceover { id: number; name: string; icon: string; @@ -50,7 +49,7 @@ export const VoiceoverSelectorMenu = ({ ); if (vo) { const selectedVO = - vo.types.find((voiceover: any) => voiceover.name === preferredVO) || + vo.types.find((voiceover: Voiceover) => voiceover.name === preferredVO) || vo.types[0]; setVoiceover({ selected: selectedVO, @@ -64,8 +63,8 @@ export const VoiceoverSelectorMenu = ({ return (
-

Озвучка

-
+

Озвучка

+
{voiceoverList && voiceoverList.length > 0 ? voiceoverList.map((vo: Voiceover) => { return ( From 5264534693bc101218096b6543407582886f051b Mon Sep 17 00:00:00 2001 From: Radiquum Date: Wed, 9 Apr 2025 17:11:08 +0500 Subject: [PATCH 08/32] feat: add a menu for selecting voiceover, source and episode inside of a player --- .../ReleasePlayer/EpisodeSelectorMenu.tsx | 131 +++++++++ .../ReleasePlayer/MediaPlayer.module.css | 9 +- .../ReleasePlayer/ReleasePlayerCustom.tsx | 260 +++++++++--------- .../ReleasePlayer/SourceSelectorMenu.tsx | 2 +- .../ReleasePlayer/VoiceoverSelectorMenu.tsx | 6 +- 5 files changed, 266 insertions(+), 142 deletions(-) diff --git a/app/components/ReleasePlayer/EpisodeSelectorMenu.tsx b/app/components/ReleasePlayer/EpisodeSelectorMenu.tsx index e69de29..e694cb4 100644 --- a/app/components/ReleasePlayer/EpisodeSelectorMenu.tsx +++ b/app/components/ReleasePlayer/EpisodeSelectorMenu.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { ENDPOINTS } from "#/api/config"; +import { useEffect, useState } from "react"; +import { _fetchAPI } from "./PlayerParsing"; + +import { Voiceover } from "./VoiceoverSelectorMenu"; +import { Source } from "./SourceSelectorMenu"; +import { getAnonEpisodesWatched } from "./ReleasePlayer"; + +export interface Episode { + position: number; + name: string; + is_watched: boolean; +} +interface EpisodeSelectorMenuProps { + release_id: number; + voiceover: Voiceover; + source: Source; + token: string | null; + setEpisode: (state) => void; + episode: Episode; + episodeList: Episode[]; + setPlayerError: (state) => void; +} + +export const EpisodeSelectorMenu = ({ + release_id, + token, + voiceover, + source, + setEpisode, + episode, + episodeList, + setPlayerError, +}: EpisodeSelectorMenuProps) => { + const [watchedEpisodes, setWatchedEpisodes] = useState([]); + useEffect(() => { + const __getInfo = async () => { + let url = `${ENDPOINTS.release.episode}/${release_id}/${voiceover.id}/${source.id}`; + if (token) { + url += `?token=${token}`; + } + const episodes = await _fetchAPI( + url, + "Не удалось получить информацию о эпизодах", + setPlayerError + ); + if (episodes) { + let anonEpisodesWatched = getAnonEpisodesWatched( + release_id, + source.id, + voiceover.id + ); + let lastEpisodeWatched = Math.max.apply( + 0, + Object.keys(anonEpisodesWatched[release_id][source.id][voiceover.id]) + ); + let selectedEpisode = + episodes.episodes.find( + (episode: Episode) => episode.position == lastEpisodeWatched + ) || episodes.episodes[0]; + + setEpisode({ + selected: selectedEpisode, + available: episodes.episodes, + }); + } + }; + if (source) { + __getInfo(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [source]); + + useEffect(() => { + if (release_id && source && voiceover) { + const anonEpisodesWatched = getAnonEpisodesWatched( + release_id, + source.id, + voiceover.id + ); + setWatchedEpisodes( + anonEpisodesWatched[release_id][source.id][voiceover.id] + ); + } + }, [release_id, source, voiceover]); + + return ( +
+

Эпизод

+
+ {episodeList && episodeList.length > 0 ? + episodeList.map((epis: Episode) => { + return ( + + ); + }) + : ""} +
+
+ ); +}; diff --git a/app/components/ReleasePlayer/MediaPlayer.module.css b/app/components/ReleasePlayer/MediaPlayer.module.css index 082e3c5..f10fa20 100644 --- a/app/components/ReleasePlayer/MediaPlayer.module.css +++ b/app/components/ReleasePlayer/MediaPlayer.module.css @@ -12,7 +12,7 @@ --media-primary-color: #fff; --media-secondary-color: transparent; - --media-menu-background: rgba(28, 28, 28, 0.6); + --media-menu-background: rgba(28, 28, 28, 0.8); --media-text-color: var(--_primary-color); --media-control-hover-background: var(--media-secondary-color); @@ -116,11 +116,12 @@ --media-settings-menu-min-width: calc(10 * var(--base)); --media-menu-transform-in: translateY(0) scale(1); --media-menu-transform-out: translateY(20px) rotate(3deg) scale(1); - background: rgb(30 30 30 / .6); + background: rgba(28, 28, 28, 0.8); min-width: var(--media-settings-menu-min-width, 170px); position: absolute; right: 10px; bottom: calc(3 * var(--base)); + padding: 0; padding-block: calc(0.15 * var(--base)); padding-inline: calc(0.6 * var(--base)); margin-right: 10px; @@ -131,6 +132,10 @@ max-height: 50%; } +.media-controller media-chrome-dialog > div { + word-wrap: normal !important; +} + .media-settings-menu[hidden] { display: block; visibility: visible; diff --git a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx b/app/components/ReleasePlayer/ReleasePlayerCustom.tsx index 22ee72a..b038e32 100644 --- a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx +++ b/app/components/ReleasePlayer/ReleasePlayerCustom.tsx @@ -43,6 +43,12 @@ import { } from "media-chrome/react/menu"; import { VoiceoverSelectorMenu } from "./VoiceoverSelectorMenu"; import { SourceSelectorMenu } from "./SourceSelectorMenu"; +import { + _fetchAnilibriaManifest, + _fetchKodikManifest, + _fetchSibnetManifest, +} from "./PlayerParsing"; +import { EpisodeSelectorMenu } from "./EpisodeSelectorMenu"; export const ReleasePlayerCustom = (props: { id: number; @@ -60,120 +66,87 @@ export const ReleasePlayerCustom = (props: { selected: null, available: null, }); - const [playerProps, SetPlayerProps] = useState({ + const [playerProps, SetPlayerProps] = useState<{ + src: string | null; + poster: string | null; + type: "hls" | "mp4" | null; + }>({ src: null, poster: null, type: null, - useCustom: false, }); const [playerError, setPlayerError] = useState(null); - // const [playbackRate, setPlaybackRate] = useState(1); + const [playbackRate, setPlaybackRate] = useState(1); // const [isErrorDetailsOpen, setIsErrorDetailsOpen] = useState(false); - // const [isLoading, setIsLoading] = useState(true); + const [isLoading, setIsLoading] = useState(true); - // useEffect(() => { - // const __getInfo = async () => { - // 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]; + useEffect(() => { + const __getInfo = async () => { + if (source.selected.name == "Kodik") { + const { manifest, poster } = await _fetchKodikManifest( + episode.selected.url, + setPlayerError + ); + if (manifest) { + SetPlayerProps({ + src: manifest, + poster: poster, + type: "hls", + }); + setIsLoading(false); + } + return; + } + if (source.selected.name == "Libria") { + const { manifest, poster } = await _fetchAnilibriaManifest( + episode.selected.url, + setPlayerError + ); + if (manifest) { + SetPlayerProps({ + src: manifest, + poster: poster, + type: "hls", + }); + setIsLoading(false); + } + return; + } + if (source.selected.name == "Sibnet") { + const { manifest, poster } = await _fetchSibnetManifest( + episode.selected.url, + setPlayerError + ); + if (manifest) { + SetPlayerProps({ + src: manifest, + poster: poster, + type: "mp4", + }); + setIsLoading(false); + } + return; + } + SetPlayerProps({ + src: episode.selected.url, + poster: null, + type: null, + }); + setIsLoading(false); + }; + if (episode.selected) { + setIsLoading(true); + setPlayerError(null); + __getInfo(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [episode.selected]); - // setEpisode({ - // selected: selectedEpisode, - // available: episodes.episodes, - // }); - // } - // }; - // if (source.selected) { - // __getInfo(); - // } - // // eslint-disable-next-line react-hooks/exhaustive-deps - // }, [source.selected]); - - // useEffect(() => { - // const __getInfo = async () => { - // if (source.selected.name == "Kodik") { - // const { manifest, poster } = await _fetchKodikManifest( - // episode.selected.url - // ); - // 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 - // ); - // 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 - // ); - // if (manifest) { - // SetPlayerProps({ - // src: manifest, - // poster: poster, - // useCustom: true, - // type: "mp4", - // }); - // setIsLoading(false); - // } - // return; - // } - // SetPlayerProps({ - // src: episode.selected.url, - // poster: null, - // useCustom: false, - // type: null, - // }); - // setIsLoading(false); - // }; - // if (episode.selected) { - // setIsLoading(true); - // setPlayerError(null); - // __getInfo(); - // } - // // eslint-disable-next-line react-hooks/exhaustive-deps - // }, [episode.selected]); + useEffect(() => { + if (document && document.querySelector("media-chrome-dialog")) { + document.querySelector("media-chrome-dialog").shadowRoot.querySelector("slot").style.width = "100%"; + } + }, []) return ( @@ -255,35 +228,40 @@ export const ReleasePlayerCustom = (props: { defaultStreamType="on-demand" className={`relative w-full overflow-hidden ${Styles["media-controller"]}`} > - {/*
- {voiceover.selected && ( - - )} - {source.selected && ( - - )} -
*/} - { - // // @ts-ignore - // setPlaybackRate(e.target.playbackRate || 1); - // }} - /> + {playerProps.type == "hls" && ( + { + // @ts-ignore + setPlaybackRate(e.target.playbackRate || 1); + }} + /> + )} + {playerProps.type == "mp4" && ( + { + // @ts-ignore + setPlaybackRate(e.target.playbackRate || 1); + }} + > + )} + {(playerProps.type == null || playerProps.src == null) && ( + + )}
+ /> +
diff --git a/app/components/ReleasePlayer/SourceSelectorMenu.tsx b/app/components/ReleasePlayer/SourceSelectorMenu.tsx index ab5f10e..ac23c7f 100644 --- a/app/components/ReleasePlayer/SourceSelectorMenu.tsx +++ b/app/components/ReleasePlayer/SourceSelectorMenu.tsx @@ -77,7 +77,7 @@ export const SourceSelectorMenu = ({ return ( ))}
- )} + } ); }; diff --git a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx b/app/components/ReleasePlayer/ReleasePlayerCustom.tsx index 66e646d..3d58d7e 100644 --- a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx +++ b/app/components/ReleasePlayer/ReleasePlayerCustom.tsx @@ -40,7 +40,10 @@ import { Episode, EpisodeSelectorMenu } from "./EpisodeSelectorMenu"; import HlsVideo from "hls-video-element/react"; import VideoJS from "videojs-video-element/react"; import { ENDPOINTS } from "#/api/config"; -import { saveAnonEpisodeWatched } from "./ReleasePlayer"; +import { + getAnonEpisodesWatched, + saveAnonEpisodeWatched, +} from "./ReleasePlayer"; import { usePreferencesStore } from "#/store/preferences"; export const ReleasePlayerCustom = (props: { @@ -147,33 +150,44 @@ export const ReleasePlayerCustom = (props: { }, []); function saveEpisodeToHistory() { - if ( - preferenceStore.flags.saveWatchHistory && - episode.selected && - !episode.selected.is_watched - ) { - const objectToReplace = episode.available.find( - (arrayItem: Episode) => arrayItem.position === episode.selected.position - ); - const newObject = { - ...episode.selected, - is_watched: true, - }; - Object.assign(objectToReplace, newObject); - - saveAnonEpisodeWatched( + if (props.id && source.selected && voiceover.selected && episode.selected) { + const anonEpisodesWatched = getAnonEpisodesWatched( props.id, source.selected.id, - voiceover.selected.id, - episode.selected.position + voiceover.selected.id ); - if (props.token) { - fetch( - `${ENDPOINTS.statistic.addHistory}/${props.id}/${source.selected.id}/${episode.selected.position}?token=${props.token}` + if ( + preferenceStore.flags.saveWatchHistory && + !episode.selected.is_watched && + !Object.keys( + anonEpisodesWatched[props.id][source.selected.id][ + voiceover.selected.id + ] + ).includes(episode.selected.position.toString()) + ) { + const objectToReplace = episode.available.find( + (arrayItem: Episode) => + arrayItem.position === episode.selected.position ); - fetch( - `${ENDPOINTS.statistic.markWatched}/${props.id}/${source.selected.id}/${episode.selected.position}?token=${props.token}` + const newObject = { + ...episode.selected, + is_watched: true, + }; + Object.assign(objectToReplace, newObject); + saveAnonEpisodeWatched( + props.id, + source.selected.id, + voiceover.selected.id, + episode.selected.position ); + if (props.token) { + fetch( + `${ENDPOINTS.statistic.addHistory}/${props.id}/${source.selected.id}/${episode.selected.position}?token=${props.token}` + ); + fetch( + `${ENDPOINTS.statistic.markWatched}/${props.id}/${source.selected.id}/${episode.selected.position}?token=${props.token}` + ); + } } } } From da6639d799a14b7f96aa790ca0079581310838a5 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Sat, 12 Apr 2025 21:28:46 +0500 Subject: [PATCH 30/32] fix: episode changing via buttons --- app/components/ReleasePlayer/ReleasePlayerCustom.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx b/app/components/ReleasePlayer/ReleasePlayerCustom.tsx index 3d58d7e..d85368a 100644 --- a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx +++ b/app/components/ReleasePlayer/ReleasePlayerCustom.tsx @@ -75,6 +75,7 @@ export const ReleasePlayerCustom = (props: { const [playerError, setPlayerError] = useState(null); const [playbackRate, setPlaybackRate] = useState(1); const [isErrorDetailsOpen, setIsErrorDetailsOpen] = useState(false); + const [isEpLoadingTimeout, setIsEpLoadingTimeout] = useState(null); const [retryCount, setRetryCount] = useState(0); @@ -130,13 +131,20 @@ export const ReleasePlayerCustom = (props: { }); }; if (episode.selected) { + if (isEpLoadingTimeout) { + clearTimeout(isEpLoadingTimeout); + } + setPlayerError(null); SetPlayerProps({ src: null, poster: null, type: null, }); - setPlayerError(null); - __getInfo(); + setIsEpLoadingTimeout( + setTimeout(() => { + __getInfo(); + }, 250) + ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [episode.selected, retryCount]); From 73fb51d9149ae906c08441a1e3f779e154e40768 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Sun, 13 Apr 2025 02:18:52 +0500 Subject: [PATCH 31/32] fix: parsing of kodik manifest --- app/components/ReleasePlayer/PlayerParsing.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/components/ReleasePlayer/PlayerParsing.ts b/app/components/ReleasePlayer/PlayerParsing.ts index 8b57b7f..5fca19b 100644 --- a/app/components/ReleasePlayer/PlayerParsing.ts +++ b/app/components/ReleasePlayer/PlayerParsing.ts @@ -84,15 +84,18 @@ export const _fetchKodikManifest = async ( } if (lowQualityLink.includes("https://")) { - // string the https prefix, since we add it manually + // 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")) { - // if link includes "animetvseries" we need to construct manifest ourselves + 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")) { @@ -139,7 +142,7 @@ export const _fetchAnilibriaManifest = async ( ) => { 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}` + const _url = `https://api.anilibria.tv/v3/title?id=${id}`; const data = await _fetchPlayer( `https://anix-player.wah.su/?url=${_url}&player=libria`, setPlayerError From 42d838a497a57828b3ca5811801807ed78f421d4 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Sun, 13 Apr 2025 02:43:32 +0500 Subject: [PATCH 32/32] fix: manual kodik manifest parsing --- app/components/ReleasePlayer/PlayerParsing.ts | 97 +++++++++++++++---- 1 file changed, 79 insertions(+), 18 deletions(-) diff --git a/app/components/ReleasePlayer/PlayerParsing.ts b/app/components/ReleasePlayer/PlayerParsing.ts index 5fca19b..bb87f33 100644 --- a/app/components/ReleasePlayer/PlayerParsing.ts +++ b/app/components/ReleasePlayer/PlayerParsing.ts @@ -61,6 +61,15 @@ 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 @@ -75,12 +84,7 @@ export const _fetchKodikManifest = async ( if (!lowQualityLink.includes("//")) { // check if link is encrypted, else do nothing - const decryptedBase64 = lowQualityLink.replace(/[a-zA-Z]/g, (e) => { - return String.fromCharCode( - (e <= "Z" ? 90 : 122) >= (e = e.charCodeAt(0) + 18) ? e : e - 26 - ); - }); - lowQualityLink = atob(decryptedBase64); + lowQualityLink = decryptKodikLink(lowQualityLink); } if (lowQualityLink.includes("https://")) { @@ -100,30 +104,87 @@ export const _fetchKodikManifest = async ( 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`); + 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"; - !data.links["360"][0].src.startsWith("https:") ? - (blobTxt += `https:${data.links["360"][0].src}\n`) - : (blobTxt += `${data.links["360"][0].src}\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"; - !data.links["480"][0].src.startsWith("https:") ? - (blobTxt += `https:${data.links["480"][0].src}\n`) - : (blobTxt += `${data.links["480"][0].src}\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"; - !data.links["720"][0].src.startsWith("https:") ? - (blobTxt += `https:${data.links["720"][0].src}\n`) - : (blobTxt += `${data.links["720"][0].src}\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", {