Accounts with limits: a Newznab key and a tier per user, daily API and NZB counts, invites
Test and deploy / test (push) Successful in 47s
Test and deploy / edge (push) Successful in 1m44s
Test and deploy / deploy (push) Skipped

ACCESS=accounts (config/access.ts; open stays the default and behaves as
before): Newznab and the catalog's releases routes name a user (apikey or
the web interface's token), whose tier sets searches and NZBs per UTC day
(Newznab 500/501, the catalog 429) and how far back they see (102/403).
Counted in one upsert per use, so the last one can't be taken twice;
caps, stats and refused requests aren't counted. Signup takes an invite
(SIGNUP), which may give a tier; ace access:invite, access:tier (extends a
running tier), access:suspend. The web interface signs in and shows the
day's usage and the Newznab key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-17 00:16:45 +02:00
1 parent c55918350e
commit 1322e42fb7
23 files changed
+1023 -39

No files matched your search

+1 -1
View File
@@ -2,4 +2,4 @@
* Newznab API formatting, independent of AdonisJS. From voyager 1 (its CONCEPT.md D23).
*/
export { CATEGORIES, categoryLabel, expandCategories, parentCategory } from './categories.js'
export { capsXml, errorXml, resultsXml, type NewznabItem, type NewznabServer } from './xml.js'
export { capsXml, errorXml, resultsXml, type ApiLimits, type NewznabItem, type NewznabServer } from './xml.js'
+24 -4
View File
@@ -27,7 +27,7 @@ export interface NewznabServer {
apiKey?: string
}
/** Newznab error codes: 100 credentials, 200/201 parameters, 202 function, 300 item. */
/** Newznab error codes: 100 credentials, 101 suspended, 102 privileges, 200/201 parameters, 202 function, 300 item, 500/501 request/download limit. */
/** Spotnet UBB markup (or the HTML of old spots) as plain text: [br] as line breaks, other tags dropped. */
export function ubbToText(ubb: string): string {
return ubb
@@ -44,11 +44,23 @@ export function errorXml(code: number, description: string): string {
.end({ prettyPrint: true })
}
export function capsXml(server: NewznabServer, limits: { max: number; default: number }): string {
/** A caller's day so far and its limits (null: none), as nZEDb-style indexers report them. */
export interface ApiLimits {
apiCurrent: number
apiMax: number | null
grabCurrent: number
grabMax: number | null
}
export function capsXml(
server: NewznabServer,
limits: { max: number; default: number },
registration = { available: false, open: false }
): string {
const caps = create({ version: '1.0', encoding: 'UTF-8' }).ele('caps')
caps.ele('server', { version: '1.0', title: server.title, strapline: 'Usenet indexer', url: `${server.baseUrl}/` })
caps.ele('limits', { max: String(limits.max), default: String(limits.default) })
caps.ele('registration', { available: 'no', open: 'no' })
caps.ele('registration', { available: registration.available ? 'yes' : 'no', open: registration.open ? 'yes' : 'no' })
const searching = caps.ele('searching')
searching.ele('search', { available: 'yes', supportedParams: 'q' })
searching.ele('tv-search', { available: 'yes', supportedParams: 'q,season,ep' })
@@ -63,7 +75,7 @@ export function capsXml(server: NewznabServer, limits: { max: number; default: n
return caps.end({ prettyPrint: true })
}
export function resultsXml(server: NewznabServer, items: NewznabItem[], offset: number, total: number): string {
export function resultsXml(server: NewznabServer, items: NewznabItem[], offset: number, total: number, limits?: ApiLimits): string {
const rss = create({ version: '1.0', encoding: 'UTF-8' }).ele('rss', {
'version': '2.0',
'xmlns:atom': 'http://www.w3.org/2005/Atom',
@@ -74,6 +86,14 @@ export function resultsXml(server: NewznabServer, items: NewznabItem[], offset:
channel.ele('description').txt(`${server.title} API results`)
channel.ele('link').txt(`${server.baseUrl}/`)
channel.ele('newznab:response', { offset: String(offset), total: String(total) })
if (limits) {
channel.ele('newznab:apilimits', {
apicurrent: String(limits.apiCurrent),
...(limits.apiMax !== null && { apimax: String(limits.apiMax) }),
grabcurrent: String(limits.grabCurrent),
...(limits.grabMax !== null && { grabmax: String(limits.grabMax) }),
})
}
const key = server.apiKey ? `&apikey=${encodeURIComponent(server.apiKey)}` : ''
for (const item of items) {