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>
92 lines
4.6 KiB
TypeScript
92 lines
4.6 KiB
TypeScript
import { test } from '@japa/runner'
|
|
import db from '@adonisjs/lucid/services/db'
|
|
import FileStore from '#services/FileStore'
|
|
import env from '#start/env'
|
|
import testUtils from '@adonisjs/core/services/test_utils'
|
|
|
|
let counter = 0
|
|
async function seed(name: string, opts: { fileTotal?: number; files?: number } = {}) {
|
|
const now = new Date()
|
|
const [release] = await db
|
|
.table('releases')
|
|
.insert({ release_key: `nz${++counter}`.padEnd(40, 'n'), name, source: 'stem', poster: 'poster@example.invalid', groups: '["alt.binaries.test"]', file_total: opts.fileTotal ?? null, posted_at: now, first_seen_at: now, last_seen_at: now })
|
|
.returning('id')
|
|
for (let i = 0; i < (opts.files ?? 1); i++) {
|
|
await FileStore.insert([{
|
|
filename: `${name}.part${i + 1}.rar`, poster: 'poster@example.invalid', date: now.getTime(), parts: 1, size: 1_000_000,
|
|
release_id: release.id, groups: '["alt.binaries.test"]', segments: [{ number: 1, id: `${name}-${i}@test`, size: 1_000_000 }],
|
|
created_at: now, updated_at: now,
|
|
}])
|
|
}
|
|
return Number(release.id)
|
|
}
|
|
|
|
const ids = (xml: string) => [...xml.matchAll(/<guid isPermaLink="false">(\d+)<\/guid>/g)].map((m) => Number(m[1]))
|
|
|
|
test.group('Newznab API', (group) => {
|
|
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
|
|
|
test('caps lists search functions and categories', async ({ client, assert }) => {
|
|
const response = await client.get('/api').qs({ t: 'caps' })
|
|
response.assertStatus(200)
|
|
assert.include(response.text(), '<tv-search available="yes" supportedParams="q,season,ep"/>')
|
|
assert.include(response.text(), '<category id="5000" name="TV">')
|
|
assert.include(response.text(), '<subcat id="5040" name="HD"/>')
|
|
})
|
|
|
|
test('tvsearch matches show, season and episode, with Newznab attributes', async ({ client, assert }) => {
|
|
const e02 = await seed('Nzbtest.Show.S01E02.1080p.WEB-DL-GRP')
|
|
const e03 = await seed('Nzbtest.Show.S01E03.1080p.WEB-DL-GRP')
|
|
const movie = await seed('Nzbtest.Show.The.Movie.2020.1080p.BluRay.x264-GRP')
|
|
|
|
const response = await client.get('/api').qs({ t: 'tvsearch', q: 'nzbtest show', season: '1', ep: '2' })
|
|
response.assertStatus(200)
|
|
const body = response.text()
|
|
assert.deepEqual(ids(body), [e02])
|
|
assert.include(body, '<newznab:attr name="category" value="5000"/>')
|
|
assert.include(body, '<newznab:attr name="category" value="5040"/>')
|
|
assert.include(body, '<newznab:attr name="season" value="S01"/>')
|
|
assert.include(body, `/api?t=get&id=${e02}`)
|
|
assert.include(body, 'type="application/x-nzb"')
|
|
|
|
const season = await client.get('/api').qs({ t: 'tvsearch', q: 'nzbtest show', season: '1' })
|
|
assert.sameMembers(ids(season.text()), [e02, e03])
|
|
const movies = await client.get('/api').qs({ t: 'movie', q: 'nzbtest show' })
|
|
assert.deepEqual(ids(movies.text()), [movie])
|
|
})
|
|
|
|
test('leaves out obfuscated names and releases known to be incomplete', async ({ client, assert }) => {
|
|
const readable = await seed('Nzbtest.Other.2021.720p.WEB-DL-GRP')
|
|
const incomplete = await seed('Nzbtest.Partial.2021.720p.WEB-DL-GRP', { fileTotal: 5, files: 2 })
|
|
const random = await seed('Xq8r2KdLm3Zw91PqA7nzbtest')
|
|
const found = ids((await client.get('/api').qs({ t: 'search', q: 'nzbtest' })).text())
|
|
assert.include(found, readable)
|
|
assert.notInclude(found, incomplete)
|
|
assert.notInclude(found, random)
|
|
})
|
|
|
|
test('get returns the NZB, errors use Newznab codes', async ({ client, assert }) => {
|
|
const id = await seed('Nzbtest.Get.S02E01.720p.HDTV-GRP')
|
|
const nzb = await client.get('/api').qs({ t: 'get', id: String(id) })
|
|
nzb.assertStatus(200)
|
|
assert.include(nzb.header('content-type'), 'application/x-nzb')
|
|
assert.include(nzb.text(), 'Nzbtest.Get.S02E01.720p.HDTV-GRP-0@test')
|
|
assert.include((await client.get('/api').qs({ t: 'get', id: '999999999' })).text(), 'code="300"')
|
|
assert.include((await client.get('/api').qs({ t: 'nonsense' })).text(), 'code="202"')
|
|
})
|
|
|
|
test('requires the API key when one is configured', async ({ client, assert }) => {
|
|
const previous = env.get('NEWZNAB_API_KEY')
|
|
env.set('NEWZNAB_API_KEY', 'test-key' as any)
|
|
try {
|
|
assert.include((await client.get('/api').qs({ t: 'caps' })).text(), 'code="100"')
|
|
assert.include((await client.get('/api').qs({ t: 'caps', apikey: 'test-key' })).text(), '<caps>')
|
|
const id = await seed('Nzbtest.Key.S01E01.720p.HDTV-GRP')
|
|
const search = await client.get('/api').qs({ t: 'search', q: 'nzbtest key', apikey: 'test-key' })
|
|
assert.include(search.text(), `id=${id}&apikey=test-key`)
|
|
} finally {
|
|
env.set('NEWZNAB_API_KEY', previous as any)
|
|
}
|
|
})
|
|
})
|