🏗️ Refactor for import of downloaded comics to use socket.io

This commit is contained in:
2022-12-21 21:04:40 -08:00
parent fe4ba87256
commit 6a4cf1d82f
6 changed files with 147 additions and 131 deletions

18
package-lock.json generated
View File

@@ -45,7 +45,6 @@
"mongoose-paginate-v2": "^1.3.18", "mongoose-paginate-v2": "^1.3.18",
"nats": "^1.3.2", "nats": "^1.3.2",
"node-calibre": "^2.1.1", "node-calibre": "^2.1.1",
"node-unrar-js": "^1.0.5",
"opds-extra": "^3.0.9", "opds-extra": "^3.0.9",
"p7zip-threetwo": "^1.0.4", "p7zip-threetwo": "^1.0.4",
"sanitize-filename-ts": "^1.0.2", "sanitize-filename-ts": "^1.0.2",
@@ -73,7 +72,7 @@
"typescript": "^4.6.4" "typescript": "^4.6.4"
}, },
"engines": { "engines": {
"node": ">= 10.x.x" "node": ">= 18.x.x"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@@ -8513,14 +8512,6 @@
"url": "https://github.com/sponsors/antelle" "url": "https://github.com/sponsors/antelle"
} }
}, },
"node_modules/node-unrar-js": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/node-unrar-js/-/node-unrar-js-1.0.6.tgz",
"integrity": "sha512-jObs9hXUmXldlQI04/oYb+97RolAx4aJO08Rz5mRE/wFdwzcftuffEPZLZao+c0nKiwXCT77ZUN+sp/5lnpA2w==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -20336,7 +20327,7 @@
}, },
"moleculer-bull": { "moleculer-bull": {
"version": "git+ssh://git@github.com/rishighan/moleculer-bull.git#487020c3f3b4879bbf1bb40c75f19a259d17dd59", "version": "git+ssh://git@github.com/rishighan/moleculer-bull.git#487020c3f3b4879bbf1bb40c75f19a259d17dd59",
"from": "moleculer-bull@rishighan/moleculer-bull#1.0.0", "from": "moleculer-bull@github:rishighan/moleculer-bull#1.0.0",
"requires": { "requires": {
"bull": "^4.10.2", "bull": "^4.10.2",
"lodash": "^4.17.21" "lodash": "^4.17.21"
@@ -20686,11 +20677,6 @@
"resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz",
"integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==" "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw=="
}, },
"node-unrar-js": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/node-unrar-js/-/node-unrar-js-1.0.6.tgz",
"integrity": "sha512-jObs9hXUmXldlQI04/oYb+97RolAx4aJO08Rz5mRE/wFdwzcftuffEPZLZao+c0nKiwXCT77ZUN+sp/5lnpA2w=="
},
"normalize-path": { "normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",

View File

@@ -73,7 +73,6 @@
"mongoose-paginate-v2": "^1.3.18", "mongoose-paginate-v2": "^1.3.18",
"nats": "^1.3.2", "nats": "^1.3.2",
"node-calibre": "^2.1.1", "node-calibre": "^2.1.1",
"node-unrar-js": "^1.0.5",
"opds-extra": "^3.0.9", "opds-extra": "^3.0.9",
"p7zip-threetwo": "^1.0.4", "p7zip-threetwo": "^1.0.4",
"sanitize-filename-ts": "^1.0.2", "sanitize-filename-ts": "^1.0.2",
@@ -85,7 +84,7 @@
"xml2js": "^0.4.23" "xml2js": "^0.4.23"
}, },
"engines": { "engines": {
"node": ">= 10.x.x" "node": ">= 18.x.x"
}, },
"jest": { "jest": {
"coverageDirectory": "<rootDir>/coverage", "coverageDirectory": "<rootDir>/coverage",

View File

@@ -34,6 +34,7 @@ SOFTWARE.
"use strict"; "use strict";
import { refineQuery } from "filename-parser"; import { refineQuery } from "filename-parser";
import { isNil, isUndefined } from "lodash";
import { Context, Service, ServiceBroker, ServiceSchema } from "moleculer"; import { Context, Service, ServiceBroker, ServiceSchema } from "moleculer";
import BullMQMixin, { SandboxedJob } from "moleculer-bull"; import BullMQMixin, { SandboxedJob } from "moleculer-bull";
import { DbMixin } from "../mixins/db.mixin"; import { DbMixin } from "../mixins/db.mixin";
@@ -92,10 +93,14 @@ export default class QueueService extends Service {
"Issue metadata inferred: ", "Issue metadata inferred: ",
JSON.stringify(inferredIssueDetails, null, 2) JSON.stringify(inferredIssueDetails, null, 2)
); );
// Add the bundleId, if present to the payload
// write to mongo let bundleId = null;
console.log("Writing to mongo..."); if (!isNil(job.data.bundleId)) {
await this.broker.call("library.rawImportToDB", { bundleId = job.data.bundleId;
}
// Orchestrate the payload
const payload = {
importStatus: { importStatus: {
isImported: true, isImported: true,
tagged: false, tagged: false,
@@ -115,30 +120,41 @@ export default class QueueService extends Service {
issue: inferredIssueDetails, issue: inferredIssueDetails,
}, },
sourcedMetadata: { sourcedMetadata: {
// except for ComicInfo.xml, everything else should be copied over from the
// parent comic
comicInfo: comicInfoJSON, comicInfo: comicInfoJSON,
comicvine: {},
}, },
// since we already have at least 1 copy // since we already have at least 1 copy
// mark it as not wanted by default // mark it as not wanted by default
acquisition: { "acquisition.source.wanted": false,
source: {
wanted: false, // clear out the downloads array
}, // "acquisition.directconnect.downloads": [],
directconnect: {
downloads: [], // mark the metadata source
}, "acquisition.source.name": job.data.sourcedFrom,
}, };
});
// Add the sourcedMetadata, if present
if (!isNil(job.data.sourcedMetadata) && !isUndefined(job.data.sourcedMetadata.comicvine)) {
Object.assign(
payload.sourcedMetadata,
job.data.sourcedMetadata
);
}
// write to mongo
const importResult = await this.broker.call(
"library.rawImportToDB",
{
importType: job.data.importType,
bundleId,
payload,
}
);
return { return {
data: { data: {
result, importResult,
inferredMetadata: {
issue: inferredIssueDetails,
},
sourcedMetadata: {
comicInfo: comicInfoJSON,
comicvine: {},
},
}, },
id: job.id, id: job.id,
worker: process.pid, worker: process.pid,
@@ -177,10 +193,18 @@ export default class QueueService extends Service {
async handler( async handler(
ctx: Context<{ ctx: Context<{
fileObject: object; fileObject: object;
importType: string;
bundleId: number;
sourcedFrom?: string;
sourcedMetadata: object;
}> }>
) { ) {
return await this.createJob("process.import", { return await this.createJob("process.import", {
fileObject: ctx.params.fileObject, fileObject: ctx.params.fileObject,
importType: ctx.params.importType,
bundleId: ctx.params.bundleId,
sourcedFrom: ctx.params.sourcedFrom,
sourcedMetadata: ctx.params.sourcedMetadata,
}); });
}, },
}, },

View File

@@ -32,7 +32,7 @@ SOFTWARE.
*/ */
"use strict"; "use strict";
import { isNil, isUndefined } from "lodash"; import { isNil } from "lodash";
import { import {
Context, Context,
Service, Service,
@@ -42,15 +42,8 @@ import {
} from "moleculer"; } from "moleculer";
import { DbMixin } from "../mixins/db.mixin"; import { DbMixin } from "../mixins/db.mixin";
import Comic from "../models/comic.model"; import Comic from "../models/comic.model";
import { import { walkFolder, getSizeOfDirectory } from "../utils/file.utils";
explodePath, import { extractFromArchive } from "../utils/uncompression.utils";
walkFolder,
getSizeOfDirectory,
} from "../utils/file.utils";
import {
extractFromArchive,
uncompressEntireArchive,
} from "../utils/uncompression.utils";
import { convertXMLToJSON } from "../utils/xml.utils"; import { convertXMLToJSON } from "../utils/xml.utils";
import { import {
IExtractComicBookCoverErrorResponse, IExtractComicBookCoverErrorResponse,
@@ -104,6 +97,45 @@ export default class ImportService extends Service {
}); });
}, },
}, },
importDownloadedComic: {
rest: "POST /importDownloadedComic",
params: {},
handler: async (ctx: Context<{ bundle: any }>) => {
console.log(ctx.params);
// Find the comic by bundleId
const referenceComicObject = await Comic.find({
"acquisition.directconnect.downloads.bundleId": `${ctx.params.bundle.data.id}`,
});
// Determine source where the comic was added from
// and gather identifying information about it
const sourceName =
referenceComicObject[0].acquisition.source.name;
const { sourcedMetadata } = referenceComicObject[0];
const filePath = `${COMICS_DIRECTORY}/${ctx.params.bundle.data.name}`;
let comicExists = await Comic.exists({
"rawFileDetails.name": `${path.basename(
ctx.params.bundle.data.name,
path.extname(ctx.params.bundle.data.name)
)}`,
});
if (!comicExists) {
// 2. Send the extraction job to the queue
await broker.call("importqueue.processImport", {
importType: "update",
sourcedFrom: sourceName,
bundleId: ctx.params.bundle.data.id,
sourcedMetadata,
fileObject: {
filePath,
// fileSize: item.stats.size,
},
});
} else {
console.log("Comic already exists in the library.");
}
},
},
newImport: { newImport: {
rest: "POST /newImport", rest: "POST /newImport",
params: {}, params: {},
@@ -113,7 +145,6 @@ export default class ImportService extends Service {
}> }>
) { ) {
// 1. Walk the Source folder // 1. Walk the Source folder
klaw(path.resolve(COMICS_DIRECTORY)) klaw(path.resolve(COMICS_DIRECTORY))
// 1.1 Filter on .cb* extensions // 1.1 Filter on .cb* extensions
.pipe( .pipe(
@@ -126,7 +157,6 @@ export default class ImportService extends Service {
) { ) {
this.push(item); this.push(item);
} }
next(); next();
}) })
) )
@@ -151,6 +181,7 @@ export default class ImportService extends Service {
filePath: item.path, filePath: item.path,
fileSize: item.stats.size, fileSize: item.stats.size,
}, },
importType: "new",
} }
); );
} else { } else {
@@ -170,35 +201,39 @@ export default class ImportService extends Service {
params: {}, params: {},
async handler( async handler(
ctx: Context<{ ctx: Context<{
_id: string; bundleId?: string;
sourcedMetadata: { importType: string;
comicvine?: { payload: {
volume: { api_detail_url: string }; _id?: string;
volumeInformation: {}; sourcedMetadata: {
comicvine?: {
volume: { api_detail_url: string };
volumeInformation: {};
};
locg?: {};
}; };
locg?: {}; inferredMetadata: {
}; issue: Object;
inferredMetadata: {
issue: Object;
};
rawFileDetails: {
name: string;
};
acquisition: {
source: {
wanted: boolean;
name?: string;
}; };
directconnect: { rawFileDetails: {
downloads: []; name: string;
};
acquisition: {
source: {
wanted: boolean;
name?: string;
};
directconnect: {
downloads: [];
};
}; };
}; };
}> }>
) { ) {
try { try {
let volumeDetails; let volumeDetails;
const comicMetadata = ctx.params; const comicMetadata = ctx.params.payload;
console.log(JSON.stringify(comicMetadata, null, 4));
// When an issue is added from the search CV feature // When an issue is added from the search CV feature
// we solicit volume information and add that to mongo // we solicit volume information and add that to mongo
if ( if (
@@ -220,23 +255,30 @@ export default class ImportService extends Service {
comicMetadata.sourcedMetadata.comicvine.volumeInformation = comicMetadata.sourcedMetadata.comicvine.volumeInformation =
volumeDetails.results; volumeDetails.results;
} }
Comic.findOneAndUpdate(
{ _id: new ObjectId(ctx.params._id) }, console.log("Saving to Mongo...");
ctx.params, console.log(
{ upsert: true, new: true }, `Import type: [${ctx.params.importType}]`
(error, data) => {
if (data) {
return data;
} else if (error) {
console.log("data", data);
console.log("error", error);
throw new Errors.MoleculerError(
"Failed to import comic book",
500
);
}
}
); );
console.log(JSON.stringify(comicMetadata, null, 4));
switch (ctx.params.importType) {
case "new":
return await Comic.create(comicMetadata);
case "update":
return await Comic.findOneAndUpdate(
{
"acquisition.directconnect.downloads.bundleId":
ctx.params.bundleId,
},
comicMetadata,
{
upsert: true,
new: true,
}
);
default:
return false;
}
} catch (error) { } catch (error) {
throw new Errors.MoleculerError( throw new Errors.MoleculerError(
"Import failed.", "Import failed.",
@@ -341,39 +383,6 @@ export default class ImportService extends Service {
}); });
}, },
}, },
importDownloadedFileToLibrary: {
rest: "POST /importDownloadedFileToLibrary",
params: {},
handler: async (
ctx: Context<{
comicObjectId: string;
comicObject: {
acquisition: {
source: {
wanted: boolean;
};
};
};
downloadStatus: { name: string };
}>
) => {
const result = await extractFromArchive(
`${COMICS_DIRECTORY}/${ctx.params.downloadStatus.name}`
);
Object.assign(ctx.params.comicObject, {
rawFileDetails: result,
});
ctx.params.comicObject.acquisition.source.wanted =
false;
const updateResult = await Comic.findOneAndUpdate(
{ _id: new ObjectId(ctx.params.comicObjectId) },
ctx.params.comicObject,
{ upsert: true, new: true }
);
await updateResult.index();
},
},
getComicBooks: { getComicBooks: {
rest: "POST /getComicBooks", rest: "POST /getComicBooks",
params: {}, params: {},

View File

@@ -51,8 +51,8 @@ export default class SocketService extends Service {
); );
console.log(data); console.log(data);
await this.broker.call( await this.broker.call(
"library.importDownloadedFileToLibrary", "library.importDownloadedComic",
data.data, { bundle: data },
{} {}
); );
break; break;

View File

@@ -73,10 +73,6 @@ export const extractComicInfoXMLFromRar = async (
filePath: string filePath: string
): Promise<any> => { ): Promise<any> => {
try { try {
const result = {
filePath,
};
// Create the target directory // Create the target directory
const directoryOptions = { const directoryOptions = {
mode: 0o2775, mode: 0o2775,
@@ -92,7 +88,7 @@ export const extractComicInfoXMLFromRar = async (
path: path.resolve(filePath), path: path.resolve(filePath),
bin: `${UNRAR_BIN_PATH}`, // this will change depending on Docker base OS bin: `${UNRAR_BIN_PATH}`, // this will change depending on Docker base OS
}); });
const filesInArchive: [RarFile] = await new Promise( const filesInArchive: [RarFile] = await new Promise(
(resolve, reject) => { (resolve, reject) => {
return archive.list((err, entries) => { return archive.list((err, entries) => {
@@ -128,7 +124,6 @@ export const extractComicInfoXMLFromRar = async (
const comicInfoXMLFilePromise = new Promise((resolve, reject) => { const comicInfoXMLFilePromise = new Promise((resolve, reject) => {
let comicinfostring = ""; let comicinfostring = "";
if (!isUndefined(comicInfoXML[0])) { if (!isUndefined(comicInfoXML[0])) {
console.log(path.basename(comicInfoXML[0].name));
const comicInfoXMLFileName = path.basename( const comicInfoXMLFileName = path.basename(
comicInfoXML[0].name comicInfoXML[0].name
); );
@@ -138,6 +133,7 @@ export const extractComicInfoXMLFromRar = async (
archive.stream(comicInfoXML[0]["name"]).pipe(writeStream); archive.stream(comicInfoXML[0]["name"]).pipe(writeStream);
writeStream.on("finish", async () => { writeStream.on("finish", async () => {
console.log(`Attempting to write comicInfo.xml...`);
const readStream = createReadStream( const readStream = createReadStream(
`${targetDirectory}/${comicInfoXMLFileName}` `${targetDirectory}/${comicInfoXMLFileName}`
); );
@@ -154,7 +150,7 @@ export const extractComicInfoXMLFromRar = async (
const comicInfoJSON = await convertXMLToJSON( const comicInfoJSON = await convertXMLToJSON(
comicinfostring.toString() comicinfostring.toString()
); );
console.log(`comicInfo.xml successfully written: ${comicInfoJSON.comicinfo}`)
resolve({ comicInfoJSON: comicInfoJSON.comicinfo }); resolve({ comicInfoJSON: comicInfoJSON.comicinfo });
} }
}); });
@@ -357,6 +353,8 @@ export const extractFromArchive = async (filePath: string) => {
case ".cbr": case ".cbr":
const cbrResult = await extractComicInfoXMLFromRar(filePath); const cbrResult = await extractComicInfoXMLFromRar(filePath);
console.log("ASDASDASDASDas");
console.log(JSON.stringify(cbrResult, null, 4))
return Object.assign({}, ...cbrResult); return Object.assign({}, ...cbrResult);
default: default: