Initial commit

This commit is contained in:
2022-07-05 00:43:09 +02:00
parent 52d0a3f65d
commit 1107184ca0
5 changed files with 802 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
# Project specific
Download/**/**
*.conf
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
+43
View File
@@ -1,2 +1,45 @@
# WNtoEmail
Node.js script to download new WebNovel chapters convert them to eBook and send to email. Mainly intended for sending to Kindle.
Script will pack your WebNovels into convinient eBooks, complete with cover image, metadata and table of contents. For ongoing series if will wait for a configured number of new chapters before sending a new volume to avoid spam.
# Dependecies
It's a Node.js project so install that.
Project is using `node-html-parser` and `nodemailer` libraries. It should be enough to:
```
npm install node-html-parser --save
npm install nodemailer --save
```
# Config
At first start it will create an empty config file `./novelConfig.conf`, adjust the setting according to comments below
```
{
"downloadLocation": "", // New chapter download location
"converterPath": "ebook-convert.exe", // Calibre eBook converter, I recommend adding it to you PATH, NOT tested when it's not in PATH
"ebookFormat": "epub", // Desired eBook format, Kindle started supporting epub dso that's default
"sendEmail": false, // If the script should send eBooks via email
"emailToAddress": "", // Email where to send your eBooks
"emailFromAddress": "", // Important for Kindle deliveries, make sure you have it added in Kindle settings
"emailProvider": "", // Gmail works fine, just need to set up 2FA and an app password
"emailUsername": "", // Usernam to your email account
"emailPassword": "", // Password to your email account
"emailAttachments": 25, // How many eBooks to attach to a single email
"supportedHosting": { // Enum to show which WebNovel host sites are supported, NOT configurable
"NF": "https://novelfull.com/"
},
"template": { // Template for a WebNovel entry in "novels" below
"novelURL": "", // WebNovel address, it's enough to copy this template to "novels" and fill only this field to start
"title": "", // Autofill
"author": "", // Autofill
"coverURL": "", // Autofill
"lastChapterURL": false, // Autofill; Can be used if not starting from the first chapter, first chapter downloaded will be NEXT from this
"lastVolume": 0, // Autofill; Can be used if not starting from the first chapter, first eBook number created will be NEXT from this
"completed": false, // Autofill; Set to false with settings above to download chapters again
"hosting": "NF", // Hosting code, see "supportedHosting"
"volumeChapterCount": 5, // After how many new/unread chapters to send a new eBook, ignored if WebNovel is completed
"completedVolumeChapterCount": 50, // How many chapters to pack per eBook
"redownload": false // TODO: redownload all chapters, repack into volumes with completedVolumeChapterCount, do not send via email, intended for completed series archiving
},
"novels": [] // Table of
}
```
+406
View File
@@ -0,0 +1,406 @@
const fs = require('fs');
const path = require('path');
const HTMLparser = require('node-html-parser');
const exec = require("child_process").execSync;
const nodemailer = require('nodemailer');
var transporter;
var config;
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function cleanPath(pathToClean) {
let regex = /[<>:"|\?\*]/g;
let cleanPath = path.normalize(pathToClean);
let isAbsolute = path.isAbsolute(cleanPath);
cleanPath = cleanPath.replace(regex, '');
if (/^win/i.test(process.platform) && isAbsolute) {
cleanPath = `${cleanPath.slice(0, 1)}:${cleanPath.slice(1)}`
}
return cleanPath;
}
function writeFile(dir, file, data) {
let cleanDir = cleanPath(dir);
let cleanFile = cleanPath(file);
if (!fs.existsSync(cleanDir)) {
fs.mkdirSync(cleanDir, { recursive: true });
}
fs.writeFileSync(`${cleanDir}/${cleanFile}`, data, function (err) {
if (err != null) console.log(err);
return err != null;
});
}
function readFile(file) {
let fileContent;
try {
fileContent = fs.readFileSync(cleanPath(file), 'utf8');
}
catch (err) {
fileContent = false;
}
return fileContent;
}
function loadConfig() {
let novelConfigDefault = {
"downloadLocation": "",
"converterPath": "ebook-convert.exe",
"ebookFormat": "epub",
"sendEmail": false,
"emailToAddress": "",
"emailFromAddress": "",
"emailProvider": "",
"emailUsername": "",
"emailPassword": "",
"emailAttachments": 25,
"supportedHosting": {
"NF": "https://novelfull.com/"
},
"template": {
"novelURL": "",
"title": "",
"author": "",
"coverURL": "",
"lastChapterURL": false,
"lastVolume": 0,
"completed": false,
"hosting": "NF",
"volumeChapterCount": 5,
"completedVolumeChapterCount": 50,
"redownload": false // TODO: redownload all chapters, repack into volumes with completedVolumeChapterCount, do not send via email, intended for completed series archiving
},
"novels": []
};
let configRead = readFile('./novelConfig.conf');
if (configRead) {
config = JSON.parse(configRead);
transporter = nodemailer.createTransport({
service: config['emailProvider'], // no need to set host or port etc.
auth: {
user: config['emailUsername'],
pass: config['emailPassword']
}
});
writeFile('.', 'novelConfig.conf', JSON.stringify(config, null, 4));
}
else {
writeFile('.', 'novelConfig.conf', JSON.stringify(novelConfigDefault, null, 4));
config = novelConfigDefault;
}
console.log(config)
}
function saveConfig() {
writeFile('.', 'novelConfig.conf', JSON.stringify(config, null, 4));
}
async function convertEbook(dir, file, params = { "cover": false, "authors": false, "title": false }, format = 'html') {
let file1Path = cleanPath(`${dir}/${file}.${format}`);
let file2Path = cleanPath(`${dir}/${file}.${config['ebookFormat']}`);
let convertParams = ' --use-auto-toc';
//convertParams += format2 == 'epub' ? ' --epub-inline-toc' : '';
convertParams += params['cover'] ? ` --cover "${params['cover']}"` : '';
convertParams += params['authors'] ? ` --authors "${params['authors']}"` : '';
convertParams += params['title'] ? ` --title "${params['title']}"` : '';
exec(`${config['converterPath']} "${file1Path}" "${file2Path}"${convertParams}`, (error, stdout, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
console.log(`stdout: ${stdout}`);
});
}
async function sendEbook(subject, ebookAttachments) { //(dir, ebook, format = 'epub') {
if (config['sendEmail']) {
let splicedAttachments = [];
while (ebookAttachments.length > config['emailAttachments']) {
splicedAttachments.push(ebookAttachments.splice(0, config['emailAttachments']))
}
if (ebookAttachments.length > 0) {
splicedAttachments.push(ebookAttachments)
}
for (i = 0; i < splicedAttachments.length; ++i) {
let message = {
from: config['emailFromAddress'],
to: config['emailToAddress'],
subject: subject + ' part ' + i,
text: subject + ' part ' + i,
attachments: splicedAttachments[i]
}
await transporter.sendMail(message, (err) => {
if (err)
console.log(err);
else
console.log(`Sent volume ${ebook}`);
});
}
}
}
async function fetch_smth(URL, hosting) {
let fetchURL = await fetch(URL);
if (fetchURL.ok) {
let response = await fetchURL.text();
let info;
return info;
}
else return fetchURL.ok;
}
async function fetchNovelInfo(URL, hosting) {
let fetchURL = await fetch(URL);
if (fetchURL.ok) {
let response = await fetchURL.text();
let novelInfo = getNovelInfo(response, hosting);
return novelInfo;
}
else return fetchURL.ok;
}
async function fetchChapter(URL, hosting) {
let fetchURL = await fetch(URL);
if (fetchURL.ok) {
let response = await fetchURL.text();
let nextChapterURL = getNextChapterURL(response, hosting);
let chapterContent = getChapterContent(response, hosting);
return [chapterContent, nextChapterURL];
}
else return fetchURL.ok;
}
function get(response, hosting) {
let html = HTMLparser.parse(response);
let info;
switch (hosting) {
case 'NF':
info = html;
break;
default:
info = false;
}
return info;
}
async function getNovelInfo(response, hosting) {
let html = HTMLparser.parse(response);
let info;
switch (hosting) {
case 'NF':
let title = html.querySelector('h3.title').innerText;
let author = html.querySelector('#truyen > div.csstransforms3d > div > div.col-xs-12.col-info-desc > div.col-xs-12.col-sm-4.col-md-4.info-holder > div.info > div:nth-child(1) > a:nth-child(2)').innerText;
let completed = html.querySelector('#truyen > div.csstransforms3d > div > div.col-xs-12.col-info-desc > div.col-xs-12.col-sm-4.col-md-4.info-holder > div.info > div:nth-child(5) > a').innerText == "Completed";
let firstChapterURL = html.querySelector('#list-chapter > div.row > div:nth-child(1) > ul > li:nth-child(1) > a').attrs['href'];
let coverURL = html.querySelector('div.book > img').attrs['src'];
info = [title, author, completed, 'https://novelfull.com' + firstChapterURL, 'https://novelfull.com' + coverURL];
break;
default:
info = false;
}
return info;
}
function getNextChapterURL(response, hosting) {
let html = HTMLparser.parse(response);
let nextChapterURL;
switch (hosting) {
case 'NF':
nextChapterURL = undefined == html.querySelector('a#next_chap').attrs['href'] ? false : 'https://novelfull.com' + html.querySelector('a#next_chap').attrs['href'];
break;
default:
nextChapterURL = false;
}
return nextChapterURL;
}
function getChapterContent(response, hosting) {
let html = HTMLparser.parse(response);
let chapterContent = '';
switch (hosting) {
case 'NF':
chapterContent += '<h1 class="chapter">' + html.querySelector('span.chapter-text').innerText + '</h3>';
html.querySelectorAll('div#chapter-content p').forEach(element => {
chapterContent += element.outerHTML;
});
break;
default:
chapterContent = false;
}
return chapterContent;
}
async function main() {
loadConfig();
// let novelDirConvert = 'C:/Users/mateu/bin/LightNovel/' + 'Warlock of the Magus World';
// let fileName = ((1 + 1) < 10 ? '0' + (1 + 1) : (1 + 1)) + '. ' + 'The Wizard World' + '; ' + 'Get Lost';
// let fileName = `${(1 + 1) < 10 ? '0' + (1 + 1) : (1 + 1)}. ${'The Wizard World'}; ${'Get Lost'}`;
// let fileName = `21. Warlock of the Magus World; The Plagiarist`;
// convertEbook(novelDirConvert, fileName);
// await sendEbook(novelDirConvert, fileName);
for (i = 0; i < config['novels'].length; ++i) {
let novel = clone(config['novels'][i]);
let chapters = [];
let nextChapterURL;
if (!novel['completed']) {
let novelInfo = await fetchNovelInfo(novel['novelURL'], 'NF');
novel['title'] = novelInfo[0];
novel['author'] = novelInfo[1];
novel['completed'] = novelInfo[2];
novel['coverURL'] = novelInfo[4];
config['novels'][i] = clone(novel);
saveConfig();
if (!novel['lastChapterURL']) {
novel['lastChapterURL'] = novelInfo[3];
let chapter = await fetchChapter(novelInfo[3], 'NF');
console.log('Download chapter ' + chapters.length + ' ' + novelInfo[3]);
chapters.push(chapter);
}
let novelDir = `${config['downloadLocation']}/${novel['title']}`;
const nextChapterURLtemp = await fetchChapter(novel['lastChapterURL'], 'NF');
nextChapterURL = nextChapterURLtemp[1];
while (nextChapterURL) {
novel['lastChapterURL'] = nextChapterURL;
let chapter = await fetchChapter(nextChapterURL, 'NF');
console.log('Download chapter ' + chapters.length + ' ' + nextChapterURL);
chapters.push(chapter);
nextChapterURL = chapter[1];
}
// writeFile(novelDir, 'chapters.json`, JSON.stringify(chapters))
//chapters = JSON.parse(readFile("C:\\Users\\mateu\\bin\\LightNovel\\chapters.json"))
let startVol = novel['lastVolume'];
let totalChapters = chapters.length;
const maxVolume = novel['completed'] ? startVol + 1 + Math.floor(totalChapters / novel['completedVolumeChapterCount']) : startVol + Math.floor(totalChapters / novel['completedVolumeChapterCount']);
let ebookAttachments = [];
for (vol = startVol; vol < maxVolume; vol++) {
let volContent = '';
for (chap = 0; chap < novel['completedVolumeChapterCount'] && chap < chapters.length; chap++) {
volContent += chapters[chap][0] + '\n';
}
novel['lastChapterURL'] = chapters[(chap - 2 < 0 ? 0 : chap - 2)][1];
novel['lastVolume'] = vol + 1;
config['novels'][i] = clone(novel);
saveConfig();
let novelFileName = `${(vol + 1) < 10 ? '0' + (vol + 1) : (vol + 1)}. ${novel['title']}; ${novel['author']}`;
writeFile(novelDir, `${novelFileName}.html`, volContent);
console.log(`Saved volume: ${novelFileName}`);
await convertEbook(novelDir, novelFileName, {
cover: novel['coverURL'],
authors: novel['author'],
title: `${(vol + 1) < 10 ? '0' + (vol + 1) : (vol + 1)}. ${novel['title']}`
});
ebookAttachments.push({
name: cleanPath(`${novelFileName}.${config['ebookFormat']}`),
path: cleanPath(`${novelDir}/${novelFileName}.${config['ebookFormat']}`)
});
chapters.splice(0, chap);
}
await sendEbook(novel['title'], ebookAttachments);
ebookAttachments = [];
startVol = novel['lastVolume'];
totalChapters = chapters.length;
for (vol = startVol; vol < startVol + Math.floor(totalChapters / novel['volumeChapterCount']); vol++) {
let volContent = '';
for (chap = 0; chap < novel['volumeChapterCount'] && chap < chapters.length; chap++) {
volContent += chapters[chap][0] + '\n';
}
novel['lastChapterURL'] = chapters[(chap - 2 < 0 ? 0 : chap - 2)][1];
novel['lastVolume'] = vol + 1;
config['novels'][i] = clone(novel);
saveConfig();
let novelFileName = `${(vol + 1) < 10 ? '0' + (vol + 1) : (vol + 1)}. ${novel['title']}; ${novel['author']}`;
writeFile(novelDir, `${novelFileName}.html`, volContent);
console.log(`Saved volume: ${novelFileName}`);
await convertEbook(novelDir, novelFileName, {
cover: novel['coverURL'],
authors: novel['author'],
title: `${(vol + 1) < 10 ? '0' + (vol + 1) : (vol + 1)}. ${novel['title']}`
});
ebookAttachments.push({
name: cleanPath(`${novelFileName}.${config['ebookFormat']}`),
path: cleanPath(`${novelDir}/${novelFileName}.${config['ebookFormat']}`)
});
chapters.splice(0, chap);
}
await sendEbook(novel['title'], ebookAttachments);
}
}
}
main();
+214
View File
@@ -0,0 +1,214 @@
{
"name": "LightNovel",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"node-html-parser": "^5.3.3"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
},
"node_modules/css-select": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
"integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.0.1",
"domhandler": "^4.3.1",
"domutils": "^2.8.0",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
"dependencies": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
]
},
"node_modules/domhandler": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
"dependencies": {
"domelementtype": "^2.2.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
"dependencies": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"bin": {
"he": "bin/he"
}
},
"node_modules/node-html-parser": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.3.3.tgz",
"integrity": "sha512-ncg1033CaX9UexbyA7e1N0aAoAYRDiV8jkTvzEnfd1GDvzFdrsXLzR4p4ik8mwLgnaKP/jyUFWDy9q3jvRT2Jw==",
"dependencies": {
"css-select": "^4.2.1",
"he": "1.2.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
}
},
"dependencies": {
"boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
},
"css-select": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
"integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
"requires": {
"boolbase": "^1.0.0",
"css-what": "^6.0.1",
"domhandler": "^4.3.1",
"domutils": "^2.8.0",
"nth-check": "^2.0.1"
}
},
"css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
},
"dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
},
"domhandler": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
"requires": {
"domelementtype": "^2.2.0"
}
},
"domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
}
},
"entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"node-html-parser": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.3.3.tgz",
"integrity": "sha512-ncg1033CaX9UexbyA7e1N0aAoAYRDiV8jkTvzEnfd1GDvzFdrsXLzR4p4ik8mwLgnaKP/jyUFWDy9q3jvRT2Jw==",
"requires": {
"css-select": "^4.2.1",
"he": "1.2.0"
}
},
"nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"requires": {
"boolbase": "^1.0.0"
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"dependencies": {
"node-html-parser": "^5.3.3"
}
}