🛠 Fixes for GraphQL related schema changes

This commit is contained in:
2026-02-17 12:05:28 -05:00
parent 136a7f494f
commit cd446a9ca3
8 changed files with 894 additions and 195 deletions

View File

@@ -1,2 +1,2 @@
export const COMICS_DIRECTORY = "./comics";
export const USERDATA_DIRECTORY = "./userdata";
export const COMICS_DIRECTORY = process.env.COMICS_DIRECTORY || "./comics";
export const USERDATA_DIRECTORY = process.env.USERDATA_DIRECTORY || "./userdata";

View File

@@ -17,23 +17,15 @@ services:
hostname: kafka1
container_name: kafka1
ports:
- "9092:9092"
- "29092:29092"
- "9999:9999"
- "127.0.0.1:9092:9092" # exposed ONLY to host localhost
environment:
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka1:19092,EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092,DOCKER://host.docker.internal:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,DOCKER:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181"
KAFKA_BROKER_ID: 1
KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO"
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_JMX_PORT: 9999
KAFKA_JMX_HOSTNAME: ${DOCKER_HOST_IP:-127.0.0.1}
KAFKA_AUTHORIZER_CLASS_NAME: kafka.security.authorizer.AclAuthorizer
KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "true"
depends_on:
- zoo1
networks:

View File

@@ -129,6 +129,10 @@ const ComicSchema = mongoose.Schema(
wanted: wantedSchema,
acquisition: {
source: {
wanted: { type: Boolean, default: false },
name: { type: String, default: null },
},
release: {},
directconnect: {
downloads: {

536
package-lock.json generated
View File

@@ -1734,6 +1734,16 @@
"node": ">=16"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@es-joy/jsdoccomment": {
"version": "0.39.4",
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.39.4.tgz",
@@ -1916,6 +1926,32 @@
"@img/sharp-libvips-darwin-arm64": "1.0.2"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz",
"integrity": "sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.2"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz",
@@ -1937,6 +1973,382 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz",
"integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"engines": {
"macos": ">=10.13",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz",
"integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz",
"integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz",
"integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz",
"integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz",
"integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz",
"integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz",
"integrity": "sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.2"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz",
"integrity": "sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.2"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz",
"integrity": "sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.2"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz",
"integrity": "sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.2"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz",
"integrity": "sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.2"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz",
"integrity": "sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.2"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.3",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz",
"integrity": "sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.1.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz",
"integrity": "sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz",
"integrity": "sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
@@ -1981,10 +2393,11 @@
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@@ -2435,6 +2848,71 @@
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz",
"integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz",
"integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz",
"integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz",
"integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz",
"integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4297,13 +4775,13 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -6030,13 +6508,15 @@
}
},
"node_modules/es-set-tostringtag": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
"integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3",
"has": "^1.0.3",
"has-tostringtag": "^1.0.0"
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -7253,12 +7733,15 @@
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -9519,9 +10002,10 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -14360,14 +14844,14 @@
}
},
"node_modules/r2-shared-js": {
"version": "1.0.84",
"resolved": "https://registry.npmjs.org/r2-shared-js/-/r2-shared-js-1.0.84.tgz",
"integrity": "sha512-Nm5ywbfndertd0zgu/CjSXcfW8T5Sm0ZnLIL/b9enrFw+9Xy2NAiHjz0Yu3pFLLD+zjvVKLUrFj6MnQpJwLEPw==",
"version": "1.0.85",
"resolved": "https://registry.npmjs.org/r2-shared-js/-/r2-shared-js-1.0.85.tgz",
"integrity": "sha512-5+0gdzG85fD8bmyDg3CBWGQzbfuZXqZC2bC13ML6Tr29kKY/I+6TcfAv5IxE04bxoY3o6rMzFPhq6D2PDZNUWA==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.9.8",
"debug": "^4.4.1",
"debug": "^4.4.3",
"fast-deep-equal": "^3.1.3",
"he": "^1.2.0",
"image-size": "^2.0.2",
@@ -14390,9 +14874,9 @@
}
},
"node_modules/r2-shared-js/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"

View File

