The pending ladder in the prober: unnamed releases probed cheapest first, signatures learned, the named moved into the catalog
pending_ladder.ts claims the oldest waiting pending releases' next target (par2, archive, lone, 7z-tail in that order) as at least PENDING_SHARE of each claim; a usable name (the largest named file of a par2 set or a listing, or a lone file's yEnc name, neither packaging nor random) hands the release to the namer, no name the next target or the end of the climb; climbs count on pending_signatures per (tool, group, poster) and (tool, group), and a signature with 100 climbs and 0.5% named or fewer is dropped: its waiting releases are discarded without a probe, the rules' sample excepted. pending_namer.ts reads a named release's articles again from the corpus or by XOVER and writes it to the catalog under the pending key (source weak-stem, the name as inner name) with its chunk and candidates. Migration 6400; config PENDING_*. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
9cf85e09d8
commit
e135076fcd
14 files changed
+1045
-50
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
export { inspect, innerName, MAX_NAMES, PROBE_VERSION, type Inspection } from './inspect.js'
|
||||
export { inspect, innerName, usableName, MAX_NAMES, PROBE_VERSION, type Inspection } from './inspect.js'
|
||||
+54
-8
@@ -5,6 +5,7 @@ import { parseRarHeaders } from '#lib/rar/index'
|
||||
import { parseSevenZipTail } from '#lib/sevenzip/index'
|
||||
import { parseZipStart } from '#lib/zip/index'
|
||||
import { sniffContentType, type ContentType } from '#lib/content/index'
|
||||
import { fileStem, looksRandom } from '#lib/release/index'
|
||||
|
||||
/** What one probed article showed (REBUILD.md, part 6; stored per release and kind). */
|
||||
export interface Inspection {
|
||||
@@ -25,7 +26,10 @@ export interface Inspection {
|
||||
*/
|
||||
hash16k?: string
|
||||
/** A par2 body's recovery set: every file it describes (not kept in release_inspections; par2_files). */
|
||||
par2?: { setId: string; files: { fileId: string; hash16k: string; length: number; name: string }[] }
|
||||
par2?: {
|
||||
setId: string
|
||||
files: { fileId: string; hash16k: string; length: number; name: string }[]
|
||||
}
|
||||
}
|
||||
|
||||
const HASH_BYTES = 16 * 1024
|
||||
@@ -63,9 +67,16 @@ function largestFirst(items: { name: string; size: number }[]): string[] {
|
||||
*/
|
||||
export function inspect(body: Buffer, kind?: string): Inspection {
|
||||
const segment = decodeBody(body)
|
||||
const result: Inspection = { yencName: segment.meta.begin.name.slice(0, MAX_NAME_LENGTH), size: segment.meta.begin.size, type: sniffContentType(segment.data) }
|
||||
const result: Inspection = {
|
||||
yencName: segment.meta.begin.name.slice(0, MAX_NAME_LENGTH),
|
||||
size: segment.meta.begin.size,
|
||||
type: sniffContentType(segment.data),
|
||||
}
|
||||
// The file's first 16 KiB, or all of a smaller file.
|
||||
if (segment.offset === 0 && (segment.data.length >= HASH_BYTES || segment.data.length === result.size)) {
|
||||
if (
|
||||
segment.offset === 0 &&
|
||||
(segment.data.length >= HASH_BYTES || segment.data.length === result.size)
|
||||
) {
|
||||
result.hash16k = createHash('md5').update(segment.data.subarray(0, HASH_BYTES)).digest('hex')
|
||||
}
|
||||
if (isPar2(segment.data)) {
|
||||
@@ -76,7 +87,12 @@ export function inspect(body: Buffer, kind?: string): Inspection {
|
||||
result.names = largestFirst(set.files.map((f) => ({ name: f.name, size: f.length })))
|
||||
result.par2 = {
|
||||
setId: set.recoverySetId,
|
||||
files: set.files.map((f) => ({ fileId: f.fileId, hash16k: f.md5_16k, length: f.length, name: f.name.slice(0, MAX_NAME_LENGTH) })),
|
||||
files: set.files.map((f) => ({
|
||||
fileId: f.fileId,
|
||||
hash16k: f.md5_16k,
|
||||
length: f.length,
|
||||
name: f.name.slice(0, MAX_NAME_LENGTH),
|
||||
})),
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -99,7 +115,10 @@ export function inspect(body: Buffer, kind?: string): Inspection {
|
||||
}
|
||||
}
|
||||
// 7z keeps its listing at the end: the last segment of the last volume (a 7z-tail), or a small archive whole.
|
||||
if ((kind === '7z-tail' || result.type === '7z') && segment.offset + segment.data.length === result.size) {
|
||||
if (
|
||||
(kind === '7z-tail' || result.type === '7z') &&
|
||||
segment.offset + segment.data.length === result.size
|
||||
) {
|
||||
const listing = parseSevenZipTail(segment.data)
|
||||
if (listing) {
|
||||
result.entries = listing.files.length
|
||||
@@ -110,16 +129,43 @@ export function inspect(body: Buffer, kind?: string): Inspection {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* The usable name one probe of an unnamed release found (the pending ladder, REBUILD.md decision f): the largest
|
||||
* named file of a par2 set or an archive's listing, or a lone file's yEnc name, when it isn't packaging and isn't
|
||||
* random itself (an obfuscated upload's par2 names its random volumes; its yEnc name is mostly the random name
|
||||
* again). Null: this probe named nothing, the next target is tried.
|
||||
*/
|
||||
export function usableName(inspection: Inspection, kind: string): string | null {
|
||||
const usable = (n: string | undefined) => {
|
||||
if (!n) return null
|
||||
const name = n.replace(/^.*[\\/]/, '').slice(0, 255)
|
||||
return name && !PACKAGING.test(name) && !looksRandom(fileStem(name)) ? name : null
|
||||
}
|
||||
const named = (inspection.names ?? []).map(usable).find((n) => n)
|
||||
if (named) return named
|
||||
return kind === 'lone' || !inspection.names?.length ? usable(inspection.yencName) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: { kind: string; result: Inspection }[], releaseName = ''): string | 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('7z-tail') ?? content('par2') ?? of('lone').find((i) => i.yencName && !PACKAGING.test(i.yencName))?.yencName
|
||||
const content = (kind: string) =>
|
||||
of(kind)
|
||||
.flatMap((i) => i.names ?? [])
|
||||
.find((n) => !PACKAGING.test(n))
|
||||
const pick =
|
||||
content('archive') ??
|
||||
content('7z-tail') ??
|
||||
content('par2') ??
|
||||
of('lone').find((i) => i.yencName && !PACKAGING.test(i.yencName))?.yencName
|
||||
const name = pick?.replace(/^.*[\\/]/, '').slice(0, 255)
|
||||
// The release's own name with an extension says nothing new (a random-named file's yEnc name is mostly its random name).
|
||||
return name && !name.toLowerCase().startsWith(releaseName.toLowerCase() || '\0') ? name : null
|
||||
|
||||
Reference in new issue
Block a user