Cleaning up code

This commit is contained in:
Rishi Ghan
2026-04-14 10:26:07 -04:00
parent c604bd8e4d
commit 4d50f22df4
10 changed files with 7469 additions and 22745 deletions

View File

@@ -4,7 +4,10 @@ module.exports = {
es6: true, es6: true,
node: true node: true
}, },
ignorePatterns: [ "test/*"], extends: [
"eslint:recommended"
],
ignorePatterns: ["test/*", ".eslintrc.js"],
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
project: "tsconfig.json", project: "tsconfig.json",
@@ -14,8 +17,6 @@ module.exports = {
rules: { rules: {
"@typescript-eslint/adjacent-overload-signatures": "error", "@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "error", "@typescript-eslint/array-type": "error",
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/class-name-casing": "off",
"@typescript-eslint/consistent-type-assertions": "error", "@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-definitions": "error", "@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/explicit-member-accessibility": [ "@typescript-eslint/explicit-member-accessibility": [
@@ -24,19 +25,6 @@ module.exports = {
accessibility: "explicit" accessibility: "explicit"
} }
], ],
"@typescript-eslint/indent": [
"off",
4,
{
FunctionDeclaration: {
parameters: "first"
},
FunctionExpression: {
parameters: "first"
}
}
],
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/member-delimiter-style": [ "@typescript-eslint/member-delimiter-style": [
"error", "error",
{ {
@@ -56,9 +44,8 @@ module.exports = {
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-misused-new": "error", "@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error", "@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "error", "@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/prefer-for-of": "error", "@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error", "@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error", "@typescript-eslint/prefer-namespace-keyword": "error",
@@ -85,7 +72,7 @@ module.exports = {
"eol-last": "error", "eol-last": "error",
eqeqeq: ["error", "smart"], eqeqeq: ["error", "smart"],
"guard-for-in": "error", "guard-for-in": "error",
"id-blacklist": ["error", "any", "Number", "number", "String", "string", "Boolean", "boolean", "Undefined", "undefined"], "id-denylist": ["error", "any", "Number", "number", "String", "string", "Boolean", "boolean", "Undefined", "undefined"],
"id-match": "error", "id-match": "error",
"import/order": "error", "import/order": "error",
"max-classes-per-file": ["error", 1], "max-classes-per-file": ["error", 1],

View File

@@ -10,7 +10,7 @@ export const resolvers = {
*/ */
searchComicVine: async (_: any, { input }: any, context: any) => { searchComicVine: async (_: any, { input }: any, context: any) => {
const { broker } = context; const { broker } = context;
if (!broker) { if (!broker) {
throw new Error("Broker not available in context"); throw new Error("Broker not available in context");
} }
@@ -20,7 +20,8 @@ export const resolvers = {
resources: input.resources, resources: input.resources,
format: input.format || "json", format: input.format || "json",
sort: input.sort, sort: input.sort,
field_list: input.field_list, // eslint-disable-next-line camelcase
field_list: input.fieldList,
limit: input.limit?.toString(), limit: input.limit?.toString(),
offset: input.offset?.toString(), offset: input.offset?.toString(),
}); });
@@ -31,7 +32,7 @@ export const resolvers = {
*/ */
volumeBasedSearch: async (_: any, { input }: any, context: any) => { volumeBasedSearch: async (_: any, { input }: any, context: any) => {
const { broker } = context; const { broker } = context;
if (!broker) { if (!broker) {
throw new Error("Broker not available in context"); throw new Error("Broker not available in context");
} }
@@ -59,7 +60,7 @@ export const resolvers = {
*/ */
getVolume: async (_: any, { input }: any, context: any) => { getVolume: async (_: any, { input }: any, context: any) => {
const { broker } = context; const { broker } = context;
if (!broker) { if (!broker) {
throw new Error("Broker not available in context"); throw new Error("Broker not available in context");
} }
@@ -75,7 +76,7 @@ export const resolvers = {
*/ */
getIssuesForSeries: async (_: any, { comicObjectId }: any, context: any) => { getIssuesForSeries: async (_: any, { comicObjectId }: any, context: any) => {
const { broker } = context; const { broker } = context;
if (!broker) { if (!broker) {
throw new Error("Broker not available in context"); throw new Error("Broker not available in context");
} }
@@ -90,7 +91,7 @@ export const resolvers = {
*/ */
getComicVineResource: async (_: any, { input }: any, context: any) => { getComicVineResource: async (_: any, { input }: any, context: any) => {
const { broker } = context; const { broker } = context;
if (!broker) { if (!broker) {
throw new Error("Broker not available in context"); throw new Error("Broker not available in context");
} }
@@ -107,7 +108,7 @@ export const resolvers = {
*/ */
getStoryArcs: async (_: any, { volumeId }: any, context: any) => { getStoryArcs: async (_: any, { volumeId }: any, context: any) => {
const { broker } = context; const { broker } = context;
if (!broker) { if (!broker) {
throw new Error("Broker not available in context"); throw new Error("Broker not available in context");
} }
@@ -122,7 +123,7 @@ export const resolvers = {
*/ */
getWeeklyPullList: async (_: any, { input }: any, context: any) => { getWeeklyPullList: async (_: any, { input }: any, context: any) => {
const { broker } = context; const { broker } = context;
if (!broker) { if (!broker) {
throw new Error("Broker not available in context"); throw new Error("Broker not available in context");
} }
@@ -156,7 +157,7 @@ export const resolvers = {
*/ */
fetchMetronResource: async (_: any, { input }: any, context: any) => { fetchMetronResource: async (_: any, { input }: any, context: any) => {
const { broker } = context; const { broker } = context;
if (!broker) { if (!broker) {
throw new Error("Broker not available in context"); throw new Error("Broker not available in context");
} }
@@ -183,14 +184,8 @@ export const resolvers = {
// Custom scalar resolver for JSON // Custom scalar resolver for JSON
JSON: { JSON: {
__parseValue(value: any): any { __parseValue: (value: any): any => value,
return value; __serialize: (value: any): any => value,
}, __parseLiteral: (ast: any): any => ast.value,
__serialize(value: any): any {
return value;
},
__parseLiteral(ast: any): any {
return ast.value;
},
}, },
}; };

View File

@@ -3,7 +3,6 @@ import {
BrokerOptions, BrokerOptions,
Errors, Errors,
MetricRegistry, MetricRegistry,
ServiceBroker,
} from "moleculer"; } from "moleculer";
/** /**
@@ -91,7 +90,7 @@ const brokerConfig: BrokerOptions = {
// Backoff factor for delay. 2 means exponential backoff. // Backoff factor for delay. 2 means exponential backoff.
factor: 2, factor: 2,
// A function to check failed requests. // A function to check failed requests.
check: (err: Errors.MoleculerError) => err && !!err.retryable, check: (err: Errors.MoleculerError | Error) => err && !!(err as Errors.MoleculerError).retryable,
}, },
// Limit of calling level. If it reaches the limit, broker will throw an MaxCallLevelError error. (Infinite loop protection) // Limit of calling level. If it reaches the limit, broker will throw an MaxCallLevelError error. (Infinite loop protection)
@@ -138,7 +137,7 @@ const brokerConfig: BrokerOptions = {
// Number of milliseconds to switch from open to half-open state // Number of milliseconds to switch from open to half-open state
halfOpenTime: 10 * 1000, halfOpenTime: 10 * 1000,
// A function to check failed requests. // A function to check failed requests.
check: (err: Errors.MoleculerError) => err && err.code >= 500, check: (err: Errors.MoleculerError | Error) => err && (err as Errors.MoleculerError).code >= 500,
}, },
// Settings of bulkhead feature. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Bulkhead // Settings of bulkhead feature. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Bulkhead
@@ -154,7 +153,7 @@ const brokerConfig: BrokerOptions = {
// Enable action & event parameter validation. More info: https://moleculer.services/docs/0.14/validating.html // Enable action & event parameter validation. More info: https://moleculer.services/docs/0.14/validating.html
validator: true, validator: true,
errorHandler: null, errorHandler: undefined,
// Enable/disable built-in metrics function. More info: https://moleculer.services/docs/0.14/metrics.html // Enable/disable built-in metrics function. More info: https://moleculer.services/docs/0.14/metrics.html
metrics: { metrics: {

29913
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,23 +21,22 @@
"author": "", "author": "",
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^9.7.0", "@faker-js/faker": "^9.7.0",
"@types/jsdom": "^16.2.14", "@types/jsdom": "^21.1.6",
"@types/lodash": "^4.14.171", "@types/lodash": "^4.14.171",
"@types/string-similarity": "^4.0.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/eslint-plugin": "^2.26.0", "@typescript-eslint/parser": "^7.18.0",
"@typescript-eslint/parser": "^2.26.0", "eslint": "^8.57.0",
"eslint": "^6.8.0", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-import": "^2.20.2", "eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-prefer-arrow": "^1.2.2", "jest": "^29.7.0",
"jest": "^25.1.0", "jest-cli": "^29.7.0",
"jest-cli": "^25.1.0", "moleculer-repl": "^0.7.4",
"moleculer-repl": "^0.6.2",
"puppeteer": "^24.7.1", "puppeteer": "^24.7.1",
"puppeteer-extra": "^3.3.6", "puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2", "puppeteer-extra-plugin-stealth": "^2.11.2",
"telnet-client": "^2.2.5", "telnet-client": "^2.2.5",
"threetwo-ui-typings": "^1.0.14", "threetwo-ui-typings": "^1.0.14",
"ts-jest": "^25.3.0", "ts-jest": "^29.1.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
@@ -47,11 +46,10 @@
"@graphql-tools/stitch": "^10.1.12", "@graphql-tools/stitch": "^10.1.12",
"@graphql-tools/utils": "^11.0.0", "@graphql-tools/utils": "^11.0.0",
"@graphql-tools/wrap": "^11.1.8", "@graphql-tools/wrap": "^11.1.8",
"@types/axios": "^0.14.0", "@types/jest": "^29.5.12",
"@types/jest": "^25.1.4",
"@types/mkdirp": "^1.0.0", "@types/mkdirp": "^1.0.0",
"@types/node": "^13.9.8", "@types/node": "^13.9.8",
"axios": "^0.21.1", "axios": "^1.7.7",
"comicgeeks": "^1.1.0", "comicgeeks": "^1.1.0",
"date-fns": "^2.27.0", "date-fns": "^2.27.0",
"delay": "^5.0.0", "delay": "^5.0.0",
@@ -61,7 +59,7 @@
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"imghash": "^0.0.9", "imghash": "^0.0.9",
"ioredis": "^4.28.1", "ioredis": "^4.28.1",
"jsdom": "^19.0.0", "jsdom": "^24.1.0",
"leven": "^3.1.0", "leven": "^3.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moleculer": "^0.14.28", "moleculer": "^0.14.28",
@@ -69,8 +67,7 @@
"moleculer-web": "^0.10.5", "moleculer-web": "^0.10.5",
"nats": "^1.3.2", "nats": "^1.3.2",
"paginate-info": "^1.0.4", "paginate-info": "^1.0.4",
"query-string": "^7.0.1", "query-string": "^7.0.1"
"string-similarity": "^4.0.4"
}, },
"engines": { "engines": {
"node": ">= 10.x.x" "node": ">= 10.x.x"
@@ -84,15 +81,15 @@
"js" "js"
], ],
"transform": { "transform": {
"^.+\\.(ts|tsx)$": "ts-jest" "^.+\\.(ts|tsx)$": [
"ts-jest",
{
"tsconfig": "tsconfig.json"
}
]
}, },
"testMatch": [ "testMatch": [
"**/*.spec.(ts|js)" "**/*.spec.(ts|js)"
], ]
"globals": {
"ts-jest": {
"tsConfig": "tsconfig.json"
}
}
} }
} }

View File

@@ -1,5 +1,4 @@
import { IncomingMessage } from "http"; import { Service, ServiceBroker } from "moleculer";
import { Service, ServiceBroker, Context } from "moleculer";
import ApiGateway from "moleculer-web"; import ApiGateway from "moleculer-web";
export default class ApiService extends Service { export default class ApiService extends Service {
@@ -72,7 +71,7 @@ export default class ApiService extends Service {
"POST /": async (req: any, res: any) => { "POST /": async (req: any, res: any) => {
try { try {
const { query, variables, operationName } = req.body; const { query, variables, operationName } = req.body;
const result = await req.$ctx.broker.call("gateway.query", { const result = await req.$ctx.broker.call("gateway.query", {
query, query,
variables, variables,
@@ -151,7 +150,7 @@ export default class ApiService extends Service {
"POST /": async (req: any, res: any) => { "POST /": async (req: any, res: any) => {
try { try {
const { query, variables, operationName } = req.body; const { query, variables, operationName } = req.body;
const result = await req.$ctx.broker.call("gateway.queryLocal", { const result = await req.$ctx.broker.call("gateway.queryLocal", {
query, query,
variables, variables,

View File

@@ -2,10 +2,9 @@
import { Service, ServiceBroker, Context } from "moleculer"; import { Service, ServiceBroker, Context } from "moleculer";
import axios from "axios"; import axios from "axios";
import { isNil, isUndefined } from "lodash"; import { isNil } from "lodash";
import { fetchReleases, FilterTypes, SortTypes } from "comicgeeks";
import { matchScorer, rankVolumes } from "../utils/searchmatchscorer.utils"; import { matchScorer, rankVolumes } from "../utils/searchmatchscorer.utils";
import { scrapeIssuePage, getWeeklyPullList } from "../utils/scraping.utils"; import { getWeeklyPullList } from "../utils/scraping.utils";
const { calculateLimitAndOffset, paginate } = require("paginate-info"); const { calculateLimitAndOffset, paginate } = require("paginate-info");
const { MoleculerError } = require("moleculer").Errors; const { MoleculerError } = require("moleculer").Errors;
@@ -25,7 +24,7 @@ export default class ComicVineService extends Service {
format: string; format: string;
sort: string; sort: string;
query: string; query: string;
field_list: string; fieldList: string;
limit: string; limit: string;
offset: string; offset: string;
resources: string; resources: string;
@@ -61,10 +60,11 @@ export default class ComicVineService extends Service {
process.env.COMICVINE_API_KEY, process.env.COMICVINE_API_KEY,
params: { params: {
format: "json", format: "json",
// eslint-disable-next-line camelcase
field_list: fieldList, field_list: fieldList,
}, },
headers: { headers: {
Accept: "application/json", "Accept": "application/json",
"User-Agent": "ThreeTwo", "User-Agent": "ThreeTwo",
}, },
}); });
@@ -97,7 +97,7 @@ export default class ComicVineService extends Service {
filter: `volume:${comicBookDetails.sourcedMetadata.comicvine.volumeInformation.id}`, filter: `volume:${comicBookDetails.sourcedMetadata.comicvine.volumeInformation.id}`,
}, },
headers: { headers: {
Accept: "application/json", "Accept": "application/json",
"User-Agent": "ThreeTwo", "User-Agent": "ThreeTwo",
}, },
}); });
@@ -173,6 +173,7 @@ export default class ComicVineService extends Service {
limit: "100", limit: "100",
format: "json", format: "json",
filter: `${filter}`, filter: `${filter}`,
// eslint-disable-next-line camelcase
field_list: `${fieldList}`, field_list: `${fieldList}`,
}, },
headers: { headers: {
@@ -201,7 +202,7 @@ export default class ComicVineService extends Service {
searchParams: { searchParams: {
name: string; name: string;
subtitle?: string; subtitle?: string;
number: string; issueNumber: string;
year: string; year: string;
}; };
}; };
@@ -209,35 +210,38 @@ export default class ComicVineService extends Service {
}> }>
) => { ) => {
try { try {
console.log(
"Searching against: ",
ctx.params.scorerConfiguration.searchParams
);
const { rawFileDetails, scorerConfiguration } = const { rawFileDetails, scorerConfiguration } =
ctx.params; ctx.params;
if (!scorerConfiguration) {
throw new Error("scorerConfiguration is required");
}
console.log(
"Searching against: ",
scorerConfiguration.searchParams
);
const results: any = []; const results: any = [];
console.log( console.log(
"passed to fetchVolumesFromCV", "passed to fetchVolumesFromCV",
ctx.params ctx.params
); );
// Send initial status to client // Send initial status to client
await this.broker.call("socket.broadcast", { await this.broker.call("socket.broadcast", {
namespace: "/", namespace: "/",
event: "CV_SCRAPING_STATUS", event: "CV_SCRAPING_STATUS",
args: [ args: [
{ {
message: `Starting volume search for: ${ctx.params.scorerConfiguration.searchParams.name}`, message: `Starting volume search for: ${scorerConfiguration.searchParams.name}`,
stage: "fetching_volumes" stage: "fetching_volumes",
}, },
], ],
}); });
const volumes = await this.fetchVolumesFromCV( const volumes = await this.fetchVolumesFromCV(
ctx.params, ctx.params,
results results
); );
// Notify client that volume fetching is complete // Notify client that volume fetching is complete
await this.broker.call("socket.broadcast", { await this.broker.call("socket.broadcast", {
namespace: "/", namespace: "/",
@@ -245,20 +249,20 @@ export default class ComicVineService extends Service {
args: [ args: [
{ {
message: `Fetched ${volumes.length} volumes, now ranking matches...`, message: `Fetched ${volumes.length} volumes, now ranking matches...`,
stage: "ranking_volumes" stage: "ranking_volumes",
}, },
], ],
}); });
// 1. Run the current batch of volumes through the matcher // 1. Run the current batch of volumes through the matcher
const potentialVolumeMatches = rankVolumes( const potentialVolumeMatches = rankVolumes(
volumes, volumes,
ctx.params.scorerConfiguration ctx.params.scorerConfiguration
); );
// Sort by totalScore in descending order to prioritize best matches // Sort by totalScore in descending order to prioritize best matches
potentialVolumeMatches.sort((a: any, b: any) => b.totalScore - a.totalScore); potentialVolumeMatches.sort((a: any, b: any) => b.totalScore - a.totalScore);
// Notify client about ranked matches // Notify client about ranked matches
await this.broker.call("socket.broadcast", { await this.broker.call("socket.broadcast", {
namespace: "/", namespace: "/",
@@ -266,11 +270,11 @@ export default class ComicVineService extends Service {
args: [ args: [
{ {
message: `Found ${potentialVolumeMatches.length} potential volume matches, searching for issues...`, message: `Found ${potentialVolumeMatches.length} potential volume matches, searching for issues...`,
stage: "searching_issues" stage: "searching_issues",
}, },
], ],
}); });
// 2. Construct the filter string // 2. Construct the filter string
// 2a. volume: 1111|2222|3333 // 2a. volume: 1111|2222|3333
let volumeIdString = "volume:"; let volumeIdString = "volume:";
@@ -291,20 +295,18 @@ export default class ComicVineService extends Service {
let coverDateFilter = ""; let coverDateFilter = "";
if ( if (
!isNil( !isNil(
ctx.params.scorerConfiguration.searchParams scorerConfiguration.searchParams.year
.year
) )
) { ) {
const issueYear = parseInt( const issueYear = parseInt(
ctx.params.scorerConfiguration.searchParams scorerConfiguration.searchParams.year,
.year,
10 10
); );
coverDateFilter = `cover_date:${ coverDateFilter = `cover_date:${
issueYear - 1 issueYear - 1
}-01-01|${issueYear + 1}-12-31`; }-01-01|${issueYear + 1}-12-31`;
} }
const filterString = `issue_number:${ctx.params.scorerConfiguration.searchParams.number},${volumeIdString},${coverDateFilter}`; const filterString = `issue_number:${scorerConfiguration.searchParams.issueNumber},${volumeIdString},${coverDateFilter}`;
console.log(filterString); console.log(filterString);
const issueMatches = await axios({ const issueMatches = await axios({
@@ -327,7 +329,7 @@ export default class ComicVineService extends Service {
console.log( console.log(
`Total issues matching the criteria: ${issueMatches.data.results.length}` `Total issues matching the criteria: ${issueMatches.data.results.length}`
); );
// Handle case when no issues are found // Handle case when no issues are found
if (issueMatches.data.results.length === 0) { if (issueMatches.data.results.length === 0) {
await this.broker.call("socket.broadcast", { await this.broker.call("socket.broadcast", {
@@ -335,19 +337,19 @@ export default class ComicVineService extends Service {
event: "CV_SCRAPING_STATUS", event: "CV_SCRAPING_STATUS",
args: [ args: [
{ {
message: `No matching issues found. Try adjusting your search criteria.`, message: "No matching issues found. Try adjusting your search criteria.",
stage: "complete" stage: "complete",
}, },
], ],
}); });
return { return {
finalMatches: [], finalMatches: [],
rawFileDetails, rawFileDetails,
scorerConfiguration, scorerConfiguration,
}; };
} }
// Notify client about issue matches found // Notify client about issue matches found
await this.broker.call("socket.broadcast", { await this.broker.call("socket.broadcast", {
namespace: "/", namespace: "/",
@@ -355,11 +357,11 @@ export default class ComicVineService extends Service {
args: [ args: [
{ {
message: `Found ${issueMatches.data.results.length} issue matches, fetching volume details...`, message: `Found ${issueMatches.data.results.length} issue matches, fetching volume details...`,
stage: "fetching_volume_details" stage: "fetching_volume_details",
}, },
], ],
}); });
// 3. get volume information for the issue matches // 3. get volume information for the issue matches
if (issueMatches.data.results.length === 1) { if (issueMatches.data.results.length === 1) {
const volumeInformation = const volumeInformation =
@@ -373,19 +375,19 @@ export default class ComicVineService extends Service {
); );
issueMatches.data.results[0].volumeInformation = issueMatches.data.results[0].volumeInformation =
volumeInformation; volumeInformation;
// Notify scoring for single match // Notify scoring for single match
await this.broker.call("socket.broadcast", { await this.broker.call("socket.broadcast", {
namespace: "/", namespace: "/",
event: "CV_SCRAPING_STATUS", event: "CV_SCRAPING_STATUS",
args: [ args: [
{ {
message: `Scoring 1 match...`, message: "Scoring 1 match...",
stage: "scoring_matches" stage: "scoring_matches",
}, },
], ],
}); });
// Score the single match // Score the single match
const scoredMatch = await this.broker.call( const scoredMatch = await this.broker.call(
"comicvine.getComicVineMatchScores", "comicvine.getComicVineMatchScores",
@@ -395,19 +397,19 @@ export default class ComicVineService extends Service {
scorerConfiguration, scorerConfiguration,
} }
); );
// Notify completion // Notify completion
await this.broker.call("socket.broadcast", { await this.broker.call("socket.broadcast", {
namespace: "/", namespace: "/",
event: "CV_SCRAPING_STATUS", event: "CV_SCRAPING_STATUS",
args: [ args: [
{ {
message: `Search complete! Found 1 match.`, message: "Search complete! Found 1 match.",
stage: "complete" stage: "complete",
}, },
], ],
}); });
return scoredMatch; return scoredMatch;
} }
const finalMatchesPromises = issueMatches.data.results.map( const finalMatchesPromises = issueMatches.data.results.map(
@@ -424,10 +426,10 @@ export default class ComicVineService extends Service {
return issue; return issue;
} }
); );
// Wait for all volume details to be fetched // Wait for all volume details to be fetched
const finalMatches = await Promise.all(finalMatchesPromises); const finalMatches = await Promise.all(finalMatchesPromises);
// Notify client about scoring // Notify client about scoring
await this.broker.call("socket.broadcast", { await this.broker.call("socket.broadcast", {
namespace: "/", namespace: "/",
@@ -435,11 +437,11 @@ export default class ComicVineService extends Service {
args: [ args: [
{ {
message: `Scoring ${finalMatches.length} matches...`, message: `Scoring ${finalMatches.length} matches...`,
stage: "scoring_matches" stage: "scoring_matches",
}, },
], ],
}); });
// Score the final matches // Score the final matches
const scoredMatches = await this.broker.call( const scoredMatches = await this.broker.call(
"comicvine.getComicVineMatchScores", "comicvine.getComicVineMatchScores",
@@ -449,41 +451,42 @@ export default class ComicVineService extends Service {
scorerConfiguration, scorerConfiguration,
} }
); );
// Notify completion // Notify completion
await this.broker.call("socket.broadcast", { await this.broker.call("socket.broadcast", {
namespace: "/", namespace: "/",
event: "CV_SCRAPING_STATUS", event: "CV_SCRAPING_STATUS",
args: [ args: [
{ {
message: `Search complete! Returning scored matches.`, message: "Search complete! Returning scored matches.",
stage: "complete" stage: "complete",
}, },
], ],
}); });
return scoredMatches; return scoredMatches;
} catch (error) { } catch (err: unknown) {
const error = err as any;
console.error("Error in volumeBasedSearch:", error); console.error("Error in volumeBasedSearch:", error);
// Surface error to UI // Surface error to UI
await this.broker.call("socket.broadcast", { await this.broker.call("socket.broadcast", {
namespace: "/", namespace: "/",
event: "CV_SCRAPING_STATUS", event: "CV_SCRAPING_STATUS",
args: [ args: [
{ {
message: `Error during search: ${error.message || 'Unknown error'}`, message: `Error during search: ${error.message || "Unknown error"}`,
stage: "error", stage: "error",
error: { error: {
message: error.message, message: error.message,
code: error.code, code: error.code,
type: error.type, type: error.type,
retryable: error.retryable retryable: error.retryable,
} },
}, },
], ],
}); });
// Re-throw or return error response // Re-throw or return error response
throw error; throw error;
} }
@@ -541,7 +544,8 @@ export default class ComicVineService extends Service {
const issuePromises = const issuePromises =
volumeData.results.issues.map( volumeData.results.issues.map(
async (issue: any) => { async (issue: any) => {
const issueUrl = `${CV_BASE_URL}issue/4000-${issue.id}/?api_key=${process.env.COMICVINE_API_KEY}&format=json&field_list=story_arc_credits,description,image`; const issueUrl = `${CV_BASE_URL}issue/4000-${issue.id}/?api_key=${process.env.COMICVINE_API_KEY}` +
"&format=json&field_list=story_arc_credits,description,image";
try { try {
const issueResponse = const issueResponse =
await axios.get(issueUrl, { await axios.get(issueUrl, {
@@ -570,7 +574,8 @@ export default class ComicVineService extends Service {
}) })
) || [] ) || []
); );
} catch (error) { } catch (err: unknown) {
const error = err as any;
console.error( console.error(
"An error occurred while fetching issue data:", "An error occurred while fetching issue data:",
error.message error.message
@@ -634,11 +639,12 @@ export default class ComicVineService extends Service {
try { try {
const response = await axios.get(issuesUrl, { const response = await axios.get(issuesUrl, {
params: { params: {
// eslint-disable-next-line camelcase
api_key: process.env.COMICVINE_API_KEY, api_key: process.env.COMICVINE_API_KEY,
filter: `volume:${volumeId}`, filter: `volume:${volumeId}`,
format: "json", format: "json",
field_list: // eslint-disable-next-line camelcase
"id,name,image,issue_number,cover_date,description", field_list: "id,name,image,issue_number,cover_date,description",
limit: 100, limit: 100,
}, },
headers: { headers: {
@@ -651,10 +657,8 @@ export default class ComicVineService extends Service {
const issuesWithDescriptionImageAndYear = const issuesWithDescriptionImageAndYear =
response.data.results.map((issue: any) => { response.data.results.map((issue: any) => {
const year = issue.cover_date const year = issue.cover_date
? new Date( ? new Date(issue.cover_date).getFullYear()
issue.cover_date : null;
).getFullYear()
: null; // Extract the year from cover_date
return { return {
...issue, ...issue,
year, year,
@@ -664,7 +668,8 @@ export default class ComicVineService extends Service {
}); });
return issuesWithDescriptionImageAndYear; return issuesWithDescriptionImageAndYear;
} catch (error) { } catch (err: unknown) {
const error = err as any;
this.logger.error( this.logger.error(
"Error fetching issues from ComicVine:", "Error fetching issues from ComicVine:",
error.message error.message

View File

@@ -121,14 +121,12 @@ export default class GatewayService extends Service {
this.logger.info("Local metadata Apollo Server started"); this.logger.info("Local metadata Apollo Server started");
// Create local executor // Create local executor
const localExecutor: AsyncExecutor = async ({ document, variables, context }) => { const localExecutor: AsyncExecutor = async ({ document, variables, context }) => execute({
return execute({
schema: localSchema, schema: localSchema,
document, document,
variableValues: variables, variableValues: variables,
contextValue: { broker: context?.broker || this.broker, ctx: context?.ctx }, contextValue: { broker: context?.broker || this.broker, ctx: context?.ctx },
}) as any; }) as any;
};
// Try to introspect remote schema // Try to introspect remote schema
let remoteSchema = null; let remoteSchema = null;
@@ -155,7 +153,7 @@ export default class GatewayService extends Service {
{ schema: remoteSchema, executor: this.createRemoteExecutor() }, { schema: remoteSchema, executor: this.createRemoteExecutor() },
], ],
mergeTypes: false, mergeTypes: false,
}) })
: localSchema; : localSchema;
this.apolloServer = new ApolloServer({ schema, introspection: true }); this.apolloServer = new ApolloServer({ schema, introspection: true });
@@ -181,11 +179,11 @@ export default class GatewayService extends Service {
/** /**
* Service lifecycle hooks * Service lifecycle hooks
*/ */
started: async function (this: any) { async started() {
await this.initApolloGateway(); await this.initApolloGateway();
}, },
stopped: async function (this: any) { async stopped() {
await this.stopApolloGateway(); await this.stopApolloGateway();
}, },
}); });

View File

@@ -3,8 +3,6 @@
import axios from "axios"; import axios from "axios";
import { Context, Service, ServiceBroker } from "moleculer"; import { Context, Service, ServiceBroker } from "moleculer";
const METRON_BASE_URL = "https://metron.cloud/api/";
export default class MetronService extends Service { export default class MetronService extends Service {
public constructor(public broker: ServiceBroker) { public constructor(public broker: ServiceBroker) {
super(broker); super(broker);
@@ -34,8 +32,8 @@ export default class MetronService extends Service {
}, },
auth: { auth: {
username: "frishi", username: "frishi",
password: "Titu@1588" password: "Titu@1588",
} },
}); });
return results.data; return results.data;
}, },

View File

@@ -34,11 +34,25 @@ SOFTWARE.
import { createWriteStream, existsSync, mkdirSync } from "fs"; import { createWriteStream, existsSync, mkdirSync } from "fs";
import path from "path"; import path from "path";
import https from "https"; import https from "https";
import stringSimilarity from "string-similarity"; import { isNil, isUndefined } from "lodash";
import { isNil, map, isUndefined } from "lodash";
import leven from "leven"; import leven from "leven";
import { isAfter, isSameYear, parseISO } from "date-fns"; import { isAfter, isSameYear, parseISO } from "date-fns";
/**
* Compute string similarity score (0-1) using Levenshtein distance.
* Replaces deprecated string-similarity package.
*/
const compareTwoStrings = (str1: string, str2: string): number => {
if (str1 === str2) {
return 1;
}
const maxLen = Math.max(str1.length, str2.length);
if (maxLen === 0) {
return 1;
}
return 1 - leven(str1, str2) / maxLen;
};
const imghash = require("imghash"); const imghash = require("imghash");
export const matchScorer = async ( export const matchScorer = async (
@@ -49,13 +63,13 @@ export const matchScorer = async (
const scoredMatches: any = []; const scoredMatches: any = [];
try { try {
// searchMatches is already an array of match objects, not promises // SearchMatches is already an array of match objects, not promises
for (const match of searchMatches) { for (const match of searchMatches) {
match.score = 0; match.score = 0;
// Check for the issue name match // Check for the issue name match
if (!isNil(searchQuery.name) && !isNil(match.name)) { if (!isNil(searchQuery.name) && !isNil(match.name)) {
const issueNameScore = stringSimilarity.compareTwoStrings( const issueNameScore = compareTwoStrings(
searchQuery.name, searchQuery.name,
match.name match.name
); );
@@ -92,7 +106,7 @@ export const rankVolumes = (volumes: any, scorerConfiguration: any) => {
// 2. If there is a strong string comparison between the volume name and the issue name ?? // 2. If there is a strong string comparison between the volume name and the issue name ??
const issueNumber = parseInt(scorerConfiguration.searchParams.number, 10); const issueNumber = parseInt(scorerConfiguration.searchParams.number, 10);
const issueYear = parseISO(scorerConfiguration.searchParams.year); const issueYear = parseISO(scorerConfiguration.searchParams.year);
const rankedVolumes = volumes.map((volume: any, idx: number) => { const rankedVolumes = volumes.map((volume: any) => {
let volumeMatchScore = 0; let volumeMatchScore = 0;
const volumeStartYear = !isNil(volume.start_year) const volumeStartYear = !isNil(volume.start_year)
? parseISO(volume.start_year) ? parseISO(volume.start_year)
@@ -103,7 +117,7 @@ export const rankVolumes = (volumes: any, scorerConfiguration: any) => {
const lastIssueNumber = !isNil(volume.last_issue) const lastIssueNumber = !isNil(volume.last_issue)
? parseInt(volume.last_issue.issue_number, 10) ? parseInt(volume.last_issue.issue_number, 10)
: null; : null;
let issueNameMatchScore = stringSimilarity.compareTwoStrings( let issueNameMatchScore = compareTwoStrings(
scorerConfiguration.searchParams.name, scorerConfiguration.searchParams.name,
volume.name volume.name
); );
@@ -111,7 +125,7 @@ export const rankVolumes = (volumes: any, scorerConfiguration: any) => {
// If not, move on. // If not, move on.
let subtitleMatchScore = 0; let subtitleMatchScore = 0;
if (!isNil(scorerConfiguration.searchParams.subtitle)) { if (!isNil(scorerConfiguration.searchParams.subtitle)) {
subtitleMatchScore = stringSimilarity.compareTwoStrings( subtitleMatchScore = compareTwoStrings(
scorerConfiguration.searchParams.subtitle, scorerConfiguration.searchParams.subtitle,
volume.name volume.name
); );
@@ -143,7 +157,7 @@ export const rankVolumes = (volumes: any, scorerConfiguration: any) => {
id: volume.id, id: volume.id,
volumeMatchScore, volumeMatchScore,
issueNameMatchScore, issueNameMatchScore,
totalScore: volumeMatchScore + issueNameMatchScore totalScore: volumeMatchScore + issueNameMatchScore,
}; };
} }
return null; return null;
@@ -152,7 +166,7 @@ export const rankVolumes = (volumes: any, scorerConfiguration: any) => {
}; };
const calculateLevenshteinDistance = async (match: any, rawFileDetails: any) => const calculateLevenshteinDistance = async (match: any, rawFileDetails: any) =>
new Promise((resolve) => { new Promise(resolve => {
https.get(match.image.small_url, (response: any) => { https.get(match.image.small_url, (response: any) => {
console.log(rawFileDetails.cover.filePath); console.log(rawFileDetails.cover.filePath);
const fileName = match.id + "_" + rawFileDetails.name + ".jpg"; const fileName = match.id + "_" + rawFileDetails.name + ".jpg";
@@ -200,7 +214,8 @@ const calculateLevenshteinDistance = async (match: any, rawFileDetails: any) =>
} }
resolve(match); resolve(match);
} catch (err) { } catch (err) {
console.warn(`Image hashing failed for ${fileName}, skipping score adjustment:`, err.message); const errorMessage = err instanceof Error ? err.message : String(err);
console.warn(`Image hashing failed for ${fileName}, skipping score adjustment:`, errorMessage);
resolve(match); resolve(match);
} }
}); });