Compare commits

...

9 Commits

12 changed files with 376 additions and 292 deletions

View File

@@ -44,10 +44,6 @@
"babel-preset-minify": "^0.5.2", "babel-preset-minify": "^0.5.2",
"better-docs": "^2.7.2", "better-docs": "^2.7.2",
"comlink-loader": "^2.0.0", "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", "date-fns": "^2.28.0",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"ellipsize": "^0.1.0", "ellipsize": "^0.1.0",
@@ -138,7 +134,6 @@
"bulma": "^0.9.3", "bulma": "^0.9.3",
"clean-webpack-plugin": "^1.0.0", "clean-webpack-plugin": "^1.0.0",
"comlink": "^4.3.0", "comlink": "^4.3.0",
"compromise-strict": "^0.0.2",
"concurrently": "^4.0.0", "concurrently": "^4.0.0",
"copy-webpack-plugin": "^9.0.1", "copy-webpack-plugin": "^9.0.1",
"css-loader": "^5.1.2", "css-loader": "^5.1.2",
@@ -182,4 +177,4 @@
"resolutions": { "resolutions": {
"@storybook/react/webpack": "^5" "@storybook/react/webpack": "^5"
} }
} }

View File

@@ -4,7 +4,10 @@ import {
PriorityEnum, PriorityEnum,
SearchResponse, SearchResponse,
} from "threetwo-ui-typings"; } 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 { import {
AIRDCPP_SEARCH_RESULTS_ADDED, AIRDCPP_SEARCH_RESULTS_ADDED,
AIRDCPP_SEARCH_RESULTS_UPDATED, AIRDCPP_SEARCH_RESULTS_UPDATED,
@@ -18,6 +21,8 @@ import {
IMS_COMIC_BOOK_DB_OBJECT_FETCHED, IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
AIRDCPP_TRANSFERS_FETCHED, AIRDCPP_TRANSFERS_FETCHED,
LIBRARY_ISSUE_BUNDLES, LIBRARY_ISSUE_BUNDLES,
AIRDCPP_SOCKET_CONNECTED,
AIRDCPP_SOCKET_DISCONNECTED,
} from "../constants/action-types"; } from "../constants/action-types";
import { isNil } from "lodash"; import { isNil } from "lodash";
import axios from "axios"; import axios from "axios";
@@ -32,190 +37,199 @@ function sleep(ms: number): Promise<NodeJS.Timeout> {
return new Promise((resolve) => setTimeout(resolve, ms)); 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 = export const search =
(data: SearchData, ADCPPSocket: any, credentials: any) => (data: SearchData, ADCPPSocket: any, credentials: any) =>
async (dispatch) => { async (dispatch) => {
try { try {
if (!ADCPPSocket.isConnected()) { if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect( await ADCPPSocket();
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;
} }
}; 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 = export const downloadAirDCPPItem =
(searchInstanceId: Number, (
searchInstanceId: Number,
resultId: String, resultId: String,
comicObjectId: String, comicObjectId: String,
name: String, size: Number, type: any, name: String,
size: Number,
type: any,
ADCPPSocket: any, ADCPPSocket: any,
credentials: any, credentials: any,
): void => ): void =>
async (dispatch) => { async (dispatch) => {
try { try {
if (!ADCPPSocket.isConnected()) { if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect( 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;
} }
}; let bundleDBImportResult = {};
const downloadResult = await ADCPPSocket.post(
`search/${searchInstanceId}/results/${resultId}/download`,
);
export const getBundlesForComic = if (!isNil(downloadResult)) {
(comicObjectId: string, ADCPPSocket: any, credentials: any) => bundleDBImportResult = await axios({
async (dispatch) => {
try {
if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect(
`${credentials.username}`,
`${credentials.password}`,
true,
);
}
const comicObject = await axios({
method: "POST", method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`, url: `${LIBRARY_SERVICE_BASE_URI}/applyAirDCPPDownloadMetadata`,
headers: { headers: {
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
}, },
data: { 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) { dispatch({
const filteredBundles = comicObject.data.acquisition.directconnect.downloads.map( 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 }) => { async ({ bundleId }) => {
return await ADCPPSocket.get(`queue/bundles/${bundleId}`); return await ADCPPSocket.get(`queue/bundles/${bundleId}`);
}, },
); );
dispatch({ dispatch({
type: AIRDCPP_BUNDLES_FETCHED, type: AIRDCPP_BUNDLES_FETCHED,
bundles: await Promise.all(filteredBundles), bundles: await Promise.all(filteredBundles),
}); });
}
} catch (error) {
throw error;
} }
}; } catch (error) {
throw error;
}
};
export const getTransfers = export const getTransfers =
(ADCPPSocket: any, credentials: any) => async (dispatch) => { (ADCPPSocket: any, credentials: any) => async (dispatch) => {
try { try {
if (!ADCPPSocket.isConnected()) { if (!ADCPPSocket.isConnected()) {
await ADCPPSocket.connect( await ADCPPSocket.connect();
`${credentials.username}`,
`${credentials.password}`,
true,
);
} }
const bundles = await ADCPPSocket.get("queue/bundles/1/85", {}); const bundles = await ADCPPSocket.get("queue/bundles/1/85", {});
if (!isNil(bundles)) { if (!isNil(bundles)) {
@@ -234,7 +248,6 @@ export const getTransfers =
type: LIBRARY_ISSUE_BUNDLES, type: LIBRARY_ISSUE_BUNDLES,
issue_bundles, issue_bundles,
}); });
} }
} catch (err) { } catch (err) {
throw err; throw err;

View File

@@ -62,6 +62,10 @@ pre {
margin-left: -300px; margin-left: -300px;
min-width: 500px; min-width: 500px;
} }
.airdcpp-status {
min-width: 300px;
line-height: 1.7rem;
}
body { body {
background: #454a59; background: #454a59;
} }

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 { searchIssue } from "../../actions/fileops.actions";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
/** /**
* Component that tabulates the contents of the user's ThreeTwo Library. * Component that tabulates the contents of the user's ThreeTwo Library.
* *
* @component * @component
* @example * @example
* <Library /> * <Library />
@@ -20,9 +19,10 @@ export const Library = (): ReactElement => {
const searchResults = useSelector( const searchResults = useSelector(
(state: RootState) => state.fileOps.libraryComics, (state: RootState) => state.fileOps.libraryComics,
); );
const searchError = useSelector( const searchError = useSelector((state: RootState) => {
(state: RootState) => state.fileOps.librarySearchError, console.log(state);
); return state.fileOps.librarySearchError;
});
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
dispatch( dispatch(
@@ -36,7 +36,7 @@ export const Library = (): ReactElement => {
from: 0, from: 0,
}, },
type: "all", type: "all",
trigger: "libraryPage" trigger: "libraryPage",
}, },
), ),
); );
@@ -89,63 +89,67 @@ export const Library = (): ReactElement => {
const WantedStatus = ({ value }) => { const WantedStatus = ({ value }) => {
return !value ? <span className="tag is-info is-light">Wanted</span> : null; return !value ? <span className="tag is-info is-light">Wanted</span> : null;
}; };
const columns = useMemo(() => [ const columns = useMemo(
{ () => [
header: "Comic Metadata", {
footer: 1, header: "Comic Metadata",
columns: [ footer: 1,
{ columns: [
header: "File Details", {
id: "fileDetails", header: "File Details",
minWidth: 400, id: "fileDetails",
accessorKey: "_source", minWidth: 400,
cell: info => { accessorKey: "_source",
return <MetadataPanel data={info.getValue()} />; cell: (info) => {
return <MetadataPanel data={info.getValue()} />;
},
}, },
}, {
{ header: "ComicInfo.xml",
header: "ComicInfo.xml", accessorKey: "_source.sourcedMetadata.comicInfo",
accessorKey: "_source.sourcedMetadata.comicInfo", align: "center",
align: "center", minWidth: 250,
minWidth: 250, cell: (info) =>
cell: info => !isEmpty(info.getValue()) ? (
!isEmpty(info.getValue()) ? ( <ComicInfoXML data={info.getValue()} />
<ComicInfoXML data={info.getValue()} /> ) : (
) : ( <span className="tag mt-5">No ComicInfo.xml</span>
<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: "Something", {
accessorKey: "_source.acquisition.source.wanted", header: "Additional Metadata",
cell: info => { columns: [
!isUndefined(info.getValue()) ? {
<WantedStatus value={info.getValue().toString()} /> : "Nothing"; 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 * Pagination control that fetches the next x (pageSize) items
@@ -153,7 +157,7 @@ export const Library = (): ReactElement => {
* @param {number} pageIndex * @param {number} pageIndex
* @param {number} pageSize * @param {number} pageSize
* @returns void * @returns void
* *
**/ **/
const nextPage = useCallback((pageIndex: number, pageSize: number) => { const nextPage = useCallback((pageIndex: number, pageSize: number) => {
dispatch( dispatch(
@@ -173,7 +177,6 @@ export const Library = (): ReactElement => {
); );
}, []); }, []);
/** /**
* Pagination control that fetches the previous x (pageSize) items * Pagination control that fetches the previous x (pageSize) items
* based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index * based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index
@@ -199,7 +202,7 @@ export const Library = (): ReactElement => {
from, from,
}, },
type: "all", type: "all",
trigger: "libraryPage" trigger: "libraryPage",
}, },
), ),
); );
@@ -229,25 +232,27 @@ export const Library = (): ReactElement => {
/> />
</div> </div>
</div> </div>
): <div className="columns"> ) : (
<div className="column is-two-thirds"> <div className="columns">
<article className="message is-link"> <div className="column is-two-thirds">
<div className="message-body"> <article className="message is-link">
No comics were found in the library, Elasticsearch reports no <div className="message-body">
indices. Try importing a few comics into the library and come No comics were found in the library, Elasticsearch reports no
back. indices. Try importing a few comics into the library and come
</div> back.
</article> </div>
<pre> </article>
{!isUndefined(searchError.data) && <pre>
JSON.stringify( {!isUndefined(searchError.data) &&
searchError.data.meta.body.error.root_cause, JSON.stringify(
null, searchError.data.meta.body.error.root_cause,
4, null,
)} 4,
</pre> )}
</pre>
</div>
</div> </div>
</div> } )}
</div> </div>
</section> </section>
); );

View File

@@ -3,15 +3,23 @@ import { SearchBar } from "./GlobalSearchBar/SearchBar";
import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick"; import { DownloadProgressTick } from "./ComicDetail/DownloadProgressTick";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { isUndefined, isEmpty } from "lodash"; import { isUndefined } from "lodash";
import { AirDCPPSocketContext } from "../context/AirDCPPSocket"; import { format, fromUnixTime } from "date-fns";
const Navbar: React.FunctionComponent = (props) => { const Navbar: React.FunctionComponent = (props) => {
const downloadProgressTick = useSelector( const downloadProgressTick = useSelector(
(state: RootState) => state.airdcpp.downloadProgressData, (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 ( return (
<nav className="navbar is-fixed-top"> <nav className="navbar is-fixed-top">
<div className="navbar-brand"> <div className="navbar-brand">
@@ -74,12 +82,10 @@ console.log(airDCPPConfiguration)
<div className="navbar-item has-dropdown is-hoverable"> <div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless"> <a className="navbar-link is-arrowless">
<i className="fa-solid fa-download"></i> <i className="fa-solid fa-download"></i>
{downloadProgressTick && ( {downloadProgressTick && <div className="pulsating-circle"></div>}
<div className="pulsating-circle"></div>
)}
</a> </a>
{!isUndefined(downloadProgressTick) ? ( {!isUndefined(downloadProgressTick) ? (
<div className="navbar-dropdown download-progress-meter"> <div className="navbar-dropdown is-right">
<a className="navbar-item"> <a className="navbar-item">
<DownloadProgressTick data={downloadProgressTick} /> <DownloadProgressTick data={downloadProgressTick} />
</a> </div> </a> </div>
@@ -87,18 +93,55 @@ console.log(airDCPPConfiguration)
</div> </div>
{/* AirDC++ socket connection status */} {/* AirDC++ socket connection status */}
<div className="navbar-item has-dropdown is-hoverable"> <div className="navbar-item has-dropdown is-hoverable">
<a className="navbar-link is-arrowless has-text-success"> {airDCPPSocketConnectionStatus ? (
{!isEmpty(airDCPPConfiguration.airDCPPState.socketConnectionInformation) ? ( <>
<i className="fa-solid fa-bolt"></i>) : null} <a className="navbar-link is-arrowless has-text-success">
</a> <i className="fa-solid fa-bolt"></i>
<div className="navbar-dropdown download-progress-meter"> </a>
<a className="navbar-item"> <div className="navbar-dropdown pt-4 pr-2 pl-2 is-right airdcpp-status">
<pre>{JSON.stringify(airDCPPConfiguration.airDCPPState.socketConnectionInformation, null, 2)}</pre> {/* AirDC++ Session Information */}
</a>
</div> <p>
Last login was{" "}
<span className="tag">
{format(
fromUnixTime(airDCPPSessionInfo.user.last_login),
"dd MMMM, yyyy",
)}
</span>
</p>
<hr className="navbar-divider" />
<p>
<span className="tag has-text-success">
{airDCPPSessionInfo.user.username}
</span>
connected to{" "}
<span className="tag has-text-success">
{airDCPPSessionInfo.system_info.client_version}
</span>{" "}
with session ID{" "}
<span className="tag has-text-success">
{airDCPPSessionInfo.session_id}
</span>
</p>
{/* <pre>{JSON.stringify(airDCPPSessionInfo, null, 2)}</pre> */}
</div>
</>
) : (
<>
<a className="navbar-link is-arrowless has-text-danger">
<i className="fa-solid fa-bolt"></i>
</a>
<div className="navbar-dropdown pr-2 pl-2 is-right">
<pre>
{JSON.stringify(socketDisconnectionReason, null, 2)}
</pre>
</div>
</>
)}
</div> </div>
<div className="navbar-item has-dropdown is-hoverable is-mega"> <div className="navbar-item has-dropdown is-hoverable is-mega">
<div className="navbar-link flex">Blog</div> <div className="navbar-link flex">Blog</div>
<div id="blogDropdown" className="navbar-dropdown"> <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 LS_SINGLE_IMPORT = "LS_SINGLE_IMPORT";
export const AIRDCPP_BUNDLES_FETCHED = "AIRDCPP_BUNDLES_FETCHED"; export const AIRDCPP_BUNDLES_FETCHED = "AIRDCPP_BUNDLES_FETCHED";
export const AIRDCPP_DOWNLOAD_PROGRESS_TICK = "AIRDCPP_DOWNLOAD_PROGRESS_TICK"; 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 // Transfers
export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED"; export const AIRDCPP_TRANSFERS_FETCHED = "AIRDCPP_TRANSFERS_FETCHED";

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ class AirDCPPSocket {
} }
const options = { const options = {
url: `${socketProtocol}://${configuration.hostname}/api/v1/`, url: `${socketProtocol}://${configuration.hostname}/api/v1/`,
autoReconnect: false, autoReconnect: true,
reconnectInterval: 5, reconnectInterval: 5,
logLevel: "verbose", logLevel: "verbose",
ignoredListenerEvents: [ ignoredListenerEvents: [
@@ -18,6 +18,8 @@ class AirDCPPSocket {
"hash_statistics", "hash_statistics",
"hub_counts_updated", "hub_counts_updated",
], ],
username: `${configuration.username}`,
password: `${configuration.password}`,
}; };
const AirDCPPSocketInstance = Socket(options, window.WebSocket as any); const AirDCPPSocketInstance = Socket(options, window.WebSocket as any);
return AirDCPPSocketInstance; return AirDCPPSocketInstance;

View File

@@ -18,7 +18,7 @@ function sleep(ms: number): Promise<NodeJS.Timeout> {
} }
export const search = async (data: SearchData) => { export const search = async (data: SearchData) => {
await SocketService.connect("admin", "password"); await SocketService.connect();
const instance: SearchInstance = await SocketService.post("search"); const instance: SearchInstance = await SocketService.post("search");
const unsubscribe = await SocketService.addListener( const unsubscribe = await SocketService.addListener(
"search", "search",

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" resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== 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: chokidar@^2.1.8:
version "2.1.8" version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" 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" resolved "https://registry.yarnpkg.com/compromise-sentences/-/compromise-sentences-0.3.0.tgz#61d34039323155bc20dad10f24340daaabf86e26"
integrity sha512-L6d5PXmmyPyWRMoJURYOM0yILhtV37zCOy9ZW8Xd6KgY+2qQBTwuokekaGL9MDpSH3/grRDL+AHKytYpGrTEmA== integrity sha512-L6d5PXmmyPyWRMoJURYOM0yILhtV37zCOy9ZW8Xd6KgY+2qQBTwuokekaGL9MDpSH3/grRDL+AHKytYpGrTEmA==
compromise-strict@^0.0.2: compromise@^13.11.4:
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:
version "13.11.4" version "13.11.4"
resolved "https://registry.yarnpkg.com/compromise/-/compromise-13.11.4.tgz#3136e6d2c2cca822ba7cebd140c9f27f405b67f1" resolved "https://registry.yarnpkg.com/compromise/-/compromise-13.11.4.tgz#3136e6d2c2cca822ba7cebd140c9f27f405b67f1"
integrity sha512-nBITcNdqIHSVDDluaG6guyFFCSNXN+Hu87fU8VlhkE5Z0PwTZN1nro2O7a8JcUH88nB5EOzrxd9zKfXLSNFqcg== integrity sha512-nBITcNdqIHSVDDluaG6guyFFCSNXN+Hu87fU8VlhkE5Z0PwTZN1nro2O7a8JcUH88nB5EOzrxd9zKfXLSNFqcg==
@@ -15855,11 +15841,6 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2" extend-shallow "^3.0.2"
safe-regex "^1.1.0" 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: regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3:
version "1.4.3" version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"