import React, { useCallback, ReactElement, useEffect, useState } from "react"; import { getBundlesForComic, sleep } from "../../actions/airdcpp.actions"; import { SearchQuery, PriorityEnum, SearchResponse } from "threetwo-ui-typings"; import { RootState, SearchInstance } from "threetwo-ui-typings"; import ellipsize from "ellipsize"; import { Form, Field } from "react-final-form"; import { difference } from "../../shared/utils/object.utils"; import { isEmpty, isNil, map } from "lodash"; import { useStore } from "../../store"; import { useShallow } from "zustand/react/shallow"; import { useQuery } from "@tanstack/react-query"; import axios from "axios"; interface IAcquisitionPanelProps { query: any; comicObjectId: any; comicObject: any; settings: any; } export const AcquisitionPanel = ( props: IAcquisitionPanelProps, ): ReactElement => { const { airDCPPSocketInstance, airDCPPClientConfiguration, airDCPPSessionInformation, airDCPPDownloadTick, } = useStore( useShallow((state) => ({ airDCPPSocketInstance: state.airDCPPSocketInstance, airDCPPClientConfiguration: state.airDCPPClientConfiguration, airDCPPSessionInformation: state.airDCPPSessionInformation, airDCPPDownloadTick: state.airDCPPDownloadTick, })), ); interface SearchData { query: Pick & Partial>; hub_urls: string[] | undefined | null; priority: PriorityEnum; } /** * Get the hubs list from an AirDCPP Socket */ const { data: hubs } = useQuery({ queryKey: ["hubs"], queryFn: async () => await airDCPPSocketInstance.get(`hubs`), }); const { comicObjectId } = props; const issueName = props.query.issue.name || ""; const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " "); const [dcppQuery, setDcppQuery] = useState({}); const [airDCPPSearchResults, setAirDCPPSearchResults] = useState([]); const [airDCPPSearchStatus, setAirDCPPSearchStatus] = useState(false); const [airDCPPSearchInstance, setAirDCPPSearchInstance] = useState({}); const [airDCPPSearchInfo, setAirDCPPSearchInfo] = useState({}); // Construct a AirDC++ query based on metadata inferred, upon component mount // Pre-populate the search input with the search string, so that // All the user has to do is hit "Search AirDC++" useEffect(() => { // AirDC++ search query const dcppSearchQuery = { query: { pattern: `${sanitizedIssueName.replace(/#/g, "")}`, extensions: ["cbz", "cbr", "cb7"], }, hub_urls: map(hubs, (item) => item.value), priority: 5, }; setDcppQuery(dcppSearchQuery); }, []); /** * Method to perform a search via an AirDC++ websocket * @param {SearchData} data - a SearchData query * @param {any} ADCPPSocket - an intialized AirDC++ socket instance */ const search = async (data: SearchData, ADCPPSocket: any) => { try { if (!ADCPPSocket.isConnected()) { await ADCPPSocket(); } const instance: SearchInstance = await ADCPPSocket.post("search"); setAirDCPPSearchStatus(true); // We want to get notified about every new result in order to make the user experience better await ADCPPSocket.addListener( `search`, "search_result_added", async (groupedResult) => { // ...add the received result in the UI // (it's probably a good idea to have some kind of throttling for the UI updates as there can be thousands of results) setAirDCPPSearchResults((state) => [...state, groupedResult]); }, instance.id, ); // We also want to update the existing items in our list when new hits arrive for the previously listed files/directories await ADCPPSocket.addListener( `search`, "search_result_updated", async (groupedResult) => { // ...update properties of the existing result in the UI const bundleToUpdateIndex = airDCPPSearchResults?.findIndex( (bundle) => bundle.result.id === groupedResult.result.id, ); const updatedState = [...airDCPPSearchResults]; if ( !isNil(difference(updatedState[bundleToUpdateIndex], groupedResult)) ) { updatedState[bundleToUpdateIndex] = groupedResult; } setAirDCPPSearchResults((state) => [...state, ...updatedState]); }, instance.id, ); // We need to show something to the user in case the search won't yield any results so that he won't be waiting forever) // Wait for 5 seconds for any results to arrive after the searches were sent to the hubs await ADCPPSocket.addListener( `search`, "search_hub_searches_sent", async (searchInfo) => { await sleep(5000); // Check the number of received results (in real use cases we should know that even without calling the API) const currentInstance = await ADCPPSocket.get( `search/${instance.id}`, ); setAirDCPPSearchInstance(currentInstance); setAirDCPPSearchInfo(searchInfo); console.log("Asdas", airDCPPSearchInfo); if (currentInstance.result_count === 0) { // ...nothing was received, show an informative message to the user console.log("No more search results."); } // The search can now be considered to be "complete" // If there's an "in progress" indicator in the UI, that could also be disabled here setAirDCPPSearchInstance(instance); setAirDCPPSearchStatus(false); }, instance.id, ); // Finally, perform the actual search await ADCPPSocket.post(`search/${instance.id}/hub_search`, data); } catch (error) { console.log(error); throw error; } }; /** * Method to download a bundle associated with a search result from AirDC++ * @param {Number} searchInstanceId - description * @param {String} resultId - description * @param {String} comicObjectId - description * @param {String} name - description * @param {Number} size - description * @param {any} type - description * @param {any} ADCPPSocket - description * @returns {void} - description */ const download = async ( searchInstanceId: Number, resultId: String, comicObjectId: String, name: String, size: Number, type: any, ADCPPSocket: any, ): void => { try { if (!ADCPPSocket.isConnected()) { await ADCPPSocket.connect(); } let bundleDBImportResult = {}; const downloadResult = await ADCPPSocket.post( `search/${searchInstanceId}/results/${resultId}/download`, ); if (!isNil(downloadResult)) { bundleDBImportResult = await axios({ method: "POST", url: `http://localhost:3000/api/library/applyAirDCPPDownloadMetadata`, headers: { "Content-Type": "application/json; charset=utf-8", }, data: { bundleId: downloadResult.bundle_info.id, comicObjectId, name, size, type, }, }); // dispatch({ // type: AIRDCPP_RESULT_DOWNLOAD_INITIATED, // downloadResult, // bundleDBImportResult, // }); // // dispatch({ // type: IMS_COMIC_BOOK_DB_OBJECT_FETCHED, // comicBookDetail: bundleDBImportResult.data, // IMS_inProgress: false, // }); } } catch (error) { throw error; } }; const getDCPPSearchResults = async (searchQuery) => { const manualQuery = { query: { pattern: `${searchQuery.issueName}`, extensions: ["cbz", "cbr", "cb7"], }, hub_urls: map(hubs, (hub) => hub.hub_url), priority: 5, }; search(manualQuery, airDCPPSocketInstance); }; // download via AirDC++ const downloadDCPPResult = useCallback( (searchInstanceId, resultId, name, size, type) => { // dispatch( // downloadAirDCPPItem( // searchInstanceId, // resultId, // props.comicObjectId, // name, // size, // type, // airDCPPConfiguration.airDCPPState.socket, // { // username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`, // password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`, // }, // ), // ); // this is to update the download count badge on the downloads tab // dispatch( // getBundlesForComic( // props.comicObjectId, // airDCPPConfiguration.airDCPPState.socket, // { // username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`, // password: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.password}`, // }, // ), // ); }, [], ); return ( <>
{!isEmpty(airDCPPSocketInstance) ? (
(
{({ input, meta }) => { return (
); }}
)} /> ) : (
AirDC++ is not configured. Please configure it in{" "} Settings > AirDC++ > Connection.
)}
{/* AirDC++ search instance details */} {!isNil(airDCPPSearchInstance) && !isEmpty(airDCPPSearchInfo) && !isNil(hubs) && (
{hubs.map((value, idx) => ( {value.identity.name} ))}
Query: {airDCPPSearchInfo.query.pattern}
Extensions: {airDCPPSearchInfo.query.extensions.join(", ")}
File type: {airDCPPSearchInfo.query.file_type}
Search Instance: {airDCPPSearchInstance.id}
Owned by {airDCPPSearchInstance.owner}
Expires in: {airDCPPSearchInstance.expires_in}
)} {/* AirDC++ results */}
{!isNil(airDCPPSearchResults) && !isEmpty(airDCPPSearchResults) ? (
{map(airDCPPSearchResults, ({ result }, idx) => { return ( ); })}
Name Type Slots Actions

{result.type.id === "directory" ? ( ) : null} {ellipsize(result.name, 70)}

{!isNil(result.dupe) ? ( Dupe ) : null} {result.users.user.nicks} {result.users.user.flags.map((flag, idx) => ( {flag} ))}
{result.type.id === "directory" ? "directory" : result.type.str}
{result.slots.free} free {result.slots.total}
) : (

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.

Searching via AirDC++ is still in alpha. Some searches may take arbitrarily long, or may not work at all. Searches from ADCS hubs are more reliable than NMDCS ones.

)}
); }; export default AcquisitionPanel;