feat: remove web-crawler and use electron (chromium) itself as crawler, implementing a function to download nhentai favorite torrents

This commit is contained in:
Xymorot 2020-11-09 18:15:30 +01:00
parent f54edba6fc
commit 1618ac552b
39 changed files with 567 additions and 856 deletions

View File

@ -34,6 +34,7 @@
}
],
"no-constant-condition": ["error", { "checkLoops": false }],
"no-throw-literal": "error",
"import/no-extraneous-dependencies": [
"error",

View File

@ -9,6 +9,9 @@ const electronMock: DeepPartial<typeof Electron> = {
return path.resolve('test-paths', name);
},
quit(): void {},
getAppPath(): string {
return path.resolve(__dirname, '..');
},
},
BrowserWindow: class {
public webContents: DeepPartial<WebContents> = {

414
package-lock.json generated
View File

@ -894,17 +894,6 @@
"integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==",
"dev": true
},
"@types/jsdom": {
"version": "16.2.3",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.3.tgz",
"integrity": "sha512-BREatezSn74rmLIDksuqGNFUTi9HNAWWQXYpFBFLK9U6wlMCO4M0QCa8CMpDsZQuqxSO9XifVLT5Q1P0vgKLqw==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/parse5": "*",
"@types/tough-cookie": "*"
}
},
"@types/json-schema": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
@ -980,12 +969,6 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
"@types/parse5": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz",
"integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==",
"dev": true
},
"@types/puppeteer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-3.0.1.tgz",
@ -1031,12 +1014,6 @@
"integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==",
"dev": true
},
"@types/tough-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz",
"integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==",
"dev": true
},
"@types/uglify-js": {
"version": "3.9.3",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.9.3.tgz",
@ -1515,41 +1492,17 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
},
"abab": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz",
"integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg=="
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"acorn": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz",
"integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ=="
},
"acorn-globals": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
"integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
"requires": {
"acorn": "^7.1.1",
"acorn-walk": "^7.1.1"
}
},
"acorn-jsx": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
"integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
"dev": true
},
"acorn-walk": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA=="
},
"agent-base": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
@ -2191,11 +2144,6 @@
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
"dev": true
},
"browser-process-hrtime": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
"integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
},
"browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
@ -3294,26 +3242,6 @@
"integrity": "sha1-Xv1sLupeof1rasV+wEJ7GEUkJOo=",
"dev": true
},
"cssom": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
"integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw=="
},
"cssstyle": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
"integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
"requires": {
"cssom": "~0.3.6"
},
"dependencies": {
"cssom": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
}
}
},
"cuint": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
@ -3344,16 +3272,6 @@
"assert-plus": "^1.0.0"
}
},
"data-urls": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
"integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==",
"requires": {
"abab": "^2.0.3",
"whatwg-mimetype": "^2.3.0",
"whatwg-url": "^8.0.0"
}
},
"date-fns": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.12.0.tgz",
@ -3379,11 +3297,6 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"decimal.js": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz",
"integrity": "sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw=="
},
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@ -3416,7 +3329,8 @@
"deep-is": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true
},
"deepmerge": {
"version": "4.2.2",
@ -3612,21 +3526,6 @@
"integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
"dev": true
},
"domexception": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
"integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==",
"requires": {
"webidl-conversions": "^5.0.0"
},
"dependencies": {
"webidl-conversions": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
"integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="
}
}
},
"dotenv": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz",
@ -4236,18 +4135,6 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"escodegen": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
"integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
"requires": {
"esprima": "^4.0.1",
"estraverse": "^4.2.0",
"esutils": "^2.0.2",
"optionator": "^0.8.1",
"source-map": "~0.6.1"
}
},
"eslint": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.6.0.tgz",
@ -4661,11 +4548,6 @@
}
}
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"esquery": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz",
@ -4695,12 +4577,14 @@
"estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true
},
"esutils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
"dev": true
},
"events": {
"version": "3.0.0",
@ -4975,7 +4859,8 @@
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
"fd-slicer": {
"version": "1.1.0",
@ -5802,14 +5687,6 @@
"integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
"dev": true
},
"html-encoding-sniffer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
"integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==",
"requires": {
"whatwg-encoding": "^1.0.5"
}
},
"html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@ -6223,11 +6100,6 @@
"resolved": "https://registry.npmjs.org/inversify/-/inversify-5.0.1.tgz",
"integrity": "sha512-Ieh06s48WnEYGcqHepdsJUIJUXpwH5o5vodAX+DK2JA/gjy4EbEcQZxw+uFfzysmKjiLXGYwNG3qDZsKVMcINQ=="
},
"ip-regex": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
"integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk="
},
"is-accessor-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
@ -6417,11 +6289,6 @@
"isobject": "^3.0.1"
}
},
"is-potential-custom-element-name": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz",
"integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c="
},
"is-promise": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
@ -6690,84 +6557,6 @@
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
},
"jsdom": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.3.0.tgz",
"integrity": "sha512-zggeX5UuEknpdZzv15+MS1dPYG0J/TftiiNunOeNxSl3qr8Z6cIlQpN0IdJa44z9aFxZRIVqRncvEhQ7X5DtZg==",
"requires": {
"abab": "^2.0.3",
"acorn": "^7.1.1",
"acorn-globals": "^6.0.0",
"cssom": "^0.4.4",
"cssstyle": "^2.2.0",
"data-urls": "^2.0.0",
"decimal.js": "^10.2.0",
"domexception": "^2.0.1",
"escodegen": "^1.14.1",
"html-encoding-sniffer": "^2.0.1",
"is-potential-custom-element-name": "^1.0.0",
"nwsapi": "^2.2.0",
"parse5": "5.1.1",
"request": "^2.88.2",
"request-promise-native": "^1.0.8",
"saxes": "^5.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^3.0.1",
"w3c-hr-time": "^1.0.2",
"w3c-xmlserializer": "^2.0.0",
"webidl-conversions": "^6.1.0",
"whatwg-encoding": "^1.0.5",
"whatwg-mimetype": "^2.3.0",
"whatwg-url": "^8.0.0",
"ws": "^7.2.3",
"xml-name-validator": "^3.0.0"
},
"dependencies": {
"request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
"dependencies": {
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
}
}
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
}
}
},
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@ -6880,15 +6669,6 @@
"readable-stream": "^2.0.5"
}
},
"levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
"integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
"requires": {
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2"
}
},
"lighthouse-logger": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.2.0.tgz",
@ -7044,11 +6824,6 @@
"integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=",
"dev": true
},
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
},
"lodash.template": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz",
@ -8148,11 +7923,6 @@
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"nwsapi": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
"integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ=="
},
"nyc": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz",
@ -8469,19 +8239,6 @@
"integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==",
"dev": true
},
"optionator": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
"integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
"requires": {
"deep-is": "~0.1.3",
"fast-levenshtein": "~2.0.6",
"levn": "~0.3.0",
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2",
"word-wrap": "~1.2.3"
}
},
"ora": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/ora/-/ora-4.0.5.tgz",
@ -8998,11 +8755,6 @@
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
"dev": true
},
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
"integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
},
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
@ -9526,42 +9278,6 @@
}
}
},
"request-promise-core": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz",
"integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==",
"requires": {
"lodash": "^4.17.19"
},
"dependencies": {
"lodash": {
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
}
}
},
"request-promise-native": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz",
"integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==",
"requires": {
"request-promise-core": "1.1.4",
"stealthy-require": "^1.1.1",
"tough-cookie": "^2.3.3"
},
"dependencies": {
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
}
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -9793,14 +9509,6 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"saxes": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
"integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
"requires": {
"xmlchars": "^2.2.0"
}
},
"schema-utils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
@ -10133,7 +9841,8 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-resolve": {
"version": "0.5.2",
@ -10410,11 +10119,6 @@
}
}
},
"stealthy-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
},
"stream-browserify": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
@ -10628,11 +10332,6 @@
"svelte-dev-helper": "^1.1.9"
}
},
"symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
},
"table": {
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
@ -11039,24 +10738,6 @@
"repeat-string": "^1.6.1"
}
},
"tough-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
"integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
"requires": {
"ip-regex": "^2.1.0",
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
},
"tr46": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz",
"integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==",
"requires": {
"punycode": "^2.1.1"
}
},
"tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@ -11203,14 +10884,6 @@
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
},
"type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
"integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
"requires": {
"prelude-ls": "~1.1.2"
}
},
"type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
@ -11522,22 +11195,6 @@
"integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==",
"dev": true
},
"w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
"integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
"requires": {
"browser-process-hrtime": "^1.0.0"
}
},
"w3c-xmlserializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
"integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==",
"requires": {
"xml-name-validator": "^3.0.0"
}
},
"watchpack": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz",
@ -11834,11 +11491,6 @@
"webdriver": "6.3.5"
}
},
"webidl-conversions": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w=="
},
"webpack": {
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.1.tgz",
@ -12070,36 +11722,6 @@
"source-map": "~0.6.1"
}
},
"whatwg-encoding": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
"integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==",
"requires": {
"iconv-lite": "0.4.24"
}
},
"whatwg-mimetype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
"integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g=="
},
"whatwg-url": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.1.0.tgz",
"integrity": "sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw==",
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^2.0.2",
"webidl-conversions": "^5.0.0"
},
"dependencies": {
"webidl-conversions": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
"integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="
}
}
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@ -12151,7 +11773,8 @@
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ=="
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true
},
"wordwrap": {
"version": "1.0.0",
@ -12243,12 +11866,8 @@
"ws": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA=="
},
"xml-name-validator": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw=="
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==",
"dev": true
},
"xml2js": {
"version": "0.4.23",
@ -12272,11 +11891,6 @@
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
"dev": true
},
"xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
"xmldom": {
"version": "0.1.31",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",

View File

@ -48,7 +48,6 @@
"electron-squirrel-startup": "^1.0.0",
"fs-extra": "^9.0.1",
"inversify": "^5.0.1",
"jsdom": "^16.2.2",
"minimist": "^1.2.5",
"node-fetch": "^2.6.0",
"reflect-metadata": "^0.1.13",
@ -62,7 +61,6 @@
"@types/chai": "^4.2.12",
"@types/chai-fs": "^2.0.2",
"@types/fs-extra": "^9.0.1",
"@types/jsdom": "^16.2.3",
"@types/minimist": "^1.2.0",
"@types/mocha": "^8.0.1",
"@types/node": "^12.12.54",

View File

@ -6,7 +6,6 @@ import { app } from 'electron';
import { isDev } from './main/core/env';
import { IAppWindow } from './main/modules/app-window/i-app-window';
import { ILogger } from './main/modules/logger/i-logger';
import { ISession } from './main/modules/session/i-session';
/**
* have a read: https://github.com/nodejs/node/issues/20392, over 100 comments as of 2020-07-26
@ -21,22 +20,19 @@ process.on('unhandledRejection', (reason) => {
process.on('uncaughtException', (error) => {
const logger: ILogger = container.get('logger');
void logger.exception(error);
if (isDev()) {
// eslint-disable-next-line no-console -- only for development purposes
console.error(error);
}
});
async function createWindow(): Promise<void> {
const session: ISession = container.get('session');
session.setHeaders();
const appWindowMain: IAppWindow = container.get('app-window-main');
// and load the index.html of the app.
await appWindowMain.open();
// Open the DevTools.
if (isDev()) {
// eslint-disable-next-line no-unused-expressions -- eslint can't handle optional chaining, yet
appWindowMain.window?.webContents.openDevTools();
}
appWindowMain.window?.on('closed', () => {
app.quit();
});
}
// This method will be called when Electron has finished
@ -46,11 +42,7 @@ app.on('ready', createWindow);
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
app.quit();
});
app.on('activate', async () => {

View File

@ -1,16 +1,17 @@
import 'reflect-metadata';
import { Container, interfaces } from 'inversify';
import { MainAppWindow } from '../modules/app-window/main-app-window';
import { I18nTranslator } from '../modules/i18n/i18n-translator';
import { Logger } from '../modules/logger/logger';
import { NhentaiApi } from '../modules/nhentai/nhentai-api';
import '../modules/nhentai/nhentai-ipc-controller';
import { Session } from '../modules/session/session';
import { NhentaiAppWindow } from '../modules/nhentai/nhentai-app-window';
import { SessionHelper } from '../modules/session/session-helper';
import { Store } from '../modules/store/store';
import { WebCrawler } from '../modules/web-crawler/web-crawler';
import BindingToSyntax = interfaces.BindingToSyntax;
export const container = {
original: new Container({ defaultScope: 'Singleton' }),
original: new Container({ defaultScope: 'Singleton', skipBaseClassChecks: true }),
bind<T>(key: string): BindingToSyntax<T> {
return this.original.bind<T>(Symbol.for(key));
},
@ -24,12 +25,13 @@ export const container = {
container.bind('logger').to(Logger);
container.bind('i18n-translator').to(I18nTranslator);
container.bind('store').to(Store);
container.bind('web-crawler').to(WebCrawler);
container.bind('session-helper').to(SessionHelper);
container.bind('nhentai-api').to(NhentaiApi);
container.bind('nhentai-app-window').to(NhentaiAppWindow);
container.bind('app-window-main').to(MainAppWindow);
container.bind('session').to(Session);

View File

@ -1,14 +1,19 @@
import { BrowserWindow } from 'electron';
import { app, BrowserWindow, Event, LoadFileOptions, LoadURLOptions, NewWindowEvent } from 'electron';
import os from 'os';
import { injectable } from 'inversify';
import path from 'path';
import { isDev } from '../../core/env';
import { ISessionHelper } from '../session/i-session-helper';
import { IAppWindow } from './i-app-window';
import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions;
let defaultOptions = {
let defaultOptions: BrowserWindowConstructorOptions = {
width: 1600,
height: 900,
webPreferences: {
enableRemoteModule: false,
nodeIntegration: false,
contextIsolation: true,
devTools: isDev(),
},
};
@ -25,12 +30,23 @@ switch (os.platform()) {
break;
}
@injectable()
export abstract class AppWindow implements IAppWindow {
protected static default = {};
protected _window: BrowserWindow | null = null;
protected constructor(options: BrowserWindowConstructorOptions = {}) {
this.initialize(options);
protected readonly sessionHelper: ISessionHelper;
protected options: BrowserWindowConstructorOptions;
protected uri: string;
protected abstract loadOptions: LoadFileOptions | LoadURLOptions;
protected constructor(sessionHelper: ISessionHelper, uri: string, options: BrowserWindowConstructorOptions = {}) {
this.sessionHelper = sessionHelper;
this.options = { ...defaultOptions, ...options };
this.uri = uri;
}
public get window(): BrowserWindow | null {
@ -38,14 +54,29 @@ export abstract class AppWindow implements IAppWindow {
}
public open(): Promise<void> {
if (this.isClosed()) {
this.initialize();
this._window = new BrowserWindow(this.options);
this.sessionHelper.setCsp(this._window, this.getCsp());
this._window.on('closed', () => {
this._window = null;
});
if (isDev()) {
this._window.webContents.openDevTools();
}
if (this._window) {
return this._window.loadFile('frontend/index.html');
this._window.webContents.on('will-navigate', this.onWillNavigate);
this._window.webContents.on('new-window', this.onNewWindow);
return this.load(this._window);
}
public close(force: boolean = false): void {
if (force) {
this._window?.destroy();
} else {
return Promise.reject(new Error('the window was not initialized'));
this._window?.close();
}
}
@ -53,11 +84,18 @@ export abstract class AppWindow implements IAppWindow {
return !this._window;
}
private initialize(options: BrowserWindowConstructorOptions = {}): void {
this._window = new BrowserWindow({ ...defaultOptions, ...options });
this._window.on('closed', () => {
this._window = null;
});
protected getCsp(): IContentSecurityPolicy {
return {};
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- it is used in child classes
protected onWillNavigate(event: Event, navigationUrl: string): void {
event.preventDefault();
}
protected onNewWindow(event: NewWindowEvent): void {
event.preventDefault();
}
protected abstract load(window: BrowserWindow): Promise<void>;
}

View File

@ -0,0 +1,21 @@
import { BrowserWindow, BrowserWindowConstructorOptions, LoadFileOptions } from 'electron';
import { ISessionHelper } from '../session/i-session-helper';
import { AppWindow } from './app-window';
export abstract class FileAppWindow extends AppWindow {
protected loadOptions: LoadFileOptions;
protected constructor(
sessionHelper: ISessionHelper,
uri: string,
options: BrowserWindowConstructorOptions = {},
loadOptions: LoadFileOptions = {}
) {
super(sessionHelper, uri, options);
this.loadOptions = loadOptions;
}
protected load(window: BrowserWindow): Promise<void> {
return window.loadFile(this.uri, this.loadOptions);
}
}

View File

@ -3,5 +3,6 @@ import BrowserWindow = Electron.BrowserWindow;
export interface IAppWindow {
window: BrowserWindow | null;
open(): Promise<void>;
close(force?: boolean): void;
isClosed(): boolean;
}

View File

@ -0,0 +1,6 @@
import { LoadURLOptions } from 'electron';
import { IAppWindow } from './i-app-window';
export interface IUrlAppWindow extends IAppWindow {
loadUrlSafe(url: string, options?: LoadURLOptions): Promise<void>;
}

View File

@ -1,10 +1,12 @@
import { injectable } from 'inversify';
import { AppWindow } from './app-window';
import { inject } from '../../core/inject';
import { ISessionHelper } from '../session/i-session-helper';
import { FileAppWindow } from './file-app-window';
@injectable()
export class MainAppWindow extends AppWindow {
public constructor() {
super({
export class MainAppWindow extends FileAppWindow {
public constructor(@inject('session-helper') sessionHelper: ISessionHelper) {
super(sessionHelper, 'frontend/index.html', {
webPreferences: {
nodeIntegration: true,
},

View File

@ -0,0 +1,57 @@
import { BrowserWindow, BrowserWindowConstructorOptions, LoadURLOptions } from 'electron';
import { promisify } from 'util';
import { ISessionHelper } from '../session/i-session-helper';
import { AppWindow } from './app-window';
import { IUrlAppWindow } from './i-url-app-window';
import { WindowClosedError } from './window-closed-error';
export abstract class UrlAppWindow extends AppWindow implements IUrlAppWindow {
protected loadOptions: LoadURLOptions;
protected constructor(
sessionHelper: ISessionHelper,
uri: string,
options: BrowserWindowConstructorOptions = {},
loadOptions: LoadURLOptions = {}
) {
const securedOptions: BrowserWindowConstructorOptions = {
...options,
...{
webPreferences: {
enableRemoteModule: false,
nodeIntegration: false,
contextIsolation: true,
},
},
};
super(sessionHelper, uri, securedOptions);
this.loadOptions = loadOptions;
}
public async loadUrlSafe(url: string, options?: LoadURLOptions): Promise<void> {
if (!this._window) {
throw new WindowClosedError();
}
const waitInterval = 1000;
let failedLoad = true;
do {
await new Promise((resolve) => {
if (!this._window) {
throw new WindowClosedError();
}
this._window.webContents.once('did-navigate', (event, navigationUrl, httpResponseCode) => {
failedLoad = HttpCode.BAD_REQUEST <= httpResponseCode;
resolve();
});
void this._window.loadURL(url, options);
});
if (failedLoad) {
await promisify(setTimeout)(waitInterval);
}
} while (failedLoad);
}
protected load(window: BrowserWindow): Promise<void> {
return window.loadURL(this.uri, this.loadOptions);
}
}

View File

@ -0,0 +1,5 @@
export class WindowClosedError extends TypeError {
public constructor(message = 'window is closed') {
super(message);
}
}

View File

@ -1,8 +0,0 @@
/**
* Error thrown when cookies could not be saved.
*/
export class CookieSaveError extends Error {
public constructor(message: string = 'failed to save cookies') {
super(message);
}
}

View File

@ -1,10 +0,0 @@
/**
* Error thrown when there is an error while initializing services or the app in general.
*
* You're pretty much fucked at this point, try to avoid unstable code on initialization
*/
export class InitializationError extends Error {
public constructor(message: string = 'initialization failed') {
super(message);
}
}

View File

@ -1,8 +0,0 @@
/**
* generic web crawler error
*/
export class WebCrawlerError extends Error {
public constructor(message: string = 'web crawler failed') {
super(message);
}
}

View File

@ -1,10 +0,0 @@
import { WebCrawlerError } from './web-crawler-error';
/**
* Error thrown when the web crawler can't work with the provided form.
*/
export class WebCrawlerFormError extends WebCrawlerError {
public constructor(message: string = 'web crawler failed') {
super(message);
}
}

View File

@ -1,10 +0,0 @@
import { WebCrawlerError } from './web-crawler-error';
/**
* Error thrown when the web crawler can't login
*/
export class WebCrawlerLoginError extends WebCrawlerError {
public constructor(message: string = 'login failed') {
super(message);
}
}

View File

@ -0,0 +1,3 @@
export interface II18nTranslator {
t(text: string): string;
}

View File

@ -0,0 +1,9 @@
import { injectable } from 'inversify';
import { II18nTranslator } from './i-i18n-translator';
@injectable()
export class I18nTranslator implements II18nTranslator {
public t(text: string): string {
return text;
}
}

View File

@ -1,4 +1,3 @@
export interface INhentaiApi {
isLoggedIn(): Promise<boolean>;
login(name: string, password: string): Promise<void>;
getFavorites(): Promise<NodeJS.ReadableStream>;
}

View File

@ -0,0 +1,5 @@
import { IUrlAppWindow } from '../app-window/i-url-app-window';
export interface INhentaiAppWindow extends IUrlAppWindow {
getFavorites(): Promise<NodeJS.ReadableStream>;
}

View File

@ -1,129 +1,17 @@
import { injectable } from 'inversify';
import { JSDOM } from 'jsdom';
import { RequestInit, Response } from 'node-fetch';
import { inject } from '../../core/inject';
import { WebCrawlerFormError } from '../error/web-crawler-form-error';
import { WebCrawlerLoginError } from '../error/web-crawler-login-error';
import { IWebCrawler } from '../web-crawler/i-web-crawler';
import { INhentaiApi } from './i-nhentai-api';
const domain = 'nhentai.net';
const url = `https://${domain}/`;
const paths = {
books: 'g/',
login: 'login/',
favorites: 'favorites/',
};
const usernameInput = 'username_or_email';
const passwordInput = 'password';
interface ILoginMeta {
[key: string]: string;
}
interface ILoginAuth {
[usernameInput]: string;
[passwordInput]: string;
}
interface ILoginParams extends ILoginMeta, ILoginAuth {}
import { INhentaiAppWindow } from './i-nhentai-app-window';
@injectable()
export class NhentaiApi implements INhentaiApi {
private webCrawler: IWebCrawler;
private readonly appWindow: INhentaiAppWindow;
public constructor(@inject('web-crawler') webCrawler: IWebCrawler) {
this.webCrawler = webCrawler;
public constructor(@inject('nhentai-app-window') appWindow: INhentaiAppWindow) {
this.appWindow = appWindow;
}
public isLoggedIn(): Promise<boolean> {
return this.webCrawler
.fetch(`${url}${paths.favorites}`, { redirect: 'manual' })
.then((res: Response) => res.status === HttpCode.OK);
}
public login(name: string, password: string): Promise<void> {
return this.getLoginMeta()
.then((meta: ILoginMeta) => {
const loginParams: ILoginParams = {
...meta,
...{
[usernameInput]: name,
[passwordInput]: password,
},
};
return this.postNHentai(paths.login, {
body: encodeURI(
Object.keys(loginParams)
.map((key: keyof ILoginParams) => `${key}=${loginParams[key]}`)
.join('&')
),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
redirect: 'manual',
});
})
.then(() => {})
.catch(() => Promise.reject(new WebCrawlerLoginError()));
}
private getNHentai(path: string): Promise<Document> {
return this.webCrawler
.fetch(`${url}${path}`)
.then((res: Response) => res.text())
.then((text: string) => {
const { document } = new JSDOM(text).window;
return document;
});
}
private postNHentai(path: string, requestInit: RequestInit = {}): Promise<Response> {
const postUrl = `${url}${path}`;
return this.webCrawler.fetch(postUrl, {
...requestInit,
...{
headers: {
...requestInit.headers,
...{
Host: domain,
Referer: postUrl,
},
},
},
method: 'post',
});
}
private getLoginMeta(): Promise<ILoginMeta> {
return this.getNHentai(paths.login).then((document: Document) => {
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < document.forms.length; i++) {
const form: HTMLFormElement = document.forms[i];
const valueStore: ILoginMeta = {};
let isLoginForm = false;
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let j = 0; j < form.elements.length; j++) {
const input = form.elements[j];
const name = input.getAttribute('name');
if (name === usernameInput || name === passwordInput) {
isLoginForm = true;
} else if (name) {
const value = input.getAttribute('value');
if (value) {
valueStore[name] = value;
}
}
}
if (isLoginForm) {
return valueStore;
}
}
return Promise.reject(new WebCrawlerFormError());
});
public getFavorites(): Promise<NodeJS.ReadableStream> {
return this.appWindow.getFavorites();
}
}

View File

@ -0,0 +1,223 @@
import { WebContents } from 'electron';
import os from 'os';
import path from 'path';
import { Readable } from 'stream';
import { URL } from 'url';
import { createReadStream, remove } from 'fs-extra';
import { injectable } from 'inversify';
import { inject } from '../../core/inject';
import { UrlAppWindow } from '../app-window/url-app-window';
import { WindowClosedError } from '../app-window/window-closed-error';
import { ISessionHelper } from '../session/i-session-helper';
import { INhentaiAppWindow } from './i-nhentai-app-window';
import { IFavorite } from './nhentai';
import {
url as nhentaiUrl,
hostname as nhentaiHostname,
paths as nhentaiPaths,
getFavoritePageUrl,
nextFavoritePageSelector,
coverLinkSelector,
downloadLinkId,
getGalleryId,
} from './nhentai-util';
const waitInterval = 2000;
@injectable()
export class NhentaiAppWindow extends UrlAppWindow implements INhentaiAppWindow {
public constructor(@inject('session-helper') sessionHelper: ISessionHelper) {
super(sessionHelper, nhentaiUrl);
}
/**
* @throws WindowClosedError when this._window is null
*/
public async getFavorites(): Promise<NodeJS.ReadableStream> {
const error = new WindowClosedError();
if (this.isClosed()) {
await this.open();
}
if (!(await this.isLoggedIn())) {
await this.login();
}
if (!this._window) {
throw error;
}
const bookUrls: string[] = [];
for await (const wc of this.getFavoritePageWebContentsGenerator()) {
bookUrls.push(
...((await wc.executeJavaScript(
`Array.from(document.querySelectorAll('${coverLinkSelector}')).map((el) => el.href)`
)) as string[])
);
}
const readable = Readable.from(
(async function* (thisArg): AsyncGenerator<IFavorite> {
for (const bookUrl of bookUrls) {
yield await thisArg.getBookTorrent(bookUrl);
}
})(this),
{
objectMode: true,
}
);
readable.once('close', this.close);
return readable;
}
protected getCsp(): IContentSecurityPolicy {
return {
'default-src': ['nhentai.net'],
'script-src': ['nhentai.net', "'unsafe-eval'"],
'script-src-elem': ['*.nhentai.net', "'unsafe-inline'", '*.google.com', '*.gstatic.com'],
'style-src': [
'*.nhentai.net',
"'unsafe-inline'",
'fonts.googleapis.com',
'cdnjs.cloudflare.com',
'maxcdn.bootstrapcdn.com',
'*.gstatic.com',
],
'img-src': ['*.nhentai.net', 'data:', '*.gstatic.com', '*.google.com'],
'font-src': ['fonts.gstatic.com', 'cdnjs.cloudflare.com', 'maxcdn.bootstrapcdn.com'],
'frame-src': ['*.google.com'],
'connect-src': ['nhentai.net', '*.google.com'],
'worker-src': ['*.google.com'],
};
}
protected onWillNavigate(event: Electron.Event, navigationUrl: string): void {
const url = new URL(navigationUrl);
if (url.hostname !== nhentaiHostname) {
event.preventDefault();
}
}
/**
* @throws WindowClosedError when this._window is null
*/
private async isLoggedIn(): Promise<boolean> {
if (!this._window) {
throw new WindowClosedError();
}
return this._window.webContents
.executeJavaScript(
`fetch('${
nhentaiUrl + nhentaiPaths.favorites
}', {credentials: 'include', redirect: 'manual'}).then((res) => res.status)`
)
.then((status: number) => status === HttpCode.OK);
}
/**
* @throws WindowClosedError when this._window is null
*/
private async login(): Promise<void> {
if (!this._window) {
throw new WindowClosedError();
}
await this.loadUrlSafe(nhentaiUrl + nhentaiPaths.login);
return new Promise((resolve, reject) => {
const timeout = setInterval(() => {
this.isLoggedIn()
.then((loggedIn) => {
if (loggedIn) {
clearTimeout(timeout);
resolve();
}
})
.catch((reason) => reject(reason));
}, waitInterval);
});
}
private async *getFavoritePageWebContentsGenerator(): AsyncGenerator<WebContents, undefined> {
const error = new WindowClosedError();
if (!this._window) {
throw error;
}
await this.loadUrlSafe(getFavoritePageUrl(1));
yield this._window.webContents;
do {
try {
const hasNextPage = (await this._window.webContents.executeJavaScript(
`!!document.querySelector('${nextFavoritePageSelector}')`
)) as boolean;
if (hasNextPage) {
yield new Promise<WebContents>((resolve) => {
if (this._window) {
this._window.webContents.once('did-finish-load', () => {
if (this._window) {
resolve(this._window.webContents);
} else {
throw error;
}
});
void this._window.webContents.executeJavaScript(
`document.querySelector('${nextFavoritePageSelector}').click()`
);
} else {
throw error;
}
});
} else {
break;
}
} catch {
break;
}
} while (true);
return undefined;
}
private async getBookTorrent(bookUrl: string): Promise<IFavorite> {
const galleryId = getGalleryId(bookUrl);
const fileName = `${galleryId}.torrent`;
const filePath = path.resolve(os.tmpdir(), fileName);
await this.loadUrlSafe(bookUrl);
await new Promise<string>((resolve, reject) => {
if (!this._window) {
throw new WindowClosedError();
}
this._window.webContents.session.once('will-download', (event, item) => {
item.setSavePath(filePath);
item.once('done', (doneEvent, state) => {
switch (state) {
case 'completed':
resolve();
break;
case 'cancelled':
case 'interrupted':
default:
reject(new Error(state));
break;
}
});
item.on('updated', () => {
item.resume();
});
});
void this._window.webContents.executeJavaScript(`document.getElementById('${downloadLinkId}').click()`);
});
const readable = createReadStream(filePath, { emitClose: true });
readable.once('close', () => {
void remove(filePath);
});
return {
name: fileName,
torrentFile: readable,
};
}
}

View File

@ -1,26 +1,44 @@
import { dialog } from 'electron';
import path from 'path';
import { createWriteStream } from 'fs-extra';
import { container } from '../../core/container';
import { II18nTranslator } from '../i18n/i-i18n-translator';
import { answer } from '../ipc/annotations/answer';
import { INhentaiApi } from './i-nhentai-api';
import { IFavorite } from './nhentai';
export class NhentaiIpcController implements IIpcController {
private nhentaiApi: INhentaiApi;
private readonly nhentaiApi: INhentaiApi;
public constructor(nhentaiApi: INhentaiApi) {
private readonly translator: II18nTranslator;
private constructor(nhentaiApi: INhentaiApi, translator: II18nTranslator) {
this.nhentaiApi = nhentaiApi;
this.translator = translator;
}
@answer(IpcChannel.LOGIN)
public login(credentials: ICredentials): Promise<void> {
return this.nhentaiApi.login(credentials.name, credentials.password);
}
@answer(IpcChannel.NHENTAI_SAVE_FAVORITES)
public async nhentaiSaveFavorites(): Promise<void> {
const result = await dialog.showOpenDialog({
properties: ['openDirectory'],
title: this.translator.t('Select torrent file save location'),
});
@answer(IpcChannel.LOGGED_IN)
public loggedIn(): Promise<boolean> {
return this.nhentaiApi.isLoggedIn();
const favoritesStream = await this.nhentaiApi.getFavorites();
return new Promise((resolve) => {
favoritesStream.on('data', (favorite: IFavorite) => {
const writable = createWriteStream(path.resolve(result.filePaths[0], favorite.name));
favorite.torrentFile.pipe(writable);
});
favoritesStream.once('close', resolve);
});
}
public get(): NhentaiIpcController {
const nhentaiApi: INhentaiApi = container.get('nhentai-api');
return new NhentaiIpcController(nhentaiApi);
const translator: II18nTranslator = container.get('i18n-translator');
return new NhentaiIpcController(nhentaiApi, translator);
}
}

View File

@ -0,0 +1,26 @@
export const hostname = 'nhentai.net';
export const url = `https://${hostname}/`;
export const paths = {
books: 'g/',
login: 'login/',
favorites: 'favorites/',
};
export const downloadLinkId = 'download';
export const nextFavoritePageSelector = 'a.next';
export const coverLinkSelector = 'a.cover';
export function getFavoritePageUrl(page: number): string {
return `${url + paths.favorites}?page=${page}`;
}
export function getGalleryId(bookUrl: string): string {
const regExpExecArray = /https:\/\/nhentai\.net\/g\/(\d+)/.exec(bookUrl);
if (regExpExecArray && regExpExecArray[1]) {
return regExpExecArray[1];
} else {
throw new TypeError(`could not get nhentai gallery if from the book url ${bookUrl}`);
}
}

4
src/main/modules/nhentai/nhentai.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
export interface IFavorite {
name: string;
torrentFile: NodeJS.ReadableStream;
}

View File

@ -0,0 +1,5 @@
import { BrowserWindow } from 'electron';
export interface ISessionHelper {
setCsp(window: BrowserWindow, csp: IContentSecurityPolicy): void;
}

View File

@ -1,3 +0,0 @@
export interface ISession {
setHeaders(): void;
}

View File

@ -0,0 +1,43 @@
import { injectable } from 'inversify';
import { isDev } from '../../core/env';
import { ISessionHelper } from './i-session-helper';
const defaultCsp: IContentSecurityPolicy = {
'default-src': ["'self'"],
'style-src': ["'unsafe-inline'"],
'object-src': ["'none'"],
};
@injectable()
export class SessionHelper implements ISessionHelper {
private static stringifyCspHeader(csp: IContentSecurityPolicy): string {
return Object.entries(csp)
.map((directive: [string, CspValue[]]) => `${directive[0]} ${directive[1]?.join(' ')}`)
.join('; ');
}
public setCsp(window: Electron.BrowserWindow, csp: IContentSecurityPolicy): void {
const mergedCsp: IContentSecurityPolicy = { ...defaultCsp, ...csp };
if (isDev()) {
mergedCsp['default-src'] = ['devtools:'].concat(mergedCsp['default-src'] ?? []);
mergedCsp['script-src'] = ["'unsafe-eval'"].concat(mergedCsp['script-src'] ?? []);
mergedCsp['script-src-elem'] = ['file:', 'devtools:', "'unsafe-inline'"].concat(
mergedCsp['script-src-elem'] ?? []
);
mergedCsp['style-src'] = ['devtools:', "'unsafe-inline'"].concat(mergedCsp['style-src'] ?? []);
mergedCsp['img-src'] = ['devtools:'].concat(mergedCsp['img-src'] ?? []);
mergedCsp['connect-src'] = ['devtools:', 'data:'].concat(mergedCsp['connect-src'] ?? []);
mergedCsp['worker-src'] = ['devtools:'].concat(mergedCsp['worker-src'] ?? []);
}
window.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': SessionHelper.stringifyCspHeader(mergedCsp),
},
});
});
}
}

