🪢 Wiring up to addTorrent endpoint #105
51
src/client/components/ComicDetail/AirDCPPBundles.tsx
Normal file
51
src/client/components/ComicDetail/AirDCPPBundles.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="overflow-x-auto w-fit mt-4 rounded-lg border border-gray-200">
|
||||||
|
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-200 text-md">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
|
Filename
|
||||||
|
</th>
|
||||||
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
|
Size
|
||||||
|
</th>
|
||||||
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
|
Download Time
|
||||||
|
</th>
|
||||||
|
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
||||||
|
Bundle ID
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{map(props.data, (bundle) => (
|
||||||
|
<tr key={bundle.id} className="text-sm">
|
||||||
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||||
|
<h5>{ellipsize(bundle.name, 58)}</h5>
|
||||||
|
<span className="text-xs">{ellipsize(bundle.target, 88)}</span>
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||||
|
{prettyBytes(bundle.size)}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||||
|
{dayjs
|
||||||
|
.unix(bundle.time_finished)
|
||||||
|
.format("h:mm on ddd, D MMM, YYYY")}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||||
|
<span className="tag is-warning">{bundle.id}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -351,7 +351,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
name: "Torrent Search",
|
name: "Torrent Search",
|
||||||
content: <TorrentSearchPanel />,
|
content: <TorrentSearchPanel comicObjectId={_id} />,
|
||||||
shouldShow: true,
|
shouldShow: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React, { useEffect, useContext, ReactElement, useState } from "react";
|
import React, { useEffect, useContext, ReactElement, useState } from "react";
|
||||||
import { RootState } from "threetwo-ui-typings";
|
import { RootState } from "threetwo-ui-typings";
|
||||||
import { isEmpty, map } from "lodash";
|
import { isEmpty, map } from "lodash";
|
||||||
import prettyBytes from "pretty-bytes";
|
import { AirDCPPBundles } from "./AirDCPPBundles";
|
||||||
import dayjs from "dayjs";
|
import { TorrentDownloads } from "./TorrentDownloads";
|
||||||
import ellipsize from "ellipsize";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
|
import {
|
||||||
|
LIBRARY_SERVICE_BASE_URI,
|
||||||
|
QBITTORRENT_SERVICE_BASE_URI,
|
||||||
|
} from "../../constants/endpoints";
|
||||||
import { useStore } from "../../store";
|
import { useStore } from "../../store";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
@@ -20,6 +22,8 @@ export const DownloadsPanel = (
|
|||||||
): ReactElement | null => {
|
): ReactElement | null => {
|
||||||
const { comicObjectId } = useParams<{ comicObjectId: string }>();
|
const { comicObjectId } = useParams<{ comicObjectId: string }>();
|
||||||
const [bundles, setBundles] = useState([]);
|
const [bundles, setBundles] = useState([]);
|
||||||
|
const [infoHashes, setInfoHashes] = useState<string[]>([]);
|
||||||
|
const [activeTab, setActiveTab] = useState("torrents");
|
||||||
const { airDCPPSocketInstance } = useStore(
|
const { airDCPPSocketInstance } = useStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
airDCPPSocketInstance: state.airDCPPSocketInstance,
|
airDCPPSocketInstance: state.airDCPPSocketInstance,
|
||||||
@@ -42,6 +46,36 @@ export const DownloadsPanel = (
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: qbittorrentConnectionResult } = useQuery({
|
||||||
|
queryFn: async () =>
|
||||||
|
axios({
|
||||||
|
url: `${QBITTORRENT_SERVICE_BASE_URI}/connect`,
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
hostname: "localhost",
|
||||||
|
protocol: "http",
|
||||||
|
port: "8080",
|
||||||
|
username: "admin",
|
||||||
|
password: "password",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
queryKey: ["qbittorrentConnection"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: torrentProperties,
|
||||||
|
isSuccess: torrentPropertiesFetched,
|
||||||
|
isFetching: torrentPropertiesFetching,
|
||||||
|
} = useQuery({
|
||||||
|
queryFn: async () =>
|
||||||
|
await axios({
|
||||||
|
url: `${QBITTORRENT_SERVICE_BASE_URI}/getTorrentDetails`,
|
||||||
|
method: "POST",
|
||||||
|
data: infoHashes,
|
||||||
|
}),
|
||||||
|
queryKey: ["torrentProperties", infoHashes],
|
||||||
|
});
|
||||||
|
|
||||||
const getBundles = async (comicObject) => {
|
const getBundles = async (comicObject) => {
|
||||||
if (comicObject?.data.acquisition.directconnect) {
|
if (comicObject?.data.acquisition.directconnect) {
|
||||||
const filteredBundles =
|
const filteredBundles =
|
||||||
@@ -58,60 +92,65 @@ export const DownloadsPanel = (
|
|||||||
getBundles(comicObject).then((result) => {
|
getBundles(comicObject).then((result) => {
|
||||||
setBundles(result);
|
setBundles(result);
|
||||||
});
|
});
|
||||||
}, [comicObject]);
|
|
||||||
|
|
||||||
const Bundles = (props) => {
|
if (comicObject?.data.acquisition.torrent.length !== 0) {
|
||||||
return (
|
// Use the functional form of setInfoHashes to avoid race conditions
|
||||||
<div className="overflow-x-auto w-fit mt-4 rounded-lg border border-gray-200">
|
setInfoHashes(() => {
|
||||||
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-200 text-md">
|
// Extract infoHashes from torrents and remove duplicates
|
||||||
<thead>
|
const newInfoHashes: any = [
|
||||||
<tr>
|
...new Set(
|
||||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
comicObject?.data.acquisition.torrent.map(
|
||||||
Filename
|
(torrent) => torrent.infoHash,
|
||||||
</th>
|
),
|
||||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
),
|
||||||
Size
|
];
|
||||||
</th>
|
return newInfoHashes;
|
||||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
});
|
||||||
Download Time
|
}
|
||||||
</th>
|
}, [comicObject]);
|
||||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
|
||||||
Bundle ID
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
{map(props.data, (bundle) => (
|
|
||||||
<tr key={bundle.id} className="text-sm">
|
|
||||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
|
||||||
<h5>{ellipsize(bundle.name, 58)}</h5>
|
|
||||||
<span className="text-xs">
|
|
||||||
{ellipsize(bundle.target, 88)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
|
||||||
{prettyBytes(bundle.size)}
|
|
||||||
</td>
|
|
||||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
|
||||||
{dayjs
|
|
||||||
.unix(bundle.time_finished)
|
|
||||||
.format("h:mm on ddd, D MMM, YYYY")}
|
|
||||||
</td>
|
|
||||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
|
||||||
<span className="tag is-warning">{bundle.id}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="columns is-multiline">
|
<div className="columns is-multiline">
|
||||||
{!isEmpty(airDCPPSocketInstance) && !isEmpty(bundles) && (
|
{!isEmpty(airDCPPSocketInstance) &&
|
||||||
<Bundles data={bundles} />
|
!isEmpty(bundles) &&
|
||||||
|
activeTab === "directconnect" && <AirDCPPBundles data={bundles} />}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="sm:hidden">
|
||||||
|
<label htmlFor="Download Type" className="sr-only">
|
||||||
|
Download Type
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select id="Tab" className="w-full rounded-md border-gray-200">
|
||||||
|
<option>DC++ Downloads</option>
|
||||||
|
<option>Torrents</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<nav className="flex gap-6" aria-label="Tabs">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="shrink-0 rounded-lg p-2 text-sm font-medium text-slate-200 hover:bg-gray-50 hover:text-gray-700"
|
||||||
|
aria-current="page"
|
||||||
|
onClick={() => setActiveTab("directconnect")}
|
||||||
|
>
|
||||||
|
DC++ Downloads
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="shrink-0 rounded-lg p-2 text-sm font-medium text-slate-200 hover:bg-gray-50 hover:text-gray-700"
|
||||||
|
onClick={() => setActiveTab("torrents")}
|
||||||
|
>
|
||||||
|
Torrents
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === "torrents" && torrentPropertiesFetched && (
|
||||||
|
<TorrentDownloads data={torrentProperties?.data} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
47
src/client/components/ComicDetail/TorrentDownloads.tsx
Normal file
47
src/client/components/ComicDetail/TorrentDownloads.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
|
||||||
|
export const TorrentDownloads = (props) => {
|
||||||
|
const { data } = props;
|
||||||
|
console.log(data);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{data.map((torrent) => {
|
||||||
|
return (
|
||||||
|
<dl>
|
||||||
|
<dt className="text-lg">{torrent.name}</dt>
|
||||||
|
<p className="text-sm">{torrent.hash}</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
Added on{" "}
|
||||||
|
{dayjs.unix(torrent.addition_date).format("ddd, D MMM, YYYY")}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-4 mt-2">
|
||||||
|
{/* Peers */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--user-hand-up-bold-duotone] h-4 w-4"></i>
|
||||||
|
</span>
|
||||||
|
<span className="text-md text-slate-900">
|
||||||
|
{torrent.peers_total}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Size */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--mirror-right-bold-duotone] h-4 w-4"></i>
|
||||||
|
</span>
|
||||||
|
<span className="text-md text-slate-900">
|
||||||
|
{prettyBytes(torrent.total_size)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TorrentDownloads;
|
||||||
@@ -10,9 +10,10 @@ import {
|
|||||||
import { isEmpty, isNil } from "lodash";
|
import { isEmpty, isNil } from "lodash";
|
||||||
|
|
||||||
export const TorrentSearchPanel = (props): ReactElement => {
|
export const TorrentSearchPanel = (props): ReactElement => {
|
||||||
|
const { comicObjectId } = props;
|
||||||
const [prowlarrSettingsData, setProwlarrSettingsData] = useState({});
|
const [prowlarrSettingsData, setProwlarrSettingsData] = useState({});
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [torrentToDownload, setTorrentToDownload] = useState([]);
|
const [torrentToDownload, setTorrentToDownload] = useState("");
|
||||||
|
|
||||||
const { data: qbittorrentConnectionResult } = useQuery({
|
const { data: qbittorrentConnectionResult } = useQuery({
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
@@ -29,6 +30,7 @@ export const TorrentSearchPanel = (props): ReactElement => {
|
|||||||
}),
|
}),
|
||||||
queryKey: ["qbittorrentConnection"],
|
queryKey: ["qbittorrentConnection"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, isSuccess } = useQuery({
|
const { data, isSuccess } = useQuery({
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
axios({
|
axios({
|
||||||
@@ -57,12 +59,13 @@ export const TorrentSearchPanel = (props): ReactElement => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
data: {
|
data: {
|
||||||
torrentToDownload,
|
torrentToDownload,
|
||||||
|
comicObjectId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
queryKey: ["addTorrentResult", torrentToDownload],
|
queryKey: ["addTorrentResult"],
|
||||||
enabled: !isEmpty(torrentToDownload),
|
enabled: !isNil(torrentToDownload) && searchTerm !== "",
|
||||||
});
|
});
|
||||||
console.log(addTorrentResult);
|
console.log(torrentToDownload);
|
||||||
const searchProwlarrIndexer = (evt) => {
|
const searchProwlarrIndexer = (evt) => {
|
||||||
setSearchTerm(evt.searchTerm);
|
setSearchTerm(evt.searchTerm);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export const Library = (): ReactElement => {
|
|||||||
<i className="icon-[solar--magnet-bold-duotone] w-5 h-5"></i>
|
<i className="icon-[solar--magnet-bold-duotone] w-5 h-5"></i>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-md text-slate-900 dark:text-slate-900">
|
<span className="text-md text-slate-900 dark:text-slate-900">
|
||||||
Torrent: {info.getValue().torrent.downloads.length}
|
Torrent: {info.getValue().torrent.length}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user