From 8dd68e9d54e0cdef1a0bca5f6e897c341ea99ac1 Mon Sep 17 00:00:00 2001 From: Rishi Ghan Date: Sat, 21 Jan 2023 02:29:32 -0800 Subject: [PATCH] AirDC++ Socket Status (#58) --- package.json | 7 +- src/client/actions/airdcpp.actions.tsx | 327 +++++++++++----------- src/client/assets/scss/App.scss | 4 + src/client/components/App.tsx | 2 +- src/client/components/Library/Library.tsx | 167 +++++------ src/client/components/Navbar.tsx | 81 ++++-- src/client/constants/action-types.ts | 2 + src/client/context/AirDCPPSocket.tsx | 30 +- src/client/reducers/airdcpp.reducer.ts | 21 +- src/client/services/DcppSearchService.ts | 4 +- src/client/services/api/SearchApi.ts | 2 +- yarn.lock | 21 +- 12 files changed, 376 insertions(+), 292 deletions(-) diff --git a/package.json b/package.json index ff95b58..07a3ce7 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,6 @@ "babel-preset-minify": "^0.5.2", "better-docs": "^2.7.2", "comlink-loader": "^2.0.0", - "compromise": "^13.11.3", - "compromise-dates": "^2.2.1", - "compromise-numbers": "^1.4.0", - "compromise-sentences": "^0.3.0", "date-fns": "^2.28.0", "dayjs": "^1.10.6", "ellipsize": "^0.1.0", @@ -138,7 +134,6 @@ "bulma": "^0.9.3", "clean-webpack-plugin": "^1.0.0", "comlink": "^4.3.0", - "compromise-strict": "^0.0.2", "concurrently": "^4.0.0", "copy-webpack-plugin": "^9.0.1", "css-loader": "^5.1.2", @@ -182,4 +177,4 @@ "resolutions": { "@storybook/react/webpack": "^5" } -} \ No newline at end of file +} diff --git a/src/client/actions/airdcpp.actions.tsx b/src/client/actions/airdcpp.actions.tsx index 663bdb2..b63268b 100644 --- a/src/client/actions/airdcpp.actions.tsx +++ b/src/client/actions/airdcpp.actions.tsx @@ -4,7 +4,10 @@ import { PriorityEnum, SearchResponse, } from "threetwo-ui-typings"; -import { LIBRARY_SERVICE_BASE_URI, SEARCH_SERVICE_BASE_URI } from "../constants/endpoints"; +import { + LIBRARY_SERVICE_BASE_URI, + SEARCH_SERVICE_BASE_URI, +} from "../constants/endpoints"; import { AIRDCPP_SEARCH_RESULTS_ADDED, AIRDCPP_SEARCH_RESULTS_UPDATED, @@ -18,6 +21,8 @@ import { IMS_COMIC_BOOK_DB_OBJECT_FETCHED, AIRDCPP_TRANSFERS_FETCHED, LIBRARY_ISSUE_BUNDLES, + AIRDCPP_SOCKET_CONNECTED, + AIRDCPP_SOCKET_DISCONNECTED, } from "../constants/action-types"; import { isNil } from "lodash"; import axios from "axios"; @@ -32,190 +37,199 @@ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } +export const toggleAirDCPPSocketConnectionStatus = + (status: String, payload?: any) => async (dispatch) => { + switch (status) { + case "connected": + dispatch({ + type: AIRDCPP_SOCKET_CONNECTED, + data: payload, + }); + break; + + case "disconnected": + dispatch({ + type: AIRDCPP_SOCKET_DISCONNECTED, + data: payload, + }); + break; + + default: + console.log("Can't set AirDC++ socket status."); + break; + } + }; export const search = (data: SearchData, ADCPPSocket: any, credentials: any) => - async (dispatch) => { - try { - if (!ADCPPSocket.isConnected()) { - await ADCPPSocket.connect( - credentials.username, - credentials.password, - true, - ); - } - const instance: SearchInstance = await ADCPPSocket.post("search"); - dispatch({ - type: AIRDCPP_SEARCH_IN_PROGRESS, - }); - - // 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) - - dispatch({ - type: AIRDCPP_SEARCH_RESULTS_ADDED, - 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 - dispatch({ - type: AIRDCPP_SEARCH_RESULTS_UPDATED, - groupedResult, - }); - }, - 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}`, - ); - 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 - dispatch({ - type: AIRDCPP_HUB_SEARCHES_SENT, - searchInfo, - instance, - }); - }, - instance.id, - ); - // Finally, perform the actual search - await ADCPPSocket.post(`search/${instance.id}/hub_search`, data); - } catch (error) { - console.log(error); - throw error; + async (dispatch) => { + try { + if (!ADCPPSocket.isConnected()) { + await ADCPPSocket(); } - }; + const instance: SearchInstance = await ADCPPSocket.post("search"); + dispatch({ + type: AIRDCPP_SEARCH_IN_PROGRESS, + }); + + // 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) + + dispatch({ + type: AIRDCPP_SEARCH_RESULTS_ADDED, + 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 + dispatch({ + type: AIRDCPP_SEARCH_RESULTS_UPDATED, + groupedResult, + }); + }, + 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}`, + ); + 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 + dispatch({ + type: AIRDCPP_HUB_SEARCHES_SENT, + searchInfo, + instance, + }); + }, + instance.id, + ); + // Finally, perform the actual search + await ADCPPSocket.post(`search/${instance.id}/hub_search`, data); + } catch (error) { + console.log(error); + throw error; + } + }; export const downloadAirDCPPItem = - (searchInstanceId: Number, + ( + searchInstanceId: Number, resultId: String, comicObjectId: String, - name: String, size: Number, type: any, + name: String, + size: Number, + type: any, ADCPPSocket: any, credentials: any, ): void => - async (dispatch) => { - try { - if (!ADCPPSocket.isConnected()) { - await ADCPPSocket.connect( - `${credentials.username}`, - `${credentials.password}`, - true, - ); - } - let bundleDBImportResult = {}; - const downloadResult = await ADCPPSocket.post( - `search/${searchInstanceId}/results/${resultId}/download`, - ); - - if (!isNil(downloadResult)) { - bundleDBImportResult = await axios({ - method: "POST", - url: `${LIBRARY_SERVICE_BASE_URI}/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; + async (dispatch) => { + try { + if (!ADCPPSocket.isConnected()) { + await ADCPPSocket.connect(); } - }; + let bundleDBImportResult = {}; + const downloadResult = await ADCPPSocket.post( + `search/${searchInstanceId}/results/${resultId}/download`, + ); -export const getBundlesForComic = - (comicObjectId: string, ADCPPSocket: any, credentials: any) => - async (dispatch) => { - try { - if (!ADCPPSocket.isConnected()) { - await ADCPPSocket.connect( - `${credentials.username}`, - `${credentials.password}`, - true, - ); - } - const comicObject = await axios({ + if (!isNil(downloadResult)) { + bundleDBImportResult = await axios({ method: "POST", - url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`, + url: `${LIBRARY_SERVICE_BASE_URI}/applyAirDCPPDownloadMetadata`, headers: { "Content-Type": "application/json; charset=utf-8", }, data: { - id: `${comicObjectId}`, + bundleId: downloadResult.bundle_info.id, + comicObjectId, + name, + size, + type, }, }); - // get only the bundles applicable for the comic - if (comicObject.data.acquisition.directconnect) { - const filteredBundles = comicObject.data.acquisition.directconnect.downloads.map( + + 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; + } + }; + +export const getBundlesForComic = + (comicObjectId: string, ADCPPSocket: any, credentials: any) => + async (dispatch) => { + try { + if (!ADCPPSocket.isConnected()) { + await ADCPPSocket.connect(); + } + const comicObject = await axios({ + method: "POST", + url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + data: { + id: `${comicObjectId}`, + }, + }); + // get only the bundles applicable for the comic + if (comicObject.data.acquisition.directconnect) { + const filteredBundles = + comicObject.data.acquisition.directconnect.downloads.map( async ({ bundleId }) => { return await ADCPPSocket.get(`queue/bundles/${bundleId}`); }, ); - dispatch({ - type: AIRDCPP_BUNDLES_FETCHED, - bundles: await Promise.all(filteredBundles), - }); - } - } catch (error) { - throw error; + dispatch({ + type: AIRDCPP_BUNDLES_FETCHED, + bundles: await Promise.all(filteredBundles), + }); } - }; + } catch (error) { + throw error; + } + }; export const getTransfers = (ADCPPSocket: any, credentials: any) => async (dispatch) => { try { if (!ADCPPSocket.isConnected()) { - await ADCPPSocket.connect( - `${credentials.username}`, - `${credentials.password}`, - true, - ); + await ADCPPSocket.connect(); } const bundles = await ADCPPSocket.get("queue/bundles/1/85", {}); if (!isNil(bundles)) { @@ -234,7 +248,6 @@ export const getTransfers = type: LIBRARY_ISSUE_BUNDLES, issue_bundles, }); - } } catch (err) { throw err; diff --git a/src/client/assets/scss/App.scss b/src/client/assets/scss/App.scss index 1bc5cf6..efb9b6b 100644 --- a/src/client/assets/scss/App.scss +++ b/src/client/assets/scss/App.scss @@ -62,6 +62,10 @@ pre { margin-left: -300px; min-width: 500px; } + .airdcpp-status { + min-width: 300px; + line-height: 1.7rem; + } body { background: #454a59; } diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index 1df28b4..6446767 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -137,4 +137,4 @@ export const App = (): ReactElement => { ); }; -export default App; \ No newline at end of file +export default App; diff --git a/src/client/components/Library/Library.tsx b/src/client/components/Library/Library.tsx index 0366e80..c808ec3 100644 --- a/src/client/components/Library/Library.tsx +++ b/src/client/components/Library/Library.tsx @@ -8,10 +8,9 @@ import { useDispatch, useSelector } from "react-redux"; import { searchIssue } from "../../actions/fileops.actions"; import ellipsize from "ellipsize"; - /** * Component that tabulates the contents of the user's ThreeTwo Library. - * + * * @component * @example * @@ -20,9 +19,10 @@ export const Library = (): ReactElement => { const searchResults = useSelector( (state: RootState) => state.fileOps.libraryComics, ); - const searchError = useSelector( - (state: RootState) => state.fileOps.librarySearchError, - ); + const searchError = useSelector((state: RootState) => { + console.log(state); + return state.fileOps.librarySearchError; + }); const dispatch = useDispatch(); useEffect(() => { dispatch( @@ -36,7 +36,7 @@ export const Library = (): ReactElement => { from: 0, }, type: "all", - trigger: "libraryPage" + trigger: "libraryPage", }, ), ); @@ -89,63 +89,67 @@ export const Library = (): ReactElement => { const WantedStatus = ({ value }) => { return !value ? Wanted : null; }; - const columns = useMemo(() => [ - { - header: "Comic Metadata", - footer: 1, - columns: [ - { - header: "File Details", - id: "fileDetails", - minWidth: 400, - accessorKey: "_source", - cell: info => { - return ; + const columns = useMemo( + () => [ + { + header: "Comic Metadata", + footer: 1, + columns: [ + { + header: "File Details", + id: "fileDetails", + minWidth: 400, + accessorKey: "_source", + cell: (info) => { + return ; + }, }, - }, - { - header: "ComicInfo.xml", - accessorKey: "_source.sourcedMetadata.comicInfo", - align: "center", - minWidth: 250, - cell: info => - !isEmpty(info.getValue()) ? ( - - ) : ( - No ComicInfo.xml - ), - }, - ], - }, - { - header: "Additional Metadata", - columns: [ - { - header: "Publisher", - accessorKey: - "_source.sourcedMetadata.comicvine.volumeInformation", - cell: info => { - return ( - !isNil(info.getValue()) && ( -
- {info.getValue().publisher.name} -
- ) - ); + { + header: "ComicInfo.xml", + accessorKey: "_source.sourcedMetadata.comicInfo", + align: "center", + minWidth: 250, + cell: (info) => + !isEmpty(info.getValue()) ? ( + + ) : ( + No ComicInfo.xml + ), }, - }, - { - header: "Something", - accessorKey: "_source.acquisition.source.wanted", - cell: info => { - !isUndefined(info.getValue()) ? - : "Nothing"; + ], + }, + { + header: "Additional Metadata", + columns: [ + { + header: "Publisher", + accessorKey: "_source.sourcedMetadata.comicvine.volumeInformation", + cell: (info) => { + return ( + !isNil(info.getValue()) && ( +
+ {info.getValue().publisher.name} +
+ ) + ); + }, }, - }, - ], - } - ], []); - + { + header: "Something", + accessorKey: "_source.acquisition.source.wanted", + cell: (info) => { + !isUndefined(info.getValue()) ? ( + + ) : ( + "Nothing" + ); + }, + }, + ], + }, + ], + [], + ); /** * Pagination control that fetches the next x (pageSize) items @@ -153,7 +157,7 @@ export const Library = (): ReactElement => { * @param {number} pageIndex * @param {number} pageSize * @returns void - * + * **/ const nextPage = useCallback((pageIndex: number, pageSize: number) => { dispatch( @@ -173,7 +177,6 @@ export const Library = (): ReactElement => { ); }, []); - /** * Pagination control that fetches the previous x (pageSize) items * based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index @@ -199,7 +202,7 @@ export const Library = (): ReactElement => { from, }, type: "all", - trigger: "libraryPage" + trigger: "libraryPage", }, ), ); @@ -229,25 +232,27 @@ export const Library = (): ReactElement => { /> - ):
-
-
-
- No comics were found in the library, Elasticsearch reports no - indices. Try importing a few comics into the library and come - back. -
-
-
-              {!isUndefined(searchError.data) &&
-                JSON.stringify(
-                  searchError.data.meta.body.error.root_cause,
-                  null,
-                  4,
-                )}
-            
+ ) : ( +
+
+
+
+ No comics were found in the library, Elasticsearch reports no + indices. Try importing a few comics into the library and come + back. +
+
+
+                {!isUndefined(searchError.data) &&
+                  JSON.stringify(
+                    searchError.data.meta.body.error.root_cause,
+                    null,
+                    4,
+                  )}
+              
+
-
} + )}
); diff --git a/src/client/components/Navbar.tsx b/src/client/components/Navbar.tsx index f7fb4b7..6de6dde 100644 --- a/src/client/components/Navbar.tsx +++ b/src/client/components/Navbar.tsx @@ -3,15 +3,23 @@ import { SearchBar } from "./GlobalSearchBar/SearchBar"; import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick"; import { Link } from "react-router-dom"; import { useSelector } from "react-redux"; -import { isUndefined, isEmpty } from "lodash"; -import { AirDCPPSocketContext } from "../context/AirDCPPSocket"; +import { isUndefined } from "lodash"; +import { format, fromUnixTime } from "date-fns"; const Navbar: React.FunctionComponent = (props) => { const downloadProgressTick = useSelector( (state: RootState) => state.airdcpp.downloadProgressData, ); - const airDCPPConfiguration = useContext(AirDCPPSocketContext); -console.log(airDCPPConfiguration) + + const airDCPPSocketConnectionStatus = useSelector( + (state: RootState) => state.airdcpp.isAirDCPPSocketConnected, + ); + const airDCPPSessionInfo = useSelector( + (state: RootState) => state.airdcpp.airDCPPSessionInfo, + ); + const socketDisconnectionReason = useSelector( + (state: RootState) => state.airdcpp.socketDisconnectionReason, + ); return (