Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TorBox Integration #132

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8bfd2c2
feat: introduces torbox user
anonymous-org-za Aug 14, 2024
4acbfd9
feat: torbox into auth hook
anonymous-org-za Aug 14, 2024
3554032
feat: torbox login page
anonymous-org-za Aug 14, 2024
cbabe23
feat: gets torbox user info
anonymous-org-za Aug 14, 2024
6bc4b1a
feat: torbox shows in home
anonymous-org-za Aug 14, 2024
171bd12
feat: instant checks for TorBox, fixes for kitsu, fixes encoding for …
anonymous-org-za Aug 14, 2024
d38877b
chore: updated readme to remove and replace outdated tools for setup.…
anonymous-org-za Aug 14, 2024
714e5f4
fix: fixes instant cache for torbox
anonymous-org-za Aug 14, 2024
85deb44
feat: enables creating torrents in movies
anonymous-org-za Aug 14, 2024
7772674
feat: delete torbox torrent in movies
anonymous-org-za Aug 14, 2024
bf75c08
feat: fetches user torrents
anonymous-org-za Aug 14, 2024
3b3d263
feat: library shows torrents
anonymous-org-za Aug 14, 2024
185cc1f
fix: properly shows data in library
anonymous-org-za Aug 14, 2024
7fa3b9d
fix: filename
anonymous-org-za Aug 14, 2024
df53ac0
feat: deletes torrent from library
anonymous-org-za Aug 14, 2024
81eb3e8
feat: made all actions usable
anonymous-org-za Aug 14, 2024
e7cedf7
fix: fixes downloading files and showing info
anonymous-org-za Aug 14, 2024
c21bc7b
feat: fetches latest torrents
anonymous-org-za Aug 14, 2024
b9511ee
fix: adds adding magnet back
anonymous-org-za Aug 14, 2024
fd96521
feat: adds tb downloads from tv shows
anonymous-org-za Aug 14, 2024
ac4c70a
feat: anime has tb addition
anonymous-org-za Aug 14, 2024
c9694a1
fix: gives correct info, redirects to download
anonymous-org-za Aug 14, 2024
b8b15b0
fix: fixes anime cached not showing
anonymous-org-za Aug 14, 2024
1ed2580
fix: fixes cache not showing on tv shows
anonymous-org-za Aug 14, 2024
da36b4d
fix: support for torrens with no files
anonymous-org-za Aug 14, 2024
cf661a7
feat: makes tips more legible, adds torbox tip
anonymous-org-za Aug 14, 2024
049c0a6
fix: small ui changes for TorBox
anonymous-org-za Aug 14, 2024
4060c15
feat: torbox hashlist page, fixes rendering issues
anonymous-org-za Aug 15, 2024
35a313d
fix: more fixing of rendering issues
anonymous-org-za Aug 15, 2024
d72d5e9
chore: removed dev
anonymous-org-za Aug 15, 2024
1a52d78
fix: changes to code using suggestions from ai
anonymous-org-za Aug 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand All @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update database connection instructions.

The instructions still mention getting a connection string from PlanetScale, which should be updated to reflect the new database service, Filess.

- Get your Prisma database connection string from PlanetScale console
+ Get your Prisma database connection string from Filess console
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
0. Signup for a free tier plan at [Filess](https://filess.io/) - 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
Get your Prisma database connection string from Filess console

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

Expand Down
7 changes: 7 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ const nextConfig = {
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'media.kitsu.app',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'cdn.myanimelist.net',
Expand All @@ -62,6 +68,7 @@ const nextConfig = {
realDebridClientId: 'X245A4XAIBGVM',
allDebridHostname: 'https://api.alldebrid.com',
allDebridAgent: 'debridMediaManager',
torboxHostname: 'https://api.torbox.app/v1/api',
traktClientId: '8a7455d06804b07fa25e27454706c6f2107b6fe5ed2ad805eff3b456a17e79f0',
},
};
Expand Down
42 changes: 40 additions & 2 deletions src/hooks/auth.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();

Expand All @@ -41,9 +58,14 @@ export const useDebridLogin = () => {
await router.push('/alldebrid/login');
};

const loginWithTorBox = async () => {
await router.push("/torbox/login");
}

return {
loginWithRealDebrid,
loginWithAllDebrid,
loginWithTorBox
};
};

Expand Down Expand Up @@ -84,6 +106,11 @@ export const useAllDebridApiKey = () => {
return apiKey;
};

export const useTorBoxApiKey = () => {
const [apiKey] = useLocalStorage<string>("tb:apiKey");
return apiKey;
}

