Prober: an inspection per candidate; no inner name that is the release's name
A release can have several candidates of a kind (two RAR sets): keyed by
(release, kind) both landed in one upsert ("ON CONFLICT DO UPDATE
command cannot affect row a second time"), and the batch was lost.
release_inspections is keyed by candidate now; the first run's results
are cleared and probed again. A random-named file's yEnc name is often
its random name: an inner name equal to the release's name is dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8bd6d6aa5f
commit
738402aefa
4 files changed
+53
-22
No files matched your search
+16
-11
@@ -144,23 +144,28 @@ export class Prober {
|
||||
const done = outcomes.filter((o) => o.inspection)
|
||||
if (done.length === 0) return
|
||||
await trx.rawQuery(
|
||||
`INSERT INTO release_inspections (release_id, kind, result)
|
||||
SELECT v.release_id, v.kind, v.result FROM jsonb_to_recordset(?::jsonb) AS v(release_id bigint, kind text, result jsonb)
|
||||
ON CONFLICT (release_id, kind) DO UPDATE SET result = excluded.result, created_at = now()`,
|
||||
[JSON.stringify(done.map((o) => ({ release_id: o.candidate.releaseId, kind: o.candidate.kind, result: o.inspection })))]
|
||||
`INSERT INTO release_inspections (release_id, candidate_id, kind, result)
|
||||
SELECT v.release_id, v.candidate_id, v.kind, v.result FROM jsonb_to_recordset(?::jsonb) AS v(release_id bigint, candidate_id bigint, kind text, result jsonb)
|
||||
ON CONFLICT (release_id, candidate_id) DO UPDATE SET result = excluded.result, created_at = now()`,
|
||||
[JSON.stringify(done.map((o) => ({ release_id: o.candidate.releaseId, candidate_id: o.candidate.id, kind: o.candidate.kind, result: o.inspection })))]
|
||||
)
|
||||
// Each touched release from all its inspections (earlier rounds may have stored its other kinds).
|
||||
// Each touched release from all its inspections (earlier rounds may have stored others).
|
||||
const ids = [...new Set(done.map((o) => o.candidate.releaseId))]
|
||||
const { rows } = await trx.rawQuery(`SELECT release_id, kind, result FROM release_inspections WHERE release_id = ANY(?)`, [ids])
|
||||
const byRelease = new Map<number, Record<string, Inspection>>()
|
||||
const { rows } = await trx.rawQuery(
|
||||
`SELECT i.release_id, i.kind, i.result, r.name FROM release_inspections i JOIN releases r ON r.id = i.release_id WHERE i.release_id = ANY(?)`,
|
||||
[ids]
|
||||
)
|
||||
const byRelease = new Map<number, { name: string; all: { kind: string; result: Inspection }[] }>()
|
||||
for (const r of rows) {
|
||||
const id = Number(r.release_id)
|
||||
byRelease.set(id, { ...byRelease.get(id), [r.kind]: r.result })
|
||||
const entry = byRelease.get(id) ?? { name: r.name as string, all: [] as { kind: string; result: Inspection }[] }
|
||||
entry.all.push({ kind: r.kind, result: r.result })
|
||||
byRelease.set(id, entry)
|
||||
}
|
||||
const updates = [...byRelease].map(([id, all]) => ({
|
||||
const updates = [...byRelease].map(([id, { name, all }]) => ({
|
||||
id,
|
||||
passworded: Object.values(all).some((i) => i.passworded === true),
|
||||
inner_name: innerName(all),
|
||||
passworded: all.some((i) => i.result.passworded === true),
|
||||
inner_name: innerName(all, name),
|
||||
}))
|
||||
await trx.rawQuery(
|
||||
`UPDATE releases r SET passworded = v.passworded, inner_name = v.inner_name, inspected_at = now()
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
|
||||
/** One inspection per probe candidate: a release can have several of a kind (two RAR sets). */
|
||||
export default class extends BaseSchema {
|
||||
async up() {
|
||||
this.schema.raw(`DELETE FROM release_inspections`)
|
||||
this.schema.raw(`ALTER TABLE release_inspections DROP CONSTRAINT release_inspections_pkey`)
|
||||
this.schema.raw(`ALTER TABLE release_inspections ADD COLUMN candidate_id bigint NOT NULL`)
|
||||
this.schema.raw(`ALTER TABLE release_inspections ADD PRIMARY KEY (release_id, candidate_id)`)
|
||||
// Their results went with the deleted rows: probed again.
|
||||
this.schema.raw(`UPDATE probe_candidates SET status = 'waiting' WHERE status = 'done'`)
|
||||
this.schema.raw(`UPDATE releases SET passworded = NULL, inner_name = NULL, inspected_at = NULL WHERE inspected_at IS NOT NULL`)
|
||||
}
|
||||
|
||||
async down() {
|
||||
this.schema.raw(`ALTER TABLE release_inspections DROP CONSTRAINT release_inspections_pkey`)
|
||||
this.schema.raw(`ALTER TABLE release_inspections DROP COLUMN candidate_id`)
|
||||
this.schema.raw(`ALTER TABLE release_inspections ADD PRIMARY KEY (release_id, kind)`)
|
||||
}
|
||||
}
|
||||
+10
-7
@@ -68,12 +68,15 @@ export function inspect(body: Buffer): Inspection {
|
||||
}
|
||||
|
||||
/**
|
||||
* A release's best inner name from its inspections: the largest named entry
|
||||
* of its archive, else the largest file of its par2 set that isn't
|
||||
* packaging, else a lone file's yEnc name. Null when none says more.
|
||||
* A release's best inner name from its inspections (by probe kind): the
|
||||
* largest named entry of an archive, else the largest file of a par2 set
|
||||
* that isn't packaging, else a lone file's yEnc name. Null when none says
|
||||
* more than the release's own name.
|
||||
*/
|
||||
export function innerName(inspections: Partial<Record<string, Inspection>>): string | null {
|
||||
const content = (i?: Inspection) => i?.names?.find((n) => !PACKAGING.test(n))
|
||||
const pick = content(inspections.archive) ?? content(inspections.par2) ?? (inspections.lone?.yencName || undefined)
|
||||
return pick ? pick.replace(/^.*[\\/]/, '').slice(0, 255) : null
|
||||
export function innerName(inspections: { kind: string; result: Inspection }[], releaseName = ''): string | null {
|
||||
const of = (kind: string) => inspections.filter((i) => i.kind === kind).map((i) => i.result)
|
||||
const content = (kind: string) => of(kind).flatMap((i) => i.names ?? []).find((n) => !PACKAGING.test(n))
|
||||
const pick = content('archive') ?? content('par2') ?? of('lone').find((i) => i.yencName)?.yencName
|
||||
const name = pick?.replace(/^.*[\\/]/, '').slice(0, 255)
|
||||
return name && name.toLowerCase() !== releaseName.toLowerCase() ? name : null
|
||||
}
|
||||
@@ -116,9 +116,12 @@ test.group('probe / innerName', () => {
|
||||
test("the archive's largest entry, else par2's largest non-packaging file, else a lone file's yEnc name", ({ assert }) => {
|
||||
const archive = { yencName: 'a.rar', size: 1, type: 'rar5' as const, names: ['Show.S01E01/show.s01e01.mkv'] }
|
||||
const par2 = { yencName: 'a.par2', size: 1, type: 'par2' as const, names: ['abc.part01.rar', 'Show.S01E01.1080p.mkv'] }
|
||||
assert.equal(innerName({ archive, par2 }), 'show.s01e01.mkv')
|
||||
assert.equal(innerName({ par2 }), 'Show.S01E01.1080p.mkv')
|
||||
assert.equal(innerName({ par2: { ...par2, names: ['abc.part01.rar', 'abc.par2'] } }), null)
|
||||
assert.equal(innerName({ lone: { yencName: 'Real.Name.mkv', size: 1, type: 'matroska' } }), 'Real.Name.mkv')
|
||||
assert.equal(innerName([{ kind: 'par2', result: par2 }, { kind: 'archive', result: archive }]), 'show.s01e01.mkv')
|
||||
assert.equal(innerName([{ kind: 'par2', result: par2 }]), 'Show.S01E01.1080p.mkv')
|
||||
assert.equal(innerName([{ kind: 'par2', result: { ...par2, names: ['abc.part01.rar', 'abc.par2'] } }]), null)
|
||||
const lone = { kind: 'lone', result: { yencName: 'Real.Name.mkv', size: 1, type: 'matroska' as const } }
|
||||
assert.equal(innerName([lone], 'k3j4h5'), 'Real.Name.mkv')
|
||||
// A random-named file whose yEnc name is its random name says nothing new.
|
||||
assert.isNull(innerName([lone], 'real.name.mkv'))
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user