Join an upload's releases by its par2 set's file names
Test and deploy / test (push) Successful in 1m5s
Test and deploy / edge (push) Successful in 2m0s
Test and deploy / deploy (push) Successful in 1m19s

The files of one upload often became several releases whose names
differ slightly: scene tracks apart from their album's nfo/sfv/par2,
"Vermaechtnis" beside a garbled "Vermächtnis", an index named
Die.Bergretter.S18E05... beside its video wishtv-diebergretter-s18e05.
The par2 merge joined releases only by first-16-KiB hashes, which
readable files never get. A name pass now looks, for every set the
merger finds, at the set poster's releases in its group posted within
12 hours: those whose files the set all names (without case or
punctuation, Latin-1-read UTF-8 read back; sidecars, par2 and images
aside) join the set's release, within the hash pass's file bound.
Merges are kept in release_merges as via 'name'. releases:merge-names
runs it over the sets found before; on 300 recent sets 22 merges of 147
releases, all one upload each.

Also: volumes split after .wmv, .mov, .mpg and other video extensions
share a stem ("Morning Lovers.wmv.001" to ".058" were 58 releases), in
the Rust edge and the TypeScript identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-23 14:02:51 +02:00
1 parent e4044cb1d1
commit e00e4f3988
7 files changed
+242 -11

No files matched your search

+1 -1
View File
@@ -49,7 +49,7 @@ test.group('Par2Merger', (group) => {
await db.table('par2_sets').insert({ set_id: setId, release_id: par2, files: 2 })
const round = await new Par2Merger().round()
assert.deepEqual(round, { sets: 1, merges: 1, releases: 3, stems: 0, stemMerges: 0, stemReleases: 0, folded: { looked: 0, sidecars: 0, folded: 0 } })
assert.deepEqual(round, { sets: 1, merges: 1, releases: 3, nameMerges: 0, nameReleases: 0, stems: 0, stemMerges: 0, stemReleases: 0, folded: { looked: 0, sidecars: 0, folded: 0 } })
const target = await db.from('releases').where('id', b).firstOrFail()
assert.equal(target.files, 4)
assert.equal(target.segments, 40)
+68
View File
@@ -0,0 +1,68 @@
import { test } from '@japa/runner'
import db from '@adonisjs/lucid/services/db'
import testUtils from '@adonisjs/core/services/test_utils'
import BatchStore from '#services/batch_store'
import { Par2Merger, fileKey } from '#services/par2_merger'
import { RangeIndexer } from '#lib/edge/index'
import { DEFAULT_RULES } from '#lib/rules/index'
import type { Overview } from '#lib/nntp/index'
const GROUP = 'alt.binaries.test'
/** One file without a counter, from `poster`, posted `minutes` after the first. */
function file(name: string, at: number, minutes: number, poster = 'up <up@load.er>'): Overview[] {
const date = new Date(Date.parse('2026-10-20T10:00:00Z') + minutes * 60_000).toUTCString().replace('GMT', '+0000')
return [[at, { 'subject': `"${name}" yEnc (1/1)`, 'from': poster, 'date': date, 'message-id': `<${at}@test>`, ':bytes': '700000', 'xref': `news.example ${GROUP}:${at}` }] as Overview]
}
let stores = 0
async function store(overviews: Overview[]) {
stores++
const g = (await db.from('groups').where('name', GROUP).first()) ?? (await db.table('groups').insert({ name: GROUP, priority: 1 }).returning('*'))[0]
const [n] = await db.table('nodes').insert({ name: `n${stores}` }).returning('id')
const [r] = await db.table('ranges').insert({ group_id: g.id, start_id: stores * 100_000, end_id: stores * 100_000 + 99_999, next_id: stores * 100_000, status: 'leased', node_id: n.id }).returning('id')
const indexer = new RangeIndexer({ group: GROUP, start: stores * 100_000, end: stores * 100_000 + 99_999, rules: { ...DEFAULT_RULES, samplePercent: 0, droppedSignatures: [], priorities: { [GROUP]: 1 } } })
indexer.add(overviews, stores * 100_000 + 100_000)
indexer.finish()
await BatchStore.store(Number(r.id), Number(n.id), indexer.take())
}
test.group('Par2Merger / by file names', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test('a name as compared: no case, spaces or punctuation, UTF-8 read as Latin-1 read back', ({ assert }) => {
assert.equal(fileKey('Some/Dir/Ein.dunkles.Vermächtnis - 1080p.MKV'), fileKey('Ein dunkles Vermächtnis 1080p.mkv'))
assert.notEqual(fileKey('Track One.flac'), fileKey('Track Two.flac'))
})
test("a set's poster's releases nearby whose files it all names join its release; others stay", async ({ assert }) => {
await store([
// The album's index files, its tracks apart (no counter: a release each), and the same poster's next album.
...file('Some.Album-WEB-2026.par2', 100_010, 0),
...file('Some.Album-WEB-2026.nfo', 100_011, 0),
...file('Track One.flac', 100_020, 1),
...file('Track Two.flac', 100_030, 2),
...file('Ein.dunkles.Vermächtnis.mkv', 100_040, 3),
...file('Another Song.flac', 100_050, 4),
// A track the set names, but posted by someone else, and one a day later.
...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 index = ids['Some.Album-WEB-2026']
const setId = 'a'.repeat(32)
await db.table('par2_files').multiInsert(
['Track One.flac', 'Track Two.flac', 'Ein.dunkles.Vermächtnis.mkv'].map((name, i) => ({ set_id: setId, file_id: String(i).repeat(32), hash16k: 'f'.repeat(32), length: 600_000, name }))
)
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)' : ''}`)
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')
assert.equal(Number(merged[0].n), 3)
// Nothing more to find.
assert.equal(await new Par2Merger().mergeByName(setId), 0)
})
})
+4
View File
@@ -16,6 +16,10 @@ test.group('release / fileStem', () => {
assert.equal(fileStem('lJzuoIKA3T8xjzAD3vqtw0.part036.rar'), 'lJzuoIKA3T8xjzAD3vqtw0')
assert.equal(fileStem('Some.Movie.2024.r07'), 'Some.Movie.2024')
assert.equal(fileStem('Some.Movie.2024.mkv.001'), 'Some.Movie.2024')
// Other video split too, one release (58 were).
assert.equal(fileStem('Morning Lovers.wmv.001'), 'Morning Lovers')
assert.equal(fileStem('Morning Lovers.wmv.058'), 'Morning Lovers')
assert.equal(fileStem('clip.mov.12'), 'clip')
assert.equal(fileStem('Some.Movie.2024.2160p.WEB-DL.H.265.mkv'), 'Some.Movie.2024.2160p.WEB-DL.H.265')
assert.equal(fileStem('Some.Movie.2024.H.264.nzb'), 'Some.Movie.2024.H.264')
assert.equal(fileStem('Some.Movie.2024.vol127-128.par2'), 'Some.Movie.2024')