Spots: a corrupt NZB is a result, not a reason to read the listing again
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ee898be8c8
commit
1520a8dc35
2 files changed
+28
-4
No files matched your search
@@ -31,6 +31,9 @@ const MAX_IMAGE_SEGMENTS = 10
|
|||||||
/** A claimed spot whose worker died is taken again after this long. */
|
/** A claimed spot whose worker died is taken again after this long. */
|
||||||
const RECLAIM_AFTER = '1 hour'
|
const RECLAIM_AFTER = '1 hour'
|
||||||
|
|
||||||
|
/** The NZB's articles were read, but they don't decode to an NZB (e.g. "invalid code lengths set"). */
|
||||||
|
export class NzbUnreadable extends Error {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A spot's title: from its XML, which is decoded properly (the Subject of a
|
* A spot's title: from its XML, which is decoded properly (the Subject of a
|
||||||
* raw header is read as ISO-8859-1), else from the header.
|
* raw header is read as ISO-8859-1), else from the header.
|
||||||
@@ -333,10 +336,17 @@ class SpotService {
|
|||||||
try {
|
try {
|
||||||
nzbXml = await this.readNzb(head)
|
nzbXml = await this.readNzb(head)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (!/^4\d\d/.test(String(error.message))) throw error
|
if (error instanceof NzbUnreadable) {
|
||||||
|
// Corrupt: reading it again won't help (LOG.md 2026-10-08 (12)).
|
||||||
|
update.error = `NZB unreadable: ${error.message}`.slice(0, 500)
|
||||||
|
outcome = 'nzb-unreadable'
|
||||||
|
} else if (/^4\d\d/.test(String(error.message))) {
|
||||||
// The NZB's articles are gone: the listing can't be downloaded any more.
|
// The NZB's articles are gone: the listing can't be downloaded any more.
|
||||||
update.error = 'NZB articles gone'
|
update.error = 'NZB articles gone'
|
||||||
outcome = 'nzb-gone'
|
outcome = 'nzb-gone'
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const nzb = nzbXml ? parseNzb(nzbXml) : null
|
const nzb = nzbXml ? parseNzb(nzbXml) : null
|
||||||
if (nzb && nzb.files.length > 0 && isDummyNzb(nzb, head.header.size)) {
|
if (nzb && nzb.files.length > 0 && isDummyNzb(nzb, head.header.size)) {
|
||||||
@@ -405,7 +415,11 @@ class SpotService {
|
|||||||
if (segments.length === 0 || segments.length > MAX_NZB_SEGMENTS) return null
|
if (segments.length === 0 || segments.length > MAX_NZB_SEGMENTS) return null
|
||||||
const bodies = []
|
const bodies = []
|
||||||
for (const id of segments) bodies.push(await this.fetch(`BODY <${id.replace(/^<|>$/g, '')}>`))
|
for (const id of segments) bodies.push(await this.fetch(`BODY <${id.replace(/^<|>$/g, '')}>`))
|
||||||
|
try {
|
||||||
return decodeSpotBinary(bodies).toString('utf8')
|
return decodeSpotBinary(bodies).toString('utf8')
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new NzbUnreadable(String(error.message))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Downloads the spot's compressed NZB and imports it as a release (D26). */
|
/** Downloads the spot's compressed NZB and imports it as a release (D26). */
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { test } from '@japa/runner'
|
|||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import db from '@adonisjs/lucid/services/db'
|
import db from '@adonisjs/lucid/services/db'
|
||||||
import testUtils from '@adonisjs/core/services/test_utils'
|
import testUtils from '@adonisjs/core/services/test_utils'
|
||||||
import SpotService, { isDummyNzb } from '#services/SpotService'
|
import SpotService, { isDummyNzb, NzbUnreadable } from '#services/SpotService'
|
||||||
import SearchService from '#services/SearchService'
|
import SearchService from '#services/SearchService'
|
||||||
|
|
||||||
test.group('SpotService listings', (group) => {
|
test.group('SpotService listings', (group) => {
|
||||||
@@ -117,7 +117,7 @@ test.group('SpotService reading listings', (group) => {
|
|||||||
<segments><segment bytes="${bytes}" number="1">a1@x</segment></segments></file></nzb>`
|
<segments><segment bytes="${bytes}" number="1">a1@x</segment></segments></file></nzb>`
|
||||||
|
|
||||||
/** A listed spot, as worker:spots leaves an old one, with its article and NZB as given. */
|
/** A listed spot, as worker:spots leaves an old one, with its article and NZB as given. */
|
||||||
async function listedSpot(id: string, read: { description?: string; nzb?: string | null; error?: string }) {
|
async function listedSpot(id: string, read: { description?: string; nzb?: string | null; error?: string; nzbError?: Error }) {
|
||||||
await db.table('spots').insert({
|
await db.table('spots').insert({
|
||||||
message_id: id, group_name: 'free.pt', article_number: 1, title: `Listed ${id}`, poster: 'Bart', key_id: 7, size: 700_000_000,
|
message_id: id, group_name: 'free.pt', article_number: 1, title: `Listed ${id}`, poster: 'Bart', key_id: 7, size: 700_000_000,
|
||||||
posted_at: new Date('2019-05-01T12:00:00Z'), verified: true, created_at: new Date(), status: 'pending',
|
posted_at: new Date('2019-05-01T12:00:00Z'), verified: true, created_at: new Date(), status: 'pending',
|
||||||
@@ -129,7 +129,10 @@ test.group('SpotService reading listings', (group) => {
|
|||||||
if (read.error) throw new Error(read.error)
|
if (read.error) throw new Error(read.error)
|
||||||
return { header: { size: 700_000_000 }, xml: { description: read.description ?? '', nzbSegments: ['n@x'] } }
|
return { header: { size: 700_000_000 }, xml: { description: read.description ?? '', nzbSegments: ['n@x'] } }
|
||||||
}
|
}
|
||||||
service.readNzb = async () => read.nzb ?? null
|
service.readNzb = async () => {
|
||||||
|
if (read.nzbError) throw read.nzbError
|
||||||
|
return read.nzb ?? null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const restore = () => {
|
const restore = () => {
|
||||||
@@ -179,5 +182,12 @@ test.group('SpotService reading listings', (group) => {
|
|||||||
await listedSpot('l6@spot.net', { error: '430 No Such Article' })
|
await listedSpot('l6@spot.net', { error: '430 No Such Article' })
|
||||||
assert.deepEqual(await SpotService.readListings(10, 1), { gone: 1 })
|
assert.deepEqual(await SpotService.readListings(10, 1), { gone: 1 })
|
||||||
assert.isNotNull((await db.from('spots').where('message_id', 'l6@spot.net').first()).nzb_read_at)
|
assert.isNotNull((await db.from('spots').where('message_id', 'l6@spot.net').first()).nzb_read_at)
|
||||||
|
|
||||||
|
// A corrupt NZB is a result, not something to read again.
|
||||||
|
await listedSpot('l7@spot.net', { nzbError: new NzbUnreadable('invalid code lengths set') })
|
||||||
|
assert.deepEqual(await SpotService.readListings(10, 1), { 'nzb-unreadable': 1 })
|
||||||
|
const l7 = await db.from('spots').where('message_id', 'l7@spot.net').first()
|
||||||
|
assert.isNotNull(l7.nzb_read_at)
|
||||||
|
assert.equal(l7.error, 'NZB unreadable: invalid code lengths set')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
Reference in new issue
Block a user