diff --git a/README.md b/README.md index 25bbb28..9696387 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Start building your media library with truly unlimited storage size! ## What is this? -Do you want a movie and TV show library that has unlimited size? Consider using a Debrid service, like Real-Debrid or AllDebrid. These services work like a shared storage space for downloading torrents. You can download as much as you want without worrying about storage limits, because the files are shared among all users. You only "own" the file when you download it to your account. +Do you want a movie and TV show library that has unlimited size? Consider using a Debrid service, like Real-Debrid, TorBox or AllDebrid. These services work like a shared storage space for downloading torrents. You can download as much as you want without worrying about storage limits, because the files are shared among all users. You only "own" the file when you download it to your account. TorBox has the ability to seed back which is unique. These Debrid services also offer a feature called a WebDAV API. Think of it as a special tool that lets you connect your media library to different devices or software. It's like your Windows Samba share but better. @@ -16,7 +16,7 @@ To make this process even easier, I've developed this **free** and open source w ## Features -This builds on top of the amazing service brought by [Real-Debrid](http://real-debrid.com/?id=9783846) and [AllDebrid](https://alldebrid.com/?uid=1kk5i&lang=en). +This builds on top of the amazing service brought by [Real-Debrid](http://real-debrid.com/?id=9783846), [TorBox](https://torbox.app) and [AllDebrid](https://alldebrid.com/?uid=1kk5i&lang=en). ### Library management @@ -32,19 +32,19 @@ You can share your whole collection or select specific items you want to share. ## Setup -0. Signup for a free tier plan at [PlanetScale](https://planetscale.com/) - this is a serverless MySQL database hosted in the cloud +0. Signup for a free tier plan at [Filess](https://filess.io/) - this is a serverless MySQL database hosted in the cloud 1. Have Tor running at `127.0.0.1:9050` (needed for DHT search; if you don't need your own search database then refer to the secion `External Search API`) 2. Clone this repository and go to the directory 3. Create a copy of the `.env` file `cp .env .env.local` and fill in the details 4. Fill in required settings in `.env.local` (e.g. `PROXY=socks5h://127.0.0.1:9050` if tor is running on your host machine) -5. Get your Prisma database connection string from PlanetScale console and put that in your `.env.local` file +5. Get your Prisma database connection string from Filess console and put that in your `.env.local` file 6. Install the dependencies `npm i` 7. This is a Next.js project so either go with `npm run dev` or `npm run build && npm run start` 8. Head to `localhost:3000` and login ### External Search API -If you don't want to build your own library, edit the config `EXTERNAL_SEARCH_API_HOSTNAME` in your `.env.local` and set it to `https://corsproxy.org/?https://debridmediamanager.com` +If you don't want to build your own library, edit the config `EXTERNAL_SEARCH_API_HOSTNAME` in your `.env.local` and set it to `https://play.rezi.one/?https://debridmediamanager.com` [CorsAnywhere](https://github.com/Rob--W/cors-anywhere) ### Docker Swarm diff --git a/next.config.js b/next.config.js index 45fef0c..622a9ba 100644 --- a/next.config.js +++ b/next.config.js @@ -46,6 +46,12 @@ const nextConfig = { port: '', pathname: '/**', }, + { + protocol: 'https', + hostname: 'media.kitsu.app', + port: '', + pathname: '/**', + }, { protocol: 'https', hostname: 'cdn.myanimelist.net', @@ -62,6 +68,7 @@ const nextConfig = { realDebridClientId: 'X245A4XAIBGVM', allDebridHostname: 'https://api.alldebrid.com', allDebridAgent: 'debridMediaManager', + torboxHostname: 'https://api.torbox.app/v1/api', traktClientId: '8a7455d06804b07fa25e27454706c6f2107b6fe5ed2ad805eff3b456a17e79f0', }, }; diff --git a/src/hooks/auth.ts b/src/hooks/auth.ts index eb53786..4df376b 100644 --- a/src/hooks/auth.ts +++ b/src/hooks/auth.ts @@ -1,5 +1,6 @@ import { getAllDebridUser } from '@/services/allDebrid'; import { getCurrentUser as getRealDebridUser, getToken } from '@/services/realDebrid'; +import { getTorBoxUser } from '@/services/torbox'; import { TraktUser, getTraktUser } from '@/services/trakt'; import { clearRdKeys } from '@/utils/clearLocalStorage'; import { useRouter } from 'next/router'; @@ -30,6 +31,22 @@ interface AllDebridUser { fidelityPoints: number; } +interface TorBoxUser { + id: number; + created_at: string; + updated_at: string; + email: string; + plan: 0 | 1 | 2 | 3; + total_downloaded: number; + customer: string; + is_subscribed: boolean; + premium_expires_at: string; + cooldown_until: string; + auth_id: string; + user_referral: string; + base_emai: string; +} + export const useDebridLogin = () => { const router = useRouter(); @@ -41,9 +58,14 @@ export const useDebridLogin = () => { await router.push('/alldebrid/login'); }; + const loginWithTorBox = async () => { + await router.push("/torbox/login"); + } + return { loginWithRealDebrid, loginWithAllDebrid, + loginWithTorBox }; }; @@ -84,6 +106,11 @@ export const useAllDebridApiKey = () => { return apiKey; }; +export const useTorBoxApiKey = () => { + const [apiKey] = useLocalStorage("tb:apiKey"); + return apiKey; +} + function removeToken(service: string) { window.localStorage.removeItem(`${service}:accessToken`); window.location.reload(); @@ -92,14 +119,17 @@ function removeToken(service: string) { export const useCurrentUser = () => { const [rdUser, setRdUser] = useState(null); const [adUser, setAdUser] = useState(null); + const [tbUser, setTbUser] = useState(null); const [traktUser, setTraktUser] = useState(null); const router = useRouter(); const [rdToken] = useLocalStorage('rd:accessToken'); const [adToken] = useLocalStorage('ad:apiKey'); + const [tbToken] = useLocalStorage('tb:apiKey'); const [traktToken] = useLocalStorage('trakt:accessToken'); const [_, setTraktUserSlug] = useLocalStorage('trakt:userSlug'); const [rdError, setRdError] = useState(null); const [adError, setAdError] = useState(null); + const [tbError, setTbError] = useState(null); const [traktError, setTraktError] = useState(null); useEffect(() => { @@ -120,6 +150,14 @@ export const useCurrentUser = () => { } catch (error: any) { setAdError(new Error(error)); } + try { + if (tbToken) { + const tbUserResponse = await getTorBoxUser(tbToken!); + if (tbUserResponse) setTbUser(tbUserResponse); + } + } catch (error: any) { + setTbError(new Error(error)); + } try { if (traktToken) { const traktUserResponse = await getTraktUser(traktToken!); @@ -133,7 +171,7 @@ export const useCurrentUser = () => { } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rdToken, adToken, traktToken, router]); + }, [rdToken, adToken, tbToken, traktToken, router]); - return { rdUser, rdError, adUser, adError, traktUser, traktError }; + return { rdUser, rdError, adUser, adError, tbUser, tbError, traktUser, traktError }; }; diff --git a/src/pages/anime/[animeid].tsx b/src/pages/anime/[animeid].tsx index c30d3e5..fe997e8 100644 --- a/src/pages/anime/[animeid].tsx +++ b/src/pages/anime/[animeid].tsx @@ -1,14 +1,14 @@ -import { useAllDebridApiKey, useRealDebridAccessToken } from '@/hooks/auth'; +import { useAllDebridApiKey, useRealDebridAccessToken, useTorBoxApiKey } from '@/hooks/auth'; import { useCastToken } from '@/hooks/cast'; import { SearchApiResponse, SearchResult } from '@/services/mediasearch'; import { TorrentInfoResponse } from '@/services/realDebrid'; import UserTorrentDB from '@/torrent/db'; import { UserTorrent } from '@/torrent/userTorrent'; -import { handleAddAsMagnetInAd, handleAddAsMagnetInRd, handleCopyMagnet } from '@/utils/addMagnet'; +import { handleAddAsMagnetInAd, handleAddAsMagnetInRd, handleAddAsMagnetInTb, handleCopyMagnet } from '@/utils/addMagnet'; import { handleCastMovie } from '@/utils/cast'; -import { handleDeleteAdTorrent, handleDeleteRdTorrent } from '@/utils/deleteTorrent'; -import { fetchAllDebrid, fetchRealDebrid } from '@/utils/fetchTorrents'; -import { instantCheckInAd, instantCheckInRd, wrapLoading } from '@/utils/instantChecks'; +import { handleDeleteAdTorrent, handleDeleteRdTorrent, handleDeleteTbTorrent } from '@/utils/deleteTorrent'; +import { fetchAllDebrid, fetchRealDebrid, fetchTorBox } from '@/utils/fetchTorrents'; +import { instantCheckInAd, instantCheckInRd, instantCheckInTb, wrapLoading } from '@/utils/instantChecks'; import { applyQuickSearch2 } from '@/utils/quickSearch'; import { borderColor, btnColor, btnIcon, fileSize, sortByBiggest } from '@/utils/results'; import { isVideo } from '@/utils/selectable'; @@ -63,6 +63,7 @@ const MovieSearch: FunctionComponent = ({ const [descLimit, setDescLimit] = useState(100); const [rdKey] = useRealDebridAccessToken(); const adKey = useAllDebridApiKey(); + const tbKey = useTorBoxApiKey(); const [onlyShowCached, setOnlyShowCached] = useState(true); const [uncachedCount, setUncachedCount] = useState(0); const dmmCastToken = useCastToken(); @@ -89,9 +90,9 @@ const MovieSearch: FunctionComponent = ({ setUncachedCount(0); try { let path = `api/torrents/anime?animeId=${animeId}&dmmProblemKey=${tokenWithTimestamp}&solution=${tokenHash}&onlyTrusted=${onlyTrustedTorrents}`; - if (config.externalSearchApiHostname) { - path = encodeURIComponent(path); - } + // if (config.externalSearchApiHostname) { + // path = encodeURIComponent(path); + // } let endpoint = `${config.externalSearchApiHostname || ''}/${path}`; const response = await axios.get(endpoint); if (response.status !== 200) { @@ -123,6 +124,11 @@ const MovieSearch: FunctionComponent = ({ instantChecks.push( wrapLoading('AD', instantCheckInAd(adKey, hashArr, setSearchResults)) ); + if (tbKey) { + instantChecks.push( + wrapLoading("TorBox cache", instantCheckInTb(tbKey, hashArr, setSearchResults)) + ) + } const counts = await Promise.all(instantChecks); setSearchState('loaded'); setUncachedCount(hashArr.length - counts.reduce((acc, cur) => acc + cur, 0)); @@ -158,7 +164,7 @@ const MovieSearch: FunctionComponent = ({ if (searchState === 'loading') return; const tokens = new Map(); // filter by cached - const toProcess = searchResults.filter((r) => r.rdAvailable || r.adAvailable); + const toProcess = searchResults.filter((r) => r.rdAvailable || r.adAvailable || r.tbAvailable); toProcess.forEach((r) => { r.title.split(/[ .\-\[\]]/).forEach((word) => { if (word.length < 3) return; @@ -248,6 +254,29 @@ const MovieSearch: FunctionComponent = ({ } } + async function addTb(hash: string) { + await handleAddAsMagnetInTb(tbKey!, hash); + await fetchTorBox( + tbKey!, + async (torrents: UserTorrent[]) => await torrentDB.addAll(torrents) + ) + await fetchHashAndProgress(); + } + + async function deleteTb(hash: string) { + const torrents = await torrentDB.getAllByHash(hash); + for (const t of torrents) { + if (!t.id.startsWith('tb:')) continue; + await handleDeleteTbTorrent(tbKey!, t.id); + await torrentDB.deleteByHash('tb', hash); + setHashAndProgress((prev) => { + const newHashAndProgress = { ...prev }; + delete newHashAndProgress[`tb:${hash}`]; + return newHashAndProgress; + }); + } + } + const backdropStyle = { backgroundImage: `linear-gradient(to bottom, hsl(0, 0%, 12%,0.5) 0%, hsl(0, 0%, 12%,0) 50%, hsl(0, 0%, 12%,0.5) 100%), url(${backdrop})`, backgroundPosition: 'center', @@ -403,11 +432,11 @@ const MovieSearch: FunctionComponent = ({ {searchResults.length > 0 && (
{filteredResults.map((r: SearchResult, i: number) => { - const downloaded = isDownloaded('rd', r.hash) || isDownloaded('ad', r.hash); + const downloaded = isDownloaded('rd', r.hash) || isDownloaded('ad', r.hash) || isDownloaded('tb', r.hash) const downloading = - isDownloading('rd', r.hash) || isDownloading('ad', r.hash); + isDownloading('rd', r.hash) || isDownloading('ad', r.hash) || isDownloading('tb', r.hash) const inYourLibrary = downloaded || downloading; - if (onlyShowCached && !r.rdAvailable && !r.adAvailable && !inYourLibrary) + if (onlyShowCached && !r.rdAvailable && !r.adAvailable && !r.tbAvailable && !inYourLibrary) return; if ( movieMaxSize !== '0' && @@ -489,7 +518,27 @@ const MovieSearch: FunctionComponent = ({ )} - {(r.rdAvailable || r.adAvailable) && ( + {/* TB */} + {tbKey && inLibrary('tb', r.hash) && ( + + )} + {tbKey && notInLibrary('tb', r.hash) && ( + + )} + + {(r.rdAvailable || r.adAvailable || r.tbAvailable) && ( - )} - {adKey && ( + ) : null} + {isClient && adKey && ( + )} {Object.keys(router.query).length !== 0 && ( )} - {(rdKey || adKey) && ( - + {isClient && (rdKey || adKey || tbKey) && ( + {userTorrentsList.length - filteredList.length} torrents hidden {' '} - because its already in your library or its not cached in RD/AD + because its already in your library or its not cached in RD/AD/TB )}
@@ -504,7 +562,7 @@ function HashlistPage() { {(t.bytes / ONE_GIGABYTE).toFixed(1)} GB - {rdKey && isDownloading('rd', t.hash) && ( + {isClient && rdKey && isDownloading('rd', t.hash) && ( )} - {rdKey && !t.rdAvailable && notInLibrary('rd', t.hash) && ( + {isClient && rdKey && !t.rdAvailable && notInLibrary('rd', t.hash) && ( )} - {rdKey && t.rdAvailable && notInLibrary('rd', t.hash) && ( + {isClient && rdKey && t.rdAvailable && notInLibrary('rd', t.hash) && ( )} - {adKey && isDownloading('ad', t.hash) && ( + {isClient && adKey && isDownloading('ad', t.hash) && ( )} - {adKey && !t.adAvailable && notInLibrary('ad', t.hash) && ( + {isClient && adKey && !t.adAvailable && notInLibrary('ad', t.hash) && ( )} - {adKey && t.adAvailable && notInLibrary('ad', t.hash) && ( + {isClient && adKey && t.adAvailable && notInLibrary('ad', t.hash) && ( )} + + {isClient && tbKey && isDownloading('tb', t.hash) && ( + + )} + {isClient && tbKey && !t.tbAvailable && notInLibrary('tb', t.hash) && ( + + )} + {isClient && tbKey && t.tbAvailable && notInLibrary('tb', t.hash) && ( + + )} ); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 841dff0..ab58e3c 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -11,7 +11,7 @@ import { Toaster, toast } from 'react-hot-toast'; function IndexPage() { const router = useRouter(); - const { rdUser, adUser, rdError, adError, traktUser, traktError } = useCurrentUser(); + const { rdUser, rdError, adUser, adError, tbUser, tbError, traktUser, traktError } = useCurrentUser(); const [deleting, setDeleting] = useState(false); const [browseTerms] = useState(getTerms(2)); @@ -26,6 +26,11 @@ function IndexPage() { 'AllDebrid get user info failed, check your email and confirm the login coming from DMM' ); } + if (tbError) { + toast.error( + 'TorBox get user info failed, try clearing DMM site data and login again' + ) + } if (traktError) { toast.error('Trakt get user info failed'); } @@ -49,7 +54,7 @@ function IndexPage() { ); }; } - }, [rdError, adError, traktError]); + }, [rdError, adError, tbError, traktError]); const handleLogout = (prefix?: string) => { if (prefix) { @@ -94,7 +99,7 @@ function IndexPage() { {/* this is made by ChatGPT */} - {!deleting && (rdUser || adUser) ? ( + {!deleting && (rdUser || adUser || tbUser) ? ( <>

Debrid Media Manager

@@ -114,6 +119,24 @@ function IndexPage() { Login with Real-Debrid )}{' '} + {tbUser ? ( + <> + + TorBox + {' '} + {tbUser.email} + + {tbUser.plan === 0 && ("Free")} {tbUser.plan === 1 && ("Essential")} {tbUser.plan === 2 && ("Pro")} {tbUser.plan === 3 && ("Standard")} + + + ) : ( + + Login with TorBox + + )} {adUser ? ( <> diff --git a/src/pages/library.tsx b/src/pages/library.tsx index c0cdf33..c37e49b 100644 --- a/src/pages/library.tsx +++ b/src/pages/library.tsx @@ -1,4 +1,4 @@ -import { useAllDebridApiKey, useRealDebridAccessToken } from '@/hooks/auth'; +import { useAllDebridApiKey, useRealDebridAccessToken, useTorBoxApiKey } from '@/hooks/auth'; import { getTorrentInfo } from '@/services/realDebrid'; import UserTorrentDB from '@/torrent/db'; import { UserTorrent, UserTorrentStatus, keyByStatus, uniqId } from '@/torrent/userTorrent'; @@ -7,16 +7,18 @@ import { handleAddAsMagnetInRd, handleAddMultipleHashesInAd, handleAddMultipleHashesInRd, + handleAddMultipleHashesInTb, handleCopyMagnet, handleReinsertTorrentinRd, handleRestartTorrent, handleSelectFilesInRd, + handleRestartTorBoxTorrent } from '@/utils/addMagnet'; import { AsyncFunction, runConcurrentFunctions } from '@/utils/batch'; import { deleteFilteredTorrents } from '@/utils/deleteList'; -import { handleDeleteAdTorrent, handleDeleteRdTorrent } from '@/utils/deleteTorrent'; +import { handleDeleteAdTorrent, handleDeleteRdTorrent, handleDeleteTbTorrent } from '@/utils/deleteTorrent'; import { extractHashes } from '@/utils/extractHashes'; -import { fetchAllDebrid, fetchRealDebrid, getRdStatus } from '@/utils/fetchTorrents'; +import { fetchAllDebrid, fetchRealDebrid, getRdStatus, fetchTorBox } from '@/utils/fetchTorrents'; import { generateHashList, handleShare } from '@/utils/hashList'; import { checkForUncachedInRd } from '@/utils/instantChecks'; import { localRestore } from '@/utils/localRestore'; @@ -24,7 +26,7 @@ import { applyQuickSearch } from '@/utils/quickSearch'; import { torrentPrefix } from '@/utils/results'; import { checkArithmeticSequenceInFilenames, isVideo } from '@/utils/selectable'; import { defaultPlayer } from '@/utils/settings'; -import { showInfoForAD, showInfoForRD } from '@/utils/showInfo'; +import { showInfoForAD, showInfoForRD, showInfoForTB } from '@/utils/showInfo'; import { isFailed, isInProgress, isSlowOrNoLinks } from '@/utils/slow'; import { shortenNumber } from '@/utils/speed'; import { libraryToastOptions, magnetToastOptions, searchToastOptions } from '@/utils/toastOptions'; @@ -67,6 +69,7 @@ function TorrentsPage() { const [loading, setLoading] = useState(true); const [rdSyncing, setRdSyncing] = useState(true); const [adSyncing, setAdSyncing] = useState(true); + const [tbSyncing, setTbSyncing] = useState(true); const [filtering, setFiltering] = useState(false); const [grouping, setGrouping] = useState(false); @@ -79,6 +82,7 @@ function TorrentsPage() { // keys const [rdKey] = useRealDebridAccessToken(); const adKey = useAllDebridApiKey(); + const tbKey = useTorBoxApiKey(); const [defaultGrouping] = useState>({}); const [movieGrouping] = useState>({}); @@ -111,6 +115,8 @@ function TorrentsPage() { handleAddMultipleHashesInRd(rdKey, hashes, async () => await fetchLatestRDTorrents(2)); if (adKey) handleAddMultipleHashesInAd(adKey, hashes, async () => await fetchLatestADTorrents()); + if (tbKey) + handleAddMultipleHashesInTb(tbKey, hashes, async () => await fetchLatestTBTorrents()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [router]); @@ -277,6 +283,75 @@ function TorrentsPage() { ); }; + const fetchLatestTBTorrents = async function () { + const oldTorrents = await torrentDB.all(); + const oldIds = new Set( + oldTorrents.map((torrent) => torrent.id).filter((id) => id.startsWith('tb:')) + ); + const inProgressIds = new Set( + oldTorrents + .filter(isInProgress) + .map((t) => t.id) + .filter((id) => id.startsWith('tb:')) + ); + const newIds = new Set(); + + if (!tbKey) { + setLoading(false); + setTbSyncing(false); + } else { + await fetchTorBox(tbKey, async (torrents: UserTorrent[]) => { + // add all new torrents to the database + torrents.forEach((torrent) => newIds.add(torrent.id)); + const newTorrents = torrents.filter((torrent) => !oldIds.has(torrent.id)); + setUserTorrentsList((prev) => { + return [...prev, ...newTorrents]; + }); + await torrentDB.addAll(newTorrents); + + // refresh the torrents that are in progress + const inProgressTorrents = torrents.filter( + (torrent) => + torrent.download_state === "downloading" || + torrent.download_state === "stalled (no seeds)" || + torrent.download_state === "checkingResumeData" || + torrent.download_state === "metaDL" || + torrent.download_state === "paused" || + inProgressIds.has(torrent.id) + ); + setUserTorrentsList((prev) => { + return prev.map((t) => { + const found = inProgressTorrents.find((i) => i.id === t.id); + if (found) { + return found; + } + return t; + }); + }); + await torrentDB.addAll(inProgressTorrents); + + setLoading(false); + }); + setTbSyncing(false); + toast.success( + `Updated ${newIds.size} torrents in your TorBox library`, + libraryToastOptions + ); + } + + const toDelete = Array.from(oldIds).filter((id) => !newIds.has(id)); + await Promise.all( + toDelete.map(async (id) => { + setUserTorrentsList((prev) => prev.filter((torrent) => torrent.id !== id)); + await torrentDB.deleteById(id); + setSelectedTorrents((prev) => { + prev.delete(id); + return new Set(prev); + }); + }) + ); + }; + // fetch list from api async function initialize() { await torrentDB.initializeDB(); @@ -291,13 +366,13 @@ function TorrentsPage() { }); setLoading(false); } - await Promise.all([fetchLatestRDTorrents(), fetchLatestADTorrents()]); + await Promise.all([fetchLatestRDTorrents(), fetchLatestADTorrents(), fetchLatestTBTorrents()]); await selectPlayableFiles(userTorrentsList); } useEffect(() => { initialize(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rdKey, adKey]); + }, [rdKey, adKey, tbKey]); // aggregate metadata useEffect(() => { @@ -407,6 +482,7 @@ function TorrentsPage() { 'Tip: You can restore a local backup by using the "Local restore" button. It will only restore the torrents that are not already in your library.', 'Tip: The quick search box will filter the list by filename and id. You can use multiple words or even regex to filter your library. This way, you can select multiple torrents and delete them at once, or share them as a hash list.', 'Have you tried clicking on a torrent? You can see the links, the progress, and the status of the torrent. You can also select the files you want to download.', + 'TorBox seeds back! Make sure you do too!', 'I don\'t know what to put here, so here\'s a random tip: "The average person walks the equivalent of five times around the world in a lifetime."', ]; function setHelpTextBasedOnTime() { @@ -589,6 +665,7 @@ function TorrentsPage() { resetSelection(); await fetchLatestRDTorrents(Math.ceil(relevantList.length * 1.1)); await fetchLatestADTorrents(); + await fetchLatestTBTorrents(); toast.success(`Reinserted ${results.length} torrents`, magnetToastOptions); } if (!errors.length && !results.length) { @@ -828,6 +905,7 @@ function TorrentsPage() { if (results.length) { await fetchLatestRDTorrents(Math.ceil(results.length * 1.1)); await fetchLatestADTorrents(); + await fetchLatestTBTorrents(); toast.success(`Merged ${results.length} torrents`, libraryToastOptions); } if (!errors.length && !results.length) { @@ -889,6 +967,7 @@ function TorrentsPage() { if (results.length) { await fetchLatestRDTorrents(Math.ceil(results.length * 1.1)); await fetchLatestADTorrents(); + await fetchLatestTBTorrents(); } resolve({ success: results.length, error: errors.length }); } @@ -933,6 +1012,9 @@ function TorrentsPage() { if (adKey && hashes && debridService === 'ad') { handleAddMultipleHashesInAd(adKey, hashes, async () => await fetchLatestADTorrents()); } + if (tbKey && hashes && debridService === "tb") { + handleAddMultipleHashesInTb(tbKey, hashes, async () => fetchLatestTBTorrents()); + } } const hasNoQueryParamsBut = (...params: string[]) => @@ -1093,6 +1175,18 @@ function TorrentsPage() { ); }; + const handleShowInfoForTB = async (t: UserTorrent) => { + let player = window.localStorage.getItem('settings:player') || defaultPlayer; + if (player === 'realdebrid') { + alert('No player selected'); + } + showInfoForTB( + window.localStorage.getItem('settings:player') || defaultPlayer, + tbKey!, + t.tbData! + ) + } + return (
@@ -1346,7 +1440,7 @@ function TorrentsPage() {
{/* End of Main Menu */} {helpText && helpText !== 'hide' && ( -
setHelpText('hide')}> +
setHelpText('hide')}> 💡 {helpText}
)} @@ -1426,11 +1520,15 @@ function TorrentsPage() { {selectedTorrents.has(torrent.id) ? `✅` : `➕`} - torrent.id.startsWith('rd:') - ? handleShowInfoForRD(torrent) - : handleShowInfoForAD(torrent) - } + onClick={() => { + if (torrent.id.startsWith("rd:")) { + handleShowInfoForRD(torrent); + } else if (torrent.id.startsWith("ad:")) { + handleShowInfoForAD(torrent); + } else if (torrent.id.startsWith("tb:")) { + handleShowInfoForTB(torrent); + } + }} className="px-1 py-1 text-sm truncate" > {!['Invalid Magnet', 'Magnet', 'noname'].includes( @@ -1452,7 +1550,7 @@ function TorrentsPage() { ] }
-  {torrent.title}{' '} +  {torrent.filename}{' '} {filterText && ( - torrent.id.startsWith('rd:') - ? handleShowInfoForRD(torrent) - : handleShowInfoForAD(torrent) - } + onClick={() => { + if (torrent.id.startsWith("rd:")) { + handleShowInfoForRD(torrent); + } else if (torrent.id.startsWith("ad:")) { + handleShowInfoForAD(torrent); + } else if (torrent.id.startsWith("tb:")) { + handleShowInfoForTB(torrent); + } + }} className="px-1 py-1 text-xs text-center" > {(torrent.bytes / ONE_GIGABYTE).toFixed(1)} GB - torrent.id.startsWith('rd:') - ? handleShowInfoForRD(torrent) - : handleShowInfoForAD(torrent) - } + onClick={() => { + if (torrent.id.startsWith("rd:")) { + handleShowInfoForRD(torrent); + } else if (torrent.id.startsWith("ad:")) { + handleShowInfoForAD(torrent); + } else if (torrent.id.startsWith("tb:")) { + handleShowInfoForTB(torrent); + } + }} className="px-1 py-1 text-xs text-center" > {torrent.status !== UserTorrentStatus.finished ? ( @@ -1533,21 +1639,29 @@ function TorrentsPage() { - torrent.id.startsWith('rd:') - ? handleShowInfoForRD(torrent) - : handleShowInfoForAD(torrent) - } + onClick={() => { + if (torrent.id.startsWith("rd:")) { + handleShowInfoForRD(torrent); + } else if (torrent.id.startsWith("ad:")) { + handleShowInfoForAD(torrent); + } else if (torrent.id.startsWith("tb:")) { + handleShowInfoForTB(torrent); + } + }} className="px-1 py-1 text-xs text-center" > {new Date(torrent.added).toLocaleString()} - torrent.id.startsWith('rd:') - ? handleShowInfoForRD(torrent) - : handleShowInfoForAD(torrent) - } + onClick={() => { + if (torrent.id.startsWith("rd:")) { + handleShowInfoForRD(torrent); + } else if (torrent.id.startsWith("ad:")) { + handleShowInfoForAD(torrent); + } else if (torrent.id.startsWith("tb:")) { + handleShowInfoForTB(torrent); + } + }} className="px-1 py-1 flex place-content-center" > )} - {(r.rdAvailable || r.adAvailable) && ( + {/* TB */} + {tbKey && inLibrary('tb', r.hash) && ( + + )} + {tbKey && notInLibrary('tb', r.hash) && ( + + )} + + {(r.rdAvailable || r.adAvailable || r.tbAvailable) && ( )} - {(r.rdAvailable || r.adAvailable) && ( + {/* TB */} + {tbKey && inLibrary('tb', r.hash) && ( + + )} + {tbKey && notInLibrary('tb', r.hash) && ( + + )} + + {(r.rdAvailable || r.adAvailable || r.tbAvailable) && ( + + Create a free account with TorBox + +
+

Data Storage Policy

Please note that no data or logs are stored on our servers diff --git a/src/pages/torbox/login.tsx b/src/pages/torbox/login.tsx new file mode 100644 index 0000000..186b7b7 --- /dev/null +++ b/src/pages/torbox/login.tsx @@ -0,0 +1,69 @@ +import useLocalStorage from '@/hooks/localStorage'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; + + +export default function TorBoxLoginPage() { + const [apiKey, setApiKey] = useLocalStorage('tb:apiKey'); + const [error, setError] = useState(); + const router = useRouter(); + + useEffect(() => { + (async () => { + if (apiKey) { + await router.push('/'); + } + })(); + }, [apiKey, router]); + + return ( +

+ + Debrid Media Manager - TorBox Login + + +
{ + event.preventDefault(); + const submittedApiKey = event.target.apiKey.value.trim(); + + const apiKeyRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + let regexTest = apiKeyRegex.test(submittedApiKey); + + if (!regexTest) { + setError("Not a valid TorBox API key.") + } else { + setApiKey(submittedApiKey); + } + }} + > + +

+ Please enter your TorBox API Key. You can find this here. +

+ + {error && ( + {error} + )} + +
+
+ ); +} \ No newline at end of file diff --git a/src/services/mediasearch.ts b/src/services/mediasearch.ts index 7bfd3cc..cc6fd39 100644 --- a/src/services/mediasearch.ts +++ b/src/services/mediasearch.ts @@ -18,6 +18,7 @@ export type SearchResult = { rdAvailable: boolean; files: FileData[]; adAvailable: boolean; + tbAvailable: boolean; noVideos: boolean; // for cached results in RD medianFileSize: number; @@ -39,6 +40,7 @@ export interface EnrichedHashlistTorrent extends HashlistTorrent { noVideos: boolean; rdAvailable: boolean; adAvailable: boolean; + tbAvailable: boolean; files: FileData[]; } diff --git a/src/services/torbox.ts b/src/services/torbox.ts new file mode 100644 index 0000000..3dbc1b0 --- /dev/null +++ b/src/services/torbox.ts @@ -0,0 +1,124 @@ +import axios from 'axios'; +import getConfig from 'next/config'; + +const { publicRuntimeConfig: config } = getConfig(); + +interface UserResponse { + success: boolean; + detail: string; + error: string; + data: { + id: number; + created_at: string; + updated_at: string; + email: string; + plan: 0 | 1 | 2 | 3; + total_downloaded: number; + customer: string; + is_subscribed: boolean; + premium_expires_at: string; + cooldown_until: string; + auth_id: string; + user_referral: string; + base_emai: string; + } +} + +export const getTorBoxUser = async (apiKey: string) => { + let endpoint = `${config.torboxHostname}/user/me` + try { + const userData = await axios.get(endpoint, { + headers: { + "Authorization": `Bearer ${apiKey}` + } + }) + return userData.data.data + } catch (error) { + console.error('Error getting TorBox user data:', (error as any).message); + throw new Error(`Failed to get TorBox user data: ${(error as any).message}`); + } +}; + +export const tbInstantCheck = async (apiKey: string, hashes: string[]) => { + const params = new URLSearchParams({ format: 'list', list_files: 'true' }); + hashes.forEach(hash => params.append('hash', hash)); + let endpoint = `${config.torboxHostname}/torrents/checkcached` + try { + const response = await axios.get(endpoint, { + headers: { + "Authorization": `Bearer ${apiKey}` + } + }) + return response.data.data + } catch (error: any) { + console.error('Error fetching magnet availability from TorBox: ', error.message); + throw error; + } +} + +export const createTorBoxTorrent = async (apiKey: string, hashes: string[]) => { + var allResponses = [] + try { + for (let i = 0; i < hashes.length; i++) { + let endpoint = `${config.torboxHostname}/torrents/createtorrent`; + const response = await axios.post(endpoint, { + magnet: `magnet:?xt=urn:btih:${hashes[i]}` + }, { + headers: { + "Content-Type": "multipart/form-data", + "Authorization": `Bearer ${apiKey}` + }, + validateStatus: () => true + }); + var responseData = response.data; + if (responseData.error) { + throw responseData.detail + } + allResponses.push(response.data) + } + return allResponses + } catch (error) { + console.error("Error creating torrent in TorBox:", (error as any).message) + throw new Error(`Error creating torrent: ${responseData.detail}`); + } +}; + +export const deleteTorBoxTorrent = async (apiKey: string, id: string) => { + try { + + let endpoint = `${config.torboxHostname}/torrents/controltorrent`; + await axios.post(endpoint, { + torrent_id: id, + operation: "delete" + }, { + headers: { + "Authorization": `Bearer ${apiKey}` + } + }) + } catch (error: any) { + console.error('Error deleting torrent from TorBox:', error.message); + throw error; + } +}; + +export const getTorBoxTorrents = async ( + apiKey: string, + cacheBypass?: boolean, + id?: string +) => { + let endpoint = `${config.torboxHostname}/torrents/mylist`; + if (cacheBypass) { + endpoint += "&bypass_cache=true" + } + try { + const response = await axios.get(endpoint, { + headers: { + "Authorization": `Bearer ${apiKey}` + } + }); + return response.data?.data; + } catch (error) { + console.error('Error fetching your TorBox torrents:', (error as any).message); + throw error; + } +}; \ No newline at end of file diff --git a/src/torrent/userTorrent.ts b/src/torrent/userTorrent.ts index d69f717..76d129c 100644 --- a/src/torrent/userTorrent.ts +++ b/src/torrent/userTorrent.ts @@ -28,6 +28,7 @@ export interface UserTorrent { speed: number; rdData?: TorrentInfoResponse; adData?: MagnetStatus; + tbData?: any; } export interface CachedHash { diff --git a/src/utils/addMagnet.ts b/src/utils/addMagnet.ts index 1aed5d8..0f893f1 100644 --- a/src/utils/addMagnet.ts +++ b/src/utils/addMagnet.ts @@ -1,5 +1,6 @@ import { restartMagnet, uploadMagnet } from '@/services/allDebrid'; import { addHashAsMagnet, getTorrentInfo, selectFiles } from '@/services/realDebrid'; +import { createTorBoxTorrent } from "@/services/torbox" import { UserTorrent } from '@/torrent/userTorrent'; import toast from 'react-hot-toast'; import { handleDeleteRdTorrent } from './deleteTorrent'; @@ -99,6 +100,36 @@ export const handleAddAsMagnetInAd = async ( } }; +export const handleAddAsMagnetInTb = async ( + tbKey: string, + hash: string, + callback?: () => Promise +) => { + try { + await createTorBoxTorrent(tbKey, [hash]); + if (callback) await callback(); + toast('Successfully added torrent to TorBox!', magnetToastOptions); + } catch (error) { + toast.error(error as any); + throw error; + } +}; + +export const handleAddMultipleHashesInTb = async ( + tbKey: string, + hashes: string[], + callback?: () => Promise +) => { + try { + await createTorBoxTorrent(tbKey, hashes); + if (callback) await callback(); + toast(`Successfully added ${hashes.length} hashes!`, magnetToastOptions); + } catch (error) { + toast.error(error as any); + throw error; + } +}; + export const handleAddMultipleHashesInAd = async ( adKey: string, hashes: string[], @@ -127,6 +158,10 @@ export const handleRestartTorrent = async (adKey: string, id: string) => { } }; +export const handleRestartTorBoxTorrent = async (tbKey: string, id: string) => { + toast.error(`This function is not allowed by TorBox.`) +} + export async function handleCopyMagnet(hash: string) { const magnet = `magnet:?xt=urn:btih:${hash}`; await navigator.clipboard.writeText(magnet); diff --git a/src/utils/deleteTorrent.ts b/src/utils/deleteTorrent.ts index b719f29..205cb1b 100644 --- a/src/utils/deleteTorrent.ts +++ b/src/utils/deleteTorrent.ts @@ -1,5 +1,6 @@ import { deleteMagnet } from '@/services/allDebrid'; import { deleteTorrent } from '@/services/realDebrid'; +import { deleteTorBoxTorrent } from "@/services/torbox"; import toast from 'react-hot-toast'; import { magnetToastOptions } from './toastOptions'; @@ -30,3 +31,17 @@ export const handleDeleteAdTorrent = async ( toast.error(`Error deleting torrent in AD (${id})`); } }; + +export const handleDeleteTbTorrent = async ( + tbKey: string, + id: string, + disableToast: boolean = false +) => { + try { + await deleteTorBoxTorrent(tbKey, id.substring(3)); + if (!disableToast) toast(`Torrent deleted (${id})`, magnetToastOptions); + } catch (error) { + console.error(error); + toast.error(`Error deleting TorBox torrent (${id})`); + } +}; diff --git a/src/utils/fetchTorrents.ts b/src/utils/fetchTorrents.ts index 344f93f..8d4adff 100644 --- a/src/utils/fetchTorrents.ts +++ b/src/utils/fetchTorrents.ts @@ -1,5 +1,6 @@ import { MagnetStatus, getMagnetStatus } from '@/services/allDebrid'; import { UserTorrentResponse, getUserTorrentsList } from '@/services/realDebrid'; +import { getTorBoxTorrents } from '@/services/torbox'; import { UserTorrent, UserTorrentStatus } from '@/torrent/userTorrent'; import { ParsedFilename, filenameParse } from '@ctrl/video-filename-parser'; import { every, some } from 'lodash'; @@ -128,7 +129,7 @@ export const fetchAllDebrid = async ( magnetInfo.filename = 'Magnet'; } let mediaType = 'other'; - let info = undefined; + let info = {} as ParsedFilename const filenames = magnetInfo.links.map((f) => f.filename); const torrentAndFiles = [magnetInfo.filename, ...filenames]; @@ -136,7 +137,7 @@ export const fetchAllDebrid = async ( if (every(torrentAndFiles, (f) => !isVideo({ path: f }))) { mediaType = 'other'; - info = undefined; + info = {} as ParsedFilename } else if ( hasEpisodes || some(torrentAndFiles, (f) => /s\d\d\d?.?e\d\d\d?/i.test(f)) || @@ -199,6 +200,81 @@ export const fetchAllDebrid = async ( } }; +export const fetchTorBox = async ( + tbKey: string, + callback: (torrents: UserTorrent[]) => Promise +) => { + try { + const magnets = (await getTorBoxTorrents(tbKey)).map((magnetInfo) => { + let mediaType = 'other'; + let info = {} as ParsedFilename; + + const filenames = magnetInfo.files?.map((f) => f.short_name) ?? [] + const torrentAndFiles = [magnetInfo.name, ...filenames]; + const hasEpisodes = checkArithmeticSequenceInFilenames(filenames); + + if (every(torrentAndFiles, (f) => !isVideo({ path: f }))) { + mediaType = 'other'; + info = {} as ParsedFilename; + } else if ( + hasEpisodes || + some(torrentAndFiles, (f) => /s\d\d\d?.?e\d\d\d?/i.test(f)) || + some(torrentAndFiles, (f) => /season.?\d+/i.test(f)) || + some(torrentAndFiles, (f) => /episodes?\s?\d+/i.test(f)) || + some(torrentAndFiles, (f) => /\b[a-fA-F0-9]{8}\b/.test(f)) + ) { + mediaType = 'tv'; + info = filenameParse(magnetInfo.name, true); + } else if ( + !hasEpisodes && + every(torrentAndFiles, (f) => !/s\d\d\d?.?e\d\d\d?/i.test(f)) && + every(torrentAndFiles, (f) => !/season.?\d+/i.test(f)) && + every(torrentAndFiles, (f) => !/episodes?\s?\d+/i.test(f)) && + every(torrentAndFiles, (f) => !/\b[a-fA-F0-9]{8}\b/.test(f)) + ) { + mediaType = 'movie'; + info = filenameParse(magnetInfo.name); + } + + const date = new Date(magnetInfo.created_at); + + if (magnetInfo.size === 0) magnetInfo.size = 1; + return { + // score: getReleaseTags(magnetInfo.filename, magnetInfo.size / ONE_GIGABYTE).score, + info, + mediaType, + title: + info && (mediaType === 'movie' || mediaType == 'tv') + ? getMediaId(info, mediaType, false) + : magnetInfo.name, + id: `tb:${magnetInfo.id}`, + filename: magnetInfo.name, + hash: magnetInfo.hash, + bytes: magnetInfo.size, + seeders: magnetInfo.seeds, + progress: magnetInfo.progress * 100, + status: magnetInfo.download_state, + serviceStatus: 200, + added: date, + speed: magnetInfo.download_speed || 0, + links: magnetInfo.files ?? [], + tbData: magnetInfo, + selectedFiles: magnetInfo.files?.map((l, idx) => ({ + fileId: idx++, + filename: l?.name || null, + filesize: l?.size || null, + link: null, + })), + }; + }) as UserTorrent[]; + await callback(magnets); + } catch (error) { + await callback([]); + toast.error('Error fetching TorBox torrents list', genericToastOptions); + console.error(error); + } +}; + export const getRdStatus = (torrentInfo: UserTorrentResponse): UserTorrentStatus => { let status: UserTorrentStatus; switch (torrentInfo.status) { diff --git a/src/utils/instantChecks.ts b/src/utils/instantChecks.ts index 724250b..7b3a98a 100644 --- a/src/utils/instantChecks.ts +++ b/src/utils/instantChecks.ts @@ -10,6 +10,7 @@ import { runConcurrentFunctions } from './batch'; import { groupBy } from './groupBy'; import { isVideo } from './selectable'; import { searchToastOptions } from './toastOptions'; +import { tbInstantCheck } from '@/services/torbox' export const wrapLoading = async function (debrid: string, checkAvailability: Promise) { return await toast.promise( @@ -253,6 +254,89 @@ export const instantCheckInAd = async ( return instantCount; }; +export const instantCheckInTb = async ( + tbKey: string, + hashes: string[], + setTorrentList: Dispatch> +): Promise => { + let instantCount = 0; + const funcs = []; + for (const hashGroup of groupBy(100, hashes)) { + funcs.push(async () => { + const resp = await tbInstantCheck(tbKey, hashGroup); + + if (resp === null) { + return [] + } + + setTorrentList((prevSearchResults) => { + const newSearchResults = [...prevSearchResults]; + for (const magnetData of resp) { + const masterHash = magnetData.hash; + const torrent = newSearchResults.find((r) => r.hash === masterHash); + if (!torrent) continue; + if (torrent.noVideos) continue; + if (!magnetData.files) continue; + + const checkVideoInFiles = (files: MagnetFile[]): boolean => { + return files.reduce((noVideo: boolean, curr: MagnetFile) => { + if (!noVideo) return false; // If we've already found a video, no need to continue checking + if (!curr.n) return false; // If 'n' property doesn't exist, it's not a video + if (curr.e) { + // If 'e' property exists, check it recursively + return checkVideoInFiles(curr.e); + } + return !isVideo({ path: curr.n }); + }, true); + }; + + let idx = 0; + torrent.files = magnetData.files + .map((file) => { + if (file.e && file.e.length > 0) { + return file.e.map((f) => { + return { + fileId: idx++, + filename: f.name, + filesize: f.size, + }; + }); + } + return { + fileId: idx++, + filename: file.name, + filesize: file.size, + }; + }) + .flat(); + + const videoFiles = torrent.files.filter((f) => isVideo({ path: f.filename })); + const sortedFileSizes = videoFiles + .map((f) => f.filesize / 1024 / 1024) + .sort((a, b) => a - b); + const mid = Math.floor(sortedFileSizes.length / 2); + torrent.medianFileSize = + torrent.medianFileSize ?? sortedFileSizes.length % 2 !== 0 + ? sortedFileSizes[mid] + : (sortedFileSizes[mid - 1] + sortedFileSizes[mid]) / 2; + torrent.biggestFileSize = sortedFileSizes[sortedFileSizes.length - 1]; + torrent.videoCount = videoFiles.length; + torrent.noVideos = checkVideoInFiles(magnetData.files); + if (!torrent.noVideos) { + torrent.tbAvailable = true; + instantCount += 1; + } else { + torrent.tbAvailable = false; + } + } + return newSearchResults; + }); + }); + } + await runConcurrentFunctions(funcs, 5, 100); + return instantCount; +}; + // for hashlists export const instantCheckInAd2 = async ( adKey: string, @@ -319,3 +403,74 @@ export const instantCheckInAd2 = async ( await runConcurrentFunctions(funcs, 5, 100); return instantCount; }; + +// for hashlists +export const instantHashListCheckInTb = async ( + tbKey: string, + hashes: string[], + setTorrentList: Dispatch> +): Promise => { + let instantCount = 0; + const funcs = []; + for (const hashGroup of groupBy(100, hashes)) { + funcs.push(async () => { + const resp = await tbInstantCheck(tbKey, hashGroup); + + if (resp === null) { + return [] + } + setTorrentList((prevSearchResults) => { + const newSearchResults = [...prevSearchResults]; + for (const magnetData of resp) { + const masterHash = magnetData.hash; + const torrent = newSearchResults.find((r) => r.hash === masterHash); + if (!torrent) continue; + if (torrent.noVideos) continue; + if (!magnetData.files) continue; + + const checkVideoInFiles = (files: MagnetFile[]): boolean => { + return files.reduce((noVideo: boolean, curr: MagnetFile) => { + if (!noVideo) return false; // If we've already found a video, no need to continue checking + if (!curr.n) return false; // If 'n' property doesn't exist, it's not a video + if (curr.e) { + // If 'e' property exists, check it recursively + return checkVideoInFiles(curr.e); + } + return !isVideo({ path: curr.n }); + }, true); + }; + + let idx = 0; + torrent.files = magnetData.files + .map((file) => { + if (file.e && file.e.length > 0) { + return file.e.map((f) => { + return { + fileId: idx++, + filename: f.name, + filesize: f.size, + }; + }); + } + return { + fileId: idx++, + filename: file.name, + filesize: file.size, + }; + }) + .flat(); + torrent.noVideos = checkVideoInFiles(magnetData.files); + if (!torrent.noVideos && magnetData.instant) { + torrent.tbAvailable = true; + instantCount += 1; + } else { + torrent.tbAvailable = false; + } + } + return newSearchResults; + }); + }); + } + await runConcurrentFunctions(funcs, 5, 100); + return instantCount; +}; diff --git a/src/utils/showInfo.ts b/src/utils/showInfo.ts index e107e43..c86764e 100644 --- a/src/utils/showInfo.ts +++ b/src/utils/showInfo.ts @@ -174,6 +174,95 @@ export const showInfoForRD = async ( }); }; +export const showInfoForTB = async ( + app: string, + tbKey: string, + info: MagnetStatus, + userId: string = '', + imdbId: string = '' +) => { + var filesList = info.files ?? [] + .map((file) => { + let size = file.size < 1024 ** 3 ? file.size / 1024 ** 2 : file.size / 1024 ** 3; + let unit = file.size < 1024 ** 3 ? 'MB' : 'GB'; + const isPlayable = isVideo({ path: file.name }); + + let downloadForm = ''; + let watchBtn = ''; + let castBtn = ''; + + downloadForm = ` + + + + `; + + // Return the list item for the file, with or without the download form + return ` +
  • + ${file.short_name} + ${size.toFixed(2)} ${unit} + ${downloadForm} + ${watchBtn} + ${castBtn} +
  • + `; + }) + .join(''); + + if (!filesList) { + filesList = ` + This torrent is currently being downloaded from TorBox + ` + } + + let html = `

    ${info.name}

    +
    +
    +
      + ${filesList} +
    +
    `; + html = html.replace( + '
    ', + `
    + + + + + + + + + + + + + + + + + + + +
    Size:${(info.size / 1024 ** 3).toFixed(2)} GB
    ID:${info.id}
    Status:${info.download_state}
    Added:${new Date(info.created_at).toLocaleString()}
    +
    ` + ); + + Swal.fire({ + // icon: 'info', + html, + showConfirmButton: false, + customClass: { + htmlContainer: '!mx-1', + }, + width: '800px', + showCloseButton: true, + inputAutoFocus: true, + }); +}; + + export const showInfoForAD = async ( app: string, rdKey: string, @@ -255,4 +344,4 @@ export const showInfoForAD = async ( showCloseButton: true, inputAutoFocus: true, }); -}; +}; \ No newline at end of file diff --git a/src/utils/withAuth.tsx b/src/utils/withAuth.tsx index 534639e..15b6eb5 100644 --- a/src/utils/withAuth.tsx +++ b/src/utils/withAuth.tsx @@ -1,4 +1,4 @@ -import { useAllDebridApiKey, useRealDebridAccessToken } from '@/hooks/auth'; +import { useAllDebridApiKey, useRealDebridAccessToken, useTorBoxApiKey } from '@/hooks/auth'; import { useRouter } from 'next/router'; import { ComponentType, useEffect, useState } from 'react'; import { supportsLookbehind } from './lookbehind'; @@ -12,11 +12,13 @@ export const withAuth =

    (Component: ComponentType

    ) => { const [isLoading, setIsLoading] = useState(true); const [rdKey, rdLoading] = useRealDebridAccessToken(); const adKey = useAllDebridApiKey(); + const tbKey = useTorBoxApiKey(); useEffect(() => { if ( !rdKey && !adKey && + !tbKey && router.pathname !== START_ROUTE && !router.pathname.endsWith(LOGIN_ROUTE) && !rdLoading @@ -25,7 +27,7 @@ export const withAuth =

    (Component: ComponentType

    ) => { } else { setIsLoading(rdLoading); } - }, [rdKey, rdLoading, adKey, router]); + }, [rdKey, rdLoading, adKey, tbKey, router]); if (isLoading) { // Render a loading indicator or placeholder on initial load