diff --git a/src/modules/anilist/anilistApi.ts b/src/modules/anilist/anilistApi.ts index 81cf79f2..5cb65d19 100644 --- a/src/modules/anilist/anilistApi.ts +++ b/src/modules/anilist/anilistApi.ts @@ -1,21 +1,20 @@ -import { app, ipcRenderer } from 'electron'; +import { app } from 'electron'; import Store from 'electron-store'; import { - AiringPage, - AiringScheduleData, AnimeData, CurrentListAnime, ListAnimeData, MostPopularAnime, TrendingAnime, } from '../../types/anilistAPITypes'; -import { MediaListStatus } from '../../types/anilistGraphQLTypes'; +import { AiringPage, AiringSchedule, Media, MediaListStatus} from '../../types/anilistGraphQLTypes'; import { ClientData } from '../../types/types'; import { clientData } from '../clientData'; import isAppImage from '../packaging/isAppImage'; import { getOptions, makeRequest } from '../requests'; + const STORE: any = new Store(); const CLIENT_DATA: ClientData = clientData; const PAGES: number = 20; @@ -28,6 +27,7 @@ const HEADERS: Object = { const MEDIA_DATA: string = ` id idMal + type title { romaji english @@ -82,6 +82,71 @@ const MEDIA_DATA: string = ` site thumbnail } + relations { + edges { + id + relationType(version: 2) + node { + id + idMal + type + title { + romaji + english + native + userPreferred + } + format + status + description + startDate { + year + month + day + } + endDate { + year + month + day + } + season + seasonYear + episodes + duration + coverImage { + large + extraLarge + color + } + bannerImage + genres + synonyms + averageScore + meanScore + popularity + favourites + isAdult + nextAiringEpisode { + id + timeUntilAiring + episode + } + mediaListEntry { + id + mediaId + status + score(format:POINT_10) + progress + } + siteUrl + trailer { + id + site + thumbnail + } + } + } + } `; /** @@ -313,7 +378,7 @@ export const getAnimesFromTitles = async (titles: string[]) => { * @param {*} animeId * @returns object with anime info */ -export const getAnimeInfo = async (animeId: any) => { +export const getAnimeInfo = async (animeId: any): Promise => { var query = ` query($id: Int) { Media(id: $id, type: ANIME) { @@ -335,7 +400,7 @@ export const getAnimeInfo = async (animeId: any) => { const options = getOptions(query, variables); const respData = await makeRequest(METHOD, GRAPH_QL_URL, headers, options); - return respData.data.Media; + return respData.data.Media as Media; }; /** @@ -441,7 +506,7 @@ export const getAiringSchedule = async ( const options = getOptions(query); const respData = await makeRequest(METHOD, GRAPH_QL_URL, headers, options); - return respData.data.Page.airingSchedules as AiringScheduleData[]; + return respData.data.Page.airingSchedules as AiringSchedule[]; }; /** diff --git a/src/modules/history.ts b/src/modules/history.ts index 3de8eec2..e443a312 100644 --- a/src/modules/history.ts +++ b/src/modules/history.ts @@ -28,6 +28,19 @@ export const getHistoryEntries = (): HistoryEntries => history.entries; */ export const getHistory = (): History => history; +/** + * Get history entry for a specific episode + * + * @param animeId + * @param episodeNumber + * @returns history entry + */ +export const getEpisodeHistory = ( + animeId: number, + episodeNumber: number +): EpisodeHistoryEntry | undefined => getAnimeHistory(animeId)?.history[episodeNumber] + + /** * Set local history. * diff --git a/src/modules/requests.ts b/src/modules/requests.ts index 6bfd022c..861fcf33 100644 --- a/src/modules/requests.ts +++ b/src/modules/requests.ts @@ -1,5 +1,35 @@ import axios from 'axios'; +var remainingRequests = 90; +var resetTime = 0; +var lockUntil = 0; + +const delay = async (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); + +const handleRateLimiting = async (current: number) => { + if (current < lockUntil) { + await delay(lockUntil - current); + } + + if (current >= resetTime) { + remainingRequests = 90; + } + + if (remainingRequests <= 0) { + await delay(60); + } +}; + +const handleResponseHeaders = (headers: any) => { + if (headers['x-ratelimit-remaining']) { + remainingRequests = parseInt(headers['x-ratelimit-remaining']); + } + + if (headers['x-ratelimit-reset']) { + resetTime = parseInt(headers['x-ratelimit-reset']); + } +}; + /** * Builds the data options for the request * @@ -24,12 +54,43 @@ export const getOptions = (query: any = {}, variables: any = {}) => { * @returns object with the fetched data * @throws error if the request was not successful */ + export const makeRequest = async ( method: 'GET' | 'POST' | string, url: string, headers: any = {}, options: any = {}, -) => { +): Promise => { + if (url === 'https://graphql.anilist.co') { + const current = Date.now() / 1000; + + await handleRateLimiting(current); + + try { + const response = await axios({ + method: method, + url: url, + headers: headers, + data: options, + }); + + handleResponseHeaders(response.headers); + + return response.data; + } catch (error) { + let response = (error as { response?: { status: number, headers: { [key: string]: any } } }).response; + + if (response && response.status === 429) { + const retryAfter = parseInt(response.headers['retry-after'] || '60', 10); + lockUntil = current + retryAfter; + await delay(retryAfter); + return makeRequest(method, url, headers, options); + } + + throw error; + } + } + const response = await axios({ method: method, url: url, diff --git a/src/modules/storeVariables.ts b/src/modules/storeVariables.ts index 2765c090..496af008 100644 --- a/src/modules/storeVariables.ts +++ b/src/modules/storeVariables.ts @@ -7,6 +7,7 @@ const defaultValues = { dubbed: false, source_flag: 'US', intro_skip_time: 85, + key_press_skip: 5, show_duration: true, trailer_volume_on: false, volume: 1, diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 0c6b2542..785a2e9d 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -1,6 +1,6 @@ -import { AiringScheduleData, AnimeData, ListAnimeData } from '../types/anilistAPITypes'; -import { Media, MediaFormat, MediaStatus } from '../types/anilistGraphQLTypes'; -import { getLastWatchedEpisode } from './history'; +import { AnimeData, ListAnimeData } from '../types/anilistAPITypes'; +import { AiringSchedule, Media, MediaFormat, MediaStatus, MediaTypes, Relation, RelationType, RelationTypes } from '../types/anilistGraphQLTypes'; +import { getAnimeHistory, getEpisodeHistory, getLastWatchedEpisode } from './history'; const MONTHS = { '1': 'January', @@ -46,18 +46,31 @@ export const getRandomDiscordPhrase = (): string => DISCORD_PHRASES[Math.floor(Math.random() * DISCORD_PHRASES.length)]; export const airingDataToListAnimeData = ( - airingScheduleData: AiringScheduleData[] + airingScheduleData: AiringSchedule[] ): ListAnimeData[] => { return airingScheduleData.map((value) => { return { id: null, mediaId: null, progress: null, - media: value.media + media: value.media as Media }; }); }; +export const relationsToListAnimeData = ( + relations: Relation[] +): ListAnimeData[] => { + return relations.map((value) => { + return { + id: null, + mediaId: null, + progress: null, + media: value.node + } + }) +} + export const animeDataToListAnimeData = ( animeData: AnimeData, ): ListAnimeData[] => { @@ -129,6 +142,43 @@ export const getEpisodes = (animeEntry: Media): number | null => : animeEntry.nextAiringEpisode.episode - 1 : animeEntry.episodes; +/** + * Get the sequel from the media. + * + * @param {*} animeEntry + * @returns sequel + */ +export const getSequel = (animeEntry: Media): Media | null => { + const relations = animeEntry.relations; + if (!relations) return null + + for(const relation of relations.edges) { + const sequel = relation.relationType === RelationTypes.Sequel && + relation.node.type === MediaTypes.Anime && + relation.node; + if (!sequel) + continue; + + return sequel; + } + + return null; + // const sequel: Relation = relations.edges.reduce((previous, value) => { + // const media = value.node; + + // console.log(value.relationType === RelationTypes.Sequel, media.type) + + // return value.relationType === RelationTypes.Sequel && + // media.type === MediaTypes.Anime && + // value || previous; + // }); + + // if (sequel.relationType !== "SEQUEL" && sequel.node.type !== MediaTypes.Anime) + // return null + + // return sequel.node; +} + /** * Gets the anime available episodes number from 'episodes' or 'nextAiringEpisode' * @@ -179,12 +229,27 @@ export const getProgress = (animeEntry: Media): number | undefined => { const animeId = (animeEntry.id || animeEntry?.mediaListEntry?.id) as number; const lastWatched = getLastWatchedEpisode(animeId); + const anilistProgress = (animeEntry.mediaListEntry === null ? 0 : animeEntry?.mediaListEntry?.progress) as number; + if(lastWatched !== undefined && lastWatched.data !== undefined) { - const progress = (lastWatched.data.episodeNumber as number) - 1; - return Number.isNaN(progress) ? 0 : progress; + let isFinished = (lastWatched.duration as number * 0.85) > lastWatched.time; + const localProgress = (parseInt(lastWatched.data.episode ?? "0")) - (isFinished ? 1 : 0); + console.log(anilistProgress, localProgress) + if(anilistProgress !== localProgress) { + const episodeEntry = getEpisodeHistory(animeId, anilistProgress); + + if(episodeEntry) { + isFinished = (episodeEntry.duration as number * 0.85) > episodeEntry.time; + return anilistProgress - (isFinished ? 1 : 0); + } + + return anilistProgress - 1; + } + + return Number.isNaN(localProgress) ? 0 : localProgress; } - return animeEntry.mediaListEntry == null ? 0 : animeEntry.mediaListEntry.progress; + return anilistProgress; } /** @@ -286,7 +351,7 @@ export const getParsedStatus = (status: MediaStatus | undefined) => { * @param {*} status * @returns */ -export const getParsedFormat = (format: MediaFormat | undefined) => { +export const getParsedFormat = (format: MediaFormat | RelationType | undefined) => { switch (format) { case 'TV': return 'TV Show'; @@ -302,6 +367,12 @@ export const getParsedFormat = (format: MediaFormat | undefined) => { return 'ONA'; case 'MUSIC': return 'Music'; + case 'SEQUEL': + return 'Sequel'; + case 'PREQUEL': + return 'Prequel'; + case 'ALTERNATIVE': + return 'Alternative'; default: return '?'; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 93a67617..a993efdc 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -9,19 +9,15 @@ import { SkeletonTheme } from 'react-loading-skeleton'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { - getAiringSchedule, getMostPopularAnime, getNextReleases, getTrendingAnime, getViewerId, getViewerInfo, getViewerList, - getAnimesFromTitles, - getAiredAnime } from '../modules/anilist/anilistApi'; -import { getRecentEpisodes } from '../modules/providers/gogoanime'; -import { airingDataToListAnimeData, animeDataToListAnimeData } from '../modules/utils'; +import { animeDataToListAnimeData } from '../modules/utils'; import { ListAnimeData, UserInfo } from '../types/anilistAPITypes'; import MainNavbar from './MainNavbar'; import Tab1 from './tabs/Tab1'; @@ -30,14 +26,13 @@ import Tab3 from './tabs/Tab3'; import Tab4 from './tabs/Tab4'; import { setDefaultStoreVariables } from '../modules/storeVariables'; -import { IpcRenderer, ipcRenderer, IpcRendererEvent } from 'electron'; +import { ipcRenderer, IpcRendererEvent } from 'electron'; import AutoUpdateModal from './components/modals/AutoUpdateModal'; import WindowControls from './WindowControls'; import { OS } from '../modules/os'; import DonateModal from './components/modals/DonateModal'; import { getHistoryEntries, getLastWatchedEpisode } from '../modules/history'; import Tab5 from './tabs/Tab5'; -import { AnimeHistoryEntry } from '../types/historyTypes'; ipcRenderer.on('console-log', (event, toPrint) => { console.log(toPrint); @@ -53,6 +48,7 @@ export default function App() { const [showUpdateModal, setShowUpdateModal] = useState(false); const [showDonateModal, setShowDonateModal] = useState(false); const [hasHistory, setHasHistory] = useState(false); + const [sectionUpdate, setSectionUpdate] = useState(0); // tab1 const [userInfo, setUserInfo] = useState(); @@ -96,8 +92,9 @@ export default function App() { const current = await getViewerList(id, 'CURRENT'); const rewatching = await getViewerList(id, 'REPEATING'); + const paused = await getViewerList(id, 'PAUSED'); - result = result.concat(current.concat(rewatching)); + result = result.concat(current.concat(rewatching).concat(paused)); } else if(historyAvailable) { setHasHistory(true); result = Object.values(entries).map((value) => value.data).sort(sortNewest); @@ -122,6 +119,9 @@ export default function App() { useEffect(() => { const updateSectionListener = async (event: IpcRendererEvent, ...sections: string[]) => { + const current = Date.now() / 1000; + if(current - sectionUpdate < 2) return; + setSectionUpdate(current); for(const section of sections) { switch(section) { case 'history': @@ -136,7 +136,7 @@ export default function App() { return () => { ipcRenderer.removeListener('update-section', updateSectionListener); }; - }); + }, [sectionUpdate]); ipcRenderer.on('auto-update', async () => { setShowDonateModal(false); diff --git a/src/renderer/components/AnimeEntry.tsx b/src/renderer/components/AnimeEntry.tsx index d5b9398d..0a00d244 100644 --- a/src/renderer/components/AnimeEntry.tsx +++ b/src/renderer/components/AnimeEntry.tsx @@ -97,7 +97,7 @@ const AnimeEntry: React.FC<{ )} -
+
{listAnimeData ? ( <> diff --git a/src/renderer/components/modals/AnimeModal.tsx b/src/renderer/components/modals/AnimeModal.tsx index a28d0849..3e8a3e72 100644 --- a/src/renderer/components/modals/AnimeModal.tsx +++ b/src/renderer/components/modals/AnimeModal.tsx @@ -26,6 +26,7 @@ import { getProgress, getTitle, getUrlByCoverType, + relationsToListAnimeData, } from '../../../modules/utils'; import { ListAnimeData } from '../../../types/anilistAPITypes'; import { EpisodeInfo } from '../../../types/types'; @@ -43,6 +44,11 @@ import EpisodesSection from './EpisodesSection'; import { ModalPage, ModalPageShadow } from './Modal'; import { ipcRenderer } from 'electron'; import { getLastWatchedEpisode } from '../../../modules/history'; +import { Media, MediaFormat, MediaTypes } from '../../../types/anilistGraphQLTypes'; +import AnimeSection from '../AnimeSection'; +import { Dots } from 'react-activity'; +import AnimeEntry from '../AnimeEntry'; +import { getAnimeInfo } from '../../../modules/anilist/anilistApi'; const modalsRoot = document.getElementById('modals-root'); const STORE = new Store(); @@ -81,9 +87,37 @@ const AnimeModal: React.FC = ({ const [localProgress, setLocalProgress] = useState(); const [alternativeBanner, setAlternativeBanner] = useState(); const [loading, setLoading] = useState(false); + const [relatedAnime, setRelatedAnime] = useState(); + + const getRelatedAnime = async () => { + if(listAnimeData.media?.relations === undefined) { + /* If you click on a related anime this'll run. */ + listAnimeData = { + id: null, + mediaId: null, + progress: null, + media: await getAnimeInfo(listAnimeData.media.id), + } + + setLocalProgress(getProgress(listAnimeData.media)); + } + const edges = listAnimeData.media.relations?.edges; + if(!edges) return; + + const list = edges.filter((value) => value.node.type === MediaTypes.Anime).map((value) => { + value.node.format = value.node.format === 'TV' ? value.relationType as MediaFormat : value.node.format; + + return value; + }); + + setRelatedAnime(relationsToListAnimeData(list)); + }; useEffect(() => { - if (show) fetchEpisodesInfo(); + if (show) { + fetchEpisodesInfo(); + getRelatedAnime(); + } }, [show]); useEffect(() => { @@ -125,9 +159,7 @@ const AnimeModal: React.FC = ({ const fetchEpisodesInfo = async () => { const animeId = listAnimeData.media.id as number; - const lastWatched = getLastWatchedEpisode(animeId); - if(lastWatched) - setLocalProgress((lastWatched.data.episodeNumber as number) - 1); + setLocalProgress(getProgress(listAnimeData.media)); axios .get(`${EPISODES_INFO_URL}${animeId}`) @@ -344,6 +376,14 @@ const AnimeModal: React.FC = ({ loading={loading} onPlay={playEpisode} /> + {relatedAnime && relatedAnime.length > 0 && +
+ +
+ }
diff --git a/src/renderer/components/modals/styles/AnimeModal.css b/src/renderer/components/modals/styles/AnimeModal.css index 88a4a91a..41c548ed 100644 --- a/src/renderer/components/modals/styles/AnimeModal.css +++ b/src/renderer/components/modals/styles/AnimeModal.css @@ -100,7 +100,7 @@ .anime-page .content-wrapper .content { display: flex; - gap: 50px; + gap: 10px; width: calc(100% - 50px); margin: 0 25px 25px 25px; border-radius: var(--border-radius); diff --git a/src/renderer/components/player/VideoPlayer.tsx b/src/renderer/components/player/VideoPlayer.tsx index c927e55d..37f96565 100644 --- a/src/renderer/components/player/VideoPlayer.tsx +++ b/src/renderer/components/player/VideoPlayer.tsx @@ -10,13 +10,16 @@ import ReactDOM from 'react-dom'; import toast, { Toaster } from 'react-hot-toast'; import { + getAnimeInfo, updateAnimeFromList, updateAnimeProgress, } from '../../../modules/anilist/anilistApi'; import { getUniversalEpisodeUrl } from '../../../modules/providers/api'; import { getAvailableEpisodes, + getMediaListId, getRandomDiscordPhrase, + getSequel, } from '../../../modules/utils'; import { ListAnimeData } from '../../../types/anilistAPITypes'; import { EpisodeInfo } from '../../../types/types'; @@ -26,7 +29,9 @@ import TopControls from './TopControls'; import { getAnimeHistory, setAnimeHistory } from '../../../modules/history'; import AniSkip from '../../../modules/aniskip'; import { SkipEvent } from '../../../types/aniskipTypes'; -import { skip } from 'node:test'; +import { getEnvironmentData } from 'node:worker_threads'; +import axios from 'axios'; +import { EPISODES_INFO_URL } from '../../../constants/utils'; const STORE = new Store(); const style = getComputedStyle(document.body); @@ -73,15 +78,17 @@ const VideoPlayer: React.FC = ({ const [episodeDescription, setEpisodeDescription] = useState(''); const [progressUpdated, setProgressUpdated] = useState(false); const [activity, setActivity] = useState(false); + const [listAnime, setListAnime] = useState(listAnimeData); + const [episodeList, setEpisodeList] = useState(episodesInfo); if (!activity && episodeTitle) { setActivity(true); ipcRenderer.send('update-presence', { - details: `Watching ${listAnimeData.media.title?.english}`, + details: `Watching ${listAnime.media.title?.english}`, state: episodeTitle, startTimestamp: Date.now(), - largeImageKey: listAnimeData.media.coverImage?.large || 'akuse', - largeImageText: listAnimeData.media.title?.english || 'akuse', + largeImageKey: listAnime.media.coverImage?.large || 'akuse', + largeImageText: listAnime.media.title?.english || 'akuse', smallImageKey: 'icon', buttons: [ { @@ -127,7 +134,7 @@ const VideoPlayer: React.FC = ({ } case 'ArrowLeft': { event.preventDefault(); - video.currentTime -= 5; + video.currentTime -= STORE.get('key_press_skip') as number; break; } case 'ArrowUp': { @@ -137,7 +144,7 @@ const VideoPlayer: React.FC = ({ } case 'ArrowRight': { event.preventDefault(); - video.currentTime += 5; + video.currentTime += STORE.get('key_press_skip') as number; break; } case 'ArrowDown': { @@ -229,7 +236,8 @@ const VideoPlayer: React.FC = ({ }, [loading]); const getSkipEvents = async (episode: number) => { - const skipEvent = await AniSkip.getSkipEvents(listAnimeData.media.idMal as number, episode ?? episodeNumber ?? animeEpisodeNumber); + const duration = videoRef.current?.duration; + const skipEvent = await AniSkip.getSkipEvents(listAnime.media.idMal as number, episode ?? episodeNumber ?? animeEpisodeNumber, Number.isNaN(duration) ? 0 : duration); setSkipEvents(skipEvent); } @@ -239,9 +247,9 @@ const VideoPlayer: React.FC = ({ playHlsVideo(video.url); // resume from tracked progress - const animeId = (listAnimeData.media.id || - (listAnimeData.media.mediaListEntry && - listAnimeData.media.mediaListEntry.id)) as number; + const animeId = (listAnime.media.id || + (listAnime.media.mediaListEntry && + listAnime.media.mediaListEntry.id)) as number; const animeHistory = getAnimeHistory(animeId); if (animeHistory !== undefined) { @@ -254,20 +262,20 @@ const VideoPlayer: React.FC = ({ setVideoData(video); setEpisodeNumber(animeEpisodeNumber); setEpisodeTitle( - episodesInfo - ? (episodesInfo[animeEpisodeNumber].title?.en ?? + episodeList + ? (episodeList[animeEpisodeNumber].title?.en ?? `Episode ${animeEpisodeNumber}`) : `Episode ${animeEpisodeNumber}`, ); setEpisodeDescription( - episodesInfo ? (episodesInfo[animeEpisodeNumber].summary ?? '') : '', + episodeList ? (episodeList[animeEpisodeNumber].summary ?? '') : '', ); setShowNextEpisodeButton(canNextEpisode(animeEpisodeNumber)); setShowPreviousEpisodeButton(canPreviousEpisode(animeEpisodeNumber)); - getSkipEvents(animeEpisodeNumber) + getSkipEvents(animeEpisodeNumber); } - }, [video, listAnimeData]); + }, [video, listAnime]); const playHlsVideo = (url: string) => { try { @@ -293,20 +301,20 @@ const VideoPlayer: React.FC = ({ const video = videoRef.current; const cTime = video?.currentTime; if (cTime === undefined) return; - const animeId = (listAnimeData.media.id || - (listAnimeData.media.mediaListEntry && - listAnimeData.media.mediaListEntry.id)) as number; - if (animeId === null || animeId === undefined) return; + const animeId = (listAnime.media.id || + (listAnime.media.mediaListEntry && + listAnime.media.mediaListEntry.id)) as number; + if (animeId === null || animeId === undefined || episodeNumber === 0) return; let entry = getAnimeHistory(animeId) ?? { history: {}, - data: listAnimeData, + data: listAnime, }; entry.history[episodeNumber] = { time: cTime, timestamp: Date.now(), duration: video?.duration, - data: (episodesInfo as EpisodeInfo[])[episodeNumber], + data: (episodeList as EpisodeInfo[])[episodeNumber], }; setAnimeHistory(entry); @@ -365,29 +373,39 @@ const VideoPlayer: React.FC = ({ }; const updateCurrentProgress = (completed: boolean = true) => { - const status = listAnimeData.media.mediaListEntry?.status; + const status = listAnime.media.mediaListEntry?.status; if (STORE.get('logged') as boolean) { - switch (status) { - case 'CURRENT': { - updateAnimeProgress(listAnimeData.media.id!, episodeNumber); - break; - } - case 'REPEATING': - case 'COMPLETED': { - updateAnimeFromList( - listAnimeData.media.id, - 'REWATCHING', - undefined, - episodeNumber, - ); - } - default: { - updateAnimeFromList( - listAnimeData.media.id, - 'CURRENT', - undefined, - episodeNumber, - ); + if(!completed) { + updateAnimeFromList( + listAnime.media.id, + 'PAUSED', + undefined, + episodeNumber, + ); + handleHistoryUpdate(); + } else { + switch (status) { + case 'CURRENT': { + updateAnimeProgress(listAnime.media.id!, episodeNumber); + break; + } + case 'REPEATING': + case 'COMPLETED': { + updateAnimeFromList( + listAnime.media.id, + 'REWATCHING', + undefined, + episodeNumber, + ); + } + default: { + updateAnimeFromList( + listAnime.media.id, + 'CURRENT', + undefined, + episodeNumber, + ); + } } } } @@ -411,7 +429,7 @@ const VideoPlayer: React.FC = ({ setCurrentTime(cTime); setDuration(dTime); setBuffered(videoRef.current?.buffered); - handleHistoryUpdate(); + // handleHistoryUpdate(); if ( (cTime * 100) / dTime > 85 && @@ -493,7 +511,8 @@ const VideoPlayer: React.FC = ({ } onClose(); - if (STORE.get('update_progress') as boolean) updateCurrentProgress(false); + if (STORE.get('update_progress')) + updateCurrentProgress((currentTime ?? 0) > (duration ?? 0) * 0.85); ipcRenderer.send('update-presence', { details: `🌸 Watch anime without ads.`, @@ -549,13 +568,48 @@ const VideoPlayer: React.FC = ({ } }; + const getEpisodeCount = () => { + const episodes = episodeList && Object.values(episodeList).filter((value) => + !Number.isNaN(parseInt(value.episode ?? '0'))); + + return episodes?.length ?? 0; + } + const changeEpisode = async ( episode: number | null, // null to play the current episode reloadAtPreviousTime?: boolean, ): Promise => { onChangeLoading(true); - const episodeToPlay = episode || episodeNumber; + const sequel = getSequel(listAnime.media); + const episodeCount = getEpisodeCount(); + + let episodeToPlay = episode || episodeNumber; + let episodes = episodeList; + let anime = listAnime; + + if(episodeCount < episodeToPlay && sequel) { + const animeId = sequel.id; + const media = await getAnimeInfo(sequel.id); + + anime = { + id: null, + mediaId: null, + progress: null, + media: media, + } + setListAnime(anime); + + episodeToPlay = 1; + animeEpisodeNumber = 1; + + const data = await axios.get(`${EPISODES_INFO_URL}${animeId}`); + + if (data.data && data.data.episodes) { + episodes = data.data.episodes + setEpisodeList(episodes); + } + } var previousTime = 0; if (reloadAtPreviousTime && videoRef.current) @@ -566,12 +620,12 @@ const VideoPlayer: React.FC = ({ setEpisodeNumber(episodeToPlay); getSkipEvents(episodeToPlay); setEpisodeTitle( - episodesInfo - ? (episodesInfo[episodeToPlay].title?.en ?? `Episode ${episode}`) - : `Episode ${episode}`, + episodes + ? (episodes[episodeToPlay].title?.en ?? `Episode ${episodeToPlay}`) + : `Episode ${episodeToPlay}`, ); setEpisodeDescription( - episodesInfo ? (episodesInfo[episodeToPlay].summary ?? '') : '', + episodes ? (episodes[episodeToPlay].summary ?? '') : '', ); playHlsVideo(value.url); // loadSource(value.url, value.isM3U8 ?? false); @@ -589,7 +643,7 @@ const VideoPlayer: React.FC = ({ onChangeLoading(false); }; - const data = await getUniversalEpisodeUrl(listAnimeData, episodeToPlay); + const data = await getUniversalEpisodeUrl(anime, episodeToPlay); if (!data) { toast(`Source not found.`, { style: { @@ -612,7 +666,16 @@ const VideoPlayer: React.FC = ({ }; const canNextEpisode = (episode: number): boolean => { - return episode !== getAvailableEpisodes(listAnimeData.media); + const hasNext = episode !== getAvailableEpisodes(listAnime.media); + + if(!hasNext) { + const sequel = getSequel(listAnime.media); + + if (!sequel) return false; + return getAvailableEpisodes(sequel) !== null; + } + + return hasNext; }; return ReactDOM.createPortal( @@ -628,7 +691,7 @@ const VideoPlayer: React.FC = ({

You are watching

- {listAnimeData.media.title?.english} + {listAnime.media.title?.english}

{episodeTitle}

{episodeDescription}

@@ -642,8 +705,8 @@ const VideoPlayer: React.FC = ({ = ({ const [selectedLanguage, setSelectedLanguage] = useState( STORE.get('source_flag') as string, ); - const [skipTime, setSkipTime] = useState( + const [introSkipTime, setIntroSkipTime] = useState( STORE.get('intro_skip_time') as number, ); + const [skipTime, setSkipTime] = useState( + STORE.get('key_press_skip') as number, + ); + const [isMuted, setIsMuted] = useState(false); const [volume, setVolume] = useState( @@ -112,9 +116,9 @@ const VideoSettings: React.FC = ({ setHlsData(hls); }, [hls]); - const handleQualityChange = (event: React.ChangeEvent) => { + const handleQualityChange = (index: number) => { if (hlsData) { - hlsData.currentLevel = parseInt(event.target.value); + hlsData.currentLevel = index; } }; @@ -188,8 +192,13 @@ const VideoSettings: React.FC = ({ } }; - const handleSkipTimeChange = (value: any) => { + const handleIntroSkipTimeChange = (value: any) => { STORE.set('intro_skip_time', parseInt(value)); + setIntroSkipTime(parseInt(value)); + }; + + const handleSkipTimeChange = (value: any) => { + STORE.set('key_press_skip', parseInt(value)); setSkipTime(parseInt(value)); }; @@ -213,7 +222,7 @@ const VideoSettings: React.FC = ({ {hlsData && ( = ({ width={100} /> +
  • + + + Skip Time + +