From 25773d372b5e9a174a3f800213340f8ab9ec8a01 Mon Sep 17 00:00:00 2001 From: Ben Sarmiento Date: Tue, 29 Aug 2023 22:30:56 +0200 Subject: [PATCH] fix stuffs --- next.config.js | 2 +- package.json | 2 +- src/components/poster.tsx | 44 +- src/pages/api/moviesearch.ts | 2 +- src/pages/api/tvsearch.ts | 2 +- src/pages/movies/[imdbid].tsx | 263 +++++------ src/pages/search-v2.tsx | 75 +-- src/pages/shows/[imdbid]/[seasonNum].tsx | 568 +++++++++++++++++++++++ 8 files changed, 773 insertions(+), 185 deletions(-) create mode 100644 src/pages/shows/[imdbid]/[seasonNum].tsx diff --git a/next.config.js b/next.config.js index 42d4585..fe1ee33 100644 --- a/next.config.js +++ b/next.config.js @@ -6,7 +6,7 @@ const nextConfig = { protocol: 'https', hostname: 'image.tmdb.org', port: '', - pathname: '/t/p/w200/**', + pathname: '/**', }, { protocol: 'https', diff --git a/package.json b/package.json index 431591c..784e8de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "debrid-media-manager", - "version": "2.7.0", + "version": "2.7.1", "private": false, "scripts": { "dev": "next dev", diff --git a/src/components/poster.tsx b/src/components/poster.tsx index 825702c..6b1a91f 100644 --- a/src/components/poster.tsx +++ b/src/components/poster.tsx @@ -1,38 +1,52 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import axios from 'axios'; import Image from 'next/image'; import { getTmdbKey } from '@/utils/freekeys'; -const TMDBPoster = ({ imdbId }: Record) => { +const Poster = ({ imdbId }: Record) => { const [posterUrl, setPosterUrl] = useState(''); + const [imgLoaded, setImgLoaded] = useState(false); + const imgRef = useRef(null); useEffect(() => { const fetchData = async () => { const response = await axios.get(`https://api.themoviedb.org/3/find/${imdbId}?api_key=${getTmdbKey()}&external_source=imdb_id`); const baseUrl = 'https://image.tmdb.org/t/p/w200'; - if (response.data.movie_results.length > 0 && response.data.movie_results[0].poster_path) { - setPosterUrl(baseUrl + response.data.movie_results[0].poster_path); - } if (response.data.tv_results.length > 0 && response.data.tv_results[0].poster_path) { - setPosterUrl(baseUrl + response.data.tv_results[0].poster_path); - } else { - // If no poster_path, set a placeholder image URL - setPosterUrl(`https://picsum.photos/seed/${imdbId}/200/300`); - } + const posterPath = response.data.movie_results[0]?.poster_path || response.data.tv_results[0]?.poster_path; + if (!posterPath) setPosterUrl(`https://picsum.photos/seed/${imdbId}/200/300`); + else setPosterUrl(`${baseUrl}${posterPath}`); }; + const imgObserver = new IntersectionObserver((entries, observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + fetchData(); + setImgLoaded(true); + observer.disconnect(); + } + }); + }); + try { - if (imdbId) fetchData(); - else setPosterUrl(`https://picsum.photos/seed/${imdbId}/200/300`); + if (imdbId && imgRef.current) { + imgObserver.observe(imgRef.current); + } else { + setPosterUrl(`https://picsum.photos/seed/${imdbId}/200/300`); + } } catch (error: any) { setPosterUrl(`https://picsum.photos/seed/${imdbId}/200/300`); } + + return () => { + imgObserver.disconnect(); + }; }, [imdbId]); return ( -
- {posterUrl && Movie poster} +
+ {imgLoaded && posterUrl && Movie poster}
); }; -export default TMDBPoster; +export default Poster; diff --git a/src/pages/api/moviesearch.ts b/src/pages/api/moviesearch.ts index d3a04f7..2c96e28 100644 --- a/src/pages/api/moviesearch.ts +++ b/src/pages/api/moviesearch.ts @@ -18,7 +18,7 @@ const handler: NextApiHandler = async (req, res) => { return; } - res.status(204).json({ results: [] }); + res.status(204).json(null); } catch (error: any) { console.error('encountered a db issue', error); res.status(500).json({ errorMessage: error.message }); diff --git a/src/pages/api/tvsearch.ts b/src/pages/api/tvsearch.ts index 7cbda47..bf002d9 100644 --- a/src/pages/api/tvsearch.ts +++ b/src/pages/api/tvsearch.ts @@ -26,7 +26,7 @@ const handler: NextApiHandler = async (req, res) => { return; } - res.status(204).json({ results: [] }); + res.status(204).json(null); } catch (error: any) { console.error('encountered a db issue', error); res.status(500).json({ errorMessage: error.message }); diff --git a/src/pages/movies/[imdbid].tsx b/src/pages/movies/[imdbid].tsx index ef556b1..b4fc30a 100644 --- a/src/pages/movies/[imdbid].tsx +++ b/src/pages/movies/[imdbid].tsx @@ -29,7 +29,11 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import toast, { Toaster } from 'react-hot-toast'; import { FaDownload, FaFastForward, FaTimes } from 'react-icons/fa'; -import { SearchApiResponse } from '../api/search'; + +export type SearchApiResponse = { + results?: SearchResult[]; + errorMessage?: string; +}; type SearchResult = { title: string; @@ -167,7 +171,7 @@ function Search() { } available in RD`, { icon: '🔍' } ); - setMasterAvailability({ ...masterAvailability, ...rdAvailability }); + setMasterAvailability(prevState => ({ ...prevState, ...rdAvailability })); } catch (error) { toast.error( 'There was an error checking availability in Real-Debrid. Please try again.' @@ -212,7 +216,7 @@ function Search() { } available in AD`, { icon: '🔍' } ); - setMasterAvailability({ ...masterAvailability, ...adAvailability }); + setMasterAvailability(prevState => ({ ...prevState, ...adAvailability })); } catch (error) { toast.error('There was an error checking availability in AllDebrid. Please try again.'); throw error; @@ -312,16 +316,13 @@ function Search() {
- Debrid Media Manager - Movie - {movieInfo?.movie_results[0].title} ( - {movieInfo?.movie_results[0].release_date.substring(0, 4)}) + Debrid Media Manager - Movie - {movieInfo?.movie_results[0].title} + {movieInfo?.movie_results[0].release_date.substring(0, 4)}
-

- {movieInfo?.movie_results[0].title}{' '} - {movieInfo?.movie_results[0].release_date.substring(0, 4)} -

+

🎥

- {!loading && - searchResults.map((r: SearchResult) => ( -
-
-

- {r.title} -

-

- Size: {(r.fileSize / 1024).toFixed(2)} GB -

-
- {rd.isDownloading(r.hash) && - rdCache![r.hash].id && ( - - )} - {rdKey && rd.notInLibrary(r.hash) && ( - - )} - {ad.isDownloading(r.hash) && - adCache![r.hash].id && ( - - )} - {adKey && ad.notInLibrary(r.hash) && ( - - )} -
-
-
- ))} +{ + !loading && searchResults.map((r: SearchResult) => ( +
+
+

+ {r.title} +

+

+ Size: {(r.fileSize / 1024).toFixed(2)} GB +

+
+ {rd.isDownloading(r.hash) && rdCache![r.hash].id && ( + + )} + {rdKey && rd.notInLibrary(r.hash) && ( + + )} + {ad.isDownloading(r.hash) && adCache![r.hash].id && ( + + )} + {adKey && ad.notInLibrary(r.hash) && ( + + )} +
+ +
+
+ )) +}
diff --git a/src/pages/search-v2.tsx b/src/pages/search-v2.tsx index cfe2420..3184b28 100644 --- a/src/pages/search-v2.tsx +++ b/src/pages/search-v2.tsx @@ -1,4 +1,4 @@ -import TMDBPoster from '@/components/poster'; +import Poster from '@/components/poster'; import { withAuth } from '@/utils/withAuth'; import Head from 'next/head'; import Link from 'next/link'; @@ -113,36 +113,51 @@ function Search() {

Search Results for "{query}"

-
+
{searchResults.map((result) => ( -
- -

{result.title}

-

Year: {result.year}

-

Score: {result.score}

- {result.type === 'movie' ? ( - - View More - - ) : ( - <> - {Array.from( - { length: result.season_count || 0 }, - (_, i) => i + 1 - ).map((season) => ( - - View Season {season} - - ))} - - )} +
+ +
+

{result.title}

+

Year: {result.year}

+

Score: {result.score}

+ {result.type === 'movie' ? ( + + + 🎥 + {' '} + View + + ) : ( + <> + {Array.from( + { length: result.season_count || 0 }, + (_, i) => i + 1 + ).map((season) => ( + + + 📺 + {' '} + Season {season} + + ))} + + )} +
))}
diff --git a/src/pages/shows/[imdbid]/[seasonNum].tsx b/src/pages/shows/[imdbid]/[seasonNum].tsx new file mode 100644 index 0000000..799877e --- /dev/null +++ b/src/pages/shows/[imdbid]/[seasonNum].tsx @@ -0,0 +1,568 @@ +import { useAllDebridApiKey, useRealDebridAccessToken } from '@/hooks/auth'; +import { useDownloadsCache } from '@/hooks/cache'; +import useLocalStorage from '@/hooks/localStorage'; +import { + AdInstantAvailabilityResponse, + MagnetFile, + adInstantCheck, + deleteMagnet, + uploadMagnet, +} from '@/services/allDebrid'; +import { Availability, HashAvailability } from '@/services/availability'; +import { + RdInstantAvailabilityResponse, + addHashAsMagnet, + deleteTorrent, + getTorrentInfo, + rdInstantCheck, + selectFiles, +} from '@/services/realDebrid'; +import { getTmdbKey } from '@/utils/freekeys'; +import { groupBy } from '@/utils/groupBy'; +import { getSelectableFiles, isVideo } from '@/utils/selectable'; +import { withAuth } from '@/utils/withAuth'; +import axios from 'axios'; +import Head from 'next/head'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import toast, { Toaster } from 'react-hot-toast'; +import { FaDownload, FaFastForward, FaTimes } from 'react-icons/fa'; + +export type SearchApiResponse = { + results?: SearchResult[]; + errorMessage?: string; +}; + +type SearchResult = { + title: string; + fileSize: number; + hash: string; + available: Availability; +}; + +type TmdbResponse = { + tv_results: { + name: string; + overview: string; + first_air_date: string; + poster_path: string; + }[]; +}; + +function Search() { + const [searchResults, setSearchResults] = useState([]); + const [errorMessage, setErrorMessage] = useState(''); + const rdKey = useRealDebridAccessToken(); + const adKey = useAllDebridApiKey(); + const [loading, setLoading] = useState(false); + const [masterAvailability, setMasterAvailability] = useState({}); + const [rdCache, rd, rdCacheAdder, removeFromRdCache] = useDownloadsCache('rd'); + const [adCache, ad, adCacheAdder, removeFromAdCache] = useDownloadsCache('ad'); + const [rdAutoInstantCheck, setRdAutoInstantCheck] = useLocalStorage( + 'rdAutoInstantCheck', + false + ); + const [adAutoInstantCheck, setAdAutoInstantCheck] = useLocalStorage( + 'adAutoInstantCheck', + false + ); + const [showInfo, setShowInfo] = useState(null); + + const router = useRouter(); + const { imdbid, seasonNum } = router.query; + + const fetchShowInfo = async (imdbId: string) => { + try { + const response = await axios.get( + `https://api.themoviedb.org/3/find/${imdbId}?api_key=${getTmdbKey()}&external_source=imdb_id` + ); + setShowInfo(response.data); + } catch (error) { + setErrorMessage('There was an error fetching movie info. Please try again.'); + console.error(`error fetching movie data`, error); + } + }; + + useEffect(() => { + if (imdbid && seasonNum) { + fetchShowInfo(imdbid as string); + fetchData(imdbid as string, parseInt(seasonNum as string)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [imdbid, seasonNum]); + + const fetchData = async (imdbId: string, seasonNum: number) => { + setSearchResults([]); + setErrorMessage(''); + setLoading(true); + try { + let endpoint = `/api/tvsearch?imdbId=${encodeURIComponent( + imdbId + )}&seasonNum=${seasonNum}`; + const response = await axios.get(endpoint); + + setSearchResults( + response.data.results?.map((r) => ({ + ...r, + available: 'unavailable', + })) || [] + ); + + if (response.data.results?.length) { + toast(`Found ${response.data.results.length} results`, { icon: '🔍' }); + + // instant checks + const hashArr = response.data.results.map((r) => r.hash); + if (rdKey && rdAutoInstantCheck) await instantCheckInRd(hashArr); + if (adKey && adAutoInstantCheck) await instantCheckInAd(hashArr); + } else { + toast(`No results found`, { icon: '🔍' }); + } + } catch (error) { + console.error(error); + setErrorMessage('There was an error searching for the query. Please try again.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + setSearchResults((prev) => + prev.map((r) => ({ + ...r, + available: masterAvailability[r.hash], + })) + ); + }, [masterAvailability]); + + const instantCheckInRd = async (hashes: string[]): Promise => { + const rdAvailability = {} as HashAvailability; + + const setInstantFromRd = (resp: RdInstantAvailabilityResponse) => { + for (const masterHash in resp) { + if ('rd' in resp[masterHash] === false) continue; + if (masterAvailability[masterHash] === 'no_videos') continue; + const variants = resp[masterHash]['rd']; + if (!variants.length) rdAvailability[masterHash] = 'no_videos'; + for (const variant of variants) { + for (const fileId in variant) { + const file = variant[fileId]; + if (isVideo({ path: file.filename })) { + rdAvailability[masterHash] = + masterAvailability[masterHash] === 'ad:available' || + masterAvailability[masterHash] === 'all:available' + ? 'all:available' + : 'rd:available'; + + break; + } + } + } + } + }; + + try { + for (const hashGroup of groupBy(100, hashes)) { + if (rdKey) await rdInstantCheck(rdKey, hashGroup).then(setInstantFromRd); + } + toast( + `Found ${ + Object.values(rdAvailability).filter((a) => a.includes(':available')).length + } available in RD`, + { icon: '🔍' } + ); + setMasterAvailability(prevState => ({ ...prevState, ...rdAvailability })); + } catch (error) { + toast.error( + 'There was an error checking availability in Real-Debrid. Please try again.' + ); + throw error; + } + }; + + const instantCheckInAd = async (hashes: string[]): Promise => { + const adAvailability = {} as HashAvailability; + + const setInstantFromAd = (resp: AdInstantAvailabilityResponse) => { + for (const magnetData of resp.data.magnets) { + const masterHash = magnetData.hash; + if (masterAvailability[masterHash] === 'no_videos') continue; + if (magnetData.instant) { + adAvailability[masterHash] = magnetData.files?.reduce( + (acc: boolean, curr: MagnetFile) => { + if (isVideo({ path: curr.n })) { + return true; + } + return acc; + }, + false + ) + ? masterAvailability[masterHash] === 'rd:available' || + masterAvailability[masterHash] === 'all:available' + ? 'all:available' + : 'ad:available' + : 'no_videos'; + } + } + }; + + try { + for (const hashGroup of groupBy(30, hashes)) { + if (adKey) await adInstantCheck(adKey, hashGroup).then(setInstantFromAd); + } + toast( + `Found ${ + Object.values(adAvailability).filter((a) => a.includes(':available')).length + } available in AD`, + { icon: '🔍' } + ); + setMasterAvailability(prevState => ({ ...prevState, ...adAvailability })); + } catch (error) { + toast.error('There was an error checking availability in AllDebrid. Please try again.'); + throw error; + } + }; + + const handleAddAsMagnetInRd = async ( + hash: string, + instantDownload: boolean = false, + disableToast: boolean = false + ) => { + try { + if (!rdKey) throw new Error('no_rd_key'); + const id = await addHashAsMagnet(rdKey, hash); + if (!disableToast) toast.success('Successfully added as magnet!'); + rdCacheAdder.single(`rd:${id}`, hash, instantDownload ? 'downloaded' : 'downloading'); + handleSelectFiles(`rd:${id}`, true); // add rd: to account for substr(3) in handleSelectFiles + } catch (error) { + if (!disableToast) + toast.error('There was an error adding as magnet. Please try again.'); + throw error; + } + }; + + const handleAddAsMagnetInAd = async ( + hash: string, + instantDownload: boolean = false, + disableToast: boolean = false + ) => { + try { + if (!adKey) throw new Error('no_ad_key'); + const resp = await uploadMagnet(adKey, [hash]); + if (resp.data.magnets.length === 0 || resp.data.magnets[0].error) + throw new Error('no_magnets'); + if (!disableToast) toast.success('Successfully added as magnet!'); + adCacheAdder.single( + `ad:${resp.data.magnets[0].id}`, + hash, + instantDownload ? 'downloaded' : 'downloading' + ); + } catch (error) { + if (!disableToast) + toast.error('There was an error adding as magnet. Please try again.'); + throw error; + } + }; + + const isAvailableInRd = (result: SearchResult) => + result.available === 'rd:available' || result.available === 'all:available'; + const isAvailableInAd = (result: SearchResult) => + result.available === 'ad:available' || result.available === 'all:available'; + const hasNoVideos = (result: SearchResult) => result.available === 'no_videos'; + + const handleDeleteTorrent = async (id: string, disableToast: boolean = false) => { + try { + if (!rdKey && !adKey) throw new Error('no_keys'); + if (rdKey && id.startsWith('rd:')) await deleteTorrent(rdKey, id.substring(3)); + if (adKey && id.startsWith('ad:')) await deleteMagnet(adKey, id.substring(3)); + if (!disableToast) toast.success(`Download canceled (${id})`); + if (id.startsWith('rd:')) removeFromRdCache(id); + if (id.startsWith('ad:')) removeFromAdCache(id); + } catch (error) { + if (!disableToast) toast.error(`Error deleting torrent (${id})`); + throw error; + } + }; + + const handleSelectFiles = async (id: string, disableToast: boolean = false) => { + try { + if (!rdKey) throw new Error('no_rd_key'); + const response = await getTorrentInfo(rdKey, id.substring(3)); + if (response.filename === 'Magnet') return; // no files yet + + const selectedFiles = getSelectableFiles(response.files.filter(isVideo)).map( + (file) => file.id + ); + if (selectedFiles.length === 0) { + handleDeleteTorrent(id, true); + throw new Error('no_files_for_selection'); + } + + await selectFiles(rdKey, id.substring(3), selectedFiles); + } catch (error) { + if ((error as Error).message === 'no_files_for_selection') { + if (!disableToast) + toast.error(`No files for selection, deleting (${id})`, { + duration: 5000, + }); + } else { + if (!disableToast) toast.error(`Error selecting files (${id})`); + } + throw error; + } + }; + + return ( +
+ + + Debrid Media Manager - TV Show - {showInfo?.tv_results[0].name} - Season{' '} + {seasonNum} + + + +
+

📺

+ + Go Home + +
+ {/* Display basic movie info */} + {showInfo && ( +
+
+ Movie poster +
+
+

{showInfo.tv_results[0].name} - Season{' '} + {seasonNum}

+

{showInfo.tv_results[0].overview}

+
+
+ )} + +
+ + {loading && ( +
+
+
+ )} + {errorMessage && ( +
+ Error: + {errorMessage} +
+ )} + {searchResults.length > 0 && ( + <> + {!loading && ( +
+ + { + const isChecked = event.target.checked; + setRdAutoInstantCheck(isChecked); + }} + />{' '} + + + { + const isChecked = event.target.checked; + setAdAutoInstantCheck(isChecked); + }} + />{' '} + +
+ )} +
+
+{ + !loading && searchResults.map((r: SearchResult) => ( +
+
+

+ {r.title} +

+

+ Size: {(r.fileSize / 1024).toFixed(2)} GB +

+
+ {rd.isDownloading(r.hash) && rdCache![r.hash].id && ( + + )} + {rdKey && rd.notInLibrary(r.hash) && ( + + )} + {ad.isDownloading(r.hash) && adCache![r.hash].id && ( + + )} + {adKey && ad.notInLibrary(r.hash) && ( + + )} +
+ +
+
+ )) +} +
+
+ + )} + {Object.keys(router.query).length !== 0 && searchResults.length === 0 && !loading && ( + <> +

+ Processing search request for "{showInfo?.tv_results[0].name} + ". Try again in a few minutes. +

+ + )} +
+ ); +} + +export default withAuth(Search);