diff --git a/src/server/routes/shows.ts b/src/server/routes/shows.ts index 9e2c415..bb6f914 100644 --- a/src/server/routes/shows.ts +++ b/src/server/routes/shows.ts @@ -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 diff --git a/src/server/services/scheduler.ts b/src/server/services/scheduler.ts index caf5a44..6269619 100644 --- a/src/server/services/scheduler.ts +++ b/src/server/services/scheduler.ts @@ -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 } + // 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 (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) { - if (isBatchRelease(item.title)) { - console.log(`[scheduler] Skipping batch: ${item.title}`) - continue - } - - // Check if already tracked - const existing = db - .prepare('SELECT id FROM episodes WHERE show_id = ? AND torrent_id = ?') - .get(show.id, item.torrent_id) as Episode | undefined - - if (existing) continue - 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)