🏗️ Refactoring archive uncompression for "Read Comic" and "Analysis" user flows (#46)

* 🔧 Refactoring uncompression methods on client-side

* ✏️ Refactoring

* 👁️ Updates to the comic viewer

* 🖼️ Added screenshots from December 2022

* ✏️ Fixed typo in README

* 🏗️ Massive refactor around archive uncompression for reading/analysis

* 🔧 Tweaked state vars for reading and analysis

* 🏗️ 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 was merged in pull request #46.
This commit is contained in:
2022-12-21 21:17:38 -08:00
committed by GitHub
parent f854ff9cc6
commit d065225d8e
14 changed files with 158 additions and 93 deletions

View File

@@ -63,7 +63,7 @@
"pretty-bytes": "^5.6.0", "pretty-bytes": "^5.6.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"qs": "^6.10.5", "qs": "^6.10.5",
"react": "^18.1.0", "react": "^18.2.0",
"react-collapsible": "^2.9.0", "react-collapsible": "^2.9.0",
"react-comic-viewer": "^0.4.0", "react-comic-viewer": "^0.4.0",
"react-day-picker": "^8.0.6", "react-day-picker": "^8.0.6",

View File

@@ -36,7 +36,7 @@ import {
CV_WEEKLY_PULLLIST_FETCHED, CV_WEEKLY_PULLLIST_FETCHED,
} from "../constants/action-types"; } from "../constants/action-types";
import { success } from "react-notification-system-redux"; import { success } from "react-notification-system-redux";
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
import { isNil, map } from "lodash"; import { isNil, map } from "lodash";
export async function walkFolder(path: string): Promise<Array<IFolderData>> { export async function walkFolder(path: string): Promise<Array<IFolderData>> {
@@ -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,
@@ -260,34 +254,31 @@ export const fetchComicVineMatches =
* @returns {any} * @returns {any}
*/ */
export const extractComicArchive = export const extractComicArchive =
(path: string, options: any): any => async (dispatch) => {
const comicBookPages: string[] = [];
console.log(options);
dispatch({
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
});
const extractedComicBookArchive = await axios({
method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: {
filePath: path,
options,
},
});
map(extractedComicBookArchive.data, (page) => {
const pageFilePath = removeLeadingPeriod(page);
const imagePath = encodeURI(`${LIBRARY_SERVICE_HOST}${pageFilePath}`);
comicBookPages.push(imagePath);
});
dispatch({
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS,
extractedComicBookArchive: comicBookPages,
});
};
(path: string, options: any): any =>
async (dispatch) => {
dispatch({
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
});
await axios({
method: "POST",
url: `${LIBRARY_SERVICE_BASE_URI}/uncompressFullArchive`,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: {
filePath: path,
options,
},
});
};
/**
* Description
* @param {any} query
* @param {any} options
* @returns {any}
*/
export const searchIssue = (query, options) => async (dispatch) => { export const searchIssue = (query, options) => async (dispatch) => {
dispatch({ dispatch({
type: SS_SEARCH_IN_PROGRESS, type: SS_SEARCH_IN_PROGRESS,

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

@@ -66,7 +66,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
); );
const extractedComicBook = useSelector( const extractedComicBook = useSelector(
(state: RootState) => state.fileOps.extractedComicBookArchive, (state: RootState) => state.fileOps.extractedComicBookArchive.reading,
); );
const { comicObjectId } = useParams<{ comicObjectId: string }>(); const { comicObjectId } = useParams<{ comicObjectId: string }>();
@@ -77,7 +77,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
dispatch( dispatch(
extractComicArchive(filePath, { extractComicArchive(filePath, {
type: "full", type: "full",
purpose: "readComicBook", purpose: "reading",
imageResizeOptions: { imageResizeOptions: {
baseWidth: 1024, baseWidth: 1024,
}, },

View File

@@ -38,7 +38,8 @@ 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

@@ -14,7 +14,7 @@ export const ArchiveOperations = (props): ReactElement => {
(state: RootState) => state.fileOps.comicBookExtractionInProgress, (state: RootState) => state.fileOps.comicBookExtractionInProgress,
); );
const extractedComicBookArchive = useSelector( const extractedComicBookArchive = useSelector(
(state: RootState) => state.fileOps.extractedComicBookArchive, (state: RootState) => state.fileOps.extractedComicBookArchive.analysis,
); );
const imageAnalysisResult = useSelector((state: RootState) => { const imageAnalysisResult = useSelector((state: RootState) => {
@@ -23,7 +23,15 @@ export const ArchiveOperations = (props): ReactElement => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const unpackComicArchive = useCallback(() => { const unpackComicArchive = useCallback(() => {
dispatch(extractComicArchive(data.rawFileDetails.filePath)); dispatch(
extractComicArchive(data.rawFileDetails.filePath, {
type: "full",
purpose: "analysis",
imageResizeOptions: {
baseWidth: 275,
},
}),
);
}, []); }, []);
// sliding panel config // sliding panel config

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

@@ -81,6 +81,8 @@ export const IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS =
export const IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_FAILED = export const IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_FAILED =
"IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_FAILED"; "IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_FAILED";
export const COMICBOOK_EXTRACTION_SUCCESS = "COMICBOOK_EXTRACTION_SUCCESS";
// Image file stats // Image file stats
export const IMG_ANALYSIS_CALL_IN_PROGRESS = "IMG_ANALYSIS_CALL_IN_PROGRESS"; export const IMG_ANALYSIS_CALL_IN_PROGRESS = "IMG_ANALYSIS_CALL_IN_PROGRESS";
export const IMG_ANALYSIS_DATA_FETCH_SUCCESS = export const IMG_ANALYSIS_DATA_FETCH_SUCCESS =

View File

@@ -29,7 +29,11 @@ import {
SS_SEARCH_FAILED, SS_SEARCH_FAILED,
SS_SEARCH_RESULTS_FETCHED_SPECIAL, SS_SEARCH_RESULTS_FETCHED_SPECIAL,
VOLUMES_FETCHED, VOLUMES_FETCHED,
COMICBOOK_EXTRACTION_SUCCESS,
} from "../constants/action-types"; } from "../constants/action-types";
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
import { LIBRARY_SERVICE_HOST } from "../constants/endpoints";
const initialState = { const initialState = {
IMSCallInProgress: false, IMSCallInProgress: false,
IMGCallInProgress: false, IMGCallInProgress: false,
@@ -42,7 +46,10 @@ const initialState = {
isComicVineMetadataImportInProgress: false, isComicVineMetadataImportInProgress: false,
comicVineMetadataImportError: {}, comicVineMetadataImportError: {},
rawImportError: {}, rawImportError: {},
extractedComicBookArchive: [], extractedComicBookArchive: {
reading: [],
analysis: [],
},
recentComics: [], recentComics: [],
wantedComics: [], wantedComics: [],
libraryComics: [], libraryComics: [],
@@ -81,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 {
@@ -131,13 +138,7 @@ function fileOpsReducer(state = initialState, action) {
comicBookExtractionInProgress: true, comicBookExtractionInProgress: true,
}; };
} }
case IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_SUCCESS: {
return {
...state,
extractedComicBookArchive: action.extractedComicBookArchive,
comicBookExtractionInProgress: false,
};
}
case LOCATION_CHANGE: { case LOCATION_CHANGE: {
return { return {
...state, ...state,
@@ -152,11 +153,44 @@ 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]
}; };
} }
case COMICBOOK_EXTRACTION_SUCCESS: {
const comicBookPages: string[] = [];
map(action.result.files, (page) => {
const pageFilePath = removeLeadingPeriod(page);
const imagePath = encodeURI(`${LIBRARY_SERVICE_HOST}${pageFilePath}`);
comicBookPages.push(imagePath);
});
switch (action.result.purpose) {
case "reading":
return {
...state,
extractedComicBookArchive: {
reading: comicBookPages,
},
comicBookExtractionInProgress: false,
};
case "analysis":
return {
...state,
extractedComicBookArchive: {
analysis: comicBookPages,
},
comicBookExtractionInProgress: false,
};
}
}
case LS_QUEUE_DRAINED: { case LS_QUEUE_DRAINED: {
console.log("drained", action); console.log("drained", action);
return { return {
@@ -229,7 +263,7 @@ function fileOpsReducer(state = initialState, action) {
volumes: action.data, volumes: action.data,
SSCallInProgress: false, SSCallInProgress: false,
}; };
case SS_SEARCH_FAILED: { case SS_SEARCH_FAILED: {
return { return {
...state, ...state,

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==