🔌 Socket + RabbitMQ setup for download-client touched folders/files

This commit is contained in:
2021-09-18 09:25:59 -07:00
parent 476a55614e
commit be43c163dc
13 changed files with 265 additions and 86 deletions

View File

@@ -1,4 +1,5 @@
import axios from "axios";
import { useContext } from "react";
import { IFolderData, IExtractedComicBookCoverFile } from "threetwo-ui-typings";
import { API_BASE_URI, SOCKET_BASE_URI } from "../constants/endpoints";
import {
@@ -15,14 +16,8 @@ import {
} from "../constants/action-types";
import { refineQuery } from "../shared/utils/filenameparser.utils";
import sortBy from "array-sort-by";
import { io } from "socket.io-client";
import {
success,
error,
warning,
info,
removeAll,
} from "react-notification-system-redux";
import { success } from "react-notification-system-redux";
import { WebSocketContext } from "../context/socket/socket.context";
export async function walkFolder(path: string): Promise<Array<IFolderData>> {
return axios
@@ -51,24 +46,8 @@ export async function walkFolder(path: string): Promise<Array<IFolderData>> {
* @return {Promise<string>} HTML of the page
*/
export const fetchComicBookMetadata = (options) => async (dispatch) => {
const socket = io(SOCKET_BASE_URI, {
reconnectionDelayMax: 10000,
secure: false,
rejectUnauthorized: false,
});
socket.on("connect", () => {
console.log(`connect ${socket.id}`);
dispatch({
type: RMQ_SOCKET_CONNECTED,
isSocketConnected: true,
socketId: socket.id,
});
});
socket.on("disconnect", () => {
console.log(`disconnect`);
});
const socket = useContext(WebSocketContext);
const extractionOptions = {
sourceFolder: options,
extractTarget: "cover",
targetExtractionFolder: "./userdata/covers",
extractionMode: "bulk",
@@ -82,7 +61,7 @@ export const fetchComicBookMetadata = (options) => async (dispatch) => {
success({
// uid: 'once-please', // you can specify your own uid if required
title: "Import Started",
message: `${socket.id} connected. ${walkedFolders.length} comics scanned.`,
message: `<span class="icon-text has-text-success"><i class="fas fa-plug"></i></span> Socket <span class="has-text-info">${socket.id}</span> connected. <strong>${walkedFolders.length}</strong> comics scanned.`,
dismissible: "click",
position: "tr",
autoDismiss: 0,

View File

@@ -28,6 +28,15 @@ $border-color: red;
.min {
overflow: visible;
margin: auto;
.tag__custom {
height: auto !important;
padding: 0.3rem;
white-space: unset !important;
width: 100%;
background-color: #effaf5;
color: #257953;
}
.tags {
display: inline;
margin-right: 5px;

View File

@@ -29,6 +29,19 @@ const style = {
},
tr: {
top: "40px",
right: "10px",
},
},
Title: {
DefaultStyle: {
fontSize: "14px",
margin: "0 0 5px 0",
padding: 0,
fontWeight: "bold",
},
success: {
color: "hsl(141, 71%, 48%)",
},
},
NotificationItem: {
@@ -36,7 +49,7 @@ const style = {
success: {
// Applied to every notification, regardless of the notification level
borderTop: "none",
backgroundColor: "none",
backgroundColor: "#FFF",
borderRadius: "0.4rem",
WebkitBoxShadow: "-7px 11px 25px -9px rgba(0, 0, 0, 0.3)",
MozBoxShadow: "-7px 11px 25px -9px rgba(0, 0, 0, 0.3)",
@@ -53,6 +66,7 @@ export const App = (): ReactElement => {
notifications={notifications}
style={style}
newOnTop={true}
allowHTML={true}
/>
<Switch>
<Route exact path="/">

View File

@@ -213,10 +213,26 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
<dl>
<dt>Raw File Details</dt>
<dd>{props.data.containedIn}</dd>
<dd>{prettyBytes(props.data.fileSize)}</dd>
<dd>{props.data.path}</dd>
<dd>
<span className="tag is-primary">{props.data.extension}</span>
<div className="field is-grouped">
<div className="control">
<div className="tags has-addons">
<span className="tag">Size</span>
<span className="tag is-info is-light">
{prettyBytes(props.data.fileSize)}
</span>
</div>
</div>
<div className="control">
<div className="tags has-addons">
<span className="tag">Extension</span>
<span className="tag is-primary is-light">
{props.data.extension}
</span>
</div>
</div>
</div>
</dd>
</dl>
</div>

View File

@@ -1,10 +1,12 @@
import React, { ReactElement, useCallback } from "react";
import { isNil, isUndefined } from "lodash";
import { isEmpty, isNil, isUndefined } from "lodash";
import { useSelector, useDispatch } from "react-redux";
import { fetchComicBookMetadata } from "../actions/fileops.actions";
import { IFolderData } from "threetwo-ui-typings";
import { LazyLog, ScrollFollow } from "react-lazylog";
import DynamicList, { createCache } from "react-window-dynamic-list";
;
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import Loader from "react-loader-spinner";
interface IProps {
matches?: unknown;
@@ -32,13 +34,27 @@ export const Import = (props: IProps): ReactElement => {
console.log(state);
return state.fileOps.isSocketConnected;
});
const importResults = useSelector(
(state: RootState) => state.fileOps.comicBookMetadata,
);
const IMSCallInProgress = useSelector(
(state: RootState) => state.fileOps.IMSCallInProgress,
);
const initiateImport = useCallback(() => {
if (typeof props.path !== "undefined") {
console.log("asdasd");
dispatch(fetchComicBookMetadata(props.path));
}
}, [dispatch]);
const cache = createCache();
const renderRow = ({ index, style }) => (
<li className="is-size-7" style={style}>
<strong>{importResults[index].comicBookCoverMetadata.name} </strong>
<br />
{importResults[index].comicBookCoverMetadata.path}
<hr />
</li>
);
return (
<div className="container">
@@ -78,6 +94,37 @@ export const Import = (props: IProps): ReactElement => {
<span>Import and Tag</span>
</button>
</p>
{!isEmpty(importResults) ? (
<>
<h3>Import Results</h3>
<hr />
<ul>
<DynamicList
data={importResults}
cache={cache}
height={800}
width={"100%"}
lazyMeasurement={false}
>
{renderRow}
</DynamicList>
</ul>
</>
) : (
<div className="progress-indicator-container">
<div className="indicator">
<Loader
type="MutatingDots"
color="#CCC"
secondaryColor="#999"
height={100}
width={100}
visible={IMSCallInProgress}
/>
</div>
</div>
)}
</section>
</div>
);

View File

@@ -0,0 +1,37 @@
import React, { createContext } from "react";
import io, { Socket } from "socket.io-client";
import { SOCKET_BASE_URI } from "../../constants/endpoints";
import { useDispatch } from "react-redux";
import { RMQ_SOCKET_CONNECTED } from "../../constants/action-types";
const WebSocketContext = createContext(null);
export const WebSocketProvider = ({ children }) => {
let socket: Socket;
let ws;
const dispatch = useDispatch();
if (!socket) {
socket = io(SOCKET_BASE_URI);
socket.on("connect", () => {
dispatch({
type: RMQ_SOCKET_CONNECTED,
isSocketConnected: true,
socketId: socket.id,
});
});
socket.on("disconnect", () => {
console.log(`disconnect`);
});
ws = {
socket: socket,
};
}
return (
<WebSocketContext.Provider value={ws}>{children}</WebSocketContext.Provider>
);
};
export { WebSocketContext };
export default WebSocketProvider;

View File

@@ -2,6 +2,9 @@ import * as React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { ConnectedRouter } from "connected-react-router";
import WebSocketProvider, {
WebSocketContext,
} from "./context/socket/socket.context";
import configureStore, { history } from "./store/index";
import App from "./components/App";
@@ -10,9 +13,11 @@ const rootEl = document.getElementById("root");
render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
<WebSocketProvider>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</WebSocketProvider>
</Provider>,
rootEl,
);

View File

@@ -12,7 +12,7 @@ import {
IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
} from "../constants/action-types";
const initialState = {
dataTransferred: false,
IMSCallInProgress: false,
comicBookMetadata: [],
isSocketConnected: false,
isComicVineMetadataImportInProgress: false,
@@ -26,7 +26,7 @@ function fileOpsReducer(state = initialState, action) {
return {
...state,
comicBookMetadata: [...state.comicBookMetadata, action.data],
dataTransferred: true,
IMSCallInProgress: false,
};
case RMQ_SOCKET_CONNECTED:
@@ -34,6 +34,7 @@ function fileOpsReducer(state = initialState, action) {
...state,
isSocketConnected: action.isSocketConnected,
socketId: action.socketId,
IMSCallInProgress: true,
};
case IMS_RAW_IMPORT_SUCCESSFUL:
return {

View File

@@ -4,9 +4,10 @@ export const detectTradePaperbacks = (deck): any => {
const paperback = [
/((trade)?\s?(paperback)|(tpb))/gim, // https://regex101.com/r/FhuowT/1
/(hard\s?cover)\s?(collect((ion)|(ed)|(ing)))/gim, //https://regex101.com/r/eFJVRM/1
/(collected\s?editions)/gim, // https://regex101.com/r/40pAm5/1
/(?:collects|issues|issue)/gim,
];
const miniSeries = [
]
const matches = paperback
.map((regex) => {
return deck.match(regex);

View File

@@ -1,8 +1,9 @@
import express, { Request, Response, Router, Express } from "express";
import bodyParser from "body-parser";
import { createServer } from "http";
import { Server, Socket } from "socket.io";
import { Server } from "socket.io";
import router from "./route";
const amqp = require("amqplib/callback_api");
// call express
const app: Express = express(); // define our app using express
@@ -42,6 +43,35 @@ io.on("connection", (socket) => {
});
});
amqp.connect("amqp://localhost", (error0, connection) => {
if (error0) {
throw error0;
}
connection.createChannel((error1, channel) => {
if (error1) {
throw error1;
}
const queue = "comicBookCovers";
channel.assertQueue(queue, {
durable: false,
});
console.log(`Connected to ${queue} queue.`);
console.log(`Waiting for comic book cover data in ${queue}`);
channel.consume(
queue,
(data) => {
//Socket Trigger All Clients
io.sockets.emit("coverExtracted", JSON.parse(data.content.toString()));
},
{
noAck: true,
},
);
});
});
// socket server
httpServer.listen(8051);
console.log(`Socket server is listening on 8051`);

View File

@@ -1,8 +1,6 @@
import router from "../router";
import { Request, Response } from "express";
const amqp = require("amqplib/callback_api");
import axios from "axios";
import { io } from "../../index";
router.route("/getComicCovers").post(async (req: Request, res: Response) => {
typeof req.body === "object" ? req.body : {};
@@ -14,41 +12,7 @@ router.route("/getComicCovers").post(async (req: Request, res: Response) => {
walkedFolders: req.body.walkedFolders,
},
});
const queueConsumer = amqp.connect(
"amqp://localhost",
(error0, connection) => {
if (error0) {
throw error0;
}
connection.createChannel((error1, channel) => {
if (error1) {
throw error1;
}
const queue = "comicBookCovers";
channel.assertQueue(queue, {
durable: false,
});
console.log(`Connected to ${queue} queue.`);
console.log(`Waiting for comic book cover data in ${queue}`);
channel.consume(
queue,
(data) => {
//Socket Trigger All Clients
io.sockets.emit(
"coverExtracted",
JSON.parse(data.content.toString()),
);
},
{
noAck: true,
},
);
});
},
);
res.send({ queue: queueConsumer });
res.send({ po: "jo" });
});
export default router;