import { create } from 'xmlbuilder2' export interface NzbFile { /** Name the downloader should use; the real name when it is known. */ name: string poster: string /** Posting time in milliseconds. */ date: number groups: string[] /** Segment number → message-id (without angle brackets) and article size. */ segments: Record } export interface NzbOptions { /** Shown by downloaders as the job name. */ title?: string /** Archive password, when known. */ password?: string } /** * Builds an NZB 1.1 document (newzbin DTD). The subject follows the common * `"name" yEnc (1/N)` form, which downloaders use to name the file. */ export function buildNzb(files: NzbFile[], options: NzbOptions = {}): string { 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' }) if (options.title || options.password) { const head = root.ele('head') if (options.title) head.ele('meta', { type: 'name' }).txt(options.title) if (options.password) head.ele('meta', { type: 'password' }).txt(options.password) } for (const file of files) { const numbers = Object.keys(file.segments) .map(Number) .sort((a, b) => a - b) const element = root.ele('file', { poster: file.poster, date: String(Math.floor(file.date / 1000)), subject: `"${file.name}" yEnc (1/${numbers.length})`, }) const groups = element.ele('groups') for (const group of file.groups) groups.ele('group').txt(group) const segments = element.ele('segments') for (const number of numbers) { const segment = file.segments[String(number)] segments.ele('segment', { bytes: String(segment.size), number: String(number) }).txt(segment.id) } } return root.end({ prettyPrint: true }) }