@@ -191,7 +191,9 @@ export default class JobQueueService extends Service {
};
} catch (error) {
console.error(
`An error occurred processing Job ID ${ctx.locals.job.id}`
`An error occurred processing Job ID ${ctx.locals.job.id}:`,
error instanceof Error ? error.message : error,
error instanceof Error ? error.stack : ""
);
throw new MoleculerError(
error,

View File

@@ -174,8 +174,13 @@ export default class ImportService extends Service {
try {
// Get params to be passed to the import jobs
const { sessionId } = ctx.params;
const resolvedPath = path.resolve(COMICS_DIRECTORY);
console.log(`Walking comics directory: ${resolvedPath}`);
// 1. Walk the Source folder
klaw(path.resolve(COMICS_DIRECTORY))
klaw(resolvedPath)
.on("error", (err) => {
console.error(`Error walking directory ${resolvedPath}:`, err);
})
// 1.1 Filter on .cb* extensions
.pipe(
through2.obj(function (item, enc, next) {

View File

@@ -174,14 +174,16 @@ export const getMimeType = async (filePath: string) => {
*/
export const createDirectory = async (options: any, directoryPath: string) => {
try {
await fse.ensureDir(directoryPath, options);
await fse.ensureDir(directoryPath);
console.info(`Directory [ %s ] was created.`, directoryPath);
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
console.error(`Failed to create directory [ ${directoryPath} ]:`, error);
throw new Errors.MoleculerError(
"Failed to create directory",
`Failed to create directory: ${directoryPath} - ${errMsg}`,
500,
"FileOpsError",
error
{ directoryPath, originalError: errMsg }
);
}
};

View File

@@ -32,6 +32,7 @@ SOFTWARE.
*/
import { createReadStream, createWriteStream, existsSync, statSync } from "fs";
import { execFile } from "child_process";
import { isEmpty, isNil, isUndefined, remove, each, map, reject } from "lodash";
import * as p7zip from "p7zip-threetwo";
import path from "path";
@@ -88,124 +89,335 @@ export const extractComicInfoXMLFromRar = async (
)}`;
await createDirectory(directoryOptions, targetDirectory);
const archive = new Unrar({
path: path.resolve(filePath),
bin: `${UNRAR_BIN_PATH}`, // this will change depending on Docker base OS
arguments: ["-v"],
});
const filesInArchive: [RarFile] = await new Promise(
(resolve, reject) => {
return archive.list((err, entries) => {
if (err) {
console.log(`DEBUG: ${JSON.stringify(err, null, 2)}`);
reject(err);
}
resolve(entries);
});
}
);
// Try unrar-based extraction first, fall back to p7zip if it fails
let unrarError: Error | null = null;
try {
const result = await extractComicInfoXMLFromRarUsingUnrar(
filePath,
mimeType,
targetDirectory,
fileNameWithoutExtension,
extension
);
return result;
} catch (err) {
unrarError = err;
console.warn(
`unrar-based extraction failed for ${filePath}: ${err.message}. Falling back to p7zip.`
);
}
remove(filesInArchive, ({ type }) => type === "Directory");
const comicInfoXML = remove(
filesInArchive,
({ name }) => path.basename(name).toLowerCase() === "comicinfo.xml"
);
try {
const result = await extractComicInfoXMLFromRarUsingP7zip(
filePath,
mimeType,
targetDirectory,
fileNameWithoutExtension,
extension
);
return result;
} catch (p7zipError) {
console.error(
`p7zip-based extraction also failed for ${filePath}: ${p7zipError.message}`
);
throw new Error(
`Failed to extract RAR archive: ${filePath}. ` +
`unrar error: ${unrarError?.message}. ` +
`p7zip error: ${p7zipError.message}. ` +
`Ensure 'unrar' is installed at ${UNRAR_BIN_PATH} or '7z' is available via SEVENZ_BINARY_PATH.`
);
}
} catch (err) {
throw err;
}
};
remove(
filesInArchive,
({ name }) =>
!IMPORT_IMAGE_FILE_FORMATS.includes(
path.extname(name).toLowerCase()
)
);
const files = filesInArchive.sort((a, b) => {
if (!isUndefined(a) && !isUndefined(b)) {
return path
.basename(a.name)
.toLowerCase()
.localeCompare(path.basename(b.name).toLowerCase());
}
});
const comicInfoXMLFilePromise = new Promise((resolve, reject) => {
let comicinfostring = "";
if (!isUndefined(comicInfoXML[0])) {
const comicInfoXMLFileName = path.basename(
comicInfoXML[0].name
);
const writeStream = createWriteStream(
`${targetDirectory}/${comicInfoXMLFileName}`
);
archive.stream(comicInfoXML[0]["name"]).pipe(writeStream);
writeStream.on("finish", async () => {
console.log(`Attempting to write comicInfo.xml...`);
const readStream = createReadStream(
`${targetDirectory}/${comicInfoXMLFileName}`
);
readStream.on("data", (data) => {
comicinfostring += data;
});
readStream.on("error", (error) => reject(error));
readStream.on("end", async () => {
if (
existsSync(
`${targetDirectory}/${comicInfoXMLFileName}`
)
) {
const comicInfoJSON = await convertXMLToJSON(
comicinfostring.toString()
);
console.log(
`comicInfo.xml successfully written: ${comicInfoJSON.comicinfo}`
);
resolve({ comicInfoJSON: comicInfoJSON.comicinfo });
}
});
});
} else {
resolve({ comicInfoJSON: null });
}
});
const coverFilePromise = new Promise((resolve, reject) => {
const coverFile = path.basename(files[0].name);
const sharpStream = sharp().resize(275).toFormat("png");
const coverExtractionStream = archive.stream(files[0].name);
const resizeStream = coverExtractionStream.pipe(sharpStream);
resizeStream.toFile(
`${targetDirectory}/${coverFile}`,
(err, info) => {
if (err) {
reject(err);
}
checkFileExists(`${targetDirectory}/${coverFile}`).then(
(bool) => {
console.log(`${coverFile} exists: ${bool}`);
// orchestrate result
resolve({
filePath,
name: fileNameWithoutExtension,
extension,
containedIn: targetDirectory,
fileSize: fse.statSync(filePath).size,
mimeType,
cover: {
filePath: path.relative(
process.cwd(),
`${targetDirectory}/${coverFile}`
),
},
});
}
/**
* List files in a RAR archive using the unrar binary directly.
* Uses `unrar lb` (bare list) for reliable output — one filename per line.
*/
const listRarFiles = (filePath: string): Promise<string[]> => {
return new Promise((resolve, reject) => {
execFile(
UNRAR_BIN_PATH,
["lb", path.resolve(filePath)],
{ maxBuffer: 10 * 1024 * 1024 },
(err, stdout, stderr) => {
if (err) {
return reject(
new Error(
`unrar lb failed for ${filePath}: ${err.message}${stderr ? ` (stderr: ${stderr})` : ""}`
)
);
}
);
});
const files = stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0);
resolve(files);
}
);
});
};
return Promise.all([comicInfoXMLFilePromise, coverFilePromise]);
} catch (err) {
reject(err);
/**
* Extract a single file from a RAR archive to stdout as a Buffer.
* Uses `unrar p -inul` (print to stdout, no messages).
*/
const extractRarFileToBuffer = (
filePath: string,
entryName: string
): Promise<Buffer> => {
return new Promise((resolve, reject) => {
execFile(
UNRAR_BIN_PATH,
["p", "-inul", path.resolve(filePath), entryName],
{ maxBuffer: 50 * 1024 * 1024, encoding: "buffer" },
(err, stdout, stderr) => {
if (err) {
return reject(
new Error(
`unrar p failed for ${entryName} in ${filePath}: ${err.message}`
)
);
}
resolve(stdout as unknown as Buffer);
}
);
});
};
/**
* Extract comic info and cover from a RAR archive using the unrar binary directly.
* Bypasses the `unrar` npm package which has parsing bugs.
*/
const extractComicInfoXMLFromRarUsingUnrar = async (
filePath: string,
mimeType: string,
targetDirectory: string,
fileNameWithoutExtension: string,
extension: string
): Promise<any> => {
// List all files in the archive using bare listing
const allFiles = await listRarFiles(filePath);
console.log(
`RAR (unrar direct): ${allFiles.length} total entries in ${filePath}`
);
// Find ComicInfo.xml
const comicInfoXMLEntry = allFiles.find(
(name) => path.basename(name).toLowerCase() === "comicinfo.xml"
);
// Filter to image files only
const imageFiles = allFiles
.filter((name) =>
IMPORT_IMAGE_FILE_FORMATS.includes(
path.extname(name).toLowerCase()
)
)
.sort((a, b) =>
path
.basename(a)
.toLowerCase()
.localeCompare(path.basename(b).toLowerCase())
);
if (imageFiles.length === 0) {
throw new Error(
`No image files found via unrar in RAR archive: ${filePath}`
);
}
// Extract and parse ComicInfo.xml if present
let comicInfoResult: { comicInfoJSON: any } = { comicInfoJSON: null };
if (comicInfoXMLEntry) {
try {
const xmlBuffer = await extractRarFileToBuffer(
filePath,
comicInfoXMLEntry
);
const comicInfoJSON = await convertXMLToJSON(
xmlBuffer.toString("utf-8")
);
console.log(
`comicInfo.xml successfully extracted: ${comicInfoJSON.comicinfo}`
);
comicInfoResult = { comicInfoJSON: comicInfoJSON.comicinfo };
} catch (xmlErr) {
console.warn(
`Failed to extract ComicInfo.xml from ${filePath}: ${xmlErr.message}`
);
}
}
// Extract and resize cover image (first image file)
const coverEntryName = imageFiles[0];
const coverFile = path.basename(coverEntryName);
const coverBaseName = sanitize(path.basename(coverFile, path.extname(coverFile)));
const coverOutputFile = `${targetDirectory}/${coverBaseName}.png`;
const coverBuffer = await extractRarFileToBuffer(
filePath,
coverEntryName
);
await sharp(coverBuffer)
.resize(275)
.toFormat("png")
.toFile(coverOutputFile);
console.log(`${coverFile} cover written to: ${coverOutputFile}`);
const relativeCoverPath = path.relative(process.cwd(), coverOutputFile);
console.log(`RAR cover path (relative): ${relativeCoverPath}`);
console.log(`RAR cover file exists: ${existsSync(coverOutputFile)}`);
const coverResult = {
filePath,
name: fileNameWithoutExtension,
extension,
containedIn: targetDirectory,
fileSize: fse.statSync(filePath).size,
mimeType,
cover: {
filePath: relativeCoverPath,
},
};
return [comicInfoResult, coverResult];
};
/**
* Fallback: Extract comic info and cover from a RAR archive using p7zip (7z).
* Uses the same approach as extractComicInfoXMLFromZip since p7zip handles RAR files.
*/
const extractComicInfoXMLFromRarUsingP7zip = async (
filePath: string,
mimeType: string,
targetDirectory: string,
fileNameWithoutExtension: string,
extension: string
): Promise<any> => {
let filesToWriteToDisk = { coverFile: null, comicInfoXML: null };
const extractionTargets = [];
// read the archive using p7zip (supports RAR)
let filesFromArchive = await p7zip.read(path.resolve(filePath));
console.log(
`RAR (p7zip): ${filesFromArchive.files.length} total entries in ${filePath}`
);
// detect ComicInfo.xml
const comicInfoXMLFileObject = remove(
filesFromArchive.files,
(file) => path.basename(file.name.toLowerCase()) === "comicinfo.xml"
);
// only allow allowed image formats
remove(
filesFromArchive.files,
({ name }) =>
!IMPORT_IMAGE_FILE_FORMATS.includes(
path.extname(name).toLowerCase()
)
);
// Natural sort
const files = filesFromArchive.files.sort((a, b) => {
if (!isUndefined(a) && !isUndefined(b)) {
return path
.basename(a.name)
.toLowerCase()
.localeCompare(path.basename(b.name).toLowerCase());
}
});
if (files.length === 0) {
throw new Error(`No image files found in RAR archive: ${filePath}`);
}
// Push the first file (cover) to our extraction target
extractionTargets.push(files[0].name);
filesToWriteToDisk.coverFile = path.basename(files[0].name);
if (!isEmpty(comicInfoXMLFileObject)) {
filesToWriteToDisk.comicInfoXML = comicInfoXMLFileObject[0].name;
extractionTargets.push(filesToWriteToDisk.comicInfoXML);
}
// Extract the files.
await p7zip.extract(
filePath,
targetDirectory,
extractionTargets,
"",
false
);
// ComicInfoXML detection, parsing and conversion to JSON
const comicInfoXMLPromise = new Promise((resolve, reject) => {
if (
!isNil(filesToWriteToDisk.comicInfoXML) &&
existsSync(
`${targetDirectory}/${path.basename(
filesToWriteToDisk.comicInfoXML
)}`
)
) {
let comicinfoString = "";
const comicInfoXMLStream = createReadStream(
`${targetDirectory}/${path.basename(
filesToWriteToDisk.comicInfoXML
)}`
);
comicInfoXMLStream.on(
"data",
(data) => (comicinfoString += data)
);
comicInfoXMLStream.on("end", async () => {
const comicInfoJSON = await convertXMLToJSON(
comicinfoString.toString()
);
resolve({
comicInfoJSON: comicInfoJSON.comicinfo,
});
});
} else {
resolve({
comicInfoJSON: null,
});
}
});
// Write the cover to disk
const coverBaseName = sanitize(path.basename(
filesToWriteToDisk.coverFile,
path.extname(filesToWriteToDisk.coverFile)
));
const coverOutputFile = `${targetDirectory}/${coverBaseName}.png`;
const coverInputFile = `${targetDirectory}/${filesToWriteToDisk.coverFile}`;
await sharp(coverInputFile)
.resize(275)
.toFormat("png")
.toFile(coverOutputFile);
const comicInfoResult = await comicInfoXMLPromise;
const coverResult = {
filePath,
name: fileNameWithoutExtension,
extension,
mimeType,
containedIn: targetDirectory,
fileSize: fse.statSync(filePath).size,
cover: {
filePath: path.relative(process.cwd(), coverOutputFile),
},
};
return [comicInfoResult, coverResult];
};
export const extractComicInfoXMLFromZip = async (
@@ -252,6 +464,11 @@ export const extractComicInfoXMLFromZip = async (
.localeCompare(path.basename(b.name).toLowerCase());
}
});
if (files.length === 0) {
throw new Error(`No image files found in ZIP archive: ${filePath}`);
}
// Push the first file (cover) to our extraction target
extractionTargets.push(files[0].name);
filesToWriteToDisk.coverFile = path.basename(files[0].name);
@@ -306,45 +523,32 @@ export const extractComicInfoXMLFromZip = async (
}
});
// Write the cover to disk
const coverFilePromise = new Promise((resolve, reject) => {
const sharpStream = sharp().resize(275).toFormat("png");
const coverStream = createReadStream(
`${targetDirectory}/${filesToWriteToDisk.coverFile}`
);
coverStream
.pipe(sharpStream)
.toFile(
`${targetDirectory}/${path.basename(
filesToWriteToDisk.coverFile
)}`,
(err, info) => {
if (err) {
reject(err);
}
// Update metadata
resolve({
filePath,
name: fileNameWithoutExtension,
extension,
mimeType,
containedIn: targetDirectory,
fileSize: fse.statSync(filePath).size,
cover: {
filePath: path.relative(
process.cwd(),
`${targetDirectory}/${path.basename(
filesToWriteToDisk.coverFile
)}`
),
},
});
}
);
});
const coverBaseName = sanitize(path.basename(filesToWriteToDisk.coverFile, path.extname(filesToWriteToDisk.coverFile)));
const coverOutputFile = `${targetDirectory}/${coverBaseName}.png`;
const coverInputFile = `${targetDirectory}/${filesToWriteToDisk.coverFile}`;
return Promise.all([comicInfoXMLPromise, coverFilePromise]);
await sharp(coverInputFile)
.resize(275)
.toFormat("png")
.toFile(coverOutputFile);
const comicInfoResult = await comicInfoXMLPromise;
const coverResult = {
filePath,
name: fileNameWithoutExtension,
extension,
mimeType,
containedIn: targetDirectory,
fileSize: fse.statSync(filePath).size,
cover: {
filePath: path.relative(process.cwd(), coverOutputFile),
},
};
return [comicInfoResult, coverResult];
} catch (err) {
reject(err);
throw err;
}
};
@@ -361,6 +565,9 @@ export const extractFromArchive = async (filePath: string) => {
filePath,
mimeType
);
if (!Array.isArray(cbzResult)) {
throw new Error(`extractComicInfoXMLFromZip returned a non-iterable result for: ${filePath}`);
}
return Object.assign({}, ...cbzResult);
case "application/x-rar; charset=binary":
@@ -368,6 +575,9 @@ export const extractFromArchive = async (filePath: string) => {
filePath,
mimeType
);
if (!Array.isArray(cbrResult)) {
throw new Error(`extractComicInfoXMLFromRar returned a non-iterable result for: ${filePath}`);
}
return Object.assign({}, ...cbrResult);
default:
@@ -419,7 +629,7 @@ export const uncompressZipArchive = async (filePath: string, options: any) => {
mode: 0o2775,
};
const { fileNameWithoutExtension } = getFileConstituents(filePath);
const targetDirectory = `${USERDATA_DIRECTORY}/expanded/${options.purpose}/${fileNameWithoutExtension}`;
const targetDirectory = `${USERDATA_DIRECTORY}/expanded/${options.purpose}/${sanitize(fileNameWithoutExtension)}`;
await createDirectory(directoryOptions, targetDirectory);
await p7zip.extract(filePath, targetDirectory, [], "", false);
@@ -433,7 +643,7 @@ export const uncompressRarArchive = async (filePath: string, options: any) => {
};
const { fileNameWithoutExtension, extension } =
getFileConstituents(filePath);
const targetDirectory = `${USERDATA_DIRECTORY}/expanded/${options.purpose}/${fileNameWithoutExtension}`;
const targetDirectory = `${USERDATA_DIRECTORY}/expanded/${options.purpose}/${sanitize(fileNameWithoutExtension)}`;
await createDirectory(directoryOptions, targetDirectory);
const archive = new Unrar({