first commit: decoding doesn't work well yet
This commit is contained in:
commit
d5b359191c
113
.gitignore
vendored
Normal file
113
.gitignore
vendored
Normal file
@ -0,0 +1,113 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-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-temporary-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/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# 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 variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
database.sqlite
|
||||
*.par2
|
||||
*.nzb
|
||||
files
|
||||
*.bin
|
||||
1266
package-lock.json
generated
Normal file
1266
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "usenet-indexer",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bullmq": "^5.77.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"ioredis": "^5.3.2",
|
||||
"log4js": "^6.9.1",
|
||||
"nntp-js": "^1.0.4",
|
||||
"node-unrar-js": "^2.0.0",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^6.0.1",
|
||||
"xmlbuilder2": "^3.1.1",
|
||||
"yencode": "^1.0.1"
|
||||
}
|
||||
}
|
||||
58
src/body.worker.js
Normal file
58
src/body.worker.js
Normal file
@ -0,0 +1,58 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import log4js from './logger.js';
|
||||
import { headerQueue } from './header.worker.js';
|
||||
import { acquire, release } from './nntp.pool.js';
|
||||
|
||||
const bodyLogger = log4js.getLogger('body');
|
||||
|
||||
const connection = {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
};
|
||||
|
||||
export const bodyQueue = new Queue('body-queue', { connection });
|
||||
|
||||
const YENC_REGEX = /=ybegin part=(\d+) total=(\d+) line=\d+ size=\d+ name=(.+)/;
|
||||
|
||||
export const startBodyWorker = () => {
|
||||
const bodyWorker = new Worker('body-queue', async job => {
|
||||
const { header } = job.data;
|
||||
bodyLogger.debug(`Processing header with unparsable subject: ${header.subject}`);
|
||||
|
||||
let conn;
|
||||
try {
|
||||
conn = await acquire();
|
||||
const bodyBuffer = (await conn.body(header['message-id'])).data;
|
||||
|
||||
const firstNewlineIndex = bodyBuffer.indexOf('\\n');
|
||||
const firstLineBuffer = (firstNewlineIndex !== -1) ? bodyBuffer.slice(0, firstNewlineIndex) : bodyBuffer;
|
||||
const firstLine = firstLineBuffer.toString();
|
||||
|
||||
const match = firstLine.match(YENC_REGEX);
|
||||
|
||||
if (match) {
|
||||
const part = parseInt(match[1], 10);
|
||||
const total = parseInt(match[2], 10);
|
||||
const filename = match[3].trim();
|
||||
|
||||
const newSubject = `"${filename}" yEnc (${part}/${total})`;
|
||||
header.subject = newSubject;
|
||||
|
||||
bodyLogger.info(`Found yEnc metadata in body. New subject: ${newSubject}`);
|
||||
await headerQueue.add('process-header', header);
|
||||
} else {
|
||||
bodyLogger.warn(`Could not find yEnc metadata in body for header: ${header.subject}`);
|
||||
}
|
||||
} catch (error) {
|
||||
bodyLogger.error('Error in body worker:', error);
|
||||
} finally {
|
||||
if (conn) {
|
||||
release(conn);
|
||||
}
|
||||
}
|
||||
}, { connection });
|
||||
|
||||
bodyWorker.on('failed', (job, err) => {
|
||||
bodyLogger.error(`Body job ${job.id} failed with error: ${err.message}`);
|
||||
});
|
||||
};
|
||||
72
src/collection.worker.js
Normal file
72
src/collection.worker.js
Normal file
@ -0,0 +1,72 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import log4js from './logger.js';
|
||||
import { getDb } from './database.js';
|
||||
import { acquire, release } from './nntp.pool.js';
|
||||
import { createExtractorFromData } from 'node-unrar-js';
|
||||
import * as yEnc from 'simple-yenc';
|
||||
|
||||
const logger = log4js.getLogger('collection');
|
||||
|
||||
const connection = {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
};
|
||||
|
||||
export const collectionQueue = new Queue('collection-queue', { connection });
|
||||
|
||||
const RAR_REGEX = /\.part0*1\.rar$/;
|
||||
|
||||
export const startCollectionWorker = () => {
|
||||
const collectionWorker = new Worker('collection-queue', async job => {
|
||||
const { fileId } = job.data;
|
||||
logger.debug(`Processing file ID ${fileId} for collection.`);
|
||||
|
||||
const db = await getDb();
|
||||
const file = await db.get('SELECT * FROM files WHERE id = ?', fileId);
|
||||
|
||||
if (!file) {
|
||||
logger.error(`File with ID ${fileId} not found in the database.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (RAR_REGEX.test(file.filename)) {
|
||||
logger.info(`File "${file.filename}" is the first part of a RAR set.`);
|
||||
|
||||
const messageIds = JSON.parse(file.message_ids);
|
||||
const firstPart = messageIds['1'];
|
||||
|
||||
if (!firstPart || !firstPart.id) {
|
||||
logger.error(`Could not find message ID for the first part of file "${file.filename}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
let conn;
|
||||
try {
|
||||
conn = await acquire();
|
||||
await conn.group('alt.binaries.test');
|
||||
const bodyBuffer = (await conn.body(`<${firstPart.id}>`)).data;
|
||||
const decodedUint8Array = yEnc.decode(bodyBuffer.toString('latin1'));
|
||||
const buffer = Buffer.from(decodedUint8Array);
|
||||
const extractor = await createExtractorFromData({ data: buffer });
|
||||
const fileList = extractor.getFileList();
|
||||
logger.info(`Files in "${file.filename}":`, fileList);
|
||||
} catch (error) {
|
||||
if (error.code === 430) {
|
||||
logger.error(`Article not found for first part of RAR set (Message ID: ${firstPart.id})`);
|
||||
} else {
|
||||
logger.error('Error processing RAR file:', error);
|
||||
}
|
||||
} finally {
|
||||
if (conn) {
|
||||
release(conn);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug(`File "${file.filename}" is not the first part of a RAR set.`);
|
||||
}
|
||||
}, { connection });
|
||||
|
||||
collectionWorker.on('failed', (job, err) => {
|
||||
logger.error(`Collection job ${job.id} failed with error: ${err.message}`);
|
||||
});
|
||||
};
|
||||
26
src/database.js
Normal file
26
src/database.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { open } from 'sqlite';
|
||||
import sqlite3 from 'sqlite3';
|
||||
|
||||
let db;
|
||||
|
||||
export const getDb = async () => {
|
||||
if (!db) {
|
||||
db = await open({
|
||||
filename: './database.sqlite',
|
||||
driver: sqlite3.Database,
|
||||
});
|
||||
|
||||
await db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
filename TEXT NOT NULL,
|
||||
poster TEXT NOT NULL,
|
||||
date INTEGER NOT NULL,
|
||||
parts INTEGER NOT NULL,
|
||||
message_ids TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
}
|
||||
return db;
|
||||
};
|
||||
72
src/download.js
Normal file
72
src/download.js
Normal file
@ -0,0 +1,72 @@
|
||||
import 'dotenv/config';
|
||||
import { getDb } from './database.js';
|
||||
import { acquire, release, shutdown } from './nntp.pool.js';
|
||||
import * as yEnc from 'simple-yenc';
|
||||
import fs from 'fs/promises';
|
||||
import log4js from './logger.js';
|
||||
|
||||
const logger = log4js.getLogger('download');
|
||||
|
||||
async function downloadFile(fileId) {
|
||||
const db = await getDb();
|
||||
const file = await db.get('SELECT * FROM files WHERE id = ?', fileId);
|
||||
|
||||
if (!file) {
|
||||
logger.error(`File with ID ${fileId} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Downloading file: ${file.filename}`);
|
||||
|
||||
const messageIds = JSON.parse(file.message_ids);
|
||||
const sortedParts = Object.entries(messageIds).sort(([a], [b]) => parseInt(a, 10) - parseInt(b, 10));
|
||||
|
||||
const parts = [];
|
||||
let conn;
|
||||
|
||||
try {
|
||||
conn = await acquire();
|
||||
await conn.group('alt.binaries.test');
|
||||
for (const [partNumber, segment] of sortedParts) {
|
||||
if (!segment || !segment.id) {
|
||||
logger.error(`Message ID for part ${partNumber} not found.`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
logger.debug(`Downloading part ${partNumber}/${file.parts} with message ID: ${segment.id}`);
|
||||
const bodyBuffer = (await conn.body(`<${segment.id}>`)).data;
|
||||
const decodedUint8Array = yEnc.decode(bodyBuffer.toString('latin1'));
|
||||
const buffer = Buffer.from(decodedUint8Array);
|
||||
parts.push(buffer);
|
||||
} catch (error) {
|
||||
if (error.code === 430) {
|
||||
logger.error(`Article not found for part ${partNumber} (Message ID: ${segment.id})`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error downloading file parts:', error);
|
||||
} finally {
|
||||
if (conn) {
|
||||
release(conn);
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.length === file.parts) {
|
||||
const completeFile = Buffer.concat(parts);
|
||||
await fs.writeFile(file.filename, completeFile);
|
||||
logger.info(`File "${file.filename}" downloaded successfully.`);
|
||||
} else {
|
||||
logger.error('Could not download all parts of the file.');
|
||||
}
|
||||
}
|
||||
|
||||
const fileId = parseInt(process.argv[2], 10);
|
||||
if (isNaN(fileId)) {
|
||||
logger.error('Please provide a valid file ID as a command-line argument.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
downloadFile(fileId).finally(() => shutdown());
|
||||
63
src/file.worker.js
Normal file
63
src/file.worker.js
Normal file
@ -0,0 +1,63 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import log4js from './logger.js';
|
||||
import { getDb } from './database.js';
|
||||
import { collectionQueue } from './collection.worker.js';
|
||||
|
||||
const logger = log4js.getLogger('file');
|
||||
|
||||
const connection = {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
};
|
||||
|
||||
export const fileQueue = new Queue('file-queue', { connection });
|
||||
|
||||
export const startFileWorker = () => {
|
||||
const fileWorker = new Worker('file-queue', async job => {
|
||||
const { filename, parts } = job.data;
|
||||
const partCount = Object.keys(parts).length;
|
||||
logger.debug(`Processing complete file: "${filename}" with ${partCount} parts.`);
|
||||
|
||||
const firstPart = JSON.parse(Object.values(parts)[0]);
|
||||
const poster = firstPart.from;
|
||||
const date = new Date(firstPart.date).getTime();
|
||||
|
||||
const messageIds = Object.entries(parts).reduce((acc, [partNumber, partData]) => {
|
||||
const part = JSON.parse(partData);
|
||||
const messageId = part['message-id'];
|
||||
if (messageId) {
|
||||
acc[partNumber] = {
|
||||
id: messageId.replace(/[<>]/g, ''),
|
||||
size: part[':bytes'],
|
||||
};
|
||||
} else {
|
||||
logger.warn(`Message ID not found for part ${partNumber} of file "${filename}"`);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (Object.keys(messageIds).length !== partCount) {
|
||||
throw new Error(`Could not process all parts for file "${filename}" due to missing message IDs.`);
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const result = await db.run(
|
||||
'INSERT INTO files (filename, poster, date, parts, message_ids) VALUES (?, ?, ?, ?, ?)',
|
||||
filename,
|
||||
poster,
|
||||
date,
|
||||
partCount,
|
||||
JSON.stringify(messageIds)
|
||||
);
|
||||
|
||||
const fileId = result.lastID;
|
||||
logger.debug(`Saved file "${filename}" to database with ID: ${fileId}`);
|
||||
|
||||
await collectionQueue.add('process-collection', { fileId });
|
||||
logger.debug(`Added file ID ${fileId} to collection queue.`);
|
||||
}, { connection });
|
||||
|
||||
fileWorker.on('failed', (job, err) => {
|
||||
logger.error(`File job ${job.id} failed with error: ${err.message}`);
|
||||
});
|
||||
};
|
||||
54
src/header.worker.js
Normal file
54
src/header.worker.js
Normal file
@ -0,0 +1,54 @@
|
||||
import { Queue, Worker } from 'bullmq';
|
||||
import Redis from 'ioredis';
|
||||
import log4js from './logger.js';
|
||||
import { fileQueue } from './file.worker.js';
|
||||
import { bodyQueue } from './body.worker.js';
|
||||
|
||||
const logger = log4js.getLogger('header');
|
||||
|
||||
const connection = {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
};
|
||||
|
||||
const redis = new Redis(connection);
|
||||
|
||||
export const headerQueue = new Queue('header-queue', { connection });
|
||||
|
||||
const SUBJECT_REGEX = /"(.+)"(?: yEnc)? \((\d+)\/(\d+)\)/;
|
||||
|
||||
export const startHeaderWorker = () => {
|
||||
const headerWorker = new Worker('header-queue', async job => {
|
||||
const header = job.data;
|
||||
const subject = header.subject;
|
||||
|
||||
const match = subject.match(SUBJECT_REGEX);
|
||||
|
||||
if (match) {
|
||||
const filename = match[1];
|
||||
const part = parseInt(match[2], 10);
|
||||
const total = parseInt(match[3], 10);
|
||||
|
||||
const fileKey = `file:${filename}`;
|
||||
await redis.hset(fileKey, part, JSON.stringify(header));
|
||||
|
||||
const partCount = await redis.hlen(fileKey);
|
||||
|
||||
if (partCount === total) {
|
||||
const fileParts = await redis.hgetall(fileKey);
|
||||
await fileQueue.add('process-file', { filename, parts: fileParts });
|
||||
await redis.del(fileKey);
|
||||
logger.info(`File "${filename}" is complete and moved to file-queue.`, fileParts);
|
||||
} else {
|
||||
logger.info(`Stored part ${part}/${total} for file "${filename}"`);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Could not parse subject: "${subject}". Moving to body-queue.`);
|
||||
await bodyQueue.add('process-body', { header });
|
||||
}
|
||||
}, { connection });
|
||||
|
||||
headerWorker.on('failed', (job, err) => {
|
||||
logger.error(`Header job ${job.id} failed with error: ${err.message}`);
|
||||
});
|
||||
};
|
||||
53
src/index.js
Normal file
53
src/index.js
Normal file
@ -0,0 +1,53 @@
|
||||
import 'dotenv/config';
|
||||
import { headerQueue, startHeaderWorker } from './header.worker.js';
|
||||
import { startFileWorker } from './file.worker.js';
|
||||
import { startCollectionWorker } from './collection.worker.js';
|
||||
import { startBodyWorker } from './body.worker.js';
|
||||
import log4js from './logger.js';
|
||||
import { acquire, release, shutdown } from './nntp.pool.js';
|
||||
|
||||
const logger = log4js.getLogger();
|
||||
|
||||
async function main() {
|
||||
let conn;
|
||||
try {
|
||||
conn = await acquire();
|
||||
logger.info('NNTP connection acquired from pool.');
|
||||
|
||||
logger.debug(`Server date: ${await conn.date()}`);
|
||||
|
||||
const group = await conn.group('alt.binaries.test');
|
||||
logger.debug(`Group info: ${JSON.stringify(group)}`);
|
||||
|
||||
const overview = await conn.xover(group.first, group.group);
|
||||
logger.info(`Fetched ${overview.overviews.length} headers.`);
|
||||
|
||||
for (const [id, header] of overview.overviews) {
|
||||
await headerQueue.add('process-header', header);
|
||||
}
|
||||
|
||||
if (overview.overviews.length > 0) {
|
||||
const lastId = overview.overviews[overview.overviews.length - 1][0];
|
||||
logger.info(`Last header ID queued: ${lastId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in main execution:', error);
|
||||
} finally {
|
||||
if (conn) {
|
||||
release(conn);
|
||||
logger.info('NNTP connection released back to the pool.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startHeaderWorker();
|
||||
startFileWorker();
|
||||
startCollectionWorker();
|
||||
startBodyWorker();
|
||||
main();
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
logger.info('Gracefully shutting down...');
|
||||
await shutdown();
|
||||
process.exit(0);
|
||||
});
|
||||
26
src/logger.js
Normal file
26
src/logger.js
Normal file
@ -0,0 +1,26 @@
|
||||
import log4js from 'log4js';
|
||||
import fs from 'fs';
|
||||
|
||||
const logsDir = './logs';
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir);
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/:/g, '-');
|
||||
|
||||
log4js.configure({
|
||||
appenders: {
|
||||
console: { type: 'console' },
|
||||
file: { type: 'file', filename: `${logsDir}/${timestamp}.log` },
|
||||
},
|
||||
categories: {
|
||||
default: { appenders: ['console', 'file'], level: 'debug' },
|
||||
header: { appenders: ['console', 'file'], level: 'info' },
|
||||
file: { appenders: ['console', 'file'], level: 'info' },
|
||||
collection: { appenders: ['console', 'file'], level: 'info' },
|
||||
body: { appenders: ['console', 'file'], level: 'info' },
|
||||
pool: { appenders: ['console', 'file'], level: 'info' },
|
||||
},
|
||||
});
|
||||
|
||||
export default log4js;
|
||||
59
src/nntp.pool.js
Normal file
59
src/nntp.pool.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { NNTP } from 'nntp-js';
|
||||
import log4js from './logger.js';
|
||||
|
||||
const logger = log4js.getLogger('pool');
|
||||
|
||||
const POOL_SIZE = 5;
|
||||
const connections = [];
|
||||
const queue = [];
|
||||
|
||||
const createConnection = async () => {
|
||||
const config = {
|
||||
host: process.env.NNTP_HOST,
|
||||
user: process.env.NNTP_USER,
|
||||
password: process.env.NNTP_PASS,
|
||||
port: 443,
|
||||
secure: true,
|
||||
};
|
||||
|
||||
const conn = new NNTP(config.host, 119);
|
||||
await conn.connect();
|
||||
await conn.login(config.user, config.password);
|
||||
return conn;
|
||||
};
|
||||
|
||||
export const acquire = async () => {
|
||||
if (connections.length > 0) {
|
||||
logger.debug('Reusing existing connection from pool.');
|
||||
return connections.pop();
|
||||
}
|
||||
|
||||
if (connections.length + queue.length < POOL_SIZE) {
|
||||
logger.info('Creating new connection.');
|
||||
return createConnection();
|
||||
}
|
||||
|
||||
logger.info('Waiting for a connection to become available.');
|
||||
return new Promise(resolve => queue.push(resolve));
|
||||
};
|
||||
|
||||
export const release = conn => {
|
||||
if (queue.length > 0) {
|
||||
logger.info('Releasing connection to a waiting consumer.');
|
||||
const resolve = queue.shift();
|
||||
resolve(conn);
|
||||
} else {
|
||||
logger.debug('Returning connection to the pool.');
|
||||
connections.push(conn);
|
||||
}
|
||||
};
|
||||
|
||||
export const shutdown = async () => {
|
||||
logger.info('Shutting down all connections in the pool.');
|
||||
const allConns = [...connections];
|
||||
connections.length = 0; // Clear the pool
|
||||
|
||||
for (const conn of allConns) {
|
||||
await conn.quit();
|
||||
}
|
||||
};
|
||||
55
src/nzb.js
Normal file
55
src/nzb.js
Normal file
@ -0,0 +1,55 @@
|
||||
import 'dotenv/config';
|
||||
import { getDb } from './database.js';
|
||||
import { create } from 'xmlbuilder2';
|
||||
import fs from 'fs/promises';
|
||||
import log4js from './logger.js';
|
||||
|
||||
const logger = log4js.getLogger('nzb');
|
||||
|
||||
async function createNzb(fileId) {
|
||||
const db = await getDb();
|
||||
const file = await db.get('SELECT * FROM files WHERE id = ?', fileId);
|
||||
|
||||
if (!file) {
|
||||
logger.error(`File with ID ${fileId} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Creating NZB for file: ${file.filename}`);
|
||||
|
||||
const messageIds = JSON.parse(file.message_ids);
|
||||
|
||||
const root = create({ version: '1.0', encoding: 'UTF-8' })
|
||||
.dtd({ pubID: '-//newzBin//DTD NZB 1.1//EN', sysID: 'http://www.newzbin.com/DTD/nzb/nzb-1.1.dtd' })
|
||||
.ele('nzb', { xmlns: 'http://www.newzbin.com/DTD/2003/nzb' });
|
||||
|
||||
const nzbFile = root.ele('file', {
|
||||
poster: file.poster,
|
||||
date: file.date,
|
||||
subject: file.filename,
|
||||
});
|
||||
|
||||
const groups = nzbFile.ele('groups');
|
||||
// This should be dynamic in a real application
|
||||
groups.ele('group').txt('alt.binaries.test');
|
||||
|
||||
const segments = nzbFile.ele('segments');
|
||||
for (let i = 1; i <= file.parts; i++) {
|
||||
const segment = messageIds[i];
|
||||
segments.ele('segment', { 'bytes': segment.size.toString(), 'number': i.toString() }).txt(segment.id);
|
||||
}
|
||||
|
||||
const xml = root.end({ prettyPrint: true });
|
||||
const nzbFilename = `${file.filename}.nzb`;
|
||||
|
||||
await fs.writeFile(nzbFilename, xml);
|
||||
logger.info(`NZB file created: ${nzbFilename}`);
|
||||
}
|
||||
|
||||
const fileId = parseInt(process.argv[2], 10);
|
||||
if (isNaN(fileId)) {
|
||||
logger.error('Please provide a valid file ID as a command-line argument.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
createNzb(fileId);
|
||||
22
src/yenc.test.js
Normal file
22
src/yenc.test.js
Normal file
@ -0,0 +1,22 @@
|
||||
import yencode from 'yencode';
|
||||
import fs from 'fs/promises';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
async function runTest() {
|
||||
const encodedData = await fs.readFile('HjVfQlWmHdUrQeQkRiLkTwEj-1779830864932@nyuu.bin');
|
||||
const correctlyDecodedData = await fs.readFile('Dragon.Ball.S01E119.MULTI.BDRip.REMASTERED.1080p.x264.DTS-LILAS.par2');
|
||||
|
||||
const decodedBuffer = yencode.decode(encodedData);
|
||||
await fs.writeFile('decoded.bin', decodedBuffer)
|
||||
|
||||
if (Buffer.compare(decodedBuffer, correctlyDecodedData) === 0) {
|
||||
console.log('Test passed: Decoded data matches the correctly decoded file.');
|
||||
} else {
|
||||
console.error('Test failed: Decoded data does not match the correctly decoded file.');
|
||||
console.error('Decoded buffer length:', decodedBuffer.length);
|
||||
console.error('Correct buffer length:', correctlyDecodedData.length);
|
||||
await fs.writeFile('test-decoded-output.bin', decodedBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
runTest();
|
||||
Loading…
x
Reference in New Issue
Block a user