23
src/main/modules/session/session.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
type CspValue = '*' | "'none'" | "'self'" | "'unsafe-inline'" | "'unsafe-eval'" | string;
/**
* This interface represents a content security policy.
*
* @see https://www.w3.org/TR/CSP/
* @see https://content-security-policy.com/
*/
interface IContentSecurityPolicy {
'child-src'?: CspValue[];
'connect-src'?: CspValue[];
'default-src'?: CspValue[];
'font-src'?: CspValue[];
'frame-src'?: CspValue[];
'img-src'?: CspValue[];
'media-src'?: CspValue[];
'object-src'?: ["'none'"];
'script-src'?: CspValue[];
'script-src-elem'?: CspValue[];
'script-src-attr'?: CspValue[];
'style-src'?: CspValue[];
'worker-src'?: CspValue[];
}

View File

@ -1,27 +0,0 @@
import { session } from 'electron';
import { injectable } from 'inversify';
import { isDev } from '../../core/env';
import { ISession } from './i-session';
@injectable()
export class Session implements ISession {
public setHeaders(): void {
// these headers only work on web requests, file:// protocol is handled via meta tags in the html
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': isDev()
? [
'default-src devtools:;' +
"script-src 'unsafe-eval';" +
"script-src-elem file: devtools: 'sha256-hl04hLzKBpmsfWF2wIA/0Vs6ZNV5T9ZNFY//3uXrgSk=';" +
"style-src devtools: 'unsafe-inline';" +
'connect-src devtools: data:',
]
: ["default-src 'self';" + "style-src 'unsafe-inline'"],
},
});
});
}
}

