voyager 3: the batch store writes releases once, by id references and bytea keys
Test and deploy / test (push) Successful in 51s
Test and deploy / edge (push) Successful in 1m48s
Test and deploy / deploy (push) Skipped

Each release chunk is a release inserted once: release_key the SHA-1 of
the identity, the owner group and the first article (deterministic; no
numbering under a group lock), identity_key the identity's SHA-1 bytes,
poster_id and group_set_id through catalog_refs (cached per process),
source as a code (weak-stem added), the name into release_texts. A
finished release's candidates are claimable at once; a provisional tail
release is finished by a later chunk of its key, a finished one sent again
is left alone. Gone: joins into earlier releases and across groups, key
numbering, the group lock, the fragment merger. Interim: chunks, counts
and samples keyed by ranges.block_id, a range without a block gets one.

Readers follow: search (posters and group_sets joined, words through
release_texts, posters by id, source by code), the par2 merger, the
sidecar folder, the judge's scraped rule, the spot inspector, the spotter,
the NZB service, releases:regroup, edge:bench. Tests insert fixtures
through tests/functional/helpers/releases.ts; the suites pass again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Fable 5.1 committed 2026-10-25 21:04:03 +01:00
1 parent bc4e17e86f
commit 2580f20a43
32 files changed
+962 -497

No files matched your search

