diff --git a/TODO.md b/TODO.md
index 35399d2..ba82d8e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -17,8 +17,8 @@
- [✔️] Make the architecture more scalable using docker swarm
- [✔️] Find cached and selectable download on search
- [✔️] 2.3.0: Show AD/RD download buttons even if status=downloaded
-- [] 2.5.0: Add instant check in AD on search page (removed due to performance impact)
+- [✔️] 2.5.0: Add instant check in AD on search page (removed due to performance impact)
- [] 2.7.0: Rescan library button in all other pages except library
-- [] 2.9.0: Add title filter buttons on search page
+- [✔️] 2.9.0: Add title filter buttons on search page
- [] 3.0.0: Add tests
- [] 4.0.0: Refactor pages into different components
diff --git a/package-lock.json b/package-lock.json
index 68a92da..8030e5b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "debrid-media-manager",
- "version": "2.4.2",
+ "version": "2.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "debrid-media-manager",
- "version": "2.4.2",
+ "version": "2.5.0",
"dependencies": {
"@ctrl/video-filename-parser": "^4.12.0",
"@octokit/rest": "^19.0.7",
diff --git a/package.json b/package.json
index bd97e58..95ad102 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "debrid-media-manager",
- "version": "2.4.2",
+ "version": "2.5.0",
"private": false,
"scripts": {
"dev": "next dev",
diff --git a/src/pages/hashlist.tsx b/src/pages/hashlist.tsx
index d521d7b..f3b83be 100644
--- a/src/pages/hashlist.tsx
+++ b/src/pages/hashlist.tsx
@@ -269,8 +269,8 @@ function TorrentsPage() {
if (rdKey && id.startsWith('rd:')) await deleteTorrent(rdKey, id.substring(3));
if (adKey && id.startsWith('ad:')) await deleteTorrent(adKey, id.substring(3));
if (!disableToast) toast.success(`Download canceled (${id})`);
- if (id.startsWith('rd:')) removeFromRdCache(id.substring(3));
- if (id.startsWith('ad:')) removeFromAdCache(id.substring(3));
+ if (id.startsWith('rd:')) removeFromRdCache(id);
+ if (id.startsWith('ad:')) removeFromAdCache(id);
} catch (error) {
if (!disableToast) toast.error(`Error deleting torrent (${id})`);
throw error;
@@ -312,7 +312,7 @@ function TorrentsPage() {
{(totalBytes / ONE_GIGABYTE / 1024).toFixed(1)} TB
-
+
Share this page ({userTorrentsList.length} files in total; size:{' '}
@@ -366,7 +366,7 @@ function TorrentsPage() {
href="/hashlist"
className="mr-2 mb-2 bg-yellow-400 hover:bg-yellow-500 text-black py-2 px-4 rounded"
>
- Clear filter
+ Reset
)}
@@ -417,10 +417,6 @@ function TorrentsPage() {
groupCount === 1 ? '' : 's'
}`
: '';
- console.log(
- rd.isDownloaded(t.hash) || ad.isDownloaded(t.hash),
- rd.isDownloading(t.hash) || ad.isDownloading(t.hash)
- );
return (
Debrid Media Manager - Library
-
+
My Library ({userTorrentsList.length} downloads in total; size:{' '}
@@ -649,7 +649,7 @@ function TorrentsPage() {
: ''
}`}
>
- Clear filter
+ Reset
diff --git a/src/pages/search.tsx b/src/pages/search.tsx
index 0867951..839c98b 100644
--- a/src/pages/search.tsx
+++ b/src/pages/search.tsx
@@ -1,6 +1,7 @@
import useMyAccount, { MyAccount } from '@/hooks/account';
import { useAllDebridApiKey, useRealDebridAccessToken } from '@/hooks/auth';
import { useDownloadsCache } from '@/hooks/cache';
+import useLocalStorage from '@/hooks/localStorage';
import {
AdInstantAvailabilityResponse,
adInstantCheck,
@@ -16,6 +17,7 @@ import {
rdInstantCheck,
selectFiles,
} from '@/services/realDebrid';
+import { groupBy } from '@/utils/groupBy';
import { getMediaId } from '@/utils/mediaId';
import { getSelectableFiles, isVideo } from '@/utils/selectable';
import { withAuth } from '@/utils/withAuth';
@@ -31,8 +33,10 @@ import { FaDownload, FaFastForward, FaTimes } from 'react-icons/fa';
import { SearchApiResponse } from './api/search';
type Availability = 'all:available' | 'rd:available' | 'ad:available' | 'unavailable' | 'no_videos';
+type HashAvailability = Record
;
type SearchResult = {
+ mediaId: string;
title: string;
fileSize: number;
hash: string;
@@ -41,19 +45,36 @@ type SearchResult = {
available: Availability;
};
+type SearchFilter = {
+ count: number;
+ title: string;
+ biggestFileSize: number;
+};
+
const { publicRuntimeConfig: config } = getConfig();
function Search() {
const [query, setQuery] = useState('');
const [searchResults, setSearchResults] = useState([]);
+ const [filteredResults, setFilteredResults] = useState([]);
const [errorMessage, setErrorMessage] = useState('');
const rdKey = useRealDebridAccessToken();
const adKey = useAllDebridApiKey();
const [loading, setLoading] = useState(false);
const [cancelTokenSource, setCancelTokenSource] = useState(null);
const [myAccount, setMyAccount] = useMyAccount();
+ const [masterAvailability, setMasterAvailability] = useState({});
+ const [searchFilters, setSearchFilters] = 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 router = useRouter();
@@ -77,26 +98,42 @@ function Search() {
const response = await axios.get(endpoint, {
cancelToken: source.token,
});
+
setSearchResults(
- response.data.searchResults?.map((r) => ({ ...r, available: 'unavailable' })) || []
+ response.data.searchResults?.map((r) => ({
+ ...r,
+ mediaId: getMediaId(r.info, r.mediaType, false),
+ available: 'unavailable',
+ })) || []
+ );
+ setSearchFilters(
+ response.data.searchResults?.reduce((acc, r) => {
+ const mediaId = getMediaId(r.info, r.mediaType, true);
+ if (acc[mediaId]) {
+ acc[mediaId].count += 1;
+ acc[mediaId].biggestFileSize = Math.max(
+ acc[mediaId].biggestFileSize,
+ r.fileSize
+ );
+ } else {
+ acc[mediaId] = {
+ title: getMediaId(r.info, r.mediaType, false),
+ biggestFileSize: r.fileSize,
+ count: 1,
+ };
+ }
+ return acc;
+ }, {} as Record)!
);
if (response.data.searchResults?.length) {
toast(`Found ${response.data.searchResults.length} results`, { icon: '🔍' });
- const availability = await instantCheckInRd(
- response.data.searchResults.map((result) => result.hash)
- );
- toast(
- `Found ${
- Object.values(availability).filter((a) => a.includes(':available')).length
- } available in RD`,
- { icon: '🔍' }
- );
- setSearchResults((prev) =>
- prev.map((r) => ({ ...r, available: availability[r.hash] }))
- );
+
+ // instant checks
+ const hashArr = response.data.searchResults.map((r) => r.hash);
+ if (rdKey && rdAutoInstantCheck) await instantCheckInRd(hashArr);
+ // if (adKey && adAutoInstantCheck) await instantCheckInAd(hashArr);
} else {
toast(`No results found`, { icon: '🔍' });
- setSearchResults([]);
}
} catch (error) {
if (axios.isCancel(error)) {
@@ -129,38 +166,56 @@ function Search() {
const { query: searchQuery } = router.query;
if (!searchQuery) return;
const decodedQuery = decodeURIComponent(searchQuery as string);
+ if (decodedQuery === query) return;
setQuery(decodedQuery);
fetchData(decodedQuery);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router.query, myAccount]);
+ useEffect(() => {
+ setSearchResults((prev) =>
+ prev.map((r) => ({
+ ...r,
+ available: masterAvailability[r.hash],
+ }))
+ );
+ }, [masterAvailability]);
+
+ useEffect(() => {
+ const { filter } = router.query;
+ if (!filter) {
+ setFilteredResults(searchResults);
+ } else {
+ const decodedFilter = decodeURIComponent(filter as string);
+ setFilteredResults(
+ searchResults.filter((r) => r.mediaId.toLocaleLowerCase() === decodedFilter)
+ );
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [router.query, searchResults]);
+
useEffect(() => {
return () => {
if (cancelTokenSource) cancelTokenSource.cancel();
};
}, [cancelTokenSource]);
- type HashAvailability = Record;
-
- const instantCheckInRd = async (hashes: string[]): Promise => {
- const availability = hashes.reduce((acc: HashAvailability, curr: string) => {
- acc[curr] = 'unavailable';
- return acc;
- }, {});
+ const instantCheckInRd = async (hashes: string[]): Promise => {
+ const rdAvailability = {} as HashAvailability;
- if (!rdKey) return availability;
-
- const setInstantFromRd = (rdAvailabilityResp: RdInstantAvailabilityResponse) => {
- for (const masterHash in rdAvailabilityResp) {
- if ('rd' in rdAvailabilityResp[masterHash] === false) continue;
- const variants = rdAvailabilityResp[masterHash]['rd'];
- if (variants.length) availability[masterHash] = 'no_videos';
+ 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 })) {
- availability[masterHash] =
- availability[masterHash] === 'ad:available'
+ rdAvailability[masterHash] =
+ masterAvailability[masterHash] === 'ad:available' ||
+ masterAvailability[masterHash] === 'all:available'
? 'all:available'
: 'rd:available';
break;
@@ -170,16 +225,17 @@ function Search() {
}
};
- const groupBy = (itemLimit: number, hashes: string[]) =>
- Array.from({ length: Math.ceil(hashes.length / itemLimit) }, (_, i) =>
- hashes.slice(i * itemLimit, (i + 1) * itemLimit)
- );
-
try {
for (const hashGroup of groupBy(100, hashes)) {
if (rdKey) await rdInstantCheck(rdKey, hashGroup).then(setInstantFromRd);
}
- return availability;
+ toast(
+ `Found ${
+ Object.values(rdAvailability).filter((a) => a.includes(':available')).length
+ } available in RD`,
+ { icon: '🔍' }
+ );
+ setMasterAvailability({ ...masterAvailability, ...rdAvailability });
} catch (error) {
toast.error(
'There was an error checking availability in Real-Debrid. Please try again.'
@@ -188,27 +244,15 @@ function Search() {
}
};
- // TODO: Add AD instant check-in support
- const instantCheckInAd = async (
- hashes: string[],
- existingAvailability?: HashAvailability
- ): Promise => {
- const availability =
- existingAvailability ||
- hashes.reduce((acc: HashAvailability, curr: string) => {
- acc[curr] = 'unavailable';
- return acc;
- }, {});
+ const instantCheckInAd = async (hashes: string[]): Promise => {
+ const adAvailability = {} as HashAvailability;
- if (!adKey) return availability;
-
- const setInstantFromAd = (adAvailabilityResp: AdInstantAvailabilityResponse) => {
- for (const magnetData of adAvailabilityResp.data.magnets) {
+ const setInstantFromAd = (resp: AdInstantAvailabilityResponse) => {
+ for (const magnetData of resp.data.magnets) {
const masterHash = magnetData.hash;
- const instant = magnetData.instant;
-
- if (masterHash in availability && instant === true) {
- availability[masterHash] = magnetData.files?.reduce(
+ 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;
@@ -217,7 +261,8 @@ function Search() {
},
false
)
- ? availability[masterHash] === 'rd:available'
+ ? masterAvailability[masterHash] === 'rd:available' ||
+ masterAvailability[masterHash] === 'all:available'
? 'all:available'
: 'ad:available'
: 'no_videos';
@@ -225,16 +270,17 @@ function Search() {
}
};
- const groupBy = (itemLimit: number, hashes: string[]) =>
- Array.from({ length: Math.ceil(hashes.length / itemLimit) }, (_, i) =>
- hashes.slice(i * itemLimit, (i + 1) * itemLimit)
- );
-
try {
for (const hashGroup of groupBy(30, hashes)) {
if (adKey) await adInstantCheck(adKey, hashGroup).then(setInstantFromAd);
}
- return availability;
+ toast(
+ `Found ${
+ Object.values(adAvailability).filter((a) => a.includes(':available')).length
+ } available in AD`,
+ { icon: '🔍' }
+ );
+ setMasterAvailability({ ...masterAvailability, ...adAvailability });
} catch (error) {
toast.error('There was an error checking availability in AllDebrid. Please try again.');
throw error;
@@ -286,6 +332,7 @@ function Search() {
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 {
@@ -293,8 +340,8 @@ function Search() {
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.substring(3));
- if (id.startsWith('ad:')) removeFromAdCache(id.substring(3));
+ if (id.startsWith('rd:')) removeFromRdCache(id);
+ if (id.startsWith('ad:')) removeFromAdCache(id);
} catch (error) {
if (!disableToast) toast.error(`Error deleting torrent (${id})`);
throw error;
@@ -330,11 +377,11 @@ function Search() {
};
return (
-
+
Debrid Media Manager - Search: {query}
-
+
Search