Two processes, one NNTP connection each (the coordinator 40 -> 38): - spots:read (service spotter): free.pt's overviews after the last article seen, each spot's full header verified (lib/spotnet from voyager 1, as Spotweb), its description, website and NZB articles; the password its title or text names; delete/dispose messages applied. - spots:inspect (service spot-inspector): each spot's compressed NZB read, its files listed (spot_files), each matched to the header pass's release by the same release identity in one of its groups within 48 h (release_spots); unmatched spots matched again hourly, after 24 h a release of their own (source spot) with the spot's NZB. - release_passwords: per release and password, its source (nzb, spot-title, spot-text) and spot; the catalog keeps only passworded. XoverClient.head(); PROBE_CONNECTIONS default 10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
45 lines
1.8 KiB
TypeScript
45 lines
1.8 KiB
TypeScript
import { gunzipSync } from 'node:zlib'
|
|
import db from '@adonisjs/lucid/services/db'
|
|
import { decodeChunk, type ChunkFile } from '#lib/chunks/index'
|
|
import { buildNzb } from '#lib/nzb/index'
|
|
|
|
/**
|
|
* A release's files and NZB (REBUILD.md, part 9), from its chunks in the
|
|
* chunk store: every chunk decoded, its files in name order. A file stored
|
|
* twice (the same name and first article, e.g. a range read again) is taken
|
|
* once.
|
|
*/
|
|
class NzbService {
|
|
async files(releaseId: number): Promise<ChunkFile[]> {
|
|
const chunks = await db.from('release_chunks').where('release_id', releaseId).orderBy('id').select('payload')
|
|
const files = new Map<string, ChunkFile>()
|
|
for (const chunk of chunks) {
|
|
for (const file of decodeChunk(chunk.payload)) files.set(`${file.name}\n${file.segments[0]?.[1] ?? ''}`, file)
|
|
}
|
|
return [...files.values()].sort((a, b) => a.name.localeCompare(b.name))
|
|
}
|
|
|
|
async build(releaseId: number): Promise<{ name: string; xml: string } | null> {
|
|
const release = await db.from('releases').where('id', releaseId).select('name', 'source').first()
|
|
if (!release) return null
|
|
// A release a spot made of its own (no files matched the header pass's): the spot's NZB as read.
|
|
if (release.source === 'spot') {
|
|
const spot = await db.from('spots').where('release_id', releaseId).whereNotNull('nzb').select('nzb').first()
|
|
if (spot) return { name: release.name, xml: gunzipSync(spot.nzb).toString('utf8') }
|
|
}
|
|
const xml = buildNzb(
|
|
(await this.files(releaseId)).map((f) => ({
|
|
name: f.name,
|
|
poster: f.poster,
|
|
date: f.postedAt ?? 0,
|
|
groups: f.groups,
|
|
segments: f.segments.map(([number, id, size]) => ({ number, id, size })),
|
|
})),
|
|
{ title: release.name }
|
|
)
|
|
return { name: release.name, xml }
|
|
}
|
|
}
|
|
|
|
export default new NzbService()
|