+399 -100
View File
@@ -2,19 +2,35 @@ import { test } from '@japa/runner'
import db from '@adonisjs/lucid/services/db'
import testUtils from '@adonisjs/core/services/test_utils'
import BatchStore, { StaleBatch } from '#services/batch_store'
import { RangeIndexer } from '#lib/edge/index'
import { decodeChunk } from '#lib/chunks/index'
import { RangeIndexer, type EdgeBatch, type ReleaseChunk } from '#lib/edge/index'
import { decodeChunk, encodeChunk } from '#lib/chunks/index'
import { DEFAULT_RULES } from '#lib/rules/index'
import type { Overview } from '#lib/nntp/index'
const GROUP = 'alt.binaries.test'
let id = 0
function post(at: number, name: string, parts: number, counter: string, tool: 'ngPost' | 'plain' = 'plain'): Overview[] {
function post(
at: number,
name: string,
parts: number,
counter: string,
tool: 'ngPost' | 'plain' = 'plain'
): Overview[] {
return Array.from({ length: parts }, (_, i) => {
id++
const mid = tool === 'ngPost' ? `${id.toString(16).padStart(32, '0')}@ngPost` : `art${id}@test`
return [at + i, { 'subject': `${counter} - "${name}" yEnc (${i + 1}/${parts})`, 'from': 'p <p@x>', 'date': 'Mon, 05 Oct 2026 10:00:00 +0000', 'message-id': `<${mid}>`, ':bytes': '700000', 'xref': `news.example ${GROUP}:${at + i}` }] as Overview
return [
at + i,
{
'subject': `${counter} - "${name}" yEnc (${i + 1}/${parts})`,
'from': 'p <p@x>',
'date': 'Mon, 05 Oct 2026 10:00:00 +0000',
'message-id': `<${mid}>`,
':bytes': '700000',
'xref': `news.example ${GROUP}:${at + i}`,
},
] as Overview
})
}
@@ -23,73 +39,218 @@ async function leasedRange() {
const [node] = await db.table('nodes').insert({ name: 'test-node' }).returning('id')
const [range] = await db
.table('ranges')
.insert({ group_id: group.id, start_id: 1, end_id: 1_000_000, next_id: 1, status: 'leased', node_id: node.id, rule_version: 1 })
.insert({
group_id: group.id,
start_id: 1,
end_id: 1_000_000,
next_id: 1,
status: 'leased',
node_id: node.id,
rule_version: 1,
})
.returning('id')
return { rangeId: Number(range.id), nodeId: Number(node.id) }
}
function indexer() {
return new RangeIndexer({ group: GROUP, start: 1, end: 1_000_000, rules: { ...DEFAULT_RULES, samplePercent: 0, droppedSignatures: [], priorities: { [GROUP]: 1 } }, edgeArticles: 1000 })
return new RangeIndexer({
group: GROUP,
start: 1,
end: 1_000_000,
rules: {
...DEFAULT_RULES,
samplePercent: 0,
droppedSignatures: [],
priorities: { [GROUP]: 1 },
},
edgeArticles: 1000,
})
}
/** One file's articles, posted `minutes` apart from `start` (an ISO time), in `group`. */
function upload(at: number, name: string, parts: number, counter: string, start: string, group = GROUP, minutes = 1): Overview[] {
function upload(
at: number,
name: string,
parts: number,
counter: string,
start: string,
group = GROUP,
minutes = 1
): Overview[] {
return Array.from({ length: parts }, (_, i) => {
id++
const date = new Date(Date.parse(start) + i * minutes * 60_000).toUTCString().replace('GMT', '+0000')
return [at + i, { 'subject': `${counter} - "${name}" yEnc (${i + 1}/${parts})`, 'from': 'p <p@x>', 'date': date, 'message-id': `<up${id}@test>`, ':bytes': '700000', 'xref': `news.example ${group}:${at + i}` }] as Overview
const date = new Date(Date.parse(start) + i * minutes * 60_000)
.toUTCString()
.replace('GMT', '+0000')
return [
at + i,
{
'subject': `${counter} - "${name}" yEnc (${i + 1}/${parts})`,
'from': 'p <p@x>',
'date': date,
'message-id': `<up${id}@test>`,
':bytes': '700000',
'xref': `news.example ${group}:${at + i}`,
},
] as Overview
})
}
async function rangeIn(groupName: string, start: number, nodeId?: number) {
const g = (await db.from('groups').where('name', groupName).first()) ?? (await db.table('groups').insert({ name: groupName, priority: groupName === GROUP ? 1 : 2 }).returning('*'))[0]
const node = nodeId ?? Number((await db.table('nodes').insert({ name: `n${groupName}${start}` }).returning('id'))[0].id)
const [range] = await db.table('ranges').insert({ group_id: g.id, start_id: start, end_id: start + 99_999, next_id: start, status: 'leased', node_id: node, rule_version: 1 }).returning('id')
const g =
(await db.from('groups').where('name', groupName).first()) ??
(
await db
.table('groups')
.insert({ name: groupName, priority: groupName === GROUP ? 1 : 2 })
.returning('*')
)[0]
const node =
nodeId ??
Number(
(
await db
.table('nodes')
.insert({ name: `n${groupName}${start}` })
.returning('id')
)[0].id
)
const [range] = await db
.table('ranges')
.insert({
group_id: g.id,
start_id: start,
end_id: start + 99_999,
next_id: start,
status: 'leased',
node_id: node,
rule_version: 1,
})
.returning('id')
return { rangeId: Number(range.id), nodeId: node }
}
function batchOf(groupName: string, start: number, overviews: Overview[]): EdgeBatch {
const ix = new RangeIndexer({
group: groupName,
start,
end: start + 99_999,
rules: {
...DEFAULT_RULES,
samplePercent: 0,
droppedSignatures: [],
priorities: { [GROUP]: 1, 'alt.binaries.other': 2 },
},
edgeArticles: 1,
})
ix.add(overviews, start + 100_000)
ix.finish()
return ix.take()
}
async function storeIn(groupName: string, start: number, overviews: Overview[]) {
const { rangeId, nodeId } = await rangeIn(groupName, start)
const ix = new RangeIndexer({ group: groupName, start, end: start + 99_999, rules: { ...DEFAULT_RULES, samplePercent: 0, droppedSignatures: [], priorities: { [GROUP]: 1, 'alt.binaries.other': 2 } }, edgeArticles: 1 })
ix.add(overviews, start + 100_000)
ix.finish()
await BatchStore.store(rangeId, nodeId, ix.take())
return BatchStore.store(rangeId, nodeId, batchOf(groupName, start, overviews))
}
const releasesOf = () => db.from('releases').orderBy('id').select('release_key', 'identity_key', 'files', 'posted_at', 'last_posted_at')
const releasesOf = () =>
db
.from('releases')
.orderBy('id')
.select(
db.raw(
`encode(release_key, 'hex') AS release_key, encode(identity_key, 'hex') AS identity_key, files, chunks, posted_at, last_posted_at, settled_at`
)
)
test.group('BatchStore', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test('stores releases, their chunks, probe candidates, rule counts and fragments, and moves the resume point', async ({ assert }) => {
test('stores releases by id references, their texts, chunks, probe candidates and rule counts, and moves the resume point', async ({
assert,
}) => {
const { rangeId, nodeId } = await leasedRange()
const ix = indexer()
ix.add([
...post(100_000, 'Some.Show.S01E01.1080p.WEB.part1.rar', 2, 'Some.Show.S01E01 [1/2]'),
...post(100_010, 'Some.Show.S01E01.1080p.WEB.par2', 1, 'Some.Show.S01E01 [2/2]'),
...post(100_020, 'Hs7dKq2LmZ9xWv3RtYp1Qa.part01.rar', 3, '[1/1]', 'ngPost'),
...post(10, 'Edge.Cut.2020.part1.rar', 3, 'Edge.Cut [1/1]').slice(1),
], 500_000)
ix.add(
[
...post(100_000, 'Some.Show.S01E01.1080p.WEB.part1.rar', 2, 'Some.Show.S01E01 [1/2]'),
...post(100_010, 'Some.Show.S01E01.1080p.WEB.par2', 1, 'Some.Show.S01E01 [2/2]'),
...post(100_020, 'Hs7dKq2LmZ9xWv3RtYp1Qa.part01.rar', 3, '[1/1]', 'ngPost'),
],
500_000
)
const first = ix.take()
const stored = await BatchStore.store(rangeId, nodeId, first)
assert.include(stored, { releases: 1, newReleases: 1, candidates: 2, fragments: 0 })
assert.deepEqual(stored, { releases: 1, newReleases: 1, finalized: 0, candidates: 2 })
const release = await db.from('releases').first()
assert.include(release, { files: 2, segments: '3', chunks: 1, readable: true, has_par2: true, file_total: 2 })
assert.include(release, {
files: 2,
segments: '3',
chunks: 1,
readable: true,
has_par2: true,
file_total: 2,
complete: true,
edge: false,
source: 1,
})
assert.isNotNull(release.settled_at)
// The poster and the groups by id, the keys 20 bytes, the name searchable.
assert.equal(
(await db.from('posters').where('id', release.poster_id).firstOrFail()).poster,
'p <p@x>'
)
assert.deepEqual(
(await db.from('group_sets').where('id', release.group_set_id).firstOrFail()).groups,
[GROUP]
)
assert.lengthOf(release.release_key, 20)
assert.lengthOf(release.identity_key, 20)
assert.deepEqual(await db.from('release_texts').select('release_id', 'kind', 'text'), [
{ release_id: release.id, kind: 1, text: 'Some Show S01E01' },
])
const chunk = await db.from('release_chunks').where('release_id', release.id).first()
assert.deepEqual(decodeChunk(chunk.payload).map((f) => f.name).sort(), ['Some.Show.S01E01.1080p.WEB.par2', 'Some.Show.S01E01.1080p.WEB.part1.rar'])
assert.deepEqual((await db.from('probe_candidates').orderBy('kind')).map((c) => c.kind), ['archive', 'par2'])
assert.deepEqual((await db.from('rule_counts')).map((c) => [c.rule, c.key, Number(c.articles)]), [['garbage', 'ngPost alt.binaries.test', 3]])
assert.equal(Number((await db.from('ranges').where('id', rangeId).first()).next_id), 500_000)
assert.deepEqual(
decodeChunk(chunk.payload)
.map((f) => f.name)
.sort(),
['Some.Show.S01E01.1080p.WEB.par2', 'Some.Show.S01E01.1080p.WEB.part1.rar']
)
// Claimable at once: the release is finished.
assert.deepEqual(
(await db.from('probe_candidates').orderBy('kind')).map((c) => [
c.kind,
c.ready,
c.unreadable,
]),
[
['archive', true, false],
['par2', true, false],
]
)
// Keyed by the range's block (a range without one is a block of its own, until blocks are leased).
const range = await db.from('ranges').where('id', rangeId).first()
assert.isNotNull(range.block_id)
assert.equal(Number(chunk.block_id), Number(range.block_id))
assert.deepEqual(
(await db.from('rule_counts')).map((c) => [
Number(c.block_id),
c.rule,
c.key,
Number(c.articles),
]),
[[Number(range.block_id), 'garbage', 'ngPost alt.binaries.test', 3]]
)
assert.equal(Number(range.next_id), 500_000)
// The same batch again (a retry after a lost reply) is refused, and changes nothing.
await assert.rejects(() => BatchStore.store(rangeId, nodeId, first), StaleBatch)
assert.equal((await db.from('releases').first()).files, 2)
// The end of the range: the cut-off file near the start edge is kept as a fragment.
// The end of the range.
ix.finish()
const last = ix.take()
assert.include(await BatchStore.store(rangeId, nodeId, last), { fragments: 1 })
await BatchStore.store(rangeId, nodeId, ix.take())
assert.equal(Number((await db.from('ranges').where('id', rangeId).first()).next_id), 1_000_001)
})
@@ -97,52 +258,192 @@ test.group('BatchStore', (group) => {
const { rangeId, nodeId } = await leasedRange()
const ix = indexer()
ix.add(post(100_000, 'A.Release.2020.mkv', 1, 'A [1/1]'), 200_000)
await assert.rejects(() => BatchStore.store(rangeId, nodeId + 1, ix.take()), /not leased to this node/)
await assert.rejects(
() => BatchStore.store(rangeId, nodeId + 1, ix.take()),
/not leased to this node/
)
})
test('a re-upload is a release of its own: in one range, or a later range; a continuation joins', async ({ assert }) => {
// One range: the same file posted Monday and again Thursday.
test('every chunk is a release of its own: a re-upload, and a part read by another range (the block merger joins those)', async ({
assert,
}) => {
// One range: the same file posted Monday and again Thursday are two uploads.
await storeIn(GROUP, 1, [
...upload(10, 'Some.Show.S01E01.1080p.mkv', 2, 'Some.Show.S01E01.1080p [1/2]', '2026-10-05T10:00:00Z'),
...upload(20, 'Some.Show.S01E01.1080p.par2', 1, 'Some.Show.S01E01.1080p [2/2]', '2026-10-05T10:05:00Z'),
...upload(500, 'Some.Show.S01E01.1080p.mkv', 2, 'Some.Show.S01E01.1080p [1/2]', '2026-10-08T10:00:00Z'),
...upload(
10,
'Some.Show.S01E01.1080p.mkv',
2,
'Some.Show.S01E01.1080p [1/2]',
'2026-10-05T10:00:00Z'
),
...upload(
20,
'Some.Show.S01E01.1080p.par2',
1,
'Some.Show.S01E01.1080p [2/2]',
'2026-10-05T10:05:00Z'
),
...upload(
500,
'Some.Show.S01E01.1080p.mkv',
2,
'Some.Show.S01E01.1080p [1/2]',
'2026-10-08T10:00:00Z'
),
])
let rows = await releasesOf()
assert.lengthOf(rows, 2)
assert.equal(rows[1].release_key, `${rows[0].release_key}:2`)
assert.deepEqual(rows.map((r) => [r.identity_key, r.files]), [[rows[0].release_key, 2], [rows[0].release_key, 1]])
assert.deepEqual(
rows.map((r) => r.files),
[2, 1]
)
assert.equal(rows[0].identity_key, rows[1].identity_key)
assert.notEqual(rows[0].release_key, rows[1].release_key)
// The next range: Thursday's par2 an hour later joins Thursday's release; a Saturday copy is a third.
await storeIn(GROUP, 100_001, upload(100_010, 'Some.Show.S01E01.1080p.par2', 1, 'Some.Show.S01E01.1080p [2/2]', '2026-10-08T11:00:00Z'))
await storeIn(GROUP, 200_001, upload(200_010, 'Some.Show.S01E01.1080p.mkv', 2, 'Some.Show.S01E01.1080p [1/2]', '2026-10-10T10:00:00Z'))
// The next range: Thursday's par2 an hour later is written as a release of its own (its key: the identity, the
// group and its first article); the window keeps such parts together, and the block merger joins the edges.
await storeIn(
GROUP,
100_001,
upload(
100_010,
'Some.Show.S01E01.1080p.par2',
1,
'Some.Show.S01E01.1080p [2/2]',
'2026-10-08T11:00:00Z'
)
)
rows = await releasesOf()
assert.deepEqual(rows.map((r) => [r.release_key.split(':')[1] ?? '1', r.files]), [['1', 2], ['2', 2], ['3', 1]])
const chunks = await db.from('release_chunks').count('* as n').first()
assert.equal(Number(chunks!.n), 4)
assert.deepEqual(
rows.map((r) => r.files),
[2, 1, 1]
)
assert.lengthOf(new Set(rows.map((r) => r.release_key)), 3)
assert.lengthOf(new Set(rows.map((r) => r.identity_key)), 1)
assert.equal(Number((await db.from('release_chunks').count('* as n').first())!.n), 3)
})
test('candidates added to a settled release (a later range of its upload) are claimable at once', async ({ assert }) => {
await storeIn(GROUP, 1, upload(10, 'Some.Show.S01E02.1080p.mkv', 2, 'Some.Show.S01E02.1080p [1/2]', '2026-10-05T10:00:00Z'))
// Settled before the next range came (the settler marks only what it settles).
await db.from('releases').update({ settled_at: new Date() })
await db.from('probe_candidates').update({ ready: true })
await storeIn(GROUP, 100_001, upload(100_010, 'Some.Show.S01E02.1080p.par2', 1, 'Some.Show.S01E02.1080p [2/2]', '2026-10-05T10:20:00Z'))
const candidates = await db.from('probe_candidates').orderBy('id').select('kind', 'ready')
assert.deepEqual(candidates.map((c) => c.kind), ['par2'])
assert.isTrue(candidates.every((c) => c.ready))
assert.lengthOf(await releasesOf(), 1)
test('a provisional tail release is finished once by a later chunk of its key, which adds the files not sent before', async ({
assert,
}) => {
const { rangeId, nodeId } = await rangeIn(GROUP, 1)
const batch = batchOf(
GROUP,
1,
upload(
10,
'Some.Show.S01E02.1080p.mkv',
2,
'Some.Show.S01E02.1080p [1/2]',
'2026-10-05T10:00:00Z'
)
)
assert.lengthOf(batch.releases, 1)
const provisional: ReleaseChunk = { ...batch.releases[0], finished: false }
const stored = await BatchStore.store(rangeId, nodeId, {
...batch,
next: 50_000,
releases: [provisional],
})
assert.include(stored, { newReleases: 1, finalized: 0 })
let [row] = await releasesOf()
assert.isNull(row.settled_at)
assert.include(row, { files: 1, chunks: 1 })
// The finish: the par2 that came within the window, under the same key (the identity, the group, the first
// article), with its probe candidate: claimable at once, the release being finished.
const second = batchOf(
GROUP,
1,
upload(
30,
'Some.Show.S01E02.1080p.par2',
1,
'Some.Show.S01E02.1080p [2/2]',
'2026-10-05T10:20:00Z'
)
)
const [par2] = second.releases
const finish: ReleaseChunk = {
...par2,
key: provisional.key,
upload: provisional.upload,
firstArticle: provisional.firstArticle,
finished: true,
}
const candidates = second.candidates.map((c) => ({
...c,
releaseKey: provisional.key,
upload: provisional.upload,
}))
assert.deepEqual(
candidates.map((c) => c.kind),
['par2']
)
const finished = await BatchStore.store(rangeId, nodeId, {
...batch,
from: 50_000,
next: 60_000,
releases: [finish],
candidates,
counts: [],
samples: [],
fragments: [],
})
assert.include(finished, { releases: 1, newReleases: 0, finalized: 1, candidates: 1 })
;[row] = await releasesOf()
assert.isNotNull(row.settled_at)
assert.include(row, { files: 2, chunks: 2 })
const release = await db.from('releases').firstOrFail()
assert.include(release, { complete: true, has_par2: true, last_article: '30' })
assert.deepEqual(await db.from('probe_candidates').select('kind', 'ready'), [
{ kind: 'par2', ready: true },
])
assert.deepEqual(
(await db.from('release_chunks').orderBy('id')).flatMap((c) =>
decodeChunk(c.payload).map((f) => f.name)
),
['Some.Show.S01E02.1080p.mkv', 'Some.Show.S01E02.1080p.par2']
)
// Sent once more, finished already: left as it is.
const again = await BatchStore.store(rangeId, nodeId, {
...batch,
from: 60_000,
next: 70_000,
releases: [{ ...finish, payload: encodeChunk([]), files: 0, segments: 0, bytes: 0 }],
candidates: [],
counts: [],
samples: [],
fragments: [],
})
assert.include(again, { releases: 1, newReleases: 0, finalized: 0 })
assert.include((await releasesOf())[0], { files: 2, chunks: 2 })
assert.equal(Number((await db.from('release_chunks').count('* as n').first())!.n), 2)
})
test('a split 7z read in pieces keeps the tail of its highest volume only; RAR gets none', async ({ assert }) => {
// As the live tail reads it while posted: each piece queues a tail for the last volume it saw so far.
test('a split 7z keeps the tail of its highest volume only; RAR gets none', async ({
assert,
}) => {
await storeIn(GROUP, 1, [
...upload(10, 'TsIgNBewuFbuwWy.7z.001', 2, '[1/4]', '2026-10-23T08:24:00Z'),
...upload(20, 'TsIgNBewuFbuwWy.7z.002', 2, '[2/4]', '2026-10-23T08:26:00Z'),
...upload(30, 'TsIgNBewuFbuwWy.7z.003', 2, '[3/4]', '2026-10-23T08:28:00Z'),
...upload(40, 'TsIgNBewuFbuwWy.7z.004', 2, '[4/4]', '2026-10-23T08:30:00Z'),
...upload(
50,
'Some.Show.S01E03.part01.rar',
2,
'Some.Show.S01E03 [1/2]',
'2026-10-23T09:00:00Z'
),
...upload(
60,
'Some.Show.S01E03.part02.rar',
2,
'Some.Show.S01E03 [2/2]',
'2026-10-23T09:02:00Z'
),
])
await storeIn(GROUP, 100_001, upload(100_010, 'TsIgNBewuFbuwWy.7z.004', 2, '[4/4]', '2026-10-23T08:30:00Z'))
// A lower volume posted late: its tail goes at once.
await storeIn(GROUP, 200_001, upload(200_010, 'TsIgNBewuFbuwWy.7z.003', 2, '[3/4]', '2026-10-23T08:28:00Z'))
await storeIn(GROUP, 300_001, upload(300_010, 'Some.Show.S01E03.part01.rar', 2, 'Some.Show.S01E03 [1/2]', '2026-10-23T09:00:00Z'))
await storeIn(GROUP, 400_001, upload(400_010, 'Some.Show.S01E03.part02.rar', 2, 'Some.Show.S01E03 [2/2]', '2026-10-23T09:02:00Z'))
const candidates = await db.from('probe_candidates').orderBy('file').select('kind', 'file')
assert.deepEqual(
candidates.map((c) => [c.kind, c.file]),
@@ -154,43 +455,36 @@ test.group('BatchStore', (group) => {
)
})
test('the same upload in another group (multi-posted) is a release of its own', async ({ assert }) => {
await storeIn(GROUP, 1, upload(10, 'Some.Show.S01E01.1080p.mkv', 2, 'Some.Show.S01E01.1080p [1/1]', '2026-10-05T10:00:00Z'))
await storeIn('alt.binaries.other', 1, upload(10, 'Some.Show.S01E01.1080p.mkv', 2, 'Some.Show.S01E01.1080p [1/1]', '2026-10-05T10:01:00Z', 'alt.binaries.other'))
test('the same upload in another group (multi-posted) is a release of its own', async ({
assert,
}) => {
await storeIn(
GROUP,
1,
upload(
10,
'Some.Show.S01E01.1080p.mkv',
2,
'Some.Show.S01E01.1080p [1/1]',
'2026-10-05T10:00:00Z'
)
)
await storeIn(
'alt.binaries.other',
1,
upload(
10,
'Some.Show.S01E01.1080p.mkv',
2,
'Some.Show.S01E01.1080p [1/1]',
'2026-10-05T10:01:00Z',
'alt.binaries.other'
)
)
const rows = await releasesOf()
assert.lengthOf(rows, 2)
assert.equal(rows[0].identity_key, rows[1].identity_key)
})
test('an upload spread over groups (other files in another group) is one release', async ({ assert }) => {
await storeIn(GROUP, 1, upload(10, 'Some.Album-WEB-2026-FTD.part1.rar', 2, 'Some.Album-WEB-2026-FTD [2/3]', '2026-10-05T10:00:00Z'))
await storeIn('alt.binaries.other', 1, upload(10, 'Some.Album-WEB-2026-FTD.part2.rar', 2, 'Some.Album-WEB-2026-FTD [3/3]', '2026-10-05T10:00:30Z', 'alt.binaries.other'))
const rows = await releasesOf()
assert.lengthOf(rows, 1)
assert.equal(rows[0].files, 2)
})
test('a release key taken meanwhile by another group\'s writer is skipped for the next number', async ({ assert }) => {
await storeIn(GROUP, 1, upload(10, 'Some.Show.S01E01.1080p.mkv', 2, 'Some.Show.S01E01.1080p [1/1]', '2026-10-05T10:00:00Z'))
const [first] = await releasesOf()
// As if another group's batch numbered the key at the same moment: key:2 exists, not counted for this key.
const [g] = await db.table('groups').insert({ name: 'alt.binaries.third', priority: 3 }).returning('id')
await db.table('releases').insert({ release_key: `${first.identity_key}:2`, identity_key: 'someone-else', group_id: g.id, name: 'x', source: 'subject', poster: 'p', groups: JSON.stringify([]), files: 0, segments: 0, bytes: 0, chunks: 0 })
await storeIn('alt.binaries.other', 1, upload(10, 'Some.Show.S01E01.1080p.mkv', 2, 'Some.Show.S01E01.1080p [1/1]', '2026-10-05T10:01:00Z', 'alt.binaries.other'))
const keys = (await releasesOf()).filter((r) => r.identity_key === first.identity_key).map((r) => r.release_key)
assert.deepEqual(keys, [first.identity_key, `${first.identity_key}:3`])
})
test('a re-upload is numbered after the highest number, not into the holes merges left', async ({ assert }) => {
for (let day = 0; day < 4; day++) {
await storeIn(GROUP, 1 + day * 100_000, upload(10 + day * 100_000, 'Some.Show.S01E01.1080p.mkv', 2, 'Some.Show.S01E01.1080p [1/1]', `2026-10-0${day * 2 + 1}T10:00:00Z`))
}
const [first] = await releasesOf()
// Merged away: key:2 and key:3 gone, key and key:4 left.
await db.from('releases').whereIn('release_key', [`${first.identity_key}:2`, `${first.identity_key}:3`]).delete()
await storeIn(GROUP, 400_001, upload(400_010, 'Some.Show.S01E01.1080p.mkv', 2, 'Some.Show.S01E01.1080p [1/1]', '2026-10-09T10:00:00Z'))
const keys = (await releasesOf()).map((r) => r.release_key.split(':')[1] ?? '1')
assert.deepEqual(keys, ['1', '4', '5'])
assert.notEqual(rows[0].release_key, rows[1].release_key)
})
test('a small release of sidecars only is marked for the sidecar fold', async ({ assert }) => {
@@ -199,7 +493,12 @@ test.group('BatchStore', (group) => {
...upload(1_000_010, 'Some.Tool.v1.zip', 1, '[01/01]', '2026-10-05T10:00:00Z'),
...upload(1_000_020, 'Some.Show.S01E01.part01.rar', 2, '[02/30]', '2026-10-05T10:00:00Z'),
])
const flags = new Map((await db.from('releases').select('name', 'sidecar_only')).map((r) => [r.name, r.sidecar_only]))
const flags = new Map(
(await db.from('releases').select('name', 'sidecar_only')).map((r) => [
r.name,
r.sidecar_only,
])
)
assert.isTrue(flags.get('Murdoch Mysteries S8D1'))
assert.isFalse(flags.get('Some.Tool.v1'))
// Over 1 MB: not looked at.
+57
View File
@@ -0,0 +1,57 @@
import db from '@adonisjs/lucid/services/db'
import CatalogRefs from '#services/catalog_refs'
import { identityKey, releaseKey } from '#lib/release/keys'
import { sourceCode } from '#lib/release/source'
/** A release as the tests describe one: text keys, a poster and groups by name, the source by name. */
export interface ReleaseFixture {
/** Any text; the stored key is its hash (keyOf), or 40 hex characters decoded. Default: from the identity, group and first article. */
release_key?: string
/** Any text (default: release_key, else the name). */
identity_key?: string
name: string
source?: string
poster?: string | null
groups?: string[] | string
group_id?: number | null
first_article?: number | null
[column: string]: unknown
}
/** The bytes a text key is stored as (identityKey: 40 hex characters decoded, anything else hashed). */
export const keyOf = identityKey
/** Inserts releases with their poster, group set, keys and source as the catalog stores them; their ids in order. */
export async function insertReleases(fixtures: ReleaseFixture[]): Promise<number[]> {
const ids: number[] = []
for (const f of fixtures) {
const { release_key: textKey, identity_key: textIdentity, source, poster, groups, ...rest } = f
const identity = textIdentity ?? textKey ?? f.name
const groupList: string[] = typeof groups === 'string' ? JSON.parse(groups) : (groups ?? [])
const [row] = await db
.table('releases')
.insert({
...rest,
release_key: textKey
? identityKey(textKey)
: releaseKey(identity, f.group_id ?? null, f.first_article ?? null),
identity_key: identityKey(identity),
source: sourceCode(source ?? 'stem'),
poster_id: poster ? await CatalogRefs.posterId(db, poster) : null,
group_set_id: await CatalogRefs.groupSetId(db, groupList),
})
.returning('id')
const id = Number(row.id)
await db.rawQuery(
`INSERT INTO release_texts (release_id, kind, text) VALUES (?, 1, left(?, 255)) ON CONFLICT DO NOTHING`,
[id, f.name]
)
ids.push(id)
}
return ids
}
export async function insertRelease(fixture: ReleaseFixture): Promise<number> {
const [id] = await insertReleases([fixture])
return id
}
+3 -1
View File
@@ -77,7 +77,9 @@ test.group('Node API', (group) => {
// The same batch again: refused.
;(await client.post(`/api/v1/node/ranges/${rangeId}/batches`).bearerToken(token).json(batch)).assertStatus(409)
assert.deepEqual((await db.from('rule_counts').where('range_id', rangeId)).map((c) => [c.rule, c.key, Number(c.articles)]), [['obfuscated', '', 3]])
// Counts by the range's block (a range without one is a block of its own, until blocks are leased).
const { block_id: blockId } = await db.from('ranges').where('id', rangeId).firstOrFail()
assert.deepEqual((await db.from('rule_counts').where('block_id', blockId)).map((c) => [c.rule, c.key, Number(c.articles)]), [['obfuscated', '', 3]])
;(await client.post(`/api/v1/node/ranges/${rangeId}/finish`).bearerToken(token)).assertStatus(200)
const range = await db.from('ranges').where('id', rangeId).first()
+9 -9
View File
@@ -45,23 +45,23 @@ test.group('par2 completeness', (group) => {
assert.isFalse(par2Complete(set, [{ name: 'Qx81.bin', size: 500 }, { name: 'Zr02.bin', size: 12 }], []))
})
test('decided once settled with a par2 set, and kept when more files come', async ({ assert }) => {
test('decided once finished, by the par2 set the release holds', async ({ assert }) => {
await store(1, [...file('Some.Upload.mkv', 3, 10), ...file('Some.Upload.par2', 1, 20)])
const release = await db.from('releases').firstOrFail()
assert.isNull(release.file_total)
await db.table('par2_files').multiInsert([setFile('Some.Upload.mkv', 2_000_000), setFile('Some.Upload.nfo', 100)].map((f, i) => ({ set_id: 's'.repeat(32), file_id: String(i).repeat(32), ...f })))
await db.table('par2_sets').insert({ set_id: 's'.repeat(32), release_id: release.id, files: 2 })
// Not settled yet: left alone.
// A provisional tail release (not finished yet): left alone.
await db.from('releases').where('id', release.id).update({ settled_at: null })
assert.equal(await decidePar2Completeness(db, [release.id]), 0)
await db.from('releases').where('id', release.id).update({ settled_at: new Date() })
assert.equal(await decidePar2Completeness(db, [release.id]), 1)
assert.isFalse((await db.from('releases').where('id', release.id).firstOrFail()).complete)
// The nfo comes in a later range: complete, and the batch store's own update keeps it.
await store(1001, file('Some.Upload.nfo', 1, 1010))
await decidePar2Completeness(db, [release.id])
await store(2001, file('Some.Upload.sfv', 1, 2010))
const after = await db.from('releases').where('id', release.id).firstOrFail()
assert.equal(after.files, 4)
assert.isTrue(after.complete)
// Another upload with the nfo: complete.
await store(1001, [...file('Some.Upload.mkv', 3, 1010), ...file('Some.Upload.par2', 1, 1020), ...file('Some.Upload.nfo', 1, 1030)])
const whole = await db.from('releases').whereNot('id', release.id).firstOrFail()
await db.table('par2_sets').insert({ set_id: 's'.repeat(32), release_id: whole.id, files: 2 })
assert.equal(await decidePar2Completeness(db, [whole.id]), 1)
assert.isTrue((await db.from('releases').where('id', whole.id).firstOrFail()).complete)
})
})
+11 -34
View File
@@ -4,6 +4,7 @@ import testUtils from '@adonisjs/core/services/test_utils'
import { Par2Merger } from '#services/par2_merger'
import { encodeChunk } from '#lib/chunks/index'
import ChunkPartitions from '#services/chunk_partitions'
import { insertRelease } from '#tests/functional/helpers/releases'
const hash = (c: string) => c.repeat(32)
@@ -13,14 +14,11 @@ test.group('Par2Merger', (group) => {
test('merges the one-file releases of an obfuscated upload by its par2 set, named by the par2', async ({ assert }) => {
const at = new Date('2022-07-08T03:46:00Z')
const release = async (key: string, posted: Date, groupId: number, files = 1) => {
const [row] = await db
.table('releases')
.insert({
const row = { id: await insertRelease({
release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'stem', groups: '[]', group_id: groupId,
files, segments: files * 10, bytes: files * 1000, first_article: 100, last_article: 200, posted_at: posted, readable: false, file_total: 20,
settled_at: posted,
})
.returning('id')
}) }
return Number(row.id)
}
// The par2 index, two data files of the upload, the same file reposted a week later, and one in another group.
@@ -95,10 +93,7 @@ test.group('Par2Merger', (group) => {
const at = new Date('2022-07-08T03:46:00Z')
const ids: number[] = []
for (const key of ['dd000001', 'dd000002']) {
const [row] = await db
.table('releases')
.insert({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'stem', groups: '[]', group_id: 1, files: 1, posted_at: at, readable: false })
.returning('id')
const row = { id: await insertRelease({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'stem', groups: '[]', group_id: 1, files: 1, posted_at: at, readable: false }) }
ids.push(Number(row.id))
}
// The par2 is held by the first; the second holds the file, and so do 20 releases elsewhere and years apart.
@@ -120,10 +115,7 @@ test.group('Par2Merger', (group) => {
const at = new Date('2022-07-08T03:46:00Z')
const ids: number[] = []
for (const [key, files] of [['bb000001', 1], ['bb000002', 1], ['bb000003', 3]] as const) {
const [row] = await db
.table('releases')
.insert({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'stem', groups: '[]', group_id: 1, files, posted_at: at, readable: false })
.returning('id')
const row = { id: await insertRelease({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'stem', groups: '[]', group_id: 1, files, posted_at: at, readable: false }) }
ids.push(Number(row.id))
}
const setId = hash('7')
@@ -145,10 +137,7 @@ test.group('Par2Merger', (group) => {
const ids: number[] = []
for (let i = 1; i <= 6; i++) {
const key = `cc00000${i}`
const [row] = await db
.table('releases')
.insert({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: `kantodo${i}`, source: 'stem', groups: '[]', group_id: 1, files: 11, posted_at: at, readable: true })
.returning('id')
const row = { id: await insertRelease({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: `kantodo${i}`, source: 'stem', groups: '[]', group_id: 1, files: 11, posted_at: at, readable: true }) }
ids.push(Number(row.id))
await db.table('par2_sets').insert({ set_id: setId, release_id: row.id, files: 1 })
}
@@ -163,10 +152,7 @@ test.group('Par2Merger', (group) => {
const release = async (poster: string, posted: Date, total: number, inner: string | null, groupId = 1) => {
n++
const key = `dd${String(n).padStart(6, '0')}`
const [row] = await db
.table('releases')
.insert({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: `hex${n}`, source: 'stem', groups: '[]', group_id: groupId, files: 1, posted_at: posted, readable: false, file_total: total, poster })
.returning('id')
const row = { id: await insertRelease({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: `hex${n}`, source: 'stem', groups: '[]', group_id: groupId, files: 1, posted_at: posted, readable: false, file_total: total, poster }) }
if (inner) {
await db.table('release_inspections').insert({ release_id: row.id, candidate_id: 5000 + n, kind: 'lone', result: JSON.stringify({ yencName: inner, type: '', size: 1 }) })
}
@@ -202,10 +188,7 @@ test.group('Par2Merger', (group) => {
const ids: number[] = []
for (let i = 1; i <= 3; i++) {
const key = `ee00000${i}`
const [row] = await db
.table('releases')
.insert({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'stem', groups: '[]', group_id: 1, files: 10, posted_at: at, readable: false, file_total: 5, poster: 'p' })
.returning('id')
const row = { id: await insertRelease({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'stem', groups: '[]', group_id: 1, files: 10, posted_at: at, readable: false, file_total: 5, poster: 'p' }) }
ids.push(Number(row.id))
await db.table('release_inspections').insert({ release_id: row.id, candidate_id: 6000 + i, kind: 'lone', result: JSON.stringify({ yencName: `${stem}.7z.00${i}` }) })
}
@@ -217,10 +200,7 @@ test.group('Par2Merger', (group) => {
const at = new Date('2022-07-08T03:46:00Z')
const ids: number[] = []
for (const key of ['aa000001', 'aa000002', 'aa000003']) {
const [row] = await db
.table('releases')
.insert({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'stem', groups: '[]', group_id: 1, files: 1, posted_at: at, readable: false })
.returning('id')
const row = { id: await insertRelease({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'stem', groups: '[]', group_id: 1, files: 1, posted_at: at, readable: false }) }
ids.push(Number(row.id))
}
// Two test uploads whose 1 GiB files start alike, and a set describing one such file, held by the third.
@@ -242,13 +222,10 @@ test.group('Par2Merger', (group) => {
const release = async (name: string, files: number, total: number | null, posted: Date, chunkFiles: string[]) => {
n++
const key = `ff${String(n).padStart(6, '0')}`.padEnd(40, '0')
const [row] = await db
.table('releases')
.insert({ release_key: key, identity_key: key, name, source: 'stem', groups: '[]', group_id: 1, files, file_total: total, poster, posted_at: posted, readable: true, settled_at: posted })
.returning('id')
const row = { id: await insertRelease({ release_key: key, identity_key: key, name, source: 'stem', groups: '[]', group_id: 1, files, file_total: total, poster, posted_at: posted, readable: true, settled_at: posted }) }
const id = Number(row.id)
const payload = encodeChunk(chunkFiles.map((f, i) => ({ name: f, poster, subject: `${name} [${String(i).padStart(2, '0')}/${total ?? 0}] - "${f}" yEnc`, postedAt: posted.getTime(), groups: ['a.b.test'], segments: [[1, `${id}-${i}@x`, 1000]] })))
await db.table('release_chunks').insert({ release_id: id, range_id: 1, batch_from: 1, files: chunkFiles.length, segments: chunkFiles.length, payload })
await db.table('release_chunks').insert({ release_id: id, block_id: 1, batch_from: 1, files: chunkFiles.length, segments: chunkFiles.length, payload })
await db.table('fold_candidates').insert({ release_id: id }).onConflict('release_id').ignore()
return id
}
+2 -2
View File
@@ -48,7 +48,7 @@ test.group('Par2Merger / by file names', (group) => {
...file('Track One.flac', 100_060, 5, 'other <o@x>'),
])
await store(file('Track Two.flac', 200_070, 24 * 60 + 10))
const ids = Object.fromEntries((await db.from('releases').select('id', 'name', 'poster', 'posted_at')).map((r) => [`${r.name}${r.poster.startsWith('other') ? ' (other)' : ''}${new Date(r.posted_at).getUTCDate() === 21 ? ' (later)' : ''}`, Number(r.id)]))
const ids = Object.fromEntries((await db.from('releases as r').leftJoin('posters as p', 'p.id', 'r.poster_id').select('r.id', 'r.name', 'p.poster', 'r.posted_at')).map((r) => [`${r.name}${r.poster.startsWith('other') ? ' (other)' : ''}${new Date(r.posted_at).getUTCDate() === 21 ? ' (later)' : ''}`, Number(r.id)]))
const index = ids['Some.Album-WEB-2026']
const setId = 'a'.repeat(32)
await db.table('par2_files').multiInsert(
@@ -57,7 +57,7 @@ test.group('Par2Merger / by file names', (group) => {
await db.table('par2_sets').insert({ set_id: setId, release_id: index, files: 3 })
assert.equal(await new Par2Merger().mergeByName(setId), 3)
const left = (await db.from('releases').orderBy('name').select('name', 'files', 'poster')).map((r) => `${r.name} ${r.files}${r.poster.startsWith('other') ? ' (other)' : ''}`)
const left = (await db.from('releases as r').leftJoin('posters as p', 'p.id', 'r.poster_id').orderBy('r.name').select('r.name', 'r.files', 'p.poster')).map((r) => `${r.name} ${r.files}${r.poster.startsWith('other') ? ' (other)' : ''}`)
assert.includeMembers(left, ['Another Song 1', 'Track One 1 (other)', 'Track Two 1'])
assert.lengthOf(left, 4)
const merged = await db.from('release_merges').where('via', 'name').count('* as n')
+6 -8
View File
@@ -2,6 +2,7 @@ import { test } from '@japa/runner'
import db from '@adonisjs/lucid/services/db'
import testUtils from '@adonisjs/core/services/test_utils'
import { Par2Pruner } from '#services/par2_pruner'
import { insertRelease } from '#tests/functional/helpers/releases'
test.group('Par2Pruner', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
@@ -12,10 +13,7 @@ test.group('Par2Pruner', (group) => {
const release = async () => {
n++
const key = `pp${String(n).padStart(6, '0')}`.padEnd(40, '0')
const [row] = await db
.table('releases')
.insert({ release_key: key, identity_key: key, name: key, source: 'stem', groups: '[]', group_id: 1, files: 2, readable: false })
.returning('id')
const row = { id: await insertRelease({ release_key: key, identity_key: key, name: key, source: 'stem', groups: '[]', group_id: 1, files: 2, readable: false }) }
return Number(row.id)
}
let candidate = 5000
@@ -59,12 +57,12 @@ test.group('Par2Pruner', (group) => {
const posted = new Date('2024-03-01T12:00:00Z')
const [group] = await db.table('groups').insert({ name: 'alt.binaries.pruned', priority: 1 }).returning('id')
const groupId = Number(group.id)
await db.from('releases').where('id', f).update({ group_id: groupId, posted_at: posted, last_posted_at: posted, first_article: 5000, last_article: 5100 })
// Finished (the window closed on it) long ago; its range done since.
await db.from('releases').where('id', f).update({ group_id: groupId, posted_at: posted, last_posted_at: posted, first_article: 5000, last_article: 5100, settled_at: old })
const [range] = await db
.table('ranges')
.insert({ group_id: groupId, start_id: 1, end_id: 10000, next_id: 10001, status: 'done', done_at: old, posted_low: new Date('2024-02-28T00:00:00Z'), posted_high: new Date('2024-03-03T00:00:00Z') })
.returning('id')
await db.table('range_blocks').insert({ group_id: groupId, start_id: 1, end_id: 10000, posted_low: new Date('2024-02-28T00:00:00Z'), posted_high: new Date('2024-03-03T00:00:00Z'), at_group_start: false })
// A lone candidate unprobed since before the range was done: its hash may still match. Kept.
await db.table('probe_candidates').insert({ id: 9001, release_id: b, kind: 'lone', message_id: '<9001@x>', file: 'x', status: 'waiting', created_at: new Date(old.getTime() - 3600_000) })
@@ -87,7 +85,7 @@ test.group('Par2Pruner', (group) => {
// A hash of its files since the merger's last window: the merger hasn't matched it yet. Kept.
const g = await release()
const unseen = await set('g', g, 1, null)
await db.from('releases').where('id', g).update({ group_id: groupId, posted_at: posted, last_posted_at: posted, first_article: 6000, last_article: 6100 })
await db.from('releases').where('id', g).update({ group_id: groupId, posted_at: posted, last_posted_at: posted, first_article: 6000, last_article: 6100, settled_at: old })
await db.table('worker_cursors').insert({ name: 'merger', at: new Date(Date.now() - 3600_000) })
await db.table('probe_candidates').insert({ id: 9002, release_id: await release(), kind: 'lone', message_id: '<9002@x>', file: 'x', status: 'done' })
await db.table('file_hashes').insert({ candidate_id: 9002, release_id: 1, hash16k: 'g0'.padEnd(32, '0'), size: 1000 })
@@ -105,7 +103,7 @@ test.group('Par2Pruner', (group) => {
let candidate = 7000
for (const letter of ['p', 'q', 'r']) {
const key = `pr${letter}`.padEnd(40, '0')
const [row] = await db.table('releases').insert({ release_key: key, identity_key: key, name: key, source: 'stem', groups: '[]', group_id: 1, files: 1, readable: false }).returning('id')
const row = { id: await insertRelease({ release_key: key, identity_key: key, name: key, source: 'stem', groups: '[]', group_id: 1, files: 1, readable: false }) }
const id = Number(row.id)
const setId = letter.repeat(32)
const h = `${letter}0`.padEnd(32, '0')
+3 -5
View File
@@ -2,15 +2,13 @@ import { test } from '@japa/runner'
import db from '@adonisjs/lucid/services/db'
import testUtils from '@adonisjs/core/services/test_utils'
import { Prober } from '#services/prober'
import { insertRelease } from '#tests/functional/helpers/releases'
test.group('Prober', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test('a claim takes each tier: the readable tiers also take unreadable candidates, a kind with only one of them too', async ({ assert }) => {
const [row] = await db
.table('releases')
.insert({ release_key: 'pb'.padEnd(40, '0'), identity_key: 'pb'.padEnd(40, '0'), name: 'pb', source: 'subject', groups: '[]', files: 3 })
.returning('id')
const row = { id: await insertRelease({ release_key: 'pb'.padEnd(40, '0'), identity_key: 'pb'.padEnd(40, '0'), name: 'pb', source: 'subject', groups: '[]', files: 3 }) }
const candidate = (kind: string, unreadable: boolean, ready = true) => ({
release_id: row.id, kind, message_id: `<${kind}-${unreadable}-${ready}@x>`, file: kind, ready, unreadable,
})
@@ -26,7 +24,7 @@ test.group('Prober', (group) => {
test('a flush stores a name with a NUL in it, and one article that two releases have as a candidate', async ({ assert }) => {
const release = async (key: string) =>
Number((await db.table('releases').insert({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'subject', groups: '[]', files: 1 }).returning('id'))[0].id)
insertRelease({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'subject', groups: '[]', files: 1 })
const a = await release('fa')
const b = await release('fb')
const candidate = async (releaseId: number) =>
+8 -6
View File
@@ -1,6 +1,7 @@
import { test } from '@japa/runner'
import db from '@adonisjs/lucid/services/db'
import testUtils from '@adonisjs/core/services/test_utils'
import { insertReleases } from '#tests/functional/helpers/releases'
const categoryOf = async (name: string) => Number((await db.rawQuery(`SELECT release_category(?) AS c`, [name])).rows[0].c)
@@ -37,20 +38,21 @@ test.group('Release categories', (group) => {
})
test("the name's, else the inner name's, else a given one (a spot's), else the groups'", async ({ assert }) => {
const row = (key: string, extra: Record<string, unknown>) => ({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), source: 'stem', files: 1, groups: '["alt.binaries.misc"]', ...extra })
await db.table('releases').multiInsert([
const row = (key: string, extra: { name: string } & Record<string, unknown>) => ({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), source: 'stem', files: 1, groups: '["alt.binaries.misc"]', ...extra })
const [a, b, c, d] = await insertReleases([
row('a', { name: 'Show.S01E02.1080p.WEB' }),
row('b', { name: 'TsIgNBewuFbuwWy' }),
row('c', { name: 'TsIgNBewuFbuwWy', groups: '["alt.binaries.pictures.erotica.puffies"]' }),
// A spot's release of its own, inserted with the spot's category.
row('d', { name: 'Some spot title', category: 2040 }),
])
const byName = async () => Object.fromEntries((await db.from('releases').select('release_key', 'category')).map((r) => [r.release_key[0], r.category]))
const letters = new Map([[a, 'a'], [b, 'b'], [c, 'c'], [d, 'd']])
const byName = async () => Object.fromEntries((await db.from('releases').select('id', 'category')).map((r) => [letters.get(Number(r.id)), r.category]))
assert.deepEqual(await byName(), { a: 5040, b: 8010, c: 6060, d: 2040 })
// The prober finds an inner name; a spot links a release (the spot inspector sets a Misc one's category).
await db.from('releases').where('release_key', 'b'.padEnd(40, '0')).update({ inner_name: 'Some.Movie.2020.1080p.BluRay.x264-GRP.mkv' })
await db.from('releases').where('release_key', 'c'.padEnd(40, '0')).update({ category: 5030 })
await db.from('releases').where('release_key', 'c'.padEnd(40, '0')).update({ inner_name: 'nothing.bin' })
await db.from('releases').where('id', b).update({ inner_name: 'Some.Movie.2020.1080p.BluRay.x264-GRP.mkv' })
await db.from('releases').where('id', c).update({ category: 5030 })
await db.from('releases').where('id', c).update({ inner_name: 'nothing.bin' })
assert.deepEqual(await byName(), { a: 5040, b: 2040, c: 5030, d: 2040 })
})
+7 -27
View File
@@ -3,6 +3,7 @@ import db from '@adonisjs/lucid/services/db'
import testUtils from '@adonisjs/core/services/test_utils'
import { ReleaseJudge } from '#services/release_judge'
import SearchService from '#services/search_service'
import { insertRelease } from '#tests/functional/helpers/releases'
test.group('ReleaseJudge', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
@@ -15,9 +16,7 @@ test.group('ReleaseJudge', (group) => {
const release = async (name: string, extra: Record<string, unknown> = {}) => {
n++
const key = `jj${String(n).padStart(6, '0')}`.padEnd(40, '0')
const [row] = await db
.table('releases')
.insert({
const row = { id: await insertRelease({
release_key: key,
identity_key: key,
name,
@@ -29,8 +28,7 @@ test.group('ReleaseJudge', (group) => {
readable: true,
settled_at: settled,
...extra,
})
.returning('id')
}) }
return Number(row.id)
}
let candidate = 9000
@@ -93,10 +91,6 @@ test.group('ReleaseJudge', (group) => {
poster: 'celeborn@not.here (Celeborn)',
source: 'subject',
})
// Scraped posters are found through posters (the batch store adds each release's).
await db
.table('posters')
.multiInsert([{ poster: '@_CelebMovie_@' }, { poster: 'celeborn@not.here (Celeborn)' }])
const locked = await release('Locked.Thing', { passworded: true })
const unlocked = await release('Known.Password', { passworded: true })
@@ -104,30 +98,19 @@ test.group('ReleaseJudge', (group) => {
.table('release_passwords')
.insert({ release_id: unlocked, password: 'pw', source: 'spot-text' })
// A block of done ranges around the unusable ones (release_final); one beside an unread range isn't judged yet.
// Unusable once finished (the window closed on it); a provisional tail release isn't judged yet.
await db
.table('groups')
.insert({ id: 1, name: 'alt.binaries.judged', priority: 1 })
.onConflict('id')
.ignore()
await db.table('ranges').multiInsert([
{
group_id: 1,
start_id: 1,
end_id: 1000,
next_id: 1001,
status: 'done',
posted_low: new Date(settled.getTime() - 86400_000),
posted_high: new Date(settled.getTime() + 86400_000),
},
{ group_id: 1, start_id: 1001, end_id: 2000, next_id: 1001, status: 'planned' },
])
const placed = { first_article: 100, last_article: 110 }
const unusable = await release('ab12CD34ef56GH78ij90', { readable: false, ...placed })
const pending = await release('pp12CD34ef56GH78ij90', {
readable: false,
first_article: 990,
last_article: 1000,
settled_at: null,
posted_at: new Date(settled.getTime() + 86400_000 - 3600_000),
})
const fresh = await release('zz12CD34ef56GH78ij90', {
@@ -188,9 +171,7 @@ test.group('ReleaseJudge', (group) => {
const release = async (name: string, extra: Record<string, unknown> = {}) => {
n++
const key = `jc${String(n).padStart(6, '0')}`.padEnd(40, '0')
const [row] = await db
.table('releases')
.insert({
const row = { id: await insertRelease({
release_key: key,
identity_key: key,
name,
@@ -201,8 +182,7 @@ test.group('ReleaseJudge', (group) => {
readable: true,
settled_at: settled,
...extra,
})
.returning('id')
}) }
return Number(row.id)
}
const judge = new ReleaseJudge()
+2
View File
@@ -94,6 +94,8 @@ test.group('Releases API', (group) => {
// As the prober leaves them: the movie's archive is encrypted, the show holds a differently named file.
await db.from('releases').where('id', movie.id).update({ passworded: true, inspected_at: new Date() })
await db.from('releases').where('id', show.id).update({ passworded: false, inner_name: 'Hidden.Gem.2023.mkv', readable: false, inspected_at: new Date() })
// The name inside, searchable (release_texts, kind file: lib/release/text_kind.ts).
await db.table('release_texts').insert({ release_id: show.id, kind: 2, text: 'Hidden.Gem.2023.mkv' })
await db.table('release_inspections').insert({ release_id: show.id, candidate_id: 1, kind: 'archive', result: JSON.stringify({ yencName: 'x.rar', size: 1, type: 'rar5', names: ['Hidden.Gem.2023.mkv'], entries: 1, passworded: false }) })
const byInner = (await client.get('/api/v1/releases?q=hidden gem')).body() as { releases: { id: number; innerName: string }[] }
+2 -1
View File
@@ -2,13 +2,14 @@ import { test } from '@japa/runner'
import db from '@adonisjs/lucid/services/db'
import testUtils from '@adonisjs/core/services/test_utils'
import { pruneSevenZipTails } from '#services/seven_zip_tails'
import { insertReleases } from '#tests/functional/helpers/releases'
test.group('7z tails', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test("of a release's waiting tails, those below its highest volume go; the highest volume's all stay", async ({ assert }) => {
const row = (key: string) => ({ release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'stem', groups: '[]', files: 4 })
const [merged, other] = (await db.table('releases').multiInsert([row('a'), row('b')]).returning('id')).map((r) => r.id)
const [merged, other] = await insertReleases([row('a'), row('b')])
const tail = (releaseId: number, volume: string, mid: string, status = 'waiting') => ({ release_id: releaseId, kind: '7z-tail', message_id: mid, file: `x.7z.${volume}`, status })
await db.table('probe_candidates').multiInsert([
// Two merged members' tails, one of the highest volume probed already.
+8 -11
View File
@@ -5,6 +5,8 @@ import { Spotter } from '#services/spotter'
import { SpotInspector } from '#services/spot_inspector'
import { releaseIdentity } from '#lib/release/index'
import { parseSubject } from '#lib/headers/index'
import { SOURCES } from '#lib/release/source'
import { insertRelease } from '#tests/functional/helpers/releases'
// Message ids whose sha1('<id>') starts with 0000: self-signed spots (key 7) pass on that hashcash alone.
const SPOT = 'spottest731@spot.net'
@@ -68,10 +70,7 @@ test.group('Spots', (group) => {
// 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 r = { id: await insertRelease({ 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 }) }
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)
@@ -100,13 +99,14 @@ test.group('Spots', (group) => {
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()
const release = await db.from('releases').where('source', SOURCES.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())
// Its poster (the spot's) by id, in posters for poster search.
assert.isNotNull(release.poster_id)
assert.isNotEmpty((await db.from('posters').where('id', release.poster_id).firstOrFail()).poster)
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.
@@ -114,10 +114,7 @@ test.group('Spots', (group) => {
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')
const h = { id: await insertRelease({ 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 }) }
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))