function removeToken(service: string) {
window.localStorage.removeItem(`${service}:accessToken`);
window.location.reload();
Expand All @@ -92,14 +119,17 @@ function removeToken(service: string) {
export const useCurrentUser = () => {
const [rdUser, setRdUser] = useState<RealDebridUser | null>(null);
const [adUser, setAdUser] = useState<AllDebridUser | null>(null);
const [tbUser, setTbUser] = useState<TorBoxUser | null>(null);
const [traktUser, setTraktUser] = useState<TraktUser | null>(null);
const router = useRouter();
const [rdToken] = useLocalStorage<string>('rd:accessToken');
const [adToken] = useLocalStorage<string>('ad:apiKey');
const [tbToken] = useLocalStorage<string>('tb:apiKey');
const [traktToken] = useLocalStorage<string>('trakt:accessToken');
const [_, setTraktUserSlug] = useLocalStorage<string>('trakt:userSlug');
const [rdError, setRdError] = useState<Error | null>(null);
const [adError, setAdError] = useState<Error | null>(null);
const [tbError, setTbError] = useState<Error | null>(null);
const [traktError, setTraktError] = useState<Error | null>(null);

useEffect(() => {
Expand All @@ -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(<TorBoxUser>tbUserResponse);
}
} catch (error: any) {
setTbError(new Error(error));
}
try {
if (traktToken) {
const traktUserResponse = await getTraktUser(traktToken!);
Expand All @@ -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 };
};
75 changes: 62 additions & 13 deletions src/pages/anime/[animeid].tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -63,6 +63,7 @@ const MovieSearch: FunctionComponent<AnimeSearchProps> = ({
const [descLimit, setDescLimit] = useState(100);
const [rdKey] = useRealDebridAccessToken();
const adKey = useAllDebridApiKey();
const tbKey = useTorBoxApiKey();
const [onlyShowCached, setOnlyShowCached] = useState<boolean>(true);
const [uncachedCount, setUncachedCount] = useState<number>(0);
const dmmCastToken = useCastToken();
Expand All @@ -89,9 +90,9 @@ const MovieSearch: FunctionComponent<AnimeSearchProps> = ({
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<SearchApiResponse>(endpoint);
if (response.status !== 200) {
Expand Down Expand Up @@ -123,6 +124,11 @@ const MovieSearch: FunctionComponent<AnimeSearchProps> = ({
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));
Expand Down Expand Up @@ -158,7 +164,7 @@ const MovieSearch: FunctionComponent<AnimeSearchProps> = ({
if (searchState === 'loading') return;
const tokens = new Map<string, number>();
// 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;
Expand Down Expand Up @@ -248,6 +254,29 @@ const MovieSearch: FunctionComponent<AnimeSearchProps> = ({
}
}

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',
Expand Down Expand Up @@ -403,11 +432,11 @@ const MovieSearch: FunctionComponent<AnimeSearchProps> = ({
{searchResults.length > 0 && (
<div className="mx-2 my-1 overflow-x-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
{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' &&
Expand Down Expand Up @@ -489,7 +518,27 @@ const MovieSearch: FunctionComponent<AnimeSearchProps> = ({
</button>
)}

{(r.rdAvailable || r.adAvailable) && (
{/* TB */}
{tbKey && inLibrary('tb', r.hash) && (
<button
className="bg-red-500 hover:bg-red-700 text-white text-xs rounded inline px-1"
onClick={() => deleteTb(r.hash)}
>
<FaTimes className="mr-2 inline" />
TB ({hashAndProgress[`tb:${r.hash}`] + '%'})
</button>
)}
{tbKey && notInLibrary('tb', r.hash) && (
<button
className={`bg-[#04BF8A] hover:bg-[#095842] text-white text-xs rounded inline px-1`}
onClick={() => addTb(r.hash)}
>
{btnIcon(r.tbAvailable)}
Add&nbsp;to&nbsp;TB&nbsp;library
</button>
)}

{(r.rdAvailable || r.adAvailable || r.tbAvailable) && (
<button
className="bg-sky-500 hover:bg-sky-700 text-white text-xs rounded inline px-1"
onClick={() => handleShowInfo(r)}
Expand Down
12 changes: 6 additions & 6 deletions src/pages/animesearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ function AnimeSearch() {
const fetchMiscData = async () => {
try {
let path = `api/browse/anime2`;
if (config.externalSearchApiHostname) {
path = encodeURIComponent(path);
}
// if (config.externalSearchApiHostname) {
// path = encodeURIComponent(path);
// }
let endpoint = `${config.externalSearchApiHostname || ''}/${path}`;
const res = await fetch(endpoint);
const data = await res.json();
Expand All @@ -67,9 +67,9 @@ function AnimeSearch() {
setSearchResults([]);
try {
let path = `api/search/anime?keyword=${q}`;
if (config.externalSearchApiHostname) {
path = encodeURIComponent(path);
}
// if (config.externalSearchApiHostname) {
// path = encodeURIComponent(path);
// }
let endpoint = `${config.externalSearchApiHostname || ''}/${path}`;
const res = await fetch(endpoint);
const data = await res.json();
Expand Down
6 changes: 3 additions & 3 deletions src/pages/browse/anime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ function Anime() {
setLoading(true);
try {
let path = 'api/browse/anime';
if (config.externalSearchApiHostname) {
path = encodeURIComponent(path);
}
// if (config.externalSearchApiHostname) {
// path = encodeURIComponent(path);
// }
let endpoint = `${config.externalSearchApiHostname || ''}/${path}`;
const res = await fetch(endpoint);
const data = await res.json();
Expand Down
6 changes: 3 additions & 3 deletions src/pages/browse/recent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ function RecentlyUpdated() {
setLoading(true);
try {
let path = 'api/browse/recent';
if (config.externalSearchApiHostname) {
path = encodeURIComponent(path);
}
// if (config.externalSearchApiHostname) {
// path = encodeURIComponent(path);
// }
let endpoint = `${config.externalSearchApiHostname || ''}/${path}`;
const res = await fetch(endpoint);
const data = await res.json();
Expand Down
Loading