From 9a3ccba719ded9242737e1114191b83ca4d6ec6e Mon Sep 17 00:00:00 2001 From: Rishi Ghan Date: Sat, 30 Mar 2024 21:41:05 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=A2=20Wiring=20up=20to=20addTorrent=20?= =?UTF-8?q?endpoint=20(#105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🪢 Wiring up to addTorrent endpoint * 🧲 Added a torrent download sub-panel * 🧲 Fixed the auto-population of search box * 🧲 Added downloads panel * 🧲 Surfacing torrent progress in UI via scheduled job * 🧲 Added visual indicators of torrent progress * 💅🏼 Formatting improvements * 💅🏼 Formatting tweaks to tabs --- .../components/ComicDetail/AirDCPPBundles.tsx | 51 +++++ .../components/ComicDetail/ComicDetail.tsx | 9 +- .../ComicDetail/ComicDetailContainer.tsx | 4 +- .../components/ComicDetail/DownloadsPanel.tsx | 142 +++++++----- .../components/ComicDetail/RawFileDetails.tsx | 2 +- .../components/ComicDetail/TabControls.tsx | 16 +- .../ComicDetail/Tabs/ArchiveOperations.tsx | 11 +- .../ComicDetail/TorrentDownloads.tsx | 77 +++++++ .../ComicDetail/TorrentSearchPanel.tsx | 207 ++++++++++++++---- src/client/components/Library/Library.tsx | 2 +- src/client/components/Search/Search.tsx | 6 +- .../QbittorrentConnectionForm.tsx | 12 +- .../components/WantedComics/WantedComics.tsx | 4 +- src/client/components/shared/DatePicker.tsx | 1 - .../components/shared/MetadataPanel.tsx | 1 - src/client/constants/endpoints.ts | 7 + 16 files changed, 416 insertions(+), 136 deletions(-) create mode 100644 src/client/components/ComicDetail/AirDCPPBundles.tsx create mode 100644 src/client/components/ComicDetail/TorrentDownloads.tsx diff --git a/src/client/components/ComicDetail/AirDCPPBundles.tsx b/src/client/components/ComicDetail/AirDCPPBundles.tsx new file mode 100644 index 0000000..66c47f1 --- /dev/null +++ b/src/client/components/ComicDetail/AirDCPPBundles.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import prettyBytes from "pretty-bytes"; +import dayjs from "dayjs"; +import ellipsize from "ellipsize"; +import { map } from "lodash"; + +export const AirDCPPBundles = (props) => { + return ( +
+ + + + + + + + + + + {map(props.data, (bundle) => ( + + + + + + + ))} + +
+ Filename + + Size + + Download Time + + Bundle ID +
+
{ellipsize(bundle.name, 58)}
+ {ellipsize(bundle.target, 88)} +
+ {prettyBytes(bundle.size)} + + {dayjs + .unix(bundle.time_finished) + .format("h:mm on ddd, D MMM, YYYY")} + + {bundle.id} +
+
+ ); +}; diff --git a/src/client/components/ComicDetail/ComicDetail.tsx b/src/client/components/ComicDetail/ComicDetail.tsx index 4c52279..293982c 100644 --- a/src/client/components/ComicDetail/ComicDetail.tsx +++ b/src/client/components/ComicDetail/ComicDetail.tsx @@ -351,13 +351,18 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { ), name: "Torrent Search", - content: , + content: , shouldShow: true, }, { id: 6, name: "Downloads", - icon: <>{acquisition?.directconnect?.downloads?.length}, + icon: ( + <> + {acquisition?.directconnect?.downloads?.length + + acquisition?.torrent.length} + + ), content: !isNil(data.data) && !isEmpty(data.data) ? ( diff --git a/src/client/components/ComicDetail/ComicDetailContainer.tsx b/src/client/components/ComicDetail/ComicDetailContainer.tsx index 4701f3f..312def2 100644 --- a/src/client/components/ComicDetail/ComicDetailContainer.tsx +++ b/src/client/components/ComicDetail/ComicDetailContainer.tsx @@ -1,7 +1,5 @@ -import { isEmpty, isNil, isUndefined } from "lodash"; -import React, { ReactElement, useContext, useEffect, useState } from "react"; +import React, { ReactElement } from "react"; import { useParams } from "react-router-dom"; -import { getComicBookDetailById } from "../../actions/comicinfo.actions"; import { ComicDetail } from "../ComicDetail/ComicDetail"; import { useQuery } from "@tanstack/react-query"; import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints"; diff --git a/src/client/components/ComicDetail/DownloadsPanel.tsx b/src/client/components/ComicDetail/DownloadsPanel.tsx index 924e03c..eb385c3 100644 --- a/src/client/components/ComicDetail/DownloadsPanel.tsx +++ b/src/client/components/ComicDetail/DownloadsPanel.tsx @@ -1,12 +1,15 @@ import React, { useEffect, useContext, ReactElement, useState } from "react"; import { RootState } from "threetwo-ui-typings"; import { isEmpty, map } from "lodash"; -import prettyBytes from "pretty-bytes"; -import dayjs from "dayjs"; -import ellipsize from "ellipsize"; +import { AirDCPPBundles } from "./AirDCPPBundles"; +import { TorrentDownloads } from "./TorrentDownloads"; import { useQuery } from "@tanstack/react-query"; import axios from "axios"; -import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints"; +import { + LIBRARY_SERVICE_BASE_URI, + QBITTORRENT_SERVICE_BASE_URI, + TORRENT_JOB_SERVICE_BASE_URI, +} from "../../constants/endpoints"; import { useStore } from "../../store"; import { useShallow } from "zustand/react/shallow"; import { useParams } from "react-router-dom"; @@ -20,12 +23,27 @@ export const DownloadsPanel = ( ): ReactElement | null => { const { comicObjectId } = useParams<{ comicObjectId: string }>(); const [bundles, setBundles] = useState([]); - const { airDCPPSocketInstance } = useStore( - useShallow((state) => ({ + const [infoHashes, setInfoHashes] = useState([]); + const [torrentDetails, setTorrentDetails] = useState([]); + const [activeTab, setActiveTab] = useState("torrents"); + const { airDCPPSocketInstance, socketIOInstance } = useStore( + useShallow((state: any) => ({ airDCPPSocketInstance: state.airDCPPSocketInstance, + socketIOInstance: state.socketIOInstance, })), ); + // React to torrent progress data sent over websockets + socketIOInstance.on("AS_TORRENT_DATA", (data) => { + const torrents = data.torrents + .flatMap(({ _id, details }) => { + if (_id === comicObjectId) { + return details; + } + }) + .filter((item) => item !== undefined); + setTorrentDetails(torrents); + }); // Fetch the downloaded files and currently-downloading file(s) from AirDC++ const { data: comicObject, isSuccess } = useQuery({ queryKey: ["bundles"], @@ -54,65 +72,75 @@ export const DownloadsPanel = ( } }; + // Call the scheduled job for fetching torrent data + // triggered by the active tab been set to "torrents" + const { data: torrentData } = useQuery({ + queryFn: () => + axios({ + url: `${TORRENT_JOB_SERVICE_BASE_URI}/getTorrentData`, + method: "GET", + params: { + trigger: activeTab, + }, + }), + queryKey: [activeTab], + }); + useEffect(() => { getBundles(comicObject).then((result) => { setBundles(result); }); }, [comicObject]); - const Bundles = (props) => { - return ( -
- - - - - - - - - - - {map(props.data, (bundle) => ( - - - - - - - ))} - -
- Filename - - Size - - Download Time - - Bundle ID -
-
{ellipsize(bundle.name, 58)}
- - {ellipsize(bundle.target, 88)} - -
- {prettyBytes(bundle.size)} - - {dayjs - .unix(bundle.time_finished) - .format("h:mm on ddd, D MMM, YYYY")} - - {bundle.id} -
-
- ); - }; - return (
- {!isEmpty(airDCPPSocketInstance) && !isEmpty(bundles) && ( - - )} + {!isEmpty(airDCPPSocketInstance) && + !isEmpty(bundles) && + activeTab === "directconnect" && } + +
+
+ + + +
+ + +
+ + {activeTab === "torrents" && }
); }; diff --git a/src/client/components/ComicDetail/RawFileDetails.tsx b/src/client/components/ComicDetail/RawFileDetails.tsx index 25fcf59..3811038 100644 --- a/src/client/components/ComicDetail/RawFileDetails.tsx +++ b/src/client/components/ComicDetail/RawFileDetails.tsx @@ -48,7 +48,7 @@ export const RawFileDetails = (props): ReactElement => {
{/* File extension */} - + diff --git a/src/client/components/ComicDetail/TabControls.tsx b/src/client/components/ComicDetail/TabControls.tsx index 636abce..e3c5f15 100644 --- a/src/client/components/ComicDetail/TabControls.tsx +++ b/src/client/components/ComicDetail/TabControls.tsx @@ -1,15 +1,9 @@ -import React, { ReactElement, useEffect, useState } from "react"; +import React, { ReactElement, useState } from "react"; import { isNil } from "lodash"; export const TabControls = (props): ReactElement => { - // const comicBookDetailData = useSelector( - // (state: RootState) => state.comicInfo.comicBookDetail, - // ); const { filteredTabs, downloadCount } = props; const [active, setActive] = useState(filteredTabs[0].id); - // useEffect(() => { - // setActive(filteredTabs[0].id); - // }, [filteredTabs]); return ( <> @@ -19,7 +13,11 @@ export const TabControls = (props): ReactElement => { {filteredTabs.map(({ id, name, icon }) => ( setActive(id)} > @@ -28,7 +26,7 @@ export const TabControls = (props): ReactElement => { {id === 6 && !isNil(downloadCount) ? ( {/* download count */} - + {icon} diff --git a/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx b/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx index 6304884..d1edfc9 100644 --- a/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx +++ b/src/client/components/ComicDetail/Tabs/ArchiveOperations.tsx @@ -30,6 +30,8 @@ export const ArchiveOperations = (props): ReactElement => { const [currentImage, setCurrentImage] = useState([]); const [uncompressedArchive, setUncompressedArchive] = useState([]); const [imageAnalysisResult, setImageAnalysisResult] = useState({}); + const [shouldRefetchComicBookData, setShouldRefetchComicBookData] = + useState(false); const constructImagePaths = (data): Array => { return data?.map((path: string) => escapePoundSymbol(encodeURI(`${LIBRARY_SERVICE_HOST}/${path}`)), @@ -63,6 +65,7 @@ export const ArchiveOperations = (props): ReactElement => { if (isMounted) { setUncompressedArchive(uncompressedArchive); + setShouldRefetchComicBookData(true); } }, }); @@ -122,8 +125,9 @@ export const ArchiveOperations = (props): ReactElement => { enabled: false, }); - if (isSuccess) { + if (isSuccess && shouldRefetchComicBookData) { queryClient.invalidateQueries({ queryKey: ["comicBookMetadata"] }); + setShouldRefetchComicBookData(false); } // sliding panel init @@ -171,7 +175,8 @@ export const ArchiveOperations = (props): ReactElement => {
- {data.rawFileDetails.archive?.uncompressed ? ( + {data.rawFileDetails.archive?.uncompressed && + !isEmpty(uncompressedArchive) ? (
{ ) : null}
- {!data.rawFileDetails?.archive?.uncompressed ? ( + {isEmpty(uncompressedArchive) ? ( + {({ input, meta }) => ( +
+
+
+ {/* Icon placeholder */} +
+ +
- ); - }} +
+ )} )} />
+ +
+
+ The default search term is an auto-detected title; you may need to + change it to get better matches if the auto-detected one doesn't work. +
+
+ {!isEmpty(data?.data) ? ( +
+ + + + + + + + + + + {data?.data.map((result, idx) => ( + + + + + + + + ))} + +
+ Name + + Indexer + + Action +
+

{ellipsize(result.fileName, 90)}

+ {/* Seeders/Leechers */} +
+ + + + + + + {result.seeders} seeders + + + + + + + + + + {result.leechers} leechers + + + {/* Size */} + + + + + + + {prettyBytes(result.size)} + + + + {/* Files */} + + + + + + + {result.files} files + + +
+
+ {result.indexer} + + +
+
+ ) : null} ); }; diff --git a/src/client/components/Library/Library.tsx b/src/client/components/Library/Library.tsx index 6d1f299..3d4bde2 100644 --- a/src/client/components/Library/Library.tsx +++ b/src/client/components/Library/Library.tsx @@ -178,7 +178,7 @@ export const Library = (): ReactElement => { - Torrent: {info.getValue().torrent.downloads.length} + Torrent: {info.getValue().torrent.length}
diff --git a/src/client/components/Search/Search.tsx b/src/client/components/Search/Search.tsx index 07fe3db..ecc386d 100644 --- a/src/client/components/Search/Search.tsx +++ b/src/client/components/Search/Search.tsx @@ -1,9 +1,7 @@ import React, { useCallback, ReactElement, useState } from "react"; import { isNil, isEmpty } from "lodash"; import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings"; -import { importToDB } from "../../actions/fileops.actions"; -import { comicinfoAPICall } from "../../actions/comicinfo.actions"; -import { search } from "../../services/api/SearchApi"; + import { Form, Field } from "react-final-form"; import Card from "../shared/Carda"; import ellipsize from "ellipsize"; @@ -27,7 +25,6 @@ export const Search = ({}: ISearchProps): ReactElement => { const [comicVineMetadata, setComicVineMetadata] = useState({}); const getCVSearchResults = (searchQuery) => { setSearchQuery(searchQuery.search); - // queryClient.invalidateQueries({ queryKey: ["comicvineSearchResults"] }); }; const { @@ -146,6 +143,7 @@ export const Search = ({}: ISearchProps): ReactElement => { )} /> + {isLoading && <>Loading kaka...} {!isNil(comicVineSearchResults?.data.results) && !isEmpty(comicVineSearchResults?.data.results) ? (
diff --git a/src/client/components/Settings/QbittorrentSettings/QbittorrentConnectionForm.tsx b/src/client/components/Settings/QbittorrentSettings/QbittorrentConnectionForm.tsx index 8b74121..e75ab4c 100644 --- a/src/client/components/Settings/QbittorrentSettings/QbittorrentConnectionForm.tsx +++ b/src/client/components/Settings/QbittorrentSettings/QbittorrentConnectionForm.tsx @@ -16,16 +16,7 @@ export const QbittorrentConnectionForm = (): ReactElement => { }); const hostDetails = data?.data?.bittorrent?.client?.host; // connect to qbittorrent client - const { data: connectionDetails } = useQuery({ - queryKey: [], - queryFn: async () => - await axios({ - url: "http://localhost:3060/api/qbittorrent/connect", - method: "POST", - data: hostDetails, - }), - enabled: !!hostDetails, - }); + // get qbittorrent client info const { data: qbittorrentClientInfo } = useQuery({ queryKey: ["qbittorrentClientInfo"], @@ -34,7 +25,6 @@ export const QbittorrentConnectionForm = (): ReactElement => { url: "http://localhost:3060/api/qbittorrent/getClientInfo", method: "GET", }), - enabled: !!connectionDetails, }); // Update action using a mutation const { mutate } = useMutation({ diff --git a/src/client/components/WantedComics/WantedComics.tsx b/src/client/components/WantedComics/WantedComics.tsx index 48435b0..4f06343 100644 --- a/src/client/components/WantedComics/WantedComics.tsx +++ b/src/client/components/WantedComics/WantedComics.tsx @@ -10,6 +10,7 @@ export const WantedComics = (props): ReactElement => { const { data: wantedComics, isSuccess, + isFetched, isError, isLoading, } = useQuery({ @@ -41,6 +42,7 @@ export const WantedComics = (props): ReactElement => { minWidth: 350, accessorFn: (data) => data, cell: (value) => { + console.log("ASDASd", value); const row = value.getValue()._source; return row && ; }, @@ -172,7 +174,7 @@ export const WantedComics = (props): ReactElement => {
- {isSuccess ? ( + {isSuccess && wantedComics?.data.hits?.hits ? (
{ - console.log(props); const { setter, apiAction } = props; const [selected, setSelected] = useState(); const [isPopperOpen, setIsPopperOpen] = useState(false); diff --git a/src/client/components/shared/MetadataPanel.tsx b/src/client/components/shared/MetadataPanel.tsx index f339548..781658d 100644 --- a/src/client/components/shared/MetadataPanel.tsx +++ b/src/client/components/shared/MetadataPanel.tsx @@ -16,7 +16,6 @@ interface IMetadatPanelProps { containerStyle: any; } export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => { - console.log(props); const { rawFileDetails, inferredMetadata, diff --git a/src/client/constants/endpoints.ts b/src/client/constants/endpoints.ts index 30600fa..2fc2125 100644 --- a/src/client/constants/endpoints.ts +++ b/src/client/constants/endpoints.ts @@ -97,3 +97,10 @@ export const PROWLARR_SERVICE_BASE_URI = hostURIBuilder({ port: "3060", apiPath: `/api/prowlarr`, }); + +export const TORRENT_JOB_SERVICE_BASE_URI = hostURIBuilder({ + protocol: "http", + host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", + port: "3000", + apiPath: `/api/torrentjobs`, +});