Rebuild: the catalog API and Newznab, from voyager 1
- /api (Newznab: caps, search, tvsearch, movie, music, book, get) for Prowlarr, Sonarr and Radarr: voyager 1's controller and XML on the new catalog; readable releases not known to miss files; NEWZNAB_API_KEY. - /api/v1/releases (q, cat, readable, complete, source, poster, size, sort, paging), /api/v1/releases/:id with its files (decoded from the chunks; NzbService.files, shared with the NZB), /api/v1/stats (estimates, plus history and tail progress), /health. Open, as voyager 1's catalog; the web interface uses it. - release_category(name): voyager 1's categorizer without the archive contents, a generated column with a (category, posted_at) index. - @adonisjs/static for the web interface (next). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
57dfa3d749
commit
8e52bff213
18 files changed
+801
-79
No files matched your search
@@ -1,7 +1,6 @@
|
||||
import { test } from '@japa/runner'
|
||||
import db from '@adonisjs/lucid/services/db'
|
||||
import testUtils from '@adonisjs/core/services/test_utils'
|
||||
import User from '#models/user'
|
||||
import BatchStore from '#services/batch_store'
|
||||
import { RangeIndexer } from '#lib/edge/index'
|
||||
import { parseNzb } from '#lib/nzb/index'
|
||||
@@ -29,15 +28,13 @@ test.group('Releases API', (group) => {
|
||||
indexer.finish()
|
||||
await BatchStore.store(Number(r.id), Number(n.id), indexer.take())
|
||||
|
||||
const user = await User.create({ email: 'u@example.invalid', password: 'secret-password' })
|
||||
const token = (await User.accessTokens.create(user)).value!.release()
|
||||
;(await client.get('/api/v1/releases?q=show s01e01')).assertStatus(401)
|
||||
const found = await client.get('/api/v1/releases?q=show s01e01').bearerToken(token)
|
||||
// Open, as voyager 1's catalog (the web interface uses it).
|
||||
const found = await client.get('/api/v1/releases?q=show s01e01')
|
||||
found.assertStatus(200)
|
||||
const releases = (found.body() as { releases: { id: number; name: string }[] }).releases
|
||||
assert.lengthOf(releases, 1)
|
||||
|
||||
const nzb = await client.get(`/api/v1/releases/${releases[0].id}/nzb`).bearerToken(token)
|
||||
const nzb = await client.get(`/api/v1/releases/${releases[0].id}/nzb`)
|
||||
nzb.assertStatus(200)
|
||||
assert.include(nzb.header('content-type'), 'application/x-nzb')
|
||||
const parsed = parseNzb(nzb.text())!
|
||||
@@ -45,4 +42,38 @@ test.group('Releases API', (group) => {
|
||||
assert.deepEqual(parsed.files.map((f) => f.segments.length), [1, 3])
|
||||
assert.equal(parsed.files[1].segments[2].id, 'Some.Show.S01E01.1080p.WEB.part1.rar-3@test')
|
||||
})
|
||||
test('details with files, categories, stats and the Newznab API', async ({ client, assert }) => {
|
||||
const [g] = await db.table('groups').insert({ name: GROUP, priority: 1 }).returning('id')
|
||||
const [n] = await db.table('nodes').insert({ name: 'api-node' }).returning('id')
|
||||
const [r] = await db.table('ranges').insert({ group_id: g.id, start_id: 1, end_id: 1000, next_id: 1, status: 'leased', node_id: n.id }).returning('id')
|
||||
const indexer = new RangeIndexer({ group: GROUP, start: 1, end: 1000, rules: { ...DEFAULT_RULES, samplePercent: 0, droppedSignatures: [], priorities: { [GROUP]: 1 } } })
|
||||
indexer.add([...file('Some.Show.S01E01.1080p.WEB.mkv', 'Some.Show.S01E01.1080p.WEB [1/1]', 2, 100), ...file('Other.Movie.2024.2160p.BluRay.x265.mkv', 'Other.Movie.2024.2160p.BluRay.x265 [1/1]', 1, 300)], 1001)
|
||||
indexer.finish()
|
||||
await BatchStore.store(Number(r.id), Number(n.id), indexer.take())
|
||||
|
||||
// Web API: a category (TV, expanded to its subcategories) and a release's files.
|
||||
const tv = (await client.get('/api/v1/releases?cat=5000')).body() as { releases: { id: number; name: string; category: number }[] }
|
||||
assert.deepEqual(tv.releases.map((x) => [x.name, x.category]), [['Some Show S01E01 1080p WEB', 5040]])
|
||||
const detail = (await client.get(`/api/v1/releases/${tv.releases[0].id}`)).body() as { files: { name: string; parts: number; size: number }[] }
|
||||
assert.deepEqual(detail.files, [{ ...detail.files[0], name: 'Some.Show.S01E01.1080p.WEB.mkv', parts: 2, size: 1400001 }])
|
||||
;(await client.get('/api/v1/releases/999999')).assertStatus(404)
|
||||
const stats = (await client.get('/api/v1/stats')).body() as { releases: number; ranges: { total: number } }
|
||||
assert.equal(stats.releases, 2)
|
||||
assert.equal(stats.ranges.total, 1)
|
||||
|
||||
// Newznab: caps, a TV search with season and episode, the NZB; the key when one is set.
|
||||
const caps = await client.get('/api?t=caps')
|
||||
assert.include(caps.text(), '<category id="5000" name="TV">')
|
||||
const search = await client.get('/api?t=tvsearch&q=some show&season=1&ep=1')
|
||||
assert.include(search.text(), '<title>Some Show S01E01 1080p WEB</title>')
|
||||
assert.include(search.text(), '<newznab:attr name="category" value="5040"/>')
|
||||
assert.notInclude(search.text(), 'Other Movie')
|
||||
const movie = await client.get('/api?t=movie&cat=2045')
|
||||
assert.include(movie.text(), 'Other Movie 2024 2160p BluRay x265')
|
||||
const get = await client.get(`/api?t=get&id=${tv.releases[0].id}`)
|
||||
assert.include(get.header('content-type'), 'application/x-nzb')
|
||||
assert.equal(parseNzb(get.text())!.files.length, 1)
|
||||
assert.include((await client.get('/api?t=get&id=999999')).text(), 'code="300"')
|
||||
assert.include((await client.get('/api?t=nope')).text(), 'code="202"')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user