Spotnet (CONCEPT D29): - Index free.pt fully and import spots with their descriptions, categories and NZBs; a spot renames its release, since Spotnet names are more trustworthy than names derived from subjects. - Find passwords in spot descriptions too, and tell "needs a password we have" from "needs a password we don't have". Content inspection: - List the contents of 7z (.7z.001) archives, SFV sets, nicovideo uploads and other archive-like files; mark yEncBin .bin containers as encrypted. - Take real names from yEnc headers for randomly named files, and merge uploads that were split across posts. Storage (D31, D32): - Store segment lists zstd-compressed behind the FileStore accessor. - group:backfill indexes older headers at low priority and pauses when the NVMe or HDD part of the database gets too large. Grouping (D35, D36): - Lone header segments wait in staged_segments until a second segment of their file arrives, which drops the per-article junk binaries. - Counter grouping joins random-subject uploads by poster, [i/M] counter, part count and day. Search and maintenance (D33, D34): - A trigram-indexed search_text column, smaller indexes, estimated stats. - Passes that clean up nonsense names; weekly index maintenance; GC budgets per kind. - Web UI: grey out stale results while a search runs and cancel superseded requests. Count only up to 1,000 matches for the page, which made filter clicks take seconds (Newznab keeps the 10,000 cap). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
52 lines
1.8 KiB
TypeScript
52 lines
1.8 KiB
TypeScript
import { create } from 'xmlbuilder2'
|
|
import type { Segment } from './segments.js'
|
|
|
|
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 })
|
|
}
|