diff --git a/src/client/components/ComicDetail/AcquisitionPanel.tsx b/src/client/components/ComicDetail/AcquisitionPanel.tsx index 34b772a..b54ea3f 100644 --- a/src/client/components/ComicDetail/AcquisitionPanel.tsx +++ b/src/client/components/ComicDetail/AcquisitionPanel.tsx @@ -10,6 +10,7 @@ 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; @@ -21,9 +22,8 @@ interface IAcquisitionPanelProps { export const AcquisitionPanel = ( props: IAcquisitionPanelProps, ): ReactElement => { - const { airDCPPSocketInstance, socketIOInstance } = useStore( + const { socketIOInstance } = useStore( useShallow((state) => ({ - airDCPPSocketInstance: state.airDCPPSocketInstance, socketIOInstance: state.socketIOInstance, })), ); @@ -45,12 +45,33 @@ export const AcquisitionPanel = ( // 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 airDCPPSocketInstance.get(`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 || ""; @@ -75,7 +96,7 @@ export const AcquisitionPanel = ( pattern: `${sanitizedIssueName.replace(/#/g, "")}`, extensions: ["cbz", "cbr", "cb7"], }, - hub_urls: map(hubs, (item) => item.value), + hub_urls: map(hubs?.data, (item) => item.value), priority: 5, }; setDcppQuery(dcppSearchQuery); @@ -116,7 +137,7 @@ export const AcquisitionPanel = ( }); }); - socketIOInstance.on("searchResultUpdated", (groupedResult) => { + socketIOInstance.on("searchResultUpdated", (groupedResult: SearchResult) => { // ...update properties of the existing result in the UI const bundleToUpdateIndex = airDCPPSearchResults?.findIndex( (bundle) => bundle.result.id === groupedResult.result.id, @@ -176,7 +197,7 @@ export const AcquisitionPanel = ( pattern: `${searchQuery.issueName}`, extensions: ["cbz", "cbr", "cb7"], }, - hub_urls: map(hubs, (hub) => hub.hub_url), + hub_urls: map(hubs?.data, (hub) => hub.hub_url), priority: 5, }; @@ -186,7 +207,7 @@ export const AcquisitionPanel = ( return ( <>
- {!isEmpty(airDCPPSocketInstance) ? ( + {!isEmpty(hubs?.data) ? (
- {hubs.map((value, idx) => ( + {hubs?.data.map((value, idx) => ( {value.identity.name} diff --git a/src/client/components/ComicDetail/DownloadsPanel.tsx b/src/client/components/ComicDetail/DownloadsPanel.tsx index eb385c3..2a4a9d8 100644 --- a/src/client/components/ComicDetail/DownloadsPanel.tsx +++ b/src/client/components/ComicDetail/DownloadsPanel.tsx @@ -59,7 +59,6 @@ export const DownloadsPanel = ( }, }), }); - const getBundles = async (comicObject) => { if (comicObject?.data.acquisition.directconnect) { const filteredBundles = @@ -94,10 +93,6 @@ export const DownloadsPanel = ( return (
- {!isEmpty(airDCPPSocketInstance) && - !isEmpty(bundles) && - activeTab === "directconnect" && } -
{activeTab === "torrents" && } + {!isEmpty(airDCPPSocketInstance) && + !isEmpty(bundles) && + activeTab === "directconnect" && }
); }; diff --git a/src/client/components/Settings/AirDCPPSettings/AirDCPPHubsForm.tsx b/src/client/components/Settings/AirDCPPSettings/AirDCPPHubsForm.tsx index 64efcc4..21e61d6 100644 --- a/src/client/components/Settings/AirDCPPSettings/AirDCPPHubsForm.tsx +++ b/src/client/components/Settings/AirDCPPSettings/AirDCPPHubsForm.tsx @@ -3,20 +3,11 @@ import { Form, Field } from "react-final-form"; import { isEmpty, isNil, isUndefined } from "lodash"; import Select from "react-select"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { useStore } from "../../../store"; import axios from "axios"; +import { AIRDCPP_SERVICE_BASE_URI } from "../../../constants/endpoints"; export const AirDCPPHubsForm = (): ReactElement => { const queryClient = useQueryClient(); - const { - airDCPPSocketInstance, - airDCPPClientConfiguration, - airDCPPSessionInformation, - } = useStore((state) => ({ - airDCPPSocketInstance: state.airDCPPSocketInstance, - airDCPPClientConfiguration: state.airDCPPClientConfiguration, - airDCPPSessionInformation: state.airDCPPSessionInformation, - })); const { data: settings, @@ -36,11 +27,19 @@ export const AirDCPPHubsForm = (): ReactElement => { */ const { data: hubs } = useQuery({ queryKey: ["hubs"], - queryFn: async () => await airDCPPSocketInstance.get(`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), }); let hubList = {}; if (!isNil(hubs)) { - hubList = hubs.map(({ hub_url, identity }) => ({ + hubList = hubs?.data.map(({ hub_url, identity }) => ({ value: hub_url, label: identity.name, })); @@ -101,7 +100,10 @@ export const AirDCPPHubsForm = (): ReactElement => { /> ) : ( <> -
+
No configured hubs detected in AirDC++.
Configure to a hub in AirDC++ and then select a default hub here. diff --git a/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsForm.tsx b/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsForm.tsx index d41e652..44f1794 100644 --- a/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsForm.tsx +++ b/src/client/components/Settings/AirDCPPSettings/AirDCPPSettingsForm.tsx @@ -1,76 +1,65 @@ -import React, { ReactElement, useCallback } from "react"; +import React, { useState, useEffect } from "react"; import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation"; -import { isUndefined, isEmpty } from "lodash"; import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm"; -import { initializeAirDCPPSocket, useStore } from "../../../store/index"; -import { useShallow } from "zustand/react/shallow"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import axios from "axios"; +import { + AIRDCPP_SERVICE_BASE_URI, + SETTINGS_SERVICE_BASE_URI, +} from "../../../constants/endpoints"; -export const AirDCPPSettingsForm = (): ReactElement => { - // cherry-picking selectors for: - // 1. initial values for the form - // 2. If initial values are present, get the socket information to display - const { setState } = useStore; - const { - airDCPPSocketConnected, - airDCPPDisconnectionInfo, - airDCPPSessionInformation, - airDCPPClientConfiguration, - airDCPPSocketInstance, - setAirDCPPSocketInstance, - } = useStore( - useShallow((state) => ({ - airDCPPSocketConnected: state.airDCPPSocketConnected, - airDCPPDisconnectionInfo: state.airDCPPDisconnectionInfo, - airDCPPClientConfiguration: state.airDCPPClientConfiguration, - airDCPPSessionInformation: state.airDCPPSessionInformation, - airDCPPSocketInstance: state.airDCPPSocketInstance, - setAirDCPPSocketInstance: state.setAirDCPPSocketInstance, - })), - ); +export const AirDCPPSettingsForm = () => { + const [airDCPPSessionInformation, setAirDCPPSessionInformation] = + useState(null); + // Fetching all settings + const { data: settingsData, isSuccess: settingsSuccess } = useQuery({ + queryKey: ["airDCPPSettings"], + queryFn: () => axios.get(`${SETTINGS_SERVICE_BASE_URI}/getAllSettings`), + }); - /** - * Mutation to update settings and subsequently initialize - * AirDC++ socket with those settings - */ + // Fetch session information + const fetchSessionInfo = (host) => { + return axios.post(`${AIRDCPP_SERVICE_BASE_URI}/initialize`, { host }); + }; + + // Use effect to trigger side effects on settings fetch success + useEffect(() => { + if (settingsSuccess && settingsData?.data?.directConnect?.client?.host) { + const host = settingsData.data.directConnect.client.host; + fetchSessionInfo(host).then((response) => { + setAirDCPPSessionInformation(response.data); + }); + } + }, [settingsSuccess, settingsData]); + + // Handle setting update and subsequent AirDC++ initialization const { mutate } = useMutation({ - mutationFn: async (values) => - await axios({ - url: `http://localhost:3000/api/settings/saveSettings`, - method: "POST", - data: { settingsPayload: values, settingsKey: "directConnect" }, - }), - onSuccess: async (values) => { - const { - data: { - directConnect: { - client: { host }, - }, - }, - } = values; - const dcppSocketInstance = await initializeAirDCPPSocket(host); - setState({ - airDCPPClientConfiguration: host, - airDCPPSocketInstance: dcppSocketInstance, + mutationFn: (values) => { + console.log(values); + return axios.post("http://localhost:3000/api/settings/saveSettings", { + settingsPayload: values, + settingsKey: "directConnect", }); }, + onSuccess: async (response) => { + const host = response?.data?.directConnect?.client?.host; + if (host) { + const response = await fetchSessionInfo(host); + setAirDCPPSessionInformation(response.data); + // setState({ airDCPPClientConfiguration: host }); + } + }, }); - const deleteSettingsMutation = useMutation( - async () => - await axios.post("http://localhost:3000/api/settings/saveSettings", { - settingsPayload: {}, - settingsKey: "directConnect", - }), + + const deleteSettingsMutation = useMutation(() => + axios.post("http://localhost:3000/api/settings/saveSettings", { + settingsPayload: {}, + settingsKey: "directConnect", + }), ); - // const removeSettings = useCallback(async () => { - // // airDCPPSettings.setSettings({}); - // }, []); - // - const initFormData = !isUndefined(airDCPPClientConfiguration) - ? airDCPPClientConfiguration - : {}; + const initFormData = settingsData?.data?.directConnect?.client?.host ?? {}; + return ( <> { formHeading={"Configure AirDC++"} /> - {!isEmpty(airDCPPSessionInformation) ? ( + {airDCPPSessionInformation && ( - ) : null} + )} - {!isEmpty(airDCPPClientConfiguration) ? ( + {settingsData?.data && (

- as

- ) : null} + )} ); }; diff --git a/src/client/constants/endpoints.ts b/src/client/constants/endpoints.ts index 2fc2125..4bf983b 100644 --- a/src/client/constants/endpoints.ts +++ b/src/client/constants/endpoints.ts @@ -104,3 +104,10 @@ export const TORRENT_JOB_SERVICE_BASE_URI = hostURIBuilder({ port: "3000", apiPath: `/api/torrentjobs`, }); + +export const AIRDCPP_SERVICE_BASE_URI = hostURIBuilder({ + protocol: "http", + host: import.meta.env.UNDERLYING_HOSTNAME || "localhost", + port: "3000", + apiPath: `/api/airdcpp`, +}); diff --git a/src/client/shared/utils/metadata.utils.ts b/src/client/shared/utils/metadata.utils.ts index d16fa65..a261ab5 100644 --- a/src/client/shared/utils/metadata.utils.ts +++ b/src/client/shared/utils/metadata.utils.ts @@ -43,7 +43,7 @@ export const determineCoverFile = (data): any => { }, }; // comicvine - if (!isUndefined(data.comicvine)) { + if (!isEmpty(data.comicvine)) { coverFile.comicvine.url = data?.comicvine?.image.small_url; coverFile.comicvine.issueName = data.comicvine.name; coverFile.comicvine.publisher = data.comicvine.publisher.name; diff --git a/src/client/store/index.ts b/src/client/store/index.ts index d5eb125..f7e6f0d 100644 --- a/src/client/store/index.ts +++ b/src/client/store/index.ts @@ -3,31 +3,15 @@ import { isNil } from "lodash"; import io from "socket.io-client"; import { SOCKET_BASE_URI } from "../constants/endpoints"; import { produce } from "immer"; -import AirDCPPSocket from "../services/DcppSearchService"; -import axios from "axios"; import { QueryClient } from "@tanstack/react-query"; /* Broadly, this file sets up: * 1. The zustand-based global client state * 2. socket.io client - * 3. AirDC++ websocket connection */ export const useStore = create((set, get) => ({ - // AirDC++ state - airDCPPSocketInstance: {}, - airDCPPSocketConnected: false, - airDCPPDisconnectionInfo: {}, - airDCPPClientConfiguration: {}, - airDCPPSessionInformation: {}, - setAirDCPPSocketConnectionStatus: () => - set((value) => ({ - airDCPPSocketConnected: value, - })), - airDCPPDownloadTick: {}, - airDCPPTransfers: {}, // Socket.io state socketIOInstance: {}, - // ComicVine Scraping status comicvine: { scrapingStatus: "", @@ -126,11 +110,12 @@ socketIOInstance.on("RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION", (data) => { // by the LS_COVER_EXTRACTED/LS_COVER_EXTRACTION_FAILED events socketIOInstance.on("LS_COVER_EXTRACTED", (data) => { const { completedJobCount, importResult } = data; + console.log(importResult); setState((state) => ({ importJobQueue: { ...state.importJobQueue, successfulJobCount: completedJobCount, - mostRecentImport: importResult.rawFileDetails.name, + mostRecentImport: importResult.data.rawFileDetails.name, }, })); }); @@ -154,7 +139,6 @@ socketIOInstance.on("LS_IMPORT_QUEUE_DRAINED", (data) => { status: "drained", }, })); - console.log("a", queryClient); queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] }); }); @@ -167,105 +151,3 @@ socketIOInstance.on("CV_SCRAPING_STATUS", (data) => { }, })); }); - -/** - * Method to init AirDC++ Socket with supplied settings - * @param configuration - credentials, and hostname details to init AirDC++ connection - * @returns Initialized AirDC++ connection socket instance - */ -export const initializeAirDCPPSocket = async (configuration): Promise => { - try { - console.log("[AirDCPP]: Initializing socket..."); - - const initializedAirDCPPSocket = new AirDCPPSocket({ - protocol: `${configuration.protocol}`, - hostname: `${configuration.hostname}:${configuration.port}`, - username: `${configuration.username}`, - password: `${configuration.password}`, - }); - - // Set up connect and disconnect handlers - initializedAirDCPPSocket.onConnected = (sessionInfo) => { - // update global state with socket connection status - setState({ - airDCPPSocketConnected: true, - }); - }; - initializedAirDCPPSocket.onDisconnected = async ( - reason, - code, - wasClean, - ) => { - // update global state with socket connection status - setState({ - disconnectionInfo: { reason, code, wasClean }, - airDCPPSocketConnected: false, - }); - }; - // AirDC++ Socket-related connection and post-connection - // Attempt connection - const airDCPPSessionInformation = await initializedAirDCPPSocket.connect(); - setState({ - airDCPPSessionInformation, - }); - - // Set up event listeners - initializedAirDCPPSocket.addListener( - `queue`, - "queue_bundle_tick", - async (downloadProgressData) => { - console.log(downloadProgressData); - setState({ - airDCPPDownloadTick: downloadProgressData, - }); - }, - ); - initializedAirDCPPSocket.addListener( - "queue", - "queue_bundle_added", - async (data) => { - console.log("JEMEN:", data); - }, - ); - - initializedAirDCPPSocket.addListener( - `queue`, - "queue_bundle_status", - async (bundleData) => { - let count = 0; - if (bundleData.status.completed && bundleData.status.downloaded) { - // dispatch the action for raw import, with the metadata - if (count < 1) { - console.log(`[AirDCPP]: Download complete.`); - - count += 1; - } - } - }, - ); - return initializedAirDCPPSocket; - } catch (error) { - console.error(error); - } -}; - -// 1. get settings from mongo -const { data } = await axios({ - url: "http://localhost:3000/api/settings/getAllSettings", - method: "GET", -}); - -const directConnectConfiguration = data?.directConnect?.client.host; - -// 2. If available, init AirDC++ Socket with those settings -if (!isNil(directConnectConfiguration)) { - const airDCPPSocketInstance = await initializeAirDCPPSocket( - directConnectConfiguration, - ); - setState({ - airDCPPSocketInstance, - airDCPPClientConfiguration: directConnectConfiguration, - }); -} else { - console.log("problem"); -}