View File

@ -1,7 +0,0 @@
import { CookieJar } from 'jsdom';
import { RequestInit, Response } from 'node-fetch';
export interface IWebCrawler {
cookieJar: CookieJar;
fetch(url: string, requestInit?: RequestInit): Promise<Response>;
}

View File

@ -1,64 +0,0 @@
import rewiremock from 'rewiremock';
import '../../../../mocks/electron';
import { expect } from 'chai';
import 'mocha';
import nock from 'nock';
import { Response } from 'node-fetch';
import sinon from 'sinon';
import { container } from '../../core/container';
import { Store } from '../store/store';
import { StoreMock } from '../store/store.mock';
import { IWebCrawler } from './i-web-crawler';
describe('Web Crawler', function () {
this.timeout(2000);
before(() => {
rewiremock.enable();
container.unbind('store');
container.bind('store').to(StoreMock);
});
beforeEach(() => {
if (!nock.isActive()) {
nock.activate();
}
});
afterEach(() => {
nock.cleanAll();
});
after(() => {
rewiremock.disable();
container.unbind('store');
container.bind('store').to(Store);
});
it('fetches websites', async () => {
const callback = sinon.spy();
const testUrl = 'https://example.com';
nock(testUrl)
.get(/.*/)
.reply(
HttpCode.OK,
() => {
callback();
return JSON.stringify([{ id: 12, comment: 'Hey there' }]);
},
{ 'Content-Type': 'application/json' }
)
.persist();
const webCrawler: IWebCrawler = container.get('web-crawler');
const res: Response = await webCrawler.fetch(testUrl);
expect(callback.callCount).to.equal(1, 'multiple requests (or none) are sent when only one should be');
const json = (await res.json()) as unknown;
expect(json).to.deep.equal([{ id: 12, comment: 'Hey there' }], 'response body is incorrect');
});
});

