🏗️ Refactor to support DC++ and socket.io integration

This refactor covers the following workflows:

1. Adding a comic from LOCG or ComicVine adds it to the wanted list
2. Downloading that comic from DC++ correctly adds download metadata to the corresponding comic object in mongo
3. Successful download triggers automatic import to library and cover extraction, metadata application
This commit is contained in:
2022-12-21 21:07:22 -08:00
parent bf6f18c5d5
commit 0d41b57d18
10 changed files with 96 additions and 70 deletions

View File

@@ -60,15 +60,6 @@ export async function walkFolder(path: string): Promise<Array<IFolderData>> {
* @return the comic book metadata * @return the comic book metadata
*/ */
export const fetchComicBookMetadata = () => async (dispatch) => { export const fetchComicBookMetadata = () => async (dispatch) => {
const extractionOptions = {
extractTarget: "cover",
targetExtractionFolder: "./userdata/covers",
extractionMode: "bulk",
paginationOptions: {
pageLimit: 25,
page: 1,
},
};
dispatch({ dispatch({
type: LS_IMPORT_CALL_IN_PROGRESS, type: LS_IMPORT_CALL_IN_PROGRESS,
}); });
@@ -86,7 +77,7 @@ export const fetchComicBookMetadata = () => async (dispatch) => {
dispatch({ dispatch({
type: LS_IMPORT, type: LS_IMPORT,
meta: { remote: true }, meta: { remote: true },
data: { extractionOptions }, data: {},
}); });
}; };
export const toggleImportQueueStatus = (options) => async (dispatch) => { export const toggleImportQueueStatus = (options) => async (dispatch) => {
@@ -136,21 +127,24 @@ export const getComicBooks = (options) => async (dispatch) => {
* @returns Nothing. * @returns Nothing.
* @param payload * @param payload
*/ */
export const importToDB = (sourceName: string, payload?: any) => (dispatch) => { export const importToDB = (sourceName: string, metadata?: any) => (dispatch) => {
try { try {
const comicBookMetadata = { const comicBookMetadata = {
rawFileDetails: { importType: "new",
name: "", payload: {
}, rawFileDetails: {
importStatus: { name: "",
isImported: true,
tagged: false,
matchedResult: {
score: "0",
}, },
}, importStatus: {
sourcedMetadata: payload || null, isImported: true,
acquisition: { source: { wanted: true, name: sourceName } }, tagged: false,
matchedResult: {
score: "0",
},
},
sourcedMetadata: metadata || null,
acquisition: { source: { wanted: true, name: sourceName } },
}
}; };
dispatch({ dispatch({
type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS, type: IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
@@ -261,22 +255,22 @@ export const fetchComicVineMatches =
*/ */
export const extractComicArchive = export const extractComicArchive =
(path: string, options: any): any => (path: string, options: any): any =>
async (dispatch) => { async (dispatch) => {
dispatch({ dispatch({
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS, type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
}); });
await axios({ await axios({
method: "POST", method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`, url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`,
headers: { headers: {
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
}, },
data: { data: {
filePath: path, filePath: path,
options, options,
}, },
}); });
}; };
/** /**
* Description * Description

View File

@@ -18,18 +18,22 @@ import {
AirDCPPSocketContext, AirDCPPSocketContext,
} from "../context/AirDCPPSocket"; } from "../context/AirDCPPSocket";
import { isEmpty, isUndefined } from "lodash"; import { isEmpty, isUndefined } from "lodash";
import { AIRDCPP_DOWNLOAD_PROGRESS_TICK } from "../constants/action-types"; import {
import { useDispatch } from "react-redux"; AIRDCPP_DOWNLOAD_PROGRESS_TICK,
LS_SINGLE_IMPORT,
} from "../constants/action-types";
import { useDispatch, useSelector } from "react-redux";
/** /**
* Method that initializes an AirDC++ socket connection * Method that initializes an AirDC++ socket connection
* 1. Initializes event listeners for download init, tick and complete events * 1. Initializes event listeners for download init, tick and complete events
* 2. Handles errors in case the connection to AirDC++ is not established or terminated * 2. Handles errors in case the connection to AirDC++ is not established or terminated
* @returns void * @returns void
*/ */
const AirDCPPSocketComponent = (): ReactElement => { const AirDCPPSocketComponent = (): ReactElement => {
const airDCPPConfiguration = useContext(AirDCPPSocketContext); const airDCPPConfiguration = useContext(AirDCPPSocketContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
const initializeAirDCPPEventListeners = async () => { const initializeAirDCPPEventListeners = async () => {
if ( if (
@@ -42,9 +46,7 @@ const AirDCPPSocketComponent = (): ReactElement => {
"queue_bundle_added", "queue_bundle_added",
async (data) => { async (data) => {
console.log("JEMEN:", data); console.log("JEMEN:", data);
},
}
); );
// download tick listener // download tick listener
await airDCPPConfiguration.airDCPPState.socket.addListener( await airDCPPConfiguration.airDCPPState.socket.addListener(
@@ -62,9 +64,18 @@ const AirDCPPSocketComponent = (): ReactElement => {
`queue`, `queue`,
"queue_bundle_status", "queue_bundle_status",
async (bundleData) => { async (bundleData) => {
let count = 0;
if (bundleData.status.completed && bundleData.status.downloaded) { if (bundleData.status.completed && bundleData.status.downloaded) {
// dispatch the action for raw import, with the metadata // dispatch the action for raw import, with the metadata
console.log("IM THE MAN UP IN THIS") if (count < 1) {
console.log(`[AirDCPP]: Download complete.`);
dispatch({
type: LS_SINGLE_IMPORT,
meta: { remote: true },
data: bundleData,
});
count += 1;
}
} }
}, },
); );
@@ -92,7 +103,10 @@ export const App = (): ReactElement => {
<Routes> <Routes>
<Route path="/" element={<Dashboard />} /> <Route path="/" element={<Dashboard />} />
<Route path="/import" element={<Import path={"./comics"} />} /> <Route path="/import" element={<Import path={"./comics"} />} />
<Route path="/library" element={<TabulatedContentContainer category="library" />} /> <Route
path="/library"
element={<TabulatedContentContainer category="library" />}
/>
<Route path="/library-grid" element={<LibraryGrid />} /> <Route path="/library-grid" element={<LibraryGrid />} />
<Route path="/downloads" element={<Downloads data={{}} />} /> <Route path="/downloads" element={<Downloads data={{}} />} />
<Route path="/search" element={<Search />} /> <Route path="/search" element={<Search />} />
@@ -105,9 +119,18 @@ export const App = (): ReactElement => {
element={<VolumeDetail />} element={<VolumeDetail />}
/> />
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="/pull-list/all" element={<TabulatedContentContainer category="pullList" />} /> <Route
<Route path="/wanted/all" element={<TabulatedContentContainer category="wanted" />} /> path="/pull-list/all"
<Route path="/volumes/all" element={<TabulatedContentContainer category="volumes" />} /> element={<TabulatedContentContainer category="pullList" />}
/>
<Route
path="/wanted/all"
element={<TabulatedContentContainer category="wanted" />}
/>
<Route
path="/volumes/all"
element={<TabulatedContentContainer category="volumes" />}
/>
</Routes> </Routes>
</div> </div>
</AirDCPPSocketContextProvider> </AirDCPPSocketContextProvider>

View File

@@ -16,6 +16,7 @@ import ellipsize from "ellipsize";
import { Form, Field } from "react-final-form"; import { Form, Field } from "react-final-form";
import { isEmpty, isNil, map } from "lodash"; import { isEmpty, isNil, map } from "lodash";
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket"; import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
interface IAcquisitionPanelProps { interface IAcquisitionPanelProps {
query: any; query: any;
comicObjectId: any; comicObjectId: any;
@@ -96,9 +97,12 @@ export const AcquisitionPanel = (
(searchInstanceId, resultId, name, size, type) => { (searchInstanceId, resultId, name, size, type) => {
dispatch( dispatch(
downloadAirDCPPItem( downloadAirDCPPItem(
searchInstanceId, resultId, searchInstanceId,
resultId,
props.comicObjectId, props.comicObjectId,
name, size, type, name,
size,
type,
airDCPPConfiguration.airDCPPState.socket, airDCPPConfiguration.airDCPPState.socket,
{ {
username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`, username: `${airDCPPConfiguration.airDCPPState.settings.directConnect.client.host.username}`,

View File

@@ -38,7 +38,7 @@ export const RawFileDetails = (props): ReactElement => {
</dd> </dd>
</dl> </dl>
</div> </div>
<div className="content comic-detail raw-file-details mt-3 column is-one-third"> <div className="content comic-detail raw-file-details mt-3 column is-three-fifths">
<dl> <dl>
{/* inferred metadata */} {/* inferred metadata */}
<dt>Inferred Issue Metadata</dt> <dt>Inferred Issue Metadata</dt>

View File

@@ -11,7 +11,7 @@ import {
getComicBooks, getComicBooks,
} from "../../actions/fileops.actions"; } from "../../actions/fileops.actions";
import { getLibraryStatistics } from "../../actions/comicinfo.actions"; import { getLibraryStatistics } from "../../actions/comicinfo.actions";
import { isEmpty } from "lodash"; import { isEmpty, isNil } from "lodash";
export const Dashboard = (): ReactElement => { export const Dashboard = (): ReactElement => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -43,7 +43,7 @@ export const Dashboard = (): ReactElement => {
}, []); }, []);
const recentComics = useSelector( const recentComics = useSelector(
(state: RootState) => state.fileOps.recentComics, (state: RootState) => state.fileOps.recentComics
); );
const wantedComics = useSelector( const wantedComics = useSelector(
(state: RootState) => state.fileOps.wantedComics, (state: RootState) => state.fileOps.wantedComics,
@@ -60,7 +60,7 @@ export const Dashboard = (): ReactElement => {
<section className="section"> <section className="section">
<h1 className="title">Dashboard</h1> <h1 className="title">Dashboard</h1>
{!isEmpty(recentComics) && !isEmpty(recentComics.docs) ? ( {!isEmpty(recentComics) ? (
<> <>
{/* Pull List */} {/* Pull List */}
<PullList issues={recentComics} /> <PullList issues={recentComics} />
@@ -74,9 +74,8 @@ export const Dashboard = (): ReactElement => {
<WantedComicsList comics={wantedComics} /> <WantedComicsList comics={wantedComics} />
)} )}
{/* Recent imports */} {/* Recent imports */}
{!isEmpty(recentComics) && (
<RecentlyImported comicBookCovers={recentComics} /> <RecentlyImported comicBookCovers={recentComics} />
)}
{/* Volumes */} {/* Volumes */}
{!isEmpty(volumeGroups) && ( {!isEmpty(volumeGroups) && (
<VolumeGroups volumeGroups={volumeGroups} /> <VolumeGroups volumeGroups={volumeGroups} />

View File

@@ -20,7 +20,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
useEffect(() => { useEffect(() => {
dispatch( dispatch(
getWeeklyPullList({ getWeeklyPullList({
startDate: "2022-11-15", startDate: "2022-12-25",
pageSize: "15", pageSize: "15",
currentPage: "1", currentPage: "1",
}), }),

View File

@@ -24,7 +24,6 @@ export const RecentlyImported = ({
700: 2, 700: 2,
600: 2, 600: 2,
}; };
return ( return (
<> <>
<div className="content mt-5"> <div className="content mt-5">
@@ -41,7 +40,7 @@ export const RecentlyImported = ({
columnClassName="recent-comics-column" columnClassName="recent-comics-column"
> >
{map( {map(
comicBookCovers.docs, comicBookCovers,
( (
{ {
_id, _id,
@@ -53,6 +52,7 @@ export const RecentlyImported = ({
}, },
idx, idx,
) => { ) => {
console.log(comicvine);
const { issueName, url } = determineCoverFile({ const { issueName, url } = determineCoverFile({
rawFileDetails, rawFileDetails,
comicvine, comicvine,
@@ -64,7 +64,7 @@ export const RecentlyImported = ({
comicInfo, comicInfo,
locg, locg,
}); });
console.log(name);
const isComicBookMetadataAvailable = const isComicBookMetadataAvailable =
!isUndefined(comicvine) && !isUndefined(comicvine) &&
!isUndefined(comicvine.volumeInformation); !isUndefined(comicvine.volumeInformation);
@@ -123,7 +123,7 @@ export const RecentlyImported = ({
</div> </div>
</Card> </Card>
{/* metadata card */} {/* metadata card */}
{!isNil(name) ? ( {!isNil(name) && (
<Card orientation="horizontal" hasDetails imageUrl={coverURL}> <Card orientation="horizontal" hasDetails imageUrl={coverURL}>
<dd className="is-size-9"> <dd className="is-size-9">
<dl> <dl>
@@ -138,7 +138,7 @@ export const RecentlyImported = ({
</dl> </dl>
</dd> </dd>
</Card> </Card>
) : null} )}
</React.Fragment> </React.Fragment>
); );
}, },

View File

@@ -88,7 +88,7 @@ function fileOpsReducer(state = initialState, action) {
case IMS_RECENT_COMICS_FETCHED: case IMS_RECENT_COMICS_FETCHED:
return { return {
...state, ...state,
recentComics: action.data, recentComics: action.data.docs,
}; };
case IMS_WANTED_COMICS_FETCHED: case IMS_WANTED_COMICS_FETCHED:
return { return {
@@ -153,9 +153,13 @@ function fileOpsReducer(state = initialState, action) {
} }
case LS_COVER_EXTRACTED: { case LS_COVER_EXTRACTED: {
console.log("BASH", action); console.log("BASH", action);
if(state.recentComics.length === 5) {
state.recentComics.pop();
}
return { return {
...state, ...state,
librarySearchResultCount: state.librarySearchResultCount + 1, librarySearchResultCount: state.librarySearchResultCount + 1,
recentComics: [...state.recentComics, action.result.data.importResult]
}; };
} }
@@ -172,7 +176,7 @@ function fileOpsReducer(state = initialState, action) {
return { return {
...state, ...state,
extractedComicBookArchive: { extractedComicBookArchive: {
reading: comicBookPages reading: comicBookPages,
}, },
comicBookExtractionInProgress: false, comicBookExtractionInProgress: false,
}; };
@@ -181,12 +185,11 @@ function fileOpsReducer(state = initialState, action) {
return { return {
...state, ...state,
extractedComicBookArchive: { extractedComicBookArchive: {
analysis: comicBookPages analysis: comicBookPages,
}, },
comicBookExtractionInProgress: false, comicBookExtractionInProgress: false,
}; };
} }
} }
case LS_QUEUE_DRAINED: { case LS_QUEUE_DRAINED: {
console.log("drained", action); console.log("drained", action);

View File

@@ -67,7 +67,10 @@ export const determineCoverFile = (data) => {
} }
}; };
export const determineExternalMetadata = (metadataSource, source) => { export const determineExternalMetadata = (
metadataSource: string,
source: any
) => {
switch (metadataSource) { switch (metadataSource) {
case "comicvine": case "comicvine":
return { return {

View File

@@ -15607,7 +15607,7 @@ react-transition-group@4.4.2, react-transition-group@^4.3.0:
loose-envify "^1.4.0" loose-envify "^1.4.0"
prop-types "^15.6.2" prop-types "^15.6.2"
react@^18.1.0: react@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==