Files
usenet-indexer/lib/nzb/build.ts
T
Daan MeijerandClaude Opus 5.5 fde1e6cc90 Rebuild: NZBs from the chunk store, and a catalog search
lib/nzb (buildNzb, parseNzb) from the current app; NzbService builds a
release's NZB from its chunks; GET /api/v1/releases?q= (every word of the
name) and GET /api/v1/releases/:id/nzb behind the api guard. A test stores
a batch, finds the release and reads its NZB back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 02:33:27 +02:00

57 lines
1.9 KiB
TypeScript

import { create } from 'xmlbuilder2'
/** One article of a file: its number, message-id (without <>) and size in bytes. */
export interface Segment {
number: number
id: string
size: number
}
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[]
segments: Segment[]
}
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 sorted = [...file.segments].sort((a, b) => a.number - b.number)
const element = root.ele('file', {
poster: file.poster,
date: String(Math.floor(file.date / 1000)),
subject: `"${file.name}" yEnc (1/${sorted.length})`,
})
const groups = element.ele('groups')
for (const group of file.groups) groups.ele('group').txt(group)
const segments = element.ele('segments')
for (const segment of sorted) {
segments.ele('segment', { bytes: String(segment.size), number: String(segment.number) }).txt(segment.id)
}
}
return root.end({ prettyPrint: true })
}