Files
ReadMeABook/src/app/api/admin/downloads/active/route.ts
T
kikootwo 4b90b35748 Add Transmission/NZBGet and per-client paths and much more
Extend multi-download-client support to include Transmission and NZBGet and introduce per-client custom download paths. Adds protocol mapping and new client types, Transmission/NZBGet integration services, API CRUD and validation changes, UI components/modal updates and live path previews, and manager routing by protocol. Includes DB migrations (download_path on download_history, interactive_search_access on users), schema updates, and related processor/service fixes and tests to ensure backward compatibility and proper path resolution.
2026-02-09 19:45:43 -05:00

151 lines
5.3 KiB
TypeScript

/**
* Component: Admin Active Downloads API
* Documentation: documentation/admin-dashboard.md
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db';
import { getConfigService } from '@/lib/services/config.service';
import { getDownloadClientManager } from '@/lib/services/download-client-manager.service';
import { RMABLogger } from '@/lib/utils/logger';
import { CLIENT_PROTOCOL_MAP, DownloadClientType } from '@/lib/interfaces/download-client.interface';
const logger = RMABLogger.create('API.Admin.Downloads');
export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
// Get active downloads with related data (both audiobook and ebook)
const activeDownloads = await prisma.request.findMany({
where: {
status: 'downloading',
deletedAt: null,
},
select: {
id: true,
status: true,
type: true, // 'audiobook' or 'ebook'
progress: true,
updatedAt: true,
audiobook: {
select: {
id: true,
title: true,
author: true,
},
},
user: {
select: {
id: true,
plexUsername: true,
},
},
downloadHistory: {
where: {
downloadStatus: 'downloading',
},
orderBy: {
createdAt: 'desc',
},
take: 1,
select: {
downloadStatus: true,
torrentName: true,
torrentHash: true,
nzbId: true,
downloadClientId: true,
downloadClient: true, // qbittorrent, sabnzbd, or direct
torrentSizeBytes: true,
startedAt: true,
createdAt: true,
},
},
},
orderBy: {
updatedAt: 'desc',
},
take: 20,
});
// Get download client manager
const configService = getConfigService();
const manager = getDownloadClientManager(configService);
// Format response with speed and ETA from download client
const formatted = await Promise.all(
activeDownloads.map(async (download) => {
let speed = 0;
let eta: number | null = null;
const downloadHistory = download.downloadHistory[0];
const downloadClient = downloadHistory?.downloadClient;
try {
if (downloadClient === 'direct') {
// Direct HTTP download (ebooks) - estimate speed from progress and time elapsed
const startedAt = downloadHistory?.startedAt || downloadHistory?.createdAt;
const totalSize = downloadHistory?.torrentSizeBytes ? Number(downloadHistory.torrentSizeBytes) : 0;
if (startedAt && download.progress > 0 && totalSize > 0) {
const elapsedMs = Date.now() - new Date(startedAt).getTime();
const elapsedSeconds = elapsedMs / 1000;
const bytesDownloaded = (download.progress / 100) * totalSize;
if (elapsedSeconds > 0) {
speed = Math.round(bytesDownloaded / elapsedSeconds);
const remainingBytes = totalSize - bytesDownloaded;
eta = speed > 0 ? Math.round(remainingBytes / speed) : null;
}
}
} else {
// Use unified interface for all download clients (qBittorrent, SABnzbd, etc.)
const clientId = downloadHistory?.downloadClientId || downloadHistory?.torrentHash || downloadHistory?.nzbId;
if (clientId && downloadClient) {
const protocol = CLIENT_PROTOCOL_MAP[downloadClient as DownloadClientType] || 'torrent';
const client = await manager.getClientServiceForProtocol(protocol as 'torrent' | 'usenet');
if (client) {
const info = await client.getDownload(clientId);
if (info) {
speed = info.downloadSpeed;
eta = info.eta > 0 ? info.eta : null;
}
}
}
}
} catch (error) {
// Download client unavailable or download not found - use defaults
logger.error('Failed to get download info', { error: error instanceof Error ? error.message : String(error) });
}
return {
requestId: download.id,
title: download.audiobook.title,
author: download.audiobook.author,
status: download.status,
type: download.type, // 'audiobook' or 'ebook'
progress: download.progress,
speed,
eta,
torrentName: downloadHistory?.torrentName || null,
downloadStatus: downloadHistory?.downloadStatus || null,
user: download.user.plexUsername,
startedAt: downloadHistory?.startedAt || downloadHistory?.createdAt || download.updatedAt,
};
})
);
return NextResponse.json({ downloads: formatted });
} catch (error) {
logger.error('Failed to fetch active downloads', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json(
{ error: 'Failed to fetch active downloads' },
{ status: 500 }
);
}
});
});
}