🧲 Added a torrent download sub-panel

This commit is contained in:
2024-03-07 05:50:49 -06:00
parent e1da6ddcef
commit 41a9428729
6 changed files with 200 additions and 60 deletions

View 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>
);
};

View File

@@ -351,7 +351,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
</span>
),
name: "Torrent Search",
content: <TorrentSearchPanel />,
content: <TorrentSearchPanel comicObjectId={_id} />,
shouldShow: true,
},
{

View File

@@ -1,12 +1,14 @@
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,
} from "../../constants/endpoints";
import { useStore } from "../../store";
import { useShallow } from "zustand/react/shallow";
import { useParams } from "react-router-dom";
@@ -20,6 +22,8 @@ export const DownloadsPanel = (
): ReactElement | null => {
const { comicObjectId } = useParams<{ comicObjectId: string }>();
const [bundles, setBundles] = useState([]);
const [infoHashes, setInfoHashes] = useState<string[]>([]);
const [activeTab, setActiveTab] = useState("torrents");
const { airDCPPSocketInstance } = useStore(
useShallow((state) => ({
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) => {
if (comicObject?.data.acquisition.directconnect) {
const filteredBundles =
@@ -58,60 +92,65 @@ export const DownloadsPanel = (
getBundles(comicObject).then((result) => {
setBundles(result);
});
}, [comicObject]);
const Bundles = (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>
);
};
if (comicObject?.data.acquisition.torrent.length !== 0) {
// Use the functional form of setInfoHashes to avoid race conditions
setInfoHashes(() => {
// Extract infoHashes from torrents and remove duplicates
const newInfoHashes: any = [
...new Set(
comicObject?.data.acquisition.torrent.map(
(torrent) => torrent.infoHash,
),
),
];
return newInfoHashes;
});
}
}, [comicObject]);
return (
<div className="columns is-multiline">
{!isEmpty(airDCPPSocketInstance) && !isEmpty(bundles) && (
<Bundles data={bundles} />
{!isEmpty(airDCPPSocketInstance) &&
!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>
);

View 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;

View File

@@ -10,9 +10,10 @@ import {
import { isEmpty, isNil } from "lodash";
export const TorrentSearchPanel = (props): ReactElement => {
const { comicObjectId } = props;
const [prowlarrSettingsData, setProwlarrSettingsData] = useState({});
const [searchTerm, setSearchTerm] = useState("");
const [torrentToDownload, setTorrentToDownload] = useState([]);
const [torrentToDownload, setTorrentToDownload] = useState("");
const { data: qbittorrentConnectionResult } = useQuery({
queryFn: async () =>
@@ -29,6 +30,7 @@ export const TorrentSearchPanel = (props): ReactElement => {
}),
queryKey: ["qbittorrentConnection"],
});
const { data, isSuccess } = useQuery({
queryFn: async () =>
axios({
@@ -57,12 +59,13 @@ export const TorrentSearchPanel = (props): ReactElement => {
method: "POST",
data: {
torrentToDownload,
comicObjectId,
},
}),
queryKey: ["addTorrentResult", torrentToDownload],
enabled: !isEmpty(torrentToDownload),
queryKey: ["addTorrentResult"],
enabled: !isNil(torrentToDownload) && searchTerm !== "",
});
console.log(addTorrentResult);
console.log(torrentToDownload);
const searchProwlarrIndexer = (evt) => {
setSearchTerm(evt.searchTerm);
};

View File

@@ -178,7 +178,7 @@ export const Library = (): ReactElement => {
<i className="icon-[solar--magnet-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-900 dark:text-slate-900">
Torrent: {info.getValue().torrent.downloads.length}
Torrent: {info.getValue().torrent.length}
</span>
</span>
</div>