🔌 AirDC++ socket statuses debug branch

This commit is contained in:
2023-01-09 12:00:36 -08:00
parent df1fbc7dd3
commit c05fb2f341
9 changed files with 294 additions and 266 deletions

View File

@@ -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"
}
}
}

View File

@@ -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,180 +37,189 @@ function sleep(ms: number): Promise<NodeJS.Timeout> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export const setAirDCPPSocketConnectionAsActive = () => async (dispatch) => {
console.log("fire, fire, fire");
dispatch({
type: AIRDCPP_SOCKET_CONNECTED,
});
};
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,
async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect(
credentials.username,
credentials.password,
true,
);
// 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;
}
};
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`,
async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect(
`${credentials.username}`,
`${credentials.password}`,
true,
);
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;
}
};
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(
`${credentials.username}`,
`${credentials.password}`,
true,
);
}
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) => {
@@ -234,7 +248,6 @@ export const getTransfers =
type: LIBRARY_ISSUE_BUNDLES,
issue_bundles,
});
}
} catch (err) {
throw err;

View File

@@ -137,4 +137,4 @@ export const App = (): ReactElement => {
);
};
export default App;
export default App;

View File

@@ -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
* <Library />
@@ -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 ? <span className="tag is-info is-light">Wanted</span> : null;
};
const columns = useMemo(() => [
{
header: "Comic Metadata",
footer: 1,
columns: [
{
header: "File Details",
id: "fileDetails",
minWidth: 400,
accessorKey: "_source",
cell: info => {
return <MetadataPanel data={info.getValue()} />;
const columns = useMemo(
() => [
{
header: "Comic Metadata",
footer: 1,
columns: [
{
header: "File Details",
id: "fileDetails",
minWidth: 400,
accessorKey: "_source",
cell: (info) => {
return <MetadataPanel data={info.getValue()} />;
},
},
},
{
header: "ComicInfo.xml",
accessorKey: "_source.sourcedMetadata.comicInfo",
align: "center",
minWidth: 250,
cell: info =>
!isEmpty(info.getValue()) ? (
<ComicInfoXML data={info.getValue()} />
) : (
<span className="tag mt-5">No ComicInfo.xml</span>
),
},
],
},
{
header: "Additional Metadata",
columns: [
{
header: "Publisher",
accessorKey:
"_source.sourcedMetadata.comicvine.volumeInformation",
cell: info => {
return (
!isNil(info.getValue()) && (
<h6 className="is-size-7 has-text-weight-bold">
{info.getValue().publisher.name}
</h6>
)
);
{
header: "ComicInfo.xml",
accessorKey: "_source.sourcedMetadata.comicInfo",
align: "center",
minWidth: 250,
cell: (info) =>
!isEmpty(info.getValue()) ? (
<ComicInfoXML data={info.getValue()} />
) : (
<span className="tag mt-5">No ComicInfo.xml</span>
),
},
},
{
header: "Something",
accessorKey: "_source.acquisition.source.wanted",
cell: info => {
!isUndefined(info.getValue()) ?
<WantedStatus value={info.getValue().toString()} /> : "Nothing";
],
},
{
header: "Additional Metadata",
columns: [
{
header: "Publisher",
accessorKey: "_source.sourcedMetadata.comicvine.volumeInformation",
cell: (info) => {
return (
!isNil(info.getValue()) && (
<h6 className="is-size-7 has-text-weight-bold">
{info.getValue().publisher.name}
</h6>
)
);
},
},
},
],
}
], []);
{
header: "Something",
accessorKey: "_source.acquisition.source.wanted",
cell: (info) => {
!isUndefined(info.getValue()) ? (
<WantedStatus value={info.getValue().toString()} />
) : (
"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 => {
/>
</div>
</div>
): <div className="columns">
<div className="column is-two-thirds">
<article className="message is-link">
<div className="message-body">
No comics were found in the library, Elasticsearch reports no
indices. Try importing a few comics into the library and come
back.
</div>
</article>
<pre>
{!isUndefined(searchError.data) &&
JSON.stringify(
searchError.data.meta.body.error.root_cause,
null,
4,
)}
</pre>
) : (
<div className="columns">
<div className="column is-two-thirds">
<article className="message is-link">
<div className="message-body">
No comics were found in the library, Elasticsearch reports no
indices. Try importing a few comics into the library and come
back.
</div>
</article>
<pre>
{!isUndefined(searchError.data) &&
JSON.stringify(
searchError.data.meta.body.error.root_cause,
null,
4,
)}
</pre>
</div>
</div>
</div> }
)}
</div>
</section>
);

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useContext } from "react";
import { SearchBar } from "./GlobalSearchBar/SearchBar";
import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick";
import { Link } from "react-router-dom";
@@ -10,6 +10,10 @@ const Navbar: React.FunctionComponent = (props) => {
(state: RootState) => state.airdcpp.downloadProgressData,
);
// const airDCPPSocketConnectionStatus = useSelector((state: RootState) => {
// console.log(state);
// });
return (
<nav className="navbar is-fixed-top">
<div className="navbar-brand">
@@ -72,9 +76,7 @@ const Navbar: React.FunctionComponent = (props) => {
<div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless">
<i className="fa-solid fa-download"></i>
{downloadProgressTick && (
<div className="pulsating-circle"></div>
)}
{downloadProgressTick && <div className="pulsating-circle"></div>}
</a>
{!isUndefined(downloadProgressTick) ? (
<div className="navbar-dropdown download-progress-meter">
@@ -86,12 +88,11 @@ const Navbar: React.FunctionComponent = (props) => {
</div>
{/* AirDC++ socket connection status */}
<div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless">
<a className="navbar-link is-arrowless has-text-success">
<i className="fa-solid fa-bolt"></i>
</a>
</div>
<div className="navbar-item has-dropdown is-hoverable is-mega">
<div className="navbar-link flex">Blog</div>
<div id="blogDropdown" className="navbar-dropdown">

View File

@@ -115,6 +115,8 @@ export const AIRDCPP_FILE_DOWNLOAD_COMPLETED =
export const LS_SINGLE_IMPORT = "LS_SINGLE_IMPORT";
export const AIRDCPP_BUNDLES_FETCHED = "AIRDCPP_BUNDLES_FETCHED";
export const AIRDCPP_DOWNLOAD_PROGRESS_TICK = "AIRDCPP_DOWNLOAD_PROGRESS_TICK";
export const AIRDCPP_SOCKET_CONNECTED = "AIRDCPP_SOCKET_CONNECTED";
export const AIRDCPP_SOCKET_DISCONNECTED = "AIRDCPP_SOCKET_DISCONNECTED";
// Transfers
export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED";

View File

@@ -1,7 +1,9 @@
import { isEmpty, isUndefined } from "lodash";
import React, { createContext, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setAirDCPPSocketConnectionAsActive } from "../actions/airdcpp.actions";
import { getSettings } from "../actions/settings.actions";
import AirDCPPSocket from "../services/DcppSearchService";
const AirDCPPSocketContextProvider = ({ children }) => {
@@ -51,16 +53,31 @@ const AirDCPPSocketContextProvider = ({ children }) => {
client: { host },
},
} = configuration;
const initializedAirDCPPSocket = new AirDCPPSocket({
protocol: `${host.protocol}`,
hostname: `${host.hostname}:${host.port}`,
});
// connect and disconnect handlers
initializedAirDCPPSocket.onConnected = (sessionInfo) => {
dispatch(setAirDCPPSocketConnectionAsActive());
};
initializedAirDCPPSocket.onDisconnected = async (
reason,
code,
wasClean,
) => {
console.log("kulkarni kamal kulthe", reason, code, wasClean);
};
const socketConnectionInformation = await initializedAirDCPPSocket.connect(
`${host.username}`,
`${host.password}`,
true,
);
// update the state with the new socket connection information
persistSettings({
...airDCPPState,
airDCPPState: {

View File

@@ -9,6 +9,8 @@ import {
AIRDCPP_BUNDLES_FETCHED,
AIRDCPP_TRANSFERS_FETCHED,
LIBRARY_ISSUE_BUNDLES,
AIRDCPP_SOCKET_CONNECTED,
AIRDCPP_SOCKET_DISCONNECTED,
} from "../constants/action-types";
import { LOCATION_CHANGE } from "redux-first-history";
import { isNil, isUndefined } from "lodash";
@@ -24,6 +26,7 @@ const initialState = {
downloadFileStatus: {},
bundles: [],
transfers: [],
isAirDCPPSocketConnected: false,
};
function airdcppReducer(state = initialState, action) {
@@ -95,6 +98,18 @@ function airdcppReducer(state = initialState, action) {
...state,
transfers: action.bundles,
};
case AIRDCPP_SOCKET_CONNECTED:
return {
...state,
isAirDCPPSocketConnected: true,
};
case AIRDCPP_SOCKET_DISCONNECTED:
return {
...state,
isAirDCPPSocketConnected: false,
};
case LOCATION_CHANGE:
return {
searchResults: [],
@@ -105,7 +120,6 @@ function airdcppReducer(state = initialState, action) {
bundleDBImportResult: null,
// bundles: [],
};
default:
return state;
}

View File

@@ -6686,13 +6686,6 @@ character-reference-invalid@^1.0.0:
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
chevrotain@7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-7.0.3.tgz#f9621971a726eba804f7763edace2183d157c5d7"
integrity sha512-G634q7M5EiqNNv+0MKcQES2jmqabbi4PvUDpzjG2t+i1XQFaMCz0o8BZ8lbQbZex4RqkzJ3pOy+UwNLFlQm4eg==
dependencies:
regexp-to-ast "0.5.0"
chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -7120,14 +7113,7 @@ compromise-sentences@^0.3.0:
resolved "https://registry.yarnpkg.com/compromise-sentences/-/compromise-sentences-0.3.0.tgz#61d34039323155bc20dad10f24340daaabf86e26"
integrity sha512-L6d5PXmmyPyWRMoJURYOM0yILhtV37zCOy9ZW8Xd6KgY+2qQBTwuokekaGL9MDpSH3/grRDL+AHKytYpGrTEmA==
compromise-strict@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/compromise-strict/-/compromise-strict-0.0.2.tgz#e4bf4cc386e8795c6408d3606ebb2cd31cab9fbd"
integrity sha512-1ApkPa6kjmyS1ZFhDRZgttDcnVv+Ci8U0kIIsxRBLCN0vautyPm6FROvBwoLimlU3pgIH3hMIpdNdVfe2yj3Gw==
dependencies:
chevrotain "7.0.3"
compromise@^13.11.3, compromise@^13.11.4:
compromise@^13.11.4:
version "13.11.4"
resolved "https://registry.yarnpkg.com/compromise/-/compromise-13.11.4.tgz#3136e6d2c2cca822ba7cebd140c9f27f405b67f1"
integrity sha512-nBITcNdqIHSVDDluaG6guyFFCSNXN+Hu87fU8VlhkE5Z0PwTZN1nro2O7a8JcUH88nB5EOzrxd9zKfXLSNFqcg==
@@ -15855,11 +15841,6 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
regexp-to-ast@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz#56c73856bee5e1fef7f73a00f1473452ab712a24"
integrity sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==
regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"