import { BaseCommand, flags } from '@adonisjs/core/ace' import type { CommandOptions } from '@adonisjs/core/types/ace' import db from '@adonisjs/lucid/services/db' import { Par2Merger } from '#services/par2_merger' /** * The par2 merger's name pass over sets found before it was built (2026-10-23): every set with its file list, newest * first, by set row. --dry-run counts what it would merge and shows examples, merging nothing. */ export default class ReleasesMergeNames extends BaseCommand { static commandName = 'releases:merge-names' static description = "Merges releases by their par2 set's file names, for sets found before the name pass; --dry-run shows only" static options: CommandOptions = { startApp: true } @flags.number({ description: 'Sets to look at (newest first); all without' }) declare sets: number @flags.boolean({ description: 'Count and show, merge nothing' }) declare dryRun: boolean async run() { const merger = new Par2Merger() // Keyset on (newest created_at, set_id): sets stored in one batch share created_at, and the timestamp stays text // so microseconds survive the round trip. let before = ['9999-12-31', ''] let looked = 0 let merges = 0 let releases = 0 const examples: string[] = [] while (this.sets === undefined || looked < this.sets) { const { rows } = await db.rawQuery( `SELECT set_id, at::text AS at FROM ( SELECT s.set_id, max(s.created_at) AS at FROM par2_sets s WHERE EXISTS (SELECT 1 FROM par2_files f WHERE f.set_id = s.set_id) GROUP BY s.set_id) x WHERE (at, set_id) < (?::timestamptz, ?) ORDER BY at DESC, set_id DESC LIMIT ${this.dryRun ? 50 : 500}`, before ) if (rows.length === 0) break for (const { set_id: setId } of rows) { looked++ if (this.dryRun) { const found = await db.transaction(async (trx) => merger.nameMatches(trx, setId.trim())) if (found) { merges++ releases += found.size if (examples.length < 15) { const { rows: names } = await db.rawQuery(`SELECT id, name, files FROM releases WHERE id = ANY(?) ORDER BY id`, [[...found.keys()]]) examples.push(names.map((n: any) => `${n.name.slice(0, 50)} [${n.files}]`).join(' + ')) } } } else { const merged = await merger.mergeByName(setId.trim()) if (merged > 0) { merges++ releases += merged + 1 } } } before = [rows.at(-1).at, rows.at(-1).set_id] if (this.dryRun || looked % 20_000 < 500) this.logger.info(`${looked} sets looked at: ${releases} releases ${this.dryRun ? 'would merge' : 'merged'} into ${merges}.`) } this.logger.info(`Done: ${looked} sets looked at, ${releases} releases ${this.dryRun ? 'would merge' : 'merged'} into ${merges}.`) for (const e of examples) this.logger.info(` ${e.slice(0, 300)}`) } }