Spots: more password phrasings; read listed spots' articles and NZBs
Test and deploy / test (push) Successful in 57s
Test and deploy / deploy (push) Successful in 4m18s

passwordFromSpot finds 30 more passwords in the stored descriptions (480
→ 510): words between keyword and separator, trailing "!!", the value on
the next line, "wachtwoord is x", "… wachtwoord. Deze is: x", "WW x" as a
whole line.

Listed spots were read from their overview only, so the NZB's own
password and the description were unknown for 2.27M of them.
SpotService.readListing reads the spot's article and NZB without
importing files: details, the NZB password (over a spot-text one), the
spot-text password, placeholder NZBs. worker:spots does a batch when
idle; spot:postprocess works through the history (nzb_read_at).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-08 13:46:32 +02:00
1 parent 36f84ae3e6
commit ee898be8c8
8 files changed
+322 -17

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
import { BaseCommand, flags } from '@adonisjs/core/ace'
import { CommandOptions } from '@adonisjs/core/types/ace'
import db from '@adonisjs/lucid/services/db'
import env from '#start/env'
import Shutdown from '#services/Shutdown'
import NntpService from '#services/NntpService'
import SpotService from '#services/SpotService'
const BATCH = 500
/**
* Reads the article and NZB of listed spots that were listed from their
* overview only (LOG.md 2026-10-08 (10)): description, the NZB's password or
* one in the spot's text, placeholder NZBs. Newest first; can be stopped and
* started again (worker:spots does the same when idle, a batch at a time).
* Connections: NNTP_POOL_SIZE of this process, --concurrency at a time.
*/
export default class SpotPostprocess extends BaseCommand {
public static commandName = 'spot:postprocess'
public static description = "Reads listed spots' articles and NZBs for their passwords and details."
public static options: CommandOptions = {
startApp: true,
}
@flags.number({ description: 'Spots read at a time (default: NNTP_POOL_SIZE)' })
declare concurrency?: number
@flags.number({ description: 'Stop after this many spots' })
declare limit?: number
public async run() {
const concurrency = this.concurrency ?? env.get('NNTP_POOL_SIZE', 4)
const [{ count }] = await db.from('spots').where('status', 'listed').whereNull('nzb_read_at').count('* as count')
this.logger.info(`${count} listed spots to read, ${concurrency} at a time.`)
const totals: Record<string, number> = {}
const started = Date.now()
let done = 0
try {
while (!Shutdown.requested && (!this.limit || done < this.limit)) {
const counts = await SpotService.readListings(Math.min(BATCH, this.limit ? this.limit - done : BATCH), concurrency)
const read = Object.values(counts).reduce((sum, n) => sum + n, 0)
if (read === 0) break
for (const [k, v] of Object.entries(counts)) totals[k] = (totals[k] ?? 0) + v
done += read
const rate = done / ((Date.now() - started) / 1000)
this.logger.info(`${done} read (${rate.toFixed(1)}/s): ${Object.entries(totals).map(([k, v]) => `${v} ${k}`).join(', ')}.`)
// Usenet unreachable: everything was given back; don't spin on it.
if (counts.retry === read) await Shutdown.sleep(30_000)
}
} finally {
await NntpService.shutdown()
}
}
}
+10 -3
View File
@@ -10,10 +10,13 @@ const IDLE_MS = 30_000
const BATCH = 500
/** Imported spots whose details are read per idle round. */
const DETAILS_BATCH = 50
/** Listed spots whose article and NZB are read per idle round (passwords, dummies). */
const LISTINGS_BATCH = 50
/**
* Works through queued Spotnet spots (CONCEPT.md D29): verifies each one,
* downloads its compressed NZB (a few KB to a few MB) and imports it.
* downloads its compressed NZB (a few KB to a few MB) and imports it. When
* idle, reads what listed spots were listed without: details, passwords.
*/
export default class SpotsWorker extends BaseCommand {
public static commandName = 'worker:spots'
@@ -37,8 +40,12 @@ export default class SpotsWorker extends BaseCommand {
const batch = await SpotService.claim(Math.min(BATCH, this.limit ? this.limit - done : Infinity))
if (batch.length === 0) {
if (this.limit) break
// Idle: read the details of imported spots that don't have them yet (newest first).
if ((await SpotService.fillDetails(DETAILS_BATCH)) === 0) await Shutdown.sleep(IDLE_MS)
// Idle: read the details of imported spots that don't have them yet (newest first), then
// the article and NZB of listed spots (their passwords; LOG.md 2026-10-08 (10)).
if ((await SpotService.fillDetails(DETAILS_BATCH)) > 0) continue
const listings = await SpotService.readListings(LISTINGS_BATCH, concurrency)
const read = Object.values(listings).reduce((sum, n) => sum + n, 0)
if (read === 0 || listings.retry === read) await Shutdown.sleep(IDLE_MS)
continue
}
// Old, verified spots are listed in bulk; the rest need their header (and maybe NZB) read.