diff --git a/.eslintrc.json b/.eslintrc.json index ab5cdb0..bffb357 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,3 @@ { - "extends": ["next/core-web-vitals", "prettier"], - "rules": { - "prettier/prettier": "error" - }, - "plugins": ["prettier"] + "extends": "next/core-web-vitals" } diff --git a/.gitignore b/.gitignore index f6c46f4..c927de2 100644 --- a/.gitignore +++ b/.gitignore @@ -52,10 +52,3 @@ traefik/traefik old/ #Trigger Vercel Prod Build - -# next-video -videos/* -!videos/*.json -!videos/*.js -!videos/*.ts -public/_next-video diff --git a/README.md b/README.md index 954ed05..c332aae 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ AniX is an unofficial web client for the Android application Anixart. It allows ## Changelog [RU] -- [3.3.0](./public/changelog/3.3.0.md) - [3.2.3](./public/changelog/3.2.3.md) - [3.2.2](./public/changelog/3.2.2.md) - [3.2.1](./public/changelog/3.2.1.md) +- [3.2.0](./public/changelog/3.2.0.md) [other versions](./public/changelog) diff --git a/app/api/config.ts b/app/api/config.ts index e4aeda4..815ec0a 100644 --- a/app/api/config.ts +++ b/app/api/config.ts @@ -1,4 +1,4 @@ -export const CURRENT_APP_VERSION = "3.3.0"; +export const CURRENT_APP_VERSION = "3.2.3"; export const API_URL = "https://api.anixart.tv"; export const API_PREFIX = "/api/proxy"; @@ -10,7 +10,6 @@ export const ENDPOINTS = { info: `${API_PREFIX}/release`, episode: `${API_PREFIX}/episode`, related: `${API_PREFIX}/related`, - licensed: `${API_PREFIX}/release/streaming/platform`, }, user: { profile: `${API_PREFIX}/profile`, diff --git a/app/api/utils.ts b/app/api/utils.ts index 53b48ae..da7c6fa 100644 --- a/app/api/utils.ts +++ b/app/api/utils.ts @@ -144,6 +144,15 @@ export function unixToDate( ); } +export const getSeasonFromUnix = (unix: number) => { + const date = new Date(unix * 1000); + const month = date.getMonth(); + if (month >= 3 && month <= 5) return "весна"; + if (month >= 6 && month <= 8) return "лето"; + if (month >= 9 && month <= 11) return "осень"; + return "зима"; +}; + export function sinceUnixDate(unixInSeconds: number) { const unix = Math.floor(unixInSeconds * 1000); const date = new Date(unix); diff --git a/app/components/ReleaseInfo/ReleaseInfo.Basics.tsx b/app/components/ReleaseInfo/ReleaseInfo.Basics.tsx index 50fdd5b..3fcac54 100644 --- a/app/components/ReleaseInfo/ReleaseInfo.Basics.tsx +++ b/app/components/ReleaseInfo/ReleaseInfo.Basics.tsx @@ -1,10 +1,8 @@ import { Card, Button } from "flowbite-react"; import { useState } from "react"; import Image from "next/image"; -import { ReleaseInfoStreaming } from "./ReleaseInfo.LicensedPlatforms"; export const ReleaseInfoBasics = (props: { - release_id: number; image: string; title: { ru: string; original: string }; note: string | null; @@ -54,7 +52,6 @@ export const ReleaseInfoBasics = (props: { > {isFullDescription ? "Скрыть" : "Показать полностью"} - diff --git a/app/components/ReleaseInfo/ReleaseInfo.Info.tsx b/app/components/ReleaseInfo/ReleaseInfo.Info.tsx index ab58da7..4f9e666 100644 --- a/app/components/ReleaseInfo/ReleaseInfo.Info.tsx +++ b/app/components/ReleaseInfo/ReleaseInfo.Info.tsx @@ -1,6 +1,6 @@ import { Card, Table } from "flowbite-react"; import { ReleaseInfoSearchLink } from "#/components/ReleaseInfo/ReleaseInfo.SearchLink"; -import { unixToDate, minutesToTime } from "#/api/utils"; +import { unixToDate, getSeasonFromUnix, minutesToTime } from "#/api/utils"; const weekDay = [ "_", "каждый понедельник", @@ -33,20 +33,21 @@ export const ReleaseInfoInfo = (props: { - {props.country ? - props.country.toLowerCase() == "япония" ? + {props.country ? ( + props.country.toLowerCase() == "япония" ? ( - : - - : - } + ) : ( + + ) + ) : ( + + )} {props.country && props.country} {(props.aired_on_date != 0 || props.year) && ", "} - {props.season && props.season != 0 ? - `${YearSeason[props.season]} ` - : ""} + {props.aired_on_date != 0 && + `${getSeasonFromUnix(props.aired_on_date)} `} {props.year && `${props.year} г.`} @@ -58,8 +59,7 @@ export const ReleaseInfoInfo = (props: { {props.episodes.released ? props.episodes.released : "?"} {"/"} {props.episodes.total ? props.episodes.total + " эп. " : "? эп. "} - {props.duration != 0 && - `по ${minutesToTime(props.duration, "daysHours")}`} + {props.duration != 0 && `по ${minutesToTime(props.duration, "daysHours")}`} @@ -69,9 +69,9 @@ export const ReleaseInfoInfo = (props: { {props.category} {", "} - {props.broadcast == 0 ? - props.status.toLowerCase() - : `выходит ${weekDay[props.broadcast]}`} + {props.broadcast == 0 + ? props.status.toLowerCase() + : `выходит ${weekDay[props.broadcast]}`} @@ -88,10 +88,7 @@ export const ReleaseInfoInfo = (props: { return (
{index > 0 && ", "} - +
); })} @@ -101,20 +98,14 @@ export const ReleaseInfoInfo = (props: { {props.author && ( <> {"Автор: "} - + {props.director && ", "} )} {props.director && ( <> {"Режиссёр: "} - + )} @@ -141,16 +132,18 @@ export const ReleaseInfoInfo = (props: { - {props.aired_on_date != 0 ? + {props.aired_on_date != 0 ? ( unixToDate(props.aired_on_date, "full") - : props.year ? + ) : props.year ? ( <> - {props.season && props.season != 0 ? - `${YearSeason[props.season]} ` - : ""} + {props.season && props.season != 0 + ? `${YearSeason[props.season]} ` + : ""} {props.year && `${props.year} г.`} - : "Скоро"} + ) : ( + "Скоро" + )}
)} diff --git a/app/components/ReleaseInfo/ReleaseInfo.LicensedPlatforms.tsx b/app/components/ReleaseInfo/ReleaseInfo.LicensedPlatforms.tsx deleted file mode 100644 index 6a1c72b..0000000 --- a/app/components/ReleaseInfo/ReleaseInfo.LicensedPlatforms.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; - -import { ENDPOINTS } from "#/api/config"; -import { useEffect, useState } from "react"; - -export const ReleaseInfoStreaming = (props: { release_id: number }) => { - const [data, setData] = useState(null); - - useEffect(() => { - const _getData = async () => { - const response = await fetch( - `${ENDPOINTS.release.licensed}/${props.release_id}` - ); - setData(await response.json()); - }; - _getData(); - }, []); - - return ( - <> - {!data ? - "" - : !(data.content.length > 0) ? - "" - :
-

Официальные источники:

-
- {data.content.map((item: any) => { - return ( - - -

{item.name}

-
- ); - })} -
-
- } - - ); -}; diff --git a/app/components/ReleasePlayer/EpisodeSelector.tsx b/app/components/ReleasePlayer/EpisodeSelector.tsx deleted file mode 100644 index 93afcd1..0000000 --- a/app/components/ReleasePlayer/EpisodeSelector.tsx +++ /dev/null @@ -1,131 +0,0 @@ -"use client"; - -import { ENDPOINTS } from "#/api/config"; -import { useState, useEffect } from "react"; -import { Swiper, SwiperSlide } from "swiper/react"; -import "swiper/css"; -import "swiper/css/navigation"; -import "swiper/css/mousewheel"; -import "swiper/css/scrollbar"; -import { Navigation, Mousewheel, Scrollbar } from "swiper/modules"; -import { Button } from "flowbite-react"; - -import { - getAnonEpisodesWatched, - saveAnonEpisodeWatched, -} from "./ReleasePlayer"; - -interface Episode { - id: number; - position: number; - name: string; - is_watched: boolean; -} - -interface Source { - id: number; - name: string; - episodes_count: number; -} - -export const EpisodeSelector = (props: { - availableEpisodes: Episode[]; - episode: Episode; - setEpisode: any; - source: Source; - release_id: any; - voiceover: any; - token: string | null; -}) => { - let anonEpisodesWatched = getAnonEpisodesWatched( - props.release_id, - props.source.id, - props.voiceover.id - ); - anonEpisodesWatched = - anonEpisodesWatched[props.release_id][props.source.id][props.voiceover.id]; - - async function saveEpisodeToHistory(episode: Episode) { - if (episode && props.token) { - fetch( - `${ENDPOINTS.statistic.addHistory}/${props.release_id}/${props.source.id}/${episode.position}?token=${props.token}` - ); - fetch( - `${ENDPOINTS.statistic.markWatched}/${props.release_id}/${props.source.id}/${episode.position}?token=${props.token}` - ); - } - } - - return ( -
- - {props.availableEpisodes.map((episode: Episode) => ( - - - - ))} - -
- ); -}; diff --git a/app/components/ReleasePlayer/ReleasePlayer.tsx b/app/components/ReleasePlayer/ReleasePlayer.tsx index 7f103e5..e077b9b 100644 --- a/app/components/ReleasePlayer/ReleasePlayer.tsx +++ b/app/components/ReleasePlayer/ReleasePlayer.tsx @@ -32,7 +32,7 @@ async function _fetch(url: string) { return data; } -export const getAnonEpisodesWatched = ( +const getAnonEpisodesWatched = ( Release: number, Source: number, Voiceover: number @@ -80,7 +80,7 @@ const getAnonCurrentEpisodeWatched = ( return anonEpisodesWatched[Release][Source][Voiceover][Episode]; }; -export const saveAnonEpisodeWatched = ( +const saveAnonEpisodeWatched = ( Release: number, Source: number, Voiceover: number, @@ -162,25 +162,32 @@ export const ReleasePlayer = (props: { id: number }) => { return; }); - if (!data || (data && Object.keys(data).length == 0)) { + if (data && Object.keys(data).length == 0) { _setError("Ошибка получение данных с сервера"); - return; } if (type == "voiceover") { - setVoiceoverInfo(data.types); - const preferredVoiceover = - data.types.find( - (voiceover: any) => voiceover.name === storedPreferredVoiceover - ) || data.types[0]; - setSelectedVoiceover(preferredVoiceover); + if (data.types.length > 0) { + setVoiceoverInfo(data.types); + const preferredVoiceover = + data.types.find( + (voiceover: any) => voiceover.name === storedPreferredVoiceover + ) || data.types[0]; + setSelectedVoiceover(preferredVoiceover); + } else { + _setError("Ошибка получения озвучек"); + } } else if (type == "sources") { - setSourcesInfo(data.sources); - const preferredSource = - data.sources.find( - (source: any) => source.name === storedPreferredPlayer - ) || data.sources[0]; - setSelectedSource(preferredSource); + if (data.sources.length > 0) { + setSourcesInfo(data.sources); + const preferredSource = + data.sources.find( + (source: any) => source.name === storedPreferredPlayer + ) || data.sources[0]; + setSelectedSource(preferredSource); + } else { + _setError("Ошибка получения источников"); + } } else if (type == "episodes") { if (data.episodes.length === 0) { const remSources = sourcesInfo.filter( @@ -189,7 +196,7 @@ export const ReleasePlayer = (props: { id: number }) => { setSourcesInfo(remSources); setSelectedSource(remSources[0]); return; - } else { + } else if (data.episodes.length > 0) { setEpisodeInfo(data.episodes); setSelectedEpisode(data.episodes[0]); @@ -213,6 +220,8 @@ export const ReleasePlayer = (props: { id: number }) => { } setSelectedEpisode(data.episodes[lastWatchedEpisode]); } + } else { + _setError("Ошибка получения эпизодов"); } } else { _setError("Неизвестный тип запроса"); diff --git a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx b/app/components/ReleasePlayer/ReleasePlayerCustom.tsx deleted file mode 100644 index 371a2fe..0000000 --- a/app/components/ReleasePlayer/ReleasePlayerCustom.tsx +++ /dev/null @@ -1,298 +0,0 @@ -"use client"; - -import { Card } from "flowbite-react"; -import { useEffect, useState } from "react"; -import { ENDPOINTS } from "#/api/config"; - -import { VoiceoverSelector } from "./VoiceoverSelector"; -import { SourceSelector } from "./SourceSelector"; -import { EpisodeSelector } from "./EpisodeSelector"; -import { Spinner } from "../Spinner/Spinner"; -import { useUserPlayerPreferencesStore } from "#/store/player"; - -import HlsVideo from "hls-video-element/react"; -import MediaThemeSutro from "player.style/sutro/react"; -import { getAnonEpisodesWatched } from "./ReleasePlayer"; - -export const ReleasePlayerCustom = (props: { - id: number; - token: string | null; -}) => { - const [voiceover, setVoiceover] = useState({ - selected: null, - available: null, - }); - const [source, setSource] = useState({ - selected: null, - available: null, - }); - const [episode, setEpisode] = useState({ - selected: null, - available: null, - }); - const [playerProps, SetPlayerProps] = useState({ - src: null, - poster: null, - type: null, - useCustom: false, - }); - - const playerPreferenceStore = useUserPlayerPreferencesStore(); - const preferredVO = playerPreferenceStore.getPreferredVoiceover(props.id); - const preferredSource = playerPreferenceStore.getPreferredPlayer(props.id); - - const _fetchVoiceover = async (release_id: number) => { - 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; - }; - - const _fetchSource = async (release_id: number, voiceover_id: number) => { - const response = await fetch( - `${ENDPOINTS.release.episode}/${release_id}/${voiceover_id}` - ); - const data = await response.json(); - return data; - }; - - const _fetchEpisode = async ( - release_id: number, - voiceover_id: number, - source_id: number - ) => { - let url = `${ENDPOINTS.release.episode}/${release_id}/${voiceover_id}/${source_id}`; - if (props.token) { - url += `?token=${props.token}`; - } - const response = await fetch(url); - const data = await response.json(); - return data; - }; - - const _fetchKodikManifest = async (url: string) => { - const response = await fetch( - `https://anix-player.wah.su/?url=${url}&player=kodik` - ); - const data = await response.json(); - let manifest = `https:${data.links["360"][0].src.replace("360.mp4:hls:", "")}`; - let poster = `https:${data.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 }; - }; - - const _fetchSibnetManifest = async (url: string) => { - const response = await fetch( - `https://sibnet.anix-player.wah.su/?url=${url}` - ); - const data = await response.json(); - - let manifest = data.video; - let poster = data.poster; - return { manifest, poster }; - }; - - useEffect(() => { - const __getInfo = async () => { - const vo = await _fetchVoiceover(props.id); - const selectedVO = - vo.types.find((voiceover: any) => voiceover.name === preferredVO) || - vo.types[0]; - setVoiceover({ - selected: selectedVO, - available: vo.types, - }); - }; - __getInfo(); - }, []); - - useEffect(() => { - const __getInfo = async () => { - const src = await _fetchSource(props.id, voiceover.selected.id); - 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(); - } - }, [voiceover.selected]); - - useEffect(() => { - const __getInfo = async () => { - const episodes = await _fetchEpisode( - props.id, - voiceover.selected.id, - source.selected.id - ); - - 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]; - - setEpisode({ - selected: selectedEpisode, - 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, - type: "hls", - }); - return; - } - if (source.selected.name == "Libria") { - const { manifest, poster } = await _fetchAnilibriaManifest( - episode.selected.url - ); - SetPlayerProps({ - src: manifest, - poster: poster, - useCustom: true, - type: "hls", - }); - return; - } - if (source.selected.name == "Sibnet") { - const { manifest, poster } = await _fetchSibnetManifest( - episode.selected.url - ); - SetPlayerProps({ - src: manifest, - poster: poster, - useCustom: true, - type: "mp4", - }); - return; - } - SetPlayerProps({ - src: episode.selected.url, - poster: null, - useCustom: false, - type: null, - }); - }; - if (episode.selected) { - __getInfo(); - } - }, [episode.selected]); - - return ( - - {( - !voiceover.selected || - !source.selected || - !episode.selected || - !playerProps.src - ) ? -
- -
- :
-
- - -
- {playerProps.useCustom ? - - {playerProps.type == "hls" ? - - : - } - - :