From 22aa77502bd7713ee89751963b9687b3fc6fbbd5 Mon Sep 17 00:00:00 2001 From: Jack <72616658+synthofficial@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:22:43 +0100 Subject: [PATCH] Fixed bugs and added resume episode Added a feature to resume an episode from where you left off. Fixed a bug with the media not loading from the continue watching section. --- release/app/package-lock.json | 4 +- release/app/package.json | 2 +- src/components/TitleBar.tsx | 9 ++-- src/components/VideoPlayer.tsx | 11 ++-- src/main/main.ts | 4 +- src/modules/api/Anime.ts | 56 +++++++++++++++++++- src/modules/api/Movies.ts | 41 ++++++++------ src/modules/functions.ts | 14 +++++ src/modules/interfaces/WatchlistItem.ts | 2 + src/renderer/pages/modals/MediaModal.tsx | 37 ++++++++++--- src/renderer/pages/tabs/ContinueWatching.tsx | 50 ++++++++--------- 11 files changed, 167 insertions(+), 63 deletions(-) diff --git a/release/app/package-lock.json b/release/app/package-lock.json index 46edfe3..9b12967 100644 --- a/release/app/package-lock.json +++ b/release/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "streamflix", - "version": "1.3.2", + "version": "1.3.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "streamflix", - "version": "1.3.2", + "version": "1.3.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/release/app/package.json b/release/app/package.json index 0673ae4..3cb46f0 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -1,6 +1,6 @@ { "name": "streamflix", - "version": "1.3.2", + "version": "1.3.3", "description": "A desktop app for streaming movies, tv series and anime.", "license": "MIT", "author": { diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index 1f15cdc..6218c23 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -27,29 +27,32 @@ const TitleBar: React.FC = () => { bg={"dark.200"} className="draggable overflow-hidden" > - + } size="sm" variant="ghost" color={iconColor} cursor={'pointer'} - _hover={{ bg: 'gray.700' }} + _hover={{ bg: 'dark.300' }} onClick={handleMinimize} /> } size="sm" variant="ghost" color={iconColor} cursor={'pointer'} - _hover={{ bg: 'gray.700' }} + _hover={{ bg: 'dark.300' }} onClick={handleMaximize} /> } size="sm" variant="ghost" diff --git a/src/components/VideoPlayer.tsx b/src/components/VideoPlayer.tsx index 098f52f..3099a66 100644 --- a/src/components/VideoPlayer.tsx +++ b/src/components/VideoPlayer.tsx @@ -9,7 +9,7 @@ import { getEpisodeSource } from "../modules/api/Movies"; import discordRPCManager from "../constants/DiscordRPC"; import { useVideoPlayer } from "../renderer/contexts/VideoPlayerContext"; import { getAnimeSource } from "../modules/api/Anime"; -import { getWatchingList, isInWatchingList, isInWatchlist, updateWatchingListMovieTime, updateWatchlistMovieTime } from "../modules/functions"; +import { convertDurationToSeconds, getWatchingList, isInWatchingList, isInWatchlist, updateEpisodeWatchTime, updateWatchingListMovieTime, updateWatchlistMovieTime } from "../modules/functions"; const convertSecondsToTime = (seconds: number) => { const hours = Math.floor(seconds / 3600); @@ -30,8 +30,6 @@ const VideoPlayer: React.FC = () => { setCurrentUrl } = useVideoPlayer(); - console.log(movieData, episodeData, episodeUrl); - const videoRef = useRef(null); const [hlsData, setHlsData] = useState(); const [episodeDetails, setEpisodeDetails] = useState(episodeData); @@ -130,6 +128,7 @@ const VideoPlayer: React.FC = () => { const onTimeUpdate = () => { setCurrentTime(convertSecondsToTime(video.currentTime)); + if(isInWatchingList(movieData?.id as string)){ updateWatchingListMovieTime(movieData?.id as string, video.currentTime); } @@ -156,9 +155,9 @@ const VideoPlayer: React.FC = () => { const handlePlayerClose = () => { setShowPlayer(false); - setCurrentMedia(null); - setCurrentEpisode(null); - setCurrentUrl(null); + if(movieData?.type === "Show"){ + updateEpisodeWatchTime(movieData?.id as string, episodeData?.id, Math.floor(videoRef.current!.currentTime), convertDurationToSeconds(movieData?.duration) as number); + } }; const handleFullscreen = () => { diff --git a/src/main/main.ts b/src/main/main.ts index 2552c2a..a753548 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -71,8 +71,8 @@ const createWindow = async () => { mainWindow = new BrowserWindow({ show: false, - width: 1024, - height: 728, + width: 1920, + height: 1080, icon: getAssetPath('icon.png'), autoHideMenuBar: true, titleBarStyle: "hidden", diff --git a/src/modules/api/Anime.ts b/src/modules/api/Anime.ts index bc8890c..832caee 100644 --- a/src/modules/api/Anime.ts +++ b/src/modules/api/Anime.ts @@ -6,7 +6,6 @@ import { fetchAnimeOrShowById } from "./Anilist"; import { ANIME_BASE_URL } from "../../constants/API"; const api = new Anilist(); -const sources = new Gogoanime(); export const getAnimeSource = async (episodeId: string) => { console.log("lets get source for episode: " + episodeId); @@ -22,6 +21,61 @@ export const fetchEpisodeInfo = async (id: string) => { return data; } +export const searchAnime = async (query: string) => { + const response = await api.search(query, 1); + + const data: Anime[] = await Promise.all( + response.results[0].map(async (anime : any) => { + // Fetch basic anime data + const animeData = await api.fetchAnimeInfo(anime.id); + console.log(animeData) + // Fetch detailed episode information + const anilistData = await fetchEpisodeInfo(anime.id); + // Extract the title object + const title: ITitle = animeData.title as ITitle; + + // Combine episode data from both sources + const combinedEpisodes = animeData.episodes?.map((ep: any, index: number) => { + const anilistEp = anilistData.episodes[index + 1]; + return { + id: ep.id, + absoluteEpisodeNumber: index, + number: ep.number, + title: anilistEp?.title?.en, + description: anilistEp?.overview, + image: anilistEp?.image || '', + releaseDate: anilistEp?.airDate || '', + }; + }) || []; + + // Return an object conforming to the Anime type + return { + title: { + english: title.english as string, + }, + episodes: combinedEpisodes, + id: animeData.id as string, + production: animeData.studios![0] as string, + genres: animeData.genres as string[], + description: animeData.description?.replaceAll("
", "").replaceAll("
", "") as string, + duration: `${animeData.duration} min`, + totalEpisodes: animeData.totalEpisodes as number, + hasSub: animeData.hasSub as boolean, + hasDub: animeData.hasDub as boolean, + isAdult: animeData.isAdult as boolean, + season: animeData.season as string, + cover: animeData.cover as string, + trailer: animeData.trailer?.site as string, + releaseDate: `${animeData.startDate?.year}-${animeData.startDate?.month}-${animeData.startDate?.day}`, + endDate: `${animeData.endDate?.year}-${animeData.endDate?.month}-${animeData.endDate?.day}`, + thumbnail: animeData.image as string, + rating: animeData.rating as number, + status: animeData.status as string, + type: "Anime", + }; + }) + ); +} export const getTrendingAnime = async(): Promise => { // Fetch popular anime diff --git a/src/modules/api/Movies.ts b/src/modules/api/Movies.ts index 696daa2..a21b103 100644 --- a/src/modules/api/Movies.ts +++ b/src/modules/api/Movies.ts @@ -5,6 +5,8 @@ import Goku from "@consumet/extensions/dist/providers/movies/goku"; import axios from "axios"; import { IMovieResult, ISearch } from "@consumet/extensions"; +import { fetchAnimeOrShowById } from "./Anilist"; +import { searchAnime } from "./Anime"; const api = new MovieHdWatch(); const flix = new FlixHQ(); @@ -109,23 +111,28 @@ export const searchMedia = async (query: string): Promise => { let episodes : any = []; if(type == "Show"){ - const tmdbData = await fetchTVSeriesDetails(data.title as string); - const epData = await fetchEpisodeDetails(tmdbData.id); - - episodes = await Promise.all( - await epData.map(async(episode : any) => { - if(!episode.id) return; - const absoluteEpisodeId = data.episodes!.find((episodeData : any) => episodeData.number === episode.number && episodeData.season === episode.season); - return { - id: absoluteEpisodeId?.id, - title: episode.name, - number: episode.number, - season: episode.season, - thumbnail: episode.image, - description: episode.summary, - } - }) - ) + if(data.genres?.includes("Animation") && data.country == "Japan"){ + const animeData = await searchAnime(result.title as string); + console.log(animeData); + }else{ + const tmdbData = await fetchTVSeriesDetails(data.title as string); + const epData = await fetchEpisodeDetails(tmdbData.id); + + episodes = await Promise.all( + await epData.map(async(episode : any) => { + if(!episode.id) return; + const absoluteEpisodeId = data.episodes!.find((episodeData : any) => episodeData.number === episode.number && episodeData.season === episode.season); + return { + id: absoluteEpisodeId?.id, + title: episode.name, + number: episode.number, + season: episode.season, + thumbnail: episode.image, + description: episode.summary, + } + }) + ) + } } return { diff --git a/src/modules/functions.ts b/src/modules/functions.ts index 1153a82..9c42cf7 100644 --- a/src/modules/functions.ts +++ b/src/modules/functions.ts @@ -65,6 +65,20 @@ export const addToWatchingList = (item : WatchlistItem) => { } } +export const updateEpisodeWatchTime = (id : string, episodeId : string, time : number, finishTime : number) => { + const watchlist = getWatchingList(); + const index = watchlist.findIndex(item => item.id === id); + if(index !== -1){ + const episodeIndex : number = watchlist[index].episodes?.findIndex(ep => ep.id === episodeId)!; + if(episodeIndex !== -1){ + console.log(time, finishTime); + watchlist[index].episodes![episodeIndex].timestamp = time; + watchlist[index].episodes![episodeIndex].completed = time >= finishTime ? true : false; + localStorage.setItem("watchinglist", JSON.stringify(watchlist)); + } + } +} + export const updateWatchingListMovieTime = (id : string, time : number) => { const watchlist = getWatchingList(); const index = watchlist.findIndex(item => item.id === id); diff --git a/src/modules/interfaces/WatchlistItem.ts b/src/modules/interfaces/WatchlistItem.ts index 1b27675..1b03bd1 100644 --- a/src/modules/interfaces/WatchlistItem.ts +++ b/src/modules/interfaces/WatchlistItem.ts @@ -16,6 +16,8 @@ interface WatchlistItem { description?: string; thumbnail?: string; url?: string; + completed? : boolean; + timestamp? : number; }[]; rating : number; production : string; diff --git a/src/renderer/pages/modals/MediaModal.tsx b/src/renderer/pages/modals/MediaModal.tsx index 7919235..1209c64 100644 --- a/src/renderer/pages/modals/MediaModal.tsx +++ b/src/renderer/pages/modals/MediaModal.tsx @@ -20,10 +20,11 @@ import { MenuButton, MenuList, MenuItem, + Progress, } from '@chakra-ui/react'; import { FaPlay, FaStar, FaClock, FaCalendar } from 'react-icons/fa'; import { IoIosArrowDown } from 'react-icons/io'; -import { convertMinutesToHours, getWatchingList } from '../../../modules/functions'; +import { convertDurationToSeconds, convertMinutesToHours, getWatchingList, getWatchlist } from '../../../modules/functions'; import { getEpisodeSource } from '../../../modules/api/Movies'; import { getAnimeSource } from '../../../modules/api/Anime'; @@ -47,6 +48,7 @@ const MediaModal: React.FC = ({ isOpen, onClose, media, onPlayC if (!media) return null; const [selectedRange, setSelectedRange] = useState(0); const [selectedSeason, setSelectedSeason] = useState(1); + const [watchList, setWatchlist] = useState(getWatchlist()); const [episodes, setEpisodes] = useState([]); const toast = useToast(); @@ -151,9 +153,9 @@ const MediaModal: React.FC = ({ isOpen, onClose, media, onPlayC }; return ( - + - + = ({ isOpen, onClose, media, onPlayC {isAnime @@ -290,8 +292,14 @@ const MediaModal: React.FC = ({ isOpen, onClose, media, onPlayC borderRadius: '24px', }, }}> - {filteredEpisodes.map((episode, index) => ( - { + const watchedEpisode = watchingList.find((item) => item.id === media.id)?.episodes?.find((ep) => ep.id === episode.id); + const watchedProgress = watchedEpisode ? watchedEpisode.timestamp : 0; + const totalDuration = convertDurationToSeconds(episode.duration || media.duration); + + console.log(`Episode ${episode.number} progress:`, watchedProgress, 'out of', totalDuration); + return( + = ({ isOpen, onClose, media, onPlayC height="100%" borderRadius="md" /> + = ({ isOpen, onClose, media, onPlayC - ))} + ) + })} diff --git a/src/renderer/pages/tabs/ContinueWatching.tsx b/src/renderer/pages/tabs/ContinueWatching.tsx index c366972..81dcd48 100644 --- a/src/renderer/pages/tabs/ContinueWatching.tsx +++ b/src/renderer/pages/tabs/ContinueWatching.tsx @@ -9,14 +9,13 @@ import MediaModal from "../modals/MediaModal"; import { FaCalendar, FaCheck, FaClock, FaStar } from "react-icons/fa"; interface Props { - onPlayClick: (media: Movie | Show | Anime) => void; + onPlayClick: (media: Movie | Show | Anime, episodeData?: any, episodeUrl?: string) => void; } const ContinueWatching: React.FC = ({ onPlayClick }) => { const [watchingList, setWatchingList] = useState(getWatchingList()); console.log(watchingList) const [selectedMedia, setSelectedMedia] = useState(null); - const [isLoading, setIsLoading] = useState(true); const { isOpen, onOpen, onClose } = useDisclosure(); const toast = useToast(); @@ -34,12 +33,13 @@ const ContinueWatching: React.FC = ({ onPlayClick }) => { onClose(); } - const handlePlayClick = () => { + const handlePlayClick = (selectedMedia?: any, episode?: any, episodeUrl?: any) => { onClose(); + console.log(episodeUrl); if (selectedMedia) { - onPlayClick(selectedMedia); + onPlayClick(selectedMedia, episode, episodeUrl); } - }; + } const handleMarkAsComplete = (e: React.MouseEvent, media: any) => { e.stopPropagation(); @@ -157,20 +157,22 @@ const ContinueWatching: React.FC = ({ onPlayClick }) => { - + {movie.type == "Movie" && ( + + )} ) @@ -179,11 +181,11 @@ const ContinueWatching: React.FC = ({ onPlayClick }) => { + isOpen={!!selectedMedia} + onClose={handleCloseModal} + media={selectedMedia!} + onPlayClick={handlePlayClick} + /> ); }