import React, { useCallback, ReactElement, useEffect, useState } from "react"; 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, useQueryClient } from "@tanstack/react-query"; import axios from "axios"; import { AIRDCPP_SERVICE_BASE_URI } from "../../constants/endpoints"; interface IAcquisitionPanelProps { query: any; comicObjectId: any; comicObject: any; settings: any; } export const AcquisitionPanel = ( props: IAcquisitionPanelProps, ): ReactElement => { const { socketIOInstance } = useStore( useShallow((state) => ({ socketIOInstance: state.socketIOInstance, })), ); interface SearchData { query: Pick & Partial>; hub_urls: string[] | undefined | null; priority: PriorityEnum; } interface SearchResult { id: string; // Add other properties as needed slots: any; type: any; users: any; name: string; dupe: Boolean; size: number; } const handleSearch = (searchQuery) => { // Use the already connected socket instance to emit events socketIOInstance.emit("initiateSearch", searchQuery); }; const { data: settings, isLoading, isError, } = useQuery({ queryKey: ["settings"], queryFn: async () => await axios({ url: "http://localhost:3000/api/settings/getAllSettings", method: "GET", }), }); /** * Get the hubs list from an AirDCPP Socket */ const { data: hubs } = useQuery({ queryKey: ["hubs"], queryFn: async () => await axios({ url: `${AIRDCPP_SERVICE_BASE_URI}/getHubs`, method: "POST", data: { host: settings?.data.directConnect?.client?.host, }, }), enabled: !isEmpty(settings?.data.directConnect?.client?.host), }); 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< SearchResult[] >([]); const [airDCPPSearchStatus, setAirDCPPSearchStatus] = useState(false); const [airDCPPSearchInstance, setAirDCPPSearchInstance] = useState({}); const [airDCPPSearchInfo, setAirDCPPSearchInfo] = useState({}); const queryClient = useQueryClient(); // 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++" to perform a search useEffect(() => { // AirDC++ search query const dcppSearchQuery = { query: { pattern: `${sanitizedIssueName.replace(/#/g, "")}`, extensions: ["cbz", "cbr", "cb7"], }, hub_urls: map(hubs?.data, (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 (searchData: any) => { setAirDCPPSearchResults([]); socketIOInstance.emit("call", "socket.search", { query: searchData, config: { protocol: `ws`, // hostname: `192.168.1.119:5600`, hostname: `127.0.0.1:5600`, username: `user`, password: `pass`, }, }); }; socketIOInstance.on("searchResultAdded", ({ result }: any) => { setAirDCPPSearchResults((previousState) => { const exists = previousState.some((item) => result.id === item.id); if (!exists) { return [...previousState, result]; } return previousState; }); }); socketIOInstance.on("searchResultUpdated", ({ result }: any) => { // ...update properties of the existing result in the UI const bundleToUpdateIndex = airDCPPSearchResults?.findIndex( (bundle) => bundle.id === result.id, ); const updatedState = [...airDCPPSearchResults]; if (!isNil(difference(updatedState[bundleToUpdateIndex], result))) { updatedState[bundleToUpdateIndex] = result; } setAirDCPPSearchResults((state) => [...state, ...updatedState]); }); socketIOInstance.on("searchInitiated", (data) => { setAirDCPPSearchInstance(data.instance); }); socketIOInstance.on("searchesSent", (data) => { setAirDCPPSearchInfo(data.searchInfo); }); /** * 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} config - description * @returns {void} - description */ const download = async ( searchInstanceId: Number, resultId: String, comicObjectId: String, name: String, size: Number, type: any, config: any, ): Promise => { socketIOInstance.emit( "call", "socket.download", { searchInstanceId, resultId, comicObjectId, name, size, type, config, }, (data: any) => console.log(data), ); }; const getDCPPSearchResults = async (searchQuery) => { const manualQuery = { query: { pattern: `${searchQuery.issueName}`, extensions: ["cbz", "cbr", "cb7"], }, hub_urls: [hubs?.data[0].hub_url], priority: 5, }; search(manualQuery); }; return ( <>
{!isEmpty(hubs?.data) ? (
( {({ input, meta }) => { return (
); }}
)} /> ) : (
No AirDC++ hub configured. Please configure it in{" "} Settings > AirDC++ > Hubs.
)}
{/* configured hub */} {!isEmpty(hubs?.data) && ( {hubs && hubs?.data[0].hub_url} )} {/* AirDC++ search instance details */} {!isNil(airDCPPSearchInstance) && !isEmpty(airDCPPSearchInfo) && !isNil(hubs) && (
{hubs?.data.map((value, idx: string) => ( {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, ({ dupe, type, name, id, slots, users, size }, idx) => { return ( ); }, )}
Name Type Slots Actions

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

{!isNil(dupe) ? ( Dupe ) : null} {/* Nicks */} {users.user.nicks} {/* Flags */} {users.user.flags.map((flag, idx) => ( {flag} ))}
{/* Extension */} {type.str} {/* Slots */} {slots.total} slots; {slots.free} free
) : (
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;