Implement centralized logging with RMABLogger

Replaces scattered console statements with a unified RMABLogger across backend API routes and services. Adds LOG_LEVEL-based filtering, job-aware database persistence, and context-based logging. Updates documentation to describe the new logging system and usage patterns. Also documents qBittorrent CSRF header fix
This commit is contained in:
kikootwo
2026-01-12 12:45:48 -05:00
parent ba5f5cf7d6
commit 682836237b
118 changed files with 1623 additions and 1079 deletions
+6
View File
@@ -55,6 +55,11 @@
- **Scheduled/recurring jobs (cron)** → [backend/services/scheduler.md](backend/services/scheduler.md) - **Scheduled/recurring jobs (cron)** → [backend/services/scheduler.md](backend/services/scheduler.md)
- **Job types:** search, download monitor, organize, Plex scan, cleanup, retries - **Job types:** search, download monitor, organize, Plex scan, cleanup, retries
## Logging
- **Centralized logging (RMABLogger)** → [backend/services/logging.md](backend/services/logging.md)
- **LOG_LEVEL configuration** → [backend/services/logging.md](backend/services/logging.md)
- **Job-aware database persistence** → [backend/services/logging.md](backend/services/logging.md)
## Frontend Components ## Frontend Components
- **Component catalog (cards, badges, forms)** → [frontend/components.md](frontend/components.md) - **Component catalog (cards, badges, forms)** → [frontend/components.md](frontend/components.md)
- **RequestCard, StatusBadge, ProgressBar** → [frontend/components.md](frontend/components.md) - **RequestCard, StatusBadge, ProgressBar** → [frontend/components.md](frontend/components.md)
@@ -94,6 +99,7 @@
**"OAuth redirects to localhost / PUBLIC_URL not working"** → [backend/services/environment.md](backend/services/environment.md) **"OAuth redirects to localhost / PUBLIC_URL not working"** → [backend/services/environment.md](backend/services/environment.md)
**"What environment variables do I need?"** → [backend/services/environment.md](backend/services/environment.md) **"What environment variables do I need?"** → [backend/services/environment.md](backend/services/environment.md)
**"How does chapter merging work?"** → [features/chapter-merging.md](features/chapter-merging.md) **"How does chapter merging work?"** → [features/chapter-merging.md](features/chapter-merging.md)
**"How does logging work?"** → [backend/services/logging.md](backend/services/logging.md)
**"How does Audiobookshelf integration work?"** → [features/audiobookshelf-integration.md](features/audiobookshelf-integration.md) (PRD only, not implemented) **"How does Audiobookshelf integration work?"** → [features/audiobookshelf-integration.md](features/audiobookshelf-integration.md) (PRD only, not implemented)
**"How do I use OIDC/Authentik/Keycloak?"** → [features/audiobookshelf-integration.md](features/audiobookshelf-integration.md) (PRD only, not implemented) **"How do I use OIDC/Authentik/Keycloak?"** → [features/audiobookshelf-integration.md](features/audiobookshelf-integration.md) (PRD only, not implemented)
**"How does manual user registration work?"** → [features/audiobookshelf-integration.md](features/audiobookshelf-integration.md) (PRD only, not implemented) **"How does manual user registration work?"** → [features/audiobookshelf-integration.md](features/audiobookshelf-integration.md) (PRD only, not implemented)
+130
View File
@@ -0,0 +1,130 @@
# Centralized Logging (RMABLogger)
**Status:** Implemented | Unified logging with LOG_LEVEL filtering + job database persistence
## Overview
Single logging infrastructure for all backend services. Replaces scattered console statements with centralized logger that respects LOG_LEVEL env var and persists job-aware logs to database.
## Key Details
### Usage Patterns
**Standard logging (no job association):**
```typescript
import { RMABLogger } from '../utils/logger';
const logger = RMABLogger.create('ServiceName');
logger.info('Operation started');
logger.debug('Detailed info', { key: 'value' });
logger.warn('Potential issue');
logger.error('Failed operation', { error: err.message });
```
**Job-aware logging (persists to database):**
```typescript
const logger = RMABLogger.forJob(jobId, 'ProcessorName');
logger.info('Processing...'); // Logs to console AND database
logger.debug('Debug info'); // Console only (never persisted)
```
### LOG_LEVEL Configuration
| Value | Logs Shown |
|-------|------------|
| `debug` | All (debug + info + warn + error) |
| `info` | Default (info + warn + error) |
| `warn` | warn + error only |
| `error` | error only |
| `quiet` | None (DB logging still works for job-aware) |
### Output Format
```
[LEVEL] [Context] Message
```
Example:
```
[INFO] [QBittorrent] Connected successfully
[DEBUG] [SearchIndexers] Found 15 results
[WARN] [MonitorDownload] Torrent stalled, retrying
[ERROR] [OrganizeFiles] Failed to move file
```
## API
### Factory Methods
| Method | Description |
|--------|-------------|
| `RMABLogger.create(context)` | Standard logger for context namespace |
| `RMABLogger.forJob(jobId, context)` | Job-aware logger with DB persistence |
### Log Methods
| Method | Description |
|--------|-------------|
| `.debug(msg, metadata?)` | Verbose debugging (console only, never DB) |
| `.info(msg, metadata?)` | Normal operations |
| `.warn(msg, metadata?)` | Warnings |
| `.error(msg, metadata?)` | Errors |
### Child Loggers
```typescript
const logger = RMABLogger.create('Parent');
const child = logger.child('SubContext');
// Output: [INFO] [Parent.SubContext] Message
```
## Database Persistence
Job-aware loggers persist to `job_events` table:
- `jobId` - Associated job ID
- `level` - info, warn, error (never debug)
- `context` - Logger context
- `message` - Log message
- `metadata` - JSON metadata (optional)
- `createdAt` - Timestamp
**Note:** Debug logs are NEVER persisted to keep job_events clean.
## Files
- **Core:** `src/lib/utils/logger.ts`
- **Backward compat:** `src/lib/utils/job-logger.ts` (deprecated wrapper)
## Context Naming Conventions
| Component Type | Pattern | Example |
|----------------|---------|---------|
| Integration | Service name | `QBittorrent`, `Plex`, `Prowlarr` |
| Processor | Job type | `SearchIndexers`, `MonitorDownload` |
| API Route | `API.{resource}` | `API.Requests`, `API.Auth` |
| Service | Service name | `ConfigService`, `JobQueue` |
## Migration Guide
**Before:**
```typescript
console.log('[ServiceName] Operation done');
console.error('[ServiceName] Error:', error);
if (process.env.LOG_LEVEL === 'debug') {
console.log('Debug info');
}
```
**After:**
```typescript
import { RMABLogger } from '../utils/logger';
const logger = RMABLogger.create('ServiceName');
logger.info('Operation done');
logger.error('Error', { error: error.message });
logger.debug('Debug info'); // Automatically filtered by LOG_LEVEL
```
## Related
- [Job Queue & Processors](jobs.md) - Background job system
- [Scheduler](scheduler.md) - Recurring tasks
+5
View File
@@ -176,6 +176,11 @@ type TorrentState = 'downloading' | 'uploading' | 'stalledDL' |
- Enhanced error messages identifying SSL/TLS certificate issues with actionable guidance - Enhanced error messages identifying SSL/TLS certificate issues with actionable guidance
- Secure by default (SSL verification enabled), with clear security warnings when disabled - Secure by default (SSL verification enabled), with clear security warnings when disabled
- URL format: `https://qbt.domain.com:443/qbittorrent` fully supported - URL format: `https://qbt.domain.com:443/qbittorrent` fully supported
**12. CSRF protection HTTP 401 errors** - qBittorrent v4.1.0+ has CSRF protection enabled by default, causing authentication failures (HTTP 401) when Referer/Origin headers missing. Browsers work because they auto-send these headers. Fixed by:
- Adding `Referer` and `Origin` headers to all login requests
- Headers set to qBittorrent base URL (e.g., `https://seedbox.example.com:443/qbittorrent`)
- Applied to both `login()` and `testConnectionWithCredentials()` methods
- Works with all qBittorrent versions and configurations
## Tech Stack ## Tech Stack
+6 -3
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { ConfigurationService } from '@/lib/services/config.service'; import { ConfigurationService } from '@/lib/services/config.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BackendMode');
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -18,7 +21,7 @@ export async function GET(request: NextRequest) {
isAudiobookshelf: backendMode === 'audiobookshelf' isAudiobookshelf: backendMode === 'audiobookshelf'
}); });
} catch (error) { } catch (error) {
console.error('[BackendMode] Failed to get backend mode:', error); logger.error('Failed to get backend mode', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to get backend mode' }, { error: 'Failed to get backend mode' },
{ status: 500 } { status: 500 }
@@ -50,7 +53,7 @@ export async function PUT(request: NextRequest) {
const { clearLibraryServiceCache } = await import('@/lib/services/library'); const { clearLibraryServiceCache } = await import('@/lib/services/library');
clearLibraryServiceCache(); clearLibraryServiceCache();
console.log(`[BackendMode] Backend mode changed to: ${mode}`); logger.info(`Backend mode changed to: ${mode}`);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
@@ -58,7 +61,7 @@ export async function PUT(request: NextRequest) {
message: `Backend mode set to ${mode}` message: `Backend mode set to ${mode}`
}); });
} catch (error) { } catch (error) {
console.error('[BackendMode] Failed to set backend mode:', error); logger.error('Failed to set backend mode', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to set backend mode' }, { error: 'Failed to set backend mode' },
{ status: 500 } { status: 500 }
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.BookDate.Toggle');
async function handler(req: AuthenticatedRequest) { async function handler(req: AuthenticatedRequest) {
try { try {
@@ -31,7 +34,7 @@ async function handler(req: AuthenticatedRequest) {
}); });
} catch (error: any) { } catch (error: any) {
console.error('[BookDate] Admin toggle error:', error); logger.error('Admin toggle error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to toggle BookDate' }, { error: error.message || 'Failed to toggle BookDate' },
{ status: 500 } { status: 500 }
+5 -2
View File
@@ -9,6 +9,9 @@ import { prisma } from '@/lib/db';
import { getQBittorrentService } from '@/lib/integrations/qbittorrent.service'; import { getQBittorrentService } from '@/lib/integrations/qbittorrent.service';
import { getSABnzbdService } from '@/lib/integrations/sabnzbd.service'; import { getSABnzbdService } from '@/lib/integrations/sabnzbd.service';
import { getConfigService } from '@/lib/services/config.service'; import { getConfigService } from '@/lib/services/config.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Downloads');
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -96,7 +99,7 @@ export async function GET(request: NextRequest) {
} }
} catch (error) { } catch (error) {
// Download client unavailable or download not found - use defaults // Download client unavailable or download not found - use defaults
console.error(`[Admin] Failed to get download info:`, error); logger.error('Failed to get download info', { error: error instanceof Error ? error.message : String(error) });
} }
return { return {
@@ -117,7 +120,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ downloads: formatted }); return NextResponse.json({ downloads: formatted });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to fetch active downloads:', error); logger.error('Failed to fetch active downloads', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch active downloads' }, { error: 'Failed to fetch active downloads' },
{ status: 500 } { status: 500 }
+7 -4
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { verifyAccessToken } from '@/lib/utils/jwt'; import { verifyAccessToken } from '@/lib/utils/jwt';
import { getJobQueueService } from '@/lib/services/job-queue.service'; import { getJobQueueService } from '@/lib/services/job-queue.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.JobStatus');
/** /**
* GET /api/admin/job-status/:id * GET /api/admin/job-status/:id
@@ -30,17 +33,17 @@ export async function GET(
// Await params in Next.js 15+ // Await params in Next.js 15+
const { id } = await params; const { id } = await params;
console.log(`[JobStatus] Fetching status for job ID: ${id}`); logger.debug(`Fetching status for job ID: ${id}`);
const jobQueueService = getJobQueueService(); const jobQueueService = getJobQueueService();
const job = await jobQueueService.getJob(id); const job = await jobQueueService.getJob(id);
if (!job) { if (!job) {
console.log(`[JobStatus] Job not found: ${id}`); logger.debug(`Job not found: ${id}`);
return NextResponse.json({ error: 'Job not found' }, { status: 404 }); return NextResponse.json({ error: 'Job not found' }, { status: 404 });
} }
console.log(`[JobStatus] Job ${id} status: ${job.status}, type: ${job.type}`); logger.debug(`Job ${id} status: ${job.status}, type: ${job.type}`);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
@@ -58,7 +61,7 @@ export async function GET(
}, },
}); });
} catch (error) { } catch (error) {
console.error('Failed to get job status:', error); logger.error('Failed to get job status', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'InternalError', error: 'InternalError',
+5 -2
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { verifyAccessToken } from '@/lib/utils/jwt'; import { verifyAccessToken } from '@/lib/utils/jwt';
import { getSchedulerService } from '@/lib/services/scheduler.service'; import { getSchedulerService } from '@/lib/services/scheduler.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Jobs');
/** /**
* PUT /api/admin/jobs/:id * PUT /api/admin/jobs/:id
@@ -45,7 +48,7 @@ export async function PUT(
job, job,
}); });
} catch (error) { } catch (error) {
console.error('Failed to update scheduled job:', error); logger.error('Failed to update scheduled job', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'InternalError', error: 'InternalError',
@@ -87,7 +90,7 @@ export async function DELETE(
message: 'Job deleted successfully', message: 'Job deleted successfully',
}); });
} catch (error) { } catch (error) {
console.error('Failed to delete scheduled job:', error); logger.error('Failed to delete scheduled job', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'InternalError', error: 'InternalError',
+6 -3
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { verifyAccessToken } from '@/lib/utils/jwt'; import { verifyAccessToken } from '@/lib/utils/jwt';
import { getSchedulerService } from '@/lib/services/scheduler.service'; import { getSchedulerService } from '@/lib/services/scheduler.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.JobTrigger');
/** /**
* POST /api/admin/jobs/:id/trigger * POST /api/admin/jobs/:id/trigger
@@ -30,12 +33,12 @@ export async function POST(
// Await params in Next.js 15+ // Await params in Next.js 15+
const { id } = await params; const { id } = await params;
console.log(`[JobTrigger] Triggering scheduled job: ${id}`); logger.info(`Triggering scheduled job: ${id}`);
const schedulerService = getSchedulerService(); const schedulerService = getSchedulerService();
const jobId = await schedulerService.triggerJobNow(id); const jobId = await schedulerService.triggerJobNow(id);
console.log(`[JobTrigger] Job triggered successfully, database job ID: ${jobId}`); logger.info(`Job triggered successfully, database job ID: ${jobId}`);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
@@ -43,7 +46,7 @@ export async function POST(
message: 'Job triggered successfully', message: 'Job triggered successfully',
}); });
} catch (error) { } catch (error) {
console.error('Failed to trigger job:', error); logger.error('Failed to trigger job', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'InternalError', error: 'InternalError',
+5 -2
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { verifyAccessToken } from '@/lib/utils/jwt'; import { verifyAccessToken } from '@/lib/utils/jwt';
import { getSchedulerService } from '@/lib/services/scheduler.service'; import { getSchedulerService } from '@/lib/services/scheduler.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Jobs');
/** /**
* GET /api/admin/jobs * GET /api/admin/jobs
@@ -31,7 +34,7 @@ export async function GET(request: NextRequest) {
jobs, jobs,
}); });
} catch (error) { } catch (error) {
console.error('Failed to get scheduled jobs:', error); logger.error('Failed to get scheduled jobs', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'InternalError', error: 'InternalError',
@@ -74,7 +77,7 @@ export async function POST(request: NextRequest) {
job, job,
}); });
} catch (error) { } catch (error) {
console.error('Failed to create scheduled job:', error); logger.error('Failed to create scheduled job', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'InternalError', error: 'InternalError',
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Logs');
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -94,7 +97,7 @@ export async function GET(request: NextRequest) {
}, },
}); });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to fetch logs:', error); logger.error('Failed to fetch logs', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch logs' }, { error: 'Failed to fetch logs' },
{ status: 500 } { status: 500 }
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Metrics');
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -77,7 +80,7 @@ export async function GET(request: NextRequest) {
systemHealth, systemHealth,
}); });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to fetch metrics:', error); logger.error('Failed to fetch metrics', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch metrics' }, { error: 'Failed to fetch metrics' },
{ status: 500 } { status: 500 }
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin } from '@/lib/middleware/auth';
import { processScanPlex } from '@/lib/processors/scan-plex.processor'; import { processScanPlex } from '@/lib/processors/scan-plex.processor';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Plex.Scan');
/** /**
* POST /api/admin/plex/scan * POST /api/admin/plex/scan
@@ -27,7 +30,7 @@ export async function POST(request: NextRequest) {
...result, ...result,
}); });
} catch (error) { } catch (error) {
console.error('[API] Plex scan failed:', error); logger.error('Plex scan failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'ScanFailed', error: 'ScanFailed',
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { deleteRequest } from '@/lib/services/request-delete.service'; import { deleteRequest } from '@/lib/services/request-delete.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Requests');
/** /**
* DELETE /api/admin/requests/[id] * DELETE /api/admin/requests/[id]
@@ -62,7 +65,7 @@ export async function DELETE(
}, },
}); });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to delete request:', error); logger.error('Failed to delete request', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'DeleteError', error: 'DeleteError',
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Requests.Recent');
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -61,7 +64,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ requests: formatted }); return NextResponse.json({ requests: formatted });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to fetch recent requests:', error); logger.error('Failed to fetch recent requests', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch recent requests' }, { error: 'Failed to fetch recent requests' },
{ status: 500 } { status: 500 }
@@ -5,20 +5,23 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.ABSLibraries');
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
console.log('[ABS Libraries] GET request received'); logger.debug('GET request received');
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
console.log('[ABS Libraries] Auth passed, user:', req.user); logger.debug('Auth passed', { user: req.user });
return requireAdmin(req, async () => { return requireAdmin(req, async () => {
console.log('[ABS Libraries] Admin check passed'); logger.debug('Admin check passed');
try { try {
// Use getConfigService like Plex endpoint does // Use getConfigService like Plex endpoint does
const { getConfigService } = await import('@/lib/services/config.service'); const { getConfigService } = await import('@/lib/services/config.service');
const configService = getConfigService(); const configService = getConfigService();
const serverUrl = await configService.get('audiobookshelf.server_url'); const serverUrl = await configService.get('audiobookshelf.server_url');
const apiToken = await configService.get('audiobookshelf.api_token'); const apiToken = await configService.get('audiobookshelf.api_token');
console.log('[ABS Libraries] Config loaded:', { hasServerUrl: !!serverUrl, hasApiToken: !!apiToken }); logger.debug('Config loaded', { hasServerUrl: !!serverUrl, hasApiToken: !!apiToken });
if (!serverUrl || !apiToken) { if (!serverUrl || !apiToken) {
return NextResponse.json( return NextResponse.json(
@@ -55,7 +58,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ libraries }); return NextResponse.json({ libraries });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to fetch ABS libraries:', error); logger.error('Failed to fetch ABS libraries', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch libraries' }, { error: 'Failed to fetch libraries' },
{ status: 500 } { status: 500 }
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { ConfigUpdate } from '@/lib/services/config.service'; import { ConfigUpdate } from '@/lib/services/config.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.Audiobookshelf');
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -41,7 +44,7 @@ export async function PUT(request: NextRequest) {
message: 'Audiobookshelf settings saved successfully' message: 'Audiobookshelf settings saved successfully'
}); });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to save Audiobookshelf settings:', error); logger.error('Failed to save Audiobookshelf settings', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to save settings' }, { error: 'Failed to save settings' },
{ status: 500 } { status: 500 }
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireLocalAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireLocalAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.ChangePassword');
/** /**
* POST /api/admin/settings/change-password * POST /api/admin/settings/change-password
@@ -114,14 +117,14 @@ export async function POST(request: NextRequest) {
}, },
}); });
console.log(`[Auth] Local admin password changed successfully for user ${user.id}`); logger.info(`Local admin password changed successfully`, { userId: user.id });
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Password changed successfully', message: 'Password changed successfully',
}); });
} catch (error) { } catch (error) {
console.error('[Auth] Failed to change password:', error); logger.error('Failed to change password', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { PathMapper } from '@/lib/utils/path-mapper'; import { PathMapper } from '@/lib/utils/path-mapper';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.DownloadClient');
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -135,7 +138,7 @@ export async function PUT(request: NextRequest) {
create: { key: 'download_client_local_path', value: localPath || '' }, create: { key: 'download_client_local_path', value: localPath || '' },
}); });
console.log('[Admin] Download client settings updated'); logger.info('Download client settings updated');
// Invalidate download client service singleton to force reload of credentials and URL // Invalidate download client service singleton to force reload of credentials and URL
if (type === 'qbittorrent') { if (type === 'qbittorrent') {
@@ -151,7 +154,7 @@ export async function PUT(request: NextRequest) {
message: 'Download client settings updated successfully', message: 'Download client settings updated successfully',
}); });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to update download client settings:', error); logger.error('Failed to update download client settings', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.Ebook');
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -73,7 +76,7 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {
console.error('Failed to save e-book settings:', error); logger.error('Failed to save e-book settings', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to save settings' }, { error: 'Failed to save settings' },
{ status: 500 } { status: 500 }
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { testFlareSolverrConnection } from '@/lib/services/ebook-scraper'; import { testFlareSolverrConnection } from '@/lib/services/ebook-scraper';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.TestFlareSolverr');
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -31,7 +34,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json(result); return NextResponse.json(result);
} catch (error) { } catch (error) {
console.error('FlareSolverr test failed:', error); logger.error('FlareSolverr test failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.OIDC');
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -62,7 +65,7 @@ export async function PUT(request: NextRequest) {
message: 'OIDC settings saved successfully' message: 'OIDC settings saved successfully'
}); });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to save OIDC settings:', error); logger.error('Failed to save OIDC settings', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to save settings' }, { error: 'Failed to save settings' },
{ status: 500 } { status: 500 }
+5 -2
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.Paths');
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -65,7 +68,7 @@ export async function PUT(request: NextRequest) {
}, },
}); });
console.log('[Admin] Paths settings updated'); logger.info('Paths settings updated');
// Invalidate qBittorrent service singleton to force reload of download_dir // Invalidate qBittorrent service singleton to force reload of download_dir
const { invalidateQBittorrentService } = await import('@/lib/integrations/qbittorrent.service'); const { invalidateQBittorrentService } = await import('@/lib/integrations/qbittorrent.service');
@@ -76,7 +79,7 @@ export async function PUT(request: NextRequest) {
message: 'Paths settings updated successfully', message: 'Paths settings updated successfully',
}); });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to update paths settings:', error); logger.error('Failed to update paths settings', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { getPlexService } from '@/lib/integrations/plex.service'; import { getPlexService } from '@/lib/integrations/plex.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.PlexLibraries');
/** /**
* GET /api/admin/settings/plex/libraries * GET /api/admin/settings/plex/libraries
@@ -51,7 +54,7 @@ export async function GET(request: NextRequest) {
})), })),
}); });
} catch (error) { } catch (error) {
console.error('[Plex] Failed to fetch libraries:', error); logger.error('Failed to fetch libraries', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
+8 -5
View File
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { getPlexService } from '@/lib/integrations/plex.service'; import { getPlexService } from '@/lib/integrations/plex.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.AdminPlexSettings');
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -69,24 +72,24 @@ export async function PUT(request: NextRequest) {
update: { value: serverInfo.info.machineIdentifier }, update: { value: serverInfo.info.machineIdentifier },
create: { key: 'plex_machine_identifier', value: serverInfo.info.machineIdentifier }, create: { key: 'plex_machine_identifier', value: serverInfo.info.machineIdentifier },
}); });
console.log('[Admin] machineIdentifier updated:', serverInfo.info.machineIdentifier); logger.info('machineIdentifier updated', { machineIdentifier: serverInfo.info.machineIdentifier });
} else { } else {
console.warn('[Admin] Could not fetch machineIdentifier'); logger.warn('Could not fetch machineIdentifier');
} }
} }
} catch (error) { } catch (error) {
console.error('[Admin] Error fetching machineIdentifier:', error); logger.error('Error fetching machineIdentifier', { error: error instanceof Error ? error.message : String(error) });
// Don't fail the request if machineIdentifier fetch fails // Don't fail the request if machineIdentifier fetch fails
} }
console.log('[Admin] Plex settings updated'); logger.info('Plex settings updated');
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Plex settings updated successfully', message: 'Plex settings updated successfully',
}); });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to update Plex settings:', error); logger.error('Failed to update Plex settings', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { getProwlarrService } from '@/lib/integrations/prowlarr.service'; import { getProwlarrService } from '@/lib/integrations/prowlarr.service';
import { getConfigService } from '@/lib/services/config.service'; import { getConfigService } from '@/lib/services/config.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.ProwlarrIndexers');
interface SavedIndexerConfig { interface SavedIndexerConfig {
id: number; id: number;
@@ -65,7 +68,7 @@ export async function GET(request: NextRequest) {
flagConfigs, flagConfigs,
}); });
} catch (error) { } catch (error) {
console.error('[Prowlarr] Failed to fetch indexers:', error); logger.error('Failed to fetch indexers', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
@@ -128,7 +131,7 @@ export async function PUT(request: NextRequest) {
message: 'Indexer configuration saved', message: 'Indexer configuration saved',
}); });
} catch (error) { } catch (error) {
console.error('[Prowlarr] Failed to save indexer config:', error); logger.error('Failed to save indexer config', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
+5 -2
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.Prowlarr');
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -36,14 +39,14 @@ export async function PUT(request: NextRequest) {
}); });
} }
console.log('[Admin] Prowlarr settings updated'); logger.info('Prowlarr settings updated');
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Prowlarr settings updated successfully', message: 'Prowlarr settings updated successfully',
}); });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to update Prowlarr settings:', error); logger.error('Failed to update Prowlarr settings', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.Registration');
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -26,7 +29,7 @@ export async function PUT(request: NextRequest) {
message: 'Registration settings saved successfully' message: 'Registration settings saved successfully'
}); });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to save registration settings:', error); logger.error('Failed to save registration settings', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to save settings' }, { error: 'Failed to save settings' },
{ status: 500 } { status: 500 }
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings');
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -103,7 +106,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json(settings); return NextResponse.json(settings);
} catch (error) { } catch (error) {
console.error('[Admin] Failed to fetch settings:', error); logger.error('Failed to fetch settings', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch settings' }, { error: 'Failed to fetch settings' },
{ status: 500 } { status: 500 }
@@ -8,6 +8,9 @@ import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middlewar
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { QBittorrentService } from '@/lib/integrations/qbittorrent.service'; import { QBittorrentService } from '@/lib/integrations/qbittorrent.service';
import { SABnzbdService } from '@/lib/integrations/sabnzbd.service'; import { SABnzbdService } from '@/lib/integrations/sabnzbd.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.TestDownloadClient');
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -24,7 +27,7 @@ export async function POST(request: NextRequest) {
localPath, localPath,
} = await request.json(); } = await request.json();
console.log('[TestDownloadClient] Received request:', { type, url, hasUsername: !!username, hasPassword: !!password }); logger.debug('Received request', { type, url, hasUsername: !!username, hasPassword: !!password });
if (!type || !url) { if (!type || !url) {
return NextResponse.json( return NextResponse.json(
@@ -61,7 +64,7 @@ export async function POST(request: NextRequest) {
let version: string | undefined; let version: string | undefined;
if (type === 'qbittorrent') { if (type === 'qbittorrent') {
console.log('[TestDownloadClient] Testing qBittorrent connection'); logger.debug('Testing qBittorrent connection');
if (!username || !actualPassword) { if (!username || !actualPassword) {
return NextResponse.json( return NextResponse.json(
{ success: false, error: 'Username and password are required for qBittorrent' }, { success: false, error: 'Username and password are required for qBittorrent' },
@@ -77,7 +80,7 @@ export async function POST(request: NextRequest) {
disableSSLVerify || false disableSSLVerify || false
); );
} else if (type === 'sabnzbd') { } else if (type === 'sabnzbd') {
console.log('[TestDownloadClient] Testing SABnzbd connection'); logger.debug('Testing SABnzbd connection');
if (!actualPassword) { if (!actualPassword) {
return NextResponse.json( return NextResponse.json(
{ success: false, error: 'API key (password) is required for SABnzbd' }, { success: false, error: 'API key (password) is required for SABnzbd' },
@@ -134,7 +137,7 @@ export async function POST(request: NextRequest) {
version, version,
}); });
} catch (error) { } catch (error) {
console.error('[Admin Settings] Download client test failed:', error); logger.error('Download client test failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { getPlexService } from '@/lib/integrations/plex.service'; import { getPlexService } from '@/lib/integrations/plex.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.TestPlex');
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -70,7 +73,7 @@ export async function POST(request: NextRequest) {
})), })),
}); });
} catch (error) { } catch (error) {
console.error('[Admin Settings] Plex test failed:', error); logger.error('Plex test failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { ProwlarrService } from '@/lib/integrations/prowlarr.service'; import { ProwlarrService } from '@/lib/integrations/prowlarr.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Settings.TestProwlarr');
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -59,7 +62,7 @@ export async function POST(request: NextRequest) {
})), })),
}); });
} catch (error) { } catch (error) {
console.error('[Admin Settings] Prowlarr test failed:', error); logger.error('Prowlarr test failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Users.Approve');
export async function POST( export async function POST(
request: NextRequest, request: NextRequest,
@@ -64,7 +67,7 @@ export async function POST(
}); });
} }
} catch (error) { } catch (error) {
console.error('[Admin] Failed to approve/reject user:', error); logger.error('Failed to approve/reject user', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to process user approval' }, { error: 'Failed to process user approval' },
{ status: 500 } { status: 500 }
+5 -2
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Users');
export async function PUT( export async function PUT(
request: NextRequest, request: NextRequest,
@@ -89,7 +92,7 @@ export async function PUT(
return NextResponse.json({ user: updatedUser }); return NextResponse.json({ user: updatedUser });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to update user:', error); logger.error('Failed to update user', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to update user' }, { error: 'Failed to update user' },
{ status: 500 } { status: 500 }
@@ -184,7 +187,7 @@ export async function DELETE(
message: `User "${targetUser.plexUsername}" has been deleted. Their ${targetUser._count.requests} request(s) have been preserved.` message: `User "${targetUser.plexUsername}" has been deleted. Their ${targetUser._count.requests} request(s) have been preserved.`
}); });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to delete user:', error); logger.error('Failed to delete user', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to delete user' }, { error: 'Failed to delete user' },
{ status: 500 } { status: 500 }
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Users.Pending');
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -30,7 +33,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ users: pendingUsers }); return NextResponse.json({ users: pendingUsers });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to fetch pending users:', error); logger.error('Failed to fetch pending users', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch pending users' }, { error: 'Failed to fetch pending users' },
{ status: 500 } { status: 500 }
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Admin.Users');
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
return requireAuth(request, async (req: AuthenticatedRequest) => { return requireAuth(request, async (req: AuthenticatedRequest) => {
@@ -40,7 +43,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ users }); return NextResponse.json({ users });
} catch (error) { } catch (error) {
console.error('[Admin] Failed to fetch users:', error); logger.error('Failed to fetch users', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch users' }, { error: 'Failed to fetch users' },
{ status: 500 } { status: 500 }
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getAudibleService } from '@/lib/integrations/audible.service'; import { getAudibleService } from '@/lib/integrations/audible.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Audiobooks.Details');
/** /**
* GET /api/audiobooks/[asin] * GET /api/audiobooks/[asin]
@@ -45,7 +48,7 @@ export async function GET(
audiobook, audiobook,
}); });
} catch (error) { } catch (error) {
console.error('Failed to get audiobook details:', error); logger.error('Failed to get audiobook details', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'FetchError', error: 'FetchError',
+4 -1
View File
@@ -7,6 +7,9 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Audiobooks.Covers');
/** /**
* GET /api/audiobooks/covers?count=100 * GET /api/audiobooks/covers?count=100
@@ -64,7 +67,7 @@ export async function GET() {
count: shuffled.length, count: shuffled.length,
}); });
} catch (error) { } catch (error) {
console.error('Failed to get audiobook covers:', error); logger.error('Failed to get audiobook covers', { error: error instanceof Error ? error.message : String(error) });
// Return empty array on error (login page will show placeholders) // Return empty array on error (login page will show placeholders)
return NextResponse.json({ return NextResponse.json({
+4 -1
View File
@@ -9,6 +9,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher'; import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher';
import { getCurrentUser } from '@/lib/middleware/auth'; import { getCurrentUser } from '@/lib/middleware/auth';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Audiobooks.NewReleases');
/** /**
* GET /api/audiobooks/new-releases?page=1&limit=20 * GET /api/audiobooks/new-releases?page=1&limit=20
@@ -128,7 +131,7 @@ export async function GET(request: NextRequest) {
lastSync: audiobooks[0]?.lastSyncedAt?.toISOString() || null, lastSync: audiobooks[0]?.lastSyncedAt?.toISOString() || null,
}); });
} catch (error) { } catch (error) {
console.error('Failed to get new releases:', error); logger.error('Failed to get new releases', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'FetchError', error: 'FetchError',
+4 -1
View File
@@ -9,6 +9,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher'; import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher';
import { getCurrentUser } from '@/lib/middleware/auth'; import { getCurrentUser } from '@/lib/middleware/auth';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Audiobooks.Popular');
/** /**
* GET /api/audiobooks/popular?page=1&limit=20 * GET /api/audiobooks/popular?page=1&limit=20
@@ -128,7 +131,7 @@ export async function GET(request: NextRequest) {
lastSync: audiobooks[0]?.lastSyncedAt?.toISOString() || null, lastSync: audiobooks[0]?.lastSyncedAt?.toISOString() || null,
}); });
} catch (error) { } catch (error) {
console.error('Failed to get popular audiobooks:', error); logger.error('Failed to get popular audiobooks', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'FetchError', error: 'FetchError',
@@ -11,6 +11,9 @@ import { prisma } from '@/lib/db';
import { getJobQueueService } from '@/lib/services/job-queue.service'; import { getJobQueueService } from '@/lib/services/job-queue.service';
import { findPlexMatch } from '@/lib/utils/audiobook-matcher'; import { findPlexMatch } from '@/lib/utils/audiobook-matcher';
import { z } from 'zod'; import { z } from 'zod';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.RequestWithTorrent');
const RequestWithTorrentSchema = z.object({ const RequestWithTorrentSchema = z.object({
audiobook: z.object({ audiobook: z.object({
@@ -153,7 +156,7 @@ export async function POST(request: NextRequest) {
} }
// Delete the existing failed/warn/cancelled request // Delete the existing failed/warn/cancelled request
console.log(`[RequestWithTorrent] Deleting existing ${existingRequest.status} request ${existingRequest.id}`); logger.debug(`Deleting existing ${existingRequest.status} request ${existingRequest.id}`);
await prisma.request.delete({ await prisma.request.delete({
where: { id: existingRequest.id }, where: { id: existingRequest.id },
}); });
@@ -190,14 +193,14 @@ export async function POST(request: NextRequest) {
torrent torrent
); );
console.log(`[RequestWithTorrent] Queued download monitor job for request ${newRequest.id}`); logger.info(`Queued download monitor job for request ${newRequest.id}`);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
request: newRequest, request: newRequest,
}, { status: 201 }); }, { status: 201 });
} catch (error) { } catch (error) {
console.error('Failed to create request with torrent:', error); logger.error('Failed to create request with torrent', { error: error instanceof Error ? error.message : String(error) });
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
return NextResponse.json( return NextResponse.json(
+24 -32
View File
@@ -10,6 +10,9 @@ import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { getProwlarrService } from '@/lib/integrations/prowlarr.service'; import { getProwlarrService } from '@/lib/integrations/prowlarr.service';
import { rankTorrents } from '@/lib/utils/ranking-algorithm'; import { rankTorrents } from '@/lib/utils/ranking-algorithm';
import { z } from 'zod'; import { z } from 'zod';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.AudiobookSearch');
const SearchSchema = z.object({ const SearchSchema = z.object({
title: z.string(), title: z.string(),
@@ -68,14 +71,14 @@ export async function POST(request: NextRequest) {
const prowlarr = await getProwlarrService(); const prowlarr = await getProwlarrService();
const searchQuery = title; // Title only - cast wide net const searchQuery = title; // Title only - cast wide net
console.log(`[AudiobookSearch] Searching ${enabledIndexerIds.length} enabled indexers for: ${searchQuery}`); logger.info(`Searching ${enabledIndexerIds.length} enabled indexers`, { searchQuery });
const results = await prowlarr.search(searchQuery, { const results = await prowlarr.search(searchQuery, {
indexerIds: enabledIndexerIds, indexerIds: enabledIndexerIds,
maxResults: 100, // Increased limit for broader search maxResults: 100, // Increased limit for broader search
}); });
console.log(`[AudiobookSearch] Found ${results.length} raw results for "${title}" by ${author}`); logger.debug(`Found ${results.length} raw results`, { title, author });
if (results.length === 0) { if (results.length === 0) {
return NextResponse.json({ return NextResponse.json({
@@ -90,41 +93,30 @@ export async function POST(request: NextRequest) {
// No threshold filtering - show all results like interactive search // No threshold filtering - show all results like interactive search
// User can see scores and make their own decision // User can see scores and make their own decision
console.log(`[AudiobookSearch] Ranked ${rankedResults.length} results (no threshold filter - user decides)`); logger.debug(`Ranked ${rankedResults.length} results (no threshold filter - user decides)`);
// Log top 3 results with detailed score breakdown for debugging // Log top 3 results with detailed score breakdown for debugging
const top3 = rankedResults.slice(0, 3); const top3 = rankedResults.slice(0, 3);
if (top3.length > 0) { if (top3.length > 0) {
console.log(`[AudiobookSearch] ==================== RANKING DEBUG ====================`); logger.debug('==================== RANKING DEBUG ====================');
console.log(`[AudiobookSearch] Requested Title: "${title}"`); logger.debug('Search parameters', { requestedTitle: title, requestedAuthor: author });
console.log(`[AudiobookSearch] Requested Author: "${author}"`); logger.debug(`Top ${top3.length} results (out of ${rankedResults.length} total)`);
console.log(`[AudiobookSearch] Top ${top3.length} results (out of ${rankedResults.length} total):`); logger.debug('--------------------------------------------------------');
console.log(`[AudiobookSearch] --------------------------------------------------------`);
top3.forEach((result, index) => { top3.forEach((result, index) => {
console.log(`[AudiobookSearch] ${index + 1}. "${result.title}"`); logger.debug(`${index + 1}. "${result.title}"`, {
console.log(`[AudiobookSearch] Indexer: ${result.indexer}${result.indexerId ? ` (ID: ${result.indexerId})` : ''}`); indexer: result.indexer,
console.log(`[AudiobookSearch] `); indexerId: result.indexerId,
console.log(`[AudiobookSearch] Base Score: ${result.score.toFixed(1)}/100`); baseScore: `${result.score.toFixed(1)}/100`,
console.log(`[AudiobookSearch] - Title/Author Match: ${result.breakdown.matchScore.toFixed(1)}/60`); matchScore: `${result.breakdown.matchScore.toFixed(1)}/60`,
console.log(`[AudiobookSearch] - Format Quality: ${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`); formatScore: `${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`,
console.log(`[AudiobookSearch] - Seeder Count: ${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders !== undefined ? result.seeders + ' seeders' : 'N/A for Usenet'})`); seederScore: `${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders !== undefined ? result.seeders + ' seeders' : 'N/A for Usenet'})`,
console.log(`[AudiobookSearch] `); bonusPoints: `+${result.bonusPoints.toFixed(1)}`,
console.log(`[AudiobookSearch] Bonus Points: +${result.bonusPoints.toFixed(1)}`); bonusModifiers: result.bonusModifiers.map(mod => `${mod.reason}: +${mod.points.toFixed(1)}`),
if (result.bonusModifiers.length > 0) { finalScore: result.finalScore.toFixed(1),
result.bonusModifiers.forEach(mod => { notes: result.breakdown.notes,
console.log(`[AudiobookSearch] - ${mod.reason}: +${mod.points.toFixed(1)}`); });
});
}
console.log(`[AudiobookSearch] `);
console.log(`[AudiobookSearch] Final Score: ${result.finalScore.toFixed(1)}`);
if (result.breakdown.notes.length > 0) {
console.log(`[AudiobookSearch] Notes: ${result.breakdown.notes.join(', ')}`);
}
if (index < top3.length - 1) {
console.log(`[AudiobookSearch] --------------------------------------------------------`);
}
}); });
console.log(`[AudiobookSearch] ========================================================`); logger.debug('========================================================');
} }
// Add rank position to each result // Add rank position to each result
@@ -141,7 +133,7 @@ export async function POST(request: NextRequest) {
: 'No results found', : 'No results found',
}); });
} catch (error) { } catch (error) {
console.error('Failed to search for torrents:', error); logger.error('Failed to search for torrents', { error: error instanceof Error ? error.message : String(error) });
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
return NextResponse.json( return NextResponse.json(
+4 -1
View File
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { getAudibleService } from '@/lib/integrations/audible.service'; import { getAudibleService } from '@/lib/integrations/audible.service';
import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher'; import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher';
import { getCurrentUser } from '@/lib/middleware/auth'; import { getCurrentUser } from '@/lib/middleware/auth';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Audiobooks.Search');
/** /**
* GET /api/audiobooks/search?q=query&page=1 * GET /api/audiobooks/search?q=query&page=1
@@ -47,7 +50,7 @@ export async function GET(request: NextRequest) {
hasMore: results.hasMore, hasMore: results.hasMore,
}); });
} catch (error) { } catch (error) {
console.error('Failed to search audiobooks:', error); logger.error('Failed to search audiobooks', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'SearchError', error: 'SearchError',
+5 -2
View File
@@ -8,6 +8,9 @@ import { prisma } from '@/lib/db';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt'; import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt';
import { getEncryptionService } from '@/lib/services/encryption.service'; import { getEncryptionService } from '@/lib/services/encryption.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Auth.AdminLogin');
/** /**
* POST /api/auth/admin/login * POST /api/auth/admin/login
@@ -58,7 +61,7 @@ export async function POST(request: NextRequest) {
const decryptedHash = encryptionService.decrypt(user.authToken || ''); const decryptedHash = encryptionService.decrypt(user.authToken || '');
passwordValid = await bcrypt.compare(password, decryptedHash); passwordValid = await bcrypt.compare(password, decryptedHash);
} catch (error) { } catch (error) {
console.error('[AdminLogin] Password verification failed:', error); logger.error('Password verification failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'AuthenticationError', error: 'AuthenticationError',
@@ -109,7 +112,7 @@ export async function POST(request: NextRequest) {
}, },
}); });
} catch (error) { } catch (error) {
console.error('Failed to authenticate admin user:', error); logger.error('Failed to authenticate admin user', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'AuthenticationError', error: 'AuthenticationError',
+10 -7
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { LocalAuthProvider } from '@/lib/services/auth/LocalAuthProvider'; import { LocalAuthProvider } from '@/lib/services/auth/LocalAuthProvider';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Auth.LocalLogin');
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@@ -25,30 +28,30 @@ export async function POST(request: NextRequest) {
); );
} }
console.log('[LocalLogin] Attempting login for username:', username); logger.info('Attempting login', { username });
const provider = new LocalAuthProvider(); const provider = new LocalAuthProvider();
const result = await provider.handleCallback({ username, password }); const result = await provider.handleCallback({ username, password });
if (!result.success) { if (!result.success) {
if (result.requiresApproval) { if (result.requiresApproval) {
console.log('[LocalLogin] Account pending approval:', username); logger.info('Account pending approval', { username });
return NextResponse.json({ return NextResponse.json({
success: false, success: false,
pendingApproval: true, pendingApproval: true,
message: 'Account pending admin approval.', message: 'Account pending admin approval.',
}); });
} }
console.error('[LocalLogin] Login failed:', result.error); logger.error('Login failed', { error: result.error });
return NextResponse.json( return NextResponse.json(
{ error: result.error }, { error: result.error },
{ status: 401 } { status: 401 }
); );
} }
console.log('[LocalLogin] Login successful for:', username); logger.info('Login successful', { username });
console.log('[LocalLogin] User data:', result.user); logger.debug('User data', { user: result.user });
console.log('[LocalLogin] Token generated successfully'); logger.debug('Token generated successfully');
// Return tokens for login // Return tokens for login
return NextResponse.json({ return NextResponse.json({
@@ -58,7 +61,7 @@ export async function POST(request: NextRequest) {
refreshToken: result.tokens!.refreshToken, refreshToken: result.tokens!.refreshToken,
}); });
} catch (error) { } catch (error) {
console.error('[LocalLogin] Error:', error); logger.error('Error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Login failed' }, { error: 'Login failed' },
{ status: 500 } { status: 500 }
+5 -2
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getAuthProvider } from '@/lib/services/auth'; import { getAuthProvider } from '@/lib/services/auth';
import { getBaseUrl } from '@/lib/utils/url'; import { getBaseUrl } from '@/lib/utils/url';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Auth.OIDC.Callback');
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams; const searchParams = request.nextUrl.searchParams;
@@ -71,7 +74,7 @@ export async function GET(request: NextRequest) {
if (result.isFirstLogin) { if (result.isFirstLogin) {
// First login - redirect to initializing page to show job progress // First login - redirect to initializing page to show job progress
redirectUrl = `${baseUrl}/setup/initializing#authData=${authDataEncoded}`; redirectUrl = `${baseUrl}/setup/initializing#authData=${authDataEncoded}`;
console.log('[OIDC Callback] First login detected - redirecting to initializing page'); logger.info('First login detected - redirecting to initializing page');
} else { } else {
// Normal login - redirect to login page with auth success // Normal login - redirect to login page with auth success
redirectUrl = `${baseUrl}/login?auth=success#authData=${authDataEncoded}`; redirectUrl = `${baseUrl}/login?auth=success#authData=${authDataEncoded}`;
@@ -132,7 +135,7 @@ export async function GET(request: NextRequest) {
return response; return response;
} catch (error) { } catch (error) {
console.error('[OIDC Callback] Authentication failed:', error); logger.error('Authentication failed', { error: error instanceof Error ? error.message : String(error) });
const errorMsg = error instanceof Error ? error.message : 'Authentication failed'; const errorMsg = error instanceof Error ? error.message : 'Authentication failed';
return NextResponse.redirect(`${baseUrl}/login?error=${encodeURIComponent(errorMsg)}`); return NextResponse.redirect(`${baseUrl}/login?error=${encodeURIComponent(errorMsg)}`);
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getAuthProvider } from '@/lib/services/auth'; import { getAuthProvider } from '@/lib/services/auth';
import { getBaseUrl } from '@/lib/utils/url'; import { getBaseUrl } from '@/lib/utils/url';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Auth.OIDC.Login');
export async function GET() { export async function GET() {
try { try {
@@ -25,7 +28,7 @@ export async function GET() {
// Redirect to OIDC provider // Redirect to OIDC provider
return NextResponse.redirect(redirectUrl); return NextResponse.redirect(redirectUrl);
} catch (error) { } catch (error) {
console.error('[OIDC Login] Failed to initiate login:', error); logger.error('Failed to initiate login', { error: error instanceof Error ? error.message : String(error) });
// Redirect to login page with error // Redirect to login page with error
const baseUrl = getBaseUrl(); const baseUrl = getBaseUrl();
+19 -16
View File
@@ -9,6 +9,9 @@ import { getEncryptionService } from '@/lib/services/encryption.service';
import { getConfigService } from '@/lib/services/config.service'; import { getConfigService } from '@/lib/services/config.service';
import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt'; import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.PlexCallback');
/** /**
* GET /api/auth/plex/callback?pinId=12345 * GET /api/auth/plex/callback?pinId=12345
@@ -52,7 +55,7 @@ export async function GET(request: NextRequest) {
// Validate user info // Validate user info
if (!plexUser || !plexUser.id) { if (!plexUser || !plexUser.id) {
console.error('[Plex OAuth] Invalid user info received:', plexUser); logger.error('Invalid user info received', { plexUser });
return NextResponse.json( return NextResponse.json(
{ {
error: 'OAuthError', error: 'OAuthError',
@@ -64,7 +67,7 @@ export async function GET(request: NextRequest) {
} }
if (!plexUser.username) { if (!plexUser.username) {
console.error('[Plex OAuth] Username missing from Plex user:', plexUser); logger.error('Username missing from Plex user', { plexUser });
return NextResponse.json( return NextResponse.json(
{ {
error: 'OAuthError', error: 'OAuthError',
@@ -84,7 +87,7 @@ export async function GET(request: NextRequest) {
// Verify server is configured // Verify server is configured
if (!plexConfig.serverUrl || !plexConfig.authToken) { if (!plexConfig.serverUrl || !plexConfig.authToken) {
console.error('[Plex OAuth] Server not configured'); logger.error('Server not configured');
return NextResponse.json( return NextResponse.json(
{ {
error: 'ConfigurationError', error: 'ConfigurationError',
@@ -99,7 +102,7 @@ export async function GET(request: NextRequest) {
const serverMachineId = plexConfig.machineIdentifier; const serverMachineId = plexConfig.machineIdentifier;
if (!serverMachineId) { if (!serverMachineId) {
console.error('[Plex OAuth] machineIdentifier not found in configuration'); logger.error('machineIdentifier not found in configuration');
return NextResponse.json( return NextResponse.json(
{ {
error: 'ConfigurationError', error: 'ConfigurationError',
@@ -109,7 +112,7 @@ export async function GET(request: NextRequest) {
); );
} }
console.log('[Plex OAuth] Using stored machineIdentifier:', serverMachineId); logger.debug('Using stored machineIdentifier', { serverMachineId });
// SECURITY: Verify user has access to the configured Plex server // SECURITY: Verify user has access to the configured Plex server
// This checks if the server appears in the user's list of accessible servers from plex.tv // This checks if the server appears in the user's list of accessible servers from plex.tv
@@ -121,7 +124,7 @@ export async function GET(request: NextRequest) {
); );
if (!hasAccess) { if (!hasAccess) {
console.warn('[Plex OAuth] User attempted to authenticate without server access:', { logger.warn('User attempted to authenticate without server access', {
plexId: plexIdString, plexId: plexIdString,
username: plexUser.username, username: plexUser.username,
serverMachineId, serverMachineId,
@@ -135,16 +138,16 @@ export async function GET(request: NextRequest) {
); );
} }
console.log('[Plex OAuth] User verified with server access:', plexUser.username); logger.info('User verified with server access', { username: plexUser.username });
// Check for Plex Home profiles // Check for Plex Home profiles
const homeUsers = await plexService.getHomeUsers(authToken); const homeUsers = await plexService.getHomeUsers(authToken);
console.log('[Plex OAuth] Found home users:', homeUsers.length); logger.debug('Found home users', { count: homeUsers.length });
// If multiple home users exist, redirect to profile selection // If multiple home users exist, redirect to profile selection
// (Only show selection if there's more than just the main account) // (Only show selection if there's more than just the main account)
if (homeUsers.length > 1) { if (homeUsers.length > 1) {
console.log('[Plex OAuth] Account has multiple home profiles, redirecting to profile selection'); logger.info('Account has multiple home profiles, redirecting to profile selection');
// Detect if this is a browser request (mobile redirect) vs AJAX (desktop popup polling) // Detect if this is a browser request (mobile redirect) vs AJAX (desktop popup polling)
const accept = request.headers.get('accept') || ''; const accept = request.headers.get('accept') || '';
@@ -157,7 +160,7 @@ export async function GET(request: NextRequest) {
(process.env.NODE_ENV === 'production' ? 'https' : 'http'); (process.env.NODE_ENV === 'production' ? 'https' : 'http');
const selectProfileUrl = `${protocol}://${host}/auth/select-profile?pinId=${pinId}`; const selectProfileUrl = `${protocol}://${host}/auth/select-profile?pinId=${pinId}`;
console.log('[Plex OAuth] Redirecting to profile selection:', selectProfileUrl); logger.debug('Redirecting to profile selection', { selectProfileUrl });
// Return HTML page with JavaScript to store token in sessionStorage and redirect // Return HTML page with JavaScript to store token in sessionStorage and redirect
const html = ` const html = `
@@ -197,7 +200,7 @@ export async function GET(request: NextRequest) {
} }
} }
console.log('[Plex OAuth] Single profile or no additional profiles, continuing with main account authentication'); logger.debug('Single profile or no additional profiles, continuing with main account authentication');
// No home users - continue with normal authentication flow using main account // No home users - continue with normal authentication flow using main account
// Check if this is the first user (should be promoted to admin) // Check if this is the first user (should be promoted to admin)
@@ -248,8 +251,8 @@ export async function GET(request: NextRequest) {
(process.env.NODE_ENV === 'production' ? 'https' : 'http'); (process.env.NODE_ENV === 'production' ? 'https' : 'http');
const redirectUrl = `${protocol}://${host}/login?auth=success`; const redirectUrl = `${protocol}://${host}/login?auth=success`;
console.log('[Plex OAuth] Setting cookies for mobile auth...'); logger.debug('Setting cookies for mobile auth');
console.log('[Plex OAuth] Redirect URL:', redirectUrl); logger.debug('Redirect URL', { redirectUrl });
// Prepare user data // Prepare user data
const userDataJson = JSON.stringify({ const userDataJson = JSON.stringify({
@@ -260,7 +263,7 @@ export async function GET(request: NextRequest) {
role: user.role, role: user.role,
avatarUrl: user.avatarUrl, avatarUrl: user.avatarUrl,
}); });
console.log('[Plex OAuth] Setting userData cookie:', userDataJson); logger.debug('Setting userData cookie', { userDataJson });
// Prepare auth data to pass via URL hash (fallback for mobile browsers that block cookies) // Prepare auth data to pass via URL hash (fallback for mobile browsers that block cookies)
const authData = { const authData = {
@@ -331,7 +334,7 @@ export async function GET(request: NextRequest) {
path: '/', path: '/',
}); });
console.log('[Plex OAuth] Cookies set successfully, returning HTML redirect to:', redirectUrl); logger.debug('Cookies set successfully, returning HTML redirect', { redirectUrl });
return response; return response;
} }
@@ -351,7 +354,7 @@ export async function GET(request: NextRequest) {
}, },
}); });
} catch (error) { } catch (error) {
console.error('Failed to complete Plex OAuth:', error); logger.error('Failed to complete Plex OAuth', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'OAuthError', error: 'OAuthError',
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getPlexService } from '@/lib/integrations/plex.service'; import { getPlexService } from '@/lib/integrations/plex.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Auth.Plex.HomeUsers');
/** /**
* GET /api/auth/plex/home-users * GET /api/auth/plex/home-users
@@ -32,7 +35,7 @@ export async function GET(request: NextRequest) {
users, users,
}); });
} catch (error) { } catch (error) {
console.error('Failed to get home users:', error); logger.error('Failed to get home users', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'ServerError', error: 'ServerError',
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getPlexService } from '@/lib/integrations/plex.service'; import { getPlexService } from '@/lib/integrations/plex.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Auth.PlexLogin');
/** /**
* POST /api/auth/plex/login * POST /api/auth/plex/login
@@ -33,7 +36,7 @@ export async function POST(request: NextRequest) {
authUrl, authUrl,
}); });
} catch (error) { } catch (error) {
console.error('Failed to initiate Plex OAuth:', error); logger.error('Failed to initiate Plex OAuth', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'OAuthError', error: 'OAuthError',
@@ -8,6 +8,9 @@ import { getPlexService } from '@/lib/integrations/plex.service';
import { getEncryptionService } from '@/lib/services/encryption.service'; import { getEncryptionService } from '@/lib/services/encryption.service';
import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt'; import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.PlexSwitchProfile');
/** /**
* POST /api/auth/plex/switch-profile * POST /api/auth/plex/switch-profile
@@ -77,7 +80,7 @@ export async function POST(request: NextRequest) {
profileUsername = profileInfo.friendlyName || `User ${userId}`; profileUsername = profileInfo.friendlyName || `User ${userId}`;
profileEmail = profileInfo.email || null; profileEmail = profileInfo.email || null;
profileThumb = profileInfo.thumb || null; profileThumb = profileInfo.thumb || null;
console.log('[Profile Switch] Using provided profile info:', { logger.debug('Using provided profile info', {
plexId: profilePlexId, plexId: profilePlexId,
username: profileUsername, username: profileUsername,
}); });
@@ -86,7 +89,7 @@ export async function POST(request: NextRequest) {
const profileUser = await plexService.getUserInfo(profileToken); const profileUser = await plexService.getUserInfo(profileToken);
if (!profileUser || !profileUser.id) { if (!profileUser || !profileUser.id) {
console.error('[Profile Switch] Failed to get profile user info'); logger.error('Failed to get profile user info');
return NextResponse.json( return NextResponse.json(
{ {
error: 'ServerError', error: 'ServerError',
@@ -100,7 +103,7 @@ export async function POST(request: NextRequest) {
profileUsername = profileUser.username || `User ${userId}`; profileUsername = profileUser.username || `User ${userId}`;
profileEmail = profileUser.email || null; profileEmail = profileUser.email || null;
profileThumb = profileUser.thumb || null; profileThumb = profileUser.thumb || null;
console.log('[Profile Switch] Using getUserInfo data:', { logger.debug('Using getUserInfo data', {
plexId: profilePlexId, plexId: profilePlexId,
username: profileUsername, username: profileUsername,
}); });
@@ -134,7 +137,7 @@ export async function POST(request: NextRequest) {
}, },
}); });
console.log('[Profile Switch] User authenticated:', { logger.info('User authenticated', {
id: user.id, id: user.id,
plexId: user.plexId, plexId: user.plexId,
username: user.plexUsername, username: user.plexUsername,
@@ -167,7 +170,7 @@ export async function POST(request: NextRequest) {
}, },
}); });
} catch (error) { } catch (error) {
console.error('Failed to switch profile:', error); logger.error('Failed to switch profile', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'ServerError', error: 'ServerError',
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { ConfigurationService } from '@/lib/services/config.service'; import { ConfigurationService } from '@/lib/services/config.service';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Auth.Providers');
export async function GET() { export async function GET() {
try { try {
@@ -58,7 +61,7 @@ export async function GET() {
}); });
} }
} catch (error) { } catch (error) {
console.error('[Auth] Failed to fetch auth providers:', error); logger.error('Failed to fetch auth providers', { error: error instanceof Error ? error.message : String(error) });
// Default to Plex mode if config can't be read // Default to Plex mode if config can't be read
const localLoginDisabled = process.env.DISABLE_LOCAL_LOGIN === 'true'; const localLoginDisabled = process.env.DISABLE_LOCAL_LOGIN === 'true';
return NextResponse.json({ return NextResponse.json({
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { verifyRefreshToken, generateAccessToken } from '@/lib/utils/jwt'; import { verifyRefreshToken, generateAccessToken } from '@/lib/utils/jwt';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Auth.Refresh');
/** /**
* POST /api/auth/refresh * POST /api/auth/refresh
@@ -68,7 +71,7 @@ export async function POST(request: NextRequest) {
expiresIn: 3600, // 1 hour in seconds expiresIn: 3600, // 1 hour in seconds
}); });
} catch (error) { } catch (error) {
console.error('Failed to refresh token:', error); logger.error('Failed to refresh token', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'RefreshError', error: 'RefreshError',
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { LocalAuthProvider } from '@/lib/services/auth/LocalAuthProvider'; import { LocalAuthProvider } from '@/lib/services/auth/LocalAuthProvider';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Auth.Register');
// Rate limiting map (in production, use Redis) // Rate limiting map (in production, use Redis)
const registrationAttempts = new Map<string, { count: number; resetAt: number }>(); const registrationAttempts = new Map<string, { count: number; resetAt: number }>();
@@ -74,7 +77,7 @@ export async function POST(request: NextRequest) {
refreshToken: result.tokens!.refreshToken, refreshToken: result.tokens!.refreshToken,
}); });
} catch (error) { } catch (error) {
console.error('[Registration] Error:', error); logger.error('Registration error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Registration failed' }, { error: 'Registration failed' },
{ status: 500 } { status: 500 }
+6 -3
View File
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { getEncryptionService } from '@/lib/services/encryption.service'; import { getEncryptionService } from '@/lib/services/encryption.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDateConfig');
// GET: Fetch global BookDate configuration (excluding API key) // GET: Fetch global BookDate configuration (excluding API key)
// Any authenticated user can check if BookDate is configured // Any authenticated user can check if BookDate is configured
@@ -24,7 +27,7 @@ async function getConfig(req: AuthenticatedRequest) {
return NextResponse.json({ config: safeConfig }); return NextResponse.json({ config: safeConfig });
} catch (error: any) { } catch (error: any) {
console.error('[BookDate] Get config error:', error); logger.error('Get config error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to fetch configuration' }, { error: error.message || 'Failed to fetch configuration' },
{ status: 500 } { status: 500 }
@@ -129,7 +132,7 @@ async function saveConfig(req: AuthenticatedRequest) {
}); });
} catch (error: any) { } catch (error: any) {
console.error('[BookDate] Save config error:', error); logger.error('Save config error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to save configuration' }, { error: error.message || 'Failed to save configuration' },
{ status: 500 } { status: 500 }
@@ -162,7 +165,7 @@ async function deleteConfig(req: AuthenticatedRequest) {
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error: any) { } catch (error: any) {
console.error('[BookDate] Delete config error:', error); logger.error('Delete config error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to delete configuration' }, { error: error.message || 'Failed to delete configuration' },
{ status: 500 } { status: 500 }
+10 -7
View File
@@ -14,6 +14,9 @@ import {
isAlreadyRequested, isAlreadyRequested,
isAlreadySwiped, isAlreadySwiped,
} from '@/lib/bookdate/helpers'; } from '@/lib/bookdate/helpers';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.Generate');
async function handler(req: AuthenticatedRequest) { async function handler(req: AuthenticatedRequest) {
try { try {
@@ -54,7 +57,7 @@ async function handler(req: AuthenticatedRequest) {
}; };
// Build prompt and call AI (same as recommendations endpoint, but doesn't check cache) // Build prompt and call AI (same as recommendations endpoint, but doesn't check cache)
console.log('[BookDate] Force generating new recommendations for user:', userId); logger.info('Force generating new recommendations for user', { userId });
const prompt = await buildAIPrompt(userId, userPreferences); const prompt = await buildAIPrompt(userId, userPreferences);
const aiResponse = await callAI(config.provider, config.model, config.apiKey, prompt); const aiResponse = await callAI(config.provider, config.model, config.apiKey, prompt);
@@ -62,7 +65,7 @@ async function handler(req: AuthenticatedRequest) {
throw new Error('Invalid AI response format: missing recommendations array'); throw new Error('Invalid AI response format: missing recommendations array');
} }
console.log(`[BookDate] AI returned ${aiResponse.recommendations.length} recommendations`); logger.debug('AI returned recommendations', { count: aiResponse.recommendations.length });
// Match to Audnexus and filter // Match to Audnexus and filter
const batchId = `batch_${Date.now()}`; const batchId = `batch_${Date.now()}`;
@@ -88,14 +91,14 @@ async function handler(req: AuthenticatedRequest) {
const audnexusMatch = await matchToAudnexus(rec.title, rec.author); const audnexusMatch = await matchToAudnexus(rec.title, rec.author);
if (!audnexusMatch) { if (!audnexusMatch) {
console.warn(`[BookDate] No Audnexus match: "${rec.title}" by ${rec.author}`); logger.warn('No Audnexus match', { title: rec.title, author: rec.author });
continue; continue;
} }
// Check again if in library with ASIN for exact matching // Check again if in library with ASIN for exact matching
// This catches books that might have different titles (e.g., "The Tenant" vs "The Tenant (Unabridged)") // This catches books that might have different titles (e.g., "The Tenant" vs "The Tenant (Unabridged)")
if (await isInLibrary(userId, audnexusMatch.title, audnexusMatch.author, audnexusMatch.asin)) { if (await isInLibrary(userId, audnexusMatch.title, audnexusMatch.author, audnexusMatch.asin)) {
console.log(`[BookDate] Book "${audnexusMatch.title}" (ASIN: ${audnexusMatch.asin}) is in library, skipping`); logger.debug('Book is in library, skipping', { title: audnexusMatch.title, asin: audnexusMatch.asin });
continue; continue;
} }
@@ -122,12 +125,12 @@ async function handler(req: AuthenticatedRequest) {
} }
} catch (error) { } catch (error) {
console.warn(`[BookDate] Match error for "${rec.title}":`, error); logger.warn('Match error', { title: rec.title, error: error instanceof Error ? error.message : String(error) });
continue; continue;
} }
} }
console.log(`[BookDate] Matched ${matched.length} new recommendations`); logger.info('Matched new recommendations', { count: matched.length });
if (matched.length === 0) { if (matched.length === 0) {
return NextResponse.json( return NextResponse.json(
@@ -163,7 +166,7 @@ async function handler(req: AuthenticatedRequest) {
}); });
} catch (error: any) { } catch (error: any) {
console.error('[BookDate] Generate error:', error); logger.error('Generate error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: error.message || 'Failed to generate new recommendations', error: error.message || 'Failed to generate new recommendations',
+5 -2
View File
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { getConfigService } from '@/lib/services/config.service'; import { getConfigService } from '@/lib/services/config.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.Preferences');
/** /**
* GET /api/bookdate/preferences * GET /api/bookdate/preferences
@@ -54,7 +57,7 @@ async function getPreferences(req: AuthenticatedRequest) {
}); });
} catch (error: any) { } catch (error: any) {
console.error('Get BookDate preferences error:', error); logger.error('Get BookDate preferences error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to get preferences' }, { error: error.message || 'Failed to get preferences' },
{ status: 500 } { status: 500 }
@@ -135,7 +138,7 @@ async function updatePreferences(req: AuthenticatedRequest) {
}); });
} catch (error: any) { } catch (error: any) {
console.error('Update BookDate preferences error:', error); logger.error('Update BookDate preferences error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to update preferences' }, { error: error.message || 'Failed to update preferences' },
{ status: 500 } { status: 500 }
+14 -11
View File
@@ -14,6 +14,9 @@ import {
isAlreadyRequested, isAlreadyRequested,
isAlreadySwiped, isAlreadySwiped,
} from '@/lib/bookdate/helpers'; } from '@/lib/bookdate/helpers';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.Recommendations');
async function handler(req: AuthenticatedRequest) { async function handler(req: AuthenticatedRequest) {
try { try {
@@ -75,7 +78,7 @@ async function handler(req: AuthenticatedRequest) {
}; };
// Build prompt and call AI // Build prompt and call AI
console.log('[BookDate] Generating new recommendations for user:', userId); logger.info('Generating new recommendations for user', { userId });
const prompt = await buildAIPrompt(userId, userPreferences); const prompt = await buildAIPrompt(userId, userPreferences);
const aiResponse = await callAI(config.provider, config.model, config.apiKey, prompt); const aiResponse = await callAI(config.provider, config.model, config.apiKey, prompt);
@@ -83,7 +86,7 @@ async function handler(req: AuthenticatedRequest) {
throw new Error('Invalid AI response format: missing recommendations array'); throw new Error('Invalid AI response format: missing recommendations array');
} }
console.log(`[BookDate] AI returned ${aiResponse.recommendations.length} recommendations`); logger.debug('AI returned recommendations', { count: aiResponse.recommendations.length });
// Match to Audnexus and filter // Match to Audnexus and filter
const batchId = `batch_${Date.now()}`; const batchId = `batch_${Date.now()}`;
@@ -91,19 +94,19 @@ async function handler(req: AuthenticatedRequest) {
for (const rec of aiResponse.recommendations) { for (const rec of aiResponse.recommendations) {
if (!rec.title || !rec.author) { if (!rec.title || !rec.author) {
console.warn('[BookDate] Skipping recommendation with missing title or author'); logger.warn('Skipping recommendation with missing title or author');
continue; continue;
} }
// Check if already swiped // Check if already swiped
if (await isAlreadySwiped(userId, rec.title, rec.author)) { if (await isAlreadySwiped(userId, rec.title, rec.author)) {
console.log(`[BookDate] Skipping already swiped: "${rec.title}"`); logger.debug('Skipping already swiped', { title: rec.title });
continue; continue;
} }
// Check if in library // Check if in library
if (await isInLibrary(userId, rec.title, rec.author)) { if (await isInLibrary(userId, rec.title, rec.author)) {
console.log(`[BookDate] Skipping already in library: "${rec.title}"`); logger.debug('Skipping already in library', { title: rec.title });
continue; continue;
} }
@@ -112,20 +115,20 @@ async function handler(req: AuthenticatedRequest) {
const audnexusMatch = await matchToAudnexus(rec.title, rec.author); const audnexusMatch = await matchToAudnexus(rec.title, rec.author);
if (!audnexusMatch) { if (!audnexusMatch) {
console.warn(`[BookDate] No Audnexus match: "${rec.title}" by ${rec.author}`); logger.warn('No Audnexus match', { title: rec.title, author: rec.author });
continue; continue;
} }
// Check again if in library with ASIN for exact matching // Check again if in library with ASIN for exact matching
// This catches books that might have different titles (e.g., "The Tenant" vs "The Tenant (Unabridged)") // This catches books that might have different titles (e.g., "The Tenant" vs "The Tenant (Unabridged)")
if (await isInLibrary(userId, audnexusMatch.title, audnexusMatch.author, audnexusMatch.asin)) { if (await isInLibrary(userId, audnexusMatch.title, audnexusMatch.author, audnexusMatch.asin)) {
console.log(`[BookDate] Book "${audnexusMatch.title}" (ASIN: ${audnexusMatch.asin}) is in library, skipping`); logger.debug('Book is in library, skipping', { title: audnexusMatch.title, asin: audnexusMatch.asin });
continue; continue;
} }
// Check if already requested // Check if already requested
if (await isAlreadyRequested(userId, audnexusMatch.asin)) { if (await isAlreadyRequested(userId, audnexusMatch.asin)) {
console.log(`[BookDate] Skipping already requested: "${rec.title}"`); logger.debug('Skipping already requested', { title: rec.title });
continue; continue;
} }
@@ -147,12 +150,12 @@ async function handler(req: AuthenticatedRequest) {
} }
} catch (error) { } catch (error) {
console.warn(`[BookDate] Match error for "${rec.title}":`, error); logger.warn('Match error', { title: rec.title, error: error instanceof Error ? error.message : String(error) });
continue; continue;
} }
} }
console.log(`[BookDate] Matched ${matched.length} recommendations`); logger.info('Matched recommendations', { count: matched.length });
// Save to database // Save to database
if (matched.length > 0) { if (matched.length > 0) {
@@ -180,7 +183,7 @@ async function handler(req: AuthenticatedRequest) {
}); });
} catch (error: any) { } catch (error: any) {
console.error('[BookDate] Recommendations error:', error); logger.error('Recommendations error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: error.message || 'Failed to generate recommendations', error: error.message || 'Failed to generate recommendations',
+7 -4
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDateSwipe');
async function handler(req: AuthenticatedRequest) { async function handler(req: AuthenticatedRequest) {
try { try {
@@ -97,7 +100,7 @@ async function handler(req: AuthenticatedRequest) {
}, },
}); });
console.log(`[BookDate] Created request for "${recommendation.title}"`); logger.info(`Created request for "${recommendation.title}"`);
// Trigger search job (same as regular request creation) // Trigger search job (same as regular request creation)
const { getJobQueueService } = await import('@/lib/services/job-queue.service'); const { getJobQueueService } = await import('@/lib/services/job-queue.service');
@@ -108,11 +111,11 @@ async function handler(req: AuthenticatedRequest) {
author: audiobook.author, author: audiobook.author,
}); });
console.log(`[BookDate] Triggered search job for request ${newRequest.id}`); logger.info(`Triggered search job for request ${newRequest.id}`);
} }
} catch (error) { } catch (error) {
console.error('[BookDate] Error creating request:', error); logger.error('Error creating request', { error: error instanceof Error ? error.message : String(error) });
// Don't fail the swipe if request creation fails // Don't fail the swipe if request creation fails
} }
} }
@@ -124,7 +127,7 @@ async function handler(req: AuthenticatedRequest) {
}); });
} catch (error: any) { } catch (error: any) {
console.error('[BookDate] Swipe error:', error); logger.error('Swipe error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to record swipe' }, { error: error.message || 'Failed to record swipe' },
{ status: 500 } { status: 500 }
+5 -2
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.Swipes');
// DELETE: Clear all users' swipe history (Admin only) // DELETE: Clear all users' swipe history (Admin only)
async function clearSwipes(req: AuthenticatedRequest) { async function clearSwipes(req: AuthenticatedRequest) {
@@ -16,7 +19,7 @@ async function clearSwipes(req: AuthenticatedRequest) {
// Also clear all cached recommendations (since swipe history affects recommendations) // Also clear all cached recommendations (since swipe history affects recommendations)
await prisma.bookDateRecommendation.deleteMany({}); await prisma.bookDateRecommendation.deleteMany({});
console.log('[BookDate] Admin cleared all swipe history and recommendations'); logger.info('Admin cleared all swipe history and recommendations');
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
@@ -24,7 +27,7 @@ async function clearSwipes(req: AuthenticatedRequest) {
}); });
} catch (error: any) { } catch (error: any) {
console.error('[BookDate] Clear swipes error:', error); logger.error('Clear swipes error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to clear swipe history' }, { error: error.message || 'Failed to clear swipe history' },
{ status: 500 } { status: 500 }
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.TestConnection');
async function authenticatedHandler(req: AuthenticatedRequest) { async function authenticatedHandler(req: AuthenticatedRequest) {
try { try {
@@ -64,7 +67,7 @@ async function authenticatedHandler(req: AuthenticatedRequest) {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('[BookDate] OpenAI API error:', errorText); logger.error('OpenAI API error', { error: errorText });
return NextResponse.json( return NextResponse.json(
{ error: 'Invalid OpenAI API key or connection failed' }, { error: 'Invalid OpenAI API key or connection failed' },
{ status: 400 } { status: 400 }
@@ -108,7 +111,7 @@ async function authenticatedHandler(req: AuthenticatedRequest) {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('[BookDate] Claude API error:', errorText); logger.error('Claude API error', { error: errorText });
return NextResponse.json( return NextResponse.json(
{ error: 'Invalid Claude API key or connection failed' }, { error: 'Invalid Claude API key or connection failed' },
{ status: 400 } { status: 400 }
@@ -123,7 +126,7 @@ async function authenticatedHandler(req: AuthenticatedRequest) {
}); });
} catch (error: any) { } catch (error: any) {
console.error('[BookDate] Test connection error:', error); logger.error('Test connection error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Connection test failed' }, { error: error.message || 'Connection test failed' },
{ status: 500 } { status: 500 }
@@ -179,7 +182,7 @@ async function unauthenticatedHandler(req: NextRequest) {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('[BookDate] OpenAI API error:', errorText); logger.error('OpenAI API error', { error: errorText });
return NextResponse.json( return NextResponse.json(
{ error: 'Invalid OpenAI API key or connection failed' }, { error: 'Invalid OpenAI API key or connection failed' },
{ status: 400 } { status: 400 }
@@ -223,7 +226,7 @@ async function unauthenticatedHandler(req: NextRequest) {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('[BookDate] Claude API error:', errorText); logger.error('Claude API error', { error: errorText });
return NextResponse.json( return NextResponse.json(
{ error: 'Invalid Claude API key or connection failed' }, { error: 'Invalid Claude API key or connection failed' },
{ status: 400 } { status: 400 }
@@ -238,7 +241,7 @@ async function unauthenticatedHandler(req: NextRequest) {
}); });
} catch (error: any) { } catch (error: any) {
console.error('[BookDate] Test connection error:', error); logger.error('Test connection error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Connection test failed' }, { error: error.message || 'Connection test failed' },
{ status: 500 } { status: 500 }
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.BookDate.Undo');
async function handler(req: AuthenticatedRequest) { async function handler(req: AuthenticatedRequest) {
try { try {
@@ -77,7 +80,7 @@ async function handler(req: AuthenticatedRequest) {
}); });
} catch (error: any) { } catch (error: any) {
console.error('[BookDate] Undo error:', error); logger.error('Undo error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error.message || 'Failed to undo swipe' }, { error: error.message || 'Failed to undo swipe' },
{ status: 500 } { status: 500 }
+4 -1
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Thumbnails');
const CACHE_DIR = '/app/cache/thumbnails'; const CACHE_DIR = '/app/cache/thumbnails';
@@ -60,7 +63,7 @@ export async function GET(
}, },
}); });
} catch (error) { } catch (error) {
console.error('[ThumbnailAPI] Error serving thumbnail:', error); logger.error('Error serving thumbnail', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: 'Internal server error' }, { error: 'Internal server error' },
{ status: 500 } { status: 500 }
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getConfigService } from '@/lib/services/config.service'; import { getConfigService } from '@/lib/services/config.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Config.Category');
// GET /api/config/:category - Get all config for a category // GET /api/config/:category - Get all config for a category
export async function GET( export async function GET(
@@ -23,7 +26,7 @@ export async function GET(
config, config,
}); });
} catch (error) { } catch (error) {
console.error(`Failed to get config for category:`, error); logger.error('Failed to get config for category', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'Failed to get configuration', error: 'Failed to get configuration',
+5 -2
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getConfigService, ConfigUpdate } from '@/lib/services/config.service'; import { getConfigService, ConfigUpdate } from '@/lib/services/config.service';
import { z } from 'zod'; import { z } from 'zod';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Config');
const ConfigUpdateSchema = z.object({ const ConfigUpdateSchema = z.object({
updates: z.array( updates: z.array(
@@ -35,7 +38,7 @@ export async function PUT(request: NextRequest) {
updated: updates.length, updated: updates.length,
}); });
} catch (error) { } catch (error) {
console.error('Failed to update configuration:', error); logger.error('Failed to update configuration', { error: error instanceof Error ? error.message : String(error) });
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
return NextResponse.json( return NextResponse.json(
@@ -69,7 +72,7 @@ export async function GET() {
config: allConfig, config: allConfig,
}); });
} catch (error) { } catch (error) {
console.error('Failed to get all configuration:', error); logger.error('Failed to get all configuration', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'Failed to get configuration', error: 'Failed to get configuration',
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Health');
export async function GET() { export async function GET() {
try { try {
@@ -17,7 +20,7 @@ export async function GET() {
database: 'connected', database: 'connected',
}); });
} catch (error) { } catch (error) {
console.error('Health check failed:', error); logger.error('Health check failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
status: 'unhealthy', status: 'unhealthy',
+6 -3
View File
@@ -8,25 +8,28 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getSchedulerService } from '@/lib/services/scheduler.service'; import { getSchedulerService } from '@/lib/services/scheduler.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Init');
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
console.log('[Init] Initializing application services...'); logger.info('Initializing application services...');
// Initialize scheduler service // Initialize scheduler service
const schedulerService = getSchedulerService(); const schedulerService = getSchedulerService();
await schedulerService.start(); await schedulerService.start();
console.log('[Init] Application services initialized successfully'); logger.info('Application services initialized successfully');
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Application services initialized', message: 'Application services initialized',
}); });
} catch (error) { } catch (error) {
console.error('[Init] Failed to initialize services:', error); logger.error('Failed to initialize services', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
+15 -12
View File
@@ -11,8 +11,9 @@ import { prisma } from '@/lib/db';
import { downloadEbook } from '@/lib/services/ebook-scraper'; import { downloadEbook } from '@/lib/services/ebook-scraper';
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import { RMABLogger } from '@/lib/utils/logger';
const DEBUG_ENABLED = process.env.LOG_LEVEL === 'debug'; const logger = RMABLogger.create('API.FetchEbook');
/** /**
* Sanitize path component (same logic as file-organizer) * Sanitize path component (same logic as file-organizer)
@@ -135,19 +136,21 @@ export async function POST(
audiobook.audibleAsin audiobook.audibleAsin
); );
if (DEBUG_ENABLED) { logger.debug('Fetch e-book request', {
console.log(`[FetchEbook] Request: ${id}, Title: "${audiobook.title}", Author: "${audiobook.author}"`); requestId: id,
console.log(`[FetchEbook] Target path: ${targetPath}`); title: audiobook.title,
console.log(`[FetchEbook] Config: format=${preferredFormat}, baseUrl=${baseUrl}, flaresolverr=${flaresolverrUrl || 'none'}`); author: audiobook.author,
} targetPath,
format: preferredFormat,
baseUrl,
flaresolverr: flaresolverrUrl || 'none'
});
// Check if target directory exists // Check if target directory exists
try { try {
await fs.access(targetPath); await fs.access(targetPath);
} catch { } catch {
if (DEBUG_ENABLED) { logger.debug(`Target directory not found: ${targetPath}`);
console.log(`[FetchEbook] Target directory not found: ${targetPath}`);
}
return NextResponse.json( return NextResponse.json(
{ error: 'Audiobook directory not found. Was the audiobook properly organized?' }, { error: 'Audiobook directory not found. Was the audiobook properly organized?' },
{ status: 400 } { status: 400 }
@@ -167,21 +170,21 @@ export async function POST(
); );
if (result.success) { if (result.success) {
console.log(`[FetchEbook] Success: ${result.filePath ? path.basename(result.filePath) : 'unknown'} for "${audiobook.title}"`); logger.info(`E-book downloaded: ${result.filePath ? path.basename(result.filePath) : 'unknown'} for "${audiobook.title}"`);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: `E-book downloaded: ${result.filePath ? path.basename(result.filePath) : 'unknown'}`, message: `E-book downloaded: ${result.filePath ? path.basename(result.filePath) : 'unknown'}`,
format: result.format, format: result.format,
}); });
} else { } else {
console.log(`[FetchEbook] Failed for "${audiobook.title}": ${result.error}`); logger.warn(`E-book download failed for "${audiobook.title}"`, { error: result.error });
return NextResponse.json({ return NextResponse.json({
success: false, success: false,
message: result.error || 'E-book download failed', message: result.error || 'E-book download failed',
}); });
} }
} catch (error) { } catch (error) {
console.error('[FetchEbook] Unexpected error:', error instanceof Error ? error.message : error); logger.error('Unexpected error', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Internal server error' }, { error: error instanceof Error ? error.message : 'Internal server error' },
{ status: 500 } { status: 500 }
@@ -8,6 +8,9 @@ import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { getProwlarrService } from '@/lib/integrations/prowlarr.service'; import { getProwlarrService } from '@/lib/integrations/prowlarr.service';
import { rankTorrents } from '@/lib/utils/ranking-algorithm'; import { rankTorrents } from '@/lib/utils/ranking-algorithm';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.InteractiveSearch');
/** /**
* POST /api/requests/[id]/interactive-search * POST /api/requests/[id]/interactive-search
@@ -96,9 +99,9 @@ export async function POST(
// Use custom title if provided, otherwise use audiobook's title // Use custom title if provided, otherwise use audiobook's title
const searchQuery = customTitle || requestRecord.audiobook.title; const searchQuery = customTitle || requestRecord.audiobook.title;
console.log(`[InteractiveSearch] Searching ${enabledIndexerIds.length} enabled indexers for: ${searchQuery}`); logger.info(`Searching ${enabledIndexerIds.length} enabled indexers`, { searchQuery });
if (customTitle) { if (customTitle) {
console.log(`[InteractiveSearch] Using custom search title (original: "${requestRecord.audiobook.title}")`); logger.debug('Using custom search title', { customTitle, originalTitle: requestRecord.audiobook.title });
} }
const results = await prowlarr.search(searchQuery, { const results = await prowlarr.search(searchQuery, {
@@ -106,7 +109,7 @@ export async function POST(
maxResults: 100, // Increased limit for broader search maxResults: 100, // Increased limit for broader search
}); });
console.log(`[InteractiveSearch] Found ${results.length} raw results for request ${id}`); logger.debug(`Found ${results.length} raw results`, { requestId: id });
if (results.length === 0) { if (results.length === 0) {
return NextResponse.json({ return NextResponse.json({
@@ -125,42 +128,30 @@ export async function POST(
// No threshold filtering for interactive search - show all results // No threshold filtering for interactive search - show all results
// User can see scores and make their own decision // User can see scores and make their own decision
console.log(`[InteractiveSearch] Ranked ${rankedResults.length} results (no threshold filter - user decides)`); logger.debug(`Ranked ${rankedResults.length} results (no threshold filter - user decides)`);
// Log top 3 results with detailed score breakdown for debugging // Log top 3 results with detailed score breakdown for debugging
const top3 = rankedResults.slice(0, 3); const top3 = rankedResults.slice(0, 3);
if (top3.length > 0) { if (top3.length > 0) {
console.log(`[InteractiveSearch] ==================== RANKING DEBUG ====================`); logger.debug('==================== RANKING DEBUG ====================');
console.log(`[InteractiveSearch] Search Query: "${searchQuery}"`); logger.debug('Search parameters', { searchQuery, requestedTitle: requestRecord.audiobook.title, requestedAuthor: requestRecord.audiobook.author });
console.log(`[InteractiveSearch] Requested Title (for ranking): "${requestRecord.audiobook.title}"`); logger.debug(`Top ${top3.length} results (out of ${rankedResults.length} total)`);
console.log(`[InteractiveSearch] Requested Author (for ranking): "${requestRecord.audiobook.author}"`); logger.debug('--------------------------------------------------------');
console.log(`[InteractiveSearch] Top ${top3.length} results (out of ${rankedResults.length} total):`);
console.log(`[InteractiveSearch] --------------------------------------------------------`);
top3.forEach((result, index) => { top3.forEach((result, index) => {
console.log(`[InteractiveSearch] ${index + 1}. "${result.title}"`); logger.debug(`${index + 1}. "${result.title}"`, {
console.log(`[InteractiveSearch] Indexer: ${result.indexer}${result.indexerId ? ` (ID: ${result.indexerId})` : ''}`); indexer: result.indexer,
console.log(`[InteractiveSearch] `); indexerId: result.indexerId,
console.log(`[InteractiveSearch] Base Score: ${result.score.toFixed(1)}/100`); baseScore: `${result.score.toFixed(1)}/100`,
console.log(`[InteractiveSearch] - Title/Author Match: ${result.breakdown.matchScore.toFixed(1)}/60`); matchScore: `${result.breakdown.matchScore.toFixed(1)}/60`,
console.log(`[InteractiveSearch] - Format Quality: ${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`); formatScore: `${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`,
console.log(`[InteractiveSearch] - Seeder Count: ${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders} seeders)`); seederScore: `${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders} seeders)`,
console.log(`[InteractiveSearch] `); bonusPoints: `+${result.bonusPoints.toFixed(1)}`,
console.log(`[InteractiveSearch] Bonus Points: +${result.bonusPoints.toFixed(1)}`); bonusModifiers: result.bonusModifiers.map(mod => `${mod.reason}: +${mod.points.toFixed(1)}`),
if (result.bonusModifiers.length > 0) { finalScore: result.finalScore.toFixed(1),
result.bonusModifiers.forEach(mod => { notes: result.breakdown.notes,
console.log(`[InteractiveSearch] - ${mod.reason}: +${mod.points.toFixed(1)}`); });
});
}
console.log(`[InteractiveSearch] `);
console.log(`[InteractiveSearch] Final Score: ${result.finalScore.toFixed(1)}`);
if (result.breakdown.notes.length > 0) {
console.log(`[InteractiveSearch] Notes: ${result.breakdown.notes.join(', ')}`);
}
if (index < top3.length - 1) {
console.log(`[InteractiveSearch] --------------------------------------------------------`);
}
}); });
console.log(`[InteractiveSearch] ========================================================`); logger.debug('========================================================');
} }
// Add rank position to each result // Add rank position to each result
@@ -177,7 +168,7 @@ export async function POST(
: 'No results found', : 'No results found',
}); });
} catch (error) { } catch (error) {
console.error('Failed to perform interactive search:', error); logger.error('Failed to perform interactive search', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'SearchError', error: 'SearchError',
@@ -7,6 +7,9 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { getJobQueueService } from '@/lib/services/job-queue.service'; import { getJobQueueService } from '@/lib/services/job-queue.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.ManualSearch');
/** /**
* POST /api/requests/[id]/manual-search * POST /api/requests/[id]/manual-search
@@ -89,7 +92,7 @@ export async function POST(
message: 'Manual search initiated', message: 'Manual search initiated',
}); });
} catch (error) { } catch (error) {
console.error('Failed to trigger manual search:', error); logger.error('Failed to trigger manual search', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'SearchError', error: 'SearchError',
+6 -3
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth'; import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.RequestById');
/** /**
* GET /api/requests/[id] * GET /api/requests/[id]
@@ -70,7 +73,7 @@ export async function GET(
request: requestRecord, request: requestRecord,
}); });
} catch (error) { } catch (error) {
console.error('Failed to get request:', error); logger.error('Failed to get request', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'FetchError', error: 'FetchError',
@@ -304,7 +307,7 @@ export async function PATCH(
{ status: 400 } { status: 400 }
); );
} catch (error) { } catch (error) {
console.error('Failed to update request:', error); logger.error('Failed to update request', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'UpdateError', error: 'UpdateError',
@@ -351,7 +354,7 @@ export async function DELETE(
message: 'Request deleted successfully', message: 'Request deleted successfully',
}); });
} catch (error) { } catch (error) {
console.error('Failed to delete request:', error); logger.error('Failed to delete request', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'DeleteError', error: 'DeleteError',
@@ -8,6 +8,9 @@ import { requireAuth, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { getJobQueueService } from '@/lib/services/job-queue.service'; import { getJobQueueService } from '@/lib/services/job-queue.service';
import { TorrentResult } from '@/lib/utils/ranking-algorithm'; import { TorrentResult } from '@/lib/utils/ranking-algorithm';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.SelectTorrent');
/** /**
* POST /api/requests/[id]/select-torrent * POST /api/requests/[id]/select-torrent
@@ -59,7 +62,7 @@ export async function POST(
); );
} }
console.log(`[SelectTorrent] User selected torrent: ${torrent.title} for request ${id}`); logger.info(`User selected torrent: ${torrent.title}`, { requestId: id });
// Trigger download job with the selected torrent // Trigger download job with the selected torrent
const jobQueue = getJobQueueService(); const jobQueue = getJobQueueService();
@@ -93,7 +96,7 @@ export async function POST(
message: 'Torrent download initiated', message: 'Torrent download initiated',
}); });
} catch (error) { } catch (error) {
console.error('Failed to select torrent:', error); logger.error('Failed to select torrent', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'DownloadError', error: 'DownloadError',
+6 -3
View File
@@ -9,6 +9,9 @@ import { prisma } from '@/lib/db';
import { getJobQueueService } from '@/lib/services/job-queue.service'; import { getJobQueueService } from '@/lib/services/job-queue.service';
import { findPlexMatch } from '@/lib/utils/audiobook-matcher'; import { findPlexMatch } from '@/lib/utils/audiobook-matcher';
import { z } from 'zod'; import { z } from 'zod';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Requests');
const CreateRequestSchema = z.object({ const CreateRequestSchema = z.object({
audiobook: z.object({ audiobook: z.object({
@@ -138,7 +141,7 @@ export async function POST(request: NextRequest) {
} }
// Delete the existing failed/warn/cancelled request // Delete the existing failed/warn/cancelled request
console.log(`[Requests] Deleting existing ${existingRequest.status} request ${existingRequest.id} to allow re-request`); logger.debug(`Deleting existing ${existingRequest.status} request ${existingRequest.id} to allow re-request`);
await prisma.request.delete({ await prisma.request.delete({
where: { id: existingRequest.id }, where: { id: existingRequest.id },
}); });
@@ -181,7 +184,7 @@ export async function POST(request: NextRequest) {
request: newRequest, request: newRequest,
}, { status: 201 }); }, { status: 201 });
} catch (error) { } catch (error) {
console.error('Failed to create request:', error); logger.error('Failed to create request', { error: error instanceof Error ? error.message : String(error) });
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
return NextResponse.json( return NextResponse.json(
@@ -255,7 +258,7 @@ export async function GET(request: NextRequest) {
count: requests.length, count: requests.length,
}); });
} catch (error) { } catch (error) {
console.error('Failed to get requests:', error); logger.error('Failed to get requests', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
error: 'FetchError', error: 'FetchError',
+12 -9
View File
@@ -9,6 +9,9 @@ import bcrypt from 'bcrypt';
import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt'; import { generateAccessToken, generateRefreshToken } from '@/lib/utils/jwt';
import { getEncryptionService } from '@/lib/services/encryption.service'; import { getEncryptionService } from '@/lib/services/encryption.service';
import { getPlexService } from '@/lib/integrations/plex.service'; import { getPlexService } from '@/lib/integrations/plex.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Setup.Complete');
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@@ -172,12 +175,12 @@ export async function POST(request: NextRequest) {
const serverInfo = await plexService.testConnection(plex.url, plex.token); const serverInfo = await plexService.testConnection(plex.url, plex.token);
if (serverInfo.success && serverInfo.info?.machineIdentifier) { if (serverInfo.success && serverInfo.info?.machineIdentifier) {
machineIdentifier = serverInfo.info.machineIdentifier; machineIdentifier = serverInfo.info.machineIdentifier;
console.log('[Setup] Fetched machineIdentifier:', machineIdentifier); logger.debug('Fetched machineIdentifier', { machineIdentifier });
} else { } else {
console.warn('[Setup] Could not fetch machineIdentifier'); logger.warn('Could not fetch machineIdentifier');
} }
} catch (error) { } catch (error) {
console.error('[Setup] Error fetching machineIdentifier:', error); logger.error('Error fetching machineIdentifier', { error: error instanceof Error ? error.message : String(error) });
} }
} }
@@ -441,7 +444,7 @@ export async function POST(request: NextRequest) {
// BookDate configuration (optional, global for all users) // BookDate configuration (optional, global for all users)
// Note: libraryScope and customPrompt are now per-user settings, not required here // Note: libraryScope and customPrompt are now per-user settings, not required here
if (bookdate && bookdate.provider && bookdate.apiKey && bookdate.model) { if (bookdate && bookdate.provider && bookdate.apiKey && bookdate.model) {
console.log('[Setup] Saving global BookDate configuration'); logger.info('Saving global BookDate configuration');
const encryptionService = getEncryptionService(); const encryptionService = getEncryptionService();
const encryptedApiKey = encryptionService.encrypt(bookdate.apiKey); const encryptedApiKey = encryptionService.encrypt(bookdate.apiKey);
@@ -478,9 +481,9 @@ export async function POST(request: NextRequest) {
}); });
} }
console.log('[Setup] Global BookDate configuration saved'); logger.debug('Global BookDate configuration saved');
} else { } else {
console.log('[Setup] BookDate configuration skipped (missing provider, apiKey, or model)'); logger.debug('BookDate configuration skipped (missing provider, apiKey, or model)');
} }
// Mark setup as complete // Mark setup as complete
@@ -502,9 +505,9 @@ export async function POST(request: NextRequest) {
}, },
}); });
console.log('[Setup] Auto jobs enabled'); logger.debug('Auto jobs enabled');
console.log('[Setup] Configuration saved successfully'); logger.info('Configuration saved successfully');
// Return response with tokens if admin user was created // Return response with tokens if admin user was created
if (adminUser && accessToken && refreshToken) { if (adminUser && accessToken && refreshToken) {
@@ -530,7 +533,7 @@ export async function POST(request: NextRequest) {
}); });
} }
} catch (error) { } catch (error) {
console.error('[Setup] Failed to save configuration:', error); logger.error('Failed to save configuration', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db'; import { prisma } from '@/lib/db';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Setup.Status');
/** /**
* GET /api/setup/status * GET /api/setup/status
@@ -24,7 +27,7 @@ export async function GET(request: NextRequest) {
}); });
} catch (error) { } catch (error) {
// If database is not ready or table doesn't exist, setup is not complete // If database is not ready or table doesn't exist, setup is not complete
console.error('[Setup Status] Check failed:', error); logger.error('Check failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json({ return NextResponse.json({
setupComplete: false, setupComplete: false,
}); });
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { QBittorrentService } from '@/lib/integrations/qbittorrent.service'; import { QBittorrentService } from '@/lib/integrations/qbittorrent.service';
import { SABnzbdService } from '@/lib/integrations/sabnzbd.service'; import { SABnzbdService } from '@/lib/integrations/sabnzbd.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Setup.TestDownloadClient');
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@@ -80,7 +83,7 @@ export async function POST(request: NextRequest) {
{ status: 400 } { status: 400 }
); );
} catch (error) { } catch (error) {
console.error('[Setup] Download client test failed:', error); logger.error('Download client test failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { Issuer } from 'openid-client'; import { Issuer } from 'openid-client';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Setup.TestOIDC');
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@@ -65,7 +68,7 @@ export async function POST(request: NextRequest) {
}, },
}); });
} catch (error) { } catch (error) {
console.error('[Test OIDC] Discovery failed:', error); logger.error('Discovery failed', { error: error instanceof Error ? error.message : String(error) });
// Determine error message // Determine error message
let errorMessage = 'OIDC discovery failed'; let errorMessage = 'OIDC discovery failed';
+9 -6
View File
@@ -6,21 +6,24 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Setup.TestPaths');
async function testPath(dirPath: string): Promise<boolean> { async function testPath(dirPath: string): Promise<boolean> {
try { try {
// Try to access the path // Try to access the path
try { try {
await fs.access(dirPath); await fs.access(dirPath);
console.log(`[Setup] Path exists: ${dirPath}`); logger.debug('Path exists', { path: dirPath });
} catch (accessError) { } catch (accessError) {
// Path doesn't exist, try to create it // Path doesn't exist, try to create it
console.log(`[Setup] Path doesn't exist, creating: ${dirPath}`); logger.debug('Path does not exist, creating', { path: dirPath });
try { try {
await fs.mkdir(dirPath, { recursive: true }); await fs.mkdir(dirPath, { recursive: true });
console.log(`[Setup] Successfully created path: ${dirPath}`); logger.debug('Successfully created path', { path: dirPath });
} catch (mkdirError) { } catch (mkdirError) {
console.error(`[Setup] Failed to create path ${dirPath}:`, mkdirError); logger.error('Failed to create path', { path: dirPath, error: mkdirError instanceof Error ? mkdirError.message : String(mkdirError) });
// If mkdir fails, it means the parent mount doesn't exist or isn't writable // If mkdir fails, it means the parent mount doesn't exist or isn't writable
return false; return false;
} }
@@ -35,7 +38,7 @@ async function testPath(dirPath: string): Promise<boolean> {
return true; return true;
} catch (error) { } catch (error) {
console.error(`[Setup] Path test failed for ${dirPath}:`, error); logger.error('Path test failed', { path: dirPath, error: error instanceof Error ? error.message : String(error) });
return false; return false;
} }
} }
@@ -81,7 +84,7 @@ export async function POST(request: NextRequest) {
message: 'Directories are ready and writable (created if needed)', message: 'Directories are ready and writable (created if needed)',
}); });
} catch (error) { } catch (error) {
console.error('[Setup] Path validation failed:', error); logger.error('Path validation failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getPlexService } from '@/lib/integrations/plex.service'; import { getPlexService } from '@/lib/integrations/plex.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Setup.TestPlex');
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@@ -49,7 +52,7 @@ export async function POST(request: NextRequest) {
})), })),
}); });
} catch (error) { } catch (error) {
console.error('[Setup] Plex test failed:', error); logger.error('Plex test failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
+4 -1
View File
@@ -5,6 +5,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { ProwlarrService } from '@/lib/integrations/prowlarr.service'; import { ProwlarrService } from '@/lib/integrations/prowlarr.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('API.Setup.TestProwlarr');
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@@ -38,7 +41,7 @@ export async function POST(request: NextRequest) {
})), })),
}); });
} catch (error) { } catch (error) {
console.error('[Setup] Prowlarr test failed:', error); logger.error('Prowlarr test failed', { error: error instanceof Error ? error.message : String(error) });
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
+49 -45
View File
@@ -9,6 +9,9 @@ import { getConfigService } from '@/lib/services/config.service';
import { AudibleService } from '@/lib/integrations/audible.service'; import { AudibleService } from '@/lib/integrations/audible.service';
import { getPlexService } from '@/lib/integrations/plex.service'; import { getPlexService } from '@/lib/integrations/plex.service';
import { findPlexMatch } from '@/lib/utils/audiobook-matcher'; import { findPlexMatch } from '@/lib/utils/audiobook-matcher';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('BookDate');
export interface LibraryBook { export interface LibraryBook {
title: string; title: string;
@@ -57,7 +60,7 @@ async function enrichWithUserRatings(
}); });
if (!user) { if (!user) {
console.warn('[BookDate] User not found'); logger.warn('User not found');
return cachedBooks.map(book => ({ return cachedBooks.map(book => ({
title: book.title, title: book.title,
author: book.author, author: book.author,
@@ -69,7 +72,7 @@ async function enrichWithUserRatings(
// Local admin users: Use cached ratings (from system Plex token) // Local admin users: Use cached ratings (from system Plex token)
// Local admins authenticate with username/password, not Plex OAuth // Local admins authenticate with username/password, not Plex OAuth
if (user.plexId.startsWith('local-')) { if (user.plexId.startsWith('local-')) {
console.log('[BookDate] User is local admin, using cached ratings (from system Plex token)'); logger.info('User is local admin, using cached ratings (from system Plex token)');
return cachedBooks.map(book => ({ return cachedBooks.map(book => ({
title: book.title, title: book.title,
author: book.author, author: book.author,
@@ -80,10 +83,10 @@ async function enrichWithUserRatings(
// Plex-authenticated users (including admins): Fetch library with their token to get personal ratings // Plex-authenticated users (including admins): Fetch library with their token to get personal ratings
// Note: /library/sections/{id}/all returns items with the authenticated user's ratings // Note: /library/sections/{id}/all returns items with the authenticated user's ratings
console.log('[BookDate] User is Plex-authenticated, fetching library with user token to get personal ratings'); logger.info('User is Plex-authenticated, fetching library with user token to get personal ratings');
if (!user.authToken) { if (!user.authToken) {
console.warn('[BookDate] User has no Plex auth token'); logger.warn('User has no Plex auth token');
return cachedBooks.map(book => ({ return cachedBooks.map(book => ({
title: book.title, title: book.title,
author: book.author, author: book.author,
@@ -97,7 +100,7 @@ async function enrichWithUserRatings(
const plexConfig = await configService.getPlexConfig(); const plexConfig = await configService.getPlexConfig();
if (!plexConfig.serverUrl || !plexConfig.libraryId) { if (!plexConfig.serverUrl || !plexConfig.libraryId) {
console.warn('[BookDate] No Plex server URL or library ID configured'); logger.warn('No Plex server URL or library ID configured');
return cachedBooks.map(book => ({ return cachedBooks.map(book => ({
title: book.title, title: book.title,
author: book.author, author: book.author,
@@ -114,7 +117,7 @@ async function enrichWithUserRatings(
} catch (decryptError) { } catch (decryptError) {
// Token might be stored as plain text (from before encryption or different implementation) // Token might be stored as plain text (from before encryption or different implementation)
// Try using it as-is // Try using it as-is
console.warn('[BookDate] Failed to decrypt user Plex token, trying as plain text'); logger.warn('Failed to decrypt user Plex token, trying as plain text');
userPlexToken = user.authToken; userPlexToken = user.authToken;
} }
@@ -126,7 +129,7 @@ async function enrichWithUserRatings(
// Get server machine ID from stored config (no need to access system token) // Get server machine ID from stored config (no need to access system token)
if (!plexConfig.machineIdentifier) { if (!plexConfig.machineIdentifier) {
console.error('[BookDate] Server machine identifier not configured'); logger.error('Server machine identifier not configured');
return cachedBooks.map(book => ({ return cachedBooks.map(book => ({
title: book.title, title: book.title,
author: book.author, author: book.author,
@@ -142,7 +145,7 @@ async function enrichWithUserRatings(
); );
if (!serverAccessToken) { if (!serverAccessToken) {
console.warn('[BookDate] Could not get server access token for user (may not have server access)'); logger.warn('Could not get server access token for user (may not have server access)');
return cachedBooks.map(book => ({ return cachedBooks.map(book => ({
title: book.title, title: book.title,
author: book.author, author: book.author,
@@ -151,7 +154,7 @@ async function enrichWithUserRatings(
})); }));
} }
console.log('[BookDate] Successfully obtained server access token for user'); logger.info('Successfully obtained server access token for user');
// Fetch library content with user's SERVER access token to get their personal ratings // Fetch library content with user's SERVER access token to get their personal ratings
const userLibrary = await plexService.getLibraryContent( const userLibrary = await plexService.getLibraryContent(
@@ -160,7 +163,7 @@ async function enrichWithUserRatings(
plexConfig.libraryId plexConfig.libraryId
); );
console.log(`[BookDate] Fetched ${userLibrary.length} items from Plex with user's token`); logger.info(`Fetched ${userLibrary.length} items from Plex with user's token`);
// Create a map of guid/ratingKey -> userRating for quick lookup // Create a map of guid/ratingKey -> userRating for quick lookup
const ratingsMap = new Map<string, number>(); const ratingsMap = new Map<string, number>();
@@ -177,7 +180,7 @@ async function enrichWithUserRatings(
} }
}); });
console.log(`[BookDate] Found ${ratingsMap.size} rated items for non-admin user`); logger.info(`Found ${ratingsMap.size} rated items for non-admin user`);
// Enrich cached books with user's ratings from the fetched library // Enrich cached books with user's ratings from the fetched library
return cachedBooks.map(book => { return cachedBooks.map(book => {
@@ -200,10 +203,10 @@ async function enrichWithUserRatings(
} catch (fetchError: any) { } catch (fetchError: any) {
if (fetchError?.response?.status === 401 || fetchError?.message?.includes('401')) { if (fetchError?.response?.status === 401 || fetchError?.message?.includes('401')) {
console.warn('[BookDate] User token unauthorized for library access (shared users may not have direct API access)'); logger.warn('User token unauthorized for library access (shared users may not have direct API access)');
console.warn('[BookDate] Falling back to recommendations without user ratings'); logger.warn('Falling back to recommendations without user ratings');
} else { } else {
console.error('[BookDate] Failed to fetch library with user token:', fetchError); logger.error('Failed to fetch library with user token', { error: fetchError instanceof Error ? fetchError.message : String(fetchError) });
} }
// Fallback: return books without ratings // Fallback: return books without ratings
return cachedBooks.map(book => ({ return cachedBooks.map(book => ({
@@ -215,7 +218,7 @@ async function enrichWithUserRatings(
} }
} catch (error) { } catch (error) {
console.error('[BookDate] Error enriching books with user ratings:', error); logger.error('Error enriching books with user ratings', { error: error instanceof Error ? error.message : String(error) });
// Fallback: return books without ratings on error // Fallback: return books without ratings on error
return cachedBooks.map(book => ({ return cachedBooks.map(book => ({
title: book.title, title: book.title,
@@ -242,7 +245,7 @@ export async function getUserLibraryBooks(
// Early validation: audiobookshelf doesn't support ratings // Early validation: audiobookshelf doesn't support ratings
if (backendMode === 'audiobookshelf' && scope === 'rated') { if (backendMode === 'audiobookshelf' && scope === 'rated') {
console.warn('[BookDate] Audiobookshelf does not support ratings, falling back to full library'); logger.warn('Audiobookshelf does not support ratings, falling back to full library');
scope = 'full'; scope = 'full';
} }
@@ -251,7 +254,7 @@ export async function getUserLibraryBooks(
if (backendMode === 'audiobookshelf') { if (backendMode === 'audiobookshelf') {
const absLibraryId = await configService.get('audiobookshelf.library_id'); const absLibraryId = await configService.get('audiobookshelf.library_id');
if (!absLibraryId) { if (!absLibraryId) {
console.warn('[BookDate] No Audiobookshelf library ID configured'); logger.warn('No Audiobookshelf library ID configured');
return []; return [];
} }
libraryId = absLibraryId; libraryId = absLibraryId;
@@ -259,7 +262,7 @@ export async function getUserLibraryBooks(
// Plex mode // Plex mode
const plexConfig = await configService.getPlexConfig(); const plexConfig = await configService.getPlexConfig();
if (!plexConfig.libraryId) { if (!plexConfig.libraryId) {
console.warn('[BookDate] No Plex library ID configured'); logger.warn('No Plex library ID configured');
return []; return [];
} }
libraryId = plexConfig.libraryId; libraryId = plexConfig.libraryId;
@@ -327,7 +330,7 @@ export async function getUserLibraryBooks(
} }
} catch (error) { } catch (error) {
console.error('[BookDate] Error fetching library books:', error); logger.error('Error fetching library books', { error: error instanceof Error ? error.message : String(error) });
return []; return [];
} }
} }
@@ -390,8 +393,8 @@ export async function getUserRecentSwipes(
(a, b) => b.createdAt.getTime() - a.createdAt.getTime() (a, b) => b.createdAt.getTime() - a.createdAt.getTime()
); );
console.log( logger.info(
`[BookDate] Fetched ${allSwipes.length} swipes: ${nonDismissSwipes.length} non-dismiss, ${dismissSwipes.length} dismiss` `Fetched ${allSwipes.length} swipes: ${nonDismissSwipes.length} non-dismiss, ${dismissSwipes.length} dismiss`
); );
return allSwipes.map((s) => ({ return allSwipes.map((s) => ({
@@ -402,7 +405,7 @@ export async function getUserRecentSwipes(
})); }));
} catch (error) { } catch (error) {
console.error('[BookDate] Error fetching swipe history:', error); logger.error('Error fetching swipe history', { error: error instanceof Error ? error.message : String(error) });
return []; return [];
} }
} }
@@ -424,11 +427,12 @@ export async function buildAIPrompt(
const swipeHistory = await getUserRecentSwipes(userId, 10); const swipeHistory = await getUserRecentSwipes(userId, 10);
console.log('[BookDate] Building AI prompt with context:'); logger.info('Building AI prompt with context:', {
console.log(`[BookDate] - Library books: ${libraryBooks.length}`); libraryBooks: libraryBooks.length,
console.log(`[BookDate] - Swipe history: ${swipeHistory.length}`); swipeHistory: swipeHistory.length,
console.log(`[BookDate] - Custom prompt: ${config.customPrompt ? 'Yes' : 'No'}`); customPrompt: config.customPrompt ? 'Yes' : 'No',
console.log(`[BookDate] - Library scope: ${config.libraryScope}`); libraryScope: config.libraryScope,
});
const prompt = { const prompt = {
task: 'recommend_audiobooks', task: 'recommend_audiobooks',
@@ -466,7 +470,7 @@ export async function buildAIPrompt(
}; };
const promptString = JSON.stringify(prompt); const promptString = JSON.stringify(prompt);
console.log('[BookDate] Full AI prompt:', promptString); logger.debug('Full AI prompt:', { prompt: promptString });
return promptString; return promptString;
} }
@@ -488,7 +492,7 @@ export async function callAI(
const encryptionService = getEncryptionService(); const encryptionService = getEncryptionService();
const apiKey = encryptionService.decrypt(encryptedApiKey); const apiKey = encryptionService.decrypt(encryptedApiKey);
console.log(`[BookDate] Calling AI provider: ${provider}, model: ${model}`); logger.info(`Calling AI provider: ${provider}, model: ${model}`);
if (provider === 'openai') { if (provider === 'openai') {
const systemMessage = 'You are an expert audiobook recommender. Analyze user preferences and suggest audiobooks they will love. Return ONLY valid JSON.'; const systemMessage = 'You are an expert audiobook recommender. Analyze user preferences and suggest audiobooks they will love. Return ONLY valid JSON.';
@@ -507,7 +511,7 @@ export async function callAI(
], ],
}; };
console.log('[BookDate] OpenAI request body:', JSON.stringify(requestBody, null, 2)); logger.debug('OpenAI request body:', { requestBody });
const response = await fetch('https://api.openai.com/v1/chat/completions', { const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST', method: 'POST',
@@ -520,13 +524,13 @@ export async function callAI(
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('[BookDate] OpenAI API error:', response.status, errorText); logger.error('OpenAI API error', { status: response.status, error: errorText });
throw new Error(`OpenAI API error: ${response.status} ${errorText}`); throw new Error(`OpenAI API error: ${response.status} ${errorText}`);
} }
const data = await response.json(); const data = await response.json();
const content = data.choices[0].message.content; const content = data.choices[0].message.content;
console.log('[BookDate] OpenAI response:', content); logger.debug('OpenAI response:', { content });
return JSON.parse(content); return JSON.parse(content);
} else if (provider === 'claude') { } else if (provider === 'claude') {
@@ -542,7 +546,7 @@ export async function callAI(
], ],
}; };
console.log('[BookDate] Claude request body:', JSON.stringify(requestBody, null, 2)); logger.debug('Claude request body:', { requestBody });
const response = await fetch('https://api.anthropic.com/v1/messages', { const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST', method: 'POST',
@@ -556,13 +560,13 @@ export async function callAI(
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('[BookDate] Claude API error:', response.status, errorText); logger.error('Claude API error', { status: response.status, error: errorText });
throw new Error(`Claude API error: ${response.status} ${errorText}`); throw new Error(`Claude API error: ${response.status} ${errorText}`);
} }
const data = await response.json(); const data = await response.json();
const content = data.content[0].text; const content = data.content[0].text;
console.log('[BookDate] Claude raw response:', content); logger.debug('Claude raw response:', { content });
// Claude sometimes wraps JSON in markdown code blocks, so clean it // Claude sometimes wraps JSON in markdown code blocks, so clean it
const cleanedContent = content const cleanedContent = content
@@ -570,7 +574,7 @@ export async function callAI(
.replace(/\s*```$/i, '') .replace(/\s*```$/i, '')
.trim(); .trim();
console.log('[BookDate] Claude cleaned response:', cleanedContent); logger.debug('Claude cleaned response:', { cleanedContent });
return JSON.parse(cleanedContent); return JSON.parse(cleanedContent);
} else { } else {
@@ -625,7 +629,7 @@ export async function matchToAudnexus(
}); });
if (cached) { if (cached) {
console.log(`[BookDate] Found in cache: "${cached.title}" by ${cached.author}`); logger.info(`Found in cache: "${cached.title}" by ${cached.author}`);
return { return {
asin: cached.asin, asin: cached.asin,
title: cached.title, title: cached.title,
@@ -638,29 +642,29 @@ export async function matchToAudnexus(
} }
// Step 2: Search Audible.com for the book // Step 2: Search Audible.com for the book
console.log(`[BookDate] Not in cache, searching Audible for "${title}" by ${author}...`); logger.info(`Not in cache, searching Audible for "${title}" by ${author}...`);
const audibleService = new AudibleService(); const audibleService = new AudibleService();
const searchQuery = `${title} ${author}`; const searchQuery = `${title} ${author}`;
const searchResults = await audibleService.search(searchQuery, 1); const searchResults = await audibleService.search(searchQuery, 1);
if (!searchResults.results || searchResults.results.length === 0) { if (!searchResults.results || searchResults.results.length === 0) {
console.warn(`[BookDate] No Audible search results for "${title}" by ${author}`); logger.warn(`No Audible search results for "${title}" by ${author}`);
return null; return null;
} }
// Take the first result (best match) // Take the first result (best match)
const firstResult = searchResults.results[0]; const firstResult = searchResults.results[0];
console.log(`[BookDate] Found on Audible: "${firstResult.title}" (ASIN: ${firstResult.asin})`); logger.info(`Found on Audible: "${firstResult.title}" (ASIN: ${firstResult.asin})`);
// Step 3: Use ASIN to fetch full details from Audnexus (or Audible as fallback) // Step 3: Use ASIN to fetch full details from Audnexus (or Audible as fallback)
const details = await audibleService.getAudiobookDetails(firstResult.asin); const details = await audibleService.getAudiobookDetails(firstResult.asin);
if (!details) { if (!details) {
console.warn(`[BookDate] Could not fetch details for ASIN ${firstResult.asin}`); logger.warn(`Could not fetch details for ASIN ${firstResult.asin}`);
return null; return null;
} }
console.log(`[BookDate] Successfully matched "${title}" to ASIN ${details.asin}`); logger.info(`Successfully matched "${title}" to ASIN ${details.asin}`);
return { return {
asin: details.asin, asin: details.asin,
@@ -673,7 +677,7 @@ export async function matchToAudnexus(
}; };
} catch (error) { } catch (error) {
console.error(`[BookDate] Audnexus matching error for "${title}":`, error); logger.error(`Audnexus matching error for "${title}"`, { error: error instanceof Error ? error.message : String(error) });
return null; return null;
} }
} }
@@ -703,12 +707,12 @@ export async function isInLibrary(
}); });
if (match) { if (match) {
console.log(`[BookDate] Book "${title}" by ${author} found in library (matched to: "${match.title}")`); logger.info(`Book "${title}" by ${author} found in library (matched to: "${match.title}")`);
} }
return !!match; return !!match;
} catch (error) { } catch (error) {
console.error(`[BookDate] Error checking library for "${title}":`, error); logger.error(`Error checking library for "${title}"`, { error: error instanceof Error ? error.message : String(error) });
return false; return false;
} }
} }
+47 -43
View File
@@ -5,6 +5,10 @@
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { RMABLogger } from '../utils/logger';
// Module-level logger
const logger = RMABLogger.create('Audible');
export interface AudibleAudiobook { export interface AudibleAudiobook {
asin: string; asin: string;
@@ -48,14 +52,14 @@ export class AudibleService {
*/ */
async getPopularAudiobooks(limit: number = 20): Promise<AudibleAudiobook[]> { async getPopularAudiobooks(limit: number = 20): Promise<AudibleAudiobook[]> {
try { try {
console.log(`[Audible] Fetching popular audiobooks (limit: ${limit})...`); logger.info(` Fetching popular audiobooks (limit: ${limit})...`);
const audiobooks: AudibleAudiobook[] = []; const audiobooks: AudibleAudiobook[] = [];
let page = 1; let page = 1;
const maxPages = Math.ceil(limit / 20); // Audible shows ~20 items per page const maxPages = Math.ceil(limit / 20); // Audible shows ~20 items per page
while (audiobooks.length < limit && page <= maxPages) { while (audiobooks.length < limit && page <= maxPages) {
console.log(`[Audible] Fetching page ${page}/${maxPages}...`); logger.info(` Fetching page ${page}/${maxPages}...`);
const response = await this.client.get('/adblbestsellers', { const response = await this.client.get('/adblbestsellers', {
params: page > 1 ? { page } : {}, params: page > 1 ? { page } : {},
@@ -105,11 +109,11 @@ export class AudibleService {
foundOnPage++; foundOnPage++;
}); });
console.log(`[Audible] Found ${foundOnPage} audiobooks on page ${page}`); logger.info(` Found ${foundOnPage} audiobooks on page ${page}`);
// If we got fewer than expected, probably no more pages // If we got fewer than expected, probably no more pages
if (foundOnPage < 10) { if (foundOnPage < 10) {
console.log(`[Audible] Reached end of available pages`); logger.info(` Reached end of available pages`);
break; break;
} }
@@ -121,10 +125,10 @@ export class AudibleService {
} }
} }
console.log(`[Audible] Found ${audiobooks.length} popular audiobooks across ${page} pages`); logger.info(` Found ${audiobooks.length} popular audiobooks across ${page} pages`);
return audiobooks; return audiobooks;
} catch (error) { } catch (error) {
console.error('[Audible] Failed to fetch popular audiobooks:', error); logger.error('Failed to fetch popular audiobooks', { error: error instanceof Error ? error.message : String(error) });
return []; return [];
} }
} }
@@ -134,14 +138,14 @@ export class AudibleService {
*/ */
async getNewReleases(limit: number = 20): Promise<AudibleAudiobook[]> { async getNewReleases(limit: number = 20): Promise<AudibleAudiobook[]> {
try { try {
console.log(`[Audible] Fetching new releases (limit: ${limit})...`); logger.info(` Fetching new releases (limit: ${limit})...`);
const audiobooks: AudibleAudiobook[] = []; const audiobooks: AudibleAudiobook[] = [];
let page = 1; let page = 1;
const maxPages = Math.ceil(limit / 20); // Audible shows ~20 items per page const maxPages = Math.ceil(limit / 20); // Audible shows ~20 items per page
while (audiobooks.length < limit && page <= maxPages) { while (audiobooks.length < limit && page <= maxPages) {
console.log(`[Audible] Fetching page ${page}/${maxPages}...`); logger.info(` Fetching page ${page}/${maxPages}...`);
const response = await this.client.get('/newreleases', { const response = await this.client.get('/newreleases', {
params: page > 1 ? { page } : {}, params: page > 1 ? { page } : {},
@@ -190,11 +194,11 @@ export class AudibleService {
foundOnPage++; foundOnPage++;
}); });
console.log(`[Audible] Found ${foundOnPage} audiobooks on page ${page}`); logger.info(` Found ${foundOnPage} audiobooks on page ${page}`);
// If we got fewer than expected, probably no more pages // If we got fewer than expected, probably no more pages
if (foundOnPage < 10) { if (foundOnPage < 10) {
console.log(`[Audible] Reached end of available pages`); logger.info(` Reached end of available pages`);
break; break;
} }
@@ -206,10 +210,10 @@ export class AudibleService {
} }
} }
console.log(`[Audible] Found ${audiobooks.length} new releases across ${page} pages`); logger.info(` Found ${audiobooks.length} new releases across ${page} pages`);
return audiobooks; return audiobooks;
} catch (error) { } catch (error) {
console.error('[Audible] Failed to fetch new releases:', error); logger.error('Failed to fetch new releases', { error: error instanceof Error ? error.message : String(error) });
return []; return [];
} }
} }
@@ -219,7 +223,7 @@ export class AudibleService {
*/ */
async search(query: string, page: number = 1): Promise<AudibleSearchResult> { async search(query: string, page: number = 1): Promise<AudibleSearchResult> {
try { try {
console.log(`[Audible] Searching for "${query}"...`); logger.info(` Searching for "${query}"...`);
const response = await this.client.get('/search', { const response = await this.client.get('/search', {
params: { params: {
@@ -285,7 +289,7 @@ export class AudibleService {
const resultsText = $('.resultsInfo').text().trim(); const resultsText = $('.resultsInfo').text().trim();
const totalResults = parseInt(resultsText.match(/of ([\d,]+)/)?.[1]?.replace(/,/g, '') || '0'); const totalResults = parseInt(resultsText.match(/of ([\d,]+)/)?.[1]?.replace(/,/g, '') || '0');
console.log(`[Audible] Found ${audiobooks.length} results for "${query}"`); logger.info(` Found ${audiobooks.length} results for "${query}"`);
return { return {
query, query,
@@ -295,7 +299,7 @@ export class AudibleService {
hasMore: audiobooks.length > 0 && totalResults > page * 20, hasMore: audiobooks.length > 0 && totalResults > page * 20,
}; };
} catch (error) { } catch (error) {
console.error('[Audible] Search failed:', error); logger.error('Search failed', { error: error instanceof Error ? error.message : String(error) });
return { return {
query, query,
results: [], results: [],
@@ -313,21 +317,21 @@ export class AudibleService {
*/ */
async getAudiobookDetails(asin: string): Promise<AudibleAudiobook | null> { async getAudiobookDetails(asin: string): Promise<AudibleAudiobook | null> {
try { try {
console.log(`[Audible] Fetching details for ASIN ${asin}...`); logger.info(` Fetching details for ASIN ${asin}...`);
// Try Audnexus first (more reliable) // Try Audnexus first (more reliable)
const audnexusData = await this.fetchFromAudnexus(asin); const audnexusData = await this.fetchFromAudnexus(asin);
if (audnexusData) { if (audnexusData) {
console.log(`[Audible] Successfully fetched from Audnexus for "${audnexusData.title}"`); logger.info(` Successfully fetched from Audnexus for "${audnexusData.title}"`);
return audnexusData; return audnexusData;
} }
console.log(`[Audible] Audnexus failed, falling back to Audible scraping...`); logger.info(` Audnexus failed, falling back to Audible scraping...`);
// Fallback to Audible scraping // Fallback to Audible scraping
return await this.scrapeAudibleDetails(asin); return await this.scrapeAudibleDetails(asin);
} catch (error) { } catch (error) {
console.error(`[Audible] Failed to fetch details for ${asin}:`, error); logger.error(`Failed to fetch details for ${asin}`, { error: error instanceof Error ? error.message : String(error) });
return null; return null;
} }
} }
@@ -337,7 +341,7 @@ export class AudibleService {
*/ */
private async fetchFromAudnexus(asin: string): Promise<AudibleAudiobook | null> { private async fetchFromAudnexus(asin: string): Promise<AudibleAudiobook | null> {
try { try {
console.log(`[Audnexus] Fetching ASIN ${asin}...`); logger.debug(`Fetching ASIN from Audnexus: ${asin}`);
const response = await axios.get(`https://api.audnex.us/books/${asin}`, { const response = await axios.get(`https://api.audnex.us/books/${asin}`, {
timeout: 10000, timeout: 10000,
@@ -367,22 +371,22 @@ export class AudibleService {
result.coverArtUrl = result.coverArtUrl.replace(/\._.*_\./, '._SL500_.'); result.coverArtUrl = result.coverArtUrl.replace(/\._.*_\./, '._SL500_.');
} }
console.log(`[Audnexus] Success:`, JSON.stringify({ logger.debug('Audnexus success', {
title: result.title, title: result.title,
author: result.author, author: result.author,
narrator: result.narrator, narrator: result.narrator,
descLength: result.description?.length || 0, descLength: result.description?.length || 0,
duration: result.durationMinutes, duration: result.durationMinutes,
rating: result.rating, rating: result.rating,
genres: result.genres?.length || 0 genreCount: result.genres?.length || 0
})); });
return result; return result;
} catch (error: any) { } catch (error: any) {
if (error.response?.status === 404) { if (error.response?.status === 404) {
console.log(`[Audnexus] Book not found (404) for ASIN ${asin}`); logger.debug(`Book not found (404) on Audnexus for ASIN ${asin}`);
} else { } else {
console.log(`[Audnexus] Error fetching ASIN ${asin}:`, error.message); logger.warn(`Error fetching from Audnexus for ASIN ${asin}`, { error: error.message });
} }
return null; return null;
} }
@@ -413,20 +417,20 @@ export class AudibleService {
const path = require('path'); const path = require('path');
const debugPath = path.join('/tmp', `audible-${asin}.html`); const debugPath = path.join('/tmp', `audible-${asin}.html`);
fs.writeFileSync(debugPath, response.data); fs.writeFileSync(debugPath, response.data);
console.log(`[Audible] Saved HTML to ${debugPath} for debugging`); logger.info(` Saved HTML to ${debugPath} for debugging`);
} }
// Try to extract JSON-LD structured data first // Try to extract JSON-LD structured data first
const jsonLdScripts = $('script[type="application/ld+json"]'); const jsonLdScripts = $('script[type="application/ld+json"]');
console.log(`[Audible] Found ${jsonLdScripts.length} JSON-LD script tags`); logger.info(` Found ${jsonLdScripts.length} JSON-LD script tags`);
jsonLdScripts.each((i, elem) => { jsonLdScripts.each((i, elem) => {
try { try {
const jsonData = JSON.parse($(elem).html() || '{}'); const jsonData = JSON.parse($(elem).html() || '{}');
console.log(`[Audible] JSON-LD ${i} type:`, jsonData['@type']); logger.info(` JSON-LD ${i} type:`, jsonData['@type']);
if (jsonData['@type'] === 'Book' || jsonData['@type'] === 'Audiobook' || jsonData['@type'] === 'Product') { if (jsonData['@type'] === 'Book' || jsonData['@type'] === 'Audiobook' || jsonData['@type'] === 'Product') {
console.log('[Audible] Found valid JSON-LD structured data'); logger.debug('Found valid JSON-LD structured data');
if (jsonData.name) result.title = jsonData.name; if (jsonData.name) result.title = jsonData.name;
@@ -455,7 +459,7 @@ export class AudibleService {
} }
} }
} catch (e) { } catch (e) {
console.log(`[Audible] JSON-LD ${i} parsing failed:`, e); logger.debug(`JSON-LD ${i} parsing failed`, { error: e instanceof Error ? e.message : String(e) });
} }
}); });
@@ -466,7 +470,7 @@ export class AudibleService {
$('h1[class*="heading"]').first().text().trim() || $('h1[class*="heading"]').first().text().trim() ||
$('.bc-container h1').first().text().trim() || $('.bc-container h1').first().text().trim() ||
$('h1').first().text().trim(); $('h1').first().text().trim();
console.log(`[Audible] Title from HTML: "${result.title}"`); logger.info(` Title from HTML: "${result.title}"`);
} }
// Author - try multiple approaches (only in product details area) // Author - try multiple approaches (only in product details area)
@@ -502,7 +506,7 @@ export class AudibleService {
} }
result.author = result.author.replace(/^By:\s*/i, '').replace(/^Written by:\s*/i, '').trim(); result.author = result.author.replace(/^By:\s*/i, '').replace(/^Written by:\s*/i, '').trim();
console.log(`[Audible] Author from HTML: "${result.author}"`); logger.info(` Author from HTML: "${result.author}"`);
} }
// Narrator - try multiple approaches (only in product details area) // Narrator - try multiple approaches (only in product details area)
@@ -538,7 +542,7 @@ export class AudibleService {
if (result.narrator) { if (result.narrator) {
result.narrator = result.narrator.replace(/^Narrated by:\s*/i, '').trim(); result.narrator = result.narrator.replace(/^Narrated by:\s*/i, '').trim();
} }
console.log(`[Audible] Narrator from HTML: "${result.narrator || ''}"`); logger.info(` Narrator from HTML: "${result.narrator || ''}"`);
} }
// Description - try multiple approaches with strict filtering // Description - try multiple approaches with strict filtering
@@ -588,7 +592,7 @@ export class AudibleService {
}); });
} }
console.log(`[Audible] Description length: ${result.description?.length || 0} chars`); logger.info(` Description length: ${result.description?.length || 0} chars`);
} }
// Cover art - try multiple selectors // Cover art - try multiple selectors
@@ -627,7 +631,7 @@ export class AudibleService {
})(); })();
result.durationMinutes = this.parseRuntime(runtimeText); result.durationMinutes = this.parseRuntime(runtimeText);
console.log(`[Audible] Duration from "${runtimeText}": ${result.durationMinutes} minutes`); logger.info(` Duration from "${runtimeText}": ${result.durationMinutes} minutes`);
} }
// Rating - try multiple approaches // Rating - try multiple approaches
@@ -653,7 +657,7 @@ export class AudibleService {
const ratingMatch = ratingText.match(/(\d+\.?\d*)\s*out of/i); const ratingMatch = ratingText.match(/(\d+\.?\d*)\s*out of/i);
result.rating = ratingMatch ? parseFloat(ratingMatch[1]) : undefined; result.rating = ratingMatch ? parseFloat(ratingMatch[1]) : undefined;
} }
console.log(`[Audible] Rating from "${ratingText}": ${result.rating}`); logger.info(` Rating from "${ratingText}": ${result.rating}`);
} }
// Release date - try multiple selectors // Release date - try multiple selectors
@@ -668,7 +672,7 @@ export class AudibleService {
if (dateMatch) { if (dateMatch) {
result.releaseDate = dateMatch[1].trim(); result.releaseDate = dateMatch[1].trim();
} }
console.log(`[Audible] Release date from "${releaseDateText}": ${result.releaseDate}`); logger.info(` Release date from "${releaseDateText}": ${result.releaseDate}`);
} }
// Genres - try to extract categories // Genres - try to extract categories
@@ -681,23 +685,23 @@ export class AudibleService {
}); });
if (genres.length > 0) { if (genres.length > 0) {
result.genres = genres.slice(0, 5); // Limit to 5 genres result.genres = genres.slice(0, 5); // Limit to 5 genres
console.log(`[Audible] Genres: ${result.genres.join(', ')}`); logger.info(` Genres: ${result.genres.join(', ')}`);
} }
console.log(`[Audible] Successfully fetched details for "${result.title}"`); logger.info(`Successfully fetched details for "${result.title}"`);
console.log(`[Audible] Final result:`, JSON.stringify({ logger.debug('Final result', {
title: result.title, title: result.title,
author: result.author, author: result.author,
narrator: result.narrator, narrator: result.narrator,
descLength: result.description?.length || 0, descLength: result.description?.length || 0,
duration: result.durationMinutes, duration: result.durationMinutes,
rating: result.rating, rating: result.rating,
genres: result.genres?.length || 0 genreCount: result.genres?.length || 0
})); });
return result; return result;
} catch (error) { } catch (error) {
console.error(`[Audible] Failed to fetch details for ${asin}:`, error); logger.error(`Failed to fetch details for ${asin}`, { error: error instanceof Error ? error.message : String(error) });
return null; return null;
} }
} }
+91 -91
View File
@@ -5,6 +5,10 @@
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import { parseStringPromise } from 'xml2js'; import { parseStringPromise } from 'xml2js';
import { RMABLogger } from '../utils/logger';
// Module-level logger
const logger = RMABLogger.create('Plex');
const PLEX_TV_API_BASE = 'https://plex.tv/api/v2'; const PLEX_TV_API_BASE = 'https://plex.tv/api/v2';
const PLEX_CLIENT_IDENTIFIER = process.env.PLEX_CLIENT_IDENTIFIER || 'readmeabook-unique-client-id'; const PLEX_CLIENT_IDENTIFIER = process.env.PLEX_CLIENT_IDENTIFIER || 'readmeabook-unique-client-id';
@@ -106,7 +110,7 @@ export class PlexService {
code: response.data.code, code: response.data.code,
}; };
} catch (error) { } catch (error) {
console.error('Failed to request Plex PIN:', error); logger.error('Failed to request PIN', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to request authentication PIN from Plex'); throw new Error('Failed to request authentication PIN from Plex');
} }
} }
@@ -125,7 +129,7 @@ export class PlexService {
return response.data.authToken || null; return response.data.authToken || null;
} catch (error) { } catch (error) {
console.error('Failed to check Plex PIN:', error); logger.error('Failed to check PIN', { error: error instanceof Error ? error.message : String(error) });
return null; return null;
} }
} }
@@ -147,36 +151,36 @@ export class PlexService {
// Handle different response formats from Plex // Handle different response formats from Plex
if (typeof response.data === 'string') { if (typeof response.data === 'string') {
// XML response - parse it // XML response - parse it
console.log('[Plex] Received XML response, parsing...'); logger.debug('Received XML response, parsing...');
const parsed = await parseStringPromise(response.data); const parsed = await parseStringPromise(response.data);
// XML attributes are in user.$ // XML attributes are in user.$
if (parsed.user && parsed.user.$) { if (parsed.user && parsed.user.$) {
userData = parsed.user.$; userData = parsed.user.$;
} else { } else {
console.error('[Plex] Unexpected XML structure:', parsed); logger.error('Unexpected XML structure', { parsed });
throw new Error('Unexpected XML structure in Plex response'); throw new Error('Unexpected XML structure in Plex response');
} }
} else if (response.data && typeof response.data === 'object') { } else if (response.data && typeof response.data === 'object') {
// JSON response // JSON response
console.log('[Plex] Received JSON response'); logger.debug('Received JSON response');
userData = response.data; userData = response.data;
} else { } else {
console.error('[Plex] Unexpected response type:', typeof response.data); logger.error('Unexpected response type', { type: typeof response.data });
throw new Error('Unexpected response format from Plex'); throw new Error('Unexpected response format from Plex');
} }
console.log('[Plex] Parsed user data:', JSON.stringify(userData, null, 2)); logger.debug('Parsed user data', { userData });
// Validate required fields // Validate required fields
if (!userData.id) { if (!userData.id) {
console.error('[Plex] User ID missing from parsed data:', userData); logger.error('User ID missing from parsed data', { userData });
throw new Error('User ID missing from Plex response'); throw new Error('User ID missing from Plex response');
} }
const username = userData.username || userData.title; const username = userData.username || userData.title;
if (!username) { if (!username) {
console.error('[Plex] Username missing from parsed data:', userData); logger.error('Username missing from parsed data', { userData });
throw new Error('Username missing from Plex response'); throw new Error('Username missing from Plex response');
} }
@@ -188,7 +192,7 @@ export class PlexService {
authToken, authToken,
}; };
} catch (error) { } catch (error) {
console.error('Failed to get Plex user info:', error); logger.error('Failed to get user info', { error: error instanceof Error ? error.message : String(error) });
if (error instanceof Error) { if (error instanceof Error) {
throw error; // Re-throw our custom errors throw error; // Re-throw our custom errors
} }
@@ -237,7 +241,7 @@ export class PlexService {
// else data is already the right format // else data is already the right format
} }
console.log('[Plex] Identity response:', JSON.stringify(data, null, 2)); logger.debug('Identity response', { data });
const info: PlexServerInfo = { const info: PlexServerInfo = {
machineIdentifier: data.machineIdentifier || 'unknown', machineIdentifier: data.machineIdentifier || 'unknown',
@@ -252,7 +256,7 @@ export class PlexService {
info, info,
}; };
} catch (error) { } catch (error) {
console.error('Plex connection test failed:', error); logger.error('Connection test failed', { error: error instanceof Error ? error.message : String(error) });
return { return {
success: false, success: false,
message: 'Could not connect to Plex server. Check server URL and token.', message: 'Could not connect to Plex server. Check server URL and token.',
@@ -275,7 +279,7 @@ export class PlexService {
userPlexToken: string userPlexToken: string
): Promise<string | null> { ): Promise<string | null> {
try { try {
console.log('[Plex] Fetching server access token for machineId:', serverMachineId); logger.debug('Fetching server access token', { serverMachineId });
// Get the list of servers/resources the user has access to // Get the list of servers/resources the user has access to
const response = await this.client.get('https://plex.tv/api/v2/resources', { const response = await this.client.get('https://plex.tv/api/v2/resources', {
@@ -300,20 +304,20 @@ export class PlexService {
}); });
if (!serverResource) { if (!serverResource) {
console.warn('[Plex] User does not have access to server:', serverMachineId); logger.warn('User does not have access to server', { serverMachineId });
return null; return null;
} }
if (!serverResource.accessToken) { if (!serverResource.accessToken) {
console.error('[Plex] Server resource found but no accessToken provided'); logger.error('Server resource found but no accessToken provided');
return null; return null;
} }
console.log('[Plex] Found server access token for:', serverResource.name); logger.debug('Found server access token', { serverName: serverResource.name });
return serverResource.accessToken; return serverResource.accessToken;
} catch (error) { } catch (error) {
console.error('[Plex] Failed to fetch server access token:', error); logger.error('Failed to fetch server access token', { error: error instanceof Error ? error.message : String(error) });
return null; return null;
} }
} }
@@ -327,7 +331,7 @@ export class PlexService {
*/ */
async verifyServerAccess(serverUrl: string, serverMachineId: string, userToken: string): Promise<boolean> { async verifyServerAccess(serverUrl: string, serverMachineId: string, userToken: string): Promise<boolean> {
try { try {
console.log('[Plex] Verifying server access for machineId:', serverMachineId); logger.debug('Verifying server access', { serverMachineId });
// Get the list of servers/resources the user has access to // Get the list of servers/resources the user has access to
const response = await this.client.get('https://plex.tv/api/v2/resources', { const response = await this.client.get('https://plex.tv/api/v2/resources', {
@@ -344,21 +348,19 @@ export class PlexService {
}); });
const resources = response.data || []; const resources = response.data || [];
console.log('[Plex] User has access to', resources.length, 'resources'); logger.debug('User has access to resources', { count: resources.length });
// Log all resources for debugging // Log all resources for debugging
console.log('[Plex] User accessible resources:', JSON.stringify( logger.debug('User accessible resources', {
resources.map((r: any) => ({ resources: resources.map((r: any) => ({
name: r.name, name: r.name,
product: r.product, product: r.product,
provides: r.provides, provides: r.provides,
clientIdentifier: r.clientIdentifier, clientIdentifier: r.clientIdentifier,
machineIdentifier: r.machineIdentifier, machineIdentifier: r.machineIdentifier,
owned: r.owned, owned: r.owned,
})), }))
null, });
2
));
// Filter to only server resources (not clients like apps) // Filter to only server resources (not clients like apps)
const servers = resources.filter((r: any) => const servers = resources.filter((r: any) =>
@@ -367,14 +369,14 @@ export class PlexService {
(r.provides && r.provides.includes && r.provides.includes('server')) (r.provides && r.provides.includes && r.provides.includes('server'))
); );
console.log('[Plex] Found', servers.length, 'server resources'); logger.debug('Found server resources', { count: servers.length });
// Check if our server is in the list of accessible resources // Check if our server is in the list of accessible resources
const hasAccess = servers.some((resource: any) => { const hasAccess = servers.some((resource: any) => {
const resourceId = resource.clientIdentifier || resource.machineIdentifier; const resourceId = resource.clientIdentifier || resource.machineIdentifier;
const match = resourceId === serverMachineId; const match = resourceId === serverMachineId;
console.log('[Plex] Comparing:', { logger.debug('Comparing resource', {
resourceId, resourceId,
serverMachineId, serverMachineId,
match, match,
@@ -382,7 +384,7 @@ export class PlexService {
}); });
if (match) { if (match) {
console.log('[Plex] ✓ Found matching server:', { logger.debug('Found matching server', {
name: resource.name, name: resource.name,
machineId: resourceId, machineId: resourceId,
owned: resource.owned, owned: resource.owned,
@@ -393,23 +395,23 @@ export class PlexService {
}); });
if (!hasAccess) { if (!hasAccess) {
console.warn('[Plex] ✗ Server not found in user\'s accessible resources'); logger.warn('Server not found in user accessible resources', {
console.warn('[Plex] Looking for machineId:', serverMachineId); serverMachineId,
console.warn('[Plex] User has access to servers:', accessibleServers: servers.map((r: any) => ({
servers.map((r: any) => ({
name: r.name, name: r.name,
clientId: r.clientIdentifier, clientId: r.clientIdentifier,
machineId: r.machineIdentifier, machineId: r.machineIdentifier,
})) }))
); });
} }
return hasAccess; return hasAccess;
} catch (error: any) { } catch (error: any) {
console.error('[Plex] Failed to verify server access:', error.response?.status || error.message); logger.error('Failed to verify server access', {
if (error.response?.data) { status: error.response?.status,
console.error('[Plex] Error response:', error.response.data); error: error.message,
} responseData: error.response?.data
});
return false; return false;
} }
} }
@@ -456,7 +458,7 @@ export class PlexService {
return libraries; return libraries;
} catch (error) { } catch (error) {
console.error('Failed to get Plex libraries:', error); logger.error('Failed to get libraries', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to retrieve libraries from Plex server'); throw new Error('Failed to retrieve libraries from Plex server');
} }
} }
@@ -488,27 +490,27 @@ export class PlexService {
} }
); );
console.log('[Plex] Recently added response type:', typeof response.data); logger.debug('Recently added response type', { type: typeof response.data });
// Handle XML response // Handle XML response
let data = response.data; let data = response.data;
if (typeof data === 'string') { if (typeof data === 'string') {
console.log('[Plex] Parsing XML response...'); logger.debug('Parsing XML response...');
const parsed = await parseStringPromise(data); const parsed = await parseStringPromise(data);
data = parsed.MediaContainer; data = parsed.MediaContainer;
} else if (data && typeof data === 'object') { } else if (data && typeof data === 'object') {
// JSON response - could be wrapped in MediaContainer // JSON response - could be wrapped in MediaContainer
if (data.MediaContainer) { if (data.MediaContainer) {
console.log('[Plex] Extracting from MediaContainer wrapper'); logger.debug('Extracting from MediaContainer wrapper');
data = data.MediaContainer; data = data.MediaContainer;
} }
} }
const tracks = data.Metadata || data.Track || data.Directory || data.Album || []; const tracks = data.Metadata || data.Track || data.Directory || data.Album || [];
console.log('[Plex] Found', Array.isArray(tracks) ? tracks.length : '(not an array)', 'recently added items'); logger.debug('Found recently added items', { count: Array.isArray(tracks) ? tracks.length : 'not an array' });
if (!Array.isArray(tracks)) { if (!Array.isArray(tracks)) {
console.warn('[Plex] tracks is not an array:', tracks); logger.warn('tracks is not an array', { tracks });
return []; return [];
} }
@@ -527,7 +529,7 @@ export class PlexService {
userRating: item.userRating ? parseFloat(item.userRating) : (item.$?.userRating ? parseFloat(item.$?.userRating) : undefined), userRating: item.userRating ? parseFloat(item.userRating) : (item.$?.userRating ? parseFloat(item.$?.userRating) : undefined),
})); }));
} catch (error) { } catch (error) {
console.error('Failed to get recently added content:', error); logger.error('Failed to get recently added content', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to retrieve recently added content from Plex library'); throw new Error('Failed to retrieve recently added content from Plex library');
} }
} }
@@ -554,30 +556,29 @@ export class PlexService {
} }
); );
console.log('[Plex] Library content response type:', typeof response.data); logger.debug('Library content response type', { type: typeof response.data });
// Handle XML response // Handle XML response
let data = response.data; let data = response.data;
if (typeof data === 'string') { if (typeof data === 'string') {
console.log('[Plex] Parsing XML response...'); logger.debug('Parsing XML response...');
const parsed = await parseStringPromise(data); const parsed = await parseStringPromise(data);
data = parsed.MediaContainer; data = parsed.MediaContainer;
} else if (data && typeof data === 'object') { } else if (data && typeof data === 'object') {
// JSON response - could be wrapped in MediaContainer // JSON response - could be wrapped in MediaContainer
if (data.MediaContainer) { if (data.MediaContainer) {
console.log('[Plex] Extracting from MediaContainer wrapper'); logger.debug('Extracting from MediaContainer wrapper');
data = data.MediaContainer; data = data.MediaContainer;
} }
} }
console.log('[Plex] Data structure keys:', Object.keys(data || {})); logger.debug('Data structure', { keys: Object.keys(data || {}) });
console.log('[Plex] Looking for content in: Metadata, Track, Directory, Album');
const tracks = data.Metadata || data.Track || data.Directory || data.Album || []; const tracks = data.Metadata || data.Track || data.Directory || data.Album || [];
console.log('[Plex] Found', Array.isArray(tracks) ? tracks.length : '(not an array)', 'items'); logger.debug('Found items', { count: Array.isArray(tracks) ? tracks.length : 'not an array' });
if (!Array.isArray(tracks)) { if (!Array.isArray(tracks)) {
console.warn('[Plex] tracks is not an array:', tracks); logger.warn('tracks is not an array', { tracks });
return []; return [];
} }
@@ -597,9 +598,9 @@ export class PlexService {
})); }));
} catch (error: any) { } catch (error: any) {
if (error?.response?.status === 401) { if (error?.response?.status === 401) {
console.error('[Plex] 401 Unauthorized when fetching library content - token may not have server access permissions'); logger.error('401 Unauthorized when fetching library content - token may not have server access permissions');
} else { } else {
console.error('[Plex] Failed to get library content:', error); logger.error('Failed to get library content', { error: error instanceof Error ? error.message : String(error) });
} }
throw new Error('Failed to retrieve content from Plex library'); throw new Error('Failed to retrieve content from Plex library');
} }
@@ -616,9 +617,9 @@ export class PlexService {
}, },
}); });
console.log(`Triggered Plex library scan for library ${libraryId}`); logger.info(`Triggered library scan for library ${libraryId}`);
} catch (error) { } catch (error) {
console.error('Failed to trigger Plex scan:', error); logger.error('Failed to trigger scan', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to trigger Plex library scan'); throw new Error('Failed to trigger Plex library scan');
} }
} }
@@ -665,7 +666,7 @@ export class PlexService {
updatedAt: item.updatedAt ? parseInt(item.updatedAt) : Date.now(), updatedAt: item.updatedAt ? parseInt(item.updatedAt) : Date.now(),
})); }));
} catch (error) { } catch (error) {
console.error('Failed to search Plex library:', error); logger.error('Failed to search library', { error: error instanceof Error ? error.message : String(error) });
return []; return [];
} }
} }
@@ -717,15 +718,15 @@ export class PlexService {
} catch (error: any) { } catch (error: any) {
// Handle 401 specifically (expired or invalid token) // Handle 401 specifically (expired or invalid token)
if (error.response?.status === 401) { if (error.response?.status === 401) {
console.warn(`[Plex] User token unauthorized for ratingKey ${ratingKey} (token may be expired or invalid)`); logger.warn('User token unauthorized', { ratingKey, reason: 'token may be expired or invalid' });
return null; return null;
} }
// Handle 404 (item not found or user doesn't have access) // Handle 404 (item not found or user doesn't have access)
if (error.response?.status === 404) { if (error.response?.status === 404) {
console.warn(`[Plex] Item not found or no access: ratingKey ${ratingKey}`); logger.warn('Item not found or no access', { ratingKey });
return null; return null;
} }
console.error(`[Plex] Failed to get metadata for ratingKey ${ratingKey}:`, error.message || error); logger.error('Failed to get metadata', { ratingKey, error: error.message || String(error) });
return null; return null;
} }
} }
@@ -765,9 +766,9 @@ export class PlexService {
// If we got many 401s, log a warning about token issues // If we got many 401s, log a warning about token issues
if (unauthorizedCount > 0) { if (unauthorizedCount > 0) {
console.warn(`[Plex] ${unauthorizedCount} of ${ratingKeys.length} items returned 401 (user token may be expired or invalid)`); logger.warn('Some rating requests failed with 401', { unauthorizedCount, totalCount: ratingKeys.length });
if (unauthorizedCount === ratingKeys.length) { if (unauthorizedCount === ratingKeys.length) {
console.error('[Plex] All rating requests failed with 401 - user needs to re-authenticate with Plex'); logger.error('All rating requests failed with 401 - user needs to re-authenticate');
} }
} }
@@ -780,7 +781,7 @@ export class PlexService {
*/ */
async getHomeUsers(authToken: string): Promise<PlexHomeUser[]> { async getHomeUsers(authToken: string): Promise<PlexHomeUser[]> {
try { try {
console.log('[Plex] Fetching home users from plex.tv/api/home/users'); logger.debug('Fetching home users');
const response = await this.client.get( const response = await this.client.get(
'https://plex.tv/api/home/users', 'https://plex.tv/api/home/users',
{ {
@@ -792,36 +793,36 @@ export class PlexService {
} }
); );
console.log('[Plex] Home users API response status:', response.status); logger.debug('Home users API response', { status: response.status, type: typeof response.data });
console.log('[Plex] Home users API response type:', typeof response.data);
// Handle XML response // Handle XML response
let data = response.data; let data = response.data;
if (typeof data === 'string') { if (typeof data === 'string') {
console.log('[Plex] Response is XML string, parsing...'); logger.debug('Response is XML string, parsing...');
const parsed = await parseStringPromise(data); const parsed = await parseStringPromise(data);
data = parsed; data = parsed;
console.log('[Plex] Parsed XML structure:', JSON.stringify(data, null, 2)); logger.debug('Parsed XML structure', { data });
} else { } else {
console.log('[Plex] Response is JSON, structure:', JSON.stringify(data, null, 2)); logger.debug('Response is JSON', { data });
} }
// Extract users from response // Extract users from response
// Response structure: { home: { users: [{ user: {...} }] } } or similar // Response structure: { home: { users: [{ user: {...} }] } } or similar
const users: any[] = []; const users: any[] = [];
console.log('[Plex] Checking for users in response...'); logger.debug('Checking for users in response', {
console.log('[Plex] data.MediaContainer exists?', !!data.MediaContainer); hasMediaContainer: !!data.MediaContainer,
console.log('[Plex] data.MediaContainer?.User exists?', !!data.MediaContainer?.User); hasMediaContainerUser: !!data.MediaContainer?.User,
console.log('[Plex] data.home exists?', !!data.home); hasHome: !!data.home,
console.log('[Plex] data.home?.users exists?', !!data.home?.users); hasHomeUsers: !!data.home?.users,
console.log('[Plex] data.users exists?', !!data.users); hasUsers: !!data.users
});
// Check for users in MediaContainer.User (XML response structure) // Check for users in MediaContainer.User (XML response structure)
if (data.MediaContainer?.User) { if (data.MediaContainer?.User) {
console.log('[Plex] Found users in data.MediaContainer.User'); logger.debug('Found users in data.MediaContainer.User');
const usersList = Array.isArray(data.MediaContainer.User) ? data.MediaContainer.User : [data.MediaContainer.User]; const usersList = Array.isArray(data.MediaContainer.User) ? data.MediaContainer.User : [data.MediaContainer.User];
console.log('[Plex] usersList length:', usersList.length); logger.debug('usersList length', { count: usersList.length });
usersList.forEach((item: any) => { usersList.forEach((item: any) => {
// XML parsed data has attributes in the $ property // XML parsed data has attributes in the $ property
if (item.$) { if (item.$) {
@@ -831,9 +832,9 @@ export class PlexService {
} }
}); });
} else if (data.home?.users) { } else if (data.home?.users) {
console.log('[Plex] Found users in data.home.users'); logger.debug('Found users in data.home.users');
const usersList = Array.isArray(data.home.users) ? data.home.users : [data.home.users]; const usersList = Array.isArray(data.home.users) ? data.home.users : [data.home.users];
console.log('[Plex] usersList length:', usersList.length); logger.debug('usersList length', { count: usersList.length });
usersList.forEach((item: any) => { usersList.forEach((item: any) => {
if (item.user) { if (item.user) {
users.push(item.user); users.push(item.user);
@@ -844,9 +845,9 @@ export class PlexService {
} }
}); });
} else if (data.users) { } else if (data.users) {
console.log('[Plex] Found users in data.users'); logger.debug('Found users in data.users');
const usersList = Array.isArray(data.users) ? data.users : [data.users]; const usersList = Array.isArray(data.users) ? data.users : [data.users];
console.log('[Plex] usersList length:', usersList.length); logger.debug('usersList length', { count: usersList.length });
usersList.forEach((item: any) => { usersList.forEach((item: any) => {
if (item.user) { if (item.user) {
users.push(item.user); users.push(item.user);
@@ -857,14 +858,13 @@ export class PlexService {
} }
}); });
} else { } else {
console.log('[Plex] No users found in expected locations. Full data structure:'); logger.debug('No users found in expected locations', { data });
console.log(JSON.stringify(data, null, 2));
} }
console.log('[Plex] Extracted', users.length, 'users from response'); logger.debug('Extracted users from response', { count: users.length });
if (users.length === 0) { if (users.length === 0) {
console.warn('[Plex] No home users found - this account may not have a Plex Home setup'); logger.warn('No home users found - account may not have Plex Home setup');
return []; return [];
} }
@@ -898,11 +898,11 @@ export class PlexService {
}; };
}); });
} catch (error: any) { } catch (error: any) {
console.error('[Plex] Failed to get home users:', error.message || error); logger.error('Failed to get home users', {
if (error.response) { error: error.message || String(error),
console.error('[Plex] Error response status:', error.response.status); status: error.response?.status,
console.error('[Plex] Error response data:', error.response.data); responseData: error.response?.data
} });
// Return empty array if no home users (not an error condition) // Return empty array if no home users (not an error condition)
return []; return [];
} }
@@ -958,7 +958,7 @@ export class PlexService {
} }
if (!authenticationToken) { if (!authenticationToken) {
console.error('[Plex] No authenticationToken found in switch response:', JSON.stringify(data, null, 2)); logger.error('No authenticationToken found in switch response', { data });
return null; return null;
} }
@@ -966,10 +966,10 @@ export class PlexService {
} catch (error: any) { } catch (error: any) {
// Handle PIN errors specifically // Handle PIN errors specifically
if (error.response?.status === 401) { if (error.response?.status === 401) {
console.error('[Plex] Invalid PIN for profile'); logger.error('Invalid PIN for profile');
throw new Error('Invalid PIN'); throw new Error('Invalid PIN');
} }
console.error('[Plex] Failed to switch home user:', error); logger.error('Failed to switch home user', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to switch to selected profile'); throw new Error('Failed to switch to selected profile');
} }
} }
+36 -33
View File
@@ -6,6 +6,10 @@
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import { XMLParser } from 'fast-xml-parser'; import { XMLParser } from 'fast-xml-parser';
import { TorrentResult } from '../utils/ranking-algorithm'; import { TorrentResult } from '../utils/ranking-algorithm';
import { RMABLogger } from '../utils/logger';
// Module-level logger
const logger = RMABLogger.create('Prowlarr');
export interface SearchFilters { export interface SearchFilters {
category?: number; category?: number;
@@ -96,8 +100,7 @@ export class ProwlarrService {
// Debug interceptor to log actual outgoing requests // Debug interceptor to log actual outgoing requests
this.client.interceptors.request.use((config) => { this.client.interceptors.request.use((config) => {
console.log(`[Prowlarr] Actual request: ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`); logger.debug(`Actual request: ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`, { params: config.params });
console.log(`[Prowlarr] Request params:`, JSON.stringify(config.params));
return config; return config;
}); });
} }
@@ -130,12 +133,12 @@ export class ProwlarrService {
} }
const response = await this.client.get('/search', { params }); const response = await this.client.get('/search', { params });
console.log(`[Prowlarr] Raw API response: ${response.data.length} results`); logger.info(` Raw API response: ${response.data.length} results`);
// Debug: Log first raw result to see structure and protocol field // Debug: Log first raw result to see structure and protocol field
if (response.data.length > 0) { if (response.data.length > 0) {
const firstResult = response.data[0]; const firstResult = response.data[0];
console.log(`[Prowlarr] First raw result - protocol: "${firstResult.protocol}", indexer: "${firstResult.indexer}", title: "${firstResult.title?.substring(0, 50)}..."`); logger.info(` First raw result - protocol: "${firstResult.protocol}", indexer: "${firstResult.indexer}", title: "${firstResult.title?.substring(0, 50)}..."`);
// Check protocol distribution in raw results // Check protocol distribution in raw results
const rawProtocols = response.data.reduce((acc: Record<string, number>, r: any) => { const rawProtocols = response.data.reduce((acc: Record<string, number>, r: any) => {
@@ -143,21 +146,21 @@ export class ProwlarrService {
acc[proto] = (acc[proto] || 0) + 1; acc[proto] = (acc[proto] || 0) + 1;
return acc; return acc;
}, {}); }, {});
console.log(`[Prowlarr] Raw protocol distribution:`, JSON.stringify(rawProtocols)); logger.info(`Raw protocol distribution`, { protocols: rawProtocols });
} }
// Debug: Log first raw result full structure (debug mode only) // Debug: Log first raw result full structure (automatically filtered by LOG_LEVEL)
if (process.env.LOG_LEVEL === 'debug' && response.data.length > 0) { if (response.data.length > 0) {
console.log('[Prowlarr] Sample raw result from API:', JSON.stringify(response.data[0], null, 2)); logger.debug('Sample raw result from API', response.data[0]);
} }
// Transform Prowlarr results to our format // Transform Prowlarr results to our format
const results = response.data const results = response.data
.map((result: ProwlarrSearchResult, index: number) => { .map((result: ProwlarrSearchResult, index: number) => {
const transformed = this.transformResult(result); const transformed = this.transformResult(result);
if (!transformed && process.env.LOG_LEVEL === 'debug') { if (!transformed) {
// Log the full raw result that was skipped (debug mode only) // Log the full raw result that was skipped (automatically filtered by LOG_LEVEL)
console.log(`[Prowlarr] Result #${index + 1} was skipped. Raw data:`, JSON.stringify(result, null, 2)); logger.debug(`Result #${index + 1} was skipped`, { rawData: result });
} }
return transformed; return transformed;
}) })
@@ -181,11 +184,11 @@ export class ProwlarrService {
filtered = filtered.slice(0, filters.maxResults); filtered = filtered.slice(0, filters.maxResults);
} }
console.log(`Prowlarr search for "${query}" returned ${filtered.length} results`); logger.info(`Search for "${query}" returned ${filtered.length} results`);
return filtered; return filtered;
} catch (error) { } catch (error) {
console.error('Prowlarr search failed:', error); logger.error('Search failed', { error: error instanceof Error ? error.message : String(error) });
throw new Error( throw new Error(
`Failed to search Prowlarr: ${error instanceof Error ? error.message : 'Unknown error'}` `Failed to search Prowlarr: ${error instanceof Error ? error.message : 'Unknown error'}`
); );
@@ -200,7 +203,7 @@ export class ProwlarrService {
const response = await this.client.get('/indexer'); const response = await this.client.get('/indexer');
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Failed to get Prowlarr indexers:', error); logger.error('Failed to get indexers', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to get indexers from Prowlarr'); throw new Error('Failed to get indexers from Prowlarr');
} }
} }
@@ -213,7 +216,7 @@ export class ProwlarrService {
await this.client.get('/health'); await this.client.get('/health');
return true; return true;
} catch (error) { } catch (error) {
console.error('Prowlarr connection test failed:', error); logger.error('Connection test failed', { error: error instanceof Error ? error.message : String(error) });
return false; return false;
} }
} }
@@ -226,7 +229,7 @@ export class ProwlarrService {
const response = await this.client.get('/indexerstats'); const response = await this.client.get('/indexerstats');
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Failed to get Prowlarr stats:', error); logger.error('Failed to get stats', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to get indexer statistics'); throw new Error('Failed to get indexer statistics');
} }
} }
@@ -292,7 +295,7 @@ export class ProwlarrService {
// Skip torrents without a valid download URL // Skip torrents without a valid download URL
if (!downloadUrl || typeof downloadUrl !== 'string' || downloadUrl.trim() === '') { if (!downloadUrl || typeof downloadUrl !== 'string' || downloadUrl.trim() === '') {
console.warn(`[Prowlarr] Skipping torrent "${item.title || 'Unknown'}" - missing download URL`); logger.warn(` Skipping torrent "${item.title || 'Unknown'}" - missing download URL`);
continue; continue;
} }
@@ -315,16 +318,16 @@ export class ProwlarrService {
results.push(result); results.push(result);
} catch (error) { } catch (error) {
console.error('Failed to parse RSS item:', error); logger.error('Failed to parse RSS item', { error: error instanceof Error ? error.message : String(error) });
// Continue with other items // Continue with other items
} }
} }
console.log(`RSS feed for indexer ${indexerId} returned ${results.length} results`); logger.info(`RSS feed for indexer ${indexerId} returned ${results.length} results`);
return results; return results;
} catch (error) { } catch (error) {
console.error(`Failed to get RSS feed for indexer ${indexerId}:`, error); logger.error(`Failed to get RSS feed for indexer ${indexerId}`, { error: error instanceof Error ? error.message : String(error) });
throw new Error(`Failed to get RSS feed: ${error instanceof Error ? error.message : 'Unknown error'}`); throw new Error(`Failed to get RSS feed: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
} }
@@ -340,12 +343,12 @@ export class ProwlarrService {
const results = await this.getRssFeed(indexerId); const results = await this.getRssFeed(indexerId);
allResults.push(...results); allResults.push(...results);
} catch (error) { } catch (error) {
console.error(`Failed to get RSS feed for indexer ${indexerId}:`, error); logger.error(`Failed to get RSS feed for indexer ${indexerId}`, { error: error instanceof Error ? error.message : String(error) });
// Continue with other indexers even if one fails // Continue with other indexers even if one fails
} }
} }
console.log(`RSS feeds from ${indexerIds.length} indexers returned ${allResults.length} total results`); logger.info(`RSS feeds from ${indexerIds.length} indexers returned ${allResults.length} total results`);
return allResults; return allResults;
} }
@@ -368,33 +371,33 @@ export class ProwlarrService {
acc[proto] = (acc[proto] || 0) + 1; acc[proto] = (acc[proto] || 0) + 1;
return acc; return acc;
}, {} as Record<string, number>); }, {} as Record<string, number>);
console.log(`[Prowlarr] Protocol distribution in ${results.length} results:`, JSON.stringify(protocolCounts)); logger.debug(`Protocol distribution in ${results.length} results`, { protocols: protocolCounts });
// Debug: Log first few results to see their protocols // Debug: Log first few results to see their protocols
if (results.length > 0 && results.length <= 5) { if (results.length > 0 && results.length <= 5) {
results.forEach((r, i) => { results.forEach((r, i) => {
console.log(`[Prowlarr] Result ${i + 1}: protocol="${r.protocol || 'undefined'}", url="${r.downloadUrl.substring(0, 80)}..."`); logger.info(` Result ${i + 1}: protocol="${r.protocol || 'undefined'}", url="${r.downloadUrl.substring(0, 80)}..."`);
}); });
} else if (results.length > 5) { } else if (results.length > 5) {
console.log(`[Prowlarr] First 3 results:`); logger.info(` First 3 results:`);
results.slice(0, 3).forEach((r, i) => { results.slice(0, 3).forEach((r, i) => {
console.log(`[Prowlarr] ${i + 1}: protocol="${r.protocol || 'undefined'}", isNZB=${ProwlarrService.isNZBResult(r)}`); logger.info(` ${i + 1}: protocol="${r.protocol || 'undefined'}", isNZB=${ProwlarrService.isNZBResult(r)}`);
}); });
} }
if (clientType === 'sabnzbd') { if (clientType === 'sabnzbd') {
// Filter for NZB results only // Filter for NZB results only
const filtered = results.filter(result => ProwlarrService.isNZBResult(result)); const filtered = results.filter(result => ProwlarrService.isNZBResult(result));
console.log(`[Prowlarr] Filtered ${results.length} results to ${filtered.length} NZB results for SABnzbd`); logger.info(` Filtered ${results.length} results to ${filtered.length} NZB results for SABnzbd`);
return filtered; return filtered;
} else { } else {
// Filter for torrent results only (default) // Filter for torrent results only (default)
const filtered = results.filter(result => !ProwlarrService.isNZBResult(result)); const filtered = results.filter(result => !ProwlarrService.isNZBResult(result));
console.log(`[Prowlarr] Filtered ${results.length} results to ${filtered.length} torrent results for qBittorrent`); logger.info(` Filtered ${results.length} results to ${filtered.length} torrent results for qBittorrent`);
return filtered; return filtered;
} }
} catch (error) { } catch (error) {
console.error('[Prowlarr] Failed to filter by protocol, returning all results:', error); logger.error('Failed to filter by protocol, returning all results', { error: error instanceof Error ? error.message : String(error) });
return results; // Fallback: return unfiltered if config fails return results; // Fallback: return unfiltered if config fails
} }
} }
@@ -435,7 +438,7 @@ export class ProwlarrService {
// Validate we have a valid download URL // Validate we have a valid download URL
if (!downloadUrl || typeof downloadUrl !== 'string' || downloadUrl.trim() === '') { if (!downloadUrl || typeof downloadUrl !== 'string' || downloadUrl.trim() === '') {
console.warn(`[Prowlarr] Skipping result "${result.title}" - missing both downloadUrl and magnetUrl`); logger.warn(` Skipping result "${result.title}" - missing both downloadUrl and magnetUrl`);
return null; return null;
} }
@@ -464,7 +467,7 @@ export class ProwlarrService {
protocol: result.protocol, // 'torrent' or 'usenet' protocol: result.protocol, // 'torrent' or 'usenet'
}; };
} catch (error) { } catch (error) {
console.error('Failed to transform result:', result, error); logger.error('Failed to transform result', { title: result?.title, error: error instanceof Error ? error.message : String(error) });
return null; return null;
} }
} }
@@ -513,7 +516,7 @@ export class ProwlarrService {
// Log detected flags for debugging // Log detected flags for debugging
if (flags.length > 0) { if (flags.length > 0) {
console.log(`[Prowlarr] ✓ Detected flags for "${result.title.substring(0, 50)}...": [${flags.join(', ')}]`); logger.info(` ✓ Detected flags for "${result.title.substring(0, 50)}...": [${flags.join(', ')}]`);
} }
return flags; return flags;
@@ -576,7 +579,7 @@ export async function getProwlarrService(): Promise<ProwlarrService> {
// Test connection // Test connection
const isConnected = await prowlarrService.testConnection(); const isConnected = await prowlarrService.testConnection();
if (!isConnected) { if (!isConnected) {
console.warn('Warning: Prowlarr connection test failed'); logger.warn('Connection test failed');
} }
} }
+72 -60
View File
@@ -7,10 +7,14 @@ import axios, { AxiosInstance } from 'axios';
import https from 'https'; import https from 'https';
import * as parseTorrentModule from 'parse-torrent'; import * as parseTorrentModule from 'parse-torrent';
import FormData from 'form-data'; import FormData from 'form-data';
import { RMABLogger } from '../utils/logger';
// Handle both ESM and CommonJS imports // Handle both ESM and CommonJS imports
const parseTorrent = (parseTorrentModule as any).default || parseTorrentModule; const parseTorrent = (parseTorrentModule as any).default || parseTorrentModule;
// Module-level logger
const logger = RMABLogger.create('QBittorrent');
export interface AddTorrentOptions { export interface AddTorrentOptions {
savePath?: string; savePath?: string;
category?: string; category?: string;
@@ -104,7 +108,7 @@ export class QBittorrentService {
this.httpsAgent = new https.Agent({ this.httpsAgent = new https.Agent({
rejectUnauthorized: false, rejectUnauthorized: false,
}); });
console.log('[qBittorrent] SSL certificate verification disabled'); logger.info('[QBittorrent] SSL certificate verification disabled');
} }
this.client = axios.create({ this.client = axios.create({
@@ -126,7 +130,11 @@ export class QBittorrentService {
password: this.password, password: this.password,
}), }),
{ {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': this.baseUrl,
'Origin': this.baseUrl,
},
httpsAgent: this.httpsAgent, httpsAgent: this.httpsAgent,
} }
); );
@@ -141,9 +149,9 @@ export class QBittorrentService {
throw new Error('Failed to authenticate with qBittorrent'); throw new Error('Failed to authenticate with qBittorrent');
} }
console.log('Successfully authenticated with qBittorrent'); logger.info('Successfully authenticated');
} catch (error) { } catch (error) {
console.error('qBittorrent login failed:', error); logger.error('Login failed', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to authenticate with qBittorrent'); throw new Error('Failed to authenticate with qBittorrent');
} }
} }
@@ -154,7 +162,7 @@ export class QBittorrentService {
async addTorrent(url: string, options?: AddTorrentOptions): Promise<string> { async addTorrent(url: string, options?: AddTorrentOptions): Promise<string> {
// Validate URL parameter // Validate URL parameter
if (!url || typeof url !== 'string' || url.trim() === '') { if (!url || typeof url !== 'string' || url.trim() === '') {
console.error('[qBittorrent] Invalid download URL:', url); logger.error('Invalid download URL', { url });
throw new Error('Invalid download URL: URL is required and must be a non-empty string'); throw new Error('Invalid download URL: URL is required and must be a non-empty string');
} }
@@ -171,21 +179,21 @@ export class QBittorrentService {
// Determine if this is a magnet link or .torrent file URL // Determine if this is a magnet link or .torrent file URL
if (url.startsWith('magnet:')) { if (url.startsWith('magnet:')) {
console.log('[qBittorrent] Detected magnet link'); logger.info('[QBittorrent] Detected magnet link');
return await this.addMagnetLink(url, category, options); return await this.addMagnetLink(url, category, options);
} else { } else {
console.log('[qBittorrent] Detected .torrent file URL'); logger.info('[QBittorrent] Detected .torrent file URL');
return await this.addTorrentFile(url, category, options); return await this.addTorrentFile(url, category, options);
} }
} catch (error) { } catch (error) {
// Try re-authenticating if we get a 403 // Try re-authenticating if we get a 403
if (axios.isAxiosError(error) && error.response?.status === 403) { if (axios.isAxiosError(error) && error.response?.status === 403) {
console.log('[qBittorrent] Session expired, re-authenticating...'); logger.info('[QBittorrent] Session expired, re-authenticating...');
await this.login(); await this.login();
return this.addTorrent(url, options); // Retry once return this.addTorrent(url, options); // Retry once
} }
console.error('[qBittorrent] Failed to add torrent:', error); logger.error('Failed to add torrent', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to add torrent to qBittorrent'); throw new Error('Failed to add torrent to qBittorrent');
} }
} }
@@ -205,12 +213,12 @@ export class QBittorrentService {
throw new Error('Invalid magnet link - could not extract info_hash'); throw new Error('Invalid magnet link - could not extract info_hash');
} }
console.log(`[qBittorrent] Extracted info_hash from magnet: ${infoHash}`); logger.info(` Extracted info_hash from magnet: ${infoHash}`);
// Check for duplicates // Check for duplicates
try { try {
const existing = await this.getTorrent(infoHash); const existing = await this.getTorrent(infoHash);
console.log(`[qBittorrent] Torrent ${infoHash} already exists (duplicate), returning existing hash`); logger.info(` Torrent ${infoHash} already exists (duplicate), returning existing hash`);
return infoHash; return infoHash;
} catch { } catch {
// Torrent doesn't exist, continue with adding // Torrent doesn't exist, continue with adding
@@ -229,7 +237,7 @@ export class QBittorrentService {
form.append('tags', options.tags.join(',')); form.append('tags', options.tags.join(','));
} }
console.log('[qBittorrent] Uploading magnet link...'); logger.info('[QBittorrent] Uploading magnet link...');
const response = await this.client.post('/torrents/add', form, { const response = await this.client.post('/torrents/add', form, {
headers: { headers: {
@@ -242,7 +250,7 @@ export class QBittorrentService {
throw new Error(`qBittorrent rejected magnet link: ${response.data}`); throw new Error(`qBittorrent rejected magnet link: ${response.data}`);
} }
console.log(`[qBittorrent] Successfully added magnet link: ${infoHash}`); logger.info(` Successfully added magnet link: ${infoHash}`);
return infoHash; return infoHash;
} }
@@ -254,7 +262,7 @@ export class QBittorrentService {
category: string, category: string,
options?: AddTorrentOptions options?: AddTorrentOptions
): Promise<string> { ): Promise<string> {
console.log(`[qBittorrent] Downloading .torrent file from: ${torrentUrl}`); logger.info(` Downloading .torrent file from: ${torrentUrl}`);
// Make initial request with maxRedirects: 0 to intercept redirects // Make initial request with maxRedirects: 0 to intercept redirects
// Some Prowlarr indexers return HTTP URLs that redirect to magnet: links // Some Prowlarr indexers return HTTP URLs that redirect to magnet: links
@@ -267,14 +275,14 @@ export class QBittorrentService {
timeout: 30000, // 30 seconds - public indexers can be slow timeout: 30000, // 30 seconds - public indexers can be slow
}); });
console.log(`[qBittorrent] Got 2xx response, size=${torrentResponse.data.length} bytes`); logger.info(` Got 2xx response, size=${torrentResponse.data.length} bytes`);
// Check if response body contains a magnet link // Check if response body contains a magnet link
if (torrentResponse.data.length > 0) { if (torrentResponse.data.length > 0) {
const responseText = torrentResponse.data.toString(); const responseText = torrentResponse.data.toString();
const magnetMatch = responseText.match(/^magnet:\?[^\s]+$/); const magnetMatch = responseText.match(/^magnet:\?[^\s]+$/);
if (magnetMatch) { if (magnetMatch) {
console.log(`[qBittorrent] Response body is a magnet link`); logger.info(` Response body is a magnet link`);
return await this.addMagnetLink(magnetMatch[0], category, options); return await this.addMagnetLink(magnetMatch[0], category, options);
} }
} }
@@ -283,7 +291,7 @@ export class QBittorrentService {
} catch (error) { } catch (error) {
if (!axios.isAxiosError(error) || !error.response) { if (!axios.isAxiosError(error) || !error.response) {
// Not an axios error or no response - re-throw // Not an axios error or no response - re-throw
console.error(`[qBittorrent] Request failed:`, error); logger.error('Request failed', { error: error instanceof Error ? error.message : String(error) });
throw error; throw error;
} }
@@ -292,26 +300,26 @@ export class QBittorrentService {
// Handle 3xx redirects // Handle 3xx redirects
if (status >= 300 && status < 400) { if (status >= 300 && status < 400) {
const location = error.response.headers['location']; const location = error.response.headers['location'];
console.log(`[qBittorrent] Got ${status} redirect to: ${location}`); logger.info(` Got ${status} redirect to: ${location}`);
// Check if redirect target is a magnet link // Check if redirect target is a magnet link
if (location && location.startsWith('magnet:')) { if (location && location.startsWith('magnet:')) {
console.log(`[qBittorrent] Redirect target is magnet link`); logger.info(` Redirect target is magnet link`);
return await this.addMagnetLink(location, category, options); return await this.addMagnetLink(location, category, options);
} }
// Regular HTTP redirect - follow it manually // Regular HTTP redirect - follow it manually
if (location && (location.startsWith('http://') || location.startsWith('https://'))) { if (location && (location.startsWith('http://') || location.startsWith('https://'))) {
console.log(`[qBittorrent] Following HTTP redirect...`); logger.info(` Following HTTP redirect...`);
try { try {
torrentResponse = await axios.get(location, { torrentResponse = await axios.get(location, {
responseType: 'arraybuffer', responseType: 'arraybuffer',
timeout: 30000, timeout: 30000,
maxRedirects: 5, maxRedirects: 5,
}); });
console.log(`[qBittorrent] After following redirect: size=${torrentResponse.data.length} bytes`); logger.info(` After following redirect: size=${torrentResponse.data.length} bytes`);
} catch (redirectError) { } catch (redirectError) {
console.error(`[qBittorrent] Failed to follow redirect:`, redirectError); logger.error('Failed to follow redirect', { error: redirectError instanceof Error ? redirectError.message : String(redirectError) });
throw new Error('Failed to download torrent file after redirect'); throw new Error('Failed to download torrent file after redirect');
} }
} else { } else {
@@ -319,20 +327,20 @@ export class QBittorrentService {
} }
} else { } else {
// Non-redirect error (4xx, 5xx) // Non-redirect error (4xx, 5xx)
console.error(`[qBittorrent] HTTP error ${status}:`, error.message); logger.error(`HTTP error ${status}`, { error: error.message });
throw new Error(`Failed to download torrent: HTTP ${status}`); throw new Error(`Failed to download torrent: HTTP ${status}`);
} }
} }
const torrentBuffer = Buffer.from(torrentResponse.data); const torrentBuffer = Buffer.from(torrentResponse.data);
console.log(`[qBittorrent] Processing torrent file: ${torrentBuffer.length} bytes`); logger.info(` Processing torrent file: ${torrentBuffer.length} bytes`);
// Parse .torrent file to extract info_hash (deterministic) // Parse .torrent file to extract info_hash (deterministic)
let parsedTorrent: any; let parsedTorrent: any;
try { try {
parsedTorrent = await parseTorrent(torrentBuffer); parsedTorrent = await parseTorrent(torrentBuffer);
} catch (error) { } catch (error) {
console.error('[qBittorrent] Failed to parse .torrent file:', error); logger.error('Failed to parse .torrent file', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Invalid .torrent file - failed to parse'); throw new Error('Invalid .torrent file - failed to parse');
} }
@@ -342,13 +350,13 @@ export class QBittorrentService {
throw new Error('Failed to extract info_hash from .torrent file'); throw new Error('Failed to extract info_hash from .torrent file');
} }
console.log(`[qBittorrent] Extracted info_hash: ${infoHash}`); logger.info(` Extracted info_hash: ${infoHash}`);
console.log(`[qBittorrent] Torrent name: ${parsedTorrent.name || 'Unknown'}`); logger.info(` Torrent name: ${parsedTorrent.name || 'Unknown'}`);
// Check for duplicates // Check for duplicates
try { try {
const existing = await this.getTorrent(infoHash); const existing = await this.getTorrent(infoHash);
console.log(`[qBittorrent] Torrent ${infoHash} already exists (duplicate), returning existing hash`); logger.info(` Torrent ${infoHash} already exists (duplicate), returning existing hash`);
return infoHash; return infoHash;
} catch { } catch {
// Torrent doesn't exist, continue with adding // Torrent doesn't exist, continue with adding
@@ -371,7 +379,7 @@ export class QBittorrentService {
formData.append('tags', options.tags.join(',')); formData.append('tags', options.tags.join(','));
} }
console.log('[qBittorrent] Uploading .torrent file content...'); logger.info('[QBittorrent] Uploading .torrent file content...');
const response = await this.client.post('/torrents/add', formData, { const response = await this.client.post('/torrents/add', formData, {
headers: { headers: {
@@ -386,7 +394,7 @@ export class QBittorrentService {
throw new Error(`qBittorrent rejected .torrent file: ${response.data}`); throw new Error(`qBittorrent rejected .torrent file: ${response.data}`);
} }
console.log(`[qBittorrent] Successfully added torrent: ${infoHash}`); logger.info(` Successfully added torrent: ${infoHash}`);
return infoHash; return infoHash;
} }
@@ -410,7 +418,7 @@ export class QBittorrentService {
if (!existingCategory) { if (!existingCategory) {
// Category doesn't exist - create it // Category doesn't exist - create it
console.log(`[qBittorrent] Creating category "${category}" with save path: ${this.defaultSavePath}`); logger.info(` Creating category "${category}" with save path: ${this.defaultSavePath}`);
await this.client.post( await this.client.post(
'/torrents/createCategory', '/torrents/createCategory',
@@ -426,13 +434,13 @@ export class QBittorrentService {
} }
); );
console.log(`[qBittorrent] Category "${category}" created successfully`); logger.info(` Category "${category}" created successfully`);
} else { } else {
// Category exists - check if save path needs updating // Category exists - check if save path needs updating
const currentSavePath = existingCategory.savePath || existingCategory.save_path; const currentSavePath = existingCategory.savePath || existingCategory.save_path;
if (currentSavePath !== this.defaultSavePath) { if (currentSavePath !== this.defaultSavePath) {
console.log(`[qBittorrent] Updating category "${category}" save path from "${currentSavePath}" to "${this.defaultSavePath}"`); logger.info(` Updating category "${category}" save path from "${currentSavePath}" to "${this.defaultSavePath}"`);
await this.client.post( await this.client.post(
'/torrents/editCategory', '/torrents/editCategory',
@@ -448,23 +456,23 @@ export class QBittorrentService {
} }
); );
console.log(`[qBittorrent] Category "${category}" save path updated successfully`); logger.info(` Category "${category}" save path updated successfully`);
} else { } else {
console.log(`[qBittorrent] Category "${category}" already has correct save path: ${this.defaultSavePath}`); logger.info(` Category "${category}" already has correct save path: ${this.defaultSavePath}`);
} }
} }
} catch (error) { } catch (error) {
// If we can't ensure the category, log error but don't throw // If we can't ensure the category, log error but don't throw
// Torrents can still be added with per-torrent savepath parameter // Torrents can still be added with per-torrent savepath parameter
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
console.error(`[qBittorrent] Failed to ensure category "${category}":`, { logger.error(` Failed to ensure category "${category}":`, {
status: error.response?.status, status: error.response?.status,
statusText: error.response?.statusText, statusText: error.response?.statusText,
data: error.response?.data, data: error.response?.data,
requestedPath: this.defaultSavePath, requestedPath: this.defaultSavePath,
}); });
} else { } else {
console.error(`[qBittorrent] Failed to ensure category:`, error); logger.error('Failed to ensure category', { error: error instanceof Error ? error.message : String(error) });
} }
} }
} }
@@ -516,7 +524,7 @@ export class QBittorrentService {
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Failed to get torrents:', error); logger.error('Failed to get torrents', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to get torrents from qBittorrent'); throw new Error('Failed to get torrents from qBittorrent');
} }
} }
@@ -541,9 +549,9 @@ export class QBittorrentService {
} }
); );
console.log(`Paused torrent: ${hash}`); logger.info(`Paused torrent: ${hash}`);
} catch (error) { } catch (error) {
console.error('Failed to pause torrent:', error); logger.error('Failed to pause torrent', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to pause torrent'); throw new Error('Failed to pause torrent');
} }
} }
@@ -568,9 +576,9 @@ export class QBittorrentService {
} }
); );
console.log(`Resumed torrent: ${hash}`); logger.info(`Resumed torrent: ${hash}`);
} catch (error) { } catch (error) {
console.error('Failed to resume torrent:', error); logger.error('Failed to resume torrent', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to resume torrent'); throw new Error('Failed to resume torrent');
} }
} }
@@ -598,9 +606,9 @@ export class QBittorrentService {
} }
); );
console.log(`Deleted torrent: ${hash}`); logger.info(`Deleted torrent: ${hash}`);
} catch (error) { } catch (error) {
console.error('Failed to delete torrent:', error); logger.error('Failed to delete torrent', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to delete torrent'); throw new Error('Failed to delete torrent');
} }
} }
@@ -621,7 +629,7 @@ export class QBittorrentService {
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Failed to get torrent files:', error); logger.error('Failed to get torrent files', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to get torrent files'); throw new Error('Failed to get torrent files');
} }
} }
@@ -649,9 +657,9 @@ export class QBittorrentService {
} }
); );
console.log(`Set category for torrent ${hash}: ${category}`); logger.info(`Set category for torrent ${hash}: ${category}`);
} catch (error) { } catch (error) {
console.error('Failed to set category:', error); logger.error('Failed to set category', { error: error instanceof Error ? error.message : String(error) });
throw new Error('Failed to set torrent category'); throw new Error('Failed to set torrent category');
} }
} }
@@ -664,7 +672,7 @@ export class QBittorrentService {
await this.login(); await this.login();
return true; return true;
} catch (error) { } catch (error) {
console.error('qBittorrent connection test failed:', error); logger.error('Connection test failed', { error: error instanceof Error ? error.message : String(error) });
return false; return false;
} }
} }
@@ -686,7 +694,7 @@ export class QBittorrentService {
httpsAgent = new https.Agent({ httpsAgent = new https.Agent({
rejectUnauthorized: false, rejectUnauthorized: false,
}); });
console.log('[qBittorrent] SSL certificate verification disabled for test connection'); logger.info('[QBittorrent] SSL certificate verification disabled for test connection');
} }
try { try {
@@ -694,7 +702,11 @@ export class QBittorrentService {
`${baseUrl}/api/v2/auth/login`, `${baseUrl}/api/v2/auth/login`,
new URLSearchParams({ username, password }), new URLSearchParams({ username, password }),
{ {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': baseUrl,
'Origin': baseUrl,
},
httpsAgent, httpsAgent,
} }
); );
@@ -714,7 +726,7 @@ export class QBittorrentService {
return versionResponse.data || 'Connected'; return versionResponse.data || 'Connected';
} catch (error) { } catch (error) {
console.error('[qBittorrent] Connection test failed:', error); logger.error('Connection test failed', { error: error instanceof Error ? error.message : String(error) });
// Enhanced error messages for common issues // Enhanced error messages for common issues
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
@@ -856,7 +868,7 @@ let configLoaded = false;
* Forces service to reload configuration from database on next use * Forces service to reload configuration from database on next use
*/ */
export function invalidateQBittorrentService(): void { export function invalidateQBittorrentService(): void {
console.log('[qBittorrent] Invalidating service singleton - will reload config on next use'); logger.info('[QBittorrent] Invalidating service singleton - will reload config on next use');
qbittorrentService = null; qbittorrentService = null;
configLoaded = false; configLoaded = false;
} }
@@ -869,7 +881,7 @@ export async function getQBittorrentService(): Promise<QBittorrentService> {
const { getConfigService } = await import('@/lib/services/config.service'); const { getConfigService } = await import('@/lib/services/config.service');
const configService = getConfigService(); const configService = getConfigService();
console.log('[qBittorrent] Loading configuration from database...'); logger.info('[QBittorrent] Loading configuration from database...');
const config = await configService.getMany([ const config = await configService.getMany([
'download_client_url', 'download_client_url',
'download_client_username', 'download_client_username',
@@ -878,7 +890,7 @@ export async function getQBittorrentService(): Promise<QBittorrentService> {
'download_client_disable_ssl_verify', 'download_client_disable_ssl_verify',
]); ]);
console.log('[qBittorrent] Config loaded:', { logger.info('[QBittorrent] Config loaded:', {
hasUrl: !!config.download_client_url, hasUrl: !!config.download_client_url,
hasUsername: !!config.download_client_username, hasUsername: !!config.download_client_username,
hasPassword: !!config.download_client_password, hasPassword: !!config.download_client_password,
@@ -904,7 +916,7 @@ export async function getQBittorrentService(): Promise<QBittorrentService> {
if (missingFields.length > 0) { if (missingFields.length > 0) {
const errorMsg = `qBittorrent is not fully configured. Missing: ${missingFields.join(', ')}. Please configure qBittorrent in the admin settings.`; const errorMsg = `qBittorrent is not fully configured. Missing: ${missingFields.join(', ')}. Please configure qBittorrent in the admin settings.`;
console.error('[qBittorrent]', errorMsg); logger.error('Configuration incomplete', { missingFields });
throw new Error(errorMsg); throw new Error(errorMsg);
} }
@@ -915,7 +927,7 @@ export async function getQBittorrentService(): Promise<QBittorrentService> {
const savePath = config.download_dir as string; const savePath = config.download_dir as string;
const disableSSLVerify = config.download_client_disable_ssl_verify === 'true'; const disableSSLVerify = config.download_client_disable_ssl_verify === 'true';
console.log('[qBittorrent] Creating service instance...'); logger.info('[QBittorrent] Creating service instance...');
qbittorrentService = new QBittorrentService( qbittorrentService = new QBittorrentService(
url, url,
username, username,
@@ -926,17 +938,17 @@ export async function getQBittorrentService(): Promise<QBittorrentService> {
); );
// Test connection // Test connection
console.log('[qBittorrent] Testing connection...'); logger.info('[QBittorrent] Testing connection...');
const isConnected = await qbittorrentService.testConnection(); const isConnected = await qbittorrentService.testConnection();
if (!isConnected) { if (!isConnected) {
console.warn('[qBittorrent] Connection test failed'); logger.warn('[QBittorrent] Connection test failed');
throw new Error('qBittorrent connection test failed. Please check your configuration in admin settings.'); throw new Error('qBittorrent connection test failed. Please check your configuration in admin settings.');
} else { } else {
console.log('[qBittorrent] Connection test successful'); logger.info('[QBittorrent] Connection test successful');
configLoaded = true; // Mark as successfully loaded configLoaded = true; // Mark as successfully loaded
} }
} catch (error) { } catch (error) {
console.error('[qBittorrent] Failed to initialize service:', error); logger.error('Failed to initialize service', { error: error instanceof Error ? error.message : String(error) });
qbittorrentService = null; // Reset service on error qbittorrentService = null; // Reset service on error
configLoaded = false; configLoaded = false;
throw error; throw error;
+10 -7
View File
@@ -5,6 +5,9 @@
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import https from 'https'; import https from 'https';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('SABnzbd');
export interface AddNZBOptions { export interface AddNZBOptions {
category?: string; category?: string;
@@ -238,7 +241,7 @@ export class SABnzbdService {
const categoryExists = config.categories.some(cat => cat.name === this.defaultCategory); const categoryExists = config.categories.some(cat => cat.name === this.defaultCategory);
if (!categoryExists) { if (!categoryExists) {
console.log(`[SABnzbd] Creating category: ${this.defaultCategory}`); logger.info(`Creating category: ${this.defaultCategory}`);
// Create category // Create category
await this.client.get('/api', { await this.client.get('/api', {
@@ -252,12 +255,12 @@ export class SABnzbdService {
}, },
}); });
console.log(`[SABnzbd] Category created successfully: ${this.defaultCategory}`); logger.info(`Category created successfully: ${this.defaultCategory}`);
} else { } else {
console.log(`[SABnzbd] Category already exists: ${this.defaultCategory}`); logger.info(`Category already exists: ${this.defaultCategory}`);
} }
} catch (error) { } catch (error) {
console.error('[SABnzbd] Failed to ensure category:', error); logger.error('Failed to ensure category', { error: error instanceof Error ? error.message : String(error) });
// Don't throw - category creation failure shouldn't block downloads // Don't throw - category creation failure shouldn't block downloads
} }
} }
@@ -267,7 +270,7 @@ export class SABnzbdService {
* Returns the NZB ID * Returns the NZB ID
*/ */
async addNZB(url: string, options?: AddNZBOptions): Promise<string> { async addNZB(url: string, options?: AddNZBOptions): Promise<string> {
console.log(`[SABnzbd] Adding NZB from URL: ${url.substring(0, 150)}...`); logger.info(`Adding NZB from URL: ${url.substring(0, 150)}...`);
const response = await this.client.get('/api', { const response = await this.client.get('/api', {
params: { params: {
@@ -291,7 +294,7 @@ export class SABnzbdService {
} }
const nzbId = nzbIds[0]; const nzbId = nzbIds[0];
console.log(`[SABnzbd] Added NZB: ${nzbId}`); logger.info(`Added NZB: ${nzbId}`);
return nzbId; return nzbId;
} }
@@ -559,5 +562,5 @@ export async function getSABnzbdService(): Promise<SABnzbdService> {
export function invalidateSABnzbdService(): void { export function invalidateSABnzbdService(): void {
sabnzbdServiceInstance = null; sabnzbdServiceInstance = null;
console.log('[SABnzbd] Service singleton invalidated'); logger.info('Service singleton invalidated');
} }
+6 -3
View File
@@ -6,6 +6,9 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { verifyAccessToken, TokenPayload } from '../utils/jwt'; import { verifyAccessToken, TokenPayload } from '../utils/jwt';
import { prisma } from '../db'; import { prisma } from '../db';
import { RMABLogger } from '../utils/logger';
const logger = RMABLogger.create('Auth');
export interface AuthenticatedRequest extends NextRequest { export interface AuthenticatedRequest extends NextRequest {
user?: TokenPayload & { id: string }; user?: TokenPayload & { id: string };
@@ -40,7 +43,7 @@ export async function requireAuth(
const token = extractToken(request); const token = extractToken(request);
if (!token) { if (!token) {
console.error('[Auth Middleware] No token provided'); logger.error('No token provided');
return NextResponse.json( return NextResponse.json(
{ {
error: 'Unauthorized', error: 'Unauthorized',
@@ -53,7 +56,7 @@ export async function requireAuth(
const payload = verifyAccessToken(token); const payload = verifyAccessToken(token);
if (!payload) { if (!payload) {
console.error('[Auth Middleware] Token verification failed'); logger.error('Token verification failed');
return NextResponse.json( return NextResponse.json(
{ {
error: 'Unauthorized', error: 'Unauthorized',
@@ -69,7 +72,7 @@ export async function requireAuth(
}); });
if (!user) { if (!user) {
console.error('[Auth Middleware] User not found in database:', payload.sub); logger.error('User not found in database', { userId: payload.sub });
return NextResponse.json( return NextResponse.json(
{ {
error: 'Unauthorized', error: 'Unauthorized',
+11 -11
View File
@@ -6,7 +6,7 @@
*/ */
import { prisma } from '../db'; import { prisma } from '../db';
import { createJobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
export interface AudibleRefreshPayload { export interface AudibleRefreshPayload {
jobId?: string; jobId?: string;
@@ -15,9 +15,9 @@ export interface AudibleRefreshPayload {
export async function processAudibleRefresh(payload: AudibleRefreshPayload): Promise<any> { export async function processAudibleRefresh(payload: AudibleRefreshPayload): Promise<any> {
const { jobId, scheduledJobId } = payload; const { jobId, scheduledJobId } = payload;
const logger = jobId ? createJobLogger(jobId, 'AudibleRefresh') : null; const logger = RMABLogger.forJob(jobId, 'AudibleRefresh');
await logger?.info('Starting Audible data refresh...'); logger.info('Starting Audible data refresh...');
const { getAudibleService } = await import('../integrations/audible.service'); const { getAudibleService } = await import('../integrations/audible.service');
const { getThumbnailCacheService } = await import('../services/thumbnail-cache.service'); const { getThumbnailCacheService } = await import('../services/thumbnail-cache.service');
@@ -40,13 +40,13 @@ export async function processAudibleRefresh(payload: AudibleRefreshPayload): Pro
newReleaseRank: null, newReleaseRank: null,
}, },
}); });
await logger?.info('Cleared previous popular/new-release flags in audible_cache'); logger.info('Cleared previous popular/new-release flags in audible_cache');
// Fetch popular and new releases - 200 items each // Fetch popular and new releases - 200 items each
const popular = await audibleService.getPopularAudiobooks(200); const popular = await audibleService.getPopularAudiobooks(200);
const newReleases = await audibleService.getNewReleases(200); const newReleases = await audibleService.getNewReleases(200);
await logger?.info(`Fetched ${popular.length} popular, ${newReleases.length} new releases from Audible`); logger.info(`Fetched ${popular.length} popular, ${newReleases.length} new releases from Audible`);
// Persist to audible_cache // Persist to audible_cache
let popularSaved = 0; let popularSaved = 0;
@@ -99,7 +99,7 @@ export async function processAudibleRefresh(payload: AudibleRefreshPayload): Pro
popularSaved++; popularSaved++;
} catch (error) { } catch (error) {
await logger?.error(`Failed to save popular audiobook ${audiobook.title}: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Failed to save popular audiobook ${audiobook.title}: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
} }
@@ -149,20 +149,20 @@ export async function processAudibleRefresh(payload: AudibleRefreshPayload): Pro
newReleasesSaved++; newReleasesSaved++;
} catch (error) { } catch (error) {
await logger?.error(`Failed to save new release ${audiobook.title}: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Failed to save new release ${audiobook.title}: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
} }
await logger?.info(`Saved ${popularSaved} popular and ${newReleasesSaved} new releases to audible_cache`); logger.info(`Saved ${popularSaved} popular and ${newReleasesSaved} new releases to audible_cache`);
// Cleanup unused thumbnails // Cleanup unused thumbnails
await logger?.info('Cleaning up unused thumbnails...'); logger.info('Cleaning up unused thumbnails...');
const allActiveAsins = await prisma.audibleCache.findMany({ const allActiveAsins = await prisma.audibleCache.findMany({
select: { asin: true }, select: { asin: true },
}); });
const activeAsinSet = new Set(allActiveAsins.map(item => item.asin)); const activeAsinSet = new Set(allActiveAsins.map(item => item.asin));
const deletedCount = await thumbnailCache.cleanupUnusedThumbnails(activeAsinSet); const deletedCount = await thumbnailCache.cleanupUnusedThumbnails(activeAsinSet);
await logger?.info(`Cleanup complete: ${deletedCount} unused thumbnails removed`); logger.info(`Cleanup complete: ${deletedCount} unused thumbnails removed`);
return { return {
success: true, success: true,
@@ -172,7 +172,7 @@ export async function processAudibleRefresh(payload: AudibleRefreshPayload): Pro
thumbnailsDeleted: deletedCount, thumbnailsDeleted: deletedCount,
}; };
} catch (error) { } catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
throw error; throw error;
} }
} }
@@ -6,7 +6,7 @@
*/ */
import { prisma } from '../db'; import { prisma } from '../db';
import { createJobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
export interface CleanupSeededTorrentsPayload { export interface CleanupSeededTorrentsPayload {
jobId?: string; jobId?: string;
@@ -15,9 +15,9 @@ export interface CleanupSeededTorrentsPayload {
export async function processCleanupSeededTorrents(payload: CleanupSeededTorrentsPayload): Promise<any> { export async function processCleanupSeededTorrents(payload: CleanupSeededTorrentsPayload): Promise<any> {
const { jobId, scheduledJobId } = payload; const { jobId, scheduledJobId } = payload;
const logger = jobId ? createJobLogger(jobId, 'CleanupSeededTorrents') : null; const logger = RMABLogger.forJob(jobId, 'CleanupSeededTorrents');
await logger?.info('Starting cleanup job for seeded torrents...'); logger.info('Starting cleanup job for seeded torrents...');
try { try {
// Get indexer configuration with per-indexer seeding times // Get indexer configuration with per-indexer seeding times
@@ -26,7 +26,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
const indexersConfigStr = await configService.get('prowlarr_indexers'); const indexersConfigStr = await configService.get('prowlarr_indexers');
if (!indexersConfigStr) { if (!indexersConfigStr) {
await logger?.warn('No indexer configuration found, skipping'); logger.warn('No indexer configuration found, skipping');
return { return {
success: false, success: false,
message: 'No indexer configuration', message: 'No indexer configuration',
@@ -42,7 +42,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
indexerConfigMap.set(indexer.name, indexer); indexerConfigMap.set(indexer.name, indexer);
} }
await logger?.info(`Loaded configuration for ${indexerConfigMap.size} indexers`); logger.info(`Loaded configuration for ${indexerConfigMap.size} indexers`);
// Find all completed requests + soft-deleted requests (orphaned downloads) // Find all completed requests + soft-deleted requests (orphaned downloads)
// IMPORTANT: Only cleanup requests that are truly complete and not being actively processed // IMPORTANT: Only cleanup requests that are truly complete and not being actively processed
@@ -76,7 +76,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
take: 100, // Limit to 100 requests per run take: 100, // Limit to 100 requests per run
}); });
await logger?.info(`Found ${completedRequests.length} requests to check (status: 'available' or soft-deleted)`); logger.info(`Found ${completedRequests.length} requests to check (status: 'available' or soft-deleted)`);
let cleaned = 0; let cleaned = 0;
let skipped = 0; let skipped = 0;
@@ -95,7 +95,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
// For soft-deleted SABnzbd requests, hard delete immediately (no seeding needed) // For soft-deleted SABnzbd requests, hard delete immediately (no seeding needed)
if (request.deletedAt) { if (request.deletedAt) {
await prisma.request.delete({ where: { id: request.id } }); await prisma.request.delete({ where: { id: request.id } });
await logger?.info(`Hard-deleted orphaned SABnzbd request ${request.id}`); logger.info(`Hard-deleted orphaned SABnzbd request ${request.id}`);
} }
continue; continue;
} }
@@ -116,7 +116,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
// For soft-deleted requests with unlimited seeding, hard delete immediately // For soft-deleted requests with unlimited seeding, hard delete immediately
if (request.deletedAt) { if (request.deletedAt) {
await prisma.request.delete({ where: { id: request.id } }); await prisma.request.delete({ where: { id: request.id } });
await logger?.info(`Hard-deleted orphaned request ${request.id} with unlimited seeding`); logger.info(`Hard-deleted orphaned request ${request.id} with unlimited seeding`);
} }
noConfig++; noConfig++;
continue; continue;
@@ -146,7 +146,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
continue; continue;
} }
await logger?.info(`Torrent ${torrent.name} (${indexerName}) has met seeding requirement (${Math.floor(actualSeedingTime / 60)}/${seedingConfig.seedingTimeMinutes} minutes)`); logger.info(`Torrent ${torrent.name} (${indexerName}) has met seeding requirement (${Math.floor(actualSeedingTime / 60)}/${seedingConfig.seedingTimeMinutes} minutes)`);
// CRITICAL: Check if any other active (non-deleted) request is using this same torrent hash // CRITICAL: Check if any other active (non-deleted) request is using this same torrent hash
// This prevents deleting shared torrents when user re-requests the same audiobook // This prevents deleting shared torrents when user re-requests the same audiobook
@@ -165,12 +165,12 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
}); });
if (otherActiveRequests.length > 0) { if (otherActiveRequests.length > 0) {
await logger?.info(`Skipping torrent deletion - ${otherActiveRequests.length} other active request(s) still using this torrent (IDs: ${otherActiveRequests.map(r => r.id).join(', ')})`); logger.info(`Skipping torrent deletion - ${otherActiveRequests.length} other active request(s) still using this torrent (IDs: ${otherActiveRequests.map(r => r.id).join(', ')})`);
// If this is a soft-deleted request, hard delete it but DON'T delete the torrent // If this is a soft-deleted request, hard delete it but DON'T delete the torrent
if (request.deletedAt) { if (request.deletedAt) {
await prisma.request.delete({ where: { id: request.id } }); await prisma.request.delete({ where: { id: request.id } });
await logger?.info(`Hard-deleted orphaned request ${request.id} (kept shared torrent for active requests)`); logger.info(`Hard-deleted orphaned request ${request.id} (kept shared torrent for active requests)`);
} }
skipped++; skipped++;
@@ -183,18 +183,18 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
// If this is a soft-deleted request (orphaned download), hard delete it now // If this is a soft-deleted request (orphaned download), hard delete it now
if (request.deletedAt) { if (request.deletedAt) {
await prisma.request.delete({ where: { id: request.id } }); await prisma.request.delete({ where: { id: request.id } });
await logger?.info(`Hard-deleted orphaned request ${request.id} after torrent cleanup`); logger.info(`Hard-deleted orphaned request ${request.id} after torrent cleanup`);
} else { } else {
await logger?.info(`Deleted torrent and files for active request ${request.id}`); logger.info(`Deleted torrent and files for active request ${request.id}`);
} }
cleaned++; cleaned++;
} catch (error) { } catch (error) {
await logger?.error(`Failed to cleanup request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Failed to cleanup request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
} }
await logger?.info(`Cleanup complete: ${cleaned} torrents cleaned, ${skipped} still seeding, ${noConfig} unlimited`); logger.info(`Cleanup complete: ${cleaned} torrents cleaned, ${skipped} still seeding, ${noConfig} unlimited`);
return { return {
success: true, success: true,
@@ -205,7 +205,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
unlimited: noConfig, unlimited: noConfig,
}; };
} catch (error) { } catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
throw error; throw error;
} }
} }
@@ -8,7 +8,7 @@ import { prisma } from '../db';
import { getQBittorrentService } from '../integrations/qbittorrent.service'; import { getQBittorrentService } from '../integrations/qbittorrent.service';
import { getSABnzbdService } from '../integrations/sabnzbd.service'; import { getSABnzbdService } from '../integrations/sabnzbd.service';
import { getConfigService } from '../services/config.service'; import { getConfigService } from '../services/config.service';
import { createJobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
/** /**
* Process download job * Process download job
@@ -18,10 +18,10 @@ import { createJobLogger } from '../utils/job-logger';
export async function processDownloadTorrent(payload: DownloadTorrentPayload): Promise<any> { export async function processDownloadTorrent(payload: DownloadTorrentPayload): Promise<any> {
const { requestId, audiobook, torrent, jobId } = payload; const { requestId, audiobook, torrent, jobId } = payload;
const logger = jobId ? createJobLogger(jobId, 'DownloadTorrent') : null; const logger = RMABLogger.forJob(jobId, 'DownloadTorrent');
await logger?.info(`Processing request ${requestId} for "${audiobook.title}"`); logger.info(`Processing request ${requestId} for "${audiobook.title}"`);
await logger?.info(`Selected result: ${torrent.title}`, { logger.info(`Selected result: ${torrent.title}`, {
size: torrent.size, size: torrent.size,
seeders: torrent.seeders, seeders: torrent.seeders,
format: torrent.format, format: torrent.format,
@@ -48,7 +48,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
if (clientType === 'sabnzbd') { if (clientType === 'sabnzbd') {
// Route to SABnzbd // Route to SABnzbd
await logger?.info(`Routing to SABnzbd`); logger.info(`Routing to SABnzbd`);
const sabnzbd = await getSABnzbdService(); const sabnzbd = await getSABnzbdService();
downloadClientId = await sabnzbd.addNZB(torrent.downloadUrl, { downloadClientId = await sabnzbd.addNZB(torrent.downloadUrl, {
@@ -57,7 +57,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
}); });
downloadClient = 'sabnzbd'; downloadClient = 'sabnzbd';
await logger?.info(`NZB added with ID: ${downloadClientId}`); logger.info(`NZB added with ID: ${downloadClientId}`);
// Create DownloadHistory record // Create DownloadHistory record
const downloadHistory = await prisma.downloadHistory.create({ const downloadHistory = await prisma.downloadHistory.create({
@@ -79,7 +79,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
}, },
}); });
await logger?.info(`Created download history record: ${downloadHistory.id}`); logger.info(`Created download history record: ${downloadHistory.id}`);
// Trigger monitor download job with initial delay // Trigger monitor download job with initial delay
const jobQueue = getJobQueueService(); const jobQueue = getJobQueueService();
@@ -91,7 +91,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
3 // Wait 3 seconds before first check 3 // Wait 3 seconds before first check
); );
await logger?.info(`Started monitoring job for request ${requestId} (SABnzbd, 3s initial delay)`); logger.info(`Started monitoring job for request ${requestId} (SABnzbd, 3s initial delay)`);
return { return {
success: true, success: true,
@@ -107,7 +107,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
}; };
} else { } else {
// Route to qBittorrent (default) // Route to qBittorrent (default)
await logger?.info(`Routing to qBittorrent`); logger.info(`Routing to qBittorrent`);
const qbt = await getQBittorrentService(); const qbt = await getQBittorrentService();
downloadClientId = await qbt.addTorrent(torrent.downloadUrl, { downloadClientId = await qbt.addTorrent(torrent.downloadUrl, {
@@ -118,7 +118,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
}); });
downloadClient = 'qbittorrent'; downloadClient = 'qbittorrent';
await logger?.info(`Torrent added with hash: ${downloadClientId}`); logger.info(`Torrent added with hash: ${downloadClientId}`);
// Create DownloadHistory record // Create DownloadHistory record
const downloadHistory = await prisma.downloadHistory.create({ const downloadHistory = await prisma.downloadHistory.create({
@@ -140,7 +140,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
}, },
}); });
await logger?.info(`Created download history record: ${downloadHistory.id}`); logger.info(`Created download history record: ${downloadHistory.id}`);
// Trigger monitor download job with initial delay // Trigger monitor download job with initial delay
const jobQueue = getJobQueueService(); const jobQueue = getJobQueueService();
@@ -152,7 +152,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
3 // Wait 3 seconds before first check to avoid race condition 3 // Wait 3 seconds before first check to avoid race condition
); );
await logger?.info(`Started monitoring job for request ${requestId} (qBittorrent, 3s initial delay)`); logger.info(`Started monitoring job for request ${requestId} (qBittorrent, 3s initial delay)`);
return { return {
success: true, success: true,
@@ -169,7 +169,7 @@ export async function processDownloadTorrent(payload: DownloadTorrentPayload): P
}; };
} }
} catch (error) { } catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
// Update request status to failed // Update request status to failed
await prisma.request.update({ await prisma.request.update({
+11 -11
View File
@@ -11,7 +11,7 @@ import { prisma } from '../db';
import { getLibraryService } from '../services/library'; import { getLibraryService } from '../services/library';
import { compareTwoStrings } from 'string-similarity'; import { compareTwoStrings } from 'string-similarity';
import { getConfigService } from '../services/config.service'; import { getConfigService } from '../services/config.service';
import { createJobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
/** /**
* Process match library job (DEPRECATED - use scan_library instead) * Process match library job (DEPRECATED - use scan_library instead)
@@ -20,10 +20,10 @@ import { createJobLogger } from '../utils/job-logger';
export async function processMatchPlex(payload: MatchPlexPayload): Promise<any> { export async function processMatchPlex(payload: MatchPlexPayload): Promise<any> {
const { requestId, audiobookId, title, author, jobId } = payload; const { requestId, audiobookId, title, author, jobId } = payload;
const logger = jobId ? createJobLogger(jobId, 'MatchLibrary') : null; const logger = RMABLogger.forJob(jobId, 'MatchLibrary');
await logger?.warn('DEPRECATED: match_plex job is deprecated. Use scan_plex instead.'); logger.warn('DEPRECATED: match_plex job is deprecated. Use scan_plex instead.');
await logger?.info(`Matching "${title}" by ${author} in library`); logger.info(`Matching "${title}" by ${author} in library`);
try { try {
// Get library service and configuration // Get library service and configuration
@@ -31,7 +31,7 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise<any>
const libraryService = await getLibraryService(); const libraryService = await getLibraryService();
const backendMode = await configService.getBackendMode(); const backendMode = await configService.getBackendMode();
await logger?.info(`Backend mode: ${backendMode}`); logger.info(`Backend mode: ${backendMode}`);
// Get configured library ID // Get configured library ID
const libraryId = backendMode === 'audiobookshelf' const libraryId = backendMode === 'audiobookshelf'
@@ -45,10 +45,10 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise<any>
// Search library using abstraction layer // Search library using abstraction layer
const searchResults = await libraryService.searchItems(libraryId, title); const searchResults = await libraryService.searchItems(libraryId, title);
await logger?.info(`Found ${searchResults.length} results in library`); logger.info(`Found ${searchResults.length} results in library`);
if (searchResults.length === 0) { if (searchResults.length === 0) {
await logger?.warn(`No matches found in library for "${title}"`); logger.warn(`No matches found in library for "${title}"`);
// Mark as completed anyway - the file is there, library just needs time to scan // Mark as completed anyway - the file is there, library just needs time to scan
await prisma.request.update({ await prisma.request.update({
@@ -92,7 +92,7 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise<any>
const bestMatch = matches[0]; const bestMatch = matches[0];
await logger?.info(`Best match: "${bestMatch.item.title}" by ${bestMatch.item.author || 'Unknown'}`, { logger.info(`Best match: "${bestMatch.item.title}" by ${bestMatch.item.author || 'Unknown'}`, {
score: Math.round(bestMatch.score * 100), score: Math.round(bestMatch.score * 100),
titleScore: Math.round(bestMatch.titleScore * 100), titleScore: Math.round(bestMatch.titleScore * 100),
authorScore: Math.round(bestMatch.authorScore * 100), authorScore: Math.round(bestMatch.authorScore * 100),
@@ -100,7 +100,7 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise<any>
// Accept match if score >= 70% // Accept match if score >= 70%
if (bestMatch.score >= 0.7) { if (bestMatch.score >= 0.7) {
await logger?.info(`Match accepted!`); logger.info(`Match accepted!`);
// Update audiobook with library item ID // Update audiobook with library item ID
const updateData: any = { const updateData: any = {
@@ -144,7 +144,7 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise<any>
}, },
}; };
} else { } else {
await logger?.warn(`Match score too low (${Math.round(bestMatch.score * 100)}%), but marking as completed anyway`); logger.warn(`Match score too low (${Math.round(bestMatch.score * 100)}%), but marking as completed anyway`);
// Mark as completed even if match is poor // Mark as completed even if match is poor
await prisma.request.update({ await prisma.request.update({
@@ -166,7 +166,7 @@ export async function processMatchPlex(payload: MatchPlexPayload): Promise<any>
}; };
} }
} catch (error) { } catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
// Don't fail the request - the files are organized correctly // Don't fail the request - the files are organized correctly
// Just log the error and mark as completed // Just log the error and mark as completed
@@ -7,7 +7,7 @@ import path from 'path';
import { MonitorDownloadPayload, getJobQueueService } from '../services/job-queue.service'; import { MonitorDownloadPayload, getJobQueueService } from '../services/job-queue.service';
import { prisma } from '../db'; import { prisma } from '../db';
import { getQBittorrentService } from '../integrations/qbittorrent.service'; import { getQBittorrentService } from '../integrations/qbittorrent.service';
import { createJobLogger, JobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
import { PathMapper } from '../utils/path-mapper'; import { PathMapper } from '../utils/path-mapper';
import { getConfigService } from '../services/config.service'; import { getConfigService } from '../services/config.service';
@@ -18,7 +18,7 @@ import { getConfigService } from '../services/config.service';
async function getTorrentWithRetry( async function getTorrentWithRetry(
qbt: any, qbt: any,
hash: string, hash: string,
logger: JobLogger | null, logger: RMABLogger,
maxRetries: number = 3, maxRetries: number = 3,
initialDelayMs: number = 500 initialDelayMs: number = 500
): Promise<any> { ): Promise<any> {
@@ -37,7 +37,7 @@ async function getTorrentWithRetry(
// Exponential backoff: 500ms, 1000ms, 2000ms // Exponential backoff: 500ms, 1000ms, 2000ms
const delayMs = initialDelayMs * Math.pow(2, attempt); const delayMs = initialDelayMs * Math.pow(2, attempt);
await logger?.warn(`Torrent ${hash} not found, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`); logger.warn(`Torrent ${hash} not found, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delayMs)); await new Promise(resolve => setTimeout(resolve, delayMs));
} }
@@ -55,7 +55,7 @@ async function getTorrentWithRetry(
export async function processMonitorDownload(payload: MonitorDownloadPayload): Promise<any> { export async function processMonitorDownload(payload: MonitorDownloadPayload): Promise<any> {
const { requestId, downloadHistoryId, downloadClientId, downloadClient, jobId } = payload; const { requestId, downloadHistoryId, downloadClientId, downloadClient, jobId } = payload;
const logger = jobId ? createJobLogger(jobId, 'MonitorDownload') : null; const logger = RMABLogger.forJob(jobId, 'MonitorDownload');
try { try {
let progress: any; let progress: any;
@@ -96,7 +96,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P
// Store download path if available (only set after completion) // Store download path if available (only set after completion)
downloadPath = nzbInfo.downloadPath; downloadPath = nzbInfo.downloadPath;
await logger?.info(`SABnzbd status: ${nzbInfo.status}`, { logger.info(`SABnzbd status: ${nzbInfo.status}`, {
progress: `${(nzbInfo.progress * 100).toFixed(1)}%`, progress: `${(nzbInfo.progress * 100).toFixed(1)}%`,
speed: `${(nzbInfo.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s`, speed: `${(nzbInfo.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s`,
}); });
@@ -123,7 +123,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P
// Check download state // Check download state
if (progress.state === 'completed') { if (progress.state === 'completed') {
await logger?.info(`Download completed for request ${requestId}`); logger.info(`Download completed for request ${requestId}`);
// Ensure we have a download path // Ensure we have a download path
if (!downloadPath) { if (!downloadPath) {
@@ -145,7 +145,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P
localPath: pathMappingConfig.download_client_local_path || '', localPath: pathMappingConfig.download_client_local_path || '',
}); });
await logger?.info(`Download completed`, { logger.info(`Download completed`, {
downloadClient, downloadClient,
downloadPath, downloadPath,
organizePath: organizePath !== downloadPath ? `${organizePath} (mapped)` : organizePath, organizePath: organizePath !== downloadPath ? `${organizePath} (mapped)` : organizePath,
@@ -183,7 +183,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P
organizePath organizePath
); );
await logger?.info(`Triggered organize_files job for request ${requestId}`); logger.info(`Triggered organize_files job for request ${requestId}`);
return { return {
success: true, success: true,
@@ -194,7 +194,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P
downloadPath: organizePath, downloadPath: organizePath,
}; };
} else if (progress.state === 'failed') { } else if (progress.state === 'failed') {
await logger?.error(`Download failed for request ${requestId}`); logger.error(`Download failed for request ${requestId}`);
// Update request to failed // Update request to failed
await prisma.request.update({ await prisma.request.update({
@@ -236,7 +236,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P
// Only log every 5% progress to reduce log spam // Only log every 5% progress to reduce log spam
const shouldLog = progress.percent % 5 === 0 || progress.percent < 5; const shouldLog = progress.percent % 5 === 0 || progress.percent < 5;
if (shouldLog) { if (shouldLog) {
await logger?.info(`Request ${requestId}: ${progress.percent}% complete (${progress.state})`, { logger.info(`Request ${requestId}: ${progress.percent}% complete (${progress.state})`, {
speed: progress.speed, speed: progress.speed,
eta: progress.eta, eta: progress.eta,
}); });
@@ -254,7 +254,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P
}; };
} }
} catch (error) { } catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
// Check if this is a transient "torrent not found" error // Check if this is a transient "torrent not found" error
const errorMessage = error instanceof Error ? error.message : ''; const errorMessage = error instanceof Error ? error.message : '';
@@ -263,7 +263,7 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P
if (isTorrentNotFound) { if (isTorrentNotFound) {
// Transient error - don't mark request as failed, let Bull retry // Transient error - don't mark request as failed, let Bull retry
// The request stays in 'downloading' status until Bull exhausts all retries // The request stays in 'downloading' status until Bull exhausts all retries
await logger?.warn(`Transient error for request ${requestId}, allowing Bull to retry`); logger.warn(`Transient error for request ${requestId}, allowing Bull to retry`);
} else { } else {
// Permanent error - mark request as failed immediately // Permanent error - mark request as failed immediately
await prisma.request.update({ await prisma.request.update({
@@ -6,7 +6,7 @@
*/ */
import { prisma } from '../db'; import { prisma } from '../db';
import { createJobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
import { getJobQueueService } from '../services/job-queue.service'; import { getJobQueueService } from '../services/job-queue.service';
export interface MonitorRssFeedsPayload { export interface MonitorRssFeedsPayload {
@@ -16,9 +16,9 @@ export interface MonitorRssFeedsPayload {
export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): Promise<any> { export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): Promise<any> {
const { jobId, scheduledJobId } = payload; const { jobId, scheduledJobId } = payload;
const logger = jobId ? createJobLogger(jobId, 'MonitorRssFeeds') : null; const logger = RMABLogger.forJob(jobId, 'MonitorRssFeeds');
await logger?.info(`Starting RSS feed monitoring...`); logger.info(`Starting RSS feed monitoring...`);
// Get indexer configuration // Get indexer configuration
const { getConfigService } = await import('../services/config.service'); const { getConfigService } = await import('../services/config.service');
@@ -26,7 +26,7 @@ export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): P
const indexersConfigStr = await configService.get('prowlarr_indexers'); const indexersConfigStr = await configService.get('prowlarr_indexers');
if (!indexersConfigStr) { if (!indexersConfigStr) {
await logger?.warn(`No indexers configured, skipping`); logger.warn(`No indexers configured, skipping`);
return { success: false, message: 'No indexers configured', skipped: true }; return { success: false, message: 'No indexers configured', skipped: true };
} }
@@ -38,11 +38,11 @@ export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): P
); );
if (rssEnabledIndexers.length === 0) { if (rssEnabledIndexers.length === 0) {
await logger?.warn(`No indexers with RSS enabled, skipping`); logger.warn(`No indexers with RSS enabled, skipping`);
return { success: false, message: 'No RSS-enabled indexers', skipped: true }; return { success: false, message: 'No RSS-enabled indexers', skipped: true };
} }
await logger?.info(`Monitoring ${rssEnabledIndexers.length} RSS-enabled indexers`); logger.info(`Monitoring ${rssEnabledIndexers.length} RSS-enabled indexers`);
// Get RSS feeds from all enabled indexers // Get RSS feeds from all enabled indexers
const { getProwlarrService } = await import('../integrations/prowlarr.service'); const { getProwlarrService } = await import('../integrations/prowlarr.service');
@@ -51,7 +51,7 @@ export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): P
const indexerIds = rssEnabledIndexers.map((i: any) => i.id); const indexerIds = rssEnabledIndexers.map((i: any) => i.id);
const rssResults = await prowlarrService.getAllRssFeeds(indexerIds); const rssResults = await prowlarrService.getAllRssFeeds(indexerIds);
await logger?.info(`Retrieved ${rssResults.length} items from RSS feeds`); logger.info(`Retrieved ${rssResults.length} items from RSS feeds`);
if (rssResults.length === 0) { if (rssResults.length === 0) {
return { success: true, message: 'No RSS results', matched: 0 }; return { success: true, message: 'No RSS results', matched: 0 };
@@ -67,7 +67,7 @@ export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): P
take: 100, take: 100,
}); });
await logger?.info(`Found ${missingRequests.length} requests awaiting search`); logger.info(`Found ${missingRequests.length} requests awaiting search`);
if (missingRequests.length === 0) { if (missingRequests.length === 0) {
return { success: true, message: 'No missing requests', matched: 0 }; return { success: true, message: 'No missing requests', matched: 0 };
@@ -92,7 +92,7 @@ export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): P
const titleMatchCount = titleWords.filter(word => word.length > 2 && torrentTitle.includes(word)).length; const titleMatchCount = titleWords.filter(word => word.length > 2 && torrentTitle.includes(word)).length;
if (hasAuthor && titleMatchCount >= 2) { if (hasAuthor && titleMatchCount >= 2) {
await logger?.info(`Match found! "${audiobook.title}" by ${audiobook.author} matches torrent: ${torrent.title}`); logger.info(`Match found! "${audiobook.title}" by ${audiobook.author} matches torrent: ${torrent.title}`);
// Trigger search job to process this request // Trigger search job to process this request
try { try {
@@ -102,9 +102,9 @@ export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): P
author: audiobook.author, author: audiobook.author,
}); });
matched++; matched++;
await logger?.info(`Triggered search job for request ${request.id}`); logger.info(`Triggered search job for request ${request.id}`);
} catch (error) { } catch (error) {
await logger?.error(`Failed to trigger search for request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Failed to trigger search for request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
// Only trigger once per request // Only trigger once per request
@@ -113,7 +113,7 @@ export async function processMonitorRssFeeds(payload: MonitorRssFeedsPayload): P
} }
} }
await logger?.info(`RSS monitoring complete: ${matched} matches found and queued for processing`); logger.info(`RSS monitoring complete: ${matched} matches found and queued for processing`);
return { return {
success: true, success: true,
+13 -14
View File
@@ -6,7 +6,7 @@
import { OrganizeFilesPayload, getJobQueueService } from '../services/job-queue.service'; import { OrganizeFilesPayload, getJobQueueService } from '../services/job-queue.service';
import { prisma } from '../db'; import { prisma } from '../db';
import { getFileOrganizer } from '../utils/file-organizer'; import { getFileOrganizer } from '../utils/file-organizer';
import { createJobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
import { getLibraryService } from '../services/library'; import { getLibraryService } from '../services/library';
import { getConfigService } from '../services/config.service'; import { getConfigService } from '../services/config.service';
@@ -17,11 +17,10 @@ import { getConfigService } from '../services/config.service';
export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promise<any> { export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promise<any> {
const { requestId, audiobookId, downloadPath, jobId } = payload; const { requestId, audiobookId, downloadPath, jobId } = payload;
// Create logger (fallback to console-only if jobId not provided) const logger = RMABLogger.forJob(jobId, 'OrganizeFiles');
const logger = jobId ? createJobLogger(jobId, 'OrganizeFiles') : null;
await logger?.info(`Processing request ${requestId}`); logger.info(`Processing request ${requestId}`);
await logger?.info(`Download path: ${downloadPath}`); logger.info(`Download path: ${downloadPath}`);
try { try {
// Update request status to processing // Update request status to processing
@@ -43,7 +42,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
throw new Error(`Audiobook ${audiobookId} not found`); throw new Error(`Audiobook ${audiobookId} not found`);
} }
await logger?.info(`Organizing: ${audiobook.title} by ${audiobook.author}`); logger.info(`Organizing: ${audiobook.title} by ${audiobook.author}`);
// Get file organizer (reads media_dir from database config) // Get file organizer (reads media_dir from database config)
const organizer = await getFileOrganizer(); const organizer = await getFileOrganizer();
@@ -65,7 +64,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
throw new Error(`File organization failed: ${result.errors.join(', ')}`); throw new Error(`File organization failed: ${result.errors.join(', ')}`);
} }
await logger?.info(`Successfully moved ${result.filesMovedCount} files to ${result.targetPath}`); logger.info(`Successfully moved ${result.filesMovedCount} files to ${result.targetPath}`);
// Update audiobook record with file path and status // Update audiobook record with file path and status
await prisma.audiobook.update({ await prisma.audiobook.update({
@@ -89,7 +88,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
}, },
}); });
await logger?.info(`Request ${requestId} completed successfully - status: downloaded`, { logger.info(`Request ${requestId} completed successfully - status: downloaded`, {
success: true, success: true,
message: 'Files organized successfully', message: 'Files organized successfully',
requestId, requestId,
@@ -128,13 +127,13 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
// Trigger scan (implementation is backend-specific) // Trigger scan (implementation is backend-specific)
await libraryService.triggerLibraryScan(libraryId); await libraryService.triggerLibraryScan(libraryId);
await logger?.info( logger.info(
`Triggered ${backendMode} filesystem scan for library ${libraryId}` `Triggered ${backendMode} filesystem scan for library ${libraryId}`
); );
} catch (error) { } catch (error) {
// Log error but don't fail the job // Log error but don't fail the job
await logger?.error( logger.error(
`Failed to trigger filesystem scan: ${error instanceof Error ? error.message : 'Unknown error'}`, `Failed to trigger filesystem scan: ${error instanceof Error ? error.message : 'Unknown error'}`,
{ {
error: error instanceof Error ? error.stack : undefined, error: error instanceof Error ? error.stack : undefined,
@@ -144,7 +143,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
// Continue - scheduled scans will eventually detect the book // Continue - scheduled scans will eventually detect the book
} }
} else { } else {
await logger?.info( logger.info(
`${backendMode} filesystem scan trigger disabled (relying on filesystem watcher)` `${backendMode} filesystem scan trigger disabled (relying on filesystem watcher)`
); );
} }
@@ -161,7 +160,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
errors: result.errors, errors: result.errors,
}; };
} catch (error) { } catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
const errorMessage = error instanceof Error ? error.message : 'File organization failed'; const errorMessage = error instanceof Error ? error.message : 'File organization failed';
@@ -191,7 +190,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
if (newAttempts < currentRequest.maxImportRetries) { if (newAttempts < currentRequest.maxImportRetries) {
// Still have retries left - queue for re-import // Still have retries left - queue for re-import
await logger?.warn(`Retryable error for request ${requestId}, queueing for retry (attempt ${newAttempts}/${currentRequest.maxImportRetries})`); logger.warn(`Retryable error for request ${requestId}, queueing for retry (attempt ${newAttempts}/${currentRequest.maxImportRetries})`);
await prisma.request.update({ await prisma.request.update({
where: { id: requestId }, where: { id: requestId },
@@ -213,7 +212,7 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
}; };
} else { } else {
// Max retries exceeded - move to warn status // Max retries exceeded - move to warn status
await logger?.warn(`Max retries (${currentRequest.maxImportRetries}) exceeded for request ${requestId}, moving to warn status`); logger.warn(`Max retries (${currentRequest.maxImportRetries}) exceeded for request ${requestId}, moving to warn status`);
await prisma.request.update({ await prisma.request.update({
where: { id: requestId }, where: { id: requestId },
@@ -6,7 +6,7 @@
*/ */
import { prisma } from '../db'; import { prisma } from '../db';
import { createJobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
import { getLibraryService } from '../services/library'; import { getLibraryService } from '../services/library';
export interface PlexRecentlyAddedPayload { export interface PlexRecentlyAddedPayload {
@@ -16,14 +16,14 @@ export interface PlexRecentlyAddedPayload {
export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPayload): Promise<any> { export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPayload): Promise<any> {
const { jobId, scheduledJobId } = payload; const { jobId, scheduledJobId } = payload;
const logger = jobId ? createJobLogger(jobId, 'RecentlyAdded') : null; const logger = RMABLogger.forJob(jobId, 'RecentlyAdded');
const { getConfigService } = await import('../services/config.service'); const { getConfigService } = await import('../services/config.service');
const configService = getConfigService(); const configService = getConfigService();
// Get backend mode // Get backend mode
const backendMode = await configService.getBackendMode(); const backendMode = await configService.getBackendMode();
await logger?.info(`Backend mode: ${backendMode}`); logger.info(`Backend mode: ${backendMode}`);
// Validate configuration based on backend mode // Validate configuration based on backend mode
if (backendMode === 'audiobookshelf') { if (backendMode === 'audiobookshelf') {
@@ -40,7 +40,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa
if (missingFields.length > 0) { if (missingFields.length > 0) {
const errorMsg = `Audiobookshelf is not configured. Missing: ${missingFields.join(', ')}`; const errorMsg = `Audiobookshelf is not configured. Missing: ${missingFields.join(', ')}`;
await logger?.warn(errorMsg); logger.warn(errorMsg);
return { success: false, message: errorMsg, skipped: true }; return { success: false, message: errorMsg, skipped: true };
} }
} else { } else {
@@ -57,12 +57,12 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa
if (missingFields.length > 0) { if (missingFields.length > 0) {
const errorMsg = `Plex is not configured. Missing: ${missingFields.join(', ')}`; const errorMsg = `Plex is not configured. Missing: ${missingFields.join(', ')}`;
await logger?.warn(errorMsg); logger.warn(errorMsg);
return { success: false, message: errorMsg, skipped: true }; return { success: false, message: errorMsg, skipped: true };
} }
} }
await logger?.info(`Starting recently added check...`); logger.info(`Starting recently added check...`);
// Get library service (automatically selects Plex or Audiobookshelf) // Get library service (automatically selects Plex or Audiobookshelf)
const libraryService = await getLibraryService(); const libraryService = await getLibraryService();
@@ -76,7 +76,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa
// Fetch top 10 recently added items using abstraction layer // Fetch top 10 recently added items using abstraction layer
const recentItems = await libraryService.getRecentlyAdded(libraryId!, 10); const recentItems = await libraryService.getRecentlyAdded(libraryId!, 10);
await logger?.info(`Found ${recentItems.length} recently added items`); logger.info(`Found ${recentItems.length} recently added items`);
if (recentItems.length === 0) { if (recentItems.length === 0) {
return { success: true, message: 'No recent items', newCount: 0, updatedCount: 0, matchedDownloads: 0 }; return { success: true, message: 'No recent items', newCount: 0, updatedCount: 0, matchedDownloads: 0 };
@@ -112,7 +112,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa
}, },
}); });
newCount++; newCount++;
await logger?.info(`New item added: ${item.title} by ${item.author}`); logger.info(`New item added: ${item.title} by ${item.author}`);
} else { } else {
await prisma.plexLibrary.update({ await prisma.plexLibrary.update({
where: { plexGuid: item.externalId }, where: { plexGuid: item.externalId },
@@ -144,7 +144,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa
}); });
if (downloadedRequests.length > 0) { if (downloadedRequests.length > 0) {
await logger?.info(`Checking ${downloadedRequests.length} downloaded requests for matches`); logger.info(`Checking ${downloadedRequests.length} downloaded requests for matches`);
const { findPlexMatch } = await import('../utils/audiobook-matcher'); const { findPlexMatch } = await import('../utils/audiobook-matcher');
@@ -159,7 +159,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa
}); });
if (match) { if (match) {
await logger?.info(`Match found: "${audiobook.title}" → "${match.title}"`); logger.info(`Match found: "${audiobook.title}" → "${match.title}"`);
// Update audiobook with matched library item ID // Update audiobook with matched library item ID
const updateData: any = { updatedAt: new Date() }; const updateData: any = { updatedAt: new Date() };
@@ -187,18 +187,18 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa
const itemId = match.plexGuid; // plexGuid contains the Audiobookshelf item ID const itemId = match.plexGuid; // plexGuid contains the Audiobookshelf item ID
const asin = audiobook.audibleAsin || undefined; const asin = audiobook.audibleAsin || undefined;
const matchInfo = asin ? ` with ASIN ${asin}` : ''; const matchInfo = asin ? ` with ASIN ${asin}` : '';
await logger?.info(`Triggering metadata match for matched item: ${itemId}${matchInfo}`); logger.info(`Triggering metadata match for matched item: ${itemId}${matchInfo}`);
const { triggerABSItemMatch } = await import('../services/audiobookshelf/api'); const { triggerABSItemMatch } = await import('../services/audiobookshelf/api');
await triggerABSItemMatch(itemId, asin); await triggerABSItemMatch(itemId, asin);
} }
} }
} catch (error) { } catch (error) {
await logger?.error(`Failed to match request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Failed to match request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
} }
} }
await logger?.info(`Complete: ${newCount} new, ${updatedCount} updated, ${matchedDownloads} matched downloads`); logger.info(`Complete: ${newCount} new, ${updatedCount} updated, ${matchedDownloads} matched downloads`);
return { return {
success: true, success: true,
@@ -209,7 +209,7 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa
matchedDownloads, matchedDownloads,
}; };
} catch (error) { } catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
throw error; throw error;
} }
} }
@@ -6,7 +6,7 @@
*/ */
import { prisma } from '../db'; import { prisma } from '../db';
import { createJobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
import { getJobQueueService } from '../services/job-queue.service'; import { getJobQueueService } from '../services/job-queue.service';
import { getConfigService } from '../services/config.service'; import { getConfigService } from '../services/config.service';
import { PathMapper } from '../utils/path-mapper'; import { PathMapper } from '../utils/path-mapper';
@@ -18,9 +18,9 @@ export interface RetryFailedImportsPayload {
export async function processRetryFailedImports(payload: RetryFailedImportsPayload): Promise<any> { export async function processRetryFailedImports(payload: RetryFailedImportsPayload): Promise<any> {
const { jobId, scheduledJobId } = payload; const { jobId, scheduledJobId } = payload;
const logger = jobId ? createJobLogger(jobId, 'RetryFailedImports') : null; const logger = RMABLogger.forJob(jobId, 'RetryFailedImports');
await logger?.info('Starting retry job for requests awaiting import...'); logger.info('Starting retry job for requests awaiting import...');
try { try {
// Load path mapping configuration once // Load path mapping configuration once
@@ -54,7 +54,7 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo
take: 50, // Limit to 50 requests per run take: 50, // Limit to 50 requests per run
}); });
await logger?.info(`Found ${requests.length} requests awaiting import`); logger.info(`Found ${requests.length} requests awaiting import`);
if (requests.length === 0) { if (requests.length === 0) {
return { return {
@@ -75,7 +75,7 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo
const downloadHistory = request.downloadHistory[0]; const downloadHistory = request.downloadHistory[0];
if (!downloadHistory) { if (!downloadHistory) {
await logger?.warn(`No download history found for request ${request.id}, skipping`); logger.warn(`No download history found for request ${request.id}, skipping`);
skipped++; skipped++;
continue; continue;
} }
@@ -91,16 +91,16 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo
const torrent = await qbt.getTorrent(downloadHistory.torrentHash); const torrent = await qbt.getTorrent(downloadHistory.torrentHash);
const qbPath = `${torrent.save_path}/${torrent.name}`; const qbPath = `${torrent.save_path}/${torrent.name}`;
downloadPath = PathMapper.transform(qbPath, mappingConfig); downloadPath = PathMapper.transform(qbPath, mappingConfig);
await logger?.info( logger.info(
`Got download path from qBittorrent for request ${request.id}: ${qbPath}` + `Got download path from qBittorrent for request ${request.id}: ${qbPath}` +
(downloadPath !== qbPath ? `${downloadPath} (mapped)` : '') (downloadPath !== qbPath ? `${downloadPath} (mapped)` : '')
); );
} catch (qbtError) { } catch (qbtError) {
// Torrent not found in qBittorrent - try to construct path from config // Torrent not found in qBittorrent - try to construct path from config
await logger?.warn(`Torrent not found in qBittorrent for request ${request.id}, falling back to configured path`); logger.warn(`Torrent not found in qBittorrent for request ${request.id}, falling back to configured path`);
if (!downloadHistory.torrentName) { if (!downloadHistory.torrentName) {
await logger?.warn(`No torrent name stored for request ${request.id}, cannot construct fallback path, skipping`); logger.warn(`No torrent name stored for request ${request.id}, cannot construct fallback path, skipping`);
skipped++; skipped++;
continue; continue;
} }
@@ -108,14 +108,14 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo
const downloadDir = await configService.get('download_dir'); const downloadDir = await configService.get('download_dir');
if (!downloadDir) { if (!downloadDir) {
await logger?.error(`download_dir not configured, cannot retry request ${request.id}, skipping`); logger.error(`download_dir not configured, cannot retry request ${request.id}, skipping`);
skipped++; skipped++;
continue; continue;
} }
const fallbackPath = `${downloadDir}/${downloadHistory.torrentName}`; const fallbackPath = `${downloadDir}/${downloadHistory.torrentName}`;
downloadPath = PathMapper.transform(fallbackPath, mappingConfig); downloadPath = PathMapper.transform(fallbackPath, mappingConfig);
await logger?.info( logger.info(
`Using fallback download path for request ${request.id}: ${fallbackPath}` + `Using fallback download path for request ${request.id}: ${fallbackPath}` +
(downloadPath !== fallbackPath ? `${downloadPath} (mapped)` : '') (downloadPath !== fallbackPath ? `${downloadPath} (mapped)` : '')
); );
@@ -128,15 +128,15 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo
const nzbInfo = await sabnzbd.getNZB(downloadHistory.nzbId); const nzbInfo = await sabnzbd.getNZB(downloadHistory.nzbId);
if (nzbInfo && nzbInfo.downloadPath) { if (nzbInfo && nzbInfo.downloadPath) {
downloadPath = PathMapper.transform(nzbInfo.downloadPath, mappingConfig); downloadPath = PathMapper.transform(nzbInfo.downloadPath, mappingConfig);
await logger?.info( logger.info(
`Got download path from SABnzbd for request ${request.id}: ${nzbInfo.downloadPath}` + `Got download path from SABnzbd for request ${request.id}: ${nzbInfo.downloadPath}` +
(downloadPath !== nzbInfo.downloadPath ? `${downloadPath} (mapped)` : '') (downloadPath !== nzbInfo.downloadPath ? `${downloadPath} (mapped)` : '')
); );
} else { } else {
await logger?.warn(`NZB ${downloadHistory.nzbId} not found or has no download path for request ${request.id}, falling back to configured path`); logger.warn(`NZB ${downloadHistory.nzbId} not found or has no download path for request ${request.id}, falling back to configured path`);
if (!downloadHistory.torrentName) { if (!downloadHistory.torrentName) {
await logger?.warn(`No name stored for request ${request.id}, cannot construct fallback path, skipping`); logger.warn(`No name stored for request ${request.id}, cannot construct fallback path, skipping`);
skipped++; skipped++;
continue; continue;
} }
@@ -144,27 +144,27 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo
const downloadDir = await configService.get('download_dir'); const downloadDir = await configService.get('download_dir');
if (!downloadDir) { if (!downloadDir) {
await logger?.error(`download_dir not configured, cannot retry request ${request.id}, skipping`); logger.error(`download_dir not configured, cannot retry request ${request.id}, skipping`);
skipped++; skipped++;
continue; continue;
} }
const fallbackPath = `${downloadDir}/${downloadHistory.torrentName}`; const fallbackPath = `${downloadDir}/${downloadHistory.torrentName}`;
downloadPath = PathMapper.transform(fallbackPath, mappingConfig); downloadPath = PathMapper.transform(fallbackPath, mappingConfig);
await logger?.info( logger.info(
`Using fallback download path for request ${request.id}: ${fallbackPath}` + `Using fallback download path for request ${request.id}: ${fallbackPath}` +
(downloadPath !== fallbackPath ? `${downloadPath} (mapped)` : '') (downloadPath !== fallbackPath ? `${downloadPath} (mapped)` : '')
); );
} }
} catch (sabnzbdError) { } catch (sabnzbdError) {
await logger?.warn(`SABnzbd error for request ${request.id}: ${sabnzbdError instanceof Error ? sabnzbdError.message : 'Unknown error'}, skipping`); logger.warn(`SABnzbd error for request ${request.id}: ${sabnzbdError instanceof Error ? sabnzbdError.message : 'Unknown error'}, skipping`);
skipped++; skipped++;
continue; continue;
} }
} else { } else {
// No download client ID - use fallback path // No download client ID - use fallback path
if (!downloadHistory.torrentName) { if (!downloadHistory.torrentName) {
await logger?.warn(`No download client ID or name for request ${request.id}, skipping`); logger.warn(`No download client ID or name for request ${request.id}, skipping`);
skipped++; skipped++;
continue; continue;
} }
@@ -172,14 +172,14 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo
const downloadDir = await configService.get('download_dir'); const downloadDir = await configService.get('download_dir');
if (!downloadDir) { if (!downloadDir) {
await logger?.error(`download_dir not configured, cannot retry request ${request.id}, skipping`); logger.error(`download_dir not configured, cannot retry request ${request.id}, skipping`);
skipped++; skipped++;
continue; continue;
} }
const configuredPath = `${downloadDir}/${downloadHistory.torrentName}`; const configuredPath = `${downloadDir}/${downloadHistory.torrentName}`;
downloadPath = PathMapper.transform(configuredPath, mappingConfig); downloadPath = PathMapper.transform(configuredPath, mappingConfig);
await logger?.info( logger.info(
`Using configured download path for request ${request.id}: ${configuredPath}` + `Using configured download path for request ${request.id}: ${configuredPath}` +
(downloadPath !== configuredPath ? `${downloadPath} (mapped)` : '') (downloadPath !== configuredPath ? `${downloadPath} (mapped)` : '')
); );
@@ -191,14 +191,14 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo
downloadPath downloadPath
); );
triggered++; triggered++;
await logger?.info(`Triggered organize job for request ${request.id}: ${request.audiobook.title}`); logger.info(`Triggered organize job for request ${request.id}: ${request.audiobook.title}`);
} catch (error) { } catch (error) {
await logger?.error(`Failed to trigger organize for request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Failed to trigger organize for request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
skipped++; skipped++;
} }
} }
await logger?.info(`Triggered ${triggered}/${requests.length} organize jobs (${skipped} skipped)`); logger.info(`Triggered ${triggered}/${requests.length} organize jobs (${skipped} skipped)`);
return { return {
success: true, success: true,
@@ -208,7 +208,7 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo
skipped, skipped,
}; };
} catch (error) { } catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
throw error; throw error;
} }
} }
@@ -6,7 +6,7 @@
*/ */
import { prisma } from '../db'; import { prisma } from '../db';
import { createJobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
import { getJobQueueService } from '../services/job-queue.service'; import { getJobQueueService } from '../services/job-queue.service';
export interface RetryMissingTorrentsPayload { export interface RetryMissingTorrentsPayload {
@@ -16,9 +16,9 @@ export interface RetryMissingTorrentsPayload {
export async function processRetryMissingTorrents(payload: RetryMissingTorrentsPayload): Promise<any> { export async function processRetryMissingTorrents(payload: RetryMissingTorrentsPayload): Promise<any> {
const { jobId, scheduledJobId } = payload; const { jobId, scheduledJobId } = payload;
const logger = jobId ? createJobLogger(jobId, 'RetryMissingTorrents') : null; const logger = RMABLogger.forJob(jobId, 'RetryMissingTorrents');
await logger?.info('Starting retry job for requests awaiting search...'); logger.info('Starting retry job for requests awaiting search...');
try { try {
// Find all active requests in awaiting_search status // Find all active requests in awaiting_search status
@@ -33,7 +33,7 @@ export async function processRetryMissingTorrents(payload: RetryMissingTorrentsP
take: 50, // Limit to 50 requests per run take: 50, // Limit to 50 requests per run
}); });
await logger?.info(`Found ${requests.length} requests awaiting search`); logger.info(`Found ${requests.length} requests awaiting search`);
if (requests.length === 0) { if (requests.length === 0) {
return { return {
@@ -55,13 +55,13 @@ export async function processRetryMissingTorrents(payload: RetryMissingTorrentsP
author: request.audiobook.author, author: request.audiobook.author,
}); });
triggered++; triggered++;
await logger?.info(`Triggered search for request ${request.id}: ${request.audiobook.title}`); logger.info(`Triggered search for request ${request.id}: ${request.audiobook.title}`);
} catch (error) { } catch (error) {
await logger?.error(`Failed to trigger search for request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Failed to trigger search for request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
} }
await logger?.info(`Triggered ${triggered}/${requests.length} search jobs`); logger.info(`Triggered ${triggered}/${requests.length} search jobs`);
return { return {
success: true, success: true,
@@ -70,7 +70,7 @@ export async function processRetryMissingTorrents(payload: RetryMissingTorrentsP
triggered, triggered,
}; };
} catch (error) { } catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
throw error; throw error;
} }
} }
+28 -28
View File
@@ -10,7 +10,7 @@ import { ScanPlexPayload } from '../services/job-queue.service';
import { prisma } from '../db'; import { prisma } from '../db';
import { getLibraryService } from '../services/library'; import { getLibraryService } from '../services/library';
import { getConfigService } from '../services/config.service'; import { getConfigService } from '../services/config.service';
import { createJobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
/** /**
* Process library scan job * Process library scan job
@@ -19,9 +19,9 @@ import { createJobLogger } from '../utils/job-logger';
export async function processScanPlex(payload: ScanPlexPayload): Promise<any> { export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
const { libraryId, partial, path, jobId } = payload; const { libraryId, partial, path, jobId } = payload;
const logger = jobId ? createJobLogger(jobId, 'ScanLibrary') : null; const logger = RMABLogger.forJob(jobId, 'ScanLibrary');
await logger?.info(`Scanning library ${libraryId || 'default'}${partial ? ' (partial)' : ''}`); logger.info(`Scanning library ${libraryId || 'default'}${partial ? ' (partial)' : ''}`);
try { try {
// 1. Get library service (automatically selects Plex or Audiobookshelf based on config) // 1. Get library service (automatically selects Plex or Audiobookshelf based on config)
@@ -29,7 +29,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
const configService = getConfigService(); const configService = getConfigService();
const backendMode = await configService.getBackendMode(); const backendMode = await configService.getBackendMode();
await logger?.info(`Backend mode: ${backendMode}`); logger.info(`Backend mode: ${backendMode}`);
// 2. Get configured library ID // 2. Get configured library ID
let targetLibraryId = libraryId; let targetLibraryId = libraryId;
@@ -50,12 +50,12 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
} }
} }
await logger?.info(`Fetching content from library ${targetLibraryId}`); logger.info(`Fetching content from library ${targetLibraryId}`);
// 3. Get all audiobooks from library using abstraction layer // 3. Get all audiobooks from library using abstraction layer
const libraryItems = await libraryService.getLibraryItems(targetLibraryId); const libraryItems = await libraryService.getLibraryItems(targetLibraryId);
await logger?.info(`Found ${libraryItems.length} items in library`); logger.info(`Found ${libraryItems.length} items in library`);
let newCount = 0; let newCount = 0;
let updatedCount = 0; let updatedCount = 0;
@@ -120,7 +120,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
}); });
newCount++; newCount++;
await logger?.info(`Added new: "${item.title}" by ${item.author}`); logger.info(`Added new: "${item.title}" by ${item.author}`);
results.push({ results.push({
id: newLibraryItem.id, id: newLibraryItem.id,
@@ -130,16 +130,16 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
}); });
} }
} catch (error) { } catch (error) {
await logger?.error(`Failed to process "${item.title}": ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Failed to process "${item.title}": ${error instanceof Error ? error.message : 'Unknown error'}`);
skippedCount++; skippedCount++;
} }
} }
await logger?.info(`Scan complete: ${libraryItems.length} items scanned, ${newCount} new, ${updatedCount} updated, ${skippedCount} skipped`); logger.info(`Scan complete: ${libraryItems.length} items scanned, ${newCount} new, ${updatedCount} updated, ${skippedCount} skipped`);
// 5. Remove stale records from plex_library (items no longer in the actual library) // 5. Remove stale records from plex_library (items no longer in the actual library)
// This ensures the database is a fresh snapshot of the library state // This ensures the database is a fresh snapshot of the library state
await logger?.info(`Checking for stale library records...`); logger.info(`Checking for stale library records...`);
const scannedPlexGuids = libraryItems const scannedPlexGuids = libraryItems
.filter(item => item.externalId) .filter(item => item.externalId)
@@ -163,7 +163,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
}); });
if (staleLibraryItems.length > 0) { if (staleLibraryItems.length > 0) {
await logger?.info(`Found ${staleLibraryItems.length} stale library records to remove`); logger.info(`Found ${staleLibraryItems.length} stale library records to remove`);
// For each stale library item, clean up references // For each stale library item, clean up references
for (const staleItem of staleLibraryItems) { for (const staleItem of staleLibraryItems) {
@@ -214,7 +214,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
} }
} }
await logger?.info(`Reset audiobook "${staleItem.title}" (no longer in library)`); logger.info(`Reset audiobook "${staleItem.title}" (no longer in library)`);
} }
// Delete the stale library record // Delete the stale library record
@@ -224,21 +224,21 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
staleRemovedCount++; staleRemovedCount++;
} catch (error) { } catch (error) {
await logger?.error(`Failed to remove stale library item "${staleItem.title}": ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Failed to remove stale library item "${staleItem.title}": ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
} }
await logger?.info(`Removed ${staleRemovedCount} stale records, reset ${audiobooksReset} audiobooks and ${requestsReset} requests`); logger.info(`Removed ${staleRemovedCount} stale records, reset ${audiobooksReset} audiobooks and ${requestsReset} requests`);
} else { } else {
await logger?.info(`No stale library records found`); logger.info(`No stale library records found`);
} }
} else { } else {
await logger?.warn(`Scan returned no items - skipping stale record cleanup to prevent data loss`); logger.warn(`Scan returned no items - skipping stale record cleanup to prevent data loss`);
} }
// 5b. Clean up orphaned audiobooks (audiobooks with plexGuid/absItemId that don't exist in plex_library) // 5b. Clean up orphaned audiobooks (audiobooks with plexGuid/absItemId that don't exist in plex_library)
// This handles cases where the library record was already deleted but audiobook record wasn't updated // This handles cases where the library record was already deleted but audiobook record wasn't updated
await logger?.info(`Checking for orphaned audiobooks...`); logger.info(`Checking for orphaned audiobooks...`);
const allPlexGuidsInLibrary = await prisma.plexLibrary.findMany({ const allPlexGuidsInLibrary = await prisma.plexLibrary.findMany({
select: { plexGuid: true }, select: { plexGuid: true },
@@ -277,7 +277,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
// This audiobook is orphaned - its library link points to nothing // This audiobook is orphaned - its library link points to nothing
try { try {
await logger?.info(`Found orphaned audiobook: "${audiobook.title}" (linked to non-existent library item)`); logger.info(`Found orphaned audiobook: "${audiobook.title}" (linked to non-existent library item)`);
// Clear library linkage // Clear library linkage
await prisma.audiobook.update({ await prisma.audiobook.update({
@@ -306,18 +306,18 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
} }
} }
} catch (error) { } catch (error) {
await logger?.error(`Failed to reset orphaned audiobook "${audiobook.title}": ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Failed to reset orphaned audiobook "${audiobook.title}": ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
} }
if (orphanedAudiobooksReset > 0) { if (orphanedAudiobooksReset > 0) {
await logger?.info(`Reset ${orphanedAudiobooksReset} orphaned audiobooks and ${orphanedRequestsReset} requests`); logger.info(`Reset ${orphanedAudiobooksReset} orphaned audiobooks and ${orphanedRequestsReset} requests`);
} else { } else {
await logger?.info(`No orphaned audiobooks found`); logger.info(`No orphaned audiobooks found`);
} }
// 6. Match downloaded requests against library // 6. Match downloaded requests against library
await logger?.info(`Checking for downloaded requests to match...`); logger.info(`Checking for downloaded requests to match...`);
const downloadedRequests = await prisma.request.findMany({ const downloadedRequests = await prisma.request.findMany({
where: { where: {
status: 'downloaded', status: 'downloaded',
@@ -327,7 +327,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
take: 50, // Limit to prevent overwhelming take: 50, // Limit to prevent overwhelming
}); });
await logger?.info(`Found ${downloadedRequests.length} downloaded requests to match`); logger.info(`Found ${downloadedRequests.length} downloaded requests to match`);
let matchedCount = 0; let matchedCount = 0;
const { findPlexMatch } = await import('../utils/audiobook-matcher'); const { findPlexMatch } = await import('../utils/audiobook-matcher');
@@ -346,7 +346,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
}); });
if (match) { if (match) {
await logger?.info(`Match found! "${audiobook.title}" -> "${match.title}"`); logger.info(`Match found! "${audiobook.title}" -> "${match.title}"`);
// Update audiobook with matched library item ID (plexGuid or abs_item_id) // Update audiobook with matched library item ID (plexGuid or abs_item_id)
const updateData: any = { updatedAt: new Date() }; const updateData: any = { updatedAt: new Date() };
@@ -379,17 +379,17 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
const itemId = match.plexGuid; // plexGuid contains the Audiobookshelf item ID const itemId = match.plexGuid; // plexGuid contains the Audiobookshelf item ID
const asin = audiobook.audibleAsin || undefined; const asin = audiobook.audibleAsin || undefined;
const matchInfo = asin ? ` with ASIN ${asin}` : ''; const matchInfo = asin ? ` with ASIN ${asin}` : '';
await logger?.info(`Triggering metadata match for matched item: ${itemId}${matchInfo}`); logger.info(`Triggering metadata match for matched item: ${itemId}${matchInfo}`);
const { triggerABSItemMatch } = await import('../services/audiobookshelf/api'); const { triggerABSItemMatch } = await import('../services/audiobookshelf/api');
await triggerABSItemMatch(itemId, asin); await triggerABSItemMatch(itemId, asin);
} }
} }
} catch (error) { } catch (error) {
await logger?.error(`Failed to match request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Failed to match request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
} }
await logger?.info(`Matched ${matchedCount}/${downloadedRequests.length} downloaded requests`, { logger.info(`Matched ${matchedCount}/${downloadedRequests.length} downloaded requests`, {
totalScanned: libraryItems.length, totalScanned: libraryItems.length,
newCount, newCount,
updatedCount, updatedCount,
@@ -420,7 +420,7 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
matchedDownloads: matchedCount, matchedDownloads: matchedCount,
}; };
} catch (error) { } catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
throw error; throw error;
} }
} }
+32 -32
View File
@@ -7,7 +7,7 @@ import { SearchIndexersPayload, getJobQueueService } from '../services/job-queue
import { prisma } from '../db'; import { prisma } from '../db';
import { getProwlarrService } from '../integrations/prowlarr.service'; import { getProwlarrService } from '../integrations/prowlarr.service';
import { getRankingAlgorithm } from '../utils/ranking-algorithm'; import { getRankingAlgorithm } from '../utils/ranking-algorithm';
import { createJobLogger } from '../utils/job-logger'; import { RMABLogger } from '../utils/logger';
/** /**
* Process search indexers job * Process search indexers job
@@ -16,9 +16,9 @@ import { createJobLogger } from '../utils/job-logger';
export async function processSearchIndexers(payload: SearchIndexersPayload): Promise<any> { export async function processSearchIndexers(payload: SearchIndexersPayload): Promise<any> {
const { requestId, audiobook, jobId } = payload; const { requestId, audiobook, jobId } = payload;
const logger = jobId ? createJobLogger(jobId, 'SearchIndexers') : null; const logger = RMABLogger.forJob(jobId, 'SearchIndexers');
await logger?.info(`Processing request ${requestId} for "${audiobook.title}"`); logger.info(`Processing request ${requestId} for "${audiobook.title}"`);
try { try {
// Update request status to searching // Update request status to searching
@@ -56,7 +56,7 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro
const flagConfigStr = await configService.get('indexer_flag_config'); const flagConfigStr = await configService.get('indexer_flag_config');
const flagConfigs = flagConfigStr ? JSON.parse(flagConfigStr) : []; const flagConfigs = flagConfigStr ? JSON.parse(flagConfigStr) : [];
await logger?.info(`Searching ${enabledIndexerIds.length} enabled indexers`); logger.info(`Searching ${enabledIndexerIds.length} enabled indexers`);
// Get Prowlarr service // Get Prowlarr service
const prowlarr = await getProwlarrService(); const prowlarr = await getProwlarrService();
@@ -64,7 +64,7 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro
// Build search query (title only - cast wide net, let ranking filter) // Build search query (title only - cast wide net, let ranking filter)
const searchQuery = audiobook.title; const searchQuery = audiobook.title;
await logger?.info(`Searching for: "${searchQuery}"`); logger.info(`Searching for: "${searchQuery}"`);
// Search indexers - ONLY enabled ones // Search indexers - ONLY enabled ones
const searchResults = await prowlarr.search(searchQuery, { const searchResults = await prowlarr.search(searchQuery, {
@@ -74,11 +74,11 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro
indexerIds: enabledIndexerIds, // Filter by enabled indexers indexerIds: enabledIndexerIds, // Filter by enabled indexers
}); });
await logger?.info(`Found ${searchResults.length} raw results`); logger.info(`Found ${searchResults.length} raw results`);
if (searchResults.length === 0) { if (searchResults.length === 0) {
// No results found - queue for re-search instead of failing // No results found - queue for re-search instead of failing
await logger?.warn(`No torrents found for request ${requestId}, marking as awaiting_search`); logger.warn(`No torrents found for request ${requestId}, marking as awaiting_search`);
await prisma.request.update({ await prisma.request.update({
where: { id: requestId }, where: { id: requestId },
@@ -117,14 +117,14 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro
result.score >= 50 && result.finalScore < 50 result.score >= 50 && result.finalScore < 50
).length; ).length;
await logger?.info(`Ranked ${rankedResults.length} results, ${filteredResults.length} above threshold (50/100 base + final)`); logger.info(`Ranked ${rankedResults.length} results, ${filteredResults.length} above threshold (50/100 base + final)`);
if (disqualifiedByNegativeBonus > 0) { if (disqualifiedByNegativeBonus > 0) {
await logger?.info(`${disqualifiedByNegativeBonus} torrents disqualified by negative flag bonuses`); logger.info(`${disqualifiedByNegativeBonus} torrents disqualified by negative flag bonuses`);
} }
if (filteredResults.length === 0) { if (filteredResults.length === 0) {
// No quality results found - queue for re-search instead of failing // No quality results found - queue for re-search instead of failing
await logger?.warn(`No quality matches found for request ${requestId} (all below 50/100), marking as awaiting_search`); logger.warn(`No quality matches found for request ${requestId} (all below 50/100), marking as awaiting_search`);
await prisma.request.update({ await prisma.request.update({
where: { id: requestId }, where: { id: requestId },
@@ -148,38 +148,38 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro
// Log top 3 results with detailed breakdown // Log top 3 results with detailed breakdown
const top3 = filteredResults.slice(0, 3); const top3 = filteredResults.slice(0, 3);
await logger?.info(`==================== RANKING DEBUG ====================`); logger.info(`==================== RANKING DEBUG ====================`);
await logger?.info(`Requested Title: "${audiobook.title}"`); logger.info(`Requested Title: "${audiobook.title}"`);
await logger?.info(`Requested Author: "${audiobook.author}"`); logger.info(`Requested Author: "${audiobook.author}"`);
await logger?.info(`Top ${top3.length} results (out of ${filteredResults.length} above threshold):`); logger.info(`Top ${top3.length} results (out of ${filteredResults.length} above threshold):`);
await logger?.info(`--------------------------------------------------------`); logger.info(`--------------------------------------------------------`);
for (let i = 0; i < top3.length; i++) { for (let i = 0; i < top3.length; i++) {
const result = top3[i]; const result = top3[i];
await logger?.info(`${i + 1}. "${result.title}"`); logger.info(`${i + 1}. "${result.title}"`);
await logger?.info(` Indexer: ${result.indexer}${result.indexerId ? ` (ID: ${result.indexerId})` : ''}`); logger.info(` Indexer: ${result.indexer}${result.indexerId ? ` (ID: ${result.indexerId})` : ''}`);
await logger?.info(``); logger.info(``);
await logger?.info(` Base Score: ${result.score.toFixed(1)}/100`); logger.info(` Base Score: ${result.score.toFixed(1)}/100`);
await logger?.info(` - Title/Author Match: ${result.breakdown.matchScore.toFixed(1)}/60`); logger.info(` - Title/Author Match: ${result.breakdown.matchScore.toFixed(1)}/60`);
await logger?.info(` - Format Quality: ${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`); logger.info(` - Format Quality: ${result.breakdown.formatScore.toFixed(1)}/25 (${result.format || 'unknown'})`);
await logger?.info(` - Seeder Count: ${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders !== undefined ? result.seeders + ' seeders' : 'N/A for Usenet'})`); logger.info(` - Seeder Count: ${result.breakdown.seederScore.toFixed(1)}/15 (${result.seeders !== undefined ? result.seeders + ' seeders' : 'N/A for Usenet'})`);
await logger?.info(``); logger.info(``);
await logger?.info(` Bonus Points: +${result.bonusPoints.toFixed(1)}`); logger.info(` Bonus Points: +${result.bonusPoints.toFixed(1)}`);
if (result.bonusModifiers.length > 0) { if (result.bonusModifiers.length > 0) {
for (const mod of result.bonusModifiers) { for (const mod of result.bonusModifiers) {
await logger?.info(` - ${mod.reason}: +${mod.points.toFixed(1)}`); logger.info(` - ${mod.reason}: +${mod.points.toFixed(1)}`);
} }
} }
await logger?.info(``); logger.info(``);
await logger?.info(` Final Score: ${result.finalScore.toFixed(1)}`); logger.info(` Final Score: ${result.finalScore.toFixed(1)}`);
if (result.breakdown.notes.length > 0) { if (result.breakdown.notes.length > 0) {
await logger?.info(` Notes: ${result.breakdown.notes.join(', ')}`); logger.info(` Notes: ${result.breakdown.notes.join(', ')}`);
} }
if (i < top3.length - 1) { if (i < top3.length - 1) {
await logger?.info(`--------------------------------------------------------`); logger.info(`--------------------------------------------------------`);
} }
} }
await logger?.info(`========================================================`); logger.info(`========================================================`);
await logger?.info(`Selected best result: ${bestResult.title} (final score: ${bestResult.finalScore.toFixed(1)})`); logger.info(`Selected best result: ${bestResult.title} (final score: ${bestResult.finalScore.toFixed(1)})`);
// Trigger download job with best result // Trigger download job with best result
const jobQueue = getJobQueueService(); const jobQueue = getJobQueueService();
@@ -202,7 +202,7 @@ export async function processSearchIndexers(payload: SearchIndexersPayload): Pro
}, },
}; };
} catch (error) { } catch (error) {
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); logger.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
await prisma.request.update({ await prisma.request.update({
where: { id: requestId }, where: { id: requestId },
+4 -1
View File
@@ -4,6 +4,9 @@
*/ */
import { getConfigService } from '../config.service'; import { getConfigService } from '../config.service';
import { RMABLogger } from '@/lib/utils/logger';
const logger = RMABLogger.create('Audiobookshelf');
interface ABSRequestOptions { interface ABSRequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
@@ -146,6 +149,6 @@ export async function triggerABSItemMatch(itemId: string, asin?: string) {
}); });
} catch (error) { } catch (error) {
// Don't throw - matching is best-effort, scan should continue even if match fails // Don't throw - matching is best-effort, scan should continue even if match fails
console.error(`[ABS] Failed to trigger match for item ${itemId}:`, error instanceof Error ? error.message : error); logger.error(`Failed to trigger match for item ${itemId}`, { error: error instanceof Error ? error.message : String(error) });
} }
} }

Some files were not shown because too many files have changed in this diff Show More