View File

@ -1,69 +0,0 @@
import { injectable } from 'inversify';
import { CookieJar } from 'jsdom';
import nodeFetch, { RequestInit, Response } from 'node-fetch';
import { inject } from '../../core/inject';
import { CookieSaveError } from '../error/cookie-save-error';
import { IStore } from '../store/i-store';
import { IWebCrawler } from './i-web-crawler';
@injectable()
export class WebCrawler implements IWebCrawler {
public cookieJar: CookieJar;
private initialized: boolean;
private store: IStore;
public constructor(@inject('store') store: IStore) {
this.initialized = false;
this.cookieJar = new CookieJar();
this.store = store;
}
public fetch(url: string, requestInit: RequestInit = {}): Promise<Response> {
return this.init().then(() => {
const cookiedInit = {
...requestInit,
...{
headers: {
...requestInit.headers,
...{
Cookie: this.cookieJar.getCookieStringSync(url),
},
},
},
};
return nodeFetch(url, cookiedInit).then((res: Response) => {
this.setCookies(res.headers.raw()['set-cookie'], url).catch((reason: Error) => {
throw new CookieSaveError(reason.message);
});
return res;
});
});
}
private init(): Promise<void> {
if (!this.initialized) {
return this.store.load(StoreKey.COOKIES).then((cookies: unknown) => {
if (cookies !== undefined) {
this.cookieJar = CookieJar.deserializeSync(cookies as string);
}
this.initialized = true;
});
} else {
return Promise.resolve();
}
}
private setCookies(header: string[], url: string): Promise<void> {
if (header) {
header.forEach((cookie: string) => {
this.cookieJar.setCookieSync(cookie, url);
});
return this.store.save(StoreKey.COOKIES, this.cookieJar.serializeSync()).catch((reason: Error) => {
throw new CookieSaveError(reason.message);
});
}
return Promise.resolve();
}
}

