Files
usenet-indexer/tests/functional/spots.spec.ts
T
Daan MeijerandClaude Opus 5.5 61bdd43b34 Categories from the inner name, spots and groups; fix the name rules
Migration 1200 passed regexes with `?` through knex, which made them
bindings: the live function held blu-$1ray, web-$2dl and h\.$326[45], so
BluRay, WEB-DL and H.264 names without a resolution were Misc. 93% of
recent releases were Misc, as the category came from the name alone.

category becomes a plain column (DROP EXPRESSION, no rewrite) set by a
trigger: the name's category (fixed, no `?` anywhere; adds XXX,
audiobooks, console and PC games, and TV codes like S01e07[ and
S04E01E02), else the prober's inner name's, else one given before (a
spot's), else the groups'. The spot inspector sets a spot's category
(spot_category) on Misc releases it links and on its own releases. The
Newznab and web category lists gain Console, PC Games, Audiobook, Anime,
XXX and Comics. releases:recategorize recomputes existing rows in id
batches, writing only changed rows and waiting under a free-space floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:07:12 +02:00

126 lines
8.2 KiB
TypeScript

import { test } from '@japa/runner'
import db from '@adonisjs/lucid/services/db'
import testUtils from '@adonisjs/core/services/test_utils'
import { Spotter } from '#services/spotter'
import { SpotInspector } from '#services/spot_inspector'
import { releaseIdentity } from '#lib/release/index'
import { parseSubject } from '#lib/headers/index'
// Message ids whose sha1('<id>') starts with 0000: self-signed spots (key 7) pass on that hashcash alone.
const SPOT = 'spottest731@spot.net'
const DISPOSE = 'spottest168100@spot.net'
const OTHER = 'spottest173225@spot.net'
// A poster's key longer than 50 characters: its spotter id makes a personal dispose count.
const PUBKEY = 'A'.repeat(64)
const STAMP = 1_760_000_000
/** A spot's raw HEAD: From with the spot header (category 1, key 7), the X-XML with title, description and NZB. */
function rawSpot(messageId: string, title: string, description: string, key = 7, stamp = STAMP) {
return [
`From: Spotter <${PUBKEY}@1${key}a09z00.734003200.0.${stamp}.0.SIG>`,
`Subject: ${title} | Tag`,
`Message-ID: <${messageId}>`,
`X-XML: <Spotnet><Posting><Key>${key}</Key><Title>${title}</Title><Description>${description}</Description>`,
`X-XML: <Category>01<Sub>01a09</Sub></Category><NZB><Segment>nzb1@spot.net</Segment></NZB><Size>734003200</Size></Posting></Spotnet>`,
'',
].join('\r\n')
}
/** An NZB as spotters post them: the files' original subjects (buildNzb writes its own). */
function nzbXml(subjects: string[], poster: string, date: number, password?: string) {
const esc = (t: string) => t.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
const head = password ? `<head><meta type="password">${esc(password)}</meta></head>` : ''
const files = subjects
.map((s, i) => `<file poster="${esc(poster)}" date="${date}" subject="${esc(s)}"><groups><group>alt.binaries.test.spots</group></groups><segments><segment bytes="400000000" number="1">seg${i}@x</segment></segments></file>`)
.join('')
return `<?xml version="1.0" encoding="UTF-8"?><nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">${head}${files}</nzb>`
}
test.group('Spots', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test('the spotter lists a verified spot with the password its text names, and a dispose removes it', async ({ assert }) => {
const spotter = new Spotter()
assert.equal(await spotter.store(rawSpot(SPOT, 'Some.Movie.2024.1080p', 'Mooie film[br]Wachtwoord: geheim123'), SPOT, 10), 'listed')
const spot = await db.from('spots').where('message_id', SPOT).firstOrFail()
assert.equal(spot.title, 'Some.Movie.2024.1080p')
assert.equal(spot.password, 'geheim123')
assert.equal(spot.password_source, 'spot-text')
assert.deepEqual(spot.nzb_segments, ['nzb1@spot.net'])
// A {{pw}} in the title wins, and is said to come from the title.
assert.equal(await spotter.store(rawSpot(OTHER, 'Other.Show{{t1tl3pw}}', 'Geen ww'), OTHER, 11), 'listed')
assert.equal((await db.from('spots').where('message_id', OTHER).firstOrFail()).password_source, 'spot-title')
// A message id without the hashcash doesn't verify.
assert.equal(await spotter.store(rawSpot('nohash@spot.net', 'X', ''), 'nohash@spot.net', 12), 'unverified')
// The poster's own dispose within 5 days.
await db.table('release_spots').insert({ release_id: 1, spot_id: SPOT, files: 3 })
assert.equal(await spotter.store(rawSpot(DISPOSE, `dispose <${SPOT}>`, '', 2, STAMP + 3600), DISPOSE, 13), 'moderation')
assert.equal((await db.from('spots').where('message_id', SPOT).firstOrFail()).status, 'removed')
assert.equal((await db.from('release_spots').where('spot_id', SPOT).select('*')).length, 0)
})
test("the inspector lists an NZB's files and links the releases they match, with the password apart", async ({ assert }) => {
const [g] = await db.table('groups').insert({ name: 'alt.binaries.test.spots', priority: 1 }).returning('id')
const groupId = Number(g.id)
const poster = 'up <up@load.er>'
const posted = new Date('2025-10-09T10:00:00Z')
const subjects = [1, 2, 3].map((i) => `Some.Movie.2024.1080p [${i}/3] - "Some.Movie.2024.1080p.part0${i}.rar" yEnc (1/50)`)
// The release the header pass built for these files: the same identity, in the group.
const parsed = parseSubject(subjects[0])!
const identity = releaseIdentity({ poster, subject: parsed.base, name: parsed.name, weak: false })!
const [r] = await db
.table('releases')
.insert({ release_key: identity.key, identity_key: identity.key, name: identity.name, source: identity.source, poster, groups: '[]', group_id: groupId, files: 3, posted_at: posted, readable: true })
.returning('id')
const releaseId = Number(r.id)
const nzb = nzbXml(subjects, poster, posted.getTime() / 1000, 'nzbpw')
await new Spotter().store(rawSpot(SPOT, 'Some.Movie.2024.1080p', 'Wachtwoord: geheim123'), SPOT, 10)
const spot = await db.from('spots').where('message_id', SPOT).firstOrFail()
const outcome = await new SpotInspector().inspectNzb(spot, nzb)
assert.deepEqual(outcome, { status: 'inspected', matched: 1 })
const files = await db.from('spot_files').where('spot_id', SPOT).orderBy('idx')
assert.deepEqual(files.map((f) => f.name), ['Some.Movie.2024.1080p.part01.rar', 'Some.Movie.2024.1080p.part02.rar', 'Some.Movie.2024.1080p.part03.rar'])
assert.deepEqual([...new Set(files.map((f) => Number(f.release_id)))], [releaseId])
assert.equal((await db.from('release_spots').where({ release_id: releaseId, spot_id: SPOT }).firstOrFail()).files, 3)
// The NZB's own password over the one in the spot's text, kept apart from the release.
const passwords = await db.from('release_passwords').where('release_id', releaseId).select('password', 'source')
assert.deepEqual(passwords, [{ password: 'nzbpw', source: 'nzb' }])
assert.isTrue((await db.from('releases').where('id', releaseId).firstOrFail()).passworded)
const after = await db.from('spots').where('message_id', SPOT).firstOrFail()
assert.equal(after.status, 'inspected')
assert.equal(Number(after.release_id), releaseId)
assert.equal(after.matched, 3)
})
test('a spot whose files match no release becomes one of its own, with its NZB', async ({ assert }) => {
await db.table('groups').insert({ name: 'alt.binaries.test.spots', priority: 1 })
const nzb = nzbXml(['[1/1] - "lonely.file.mkv" yEnc (1/9)'], 'p', STAMP)
await new Spotter().store(rawSpot(SPOT, 'Lonely File', ''), SPOT, 10)
const inspector = new SpotInspector()
assert.deepEqual(await inspector.inspectNzb(await db.from('spots').where('message_id', SPOT).firstOrFail(), nzb), { status: 'inspected', matched: 0 })
assert.isTrue(await inspector.standalone(SPOT))
const release = await db.from('releases').where('source', 'spot').firstOrFail()
assert.equal(release.name, 'Lonely File')
// Its title says nothing: the spot's category (01, a09: a movie, x264).
assert.equal(release.category, 2040)
assert.equal(release.files, 1)
assert.isNotNull(release.poster)
assert.isNotNull(await db.from('posters').where('poster', release.poster).first())
const { default: NzbService } = await import('#services/nzb_service')
assert.include((await NzbService.build(Number(release.id)))!.xml, 'lonely.file.mkv')
// Not again while it has one; once the header pass builds the file's release, the spot moves there.
assert.deepEqual(await inspector.rematch(), { matched: 0, standalone: 0 })
const group = await db.from('groups').where('name', 'alt.binaries.test.spots').firstOrFail()
const parsed = parseSubject('[1/1] - "lonely.file.mkv" yEnc (1/9)')!
const identity = releaseIdentity({ poster: 'p', subject: parsed.base, name: parsed.name, weak: false })!
const [h] = await db
.table('releases')
.insert({ release_key: identity.key, identity_key: identity.key, name: identity.name, source: identity.source, poster: 'p', groups: '[]', group_id: group.id, files: 1, posted_at: new Date(STAMP * 1000), readable: true })
.returning('id')
assert.equal(await inspector.match(SPOT), 1)
assert.isNull(await db.from('releases').where('id', release.id).first())
assert.equal(Number((await db.from('spots').where('message_id', SPOT).firstOrFail()).release_id), Number(h.id))
})
})