feat: auto-download from episode 1 on show add
When a show is added, ingest the Nyaa feed, sort episodes oldest→newest (ep 1 first), and auto-download them all in order rather than leaving them as pending. Ongoing polls continue downloading new episodes the same way. Extracted a shared downloadItems() helper from the scheduler so both the initial ingest and the cron poll use identical insert→download→ status-update logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Router } from 'express'
|
||||
import { db } from '../db/index.js'
|
||||
import { searchNyaa, parseEpisodeCode, isBatchRelease, buildRssUrl } from '../services/nyaa.js'
|
||||
import { downloadItems } from '../services/scheduler.js'
|
||||
import type { Show } from '../types.js'
|
||||
|
||||
const router = Router()
|
||||
@@ -42,7 +43,7 @@ router.post('/', async (req, res) => {
|
||||
|
||||
const showId = result.lastInsertRowid
|
||||
|
||||
// Ingest existing Nyaa feed as pending episodes (fire and forget, don't block response)
|
||||
// Ingest existing feed and auto-download from ep 1 upward (fire and forget)
|
||||
ingestExistingEpisodes(Number(showId), search_query, quality, sub_group).catch((err: unknown) =>
|
||||
console.error('[shows] Ingest error:', err)
|
||||
)
|
||||
@@ -98,28 +99,27 @@ async function ingestExistingEpisodes(
|
||||
try {
|
||||
items = await searchNyaa(query)
|
||||
} catch (err) {
|
||||
console.error('[shows] Failed to ingest episodes:', err)
|
||||
console.error('[shows] Failed to fetch episodes from Nyaa:', err)
|
||||
return
|
||||
}
|
||||
|
||||
const insert = db.prepare(`
|
||||
INSERT OR IGNORE INTO episodes
|
||||
(show_id, episode_code, title, torrent_id, torrent_url, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'pending', datetime('now'), datetime('now'))
|
||||
`)
|
||||
// Filter batches, then sort oldest episode first so downloads begin from ep 1
|
||||
const filtered = items
|
||||
.filter((item) => !isBatchRelease(item.title))
|
||||
.sort((a, b) => {
|
||||
const na = parseFloat(parseEpisodeCode(a.title))
|
||||
const nb = parseFloat(parseEpisodeCode(b.title))
|
||||
return (isNaN(na) ? Infinity : na) - (isNaN(nb) ? Infinity : nb)
|
||||
})
|
||||
|
||||
db.exec('BEGIN')
|
||||
try {
|
||||
for (const item of items) {
|
||||
if (isBatchRelease(item.title)) continue
|
||||
insert.run(showId, parseEpisodeCode(item.title), item.title, item.torrent_id, item.torrent_url)
|
||||
}
|
||||
db.exec('COMMIT')
|
||||
} catch (err) {
|
||||
db.exec('ROLLBACK')
|
||||
throw err
|
||||
}
|
||||
console.log(`[shows] Ingested ${items.length} episodes for show ${showId}`)
|
||||
console.log(`[shows] Ingesting ${filtered.length} episode(s) for show ${showId} (oldest→newest)`)
|
||||
|
||||
// Look up the show record needed by downloadItems
|
||||
const show = db.prepare('SELECT * FROM shows WHERE id = ?').get(showId) as unknown as Show | undefined
|
||||
if (!show) return
|
||||
|
||||
// downloadItems handles insert + download + status update in order
|
||||
await downloadItems(show, filtered)
|
||||
}
|
||||
|
||||
export default router
|
||||
|
||||
@@ -2,7 +2,7 @@ import cron from 'node-cron'
|
||||
import { db } from '../db/index.js'
|
||||
import { fetchRss, parseEpisodeCode, isBatchRelease } from './nyaa.js'
|
||||
import { downloadTorrent, slugify } from './downloader.js'
|
||||
import type { Show, Episode } from '../types.js'
|
||||
import type { Show, Episode, NyaaItem } from '../types.js'
|
||||
|
||||
let currentTask: cron.ScheduledTask | null = null
|
||||
|
||||
@@ -61,7 +61,7 @@ async function pollShow(show: Show) {
|
||||
const rssUrl = show.rss_url ?? buildRssUrl(show)
|
||||
console.log(`[scheduler] Polling "${show.name}" via ${rssUrl}`)
|
||||
|
||||
let items
|
||||
let items: NyaaItem[]
|
||||
try {
|
||||
items = await fetchRss(rssUrl)
|
||||
} catch (err) {
|
||||
@@ -69,24 +69,34 @@ async function pollShow(show: Show) {
|
||||
return
|
||||
}
|
||||
|
||||
const slug = slugify(show.name)
|
||||
|
||||
for (const item of items) {
|
||||
if (isBatchRelease(item.title)) {
|
||||
console.log(`[scheduler] Skipping batch: ${item.title}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if already tracked
|
||||
// Filter out batches and already-tracked items, sort oldest→newest
|
||||
const newItems = items
|
||||
.filter((item) => {
|
||||
if (isBatchRelease(item.title)) return false
|
||||
const existing = db
|
||||
.prepare('SELECT id FROM episodes WHERE show_id = ? AND torrent_id = ?')
|
||||
.get(show.id, item.torrent_id) as Episode | undefined
|
||||
return !existing
|
||||
})
|
||||
.sort((a, b) => episodeNum(parseEpisodeCode(a.title)) - episodeNum(parseEpisodeCode(b.title)))
|
||||
|
||||
if (existing) continue
|
||||
if (newItems.length === 0) return
|
||||
|
||||
console.log(`[scheduler] Found ${newItems.length} new episode(s) for "${show.name}"`)
|
||||
await downloadItems(show, newItems)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared download pipeline used by both the scheduler and the initial ingest.
|
||||
* Items should already be sorted oldest→newest before calling.
|
||||
*/
|
||||
export async function downloadItems(show: Show, items: NyaaItem[]) {
|
||||
const slug = slugify(show.name)
|
||||
|
||||
for (const item of items) {
|
||||
const episodeCode = parseEpisodeCode(item.title)
|
||||
|
||||
// Insert as pending first
|
||||
// Insert as pending (idempotent)
|
||||
const insertResult = db
|
||||
.prepare(`
|
||||
INSERT OR IGNORE INTO episodes
|
||||
@@ -99,7 +109,7 @@ async function pollShow(show: Show) {
|
||||
|
||||
const epId = insertResult.lastInsertRowid
|
||||
|
||||
// Auto-download
|
||||
// Download the .torrent file
|
||||
try {
|
||||
console.log(`[scheduler] Downloading torrent for "${item.title}"`)
|
||||
await downloadTorrent(item.torrent_url, slug, episodeCode, item.torrent_id)
|
||||
@@ -115,6 +125,12 @@ async function pollShow(show: Show) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse an episode code string to a float for numeric sorting. Unknown → Infinity (goes last). */
|
||||
function episodeNum(code: string): number {
|
||||
const n = parseFloat(code)
|
||||
return isNaN(n) ? Infinity : n
|
||||
}
|
||||
|
||||
function buildRssUrl(show: Show): string {
|
||||
const parts = [show.search_query]
|
||||
if (show.quality) parts.push(show.quality)
|
||||
|
||||
Reference in New Issue
Block a user