View File

@ -1,43 +1,15 @@
<script>
import { onMount } from 'svelte/internal';
import { t } from '../../services/utils';
import Bttn from '../1-atoms/Bttn.svelte';
import { loggedIn } from '../../services/store';
let form = {
name: '',
password: '',
};
import { nhentaiSaveFavorites } from '../../services/api';
function handleClick() {
loggedIn.fetchLogin(form).catch((reason) => {
console.log(reason);
});
nhentaiSaveFavorites();
}
onMount(() => {
loggedIn.fetchIsLoggedIn();
return () => {};
});
</script>
<style></style>
<div class="nhentai-login">
{#if $loggedIn}
<div>{ t('logged in!') }</div>
{:else}
<form class="nhentai-login">
<label>
<span>{ t('Username/Email') }</span>
<input bind:value="{form.name}" />
</label>
<label>
<span>{ t('Password') }</span>
<input bind:value="{form.password}" type="password" />
</label>
<Bttn on:click="{handleClick}">{ t('submit') }</Bttn>
</form>
{/if}
<Bttn on:click="{handleClick}">{ t('Save nhentai Favorites') }</Bttn>
</div>

View File

@ -27,10 +27,6 @@ const ipcClient: IIpcClient = {
},
};
export function login(credentials: ICredentials): Promise<void> {
return ipcClient.ask(IpcChannel.LOGIN, credentials) as Promise<void>;
}
export function isLoggedIn(): Promise<boolean> {
return ipcClient.ask(IpcChannel.LOGGED_IN) as Promise<boolean>;
export function nhentaiSaveFavorites(): Promise<void> {
return ipcClient.ask(IpcChannel.NHENTAI_SAVE_FAVORITES) as Promise<void>;
}

View File

@ -1,21 +0,0 @@
import { writable, Readable } from 'svelte/store';
import * as api from './api';
const { subscribe, set } = writable<boolean>(false);
interface ILoggedIn extends Readable<boolean> {
fetchIsLoggedIn(): Promise<void>;
fetchLogin(credentials: ICredentials): Promise<void>;
}
export const loggedIn: ILoggedIn = {
subscribe,
fetchIsLoggedIn(): Promise<void> {
return api.isLoggedIn().then((isLoggedIn: boolean) => {
set(isLoggedIn);
});
},
fetchLogin(this: ILoggedIn, credentials: ICredentials): Promise<void> {
return api.login(credentials).then(this.fetchIsLoggedIn);
},
};

8
types/ipc.d.ts vendored
View File

@ -1,6 +1,5 @@
declare const enum IpcChannel {
LOGIN = 'LOGIN',
LOGGED_IN = 'LOGGED_IN',
NHENTAI_SAVE_FAVORITES = 'NHENTAI_SAVE_FAVORITES',
}
interface IIpcPayload {
@ -16,11 +15,6 @@ interface IIpcResponse {
error?: string;
}
interface ICredentials {
name: string;
password: string;
}
interface IIpcClient {
ask: (channel: IpcChannel, data?: unknown) => Promise<unknown>;
}