diff --git a/app/services/prober.ts b/app/services/prober.ts index fea8800..87add1d 100644 --- a/app/services/prober.ts +++ b/app/services/prober.ts @@ -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>() + 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() 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() diff --git a/database/migrations/1770000001800_release_inspections_per_candidate.ts b/database/migrations/1770000001800_release_inspections_per_candidate.ts new file mode 100644 index 0000000..24b071f --- /dev/null +++ b/database/migrations/1770000001800_release_inspections_per_candidate.ts @@ -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)`) + } +} diff --git a/lib/probe/inspect.ts b/lib/probe/inspect.ts index 0f67946..69a4950 100644 --- a/lib/probe/inspect.ts +++ b/lib/probe/inspect.ts @@ -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>): 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 } diff --git a/tests/unit/probe/inspect.spec.ts b/tests/unit/probe/inspect.spec.ts index 992beb0..b0adfcf 100644 --- a/tests/unit/probe/inspect.spec.ts +++ b/tests/unit/probe/inspect.spec.ts @@ -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')) }) })